Skip to content

Commit 249d462

Browse files
author
Robert Jackson
authored
Merge pull request #11 from ijlee2/support-nested-component-structure
2 parents 51379ef + 23766e2 commit 249d462

File tree

17 files changed

+1047
-146
lines changed

17 files changed

+1047
-146
lines changed

README.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,43 @@ cd your/project/path
1616
npx github:ember-codemods/ember-component-template-colocation-migrator
1717
```
1818

19+
By default, the migrator changes the **classic** component structure to the **flat** component structure.
20+
21+
```
22+
your-project-name
23+
├── app
24+
│ └── components
25+
│ ├── foo-bar
26+
│ │ ├── baz.hbs
27+
│ │ └── baz.js
28+
│ ├── foo-bar.hbs
29+
│ └── foo-bar.js
30+
│ ...
31+
```
32+
33+
If you want to change from **classic** to **nested**, you can add the `-ns` flag:
34+
35+
```sh
36+
cd your/project/path
37+
npx github:ember-codemods/ember-component-template-colocation-migrator -ns
38+
```
39+
40+
The nested component structure looks like:
41+
42+
```
43+
your-project-name
44+
├── app
45+
│ └── components
46+
│ └── foo-bar
47+
│ ├── baz
48+
│ │ ├── index.hbs
49+
│ │ └── index.js
50+
│ ├── index.hbs
51+
│ └── index.js
52+
│ ...
53+
```
54+
55+
1956
### Running Tests
2057

21-
* `npm run test`
58+
* `npm run test`

bin/ember-component-template-colocation-migrator

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,23 @@ let options = {
1414
let parsed = nopt(options);
1515
let projectRoot = parsed['project-root'] || process.cwd();
1616

17-
let migrator = new Migrator({ projectRoot });
17+
const { argv } = require('yargs');
18+
19+
// Allow passing the flag, -fs (flat) or -ns (nested), to specify component structure
20+
const changeToFlatStructure = argv.f && argv.s;
21+
const changeToNestedStructure = argv.n && argv.s;
22+
23+
let structure = 'flat';
24+
25+
if (changeToFlatStructure) {
26+
structure = 'flat';
27+
28+
} else if (changeToNestedStructure) {
29+
structure = 'nested';
30+
31+
}
32+
33+
let migrator = new Migrator({ projectRoot, structure });
1834

1935
migrator.execute().then(function() {
2036
console.log('Codemod finished successfully!');

lib/migrator.js

Lines changed: 157 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,53 +6,175 @@ const { moveFile, removeDirs } = require('./utils/file')
66

77
module.exports = class Migrator {
88
constructor(options) {
9-
this.options = options;
9+
const { projectRoot, structure } = options;
10+
11+
this.projectRoot = projectRoot;
12+
this.structure = structure;
1013
}
1114

12-
async execute() {
13-
let sourceComponentTemplatesPath = path.join(this.options.projectRoot, 'app/templates/components');
14-
var sourceComponentTemplateFilePaths = glob.sync(`${sourceComponentTemplatesPath}/**/*.hbs`);
15-
16-
let sourceComponentPath = path.join(this.options.projectRoot, 'app/components');
17-
let sourceComponentFilePaths = glob.sync(`${sourceComponentPath}/**/*.js`);
18-
let templatesWithLayoutName = getLayoutNameTemplates(sourceComponentFilePaths);
19-
if (templatesWithLayoutName.length) {
20-
sourceComponentTemplateFilePaths = sourceComponentTemplateFilePaths.filter(sourceTemplateFilePath => {
21-
let sourceTemplatePathInApp = sourceTemplateFilePath.slice(this.options.projectRoot.length); // '/app/templates/components/nested1/nested-component.hbs'
22-
let templatePath = sourceTemplatePathInApp.slice('app/templates/'.length); // '/nested1/nested-component.hbs'
23-
return !templatesWithLayoutName.includes(templatePath.slice(1).replace('.hbs', ''));
15+
findClassicComponentTemplates() {
16+
const templateFolderPath = path.join(this.projectRoot, 'app/templates/components');
17+
const templateFilePaths = glob.sync(`${templateFolderPath}/**/*.hbs`);
18+
19+
return templateFilePaths;
20+
}
21+
22+
findClassicComponentClasses() {
23+
const classFolderPath = path.join(this.projectRoot, 'app/components');
24+
const classFilePaths = glob.sync(`${classFolderPath}/**/*.{js,ts}`);
25+
26+
return classFilePaths;
27+
}
28+
29+
findTemplates() {
30+
const templateFolderPath = path.join(this.projectRoot, 'app/templates');
31+
const templateFilePaths = glob.sync(`${templateFolderPath}/**/*.hbs`);
32+
33+
return templateFilePaths;
34+
}
35+
36+
skipTemplatesUsedAsLayoutName(templateFilePaths) {
37+
console.info(`\nChecking if any component templates are used as templates of other components using \`layoutName\``);
38+
39+
const classFilePaths = this.findClassicComponentClasses();
40+
const componentsWithLayoutName = getLayoutNameTemplates(classFilePaths);
41+
42+
if (componentsWithLayoutName.length) {
43+
componentsWithLayoutName.sort().forEach(component => {
44+
console.info(`❌ Did not move '${component}' due to usage as "layoutName" in a component`);
45+
});
46+
47+
templateFilePaths = templateFilePaths.filter(templateFilePath => {
48+
// Extract '/app/templates/components/nested1/nested-component.hbs'
49+
const filePathFromApp = templateFilePath.slice(this.projectRoot.length);
50+
51+
// Extract '/components/nested1/nested-component.hbs'
52+
const filePathFromAppTemplates = filePathFromApp.slice('app/templates/'.length);
53+
54+
// Extract 'components/nested1/nested-component'
55+
const classFilePath = filePathFromAppTemplates.slice(1).replace('.hbs', '');
56+
57+
return !componentsWithLayoutName.includes(classFilePath);
2458
});
2559
}
2660

