diff --git a/packages/devkit/ngcli-adapter.ts b/packages/devkit/ngcli-adapter.ts index 5688e6e499..470e2d8311 100644 --- a/packages/devkit/ngcli-adapter.ts +++ b/packages/devkit/ngcli-adapter.ts @@ -1,4 +1,6 @@ export { wrapAngularDevkitSchematic, + overrideCollectionResolutionForTesting, + mockSchematicsForTesting, NxScopedHost, } from '@nrwl/tao/src/commands/ngcli-adapter'; diff --git a/packages/tao/src/commands/ngcli-adapter.ts b/packages/tao/src/commands/ngcli-adapter.ts index 7429260ffb..13809971ea 100644 --- a/packages/tao/src/commands/ngcli-adapter.ts +++ b/packages/tao/src/commands/ngcli-adapter.ts @@ -289,13 +289,7 @@ export class NxScopedHost extends virtualFs.ScopedHost { actualConfigFileName: any; isNewFormat: boolean; }> { - const p = path.toString(); - if ( - p === 'angular.json' || - p === '/angular.json' || - p === 'workspace.json' || - p === '/workspace.json' - ) { + if (isWorkspaceConfigPath(path)) { return super.exists('/angular.json' as any).pipe( switchMap((isAngularJson) => { const actualConfigFileName = isAngularJson @@ -340,7 +334,7 @@ export class NxScopedHostForMigrations extends NxScopedHost { } read(path: Path): Observable { - if (this.isWorkspaceConfig(path)) { + if (isWorkspaceConfigPath(path)) { return super.read(path).pipe(map(processConfigWhenReading)); } else { return super.read(path); @@ -348,22 +342,12 @@ export class NxScopedHostForMigrations extends NxScopedHost { } write(path: Path, content: FileBuffer) { - if (this.isWorkspaceConfig(path)) { + if (isWorkspaceConfigPath(path)) { return super.write(path, processConfigWhenWriting(content)); } else { return super.write(path, content); } } - - protected isWorkspaceConfig(path: Path) { - const p = path.toString(); - return ( - p === 'angular.json' || - p === '/angular.json' || - p === 'workspace.json' || - p === '/workspace.json' - ); - } } export class NxScopeHostUsedForWrappedSchematics extends NxScopedHost { @@ -372,46 +356,77 @@ export class NxScopeHostUsedForWrappedSchematics extends NxScopedHost { } read(path: Path): Observable { - return this.context(path).pipe( - switchMap((r) => { - // if it is a workspace config, handle it in a special way - if (r.isWorkspaceConfig) { - const match = this.host - .listChanges() - .find( - (f) => f.path == 'workspace.json' || f.path == 'angular.json' - ); + if (isWorkspaceConfigPath(path)) { + const match = findWorkspaceConfigFileChange(this.host); + // no match, default to existing behavior + if (!match) { + return super.read(path); + } - // no match, default to existing behavior - if (!match) { - return super.read(path); - } - - // we try to format it, if it changes, return it, otherwise return the original change - try { - const w = JSON.parse(Buffer.from(match.content).toString()); - const formatted = toOldFormatOrNull(w); - return of( - formatted - ? Buffer.from(JSON.stringify(formatted, null, 2)) - : Buffer.from(match.content) - ); - } catch (e) { - return super.read(path); - } - } else { - const targetPath = path.startsWith('/') ? path.substring(1) : path; - // found a matching change in the host - const match = this.host - .listChanges() - .find( - (f) => f.path == targetPath.toString() && f.type !== 'DELETE' - ); - return match ? of(Buffer.from(match.content)) : super.read(path); - } - }) - ); + // we try to format it, if it changes, return it, otherwise return the original change + try { + const w = JSON.parse(Buffer.from(match.content).toString()); + const formatted = toOldFormatOrNull(w); + return of( + formatted + ? Buffer.from(JSON.stringify(formatted, null, 2)) + : Buffer.from(match.content) + ); + } catch (e) { + return super.read(path); + } + } else { + // found a matching change in the host + const match = findMatchingFileChange(this.host, path); + return match ? of(Buffer.from(match.content)) : super.read(path); + } } + + exists(path: Path): Observable { + if (isWorkspaceConfigPath(path)) { + return findWorkspaceConfigFileChange(this.host) + ? of(true) + : super.exists(path); + } else { + return findMatchingFileChange(this.host, path) + ? of(true) + : super.exists(path); + } + } + + isFile(path: Path): Observable { + if (isWorkspaceConfigPath(path)) { + return findWorkspaceConfigFileChange(this.host) + ? of(true) + : super.isFile(path); + } else { + return findMatchingFileChange(this.host, path) + ? of(true) + : super.isFile(path); + } + } +} + +function findWorkspaceConfigFileChange(host: Tree) { + return host + .listChanges() + .find((f) => f.path == 'workspace.json' || f.path == 'angular.json'); +} + +function findMatchingFileChange(host: Tree, path: Path) { + const targetPath = path.startsWith('/') ? path.substring(1) : path.toString; + return host + .listChanges() + .find((f) => f.path == targetPath.toString() && f.type !== 'DELETE'); +} + +function isWorkspaceConfigPath(p: Path | string) { + return ( + p === 'angular.json' || + p === '/angular.json' || + p === 'workspace.json' || + p === '/workspace.json' + ); } function processConfigWhenReading(content: ArrayBuffer) { @@ -637,11 +652,77 @@ function convertEventTypeToHandleMultipleConfigNames( } } +let collectionResolutionOverrides = null; +let mockedSchematics = null; + +/** + * By default, Angular Devkit schematic collections will be resolved using the Node resolution. + * This doesn't work if you are testing schematics that refer to other schematics in the + * same repo. + * + * This function can can be used to override the resolution behaviour. + * + * Example: + * + * ``` + * overrideCollectionResolutionForTesting({ + * '@nrwl/workspace': path.join(__dirname, '../../../../workspace/collection.json'), + * '@nrwl/angular': path.join(__dirname, '../../../../angular/collection.json'), + * '@nrwl/linter': path.join(__dirname, '../../../../linter/collection.json') + * }); + * + * ``` + */ +export function overrideCollectionResolutionForTesting(collections: { + [name: string]: string; +}) { + collectionResolutionOverrides = collections; +} + +/** + * If you have an Nx Devkit generator invoking the wrapped Angular Devkit schematic, + * and you don't want the Angular Devkit schematic to run, you can mock it up using this function. + * + * Unfortunately, there are some edge cases in the Nx-Angular devkit integration that + * can be seen in the unit tests context. This function is useful for handling that as well. + * + * In this case, you can mock it up. + * + * Example: + * + * ``` + * mockSchematicsForTesting({ + * 'mycollection:myschematic': (tree, params) => { + * tree.write('README.md'); + * } + * }); + * + * ``` + */ +export function mockSchematicsForTesting(schematics: { + [name: string]: ( + host: Tree, + generatorOptions: { [k: string]: any } + ) => Promise; +}) { + mockedSchematics = schematics; +} + export function wrapAngularDevkitSchematic( collectionName: string, generatorName: string ) { return async (host: Tree, generatorOptions: { [k: string]: any }) => { + if ( + mockedSchematics && + mockedSchematics[`${collectionName}:${generatorName}`] + ) { + return await mockedSchematics[`${collectionName}:${generatorName}`]( + host, + generatorOptions + ); + } + const emptyLogger = { log: (e) => {}, info: (e) => {}, @@ -694,6 +775,19 @@ export function wrapAngularDevkitSchematic( defaults: false, }; const workflow = createWorkflow(fsHost, host.root, options); + + // used for testing + if (collectionResolutionOverrides) { + const r = workflow.engineHost.resolve; + workflow.engineHost.resolve = (collection, b, c) => { + if (collectionResolutionOverrides[collection]) { + return collectionResolutionOverrides[collection]; + } else { + return r.apply(workflow.engineHost, [collection, b, c]); + } + }; + } + const collection = getCollection(workflow, collectionName); const schematic = collection.createSchematic(generatorName, true); const res = await runSchematic( diff --git a/packages/workspace/src/generators/preset/__snapshots__/preset.spec.ts.snap b/packages/workspace/src/generators/preset/__snapshots__/preset.spec.ts.snap new file mode 100644 index 0000000000..a44d399a1b --- /dev/null +++ b/packages/workspace/src/generators/preset/__snapshots__/preset.spec.ts.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`preset should create files (preset = angular) 1`] = ` +Array [ + "tsconfig.editor.json", + "tsconfig.json", + "src", + ".browserslistrc", + "tsconfig.app.json", + ".eslintrc.json", + "jest.config.js", + "tsconfig.spec.json", +] +`; + +exports[`preset should create files (preset = angular) 2`] = ` +Array [ + "favicon.ico", + "index.html", + "main.ts", + "polyfills.ts", + "styles.css", + "assets", + "environments", + "app", + "test-setup.ts", +] +`; + +exports[`preset should create files (preset = angular) 3`] = ` +Array [ + "app.module.ts", + "app.component.css", + "app.component.html", + "app.component.spec.ts", + "app.component.ts", +] +`; diff --git a/packages/workspace/src/generators/preset/preset.spec.ts b/packages/workspace/src/generators/preset/preset.spec.ts index c9032d2463..eec75e9fa4 100644 --- a/packages/workspace/src/generators/preset/preset.spec.ts +++ b/packages/workspace/src/generators/preset/preset.spec.ts @@ -1,131 +1,132 @@ import { readJson, Tree } from '@nrwl/devkit'; import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing'; +import { overrideCollectionResolutionForTesting } from '@nrwl/devkit/ngcli-adapter'; import { presetGenerator } from './preset'; +import * as path from 'path'; describe('preset', () => { let tree: Tree; beforeEach(() => { tree = createTreeWithEmptyWorkspace(); - }); - - describe('--preset', () => { - // TODO: reenable. This doesn't work because wrapAngularDevkit uses the fs - xdescribe('angular', () => { - it('should create files (preset = angular)', async () => { - await presetGenerator(tree, { - name: 'proj', - preset: 'angular', - cli: 'nx', - }); - expect(tree.children('apps/proj')).toMatchSnapshot(); - expect(tree.children('apps/proj/src/')).toMatchSnapshot(); - expect(tree.children('apps/proj/src/app')).toMatchSnapshot(); - console.log(tree.children('')); - - expect( - JSON.parse(tree.read('/workspace.json').toString()).cli - .defaultCollection - ).toBe('@nrwl/angular'); - }); - }); - - describe('web-components', () => { - it('should create files (preset = web-components)', async () => { - await presetGenerator(tree, { - name: 'proj', - preset: 'web-components', - cli: 'nx', - }); - expect(tree.exists('/apps/proj/src/main.ts')).toBe(true); - expect(readJson(tree, '/workspace.json').cli.defaultCollection).toBe( - '@nrwl/web' - ); - }); + overrideCollectionResolutionForTesting({ + '@nrwl/workspace': path.join( + __dirname, + '../../../../workspace/collection.json' + ), + '@nrwl/angular': path.join( + __dirname, + '../../../../angular/collection.json' + ), + '@nrwl/linter': path.join( + __dirname, + '../../../../linter/collection.json' + ), + '@nrwl/nest': path.join(__dirname, '../../../../nest/collection.json'), + '@nrwl/node': path.join(__dirname, '../../../../node/collection.json'), + '@nrwl/jest': path.join(__dirname, '../../../../jest/collection.json'), + '@nrwl/cypress': path.join( + __dirname, + '../../../../cypress/collection.json' + ), + '@nrwl/express': path.join( + __dirname, + '../../../../express/collection.json' + ), }); }); - // it('should create files (preset = react)', async () => { - // const tree = await runSchematic( - // 'preset', - // { name: 'proj', preset: 'react' }, - // tree - // ); - // expect(tree.exists('/apps/proj/src/main.tsx')).toBe(true); - // expect( - // JSON.parse(tree.readContent('/workspace.json')).cli.defaultCollection - // ).toBe('@nrwl/react'); - // }); - // - // - // it('should create files (preset = next)', async () => { - // const tree = await runSchematic( - // 'preset', - // { name: 'proj', preset: 'next' }, - // tree - // ); - // expect(tree.exists('/apps/proj/pages/index.tsx')).toBe(true); - // expect( - // JSON.parse(tree.readContent('/workspace.json')).cli.defaultCollection - // ).toBe('@nrwl/next'); - // }); - // - // describe('--preset angular-nest', () => { - // it('should create files', async () => { - // const tree = await runSchematic( - // 'preset', - // { name: 'proj', preset: 'angular-nest' }, - // tree - // ); - // expect(tree.exists('/apps/proj/src/app/app.component.ts')).toBe(true); - // expect(tree.exists('/apps/api/src/app/app.controller.ts')).toBe(true); - // expect( - // tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts') - // ).toBe(true); - // }); - // - // it('should work with unnormalized names', async () => { - // const tree = await runSchematic( - // 'preset', - // { name: 'myProj', preset: 'angular-nest' }, - // tree - // ); - // - // expect(tree.exists('/apps/my-proj/src/app/app.component.ts')).toBe(true); - // expect(tree.exists('/apps/api/src/app/app.controller.ts')).toBe(true); - // expect( - // tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts') - // ).toBe(true); - // }); - // }); - // - // describe('--preset react-express', () => { - // it('should create files', async () => { - // const tree = await runSchematic( - // 'preset', - // { name: 'proj', preset: 'react-express' }, - // tree - // ); - // expect(tree.exists('/apps/proj/src/app/app.tsx')).toBe(true); - // expect( - // tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts') - // ).toBe(true); - // expect(tree.exists('/apps/proj/.eslintrc.json')).toBe(true); - // expect(tree.exists('/apps/api/.eslintrc.json')).toBe(true); - // expect(tree.exists('/libs/api-interfaces/.eslintrc.json')).toBe(true); - // }); - // - // it('should work with unnormalized names', async () => { - // const tree = await runSchematic( - // 'preset', - // { name: 'myProj', preset: 'react-express' }, - // tree - // ); - // - // expect(tree.exists('/apps/my-proj/src/app/app.tsx')).toBe(true); - // expect( - // tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts') - // ).toBe(true); - // }); - // }); + afterEach(() => { + overrideCollectionResolutionForTesting(null); + }); + + it('should create files (preset = angular)', async () => { + await presetGenerator(tree, { + name: 'proj', + preset: 'angular', + cli: 'nx', + }); + expect(tree.children('apps/proj')).toMatchSnapshot(); + expect(tree.children('apps/proj/src/')).toMatchSnapshot(); + expect(tree.children('apps/proj/src/app')).toMatchSnapshot(); + console.log(tree.children('')); + + expect( + JSON.parse(tree.read('/workspace.json').toString()).cli.defaultCollection + ).toBe('@nrwl/angular'); + }); + + it('should create files (preset = web-components)', async () => { + await presetGenerator(tree, { + name: 'proj', + preset: 'web-components', + cli: 'nx', + }); + expect(tree.exists('/apps/proj/src/main.ts')).toBe(true); + expect(readJson(tree, '/workspace.json').cli.defaultCollection).toBe( + '@nrwl/web' + ); + }); + + it('should create files (preset = react)', async () => { + await presetGenerator(tree, { + name: 'proj', + preset: 'react', + style: 'css', + linter: 'eslint', + cli: 'nx', + }); + expect(tree.exists('/apps/proj/src/main.tsx')).toBe(true); + expect(readJson(tree, '/workspace.json').cli.defaultCollection).toBe( + '@nrwl/react' + ); + }); + + it('should create files (preset = next)', async () => { + await presetGenerator(tree, { + name: 'proj', + preset: 'next', + style: 'css', + linter: 'eslint', + cli: 'nx', + }); + expect(tree.exists('/apps/proj/pages/index.tsx')).toBe(true); + expect(readJson(tree, '/workspace.json').cli.defaultCollection).toBe( + '@nrwl/next' + ); + }); + + it('should create files (preset = angular-nest)', async () => { + await presetGenerator(tree, { + name: 'proj', + preset: 'angular-nest', + style: 'css', + linter: 'eslint', + cli: 'nx', + }); + + expect(tree.exists('/apps/proj/src/app/app.component.ts')).toBe(true); + expect(tree.exists('/apps/api/src/app/app.controller.ts')).toBe(true); + expect(tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts')).toBe( + true + ); + }); + + it('should create files (preset react-express)', async () => { + await presetGenerator(tree, { + name: 'proj', + preset: 'react-express', + style: 'css', + linter: 'eslint', + cli: 'nx', + }); + + expect(tree.exists('/apps/proj/src/app/app.tsx')).toBe(true); + expect(tree.exists('/libs/api-interfaces/src/lib/api-interfaces.ts')).toBe( + true + ); + expect(tree.exists('/apps/proj/.eslintrc.json')).toBe(true); + expect(tree.exists('/apps/api/.eslintrc.json')).toBe(true); + expect(tree.exists('/libs/api-interfaces/.eslintrc.json')).toBe(true); + }); }); diff --git a/packages/workspace/src/utils/workspace.ts b/packages/workspace/src/utils/workspace.ts index 53607652fd..7961c77679 100644 --- a/packages/workspace/src/utils/workspace.ts +++ b/packages/workspace/src/utils/workspace.ts @@ -20,6 +20,8 @@ function createHost(tree: Tree): workspaces.WorkspaceHost { }, async isDirectory(path: string): Promise { // approximate a directory check + // special case needed when testing wrapped schematics + if (path === '/') return true; return !tree.exists(path) && tree.getDir(path).subfiles.length > 0; }, async isFile(path: string): Promise {