Skip to content

Commit

Permalink
Merge pull request #11 from ijlee2/support-nested-component-structure
Browse files Browse the repository at this point in the history
  • Loading branch information
rwjblue authored Apr 29, 2020
2 parents 51379ef + 23766e2 commit 249d462
Show file tree
Hide file tree
Showing 17 changed files with 1,047 additions and 146 deletions.
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,43 @@ cd your/project/path
npx github:ember-codemods/ember-component-template-colocation-migrator
```

By default, the migrator changes the **classic** component structure to the **flat** component structure.

```
your-project-name
├── app
│ └── components
│ ├── foo-bar
│ │ ├── baz.hbs
│ │ └── baz.js
│ ├── foo-bar.hbs
│ └── foo-bar.js
│ ...
```

If you want to change from **classic** to **nested**, you can add the `-ns` flag:

```sh
cd your/project/path
npx github:ember-codemods/ember-component-template-colocation-migrator -ns
```

The nested component structure looks like:

```
your-project-name
├── app
│ └── components
│ └── foo-bar
│ ├── baz
│ │ ├── index.hbs
│ │ └── index.js
│ ├── index.hbs
│ └── index.js
│ ...
```


### Running Tests

* `npm run test`
* `npm run test`
18 changes: 17 additions & 1 deletion bin/ember-component-template-colocation-migrator
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,23 @@ let options = {
let parsed = nopt(options);
let projectRoot = parsed['project-root'] || process.cwd();

let migrator = new Migrator({ projectRoot });
const { argv } = require('yargs');

// Allow passing the flag, -fs (flat) or -ns (nested), to specify component structure
const changeToFlatStructure = argv.f && argv.s;
const changeToNestedStructure = argv.n && argv.s;

let structure = 'flat';

if (changeToFlatStructure) {
structure = 'flat';

} else if (changeToNestedStructure) {
structure = 'nested';

}

let migrator = new Migrator({ projectRoot, structure });

migrator.execute().then(function() {
console.log('Codemod finished successfully!');
Expand Down
192 changes: 157 additions & 35 deletions lib/migrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,175 @@ const { moveFile, removeDirs } = require('./utils/file')

module.exports = class Migrator {
constructor(options) {
this.options = options;
const { projectRoot, structure } = options;

this.projectRoot = projectRoot;
this.structure = structure;
}

async execute() {
let sourceComponentTemplatesPath = path.join(this.options.projectRoot, 'app/templates/components');
var sourceComponentTemplateFilePaths = glob.sync(`${sourceComponentTemplatesPath}/**/*.hbs`);

let sourceComponentPath = path.join(this.options.projectRoot, 'app/components');
let sourceComponentFilePaths = glob.sync(`${sourceComponentPath}/**/*.js`);
let templatesWithLayoutName = getLayoutNameTemplates(sourceComponentFilePaths);
if (templatesWithLayoutName.length) {
sourceComponentTemplateFilePaths = sourceComponentTemplateFilePaths.filter(sourceTemplateFilePath => {
let sourceTemplatePathInApp = sourceTemplateFilePath.slice(this.options.projectRoot.length); // '/app/templates/components/nested1/nested-component.hbs'
let templatePath = sourceTemplatePathInApp.slice('app/templates/'.length); // '/nested1/nested-component.hbs'
return !templatesWithLayoutName.includes(templatePath.slice(1).replace('.hbs', ''));
findClassicComponentTemplates() {
const templateFolderPath = path.join(this.projectRoot, 'app/templates/components');
const templateFilePaths = glob.sync(`${templateFolderPath}/**/*.hbs`);

return templateFilePaths;
}

findClassicComponentClasses() {
const classFolderPath = path.join(this.projectRoot, 'app/components');
const classFilePaths = glob.sync(`${classFolderPath}/**/*.{js,ts}`);

return classFilePaths;
}

findTemplates() {
const templateFolderPath = path.join(this.projectRoot, 'app/templates');
const templateFilePaths = glob.sync(`${templateFolderPath}/**/*.hbs`);

return templateFilePaths;
}

skipTemplatesUsedAsLayoutName(templateFilePaths) {
console.info(`\nChecking if any component templates are used as templates of other components using \`layoutName\``);