27-
let sourceTemplatesPath = path.join(this.options.projectRoot, 'app/templates');
28-
var sourceTemplateFilePaths = glob.sync(`${sourceTemplatesPath}/**/*.hbs`);
29-
let templatesInPartials = getPartialTemplates(sourceTemplateFilePaths);
30-
if (templatesInPartials.length) {
31-
sourceComponentTemplateFilePaths = sourceComponentTemplateFilePaths.filter(sourceTemplateFilePath => {
32-
let sourceTemplatePathInApp = sourceTemplateFilePath.slice(this.options.projectRoot.length); // '/app/templates/components/nested1/nested-component.hbs'
33-
if (/\/\-[\w\-]+\.hbs/.test(sourceTemplatePathInApp)) {
34-
sourceTemplatePathInApp = sourceTemplatePathInApp.replace('/-', '/');
61+
return templateFilePaths;
62+
}
63+
64+
skipTemplatesUsedAsPartial(templateFilePaths) {
65+
console.info(`\nChecking if any component templates are used as partials`);
66+
67+
const componentsWithPartial = getPartialTemplates(this.findTemplates());
68+
69+
if (componentsWithPartial.length) {
70+
componentsWithPartial.sort().forEach(component => {
71+
console.info(`❌ Did not move '${component}' due to usage as a "partial"`);
72+
});
73+
74+
templateFilePaths = templateFilePaths.filter(templateFilePath => {
75+
// Extract '/app/templates/components/nested1/nested-component.hbs'
76+
let filePathFromApp = templateFilePath.slice(this.projectRoot.length);
77+
78+
/*
79+
When Ember sees `{{partial "foo"}}`, it will look for the template in
80+
two locations:
81+
82+
- `app/templates/foo.hbs`
83+
- `app/templates/-foo.hbs`
84+
85+
If `filePathFromApp` matches the latter pattern, we remove the hyphen.
86+
*/
87+
if (/\/\-[\w\-]+\.hbs/.test(filePathFromApp)) {
88+
filePathFromApp = filePathFromApp.replace('/-', '/');
3589
}
36-
let templatePath = sourceTemplatePathInApp.slice('app/templates/'.length); // '/nested1/nested-component.hbs'
37-
return !templatesInPartials.includes(templatePath.slice(1).replace('.hbs', ''));
90+
91+
// Extract '/components/nested1/nested-component.hbs'
92+
const filePathFromAppTemplates = filePathFromApp.slice('app/templates/'.length);
93+
94+
// Extract 'components/nested1/nested-component'
95+
const classFilePath = filePathFromAppTemplates.slice(1).replace('.hbs', '');
96+
97+
return !componentsWithPartial.includes(classFilePath);
3898
});
3999
}
40100

41-
sourceComponentTemplateFilePaths.forEach(sourceTemplateFilePath => {
42-
let sourceTemplatePathInApp = sourceTemplateFilePath.slice(this.options.projectRoot.length); // '/app/templates/components/nested1/nested-component.hbs'
43-
let templatePath = sourceTemplatePathInApp.slice('app/templates/components/'.length); // '/nested1/nested-component.hbs'
44-
let targetTemplateFilePath = path.join(this.options.projectRoot, 'app/components', templatePath); // '[APP_PATH]/app/components/nested1/nested-component.hbs'
45-
moveFile(sourceTemplateFilePath, targetTemplateFilePath);
46-
});
101+
return templateFilePaths;
102+
}
103+
104+
changeComponentStructureToFlat(templateFilePaths) {
105+
templateFilePaths.forEach(templateFilePath => {
106+
// Extract '/app/templates/components/nested1/nested-component.hbs'
107+
const filePathFromApp = templateFilePath.slice(this.projectRoot.length);
108+
109+
// Extract '/nested1/nested-component.hbs'
110+
const filePathFromAppTemplatesComponents = filePathFromApp.slice('app/templates/components/'.length);
47111

48-
templatesWithLayoutName.sort().forEach(template => {
49-
console.info(`❌ Did not move '${template}' due to usage as "layoutName" in a component`);
112+
// '[APP_PATH]/app/components/nested1/nested-component.hbs'
113+
const newTemplateFilePath = path.join(this.projectRoot, 'app/components', filePathFromAppTemplatesComponents);
114+
moveFile(templateFilePath, newTemplateFilePath);
50115
});
51-
templatesInPartials.sort().forEach(template => {
52-
console.info(`❌ Did not move '${template}' due to usage as a "partial"`);
116+
}
117+
118+
changeComponentStructureToNested(templateFilePaths) {
119+
const classFilePaths = this.findClassicComponentClasses();
120+
121+
templateFilePaths.forEach(templateFilePath => {
122+
// Extract '/app/templates/components/nested1/nested-component.hbs'
123+
const filePathFromApp = templateFilePath.slice(this.projectRoot.length);
124+
125+
// Extract '/nested1/nested-component.hbs'
126+
const filePathFromAppTemplatesComponents = filePathFromApp.slice('app/templates/components/'.length);
127+
const fileExtension = path.extname(filePathFromAppTemplatesComponents);
128+
129+
// Extract '/nested1/nested-component'
130+
const targetPath = filePathFromAppTemplatesComponents.slice(0, -fileExtension.length);
131+
132+
// Build '[APP_PATH]/app/components/nested1/nested-component/index.hbs'
133+
const newTemplateFilePath = path.join(this.projectRoot, 'app/components', targetPath, 'index.hbs');
134+
moveFile(templateFilePath, newTemplateFilePath);
135+
136+
// Build '[APP_PATH]/app/components/nested1/nested-component/index.js'
137+
const classFilePath = {
138+
js: path.join(this.projectRoot, 'app/components', `${targetPath}.js`),
139+
ts: path.join(this.projectRoot, 'app/components', `${targetPath}.ts`)
140+
};
141+
142+
if (classFilePaths.includes(classFilePath.js)) {
143+
const newClassFilePath = path.join(this.projectRoot, 'app/components', targetPath, 'index.js');
144+
moveFile(classFilePath.js, newClassFilePath);
145+
146+
} else if (classFilePaths.includes(classFilePath.ts)) {
147+
const newClassFilePath = path.join(this.projectRoot, 'app/components', targetPath, 'index.ts');
148+
moveFile(classFilePath.ts, newClassFilePath);
149+
150+
}
53151
});
152+
}
153+
154+
async removeEmptyClassicComponentDirectories() {
155+
const templateFolderPath = path.join(this.projectRoot, 'app/templates/components');
156+
157+
const classFilePaths = this.findClassicComponentClasses();
158+
const templatesWithLayoutName = getLayoutNameTemplates(classFilePaths);
159+
const removeOnlyEmptyDirectories = Boolean(templatesWithLayoutName.length);
160+
161+
await removeDirs(templateFolderPath, removeOnlyEmptyDirectories);
162+
}
163+
164+
async execute() {
165+
let templateFilePaths = this.findClassicComponentTemplates();
166+
templateFilePaths = this.skipTemplatesUsedAsLayoutName(templateFilePaths);
167+
templateFilePaths = this.skipTemplatesUsedAsPartial(templateFilePaths);
168+
169+
if (this.structure === 'flat') {
170+
this.changeComponentStructureToFlat(templateFilePaths);
171+
172+
} else if (this.structure === 'nested') {
173+
this.changeComponentStructureToNested(templateFilePaths);
174+
175+
}
54176

55-
let onlyRemoveEmptyDirs = Boolean(templatesWithLayoutName.length);
56-
await removeDirs(sourceComponentTemplatesPath, onlyRemoveEmptyDirs);
177+
// Clean up
178+
await this.removeEmptyClassicComponentDirectories();
57179
}
58180
}

lib/utils/templates.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ const jsTraverse = require('@babel/traverse').default;
44
const { parse, traverse } = require('ember-template-recast');
55

66
function getLayoutNameTemplates(files) {
7-
console.info(`Checking if any component templates are used as templates of other components using \`layoutName\``);
87
let names = files.map(file => {
98
let content = readFileSync(file, 'utf8');
109
return fileInLayoutName(content);
@@ -33,7 +32,6 @@ function fileInLayoutName(content) {
3332
}
3433

3534
function getPartialTemplates(files) {
36-
console.info(`Checking if any component templates are used as partials`);
3735
let names = files.reduce((acc, file) => {
3836
let content = readFileSync(file, 'utf8');
3937
let partials = filesInPartials(content);

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"fs-extra": "^7.0.1",
2929
"glob": "^7.1.4",
3030
"nopt": "^4.0.1",
31-
"remove-empty-directories": "^0.0.1"
31+
"remove-empty-directories": "^0.0.1",
32+
"yargs": "^15.3.1"
3233
},
3334
"repository": {
3435
"type": "git",

test/fixtures/classic-app/input.js

Lines changed: 0 additions & 38 deletions
This file was deleted.

test/fixtures/classic-app/output.js

Lines changed: 0 additions & 36 deletions
This file was deleted.

0 commit comments

Comments
 (0)