@ethlete/core 5.0.0-next.6 → 5.0.0-next.61

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 (35) hide show
  1. package/CHANGELOG.md +714 -0
  2. package/README.md +28 -4
  3. package/fesm2022/ethlete-core.mjs +8061 -3502
  4. package/fesm2022/ethlete-core.mjs.map +1 -1
  5. package/generators/devtools-about/generator.js +122 -0
  6. package/generators/devtools-about/schema.json +25 -0
  7. package/generators/generators.json +26 -5
  8. package/generators/migrate-provider-shape/migration-scope.js +35 -0
  9. package/generators/migrate-provider-shape/migration.js +50 -0
  10. package/generators/migrate-provider-shape/provider-shape.js +284 -0
  11. package/generators/migrate-provider-shape/schema.json +26 -0
  12. package/generators/migrate-surface-interaction-swatch/interaction-swatch.js +63 -0
  13. package/generators/migrate-surface-interaction-swatch/migration.js +36 -0
  14. package/generators/migrate-surface-interaction-swatch/schema.json +26 -0
  15. package/generators/migrate-to-v5/apply-replacements.js +14 -0
  16. package/generators/migrate-to-v5/color-naming.js +124 -0
  17. package/generators/migrate-to-v5/create-provider.js +67 -55
  18. package/generators/migrate-to-v5/migration-files.js +13 -0
  19. package/generators/migrate-to-v5/migration.js +33 -19
  20. package/generators/migrate-to-v5/removed-exports.js +73 -0
  21. package/generators/migrate-to-v5/router-state-service.js +123 -330
  22. package/generators/migrate-to-v5/schema.json +22 -0
  23. package/generators/migrate-to-v5/service-classes.js +108 -0
  24. package/generators/migrate-to-v5/viewport-service.js +224 -491
  25. package/generators/tailwind-4-color-theme/generator.js +594 -0
  26. package/generators/tailwind-4-color-theme/schema.json +44 -0
  27. package/generators/tailwind-4-surface-theme/generator.js +599 -0
  28. package/generators/tailwind-4-surface-theme/schema.json +48 -0
  29. package/migrations/seo-directive-removed.md +80 -0
  30. package/migrations/surface-theme-regenerate.md +20 -0
  31. package/migrations.json +43 -0
  32. package/package.json +25 -14
  33. package/types/ethlete-core.d.ts +3337 -1775
  34. package/generators/tailwind-4-theme/generator.js +0 -490
  35. package/generators/tailwind-4-theme/schema.json +0 -32
