ember-codemod-remove-global-styles 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -9,7 +9,11 @@ _Codemod to localize global styles_
9
9
 
10
10
  ### Arguments
11
11
 
12
- [PROVIDE REQUIRED AND OPTIONAL ARGUMENTS.]
12
+ You must pass `--src` to indicate the location of your global stylesheet.
13
+
14
+ ```sh
15
+ ember-codemod-remove-global-styles --src app/assets/app.css
16
+ ```
13
17
 
14
18
  <details>
15
19
 
package/dist/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import { analyzeProject, createCssModuleFiles, createOptions, } from './steps/index.js';
1
+ import { analyzeProject, createCssModuleFiles, 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
5
  createCssModuleFiles(project, options);
6
+ updateProject(project, options);
6
7
  }
@@ -0,0 +1,22 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { findFiles } from '@codemod-utils/files';
4
+ import { getEntityData } from './get-entity-data.js';
5
+ export function analyzeComponents(classToStyles, options) {
6
+ const { projectRoot } = options;
7
+ const filePaths = findFiles('app/components/**/*.{gjs,gts,hbs}', {
8
+ projectRoot,
9
+ });
10
+ const components = new Map();
11
+ filePaths.forEach((filePath) => {
12
+ const file = readFileSync(join(projectRoot, filePath), 'utf8');
13
+ const entityData = getEntityData(file, {
14
+ classToStyles,
15
+ isHbs: filePath.endsWith('.hbs'),
16
+ });
17
+ if (entityData.localStyles.length > 0) {
18
+ components.set(filePath, entityData);
19
+ }
20
+ });
21
+ return components;
22
+ }
@@ -0,0 +1,22 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { findFiles } from '@codemod-utils/files';
4
+ import { getEntityData } from './get-entity-data.js';
5
+ export function analyzeRoutes(classToStyles, options) {
6
+ const { projectRoot } = options;
7
+ const filePaths = findFiles('app/templates/**/*.{gjs,gts,hbs}', {
8
+ projectRoot,
9
+ });
10
+ const routes = new Map();
11
+ filePaths.forEach((filePath) => {
12
+ const file = readFileSync(join(projectRoot, filePath), 'utf8');
13
+ const entityData = getEntityData(file, {
14
+ classToStyles,
15
+ isHbs: filePath.endsWith('.hbs'),
16
+ });
17
+ if (entityData.localStyles.length > 0) {
18
+ routes.set(filePath, entityData);
19
+ }
20
+ });
21
+ return routes;
22
+ }
@@ -0,0 +1,36 @@
1
+ import { findTemplateTags } from '@codemod-utils/ast-template-tag';
2
+ import { getClasses } from '../../utils/css/index.js';
3
+ function getLocalStyles(classes, data) {
4
+ const classesSet = new Set(classes);
5
+ const localStyles = classes.reduce((accumulator, className) => {
6
+ const styles = data.classToStyles.get(className) ?? [];
7
+ const filteredStyles = styles.filter(({ classes }) => {
8
+ return classes.every((className) => classesSet.has(className));
9
+ });
10
+ accumulator.push(...filteredStyles);
11
+ return accumulator;
12
+ }, []);
13
+ return localStyles;
14
+ }
15
+ export function getEntityData(file, data) {
16
+ const classes = [];
17
+ const errors = [];
18
+ if (data.isHbs) {
19
+ const output = getClasses(file);
20
+ classes.push(...output.classes);
21
+ errors.push(...output.errors);
22
+ }
23
+ else {
24
+ const templateTags = findTemplateTags(file);
25
+ templateTags.forEach(({ contents }) => {
26
+ const output = getClasses(contents);
27
+ classes.push(...output.classes);
28
+ errors.push(...output.errors);
29
+ });
30
+ }
31
+ return {
32
+ classes,
33
+ errors,
34
+ localStyles: getLocalStyles(classes, data),
35
+ };
36
+ }
@@ -0,0 +1,2 @@
1
+ export * from './analyze-components.js';
2
+ export * from './analyze-routes.js';
@@ -1,11 +1,15 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { getClassToStyles } from '../utils/css/index.js';
4
+ import { analyzeComponents, analyzeRoutes } from './analyze-project/index.js';
4
5
  export function analyzeProject(options) {
5
6
  const { projectRoot, src } = options;
6
7
  const stylesheet = readFileSync(join(projectRoot, src), 'utf8');
7
8
  const classToStyles = getClassToStyles(stylesheet);
9
+ const components = analyzeComponents(classToStyles, options);
10
+ const routes = analyzeRoutes(classToStyles, options);
8
11
  return {
9
- classToStyles,
12
+ components,
13
+ routes,
10
14
  };
11
15
  }
