ember-codemod-remove-ember-css-modules 0.1.4 → 0.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +6 -1
  2. package/{bin → dist/bin}/ember-codemod-remove-ember-css-modules.js +9 -17
  3. package/{src → dist/src}/index.js +1 -2
  4. package/dist/src/migration/ember-app/index.js +21 -0
  5. package/dist/src/migration/ember-app/steps/analyze-app.js +98 -0
  6. package/dist/src/migration/ember-app/steps/create-options.js +24 -0
  7. package/dist/src/migration/ember-app/steps/import-styles-in-components.js +35 -0
  8. package/dist/src/migration/ember-app/steps/import-styles-in-routes.js +32 -0
  9. package/dist/src/migration/ember-app/steps/move-stylesheets.js +35 -0
  10. package/dist/src/migration/ember-app/steps/shared/import-styles.js +136 -0
  11. package/dist/src/migration/ember-app/steps/shared/update-templates.js +354 -0
  12. package/dist/src/migration/ember-app/steps/update-app-app-js.js +46 -0
  13. package/dist/src/migration/ember-app/steps/update-component-templates.js +22 -0
  14. package/dist/src/migration/ember-app/steps/update-package-json.js +38 -0
  15. package/dist/src/migration/ember-app/steps/update-route-templates.js +18 -0
  16. package/dist/src/types/index.js +1 -0
  17. package/dist/src/utils/blueprints/blueprints-root.js +4 -0
  18. package/dist/src/utils/blueprints/get-version.js +13 -0
  19. package/dist/src/utils/string/ember-cli-string-utils/camelize.js +11 -0
  20. package/dist/src/utils/string/ember-cli-string-utils/classify.js +10 -0
  21. package/{src → dist/src}/utils/string/entity-name/classify.js +1 -2
  22. package/{src → dist/src}/utils/string/entity-name/double-colonize.js +3 -6
  23. package/dist/src/utils/string/entity-name/parse.js +15 -0
  24. package/package.json +30 -25
  25. package/src/migration/ember-app/index.js +0 -37
  26. package/src/migration/ember-app/steps/analyze-app.js +0 -155
  27. package/src/migration/ember-app/steps/create-options.js +0 -44
  28. package/src/migration/ember-app/steps/import-styles-in-components.js +0 -43
  29. package/src/migration/ember-app/steps/import-styles-in-routes.js +0 -39
  30. package/src/migration/ember-app/steps/move-stylesheets.js +0 -51
  31. package/src/migration/ember-app/steps/shared/import-styles.js +0 -210
  32. package/src/migration/ember-app/steps/shared/index.js +0 -2
  33. package/src/migration/ember-app/steps/shared/update-templates.js +0 -438
  34. package/src/migration/ember-app/steps/update-app-app-js.js +0 -65
  35. package/src/migration/ember-app/steps/update-component-templates.js +0 -28
  36. package/src/migration/ember-app/steps/update-package-json.js +0 -59
  37. package/src/migration/ember-app/steps/update-route-templates.js +0 -22
  38. package/src/utils/abstract-syntax-tree/handlebars.js +0 -20
  39. package/src/utils/abstract-syntax-tree/javascript.js +0 -103
  40. package/src/utils/abstract-syntax-tree.js +0 -2
  41. package/src/utils/blueprints/blueprints-root.js +0 -8
  42. package/src/utils/blueprints/get-version.js +0 -16
  43. package/src/utils/string/ember-cli-string-utils/camelize.js +0 -12
  44. package/src/utils/string/ember-cli-string-utils/classify.js +0 -14
  45. package/src/utils/string/entity-name/parse.js +0 -19
  46. /package/{src/blueprints → blueprints}/ember-cli/component/javascript.js +0 -0
  47. /package/{src/blueprints → blueprints}/ember-cli/component/typescript.ts +0 -0
  48. /package/{src/blueprints → blueprints}/ember-cli/controller/javascript.js +0 -0
  49. /package/{src/blueprints → blueprints}/ember-cli/controller/typescript.ts +0 -0
  50. /package/{src → dist/src}/migration/ember-app/steps/index.js +0 -0
  51. /package/{src → dist/src}/migration/index.js +0 -0
  52. /package/{src → dist/src}/utils/blueprints.js +0 -0
  53. /package/{src → dist/src}/utils/string.js +0 -0
package/README.md CHANGED
@@ -67,7 +67,12 @@ To better meet your needs, consider cloning the repo and running the codemod loc
67
67
 
