Skip to content

Commit 13516d6

Browse files
committed
fix: respect extensions and module imports when generating components
fixes #78, #54
1 parent 33edbea commit 13516d6

File tree

5 files changed

+257
-42
lines changed

5 files changed

+257
-42
lines changed

src/generate/component/find-module.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { dirname } from 'path';
2+
3+
import { Tree, DirEntry } from "@angular-devkit/schematics";
4+
import { join, normalize } from '@angular-devkit/core';
5+
import { dasherize } from '@angular-devkit/core/src/utils/strings';
6+
7+
import { Schema as ComponentOptions } from './schema';
8+
9+
export function findModule(tree: Tree, options: ComponentOptions, path: string, extension: string) {
10+
if (options.module) {
11+
return findExplicitModule(tree, path, extension, options.module);
12+
} else {
13+
const pathToCheck = (path || '')
14+
+ (options.flat ? '' : '/' + dasherize(options.name));
15+
16+
return findImplicitModule(tree, pathToCheck, extension);
17+
}
18+
}
19+
20+
function findExplicitModule(tree: Tree, path: string, extension: string, moduleName: string) {
21+
while (path) {
22+
const modulePath = normalize(`/${path}/${moduleName}`);
23+
const moduleBaseName = normalize(modulePath).split('/').pop();
24+
25+
if (tree.exists(modulePath)) {
26+
return normalize(modulePath);
27+
} else if (tree.exists(modulePath + extension + '.ts')) {
28+
return normalize(modulePath + extension + '.ts');
29+
} else if (tree.exists(modulePath + '.module' + extension + '.ts')) {
30+
return normalize(modulePath + '.module' + extension + '.ts');
31+
} else if (tree.exists(modulePath + '/' + moduleBaseName + '.module' + extension + '.ts')) {
32+
return normalize(modulePath + '/' + moduleBaseName + '.module' + extension + '.ts');
33+
}
34+
35+
path = dirname(path);
36+
}
37+
38+
throw new Error('Specified module does not exist');
39+
}
40+
41+
function findImplicitModule(tree: Tree, path: string, extension: string) {
42+
let dir: DirEntry | null = tree.getDir(`/${path}`);
43+
44+
const moduleRe = new RegExp(`.module${extension}.ts`);
45+
const routingModuleRe = new RegExp(`-routing.module${extension}.ts`);
46+
47+
while (dir) {
48+
const matches = dir.subfiles.filter(p => moduleRe.test(p) && !routingModuleRe.test(p));
49+
if (matches.length == 1) {
50+
return join(dir.path, matches[0]);
51+
} else if (matches.length > 1) {
52+
throw new Error('More than one module matches. Use skip-import option to skip importing '
53+
+ 'the component into the closest module.');
54+
}
55+
56+
dir = dir.parent;
57+
}
58+
throw new Error('Could not find an NgModule. Use the skip-import '
59+
+ 'option to skip importing in NgModule.');
60+
}

src/generate/component/index.ts