@@ -0,0 +1,11 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ export * from './get-file.js';
2
+ export * from './log-errors.js';
@@ -0,0 +1,8 @@
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,54 +1,21 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { findTemplateTags } from '@codemod-utils/ast-template-tag';
4
- import { createFiles, findFiles } from '@codemod-utils/files';
5
- import { getClasses, getModuleFilePath, printStyles, } from '../utils/css/index.js';
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';
6
4
  export function createCssModuleFiles(project, options) {
7
- const { projectRoot } = options;
8
- const filePaths = findFiles('app/{components,templates}/**/*.{gjs,gts,hbs}', {
9
- projectRoot,
10
- });
11
5
  const fileMap = new Map();
12
- filePaths.forEach((filePath) => {
13
- const file = readFileSync(join(projectRoot, filePath), 'utf8');
14
- const classes = [];
15
- const errors = [];
16
- if (filePath.endsWith('.hbs')) {
17
- const output = getClasses(file);
18
- classes.push(...output.classes);
19
- errors.push(...output.errors);
20
- }
21
- else {
22
- const templateTags = findTemplateTags(file);
23
- templateTags.forEach(({ contents }) => {
24
- const output = getClasses(contents);
25
- classes.push(...output.classes);
26
- errors.push(...output.errors);
27
- });
28
- }
29
- const classesSet = new Set(classes);
30
- const localStyles = classes.reduce((accumulator, className) => {
31
- const styles = project.classToStyles.get(className) ?? [];
32
- const filteredStyles = styles.filter(({ classes }) => {
33
- return classes.every((className) => classesSet.has(className));
34
- });
35
- accumulator.push(...filteredStyles);
36
- return accumulator;
37
- }, []);
38
- if (localStyles.length === 0) {
39
- return;
40
- }
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) => {
41
14
  const cssModuleFilePath = getModuleFilePath(filePath);
42
- let cssModuleFile = existsSync(join(projectRoot, cssModuleFilePath))
43
- ? readFileSync(join(projectRoot, cssModuleFilePath), 'utf8')
44
- : '';
45
- cssModuleFile += `${printStyles(localStyles)}\n`;
15
+ let cssModuleFile = getFile(cssModuleFilePath, options);
16
+ cssModuleFile += `${printStyles(data.localStyles)}\n`;
46
17
  fileMap.set(cssModuleFilePath, cssModuleFile);
47
- if (errors.length > 0) {
48
- console.warn(`WARNING: ${cssModuleFilePath} may be incorrect.`);
49
- console.warn(errors.map((error) => `- ${error}`).join('\n'));
50
- console.log();
51
- }
18
+ logErrors(data.errors, { cssModuleFilePath });
52
19
  });
53
- createFiles(fileMap, { projectRoot });
20
+ createFiles(fileMap, options);
54
21
  }
@@ -1,3 +1,4 @@
1
1
  export * from './analyze-project.js';
2
2
  export * from './create-css-module-files.js';
3
3
  export * from './create-options.js';
