ember-codemod-remove-global-styles 0.2.0 → 0.3.0

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 (30) hide show
  1. package/README.md +37 -2
  2. package/dist/bin/ember-codemod-remove-global-styles.js +12 -0
  3. package/dist/src/index.js +1 -2
  4. package/dist/src/steps/analyze-project/analyze-components.js +6 -3
  5. package/dist/src/steps/analyze-project/analyze-routes.js +6 -3
  6. package/dist/src/steps/create-options.js +6 -1
  7. package/dist/src/steps/index.js +0 -1
  8. package/dist/src/steps/update-project/create-stylesheets.js +39 -0
  9. package/dist/src/steps/update-project/index.js +3 -0
  10. package/dist/src/steps/update-project/update-classes.js +14 -0
  11. package/dist/src/steps/update-project/update-templates.js +14 -0
  12. package/dist/src/steps/update-project.js +4 -42
  13. package/dist/src/utils/css/add-local-classes/processor.js +43 -7
  14. package/dist/src/utils/css/add-local-classes.js +16 -0
  15. package/dist/src/utils/css/get-classes/processor.js +29 -0
  16. package/dist/src/utils/css/get-classes.js +15 -0
  17. package/dist/src/utils/css/get-module-file-path.js +20 -2
  18. package/dist/src/utils/update-project/index.js +2 -0
  19. package/dist/src/utils/update-project/update-class/add-styles-to-class.js +22 -0
  20. package/dist/src/utils/update-project/update-class/get-class-file.js +61 -0
  21. package/dist/src/utils/update-project/update-class/import-stylesheet.js +18 -0
  22. package/dist/src/utils/update-project/update-class/index.js +4 -0
  23. package/dist/src/utils/update-project/update-class/replace-template-only-component.js +39 -0
  24. package/dist/src/utils/update-project/update-class.js +29 -0
  25. package/dist/src/utils/update-project/update-template.js +19 -0
  26. package/package.json +3 -3
  27. package/dist/src/steps/create-css-module-files/get-file.js +0 -11
  28. package/dist/src/steps/create-css-module-files/index.js +0 -2
  29. package/dist/src/steps/create-css-module-files/log-errors.js +0 -8
  30. package/dist/src/steps/create-css-module-files.js +0 -21
package/README.md CHANGED
@@ -12,17 +12,52 @@ _Codemod to localize global styles_
12
12
  You must pass `--src` to indicate the location of your global stylesheet.
13
13
 
14
14
  ```sh
15
- ember-codemod-remove-global-styles --src app/assets/app.css
15
+ pnpx ember-codemod-remove-global-styles --src app/assets/app.css
16
16
  ```
17
17
 
18
18
  <details>
19
19
 
20
+ <summary>Optional: Limit types of files to consider</summary>
21
+
22
+ By default, the codemod considers components and routes. Pass `--convert` to consider a subset of these.
23
+
24
+ ```sh
25
+ # Components only
26
+ pnpx ember-codemod-remove-global-styles --convert components
27
+
28
+ # Routes only
29
+ pnpx ember-codemod-remove-global-styles --convert routes
30
+ ```
31
+
32
+ </details>
33
+
34
+ <details>
35
+
36
+ <summary>Optional: Limit folders to consider</summary>
37
+
38
+ By default, the codemod considers all files and folders for components and routes. Pass `--folder` to limit the search to 1 folder. (You may use glob patterns to specify multiple folders.)
39
+
40
+ ```sh
41
+ # `ui` folder only
42
+ pnpx ember-codemod-remove-global-styles --folder ui
43
+
44
+ # `ui/form` folder only
45
+ pnpx ember-codemod-remove-global-styles --folder ui/form
46
+
47
+ # `route1` and `route2` folders only
48
+ pnpx ember-codemod-remove-global-styles --convert routes --folder "{route1,route2}"
49
+ ```
50
+
51
+ </details>
52
+
53
+ <details>
54
+
20
55
  <summary>Optional: Specify the project root</summary>
21
56
 
22
57
  Pass `--root` to run the codemod somewhere else (i.e. not in the current directory).
23
58
 