@@ -0,0 +1,36 @@
1
+ import { formatFiles } from '@nx/devkit';
2
+ import { createMigrationScope } from '../migrate-provider-shape/migration-scope.js';
3
+ import { migrateInteractionSwatchInFile } from './interaction-swatch.js';
4
+ export default async function migrateSurfaceInteractionSwatch(tree, schema) {
5
+ console.log('\nšŸ”„ Migrating surface interactionColor to the swatch shape...');
6
+ const scope = createMigrationScope(tree, schema);
7
+ console.log(` Scope: ${scope.describe()}`);
8
+ let filesChanged = 0;
9
+ let unresolvedCount = 0;
10
+ scope.visit(tree, (filePath) => {
11
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.d.ts'))
12
+ return;
13
+ const before = tree.read(filePath, 'utf-8');
14
+ if (!before)
15
+ return;
16
+ const result = migrateInteractionSwatchInFile(filePath, before);
17
+ for (const expression of result.unresolved ?? []) {
18
+ unresolvedCount++;
19
+ console.warn(` ⚠ ${filePath}: could not resolve interactionColor value ${expression}`);
20
+ }
21
+ if (!result.changed)
22
+ return;
23
+ tree.write(filePath, result.content);
24
+ filesChanged++;
25
+ console.log(` āœ“ ${filePath}`);
26
+ });
27
+ console.log(filesChanged === 0
28
+ ? ' Nothing to migrate.\n'
29
+ : `\nāœ… Migrated ${filesChanged} file(s). Regenerate your surface CSS with \`nx g @ethlete/core:tailwind-4-surface-theme\`.\n`);
30
+ if (unresolvedCount > 0) {
31
+ console.warn(` Review ${unresolvedCount} unresolved interactionColor value(s) manually.\n`);
32
+ }
33
+ if (!schema.skipFormat) {
34
+ await formatFiles(tree);
35
+ }
36
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "migrate-surface-interaction-swatch",
4
+ "title": "Migrate surface interactionColor to the swatch shape",
5
+ "type": "object",
6
+ "properties": {
7
+ "projects": {
8
+ "type": "array",
9
+ "items": { "type": "string" },
10
+ "description": "Nx projects to migrate. Defaults to every project in the workspace.",
11
+ "alias": "project"
12
+ },
13
+ "include": {
14
+ "type": "array",
15
+ "items": { "type": "string" },
16
+ "description": "Path prefixes to migrate, relative to the workspace root (e.g. 'apps/web'). Unioned with `projects`. Defaults to the whole workspace.",
17
+ "alias": "includePath"
18
+ },
19
+ "skipFormat": {
20
+ "type": "boolean",
21
+ "description": "Skip formatting files after migration",
22
+ "default": false
23
+ }
24
+ },
25
+ "required": []
26
+ }
@@ -0,0 +1,14 @@
1
+ const IDENTIFIER_CHAR = /[\w$]/;
2
+ export function applyReplacements(content, replacements) {
3
+ return [...replacements]
4
+ .sort(([a], [b]) => b.length - a.length)
5
+ .reduce((result, [original, replacement]) => result.replace(snippetPattern(original), () => replacement), content);
6
+ }
7
+ function snippetPattern(snippet) {
8
+ const start = IDENTIFIER_CHAR.test(snippet.charAt(0)) ? '(?<![\\w$])' : '';
9
+ const end = IDENTIFIER_CHAR.test(snippet.charAt(snippet.length - 1)) ? '(?![\\w$])' : '';
10
+ return new RegExp(`${start}${escapeRegExp(snippet)}${end}`, 'g');
11
+ }
12
+ function escapeRegExp(value) {
13
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
14
+ }
@@ -0,0 +1,124 @@
1
+ import { collectFiles } from './migration-files.js';
2
+ /**
3
+ * Symbol renames in TypeScript files (imports + usage).
4
+ */
5
+ const TS_SYMBOL_RENAMES = {
6
+ ProvideThemeDirective: 'ProvideColorDirective',
7
+ THEME_PROVIDER: 'COLOR_PROVIDER',
8
+ ColorThemedDirective: 'ColorInteractiveDirective',
9
+ ColorThemedStylesComponent: 'ColorInteractiveStylesComponent',
10
+ SurfaceThemedDirective: 'SurfaceInteractiveDirective',
11
+ SurfaceThemedStylesComponent: 'SurfaceInteractiveStylesComponent',
12
+ injectThemesPrefix: 'injectColorThemesPrefix',
13
+ createCssThemeName: 'createCssColorThemeName',
14
+ };
15
+ /**
16
+ * String replacements applied to HTML templates and inline templates.
17
+ */
18
+ const TEMPLATE_REPLACEMENTS = [
19
+ ['[etProvideTheme]', '[etProvideColor]'],
20
+ ['etProvideTheme', 'etProvideColor'],
21
+ ['[etColorThemed]', '[etColorInteractive]'],
22
+ ['etColorThemed', 'etColorInteractive'],
23
+ ['[etSurfaceThemed]', '[etSurfaceInteractive]'],
24
+ ['etSurfaceThemed', 'etSurfaceInteractive'],
25
+ ['et-theme-alt--', 'et-color-alt--'],
26
+ ['et-theme--', 'et-color--'],
27
+ ];
28
+ /**
29
+ * CSS class name replacements.
30
+ */
31
+ const CSS_REPLACEMENTS = [
32
+ ['.et-color-themed', '.et-color-interactive'],
33
+ ['.et-surface-themed', '.et-surface-interactive'],
34
+ ['.et-theme--', '.et-color--'],
35
+ ['.et-theme-alt--', '.et-color-alt--'],
36
+ ];
37
+ /**
38
+ * Host directive input alias replacements in TypeScript files.
39
+ */
40
+ const HOST_DIRECTIVE_REPLACEMENTS = [
41
+ ["'etProvideTheme:theme'", "'etProvideColor:color'"],
42
+ ["'etProvideTheme:color'", "'etProvideColor:color'"],
43
+ ['"etProvideTheme:theme"', '"etProvideColor:color"'],
44
+ ['"etProvideTheme"', '"etProvideColor"'],
45
+ ["'etProvideTheme: theme'", "'etProvideColor: color'"],
46
+ ['"etProvideTheme: theme"', '"etProvideColor: color"'],
47
+ ];
48
+ export default async function migrateColorNaming(tree, scope) {
49
+ let filesModified = 0;
50
+ for (const filePath of collectFiles(tree, scope, ['.ts', '.html', '.css', '.scss'])) {
51
+ const wasModified = filePath.endsWith('.ts')
52
+ ? migrateTypeScriptFile(tree, filePath)
53
+ : filePath.endsWith('.html')
54
+ ? migrateTemplateFile(tree, filePath)
55
+ : migrateCssFile(tree, filePath);
56
+ if (wasModified)
57
+ filesModified++;
58
+ }
59
+ return { filesChanged: filesModified, review: [] };
60
+ }
61
+ function migrateTypeScriptFile(tree, filePath) {
62
+ let content = tree.read(filePath, 'utf-8');
63
+ if (!content)
64
+ return false;
65
+ const original = content;
66
+ if (!/from\s+['"]@ethlete\/(?:core|cdk|theming)['"]/.test(content))
67
+ return false;
68
+ // Rename symbols
69
+ for (const [oldName, newName] of Object.entries(TS_SYMBOL_RENAMES)) {
70
+ content = replaceWholeWord(content, oldName, newName);
71
+ }
72
+ // Rename host directive input aliases
73
+ for (const [oldStr, newStr] of HOST_DIRECTIVE_REPLACEMENTS) {
74
+ content = content.replaceAll(oldStr, newStr);
75
+ }
76
+ // Handle inline templates in TypeScript files
77
+ for (const [oldStr, newStr] of TEMPLATE_REPLACEMENTS) {
78
+ content = content.replaceAll(oldStr, newStr);
79
+ }
80
+ // Handle CSS in TypeScript files
81
+ for (const [oldStr, newStr] of CSS_REPLACEMENTS) {
82
+ content = content.replaceAll(oldStr, newStr);
83
+ }
84
+ if (content !== original) {
85
+ tree.write(filePath, content);
86
+ return true;
87
+ }
88
+ return false;
89
+ }
90
+ function migrateTemplateFile(tree, filePath) {
91
+ let content = tree.read(filePath, 'utf-8');
92
+ if (!content)
93
+ return false;
94
+ const original = content;
95
+ for (const [oldStr, newStr] of TEMPLATE_REPLACEMENTS) {
96
+ content = content.replaceAll(oldStr, newStr);
97
+ }
98
+ if (content !== original) {
99
+ tree.write(filePath, content);
100
+ return true;
101
+ }
102
+ return false;
103
+ }
104
+ function migrateCssFile(tree, filePath) {
105
+ let content = tree.read(filePath, 'utf-8');
106
+ if (!content)
107
+ return false;
108
+ const original = content;
109
+ for (const [oldStr, newStr] of CSS_REPLACEMENTS) {
110
+ content = content.replaceAll(oldStr, newStr);
111
+ }
112
+ if (content !== original) {
113
+ tree.write(filePath, content);
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+ function replaceWholeWord(content, oldWord, newWord) {
119
+ const regex = new RegExp(`\\b${escapeRegex(oldWord)}\\b`, 'g');
120
+ return content.replace(regex, newWord);
121
+ }
122
+ function escapeRegex(str) {
123
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
124
+ }
@@ -1,38 +1,65 @@
1
- import { logger } from '@nx/devkit';
2
1
  import * as ts from 'typescript';
3
- export default async function migrateCreateProvider(tree) {
4
- logger.log('\nšŸ”„ Migrating createProvider imports from @ethlete/cdk to @ethlete/core...\n');
5
- const tsFiles = [];
6
- // Recursively find all TypeScript files
7
- function findTsFiles(dir) {
8
- const children = tree.children(dir);
9
- for (const child of children) {
10
- const path = dir === '.' ? child : `${dir}/${child}`;
11
- if (tree.isFile(path)) {
12
- if (path.endsWith('.ts') && !path.includes('node_modules') && !path.endsWith('.spec.ts')) {
13
- tsFiles.push(path);
14
- }
15
- }
16
- else {
17
- findTsFiles(path);
18
- }
19
- }
20
- }
21
- findTsFiles('.');
2
+ import { collectFiles } from './migration-files.js';
3
+ export default async function migrateCreateProvider(tree, scope) {
4
+ const review = [];
22
5
  let filesModified = 0;
23
- for (const filePath of tsFiles) {
24
- const wasModified = migrateCreateProviderInFile(tree, filePath);
25
- if (wasModified) {
6
+ for (const filePath of collectFiles(tree, scope, ['.ts'])) {
7
+ const reexportsModified = migrateCreateProviderReexports(tree, filePath, review);
8
+ const wasModified = migrateCreateProviderInFile(tree, filePath) || reexportsModified;
9
+ if (wasModified)
26
10
  filesModified++;
27
- logger.log(` āœ“ ${filePath}`);
28
- }
29
11
  }
30
- if (filesModified > 0) {
31
- logger.log(`\nāœ… Successfully migrated createProvider in ${filesModified} file(s)\n`);
12
+ return { filesChanged: filesModified, review };
13
+ }
14
+ function migrateCreateProviderReexports(tree, filePath, review) {
15
+ const content = tree.read(filePath, 'utf-8');
16
+ if (!content || !content.includes('createProvider'))
17
+ return false;
18
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
19
+ const edits = [];
20
+ for (const statement of sourceFile.statements) {
21
+ if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement))
22
+ continue;
23
+ if (!statement.moduleSpecifier || !ts.isStringLiteral(statement.moduleSpecifier))
24
+ continue;
25
+ if (statement.moduleSpecifier.text !== '@ethlete/cdk')
26
+ continue;
27
+ if (ts.isImportDeclaration(statement)) {
28
+ const bindings = statement.importClause?.namedBindings;
29
+ if (bindings && ts.isNamespaceImport(bindings) && content.includes(`${bindings.name.text}.createProvider`)) {
30
+ review.push(`${filePath}: ${bindings.name.text}.createProvider is used through a namespace import of @ethlete/cdk. Switch it to createProvider from @ethlete/core manually.`);
31
+ }
32
+ continue;
33
+ }
34
+ const clause = statement.exportClause;
35
+ if (!clause) {
36
+ review.push(`${filePath}: "export * from '@ethlete/cdk'" may re-export createProvider. Re-export it from @ethlete/core manually.`);
37
+ continue;
38
+ }
39
+ if (ts.isNamespaceExport(clause))
40
+ continue;
41
+ const isCreateProvider = (element) => (element.propertyName?.text ?? element.name.text) === 'createProvider';
42
+ const moved = clause.elements.filter(isCreateProvider);
43
+ if (moved.length === 0)
44
+ continue;
45
+ const rest = clause.elements.filter((element) => !isCreateProvider(element));
46
+ const typeOnly = statement.isTypeOnly ? 'type ' : '';
47
+ const lines = [
48
+ ...(rest.length > 0
49
+ ? [`export ${typeOnly}{ ${rest.map((el) => el.getText(sourceFile)).join(', ')} } from '@ethlete/cdk';`]
50
+ : []),
51
+ `export ${typeOnly}{ ${moved.map((el) => el.getText(sourceFile)).join(', ')} } from '@ethlete/core';`,
52
+ ];
53
+ edits.push({ start: statement.getStart(sourceFile), end: statement.getEnd(), text: lines.join('\n') });
32
54
  }
33
- else {
34
- logger.log('\nā„¹ļø No files needed migration\n');
55
+ if (edits.length === 0)
56
+ return false;
57
+ let updatedContent = content;
58
+ for (const edit of edits.reverse()) {
59
+ updatedContent = updatedContent.slice(0, edit.start) + edit.text + updatedContent.slice(edit.end);
35
60
  }
61
+ tree.write(filePath, updatedContent);
62
+ return true;
36
63
  }
37
64
  function migrateCreateProviderInFile(tree, filePath) {
38
65
  const content = tree.read(filePath, 'utf-8');
@@ -40,35 +67,30 @@ function migrateCreateProviderInFile(tree, filePath) {
40
67
  return false;
41
68
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
42
69
  let cdkImport;
43
- let coreImport;
44
70
  let hasCreateProviderInCdk = false;
45
- // Find relevant imports
71
+ let createProviderImport = 'createProvider';
46
72
  for (const statement of sourceFile.statements) {
47
73
  if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
48
74
  const modulePath = statement.moduleSpecifier.text;
49
75
  if (modulePath === '@ethlete/cdk') {
50
76
  cdkImport = statement;
51
77
  if (statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings)) {
52
- hasCreateProviderInCdk = statement.importClause.namedBindings.elements.some((element) => element.name.text === 'createProvider');
78
+ const specifier = statement.importClause.namedBindings.elements.find((element) => (element.propertyName?.text ?? element.name.text) === 'createProvider');
79
+ hasCreateProviderInCdk = !!specifier;
80
+ createProviderImport = specifier?.getText(sourceFile) ?? createProviderImport;
53
81
  }
54
82
  }
55
- else if (modulePath === '@ethlete/core') {
56
- coreImport = statement;
57
- }
58
83
  }
59
84
  }
60
- // If createProvider is not imported from @ethlete/cdk, nothing to do
61
85
  if (!hasCreateProviderInCdk)
62
86
  return false;
63
87
  let updatedContent = content;
64
- // Remove createProvider from @ethlete/cdk import
65
88
  if (cdkImport && cdkImport.importClause?.namedBindings && ts.isNamedImports(cdkImport.importClause.namedBindings)) {
66
89
  const namedBindings = cdkImport.importClause.namedBindings;
67
- const otherImports = namedBindings.elements.filter((element) => element.name.text !== 'createProvider');
90
+ const otherImports = namedBindings.elements.filter((element) => (element.propertyName?.text ?? element.name.text) !== 'createProvider');
68
91
  const importStart = cdkImport.getStart(sourceFile);
69
92
  const importEnd = cdkImport.getEnd();
70
93
  if (otherImports.length === 0) {
71
- // Remove entire import line including newline
72
94
  let lineStart = importStart;
73
95
  while (lineStart > 0 && content[lineStart - 1] !== '\n') {
74
96
  lineStart--;
@@ -83,15 +105,12 @@ function migrateCreateProviderInFile(tree, filePath) {
83
105
  updatedContent = content.slice(0, lineStart) + content.slice(lineEnd);
84
106
  }
85
107
  else {
86
- // Keep other imports
87
- const newImports = otherImports.map((el) => el.name.text).join(', ');
108
+ const newImports = otherImports.map((el) => el.getText(sourceFile)).join(', ');
88
109
  const newImportText = `import { ${newImports} } from '@ethlete/cdk';`;
89
110
  updatedContent = content.slice(0, importStart) + newImportText + content.slice(importEnd);
90
111
  }
91
112
  }
92
- // Re-parse after first modification
93
113
  const intermediateSourceFile = ts.createSourceFile(filePath, updatedContent, ts.ScriptTarget.Latest, true);
94
- // Find @ethlete/core import in the updated content
95
114
  let updatedCoreImport;
96
115
  for (const statement of intermediateSourceFile.statements) {
97
116
  if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
@@ -101,26 +120,22 @@ function migrateCreateProviderInFile(tree, filePath) {
101
120
  }
102
121
  }
103
122
  }
104
- // Add createProvider to @ethlete/core import
105
123
  if (updatedCoreImport?.importClause?.namedBindings &&
106
124
  ts.isNamedImports(updatedCoreImport.importClause.namedBindings)) {
107
125
  const namedBindings = updatedCoreImport.importClause.namedBindings;
108
- // Check if createProvider already exists
109
- const hasCreateProvider = namedBindings.elements.some((element) => element.name.text === 'createProvider');
126
+ const hasCreateProvider = namedBindings.elements.some((element) => (element.propertyName?.text ?? element.name.text) === 'createProvider');
110
127
  if (!hasCreateProvider) {
111
128
  const importStart = updatedCoreImport.getStart(intermediateSourceFile);
112
129
  const importEnd = updatedCoreImport.getEnd();
113
- const existingImports = namedBindings.elements.map((el) => el.name.text);
114
- const allImports = [...existingImports, 'createProvider'].sort();
130
+ const existingImports = namedBindings.elements.map((el) => el.getText(intermediateSourceFile));
131
+ const allImports = [...existingImports, createProviderImport].sort();
115
132
  const newImportText = `import { ${allImports.join(', ')} } from '@ethlete/core';`;
116
133
  updatedContent = updatedContent.slice(0, importStart) + newImportText + updatedContent.slice(importEnd);
117
134
  }
118
135
  }
119
136
  else {
120
- // Add new import from @ethlete/core at the top
121
137
  const firstImportIndex = updatedContent.indexOf('import');
122
138
  if (firstImportIndex !== -1) {
123
- // Add after other imports
124
139
  let insertPosition;
125
140
  let lastImportEnd = firstImportIndex;
126
141
  for (const statement of intermediateSourceFile.statements) {
@@ -132,7 +147,6 @@ function migrateCreateProviderInFile(tree, filePath) {
132
147
  }
133
148
  }
134
149
  insertPosition = lastImportEnd;
135
- // Find end of line
136
150
  while (insertPosition < updatedContent.length && updatedContent[insertPosition] !== '\n') {
137
151
  insertPosition++;
138
152
  }
@@ -141,12 +155,11 @@ function migrateCreateProviderInFile(tree, filePath) {
141
155
  }
142
156
  updatedContent =
143
157
  updatedContent.slice(0, insertPosition) +
144
- "import { createProvider } from '@ethlete/core';\n" +
158
+ `import { ${createProviderImport} } from '@ethlete/core';\n` +
145
159
  updatedContent.slice(insertPosition);
146
160
  }
147
161
  else {
148
- // No imports, add at the beginning
149
- updatedContent = "import { createProvider } from '@ethlete/core';\n\n" + updatedContent;
162
+ updatedContent = `import { ${createProviderImport} } from '@ethlete/core';\n\n` + updatedContent;
150
163
  }
151
164
  }
152
165
  if (updatedContent !== content) {
@@ -155,4 +168,3 @@ function migrateCreateProviderInFile(tree, filePath) {
155
168
  }
156
169
  return false;
157
170
  }
158
- //# sourceMappingURL=create-provider.js.map
@@ -0,0 +1,13 @@
1
+ import { createMigrationScope } from '../migrate-provider-shape/migration-scope.js';
2
+ const SKIPPED_DIRECTORIES = new Set(['node_modules', 'dist', '.git', '.angular', '.nx']);
3
+ export const collectFiles = (tree, scope, extensions) => {
4
+ const files = [];
5
+ (scope ?? createMigrationScope(tree, {})).visit(tree, (filePath) => {
6
+ if (filePath.split('/').some((segment) => SKIPPED_DIRECTORIES.has(segment)))
7
+ return;
8
+ if (!extensions.some((extension) => filePath.endsWith(extension)))
9
+ return;
10
+ files.push(filePath);
11
+ });
12
+ return files;
13
+ };
@@ -1,28 +1,42 @@
1
- import { formatFiles } from '@nx/devkit';
1
+ import { formatFiles, logger } from '@nx/devkit';
2
+ import { createMigrationScope, } from '../migrate-provider-shape/migration-scope.js';
3
+ import migrateColorNaming from './color-naming.js';
2
4
  import migrateCreateProvider from './create-provider.js';
5
+ import reportRemovedExports from './removed-exports.js';
3
6
  import migrateRouterStateService from './router-state-service.js';
4
7
  import migrateViewportService from './viewport-service.js';
5
8
  export default async function migrate(tree, schema) {
6
- console.log('\nšŸ”„ Starting core v5 migration...');
7
- const shouldMigrateViewportService = schema.migrateViewportService !== false;
8
- const shouldMigrateCreateProvider = schema.migrateCreateProvider !== false;
9
- const shouldMigrateRouterStateService = schema.migrateRouterStateService !== false;
10
- if (shouldMigrateViewportService) {
11
- console.log(' • Migrating viewport service...');
12
- await migrateViewportService(tree);
13
- }
14
- if (shouldMigrateCreateProvider) {
15
- console.log(' • Migrating create provider...');
16
- await migrateCreateProvider(tree);
17
- }
18
- if (shouldMigrateRouterStateService) {
19
- await migrateRouterStateService(tree);
9
+ const scope = createMigrationScope(tree, schema);
10
+ logger.info('\nšŸ”„ Starting core v5 migration...');
11
+ logger.info(` Scope: ${scope.describe()}`);
12
+ const transforms = [
13
+ { label: 'ViewportService', enabled: schema.migrateViewportService !== false, run: migrateViewportService },
14
+ { label: 'createProvider', enabled: schema.migrateCreateProvider !== false, run: migrateCreateProvider },
15
+ {
16
+ label: 'RouterStateService',
17
+ enabled: schema.migrateRouterStateService !== false,
18
+ run: migrateRouterStateService,
19
+ },
20
+ { label: 'Theme → color naming', enabled: schema.migrateColorNaming !== false, run: migrateColorNaming },
21
+ { label: 'Removed exports', enabled: schema.reportRemovedExports !== false, run: reportRemovedExports },
22
+ ];
23
+ const reports = [];
24
+ for (const transform of transforms) {
25
+ if (!transform.enabled)
26
+ continue;
27
+ logger.info(` • ${transform.label}...`);
28
+ reports.push([transform.label, await transform.run(tree, scope)]);
20
29
  }
21
30
  if (!schema.skipFormat) {
22
- console.log(' • Formatting files...');
23
31
  await formatFiles(tree);
24
32
  }
25
- console.log('\nāœ… Migration completed successfully!');
33
+ logger.info('\nSummary:');
34
+ for (const [label, report] of reports) {
35
+ const review = report.review.length > 0 ? `, ${report.review.length} to review manually` : '';
36
+ logger.info(` • ${label}: ${report.filesChanged} file(s) changed${review}`);
37
+ for (const message of report.review) {
38
+ logger.warn(` ⚠ ${message}`);
39
+ }
40
+ }
41
+ logger.info('\nāœ… Migration completed. Review the diff before committing.');
26
42
  }
27
- //#endregion
28
- //# sourceMappingURL=migration.js.map
@@ -0,0 +1,73 @@
1
+ import * as ts from 'typescript';
2
+ import { collectFiles } from './migration-files.js';
3
+ const REMOVED_EXPORTS = new Map([
4
+ ...['Memo', 'MemoConfig', 'MemoResolver', 'MapLike'].map((name) => [name, 'the @Memo decorator']),
5
+ ...[
6
+ 'createProps',
7
+ 'createHostProps',
8
+ 'createPropHandlers',
9
+ 'createSetup',
10
+ 'createDependencyStash',
11
+ 'createElementDictionary',
12
+ 'bindProps',
13
+ 'unbindProps',
14
+ 'PropsDirective',
15
+ 'templateComputed',
16
+ 'ComponentType',
17
+ 'AnyTemplateType',
18
+ 'TemplateRefWithContext',
19
+ 'ComponentTypeWithInputs',
20
+ 'StringTemplate',
21
+ 'NgTemplateTemplate',
22
+ 'ComponentTemplate',
23
+ 'Props',
24
+ 'PropsAttachedElements',
25
+ 'PropsAttachedElementsInternal',
26
+ 'PropsInternal',
27
+ 'CreatePropsOptions',
28
+ 'HostProps',
29
+ 'PropHandlers',
30
+ 'BindPropsOptions',
31
+ 'UnbindPropsOptions',
32
+ ].map((name) => [name, 'the props module']),
33
+ ]);
34
+ const ET_PROPS_ATTRIBUTE = /\betProps\b/;
35
+ export default async function reportRemovedExports(tree, scope) {
36
+ const review = [];
37
+ for (const filePath of collectFiles(tree, scope, ['.ts', '.html'])) {
38
+ const messages = filePath.endsWith('.ts') ? findRemovedImports(tree, filePath) : findEtPropsUsage(tree, filePath);
39
+ review.push(...messages.map((message) => `${filePath}: ${message}`));
40
+ }
41
+ return { filesChanged: 0, review };
42
+ }
43
+ function findRemovedImports(tree, filePath) {
44
+ const content = tree.read(filePath, 'utf-8');
45
+ if (!content || !content.includes('@ethlete/core'))
46
+ return [];
47
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
48
+ const messages = [];
49
+ for (const statement of sourceFile.statements) {
50
+ if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement))
51
+ continue;
52
+ const specifier = statement.moduleSpecifier;
53
+ if (!specifier || !ts.isStringLiteral(specifier) || specifier.text !== '@ethlete/core')
54
+ continue;
55
+ const bindings = ts.isImportDeclaration(statement) ? statement.importClause?.namedBindings : statement.exportClause;
56
+ if (!bindings || ts.isNamespaceImport(bindings) || ts.isNamespaceExport(bindings))
57
+ continue;
58
+ for (const element of bindings.elements) {
59
+ const name = (element.propertyName ?? element.name).text;
60
+ const origin = REMOVED_EXPORTS.get(name);
61
+ if (origin) {
62
+ messages.push(`${name} was removed from @ethlete/core together with ${origin} and has no replacement.`);
63
+ }
64
+ }
65
+ }
66
+ return messages;
67
+ }
68
+ function findEtPropsUsage(tree, filePath) {
69
+ const content = tree.read(filePath, 'utf-8');
70
+ if (!content || !ET_PROPS_ATTRIBUTE.test(content))
71
+ return [];
72
+ return ['[etProps] (PropsDirective) was removed from @ethlete/core together with the props module.'];
73
+ }