68
68
  ```sh
69
69
  cd <path/to/cloned/repo>
70
- ./bin/ember-codemod-remove-ember-css-modules.js --root=<path/to/your/project>
70
+
71
+ # Compile TypeScript
72
+ pnpm build
73
+
74
+ # Run codemod
75
+ ./dist/bin/ember-codemod-remove-ember-css-modules.js --root=<path/to/your/project>
71
76
  ```
72
77
 
73
78
 
@@ -1,34 +1,26 @@
1
1
  #!/usr/bin/env node
2
+ // eslint-disable-next-line n/shebang
2
3
  'use strict';
3
-
4
4
  import yargs from 'yargs';
5
5
  import { hideBin } from 'yargs/helpers';
6
-
7
6
  import { removeEmberCssModules } from '../src/index.js';
8
-
9
7
  // Provide a title to the process in `ps`
10
8
  process.title = 'ember-codemod-remove-ember-css-modules';
11
-
12
9
  // Set codemod options
13
- const { argv } = yargs(hideBin(process.argv))
14
- .option('component-structure', {
10
+ const argv = yargs(hideBin(process.argv))
11
+ .option('component-structure', {
15
12
  choices: ['flat', 'nested'],
16
13
  default: 'flat',
17
14
  describe: 'Component structure (how your components are colocated)',
18
15
  type: 'string',
19
- })
20
- .option('root', {
16
+ })
17
+ .option('root', {
21
18
  describe: 'Location of your Ember project',
22
19
  type: 'string',
23
- });
24
-
25
- function castEmptyStringToUndefined(string) {
26
- return string === '' ? undefined : string;
27
- }
28
-
20
+ })
21
+ .parseSync();
29
22
  const codemodOptions = {
30
- componentStructure: castEmptyStringToUndefined(argv['component-structure']),
31
- projectRoot: argv['root'] ?? process.cwd(),
23
+ componentStructure: argv['component-structure'],
24
+ projectRoot: argv['root'] ?? process.cwd(),
32
25
  };
33
-
34
26
  removeEmberCssModules(codemodOptions);
@@ -1,5 +1,4 @@
1
1
  import { migrateEmberApp } from './migration/index.js';
2
-
3
2
  export function removeEmberCssModules(codemodOptions) {
4
- migrateEmberApp(codemodOptions);
3
+ migrateEmberApp(codemodOptions);
5
4
  }