4
+ export * from './update-project.js';
@@ -0,0 +1,44 @@
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';
6
+ 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);
44
+ }
@@ -0,0 +1 @@
1
+ export * from './processor.js';
@@ -0,0 +1,110 @@
1
+ import { AST } from '@codemod-utils/ast-template';
2
+ export class Processor {
3
+ classToStyles;
4
+ constructor(args) {
5
+ this.classToStyles = args.classToStyles;
6
+ }
7
+ isLocal(className) {
8
+ return this.classToStyles.has(className);
9
+ }
10
+ processConcatStatement(nodeValue) {
11
+ const parts = nodeValue.parts
12
+ .map((part) => {
13
+ switch (part.type) {
14
+ case 'MustacheStatement': {
15
+ return [
16
+ this.processMustacheStatement(part),
17
+ AST.builders.text(' '),
18
+ ];
19
+ }
20
+ case 'TextNode': {
21
+ return [this.processTextNode(part), AST.builders.text(' ')];
22
+ }
23
+ default: {
24
+ return part;
25
+ }
26
+ }
27
+ })
28
+ .flat();
29
+ return AST.builders.concat(parts);
30
+ }
31
+ processMustacheStatement(nodeValue) {
32
+ switch (nodeValue.path.type) {
33
+ case 'PathExpression': {
34
+ switch (nodeValue.path.original) {
35
+ case 'if':
36
+ case 'unless': {
37
+ if (nodeValue.params[1]?.type === 'StringLiteral') {
38
+ // @ts-expect-error: Incorrect type
39
+ nodeValue.params[1] = this.processStringLiteral(nodeValue.params[1]);
40
+ }
41
+ if (nodeValue.params[2]?.type === 'StringLiteral') {
42
+ // @ts-expect-error: Incorrect type
43
+ nodeValue.params[2] = this.processStringLiteral(nodeValue.params[2]);
44
+ }
45
+ break;
46
+ }
47
+ }
48
+ break;
49
+ }
50
+ case 'StringLiteral': {
51
+ // @ts-expect-error: Incorrect type
52
+ nodeValue.path = this.processStringLiteral(nodeValue.path);
53
+ break;
54
+ }
55
+ }
56
+ return nodeValue;
57
+ }
58
+ processStringLiteral(nodeValue) {
59
+ const classNames = nodeValue.original.split(/\s+/).filter(Boolean);
60
+ if (classNames.length === 0) {
61
+ return AST.builders.text('');
62
+ }
63
+ if (classNames.length === 1) {
64
+ const className = classNames[0];
65
+ return this.isLocal(className)
66
+ ? AST.builders.path(`styles.${className}`)
67
+ : nodeValue;
68
+ }
69
+ const hasLocalClass = classNames.some(this.isLocal.bind(this));
70
+ if (!hasLocalClass) {
71
+ return nodeValue;
72
+ }
73
+ const parts = classNames
74
+ .map((className) => {
75
+ return this.isLocal(className)
76
+ ? [AST.builders.path(`styles.${className}`), AST.builders.string(' ')]
77
+ : [AST.builders.string(`${className} `), AST.builders.string(' ')];
78
+ })
79
+ .flat();
80
+ // Remove space at the end
81
+ parts.splice(-1);
82
+ return AST.builders.sexpr(AST.builders.path('concat'), parts);
83
+ }
84
+ processTextNode(nodeValue) {
85
+ const classNames = nodeValue.chars.split(/\s+/).filter(Boolean);
86
+ if (classNames.length === 0) {
87
+ return AST.builders.text('');
88
+ }
89
+ if (classNames.length === 1) {
90
+ const className = classNames[0];
91
+ return this.isLocal(className)
92
+ ? AST.builders.mustache(`styles.${className}`)
93
+ : nodeValue;
94
+ }
95
+ const hasLocalClass = classNames.some(this.isLocal.bind(this));
96
+ if (!hasLocalClass) {
97
+ return nodeValue;
98
+ }
99
+ const parts = classNames
100
+ .map((className) => {
101
+ return this.isLocal(className)
102
+ ? [AST.builders.path(`styles.${className}`), AST.builders.string(' ')]
103
+ : [AST.builders.string(className), AST.builders.string(' ')];
104
+ })
105
+ .flat();
106
+ // Remove space at the end
107
+ parts.splice(-1);
108
+ return AST.builders.mustache(AST.builders.path('concat'), parts);
109
+ }
110
+ }
@@ -0,0 +1,28 @@
1
+ import { AST } from '@codemod-utils/ast-template';
2
+ import { Processor } from './add-local-classes/index.js';
3
+ export function addLocalClasses(file, data) {
4
+ const processor = new Processor(data);
5
+ const traverse = AST.traverse();
6
+ const ast = traverse(file, {
7
+ AttrNode(node) {
8
+ if (node.name !== 'class') {
9
+ return;
10
+ }
11
+ switch (node.value.type) {
12
+ case 'ConcatStatement': {
13
+ node.value = processor.processConcatStatement(node.value);
14
+ break;
15
+ }
16
+ case 'MustacheStatement': {
17
+ node.value = processor.processMustacheStatement(node.value);
18
+ break;
19
+ }
20
+ case 'TextNode': {
21
+ node.value = processor.processTextNode(node.value);
22
+ break;
23
+ }
24
+ }
25
+ },
26
+ });
27
+ return AST.print(ast);
28
+ }
@@ -0,0 +1,4 @@
1
+ export function extractClasses(selector) {
2
+ const matches = Array.from(selector.matchAll(/\.([\w-]+)/g));
3
+ return matches.map((results) => results[1]);
4
+ }
@@ -0,0 +1,7 @@
1
+ export function extractRootClass(selector) {
2
+ const matches = selector.match(/^\.([\w-]+).*$/);
3
+ if (matches === null) {
4
+ return undefined;
5
+ }
6
+ return matches[1];
7
+ }
@@ -0,0 +1,3 @@
1
+ export function extractSelectors(selectorList) {
2
+ return selectorList.split(/\s*,\s*/);
3
+ }
@@ -0,0 +1,3 @@
1
+ export * from './extract-classes.js';
2
+ export * from './extract-root-class.js';
3
+ export * from './extract-selectors.js';
@@ -1,28 +1,18 @@
1
1
  import postcss from 'postcss';