24
59
  ```sh
25
- npx ember-codemod-remove-global-styles --root <path/to/your/project>
60
+ pnpx ember-codemod-remove-global-styles --root <path/to/your/project>
26
61
  ```
27
62
 
28
63
  </details>
@@ -7,6 +7,15 @@ import { runCodemod } from '../src/index.js';
7
7
  process.title = 'ember-codemod-remove-global-styles';
8
8
  // Set codemod options
9
9
  const argv = yargs(hideBin(process.argv))
10
+ .option('convert', {
11
+ choices: ['components', 'routes'],
12
+ describe: 'Which type of files to consider',
13
+ type: 'array',
14
+ })
15
+ .option('folder', {
16
+ describe: 'Which folder to consider',
17
+ type: 'string',
18
+ })
10
19
  .option('root', {
11
20
  describe: 'Where to run the codemod',
12
21
  type: 'string',
@@ -17,7 +26,10 @@ const argv = yargs(hideBin(process.argv))
17
26
  type: 'string',
18
27
  })
19
28
  .parseSync();
29
+ const DEFAULT_FOR_CONVERT = ['components', 'routes'];
20
30
  const codemodOptions = {
31
+ convert: new Set(argv['convert'] ?? DEFAULT_FOR_CONVERT),
32
+ folder: argv['folder'] ?? '',
21
33
  projectRoot: argv['root'] ?? process.cwd(),
22
34
  src: argv['src'],
23
35
  };
package/dist/src/index.js CHANGED
@@ -1,7 +1,6 @@
1
- import { analyzeProject, createCssModuleFiles, createOptions, updateProject, } from './steps/index.js';
1
+ import { analyzeProject, createOptions, updateProject } from './steps/index.js';
2
2
  export function runCodemod(codemodOptions) {
3
3
  const options = createOptions(codemodOptions);
4
4
  const project = analyzeProject(options);
5
- createCssModuleFiles(project, options);
6
5
  updateProject(project, options);
7
6
  }
@@ -3,11 +3,14 @@ import { join } from 'node:path';
3
3
  import { findFiles } from '@codemod-utils/files';
4
4
  import { getEntityData } from './get-entity-data.js';