const classFilePaths = this.findClassicComponentClasses();
const componentsWithLayoutName = getLayoutNameTemplates(classFilePaths);

if (componentsWithLayoutName.length) {
componentsWithLayoutName.sort().forEach(component => {
console.info(`❌ Did not move '${component}' due to usage as "layoutName" in a component`);
});

templateFilePaths = templateFilePaths.filter(templateFilePath => {
// Extract '/app/templates/components/nested1/nested-component.hbs'
const filePathFromApp = templateFilePath.slice(this.projectRoot.length);

// Extract '/components/nested1/nested-component.hbs'
const filePathFromAppTemplates = filePathFromApp.slice('app/templates/'.length);

// Extract 'components/nested1/nested-component'
const classFilePath = filePathFromAppTemplates.slice(1).replace('.hbs', '');

return !componentsWithLayoutName.includes(classFilePath);
});
}

let sourceTemplatesPath = path.join(this.options.projectRoot, 'app/templates');
var sourceTemplateFilePaths = glob.sync(`${sourceTemplatesPath}/**/*.hbs`);
let templatesInPartials = getPartialTemplates(sourceTemplateFilePaths);
if (templatesInPartials.length) {
sourceComponentTemplateFilePaths = sourceComponentTemplateFilePaths.filter(sourceTemplateFilePath => {
let sourceTemplatePathInApp = sourceTemplateFilePath.slice(this.options.projectRoot.length); // '/app/templates/components/nested1/nested-component.hbs'
if (/\/\-[\w\-]+\.hbs/.test(sourceTemplatePathInApp)) {
sourceTemplatePathInApp = sourceTemplatePathInApp.replace('/-', '/');
return templateFilePaths;
}

skipTemplatesUsedAsPartial(templateFilePaths) {
console.info(`\nChecking if any component templates are used as partials`);

const componentsWithPartial = getPartialTemplates(this.findTemplates());

if (componentsWithPartial.length) {
componentsWithPartial.sort().forEach(component => {
console.info(`❌ Did not move '${component}' due to usage as a "partial"`);
});

templateFilePaths = templateFilePaths.filter(templateFilePath => {
// Extract '/app/templates/components/nested1/nested-component.hbs'
let filePathFromApp = templateFilePath.slice(this.projectRoot.length);

/*
When Ember sees `{{partial "foo"}}`, it will look for the template in
two locations:
- `app/templates/foo.hbs`
- `app/templates/-foo.hbs`
If `filePathFromApp` matches the latter pattern, we remove the hyphen.
*/
if (/\/\-[\w\-]+\.hbs/.test(filePathFromApp)) {
filePathFromApp = filePathFromApp.replace('/-', '/');
}
let templatePath = sourceTemplatePathInApp.slice('app/templates/'.length); // '/nested1/nested-component.hbs'
return !templatesInPartials.includes(templatePath.slice(1).replace('.hbs', ''));

// Extract '/components/nested1/nested-component.hbs'
const filePathFromAppTemplates = filePathFromApp.slice('app/templates/'.length);

// Extract 'components/nested1/nested-component'
const classFilePath = filePathFromAppTemplates.slice(1).replace('.hbs', '');

return !componentsWithPartial.includes(classFilePath);
});
}

sourceComponentTemplateFilePaths.forEach(sourceTemplateFilePath => {
let sourceTemplatePathInApp = sourceTemplateFilePath.slice(this.options.projectRoot.length); // '/app/templates/components/nested1/nested-component.hbs'
let templatePath = sourceTemplatePathInApp.slice('app/templates/components/'.length); // '/nested1/nested-component.hbs'
let targetTemplateFilePath = path.join(this.options.projectRoot, 'app/components', templatePath); // '[APP_PATH]/app/components/nested1/nested-component.hbs'
moveFile(sourceTemplateFilePath, targetTemplateFilePath);
});
return templateFilePaths;
}

changeComponentStructureToFlat(templateFilePaths) {
templateFilePaths.forEach(templateFilePath => {
// Extract '/app/templates/components/nested1/nested-component.hbs'
const filePathFromApp = templateFilePath.slice(this.projectRoot.length);

// Extract '/nested1/nested-component.hbs'
const filePathFromAppTemplatesComponents = filePathFromApp.slice('app/templates/components/'.length);

templatesWithLayoutName.sort().forEach(template => {
console.info(`❌ Did not move '${template}' due to usage as "layoutName" in a component`);
// '[APP_PATH]/app/components/nested1/nested-component.hbs'
const newTemplateFilePath = path.join(this.projectRoot, 'app/components', filePathFromAppTemplatesComponents);
moveFile(templateFilePath, newTemplateFilePath);
});
templatesInPartials.sort().forEach(template => {
console.info(`❌ Did not move '${template}' due to usage as a "partial"`);
}

changeComponentStructureToNested(templateFilePaths) {
const classFilePaths = this.findClassicComponentClasses();

templateFilePaths.forEach(templateFilePath => {
// Extract '/app/templates/components/nested1/nested-component.hbs'
const filePathFromApp = templateFilePath.slice(this.projectRoot.length);

// Extract '/nested1/nested-component.hbs'
const filePathFromAppTemplatesComponents = filePathFromApp.slice('app/templates/components/'.length);
const fileExtension = path.extname(filePathFromAppTemplatesComponents);

// Extract '/nested1/nested-component'
const targetPath = filePathFromAppTemplatesComponents.slice(0, -fileExtension.length);

// Build '[APP_PATH]/app/components/nested1/nested-component/index.hbs'
const newTemplateFilePath = path.join(this.projectRoot, 'app/components', targetPath, 'index.hbs');
moveFile(templateFilePath, newTemplateFilePath);

// Build '[APP_PATH]/app/components/nested1/nested-component/index.js'
const classFilePath = {
js: path.join(this.projectRoot, 'app/components', `${targetPath}.js`),
ts: path.join(this.projectRoot, 'app/components', `${targetPath}.ts`)
};

if (classFilePaths.includes(classFilePath.js)) {
const newClassFilePath = path.join(this.projectRoot, 'app/components', targetPath, 'index.js');
moveFile(classFilePath.js, newClassFilePath);

} else if (classFilePaths.includes(classFilePath.ts)) {
const newClassFilePath = path.join(this.projectRoot, 'app/components', targetPath, 'index.ts');
moveFile(classFilePath.ts, newClassFilePath);

}
});
}

async removeEmptyClassicComponentDirectories() {
const templateFolderPath = path.join(this.projectRoot, 'app/templates/components');

const classFilePaths = this.findClassicComponentClasses();
const templatesWithLayoutName = getLayoutNameTemplates(classFilePaths);
const removeOnlyEmptyDirectories = Boolean(templatesWithLayoutName.length);

await removeDirs(templateFolderPath, removeOnlyEmptyDirectories);
}

async execute() {
let templateFilePaths = this.findClassicComponentTemplates();
templateFilePaths = this.skipTemplatesUsedAsLayoutName(templateFilePaths);
templateFilePaths = this.skipTemplatesUsedAsPartial(templateFilePaths);

if (this.structure === 'flat') {
this.changeComponentStructureToFlat(templateFilePaths);

} else if (this.structure === 'nested') {
this.changeComponentStructureToNested(templateFilePaths);

}

let onlyRemoveEmptyDirs = Boolean(templatesWithLayoutName.length);
await removeDirs(sourceComponentTemplatesPath, onlyRemoveEmptyDirs);
// Clean up
await this.removeEmptyClassicComponentDirectories();
}
}
2 changes: 0 additions & 2 deletions lib/utils/templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const jsTraverse = require('@babel/traverse').default;
const { parse, traverse } = require('ember-template-recast');

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

function getPartialTemplates(files) {
console.info(`Checking if any component templates are used as partials`);
let names = files.reduce((acc, file) => {
let content = readFileSync(file, 'utf8');
let partials = filesInPartials(content);
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"fs-extra": "^7.0.1",
"glob": "^7.1.4",
"nopt": "^4.0.1",
"remove-empty-directories": "^0.0.1"
"remove-empty-directories": "^0.0.1",
"yargs": "^15.3.1"
},
"repository": {
"type": "git",
Expand Down
38 changes: 0 additions & 38 deletions test/fixtures/classic-app/input.js

This file was deleted.

36 changes: 0 additions & 36 deletions test/fixtures/classic-app/output.js

This file was deleted.

Loading

0 comments on commit 249d462

Please sign in to comment.