import 'nx/src/internal-testing-utils/mock-project-graph'; import { readJson, readProjectConfiguration, Tree, updateJson, writeJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { nxVersion } from '../../utils/versions'; import libraryGenerator from './library'; import { Schema } from './schema'; describe('library', () => { let tree: Tree; let defaultSchema: Schema = { directory: 'my-lib', linter: 'eslint', skipFormat: false, skipTsConfig: false, unitTestRunner: 'vitest', component: true, strict: true, }; beforeEach(() => { tree = createTreeWithEmptyWorkspace(); updateJson(tree, '/package.json', (json) => { json.devDependencies = { '@nx/cypress': nxVersion, '@nx/rollup': nxVersion, '@nx/vite': nxVersion, }; return json; }); }); it('should add vite types to tsconfigs and generate correct vite.config.ts file', async () => { await libraryGenerator(tree, { ...defaultSchema, bundler: 'vite', unitTestRunner: 'vitest', }); const tsconfigApp = readJson(tree, 'my-lib/tsconfig.lib.json'); expect(tsconfigApp.compilerOptions.types).toEqual(['vite/client']); const tsconfigSpec = readJson(tree, 'my-lib/tsconfig.spec.json'); expect(tsconfigSpec.compilerOptions.types).toEqual([ 'vitest/globals', 'vitest/importMeta', 'vite/client', 'node', 'vitest', ]); expect(tree.read('my-lib/vite.config.ts', 'utf-8')).toMatchSnapshot(); }); it('should update tags', async () => { await libraryGenerator(tree, { ...defaultSchema, tags: 'one,two' }); const project = readProjectConfiguration(tree, 'my-lib'); expect(project).toEqual( expect.objectContaining({ tags: ['one', 'two'], }) ); }); it('should add vue, vite and vitest to package.json', async () => { await libraryGenerator(tree, defaultSchema); expect(readJson(tree, '/package.json')).toMatchSnapshot(); expect(tree.read('my-lib/tsconfig.lib.json', 'utf-8')).toMatchSnapshot(); }); it('should update root tsconfig.base.json', async () => { await libraryGenerator(tree, defaultSchema); const tsconfigJson = readJson(tree, '/tsconfig.base.json'); expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([ 'my-lib/src/index.ts', ]); }); it('should create tsconfig.base.json out of tsconfig.json', async () => { tree.rename('tsconfig.base.json', 'tsconfig.json'); await libraryGenerator(tree, defaultSchema); expect(tree.exists('tsconfig.base.json')).toEqual(true); const tsconfigJson = readJson(tree, 'tsconfig.base.json'); expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([ 'my-lib/src/index.ts', ]); }); it('should update root tsconfig.base.json (no existing path mappings)', async () => { updateJson(tree, 'tsconfig.base.json', (json) => { json.compilerOptions.paths = undefined; return json; }); await libraryGenerator(tree, defaultSchema); const tsconfigJson = readJson(tree, '/tsconfig.base.json'); expect(tsconfigJson.compilerOptions.paths['@proj/my-lib']).toEqual([ 'my-lib/src/index.ts', ]); }); it('should create a local tsconfig.json', async () => { await libraryGenerator(tree, defaultSchema); const tsconfigJson = readJson(tree, 'my-lib/tsconfig.json'); expect(tsconfigJson.extends).toBe('../tsconfig.base.json'); expect(tsconfigJson.references).toEqual([ { path: './tsconfig.lib.json', }, { path: './tsconfig.spec.json', }, ]); }); it('should extend the tsconfig.lib.json with tsconfig.spec.json', async () => { await libraryGenerator(tree, defaultSchema); const tsconfigJson = readJson(tree, 'my-lib/tsconfig.spec.json'); expect(tsconfigJson.extends).toEqual('./tsconfig.json'); }); it('should extend ./tsconfig.json with tsconfig.lib.json', async () => { await libraryGenerator(tree, defaultSchema); const tsconfigJson = readJson(tree, 'my-lib/tsconfig.lib.json'); expect(tsconfigJson.extends).toEqual('./tsconfig.json'); }); it('should ignore test files in tsconfig.lib.json', async () => { await libraryGenerator(tree, defaultSchema); const tsconfigJson = readJson(tree, 'my-lib/tsconfig.lib.json'); expect(tsconfigJson.exclude).toMatchSnapshot(); }); it('should generate files', async () => { await libraryGenerator(tree, defaultSchema); expect(tree.exists('my-lib/package.json')).toBeFalsy(); expect(tree.exists('my-lib/src/index.ts')).toBeTruthy(); expect(tree.exists('my-lib/src/lib/my-lib.vue')).toBeTruthy(); expect(tree.exists('my-lib/src/lib/my-lib.spec.ts')).toBeTruthy(); const eslintJson = readJson(tree, 'my-lib/.eslintrc.json'); expect(eslintJson).toMatchSnapshot(); }); it('should support eslint flat config CJS', async () => { tree.write( 'eslint.config.cjs', `const { FlatCompat } = require('@eslint/eslintrc'); const nxEslintPlugin = require('@nx/eslint-plugin'); const js = require('@eslint/js'); const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: js.configs.recommended, }); module.exports = [ { plugins: { '@nx': nxEslintPlugin } }, { files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], rules: { '@nx/enforce-module-boundaries': [ 'error', { enforceBuildableLibDependency: true, allow: [], depConstraints: [ { sourceTag: '*', onlyDependOnLibsWithTags: ['*'], }, ], }, ], }, }, ...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({ ...config, files: ['**/*.ts', '**/*.tsx'], rules: { ...config.rules, }, })), ...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({ ...config, files: ['**/*.js', '**/*.jsx'], rules: { ...config.rules, }, })), ...compat.config({ env: { jest: true } }).map((config) => ({ ...config, files: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.spec.js', '**/*.spec.jsx'], rules: { ...config.rules, }, })), ]; ` ); await libraryGenerator(tree, defaultSchema); const eslintJson = tree.read('my-lib/eslint.config.cjs', 'utf-8'); expect(eslintJson).toMatchSnapshot(); // assert **/*.vue was added to override in base eslint config const eslintBaseJson = tree.read('eslint.config.cjs', 'utf-8'); expect(eslintBaseJson).toContain( `files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],` ); }); it('should support eslint flat config ESM', async () => { tree.write( 'eslint.config.mjs', `import { FlatCompat } from '@eslint/eslintrc'; import { dirname } from 'path'; import { fileURLToPath } from 'url'; import js from '@eslint/js'; import nx from '@nx/eslint-plugin'; import baseConfig from '../eslint.config.mjs'; const compat = new FlatCompat({ baseDirectory: dirname(fileURLToPath(import.meta.url)), recommendedConfig: js.configs.recommended, }); export default [ { plugins: { '@nx': nxEslintPlugin } }, { files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], rules: { '@nx/enforce-module-boundaries': [ 'error', { enforceBuildableLibDependency: true, allow: [], depConstraints: [ { sourceTag: '*', onlyDependOnLibsWithTags: ['*'], }, ], }, ], }, }, ...compat.config({ extends: ['plugin:@nx/typescript'] }).map((config) => ({ ...config, files: ['**/*.ts', '**/*.tsx'], rules: { ...config.rules, }, })), ...compat.config({ extends: ['plugin:@nx/javascript'] }).map((config) => ({ ...config, files: ['**/*.js', '**/*.jsx'], rules: { ...config.rules, }, })), ...compat.config({ env: { jest: true } }).map((config) => ({ ...config, files: ['**/*.spec.ts', '**/*.spec.tsx', '**/*.spec.js', '**/*.spec.jsx'], rules: { ...config.rules, }, })), ]` ); await libraryGenerator(tree, defaultSchema); const eslintJson = tree.read('my-lib/eslint.config.mjs', 'utf-8'); expect(eslintJson).toMatchSnapshot(); // assert **/*.vue was added to override in base eslint config const eslintBaseJson = tree.read('eslint.config.mjs', 'utf-8'); expect(eslintBaseJson).toContain( `files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],` ); }); describe('nested', () => { it('should update tags and implicitDependencies', async () => { await libraryGenerator(tree, { ...defaultSchema, name: 'my-dir-my-lib', directory: 'my-dir/my-lib', tags: 'one', }); const myLib = readProjectConfiguration(tree, 'my-dir-my-lib'); expect(myLib).toEqual( expect.objectContaining({ tags: ['one'], }) ); await libraryGenerator(tree, { ...defaultSchema, name: 'my-dir-my-lib2', directory: 'my-dir/my-lib2', tags: 'one,two', }); const myLib2 = readProjectConfiguration(tree, 'my-dir-my-lib2'); expect(myLib2).toEqual( expect.objectContaining({ tags: ['one', 'two'], }) ); }); it('should generate files', async () => { await libraryGenerator(tree, { ...defaultSchema, name: 'my-dir-my-lib', directory: 'my-dir/my-lib', }); expect(tree.exists('my-dir/my-lib/src/index.ts')).toBeTruthy(); expect( tree.exists('my-dir/my-lib/src/lib/my-dir-my-lib.vue') ).toBeTruthy(); expect( tree.exists('my-dir/my-lib/src/lib/my-dir-my-lib.spec.ts') ).toBeTruthy(); }); it('should update project configurations', async () => { await libraryGenerator(tree, { ...defaultSchema, name: 'my-dir-my-lib', directory: 'my-dir/my-lib', }); const config = readProjectConfiguration(tree, 'my-dir-my-lib'); expect(config.root).toEqual('my-dir/my-lib'); }); it('should update root tsconfig.base.json', async () => { await libraryGenerator(tree, { ...defaultSchema, name: 'my-dir-my-lib', importPath: '@proj/my-dir/my-lib', directory: 'my-dir/my-lib', }); const tsconfigJson = readJson(tree, '/tsconfig.base.json'); expect(tsconfigJson.compilerOptions.paths['@proj/my-dir/my-lib']).toEqual( ['my-dir/my-lib/src/index.ts'] ); expect( tsconfigJson.compilerOptions.paths['my-dir-my-lib/*'] ).toBeUndefined(); }); it('should create a local tsconfig.json', async () => { await libraryGenerator(tree, { ...defaultSchema, directory: 'my-dir/my-lib', }); const tsconfigJson = readJson(tree, 'my-dir/my-lib/tsconfig.json'); expect(tsconfigJson).toMatchSnapshot(); }); }); describe('--no-component', () => { it('should not generate components or styles', async () => { await libraryGenerator(tree, { ...defaultSchema, component: false }); expect(tree.exists('my-lib/src/lib')).toBeFalsy(); }); }); describe('--unit-test-runner none', () => { it('should not generate test configuration', async () => { await libraryGenerator(tree, { ...defaultSchema, unitTestRunner: 'none', }); expect(tree.exists('my-lib/tsconfig.spec.json')).toBeFalsy(); expect(tree.read('my-lib/vite.config.ts', 'utf-8')).toMatchSnapshot(); }); }); describe('--publishable', () => { it('should add build targets', async () => { await libraryGenerator(tree, { ...defaultSchema, publishable: true, importPath: '@proj/my-lib', }); expect(tree.read('my-lib/vite.config.ts', 'utf-8')).toMatchSnapshot(); }); it('should fail if no importPath is provided with publishable', async () => { expect.assertions(1); try { await libraryGenerator(tree, { ...defaultSchema, directory: 'myDir', publishable: true, }); } catch (e) { expect(e.message).toContain( 'For publishable libs you have to provide a proper "--importPath" which needs to be a valid npm package name (e.g. my-awesome-lib or @myorg/my-lib)' ); } }); it('should add package.json and .babelrc', async () => { await libraryGenerator(tree, { ...defaultSchema, publishable: true, importPath: '@proj/my-lib', }); const packageJson = readJson(tree, '/my-lib/package.json'); expect(packageJson.name).toEqual('@proj/my-lib'); expect(tree.exists('/my-lib/.babelrc')); }); }); describe('--js', () => { it('should generate JS files', async () => { await libraryGenerator(tree, { ...defaultSchema, js: true, }); expect(tree.exists('/my-lib/src/index.js')).toBe(true); }); }); describe('--importPath', () => { it('should update the package.json & tsconfig with the given import path', async () => { await libraryGenerator(tree, { ...defaultSchema, publishable: true, directory: 'my-dir/my-lib', importPath: '@myorg/lib', }); const packageJson = readJson(tree, 'my-dir/my-lib/package.json'); const tsconfigJson = readJson(tree, '/tsconfig.base.json'); expect(packageJson.name).toBe('@myorg/lib'); expect( tsconfigJson.compilerOptions.paths[packageJson.name] ).toBeDefined(); }); it('should fail if the same importPath has already been used', async () => { await libraryGenerator(tree, { ...defaultSchema, directory: 'my-lib1', publishable: true, importPath: '@myorg/lib', }); try { await libraryGenerator(tree, { ...defaultSchema, directory: 'myLib2', publishable: true, importPath: '@myorg/lib', }); } catch (e) { expect(e.message).toContain( 'You already have a library using the import path' ); } expect.assertions(1); }); }); describe('--no-strict', () => { it('should not add options for strict mode', async () => { await libraryGenerator(tree, { ...defaultSchema, strict: false, }); const tsconfigJson = readJson(tree, '/my-lib/tsconfig.json'); expect(tsconfigJson.compilerOptions.strict).toEqual(false); }); }); describe('--setParserOptionsProject', () => { it('should set the parserOptions.project in the eslintrc.json file', async () => { await libraryGenerator(tree, { ...defaultSchema, setParserOptionsProject: true, }); const eslintConfig = readJson(tree, 'my-lib/.eslintrc.json'); expect(eslintConfig.overrides[0].parserOptions.project).toEqual([ 'my-lib/tsconfig.*?.json', ]); expect(eslintConfig.overrides[0].files).toContain('*.vue'); }); }); describe('TS solution setup', () => { beforeEach(() => { tree = createTreeWithEmptyWorkspace(); updateJson(tree, 'package.json', (json) => { json.workspaces = ['packages/*', 'apps/*']; return json; }); writeJson(tree, 'tsconfig.base.json', { compilerOptions: { composite: true, declaration: true, customConditions: ['development'], }, }); writeJson(tree, 'tsconfig.json', { extends: './tsconfig.base.json', files: [], references: [], }); }); it('should add project references when using TS solution', async () => { await libraryGenerator(tree, { ...defaultSchema, setParserOptionsProject: true, linter: 'eslint', useProjectJson: false, }); expect(tree.read('my-lib/vite.config.ts', 'utf-8')) .toMatchInlineSnapshot(` "import vue from '@vitejs/plugin-vue'; import { defineConfig } from 'vite'; export default defineConfig(() => ({ root: __dirname, cacheDir: '../node_modules/.vite/my-lib', plugins: [vue()], // Uncomment this if you are using workers. // worker: { // plugins: [ nxViteTsPaths() ], // }, test: { watch: false, globals: true, environment: 'jsdom', include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], reporters: ['default'], coverage: { reportsDirectory: './test-output/vitest/coverage', provider: 'v8' as const, }, }, })); " `); expect(readJson(tree, 'tsconfig.json').references).toMatchInlineSnapshot(` [ { "path": "./my-lib", }, ] `); // Make sure keys are in idiomatic order expect(Object.keys(readJson(tree, 'my-lib/package.json'))) .toMatchInlineSnapshot(` [ "name", "version", "module", "types", "exports", "nx", ] `); expect(tree.read('my-lib/package.json', 'utf-8')).toMatchInlineSnapshot(` "{ "name": "@proj/my-lib", "version": "0.0.1", "module": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": { "types": "./src/index.ts", "import": "./src/index.ts", "default": "./src/index.ts" }, "./package.json": "./package.json" }, "nx": { "targets": { "lint": { "executor": "@nx/eslint:lint" }, "test": { "executor": "@nx/vite:test", "outputs": [ "{options.reportsDirectory}" ], "options": { "reportsDirectory": "../coverage/my-lib" } } } } } " `); expect(readJson(tree, 'my-lib/tsconfig.json')).toMatchInlineSnapshot(` { "extends": "../tsconfig.base.json", "files": [], "include": [], "references": [ { "path": "./tsconfig.lib.json", }, { "path": "./tsconfig.spec.json", }, ], } `); expect(readJson(tree, 'my-lib/tsconfig.lib.json')).toMatchInlineSnapshot(` { "compilerOptions": { "jsx": "preserve", "jsxImportSource": "vue", "module": "esnext", "moduleResolution": "bundler", "outDir": "dist", "resolveJsonModule": true, "rootDir": "src", "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", "types": [ "vite/client", ], }, "exclude": [ "out-tsc", "dist", "src/**/__tests__/*", "src/**/*.spec.vue", "src/**/*.test.vue", "vite.config.ts", "vite.config.mts", "vitest.config.ts", "vitest.config.mts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.test.tsx", "src/**/*.spec.tsx", "src/**/*.test.js", "src/**/*.spec.js", "src/**/*.test.jsx", "src/**/*.spec.jsx", "eslint.config.js", "eslint.config.cjs", "eslint.config.mjs", ], "extends": "../tsconfig.base.json", "include": [ "src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", ], } `); expect(readJson(tree, 'my-lib/tsconfig.spec.json')) .toMatchInlineSnapshot(` { "compilerOptions": { "jsx": "preserve", "jsxImportSource": "vue", "module": "esnext", "moduleResolution": "bundler", "outDir": "./out-tsc/vitest", "resolveJsonModule": true, "types": [ "vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest", ], }, "extends": "../tsconfig.base.json", "include": [ "vite.config.ts", "vite.config.mts", "vitest.config.ts", "vitest.config.mts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.test.tsx", "src/**/*.spec.tsx", "src/**/*.test.js", "src/**/*.spec.js", "src/**/*.test.jsx", "src/**/*.spec.jsx", "src/**/*.d.ts", ], "references": [ { "path": "./tsconfig.lib.json", }, ], } `); }); it('should create a correct package.json for buildable libraries', async () => { await libraryGenerator(tree, { ...defaultSchema, setParserOptionsProject: true, linter: 'eslint', addPlugin: true, useProjectJson: false, bundler: 'vite', skipFormat: true, }); expect(tree.read('my-lib/package.json', 'utf-8')).toMatchInlineSnapshot(` "{ "name": "@proj/my-lib", "version": "0.0.1", "type": "module", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { "./package.json": "./package.json", ".": { "development": "./src/index.ts", "types": "./dist/index.d.ts", "import": "./dist/index.js", "default": "./dist/index.js" } } } " `); }); it('should not set the "development" condition in exports when it does not exist in tsconfig.base.json', async () => { updateJson(tree, 'tsconfig.base.json', (json) => { delete json.compilerOptions.customConditions; return json; }); await libraryGenerator(tree, { ...defaultSchema, setParserOptionsProject: true, linter: 'eslint', addPlugin: true, useProjectJson: false, bundler: 'vite', skipFormat: true, }); expect( readJson(tree, 'my-lib/package.json').exports['.'] ).not.toHaveProperty('development'); }); it('should set "nx.name" in package.json when the user provides a name that is different than the package name', async () => { await libraryGenerator(tree, { ...defaultSchema, directory: 'my-lib', name: 'my-lib', // import path contains the npm scope, so it would be different addPlugin: true, useProjectJson: false, skipFormat: true, }); expect(readJson(tree, 'my-lib/package.json').nx).toStrictEqual({ name: 'my-lib', }); }); it('should not set "nx.name" in package.json when the provided name matches the package name', async () => { await libraryGenerator(tree, { ...defaultSchema, directory: 'my-lib', name: '@proj/my-lib', addPlugin: true, useProjectJson: false, skipFormat: true, }); expect(readJson(tree, 'my-lib/package.json').nx).toBeUndefined(); }); it('should not set "nx.name" in package.json when the user does not provide a name', async () => { await libraryGenerator(tree, { ...defaultSchema, // defaultSchema has no name directory: 'my-lib', addPlugin: true, useProjectJson: false, skipFormat: true, }); expect(readJson(tree, 'my-lib/package.json').nx).toBeUndefined(); }); it('should generate project.json if useProjectJson is true', async () => { await libraryGenerator(tree, { ...defaultSchema, linter: 'eslint', addPlugin: true, useProjectJson: true, skipFormat: true, }); expect(tree.exists('my-lib/project.json')).toBeTruthy(); expect(readProjectConfiguration(tree, '@proj/my-lib')) .toMatchInlineSnapshot(` { "$schema": "../node_modules/nx/schemas/project-schema.json", "name": "@proj/my-lib", "projectType": "library", "root": "my-lib", "sourceRoot": "my-lib/src", "tags": [], "targets": {}, } `); expect(readJson(tree, 'my-lib/package.json').nx).toBeUndefined(); }); }); });