@@ -0,0 +1,21 @@
1
+ import { analyzeApp, createOptions, importStylesInComponents, importStylesInRoutes, moveStylesheets, updateAppAppJs, updateComponentTemplates, updatePackageJson, updateRouteTemplates, } from './steps/index.js';
2
+ export function migrateEmberApp(codemodOptions) {
3
+ const options = createOptions(codemodOptions);
4
+ const { hasEmberCssModules } = options.project;
5
+ // Guarantee idempotency
6
+ if (!hasEmberCssModules) {
7
+ return;
8
+ }
9
+ // Prepare for migration
10
+ const context = analyzeApp(options);
11
+ // Import styles in classes
12
+ moveStylesheets(options);
13
+ importStylesInComponents(context, options);
14
+ importStylesInRoutes(context, options);
15
+ // Update templates
16
+ updateComponentTemplates(context, options);
17
+ updateRouteTemplates(context, options);
18
+ // Fine-tune individual files
19
+ updateAppAppJs(options);
20
+ updatePackageJson(options);
21
+ }
@@ -0,0 +1,98 @@
1
+ import { join, parse } from 'node:path';
2
+ import { findFiles, renamePathByDirectory } from '@codemod-utils/files';
3
+ function analyzeFilePaths(filePaths) {
4
+ const entities = new Map();
5
+ filePaths.forEach((filePath) => {
6
+ const { dir, ext, name } = parse(filePath);
7
+ const entityName = join(dir, name);
8
+ if (entities.has(entityName)) {
9
+ entities.get(entityName).add(ext);
10
+ return;
11
+ }
12
+ entities.set(entityName, new Set([ext]));
13
+ });
14
+ return entities;
15
+ }
16
+ function analyzeComponents(options) {
17
+ const { componentStructure, projectRoot } = options;
18
+ const classFilePaths = findFiles('app/components/**/*.{js,ts}', {
19
+ ignoreList: ['app/components/**/*.d.ts'],
20
+ projectRoot,
21
+ }).map((filePath) => {
22
+ return renamePathByDirectory(filePath, {
23
+ from: 'app/components',
24
+ to: '',
25
+ });
26
+ });
27
+ const stylesheetFilePaths = findFiles('app/components/**/*.css', {
28
+ projectRoot,
29
+ }).map((filePath) => {
30
+ return renamePathByDirectory(filePath, {
31
+ from: 'app/components',
32
+ to: '',
33
+ });
34
+ });
35
+ const templateFilePaths = findFiles('app/components/**/*.hbs', {
36
+ projectRoot,
37
+ }).map((filePath) => {
38
+ return renamePathByDirectory(filePath, {
39
+ from: 'app/components',
40
+ to: '',
41
+ });
42
+ });
43
+ const filePaths = [
44
+ ...classFilePaths,
45
+ ...stylesheetFilePaths,
46
+ ...templateFilePaths,
47
+ ].sort();
48
+ const entities = analyzeFilePaths(filePaths);
49
+ if (componentStructure === 'nested') {
50
+ return new Map(Array.from(entities.entries()).map(([entityName, extensions]) => {
51
+ const newEntityName = entityName.replace(/\/index$/, '');
52
+ return [newEntityName, extensions];
53
+ }));
54
+ }
55
+ return entities;
56
+ }
57
+ function analyzeRoutes(options) {
58
+ const { projectRoot } = options;
59
+ const classFilePaths = findFiles('app/controllers/**/*.{js,ts}', {
60
+ ignoreList: ['app/controllers/**/*.d.ts'],
61
+ projectRoot,
62
+ }).map((filePath) => {
63
+ return renamePathByDirectory(filePath, {
64
+ from: 'app/controllers',
65
+ to: '',
66
+ });
67
+ });
68
+ const stylesheetFilePaths = findFiles('app/styles/**/*.css', {
69
+ ignoreList: ['app/styles/app.css'],
70
+ projectRoot,
71
+ }).map((filePath) => {
72
+ return renamePathByDirectory(filePath, {
73
+ from: 'app/styles',
74
+ to: '',
75
+ });
76
+ });
77
+ const templateFilePaths = findFiles('app/templates/**/*.hbs', {
78
+ ignoreList: ['app/templates/components/**/*'],
79
+ projectRoot,
80
+ }).map((filePath) => {
81
+ return renamePathByDirectory(filePath, {
82
+ from: 'app/templates',
83
+ to: '',
84
+ });
85
+ });
86
+ const filePaths = [
87
+ ...classFilePaths,
88
+ ...stylesheetFilePaths,
89
+ ...templateFilePaths,
90
+ ].sort();
91
+ return analyzeFilePaths(filePaths);
92
+ }
93
+ export function analyzeApp(options) {
94
+ return {
95
+ components: analyzeComponents(options),
96
+ routes: analyzeRoutes(options),
97
+ };
98
+ }
@@ -0,0 +1,24 @@
1
+ import { readPackageJson } from '@codemod-utils/json';
2
+ function analyzePackageJson(codemodOptions) {
3
+ const { dependencies, devDependencies } = readPackageJson(codemodOptions);
4
+ const projectDependencies = new Map([
5
+ ...Object.entries(dependencies ?? {}),
6
+ ...Object.entries(devDependencies ?? {}),
7
+ ]);
8
+ return {
9
+ dependencies: projectDependencies,
10
+ hasEmberCssModules: projectDependencies.has('ember-css-modules'),
11
+ hasGlint: projectDependencies.has('@glint/core'),
12
+ hasTypeScript: projectDependencies.has('typescript'),
13
+ };
14
+ }
15
+ export function createOptions(codemodOptions) {
16
+ const project = analyzePackageJson(codemodOptions);
17
+ const options = {
18
+ __styles__: 'styles',
19
+ componentStructure: codemodOptions.componentStructure,
20
+ project,
21
+ projectRoot: codemodOptions.projectRoot,
22
+ };
23
+ return options;
24
+ }
@@ -0,0 +1,35 @@
1
+ import { join } from 'node:path';
2
+ import { importStyles } from './shared/import-styles.js';
3
+ function getBlueprintFilePaths(options) {
4
+ const { project } = options;
5
+ if (project.hasTypeScript) {
6
+ return ['ember-cli/component/typescript.ts'];
7
+ }
8
+ return ['ember-cli/component/javascript.js'];
9
+ }
10
+ function getFilePath(options) {
11
+ const { componentStructure, project } = options;
12
+ return function (entityName) {
13
+ let filePath = join('app/components', entityName);
14
+ if (componentStructure === 'nested') {
15
+ filePath += '/index';
16
+ }
17
+ if (project.hasTypeScript) {
18
+ filePath += '.ts';
19
+ }
20
+ else {
21
+ filePath += '.js';
22
+ }
23
+ return filePath;
24
+ };
25
+ }
26
+ export function importStylesInComponents(context, options) {
27
+ const customizations = {
28
+ blueprintFilePaths: getBlueprintFilePaths(options),
29
+ getFilePath: getFilePath(options),
30
+ };
31
+ importStyles(context.components, {
32
+ customizations,
33
+ options,
34
+ });
35
+ }
@@ -0,0 +1,32 @@
1
+ import { join } from 'node:path';
2
+ import { importStyles } from './shared/import-styles.js';
3
+ function getBlueprintFilePaths(options) {
4
+ const { project } = options;
5
+ if (project.hasTypeScript) {
6
+ return ['ember-cli/controller/typescript.ts'];
7
+ }
8
+ return ['ember-cli/controller/javascript.js'];
9
+ }
10
+ function getFilePath(options) {
11
+ const { project } = options;
12
+ return function (entityName) {
13
+ let filePath = join('app/controllers', entityName);
14
+ if (project.hasTypeScript) {
15
+ filePath += '.ts';
16
+ }
17
+ else {
18
+ filePath += '.js';
19
+ }
20
+ return filePath;
21
+ };
22
+ }
23
+ export function importStylesInRoutes(context, options) {
24
+ const customizations = {
25
+ blueprintFilePaths: getBlueprintFilePaths(options),
26
+ getFilePath: getFilePath(options),
27
+ };
28
+ importStyles(context.routes, {
29
+ customizations,
30
+ options,
31
+ });
32
+ }
@@ -0,0 +1,35 @@
1
+ import { createFiles, findFiles, mapFilePaths, moveFiles, } from '@codemod-utils/files';
2
+ function moveRouteStylesheets(options) {
3
+ const { projectRoot } = options;
4
+ const filePaths = findFiles('app/styles/**/*.css', {
5
+ ignoreList: ['app/styles/app.css'],
6
+ projectRoot,
7
+ });
8
+ const pathMapping = mapFilePaths(filePaths, {
9
+ from: 'app/styles',
10
+ to: 'app/controllers',
11
+ });
12
+ moveFiles(pathMapping, options);
13
+ }
14
+ function moveAppCssToAssets(options) {
15
+ const { projectRoot } = options;
16
+ const filePaths = findFiles('app/styles/app.css', {
17
+ projectRoot,
18
+ });
19
+ const pathMapping = mapFilePaths(filePaths, {
20
+ from: 'app/styles',
21
+ to: 'app/assets',
22
+ });
23
+ moveFiles(pathMapping, options);
24
+ const fileMap = new Map([
25
+ [
26
+ 'app/styles/app.css',
27
+ '/* Ember supports plain CSS out of the box. More info: https://cli.emberjs.com/release/advanced-use/stylesheets/ */\n',
28
+ ],
29
+ ]);
30
+ createFiles(fileMap, options);
31
+ }
32
+ export function moveStylesheets(options) {
33
+ moveRouteStylesheets(options);
34
+ moveAppCssToAssets(options);
35
+ }
@@ -0,0 +1,136 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ import { readFileSync } from 'node:fs';
3
+ import { join, parse } from 'node:path';
4
+ import { ASTJavaScript as AST } from '@codemod-utils/ast';
5
+ import { processTemplate } from '@codemod-utils/blueprints';
6
+ import { createFiles } from '@codemod-utils/files';
7
+ import { blueprintsRoot } from '../../../../utils/blueprints.js';
8
+ import { parseEntityName } from '../../../../utils/string.js';
9
+ function removeTemplateOnlyComponentMethod(file, data) {
10
+ const traverse = AST.traverse(data.isTypeScript);
11
+ const ast = traverse(file, {
12
+ visitCallExpression(path) {
13
+ if (path.value.callee.name !== 'templateOnlyComponent') {
14
+ return false;
15
+ }
16
+ const superClass = AST.builders.identifier('Component');
17
+ if (data.isTypeScript) {
18
+ superClass.typeAnnotation = path.value.typeParameters;
19
+ }
20
+ return AST.builders.classExpression(null, AST.builders.classBody([
21
+ AST.builders.classProperty(AST.builders.identifier(data.__styles__), AST.builders.identifier(data.__styles__)),
22
+ ]), superClass);
23
+ },
24
+ visitImportDeclaration(path) {
25
+ if (path.value.source.value !== '@ember/component/template-only') {
26
+ return false;
27
+ }
28
+ const defaultImport = path.value.specifiers.find((specifier) => specifier.type === 'ImportDefaultSpecifier');
29
+ if (defaultImport?.local?.name !== 'templateOnlyComponent') {
30
+ return false;
31
+ }
32
+ return AST.builders.importDeclaration([
33
+ AST.builders.importDefaultSpecifier(AST.builders.identifier('Component')),
34
+ ], AST.builders.literal('@glimmer/component'));
35
+ },
36
+ });
37
+ return AST.print(ast);
38
+ }
39
+ function importStylesInClass(file, data) {
40
+ const traverse = AST.traverse(data.isTypeScript);
41
+ // Find the last import statement
42
+ let lastImportDeclarationPath;
43
+ const ast = traverse(file, {
44
+ visitImportDeclaration(path) {
45
+ if (!lastImportDeclarationPath) {
46
+ lastImportDeclarationPath = path;
47
+ // @ts-ignore: Assume that types from external packages are correct
48
+ }
49
+ else if (path.node.start > lastImportDeclarationPath.node.start) {
50
+ lastImportDeclarationPath = path;
51
+ }
52
+ return false;
53
+ },
54
+ });
55
+ // Append the styles import
56
+ // @ts-ignore: Assume that types from external packages are correct
57
+ const nodes = ast.program.body;
58
+ // @ts-ignore: Assume that types from external packages are correct
59
+ const index = lastImportDeclarationPath?.name ?? -1;
60
+ nodes.splice(index + 1, 0, AST.builders.importDeclaration([
61
+ AST.builders.importDefaultSpecifier(AST.builders.identifier(data.__styles__)),
62
+ ], AST.builders.literal(`./${data.fileName}.css`)));
63
+ return AST.print(ast);
64
+ }
65
+ function addStylesAsClassProperty(file, data) {
66
+ const traverse = AST.traverse(data.isTypeScript);
67
+ const ast = traverse(file, {
68
+ visitClassDeclaration(path) {
69
+ const { body } = path.node.body;
70
+ const nodesToAdd = [
71
+ AST.builders.classProperty(AST.builders.identifier(data.__styles__), AST.builders.identifier(data.__styles__)),
72
+ ];
73
+ if (body.length > 0) {
74
+ // @ts-ignore: Assume that types from external packages are correct
75
+ nodesToAdd.push(AST.builders.noop());
76
+ }
77
+ body.unshift(...nodesToAdd);
78
+ return false;
79
+ },
80
+ });
81
+ return AST.print(ast);
82
+ }
83
+ function createClass(entityName, { customizations, options }) {
84
+ const { blueprintFilePaths, getFilePath } = customizations;
85
+ const entity = parseEntityName(entityName);
86
+ const filePath = getFilePath(entityName);
87
+ const fileMap = new Map(blueprintFilePaths.map((blueprintFilePath) => {
88
+ const blueprintFile = readFileSync(join(blueprintsRoot, blueprintFilePath), 'utf8');
89
+ const file = processTemplate(blueprintFile, {
90
+ entity,
91
+ options,
92
+ });
93
+ return [filePath, file];
94
+ }));
95
+ createFiles(fileMap, options);
96
+ }
97
+ function updateClass(entityName, { customizations, options }) {
98
+ const { getFilePath } = customizations;
99
+ const { __styles__, projectRoot } = options;
100
+ const filePath = getFilePath(entityName);
101
+ const { ext: fileExtension, name: fileName } = parse(filePath);
102
+ const data = {
103
+ __styles__,
104
+ fileName,
105
+ isTypeScript: fileExtension === '.ts',
106
+ };
107
+ try {
108
+ let file = readFileSync(join(projectRoot, filePath), 'utf8');
109
+ file = removeTemplateOnlyComponentMethod(file, data);
110
+ file = importStylesInClass(file, data);
111
+ file = addStylesAsClassProperty(file, data);
112
+ const fileMap = new Map([[filePath, file]]);
113
+ createFiles(fileMap, options);
114
+ }
115
+ catch (error) {
116
+ let message = `WARNING: updateClass could not update \`${filePath}\`. Please update the file manually.`;
117
+ if (error instanceof Error) {
118
+ message += ` (${error.message})`;
119
+ }
120
+ console.warn(`${message}\n`);
121
+ }
122
+ }
123
+ export function importStyles(entities, options) {
124
+ for (const [entityName, extensions] of entities) {
125
+ const hasClass = extensions.has('.js') || extensions.has('.ts');
126
+ const hasStylesheet = extensions.has('.css');
127
+ if (!hasStylesheet) {
128
+ continue;
129
+ }
130
+ if (!hasClass) {
131
+ createClass(entityName, options);
132
+ continue;
133
+ }
134
+ updateClass(entityName, options);
135
+ }
136
+ }