5
5
  export function analyzeComponents(classToStyles, options) {
6
- const { projectRoot } = options;
7
- const filePaths = findFiles('app/components/**/*.{gjs,gts,hbs}', {
6
+ const { convert, folder, projectRoot } = options;
7
+ const components = new Map();
8
+ if (!convert.components) {
9
+ return components;
10
+ }
11
+ const filePaths = findFiles(join('app/components', folder, '**/*.{gjs,gts,hbs}'), {
8
12
  projectRoot,
9
13
  });
10
- const components = new Map();
11
14
  filePaths.forEach((filePath) => {
12
15
  const file = readFileSync(join(projectRoot, filePath), 'utf8');
13
16
  const entityData = getEntityData(file, {
@@ -3,11 +3,14 @@ import { join } from 'node:path';
3
3
  import { findFiles } from '@codemod-utils/files';
4
4
  import { getEntityData } from './get-entity-data.js';
5
5
  export function analyzeRoutes(classToStyles, options) {
6
- const { projectRoot } = options;
7
- const filePaths = findFiles('app/templates/**/*.{gjs,gts,hbs}', {
6
+ const { convert, folder, projectRoot } = options;
7
+ const routes = new Map();
8
+ if (!convert.routes) {
9
+ return routes;
10
+ }
11
+ const filePaths = findFiles(join('app/templates', folder, '**/*.{gjs,gts,hbs}'), {
8
12
  projectRoot,
9
13
  });
10
- const routes = new Map();
11
14
  filePaths.forEach((filePath) => {
12
15
  const file = readFileSync(join(projectRoot, filePath), 'utf8');
13
16
  const entityData = getEntityData(file, {
@@ -1,6 +1,11 @@
1
1
  export function createOptions(codemodOptions) {
2
- const { projectRoot, src } = codemodOptions;
2
+ const { convert, folder, projectRoot, src } = codemodOptions;
3
3
  return {
4
+ convert: {
5
+ components: convert.has('components'),
6
+ routes: convert.has('routes'),
7
+ },
8
+ folder,
4
9
  projectRoot,
5
10
  src,
6
11
  };
@@ -1,4 +1,3 @@
1
1
  export * from './analyze-project.js';
2
- export * from './create-css-module-files.js';
3
2
  export * from './create-options.js';
4
3
  export * from './update-project.js';
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { createFiles } from '@codemod-utils/files';
4
+ import { getModuleFilePath, printStyles } from '../../utils/css/index.js';
5
+ function getFile(filePath, options) {
6
+ const { projectRoot } = options;
7
+ try {
8
+ return readFileSync(join(projectRoot, filePath), 'utf8');
9
+ }
10
+ catch {
11
+ return '';
12
+ }
13
+ }
14
+ function logErrors(cssModuleFilePath, errors) {
15
+ if (errors.length === 0) {
16
+ return;
17
+ }
18
+ console.warn(`WARNING: ${cssModuleFilePath} may be incorrect.`);
19
+ console.warn(errors.map((error) => `- ${error}`).join('\n'));
20
+ console.log();
21
+ }
22
+ export function createStylesheets(project, options) {
23
+ const fileMap = new Map();
24
+ project.components.forEach((data, filePath) => {
25
+ const cssModuleFilePath = getModuleFilePath(filePath);
26
+ let cssModuleFile = getFile(cssModuleFilePath, options);
27
+ cssModuleFile += `${printStyles(data.localStyles)}\n`;
28
+ fileMap.set(cssModuleFilePath, cssModuleFile);
29
+ logErrors(cssModuleFilePath, data.errors);
30
+ });
31
+ project.routes.forEach((data, filePath) => {
32
+ const cssModuleFilePath = getModuleFilePath(filePath);
33
+ let cssModuleFile = getFile(cssModuleFilePath, options);
34
+ cssModuleFile += `${printStyles(data.localStyles)}\n`;
35
+ fileMap.set(cssModuleFilePath, cssModuleFile);
36
+ logErrors(cssModuleFilePath, data.errors);
37
+ });
38
+ createFiles(fileMap, options);
39
+ }
@@ -0,0 +1,3 @@
1
+ export * from './create-stylesheets.js';
2
+ export * from './update-classes.js';
3
+ export * from './update-templates.js';
@@ -0,0 +1,14 @@
1
+ import { createFiles } from '@codemod-utils/files';
2
+ import { updateClass } from '../../utils/update-project/index.js';
3
+ export function updateClasses(project, options) {
4
+ const fileMap = new Map();
5
+ project.components.forEach((_data, templateFilePath) => {
6
+ const { classFile, classFilePath } = updateClass(templateFilePath, options);
7
+ fileMap.set(classFilePath, classFile);
8
+ });
9
+ project.routes.forEach((_data, templateFilePath) => {
10
+ const { classFile, classFilePath } = updateClass(templateFilePath, options);
11
+ fileMap.set(classFilePath, classFile);
12
+ });
13
+ createFiles(fileMap, options);
14
+ }
@@ -0,0 +1,14 @@
1
+ import { createFiles } from '@codemod-utils/files';
2
+ import { updateTemplate } from '../../utils/update-project/index.js';
3
+ export function updateTemplates(project, options) {
4
+ const fileMap = new Map();
5
+ project.components.forEach((_data, templateFilePath) => {
6
+ const templateFile = updateTemplate(templateFilePath, options);
7
+ fileMap.set(templateFilePath, templateFile);
8
+ });
9
+ project.routes.forEach((_data, templateFilePath) => {
10
+ const templateFile = updateTemplate(templateFilePath, options);
11
+ fileMap.set(templateFilePath, templateFile);
12
+ });
13
+ createFiles(fileMap, options);
14
+ }
@@ -1,44 +1,6 @@
1
- import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { updateTemplates } from '@codemod-utils/ast-template-tag';
4
- import { createFiles } from '@codemod-utils/files';
5
- import { addLocalClasses, getClassToStyles, getModuleFilePath, } from '../utils/css/index.js';
1
+ import { createStylesheets, updateClasses, updateTemplates, } from './update-project/index.js';
6
2
  export function updateProject(project, options) {
7
- function getFile(filePath) {
8
- return readFileSync(join(options.projectRoot, filePath), 'utf8');
9
- }
10
- const fileMap = new Map();
11
- project.components.forEach((_data, filePath) => {
12
- const cssModuleFile = getFile(getModuleFilePath(filePath));
13
- let file = getFile(filePath);
14
- const data = {
15
- classToStyles: getClassToStyles(cssModuleFile),
16
- };
17
- if (filePath.endsWith('.hbs')) {
18
- file = addLocalClasses(file, data);
19
- }
20
- else {
21
- file = updateTemplates(file, (code) => {
22
- return addLocalClasses(code, data);
23
- });
24
- }
25
- fileMap.set(filePath, file);
26
- });
27
- project.routes.forEach((_data, filePath) => {
28
- const cssModuleFile = getFile(getModuleFilePath(filePath));
29
- let file = getFile(filePath);
30
- const data = {
31
- classToStyles: getClassToStyles(cssModuleFile),
32
- };
33
- if (filePath.endsWith('.hbs')) {
34
- file = addLocalClasses(file, data);
35
- }
36
- else {
37
- file = updateTemplates(file, (code) => {
38
- return addLocalClasses(code, data);
39
- });
40
- }
41
- fileMap.set(filePath, file);
42
- });
43
- createFiles(fileMap, options);
3
+ createStylesheets(project, options);
4
+ updateClasses(project, options);
5
+ updateTemplates(project, options);
44
6
  }
@@ -1,11 +1,14 @@
1
1
  import { AST } from '@codemod-utils/ast-template';
2
2
  export class Processor {
3
- classToStyles;
3
+ args;
4
4
  constructor(args) {
5
- this.classToStyles = args.classToStyles;
5
+ this.args = args;
6
+ }
7
+ getLocalClass(className) {
8
+ return this.args.isHbs ? `this.styles.${className}` : `styles.${className}`;
6
9
  }
7
10
  isLocal(className) {
8
- return this.classToStyles.has(className);
11
+ return this.args.classToStyles.has(className);
9
12
  }
10
13
  processConcatStatement(nodeValue) {
11
14
  const parts = nodeValue.parts
@@ -63,7 +66,7 @@ export class Processor {
63
66
  if (classNames.length === 1) {
64
67
  const className = classNames[0];
65
68
  return this.isLocal(className)
66
- ? AST.builders.path(`styles.${className}`)
69
+ ? AST.builders.path(this.getLocalClass(className))
67
70
  : nodeValue;
68
71
  }
69
72
  const hasLocalClass = classNames.some(this.isLocal.bind(this));
@@ -73,7 +76,10 @@ export class Processor {
73
76
  const parts = classNames
74
77
  .map((className) => {
75
78
  return this.isLocal(className)
76
- ? [AST.builders.path(`styles.${className}`), AST.builders.string(' ')]
79
+ ? [
80
+ AST.builders.path(this.getLocalClass(className)),
81
+ AST.builders.string(' '),
82
+ ]
77
83
  : [AST.builders.string(`${className} `), AST.builders.string(' ')];
78
84
  })
79
85
  .flat();
@@ -81,6 +87,33 @@ export class Processor {
81
87
  parts.splice(-1);
82
88
  return AST.builders.sexpr(AST.builders.path('concat'), parts);
83
89
  }
90
+ processSubExpression(nodeValue) {
91
+ switch (nodeValue.path.type) {
92
+ case 'PathExpression': {
93
+ switch (nodeValue.path.original) {
94
+ case 'if':
95
+ case 'unless': {
96
+ if (nodeValue.params[1]?.type === 'StringLiteral') {
97
+ // @ts-expect-error: Incorrect type
98
+ nodeValue.params[1] = this.processStringLiteral(nodeValue.params[1]);
99
+ }
100
+ if (nodeValue.params[2]?.type === 'StringLiteral') {
101
+ // @ts-expect-error: Incorrect type
102
+ nodeValue.params[2] = this.processStringLiteral(nodeValue.params[2]);
103
+ }
104
+ break;
105
+ }
106
+ }
107
+ break;
108
+ }
109
+ case 'StringLiteral': {
110
+ // @ts-expect-error: Incorrect type
111
+ nodeValue.path = this.processStringLiteral(nodeValue.path);
112
+ break;
113
+ }
114
+ }
115
+ return nodeValue;
116
+ }
84
117
  processTextNode(nodeValue) {
85
118
  const classNames = nodeValue.chars.split(/\s+/).filter(Boolean);
86
119
  if (classNames.length === 0) {
@@ -89,7 +122,7 @@ export class Processor {
89
122
  if (classNames.length === 1) {
90
123
  const className = classNames[0];
91
124
  return this.isLocal(className)
92
- ? AST.builders.mustache(`styles.${className}`)
125
+ ? AST.builders.mustache(this.getLocalClass(className))
93
126
  : nodeValue;
94
127
  }
95
128
  const hasLocalClass = classNames.some(this.isLocal.bind(this));
@@ -99,7 +132,10 @@ export class Processor {
99
132
  const parts = classNames
100
133
  .map((className) => {
101
134
  return this.isLocal(className)
102
- ? [AST.builders.path(`styles.${className}`), AST.builders.string(' ')]
135
+ ? [
136
+ AST.builders.path(this.getLocalClass(className)),
137
+ AST.builders.string(' '),
138
+ ]
103
139
  : [AST.builders.string(className), AST.builders.string(' ')];
104
140
  })
105
141
  .flat();
@@ -23,6 +23,22 @@ export function addLocalClasses(file, data) {
23
23
  }
24
24
  }
25
25
  },
26
+ HashPair(node) {
27
+ if (node.key !== 'class') {
28
+ return;
29
+ }
30
+ switch (node.value.type) {
31
+ case 'StringLiteral': {
32
+ // @ts-expect-error: Incorrect type
33
+ node.value = processor.processStringLiteral(node.value);
34
+ break;
35
+ }
36
+ case 'SubExpression': {
37
+ node.value = processor.processSubExpression(node.value);
38
+ break;
39
+ }
40
+ }
41
+ },
26
42
  });
27
43
  return AST.print(ast);
28
44
  }
@@ -60,6 +60,35 @@ export class Processor {
60
60
  this.classes.add(className);
61
61
  });
62
62
  }
63
+ processSubExpression(nodeValue) {
64
+ switch (nodeValue.path.type) {
65
+ case 'PathExpression': {
66
+ switch (nodeValue.path.original) {
67
+ case 'if':
68
+ case 'unless': {
69
+ if (nodeValue.params[1]?.type === 'StringLiteral') {
70
+ this.processStringLiteral(nodeValue.params[1]);
71
+ }
72
+ if (nodeValue.params[2]?.type === 'StringLiteral') {
73
+ this.processStringLiteral(nodeValue.params[2]);
74
+ }
75
+ break;
76
+ }
77
+ default: {
78
+ const isLocalClass = nodeValue.path.original.startsWith('styles.');
79
+ if (!isLocalClass) {
80
+ this.errors.push(`Could not analyze {{${nodeValue.path.original}}} in template, line ${nodeValue.loc.start.line}.`);
81
+ }
82
+ }
83
+ }
84
+ break;
85
+ }
86
+ case 'StringLiteral': {
87
+ this.processStringLiteral(nodeValue.path);
88
+ break;
89
+ }
90
+ }
91
+ }
63
92
  processTextNode(nodeValue) {
64
93
  const classNames = extractClasses(nodeValue.chars);
65
94
  classNames.forEach((className) => {
@@ -23,6 +23,21 @@ export function getClasses(file) {
23
23
  }
24
24
  }
25
25
  },
26
+ HashPair(node) {
27
+ if (node.key !== 'class') {
28
+ return;
29
+ }
30
+ switch (node.value.type) {
31
+ case 'StringLiteral': {
32
+ processor.processStringLiteral(node.value);
33
+ break;
34
+ }
35
+ case 'SubExpression': {
36
+ processor.processSubExpression(node.value);
37
+ break;
38
+ }
39
+ }
40
+ },
26
41
  });
27
42
  return processor.print();
28
43
  }
@@ -1,3 +1,21 @@
1
- export function getModuleFilePath(filePath) {
2
- return filePath.replace(/\.(gjs|gts|hbs)$/, '.module.css');
1
+ import { join, relative } from 'node:path';
2
+ import { parseFilePath } from '@codemod-utils/files';
3
+ export function getModuleFilePath(templateFilePath) {
4
+ const { dir, ext, name } = parseFilePath(templateFilePath);
5
+ if (!['.gjs', '.gts', '.hbs'].includes(ext)) {
6
+ throw new RangeError('File extension is incorrect.');
7
+ }
8
+ const data = {
9
+ isComponentTemplate: dir.startsWith('app/components'),
10
+ isRouteTemplate: dir.startsWith('app/templates'),
11
+ isTemplateTag: ext === '.gjs' || ext === '.gts',
12
+ };
13
+ if (data.isTemplateTag) {
14
+ return join(dir, `${name}.module.css`);
15
+ }
16
+ if (data.isRouteTemplate) {
17
+ const controllersDir = join(dir, relative(dir, 'app/controllers'));
18
+ return join(controllersDir, `${name}.module.css`);
19
+ }
20
+ return join(dir, `${name}.module.css`);
3
21
  }
@@ -0,0 +1,2 @@
1
+ export * from './update-class.js';
2
+ export * from './update-template.js';
@@ -0,0 +1,22 @@
1
+ import { AST } from '@codemod-utils/ast-javascript';
2
+ export function addStylesToClass(file, data) {
3
+ if (data.isTemplateTag) {
4
+ return file;
5
+ }
6
+ const traverse = AST.traverse(data.isTypeScript);
7
+ const ast = traverse(file, {
8
+ visitClassDeclaration(path) {
9
+ const { body } = path.node.body;
10
+ const nodesToAdd = [
11
+ AST.builders.classProperty(AST.builders.identifier('styles'), AST.builders.identifier('styles')),
12
+ ];
13
+ if (body.length > 0) {
14
+ // @ts-expect-error: Incorrect type
15
+ nodesToAdd.push(AST.builders.noop());
16
+ }
17
+ body.unshift(...nodesToAdd);
18
+ return false;
19
+ },
20
+ });
21
+ return AST.print(ast);
22
+ }
@@ -0,0 +1,61 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join, relative } from 'node:path';
3
+ import { findFiles, parseFilePath } from '@codemod-utils/files';
4
+ export function getClassFile(templateFilePath, options) {
5
+ const { projectRoot } = options;
6
+ const { dir, ext, name } = parseFilePath(templateFilePath);
7
+ const data = {
8
+ isRouteTemplate: dir.startsWith('app/templates'),
9
+ isTemplateTag: ext === '.gjs' || ext === '.gts',
10
+ };
11
+ if (data.isTemplateTag) {
12
+ const classFile = readFileSync(join(projectRoot, templateFilePath), 'utf8');
13
+ return {
14
+ classFile,
15
+ classFilePath: templateFilePath,
16
+ };
17
+ }
18
+ if (data.isRouteTemplate) {
19
+ const controllersDir = join(dir, relative(dir, 'app/controllers'));
20
+ const classFilePaths = findFiles(join(controllersDir, `${name}.{js,ts}`), {
21
+ projectRoot,
22
+ });
23
+ if (classFilePaths.length === 0) {
24
+ const classFilePath = join(controllersDir, `${name}.js`);
25
+ return {
26
+ classFilePath,
27
+ classFile: [
28
+ `import Controller from '@ember/controller';`,
29
+ ``,
30
+ `export default class extends Controller {}`,
31
+ ``,
32
+ ].join('\n'),
33
+ };
34
+ }
35
+ const classFilePath = classFilePaths[0];
36
+ return {
37
+ classFilePath,
38
+ classFile: readFileSync(join(projectRoot, classFilePath), 'utf8'),
39
+ };
40
+ }
41
+ const classFilePaths = findFiles(join(dir, `${name}.{js,ts}`), {
42
+ projectRoot,
43
+ });
44
+ if (classFilePaths.length === 0) {
45
+ const classFilePath = join(dir, `${name}.js`);
46
+ return {
47
+ classFilePath,
48
+ classFile: [
49
+ `import Component from '@glimmer/component';`,
50
+ ``,
51
+ `export default class extends Component {}`,
52
+ ``,
53
+ ].join('\n'),
54
+ };
55
+ }
56
+ const classFilePath = classFilePaths[0];
57
+ return {
58
+ classFilePath,
59
+ classFile: readFileSync(join(projectRoot, classFilePath), 'utf8'),
60
+ };
61
+ }
@@ -0,0 +1,18 @@
1
+ import { AST } from '@codemod-utils/ast-javascript';
2
+ export function importStylesheet(file, data) {
3
+ const traverse = AST.traverse(data.isTypeScript);
4
+ let canSkip = false;
5
+ traverse(file, {
6
+ visitImportDeclaration(path) {
7
+ const importPath = path.node.source.value;
8
+ if (importPath === `./${data.fileName}.css` ||
9
+ importPath === `./${data.fileName}.module.css`) {
10
+ canSkip = true;
11
+ }
12
+ return false;
13
+ },
14
+ });
15
+ return canSkip
16
+ ? file
17
+ : [`import styles from './${data.fileName}.module.css';`, file].join('\n');
18
+ }
@@ -0,0 +1,4 @@
1
+ export * from './add-styles-to-class.js';
2
+ export * from './get-class-file.js';
3
+ export * from './import-stylesheet.js';
4
+ export * from './replace-template-only-component.js';
@@ -0,0 +1,39 @@
1
+ import { AST } from '@codemod-utils/ast-javascript';
2
+ export function replaceTemplateOnlyComponent(file, data) {
3
+ if (data.isTemplateTag) {
4
+ return file;
5
+ }
6
+ const traverse = AST.traverse(data.isTypeScript);
7
+ const ast = traverse(file, {
8
+ visitCallExpression(path) {
9
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
10
+ if (path.value.callee.name !== 'templateOnlyComponent') {
11
+ return false;
12
+ }
13
+ const superClass = AST.builders.identifier('Component');
14
+ if (data.isTypeScript) {
15
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
16
+ superClass.typeAnnotation = path.value.typeParameters;
17
+ }
18
+ return AST.builders.classExpression(null, AST.builders.classBody([
19
+ AST.builders.classProperty(AST.builders.identifier('styles'), AST.builders.identifier('styles')),
20
+ ]), superClass);
21
+ },
22
+ visitImportDeclaration(path) {
23
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
24
+ if (path.value.source.value !== '@ember/component/template-only') {
25
+ return false;
26
+ }
27
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
28
+ const defaultImport = path.value.specifiers.find((specifier) => specifier.type === 'ImportDefaultSpecifier');
29
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
30
+ if (defaultImport?.local?.name !== 'templateOnlyComponent') {
31
+ return false;
32
+ }
33
+ return AST.builders.importDeclaration([
34
+ AST.builders.importDefaultSpecifier(AST.builders.identifier('Component')),
35
+ ], AST.builders.literal('@glimmer/component'));
36
+ },
37
+ });
38
+ return AST.print(ast);
39
+ }
@@ -0,0 +1,29 @@
1
+ import { updateJavaScript } from '@codemod-utils/ast-template-tag';
2
+ import { parseFilePath } from '@codemod-utils/files';
3
+ import { addStylesToClass, getClassFile, importStylesheet, replaceTemplateOnlyComponent, } from './update-class/index.js';
4
+ export function updateClass(templateFilePath, options) {
5
+ // eslint-disable-next-line prefer-const
6
+ let { classFile, classFilePath } = getClassFile(templateFilePath, options);
7
+ const { ext, name } = parseFilePath(classFilePath);
8
+ const data = {
9
+ fileName: name,
10
+ isTemplateTag: ext === '.gjs' || ext === '.gts',
11
+ isTypeScript: ext === '.gts' || ext === '.ts',
12
+ };
13
+ if (data.isTemplateTag) {
14
+ classFile = updateJavaScript(classFile, (code) => {
15
+ return importStylesheet(code, data);
16
+ });
17
+ return {
18
+ classFile,
19
+ classFilePath,
20
+ };
21
+ }
22
+ classFile = replaceTemplateOnlyComponent(classFile, data);
23
+ classFile = importStylesheet(classFile, data);
24
+ classFile = addStylesToClass(classFile, data);
25
+ return {
26
+ classFile,
27
+ classFilePath,
28
+ };
29
+ }
@@ -0,0 +1,19 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { updateTemplates } from '@codemod-utils/ast-template-tag';
4
+ import { addLocalClasses, getClassToStyles, getModuleFilePath, } from '../css/index.js';
5
+ export function updateTemplate(templateFilePath, options) {
6
+ const { projectRoot } = options;
7
+ const cssModuleFile = readFileSync(join(projectRoot, getModuleFilePath(templateFilePath)), 'utf8');
8
+ const templateFile = readFileSync(join(projectRoot, templateFilePath), 'utf8');
9
+ const data = {
10
+ classToStyles: getClassToStyles(cssModuleFile),
11
+ isHbs: templateFilePath.endsWith('.hbs'),
12
+ };
13
+ if (data.isHbs) {
14
+ return addLocalClasses(templateFile, data);
15
+ }
16
+ return updateTemplates(templateFile, (code) => {
17
+ return addLocalClasses(code, data);
18
+ });
19
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-codemod-remove-global-styles",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Codemod to localize global styles",
5
5
  "keywords": [
6
6
  "codemod",
@@ -42,9 +42,9 @@
42
42
  "eslint": "^9.39.1",
43
43
  "prettier": "^3.6.2",
44
44
  "typescript": "^5.9.3",
45
- "@shared-configs/eslint-config-node": "0.0.0",
46
45
  "@shared-configs/prettier": "0.0.0",
47
- "@shared-configs/typescript": "0.0.0"
46
+ "@shared-configs/typescript": "0.0.0",
47
+ "@shared-configs/eslint-config-node": "0.0.0"
48
48
  },
49
49
  "engines": {
50
50
  "node": "20.* || >= 22"
@@ -1,11 +0,0 @@
1
- import { readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- export function getFile(filePath, options) {
4
- const { projectRoot } = options;
5
- try {
6
- return readFileSync(join(projectRoot, filePath), 'utf8');
7
- }
8
- catch {
9
- return '';
10
- }
11
- }
@@ -1,2 +0,0 @@
1
- export * from './get-file.js';
2
- export * from './log-errors.js';
@@ -1,8 +0,0 @@
1
- export function logErrors(errors, data) {
2
- if (errors.length === 0) {
3
- return;
4
- }
5
- console.warn(`WARNING: ${data.cssModuleFilePath} may be incorrect.`);
6
- console.warn(errors.map((error) => `- ${error}`).join('\n'));
7
- console.log();
8
- }
@@ -1,21 +0,0 @@
1
- import { createFiles } from '@codemod-utils/files';
2
- import { getModuleFilePath, printStyles } from '../utils/css/index.js';
3
- import { getFile, logErrors } from './create-css-module-files/index.js';
4
- export function createCssModuleFiles(project, options) {
5
- const fileMap = new Map();
6
- project.components.forEach((data, filePath) => {
7
- const cssModuleFilePath = getModuleFilePath(filePath);
8
- let cssModuleFile = getFile(cssModuleFilePath, options);
9
- cssModuleFile += `${printStyles(data.localStyles)}\n`;
10
- fileMap.set(cssModuleFilePath, cssModuleFile);
11
- logErrors(data.errors, { cssModuleFilePath });
12
- });
13
- project.routes.forEach((data, filePath) => {
14
- const cssModuleFilePath = getModuleFilePath(filePath);
15
- let cssModuleFile = getFile(cssModuleFilePath, options);
16
- cssModuleFile += `${printStyles(data.localStyles)}\n`;
17
- fileMap.set(cssModuleFilePath, cssModuleFile);
18
- logErrors(data.errors, { cssModuleFilePath });
19
- });
20
- createFiles(fileMap, options);
21
- }