+29-13
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { dirname } from 'path';
2+
13
import {
24
Rule,
35
SchematicContext,
@@ -11,14 +13,17 @@ import {
1113
mergeWith,
1214
TemplateOptions,
1315
filter,
16+
DirEntry,
1417
} from '@angular-devkit/schematics';
1518

16-
import { insertModuleId } from '../../ast-utils';
17-
import { Schema as ComponentOptions } from './schema';
1819
import { dasherize } from '@angular-devkit/core/src/utils/strings';
20+
import { Path } from '@angular-devkit/core';
1921
import { parseName } from '@schematics/angular/utility/parse-name';
22+
2023
import { Extensions, getExtensions, removeNsSchemaOptions, PlatformUse, getPlatformUse, validateGenerateOptions } from '../utils';
21-
import { Path } from '@angular-devkit/core';
24+
import { addDeclarationToNgModule, insertModuleId } from './ast-utils';
25+
import { Schema as ComponentOptions } from './schema';
26+
import { findModule } from './find-module';
2227

2328
class ComponentInfo {
2429
classPath: string;
@@ -42,25 +47,38 @@ export default function (options: ComponentOptions): Rule {
4247
options.spec = false;
4348
}
4449

45-
if (
46-
!platformUse.nsOnly && // the project is shared
47-
platformUse.useNs && !platformUse.useWeb // the new component is only for {N}
48-
) {
49-
options.skipImport = true; // don't declare it in the web NgModule
50-
}
51-
5250
validateGenerateOptions(platformUse, options);
5351
validateGenerateComponentOptions(platformUse, options);
5452

5553
return tree;
5654
},
5755

58-
() => externalSchematic('@schematics/angular', 'component', removeNsSchemaOptions(options)),
56+
() => externalSchematic('@schematics/angular', 'component', removeNsSchemaOptions({ ...options, skipImport: true })),
5957

6058
(tree: Tree) => {
6159
extensions = getExtensions(tree, options);
6260
componentInfo = parseComponentInfo(tree, options);
6361
},
62+
63+
(tree: Tree) => {
64+
if (options.skipImport) {
65+
return tree;
66+
}
67+
68+
const componentPath = componentInfo.classPath;
69+
const componentDir = dirname(componentPath);
70+
if (platformUse.useWeb) {
71+
const webModule = findModule(tree, options, componentDir, extensions.web);
72+
addDeclarationToNgModule(tree, options, componentPath, webModule);
73+
}
74+
75+
if (platformUse.useNs) {
76+
const nsModule = findModule(tree, options, componentDir, extensions.ns);
77+
addDeclarationToNgModule(tree, options, componentPath, nsModule);
78+
}
79+
80+
return tree;
81+
},
6482

6583
(tree: Tree) => {
6684
if (platformUse.nsOnly) {
@@ -91,7 +109,6 @@ const validateGenerateComponentOptions = (platformUse: PlatformUse, options: Com
91109
};
92110

93111
const parseComponentInfo = (tree: Tree, options: ComponentOptions): ComponentInfo => {
94-
// const path = `/${projectSettings.root}/${projectSettings.sourceRoot}/app`;
95112
const component = new ComponentInfo();
96113

97114
const parsedPath = parseName(options.path || '', options.name);
@@ -144,5 +161,4 @@ const addNativeScriptFiles = (component: ComponentInfo) => {
144161
]);
145162

146163
return mergeWith(templateSource);
147-
148164
};

src/generate/component/index_spec.ts

+160-25
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { join } from 'path';
33
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
44
import { getFileContent } from '@schematics/angular/utility/test';
55

6-
import { createEmptyNsOnlyProject, createEmptySharedProject, toComponentClassName } from '../../utils';
6+
import { createEmptyNsOnlyProject, createEmptySharedProject, toComponentClassName, callRuleSync } from '../../utils';
77
import { DEFAULT_SHARED_EXTENSIONS } from '../utils';
88
import { isInComponentMetadata, isInModuleMetadata } from '../../test-utils';
99
import { Schema as ComponentOptions } from './schema';
10+
import { Schema as ApplicationOptions } from '../../ng-new/shared/schema';
11+
import { move } from '@angular-devkit/schematics';
1012

1113
describe('Component Schematic', () => {
1214
const name = 'foo';
@@ -144,7 +146,7 @@ describe('Component Schematic', () => {
144146
describe('specifying custom extension', () => {
145147
describe('in ns only project', () => {
146148
beforeEach(() => {
147-
appTree = createEmptyNsOnlyProject(project);
149+
appTree = createEmptyNsOnlyProject(project, '.mobile');
148150
});
149151

150152
it('should respect specified {N} extension', () => {
@@ -155,40 +157,173 @@ describe('Component Schematic', () => {
155157
const componentTemplatePath = getTemplatePath(customExtension);
156158
expect(appTree.exists(componentTemplatePath)).toBeTruthy();
157159
});
158-
})
160+
});
161+
159162
describe('in ns+web project', () => {
160-
beforeEach(() => {
161-
appTree = createEmptySharedProject(project);
163+
describe('when a custom web extension is specified', () => {
164+
const customExtension = '.web';
165+
const componentOptions = { ...defaultOptions, webExtension: customExtension, web: true };
166+
167+
beforeEach(() => {
168+
appTree = createEmptySharedProject(project, customExtension, '.tns');
169+
});
170+
171+
it('should create the files with this extension', () => {
172+
const options = { ...componentOptions };
173+
appTree = schematicRunner.runSchematic('component', options, appTree);
174+
175+
const componentTemplatePath = getTemplatePath(customExtension);
176+
expect(appTree.exists(componentTemplatePath)).toBeTruthy();
177+
});
178+
179+
it('should declare in NgModule', () => {
180+
const options = { ...componentOptions };
181+
appTree = schematicRunner.runSchematic('component', options, appTree);
182+
183+
const webModulePath = `src/app/app.module${customExtension}.ts`;
184+
const nsModulePath = `src/app/app.module.tns.ts`;
185+
const matcher = isInModuleMetadata('AppModule', 'declarations', componentClassName, true);
186+
187+
const webModuleContent = getFileContent(appTree, webModulePath);
188+
expect(webModuleContent).toMatch(matcher);
189+
190+
const nsModuleContent = getFileContent(appTree, nsModulePath);
191+
expect(nsModuleContent).toMatch(matcher);
192+
});
193+
194+
it('should respect the module option', () => {
195+
const moduleName = 'random';
196+
const webModulePath = `src/app/${moduleName}/${moduleName}.module${customExtension}.ts`;
197+
const nsModulePath = `src/app/${moduleName}/${moduleName}.module.tns.ts`;
198+
appTree = schematicRunner.runSchematic('module', {
199+
project,
200+
name: moduleName,
201+
webExtension: customExtension,
202+
}, appTree);
203+
204+
const options = { ...componentOptions, module: moduleName };
205+
appTree = schematicRunner.runSchematic('component', options, appTree);
206+
207+
const matcher = isInModuleMetadata('RandomModule', 'declarations', componentClassName, true);
208+
209+
const webModuleContent = getFileContent(appTree, webModulePath);
210+
expect(webModuleContent).toMatch(matcher);
211+
212+
const nsModuleContent = getFileContent(appTree, nsModulePath);
213+
expect(nsModuleContent).toMatch(matcher);
214+
});
162215
});
163216

164-
it('should respect specified {N} extension', () => {
217+
describe('when a custon {N} extension is specified', () => {
165218
const customExtension = '.mobile';
166-
const options = { ...defaultOptions, nsExtension: customExtension, nativescript: true };
167-
appTree = schematicRunner.runSchematic('component', options, appTree);
219+
const componentOptions = { ...defaultOptions, nsExtension: customExtension, nativescript: true };
168220

169-
const componentTemplatePath = getTemplatePath(customExtension);
170-
expect(appTree.exists(componentTemplatePath)).toBeTruthy();
171-
});
221+
beforeEach(() => {
222+
appTree = createEmptySharedProject(project, '', customExtension);
223+
});
172224

173-
it('should respect specified web extension', () => {
174-
const customExtension = '.web';
175-
const options = { ...defaultOptions, webExtension: customExtension, web: true };
176-
appTree = schematicRunner.runSchematic('component', options, appTree);
225+
it('should create the files with this extension', () => {
226+
const options = { ...componentOptions };
227+
appTree = schematicRunner.runSchematic('component', options, appTree);
177228

178-
const componentTemplatePath = getTemplatePath(customExtension);
179-
expect(appTree.exists(componentTemplatePath)).toBeTruthy();
229+
const componentTemplatePath = getTemplatePath(customExtension);
230+
expect(appTree.exists(componentTemplatePath)).toBeTruthy();
231+
});
232+
233+
it('should declare in NgModule', () => {
234+
const options = { ...componentOptions };
235+
appTree = schematicRunner.runSchematic('component', options, appTree);
236+
237+
const webModulePath = `src/app/app.module.ts`;
238+
const nsModulePath = `src/app/app.module${customExtension}.ts`;
239+
const matcher = isInModuleMetadata('AppModule', 'declarations', componentClassName, true);
240+
241+
const webModuleContent = getFileContent(appTree, webModulePath);
242+
expect(webModuleContent).toMatch(matcher);
243+
244+
const nsModuleContent = getFileContent(appTree, nsModulePath);
245+
expect(nsModuleContent).toMatch(matcher);
246+
});
247+
248+
it('should respect the module option', () => {
249+
const moduleName = 'random';
250+
const webModulePath = `src/app/${moduleName}/${moduleName}.module.ts`;
251+
const nsModulePath = `src/app/${moduleName}/${moduleName}.module${customExtension}.ts`;
252+
appTree = schematicRunner.runSchematic('module', {
253+
project,
254+
name: moduleName,
255+
nsExtension: customExtension,
256+
}, appTree);
257+
258+
const options = { ...componentOptions, module: moduleName };
259+
appTree = schematicRunner.runSchematic('component', options, appTree);
260+
261+
const matcher = isInModuleMetadata('RandomModule', 'declarations', componentClassName, true);
262+
263+
const webModuleContent = getFileContent(appTree, webModulePath);
264+
expect(webModuleContent).toMatch(matcher);
265+
266+
const nsModuleContent = getFileContent(appTree, nsModulePath);
267+
expect(nsModuleContent).toMatch(matcher);
268+
});
180269
});
181270

182-
it('should respect both web and {N} extensions', () => {
271+
describe('when custom web and {N} extensions are specified', () => {
183272
const nsExtension = '.mobile';
184273
const webExtension = '.web';
185-
const options = { ...defaultOptions, nsExtension, webExtension, web: true, nativescript: true };
186-
appTree = schematicRunner.runSchematic('component', options, appTree);
187-
188-
const nsTemplate = getTemplatePath(nsExtension);
189-
const webTemplate = getTemplatePath(webExtension);
190-
expect(appTree.exists(nsTemplate)).toBeTruthy();
191-
expect(appTree.exists(webTemplate)).toBeTruthy();
274+
const componentOptions = { ...defaultOptions, nsExtension, webExtension, web: true, nativescript: true };
275+
276+
beforeEach(() => {
277+
appTree = createEmptySharedProject(project, webExtension, nsExtension);
278+
});
279+
280+
it('should create the files with these extensions', () => {
281+
const options = { ...componentOptions };
282+
appTree = schematicRunner.runSchematic('component', options, appTree);
283+
284+
const nsTemplate = getTemplatePath(nsExtension);
285+
const webTemplate = getTemplatePath(webExtension);
286+
expect(appTree.exists(nsTemplate)).toBeTruthy();
287+
expect(appTree.exists(webTemplate)).toBeTruthy();
288+
});
289+
290+
it('should declare in NgModule', () => {
291+
const options = { ...componentOptions };
292+
appTree = schematicRunner.runSchematic('component', options, appTree);
293+
294+
const webModulePath = `src/app/app.module${webExtension}.ts`;
295+
const nsModulePath = `src/app/app.module${nsExtension}.ts`;
296+
const matcher = isInModuleMetadata('AppModule', 'declarations', componentClassName, true);
297+
298+
const webModuleContent = getFileContent(appTree, webModulePath);
299+
expect(webModuleContent).toMatch(matcher);
300+
301+
const nsModuleContent = getFileContent(appTree, nsModulePath);
302+
expect(nsModuleContent).toMatch(matcher);
303+
});
304+
305+
it('should respect the module option', () => {
306+
const moduleName = 'random';
307+
const webModulePath = `src/app/${moduleName}/${moduleName}.module${webExtension}.ts`;
308+
const nsModulePath = `src/app/${moduleName}/${moduleName}.module${nsExtension}.ts`;
309+
appTree = schematicRunner.runSchematic('module', {
310+
project,
311+
name: moduleName,
312+
webExtension,
313+
nsExtension,
314+
}, appTree);
315+
316+
const options = { ...componentOptions, module: moduleName };
317+
appTree = schematicRunner.runSchematic('component', options, appTree);
318+
319+
const matcher = isInModuleMetadata('RandomModule', 'declarations', componentClassName, true);
320+
321+
const webModuleContent = getFileContent(appTree, webModulePath);
322+
expect(webModuleContent).toMatch(matcher);
323+
324+
const nsModuleContent = getFileContent(appTree, nsModulePath);
325+
expect(nsModuleContent).toMatch(matcher);
326+
});
192327
});
193328
})
194329
});

src/migrate-component/component-info-utils.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const parseComponentInfo = (options: MigrateComponentSchema) => (tree: Tr
2727
? options.name
2828
: classify(`${options.name}Component`);
2929

30-
// if no module provided and skipModule flag is on, then don't search for module path
30+
// if no module is provided and the skipModule flag is on, then don't search for module path
3131
const modulePath = (!options.module && options.skipModule) ? '' : findModulePath(options, tree);
3232

3333
const componentPath = findComponentPath(className, modulePath, options, tree);
@@ -123,7 +123,11 @@ const findComponentPath = (componentClassName: string, modulePath: string, optio
123123
const componentImportPath = findImportPath(source, componentClassName);
124124
console.log(`${componentClassName} import found in its module at: ${componentImportPath}`);
125125

126-
componentPath = join(dirname(modulePath), componentImportPath + '.ts')
126+
componentPath = join(dirname(modulePath), componentImportPath);
127+
if (!componentPath.endsWith('.ts')) {
128+
componentPath = componentPath + '.ts';
129+
}
130+
127131
if (tree.exists(componentPath)) {
128132
console.log(`${componentClassName} file found at: ${componentPath}`);
129133
} else {

0 commit comments

Comments
 (0)