2
- function getClasses(selector) {
3
- const matches = Array.from(selector.matchAll(/\.([\w-]+)/g));
4
- return matches.map((results) => results[1]);
5
- }
6
- function getRootClass(selector) {
7
- const matches = selector.match(/^\.([\w-]+).*$/);
8
- if (matches === null) {
9
- return undefined;
10
- }
11
- return matches[1];
12
- }
2
+ import { extractClasses, extractRootClass, extractSelectors, } from './get-class-to-styles/index.js';
13
3
  export function getClassToStyles(file) {
14
4
  const classToStyles = new Map();
15
5
  function processRule(node) {
16
- const allSelectors = node.selector.split(/\s*,\s*/);
6
+ const selectors = extractSelectors(node.selector);
17
7
  const clone = node.clone();
18
- allSelectors.forEach((selector) => {
19
- const containerClass = getRootClass(selector);
8
+ selectors.forEach((selector) => {
9
+ const containerClass = extractRootClass(selector);
20
10
  if (containerClass === undefined) {
21
11
  return;
22
12
  }
23
13
  clone.selector = selector;
24
14
  const data = {
25
- classes: getClasses(selector),
15
+ classes: extractClasses(selector),
26
16
  location: {
27
17
  end: node.source.end,
28
18
  start: node.source.start,
@@ -0,0 +1 @@
1
+ export * from './processor.js';
@@ -0,0 +1,69 @@
1
+ function extractClasses(value) {
2
+ return value.split(/\s+/).filter(Boolean);
3
+ }
4
+ export class Processor {
5
+ classes = new Set();
6
+ errors = [];
7
+ print() {
8
+ const { classes, errors } = this;
9
+ return {
10
+ classes: Array.from(classes),
11
+ errors,
12
+ };
13
+ }
14
+ processConcatStatement(nodeValue) {
15
+ nodeValue.parts.forEach((part) => {
16
+ switch (part.type) {
17
+ case 'MustacheStatement': {
18
+ this.processMustacheStatement(part);
19
+ break;
20
+ }
21
+ case 'TextNode': {
22
+ this.processTextNode(part);
23
+ break;
24
+ }
25
+ }
26
+ });
27
+ }
28
+ processMustacheStatement(nodeValue) {
29
+ switch (nodeValue.path.type) {
30
+ case 'PathExpression': {
31
+ switch (nodeValue.path.original) {
32
+ case 'if':
33
+ case 'unless': {
34
+ if (nodeValue.params[1]?.type === 'StringLiteral') {
35
+ this.processStringLiteral(nodeValue.params[1]);
36
+ }
37
+ if (nodeValue.params[2]?.type === 'StringLiteral') {
38
+ this.processStringLiteral(nodeValue.params[2]);
39
+ }
40
+ break;
41
+ }
42
+ default: {
43
+ const isLocalClass = nodeValue.path.original.startsWith('styles.');
44
+ if (!isLocalClass) {
45
+ this.errors.push(`Could not analyze {{${nodeValue.path.original}}} in template, line ${nodeValue.loc.start.line}.`);
46
+ }
47
+ }
48
+ }
49
+ break;
50
+ }
51
+ case 'StringLiteral': {
52
+ this.processStringLiteral(nodeValue.path);
53
+ break;
54
+ }
55
+ }
56
+ }
57
+ processStringLiteral(nodeValue) {
58
+ const classNames = extractClasses(nodeValue.original);
59
+ classNames.forEach((className) => {
60
+ this.classes.add(className);
61
+ });
62
+ }
63
+ processTextNode(nodeValue) {
64
+ const classNames = extractClasses(nodeValue.chars);
65
+ classNames.forEach((className) => {
66
+ this.classes.add(className);
67
+ });
68
+ }
69
+ }
@@ -1,48 +1,7 @@
1
1
  import { AST } from '@codemod-utils/ast-template';
2
+ import { Processor } from './get-classes/index.js';
2
3
  export function getClasses(file) {
3
- const classes = new Set();
4
- const errors = [];
5
- function processMustacheStatement(nodeValue) {
6
- switch (nodeValue.path.type) {
7
- case 'PathExpression': {
8
- switch (nodeValue.path.original) {
9
- case 'if':
10
- case 'unless': {
11
- if (nodeValue.params[1]?.type === 'StringLiteral') {
12
- processStringLiteral(nodeValue.params[1]);
13
- }
14
- if (nodeValue.params[2]?.type === 'StringLiteral') {
15
- processStringLiteral(nodeValue.params[2]);
16
- }
17
- break;
18
- }
19
- default: {
20
- const isLocalClass = nodeValue.path.original.startsWith('styles.');
21
- if (!isLocalClass) {
22
- errors.push(`Could not analyze {{${nodeValue.path.original}}} in template, line ${nodeValue.loc.start.line}.`);
23
- }
24
- }
25
- }
26
- break;
27
- }
28
- case 'StringLiteral': {
29
- processStringLiteral(nodeValue.path);
30
- break;
31
- }
32
- }
33
- }
34
- function processStringLiteral(nodeValue) {
35
- const classNames = nodeValue.original.split(/\s+/).filter(Boolean);
36
- classNames.forEach((className) => {
37
- classes.add(className);
38
- });
39
- }
40
- function processTextNode(nodeValue) {
41
- const classNames = nodeValue.chars.split(/\s+/).filter(Boolean);
42
- classNames.forEach((className) => {
43
- classes.add(className);
44
- });
45
- }
4
+ const processor = new Processor();
46
5
  const traverse = AST.traverse();
47
6
  traverse(file, {
48
7
  AttrNode(node) {
@@ -51,33 +10,19 @@ export function getClasses(file) {
51
10
  }
52
11
  switch (node.value.type) {
53
12
  case 'ConcatStatement': {
54
- node.value.parts.forEach((part) => {
55
- switch (part.type) {
56
- case 'MustacheStatement': {
57
- processMustacheStatement(part);
58
- break;
59
- }
60
- case 'TextNode': {
61
- processTextNode(part);
62
- break;
63
- }
64
- }
65
- });
13
+ processor.processConcatStatement(node.value);
66
14
  break;
67
15
  }
68
16
  case 'MustacheStatement': {
69
- processMustacheStatement(node.value);
17
+ processor.processMustacheStatement(node.value);
70
18
  break;
71
19
  }
72
20
  case 'TextNode': {
73
- processTextNode(node.value);
21
+ processor.processTextNode(node.value);
74
22
  break;
75
23
  }
76
24
  }
77
25
  },
78
26
  });
79
- return {
80
- classes: Array.from(classes),
81
- errors,
82
- };
27
+ return processor.print();
83
28
  }
@@ -1,3 +1,4 @@
1
+ export * from './add-local-classes.js';
1
2
  export * from './get-class-to-styles.js';
2
3
  export * from './get-classes.js';
3
4
  export * from './get-module-file-path.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-codemod-remove-global-styles",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Codemod to localize global styles",
5
5
  "keywords": [
6
6
  "codemod",