@ethlete/core 5.0.0-next.7 → 5.0.0-next.70

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 (54) hide show
  1. package/CHANGELOG.md +843 -0
  2. package/README.md +28 -4
  3. package/design/interactive-styles.ts +5 -0
  4. package/design/theme-css.js +392 -0
  5. package/design/theme-css.ts +547 -0
  6. package/fesm2022/ethlete-core.mjs +9338 -5667
  7. package/fesm2022/ethlete-core.mjs.map +1 -1
  8. package/generators/devtools-about/generator.js +122 -0
  9. package/generators/devtools-about/schema.json +25 -0
  10. package/generators/generators.json +41 -5
  11. package/generators/migrate-color-theme-variants/color-theme-variants.js +136 -0
  12. package/generators/migrate-color-theme-variants/migration.js +46 -0
  13. package/generators/migrate-color-theme-variants/schema.json +26 -0
  14. package/generators/migrate-create-destroy/create-destroy.js +317 -0
  15. package/generators/migrate-create-destroy/migration.js +46 -0
  16. package/generators/migrate-create-destroy/schema.json +26 -0
  17. package/generators/migrate-legacy-color-themes/legacy-color-themes.js +123 -0
  18. package/generators/migrate-legacy-color-themes/migration.js +93 -0
  19. package/generators/migrate-legacy-color-themes/schema.json +26 -0
  20. package/generators/migrate-provider-shape/migration-scope.js +35 -0
  21. package/generators/migrate-provider-shape/migration.js +50 -0
  22. package/generators/migrate-provider-shape/provider-shape.js +284 -0
  23. package/generators/migrate-provider-shape/schema.json +26 -0
  24. package/generators/migrate-surface-interaction-swatch/interaction-swatch.js +63 -0
  25. package/generators/migrate-surface-interaction-swatch/migration.js +36 -0
  26. package/generators/migrate-surface-interaction-swatch/schema.json +26 -0
  27. package/generators/migrate-to-v5/apply-replacements.js +14 -0
  28. package/generators/migrate-to-v5/color-naming.js +124 -0
  29. package/generators/migrate-to-v5/create-provider.js +67 -55
  30. package/generators/migrate-to-v5/migration-files.js +13 -0
  31. package/generators/migrate-to-v5/migration.js +33 -19
  32. package/generators/migrate-to-v5/removed-exports.js +161 -0
  33. package/generators/migrate-to-v5/router-state-service.js +125 -330
  34. package/generators/migrate-to-v5/schema.json +22 -0
  35. package/generators/migrate-to-v5/service-classes.js +108 -0
  36. package/generators/migrate-to-v5/sync-observable-consumers.js +80 -0
  37. package/generators/migrate-to-v5/viewport-service.js +226 -491
  38. package/generators/tailwind-4-color-theme/generator.js +411 -0
  39. package/generators/tailwind-4-color-theme/schema.json +44 -0
  40. package/generators/tailwind-4-surface-theme/generator.js +379 -0
  41. package/generators/tailwind-4-surface-theme/schema.json +48 -0
  42. package/migrations/legacy-color-themes.md +38 -0
  43. package/migrations/seo-directive-removed.md +81 -0
  44. package/migrations/surface-theme-regenerate.md +20 -0
  45. package/migrations.json +59 -0
  46. package/package.json +38 -21
  47. package/src/lib/animations/easing-tokens-styles.component.css +155 -0
  48. package/src/lib/providers/visually-hidden-styles.component.css +13 -0
  49. package/src/lib/resize-handles/resize-handles.component.css +137 -0
  50. package/src/lib/theming/color-interactive-styles.component.css +147 -0
  51. package/src/lib/theming/surface-interactive-styles.component.css +42 -0
  52. package/types/ethlete-core.d.ts +3842 -2205
  53. package/generators/tailwind-4-theme/generator.js +0 -514
  54. package/generators/tailwind-4-theme/schema.json +0 -32
@@ -4,6 +4,18 @@
4
4
  "title": "Migrate to Core v5",
5
5
  "type": "object",
6
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
+ },
7
19
  "skipFormat": {
8
20
  "type": "boolean",
9
21
  "description": "Skip formatting files after migration",
@@ -23,6 +35,16 @@
23
35
  "type": "boolean",
24
36
  "description": "Migrate router state service to v5",
25
37
  "default": true
38
+ },
39
+ "migrateColorNaming": {
40
+ "type": "boolean",
41
+ "description": "Migrate theme naming to color naming (ProvideThemeDirective -> ProvideColorDirective, etc.)",
42
+ "default": true
43
+ },
44
+ "reportRemovedExports": {
45
+ "type": "boolean",
46
+ "description": "List imports of exports removed in v5 without a replacement (Memo, the props module)",
47
+ "default": true
26
48
  }
27
49
  },
28
50
  "required": []
@@ -0,0 +1,108 @@
1
+ import * as ts from 'typescript';
2
+ import { applyReplacements } from './apply-replacements.js';
3
+ export function getServiceImport(sourceFile, importedName) {
4
+ for (const statement of sourceFile.statements) {
5
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
6
+ continue;
7
+ const bindings = statement.importClause?.namedBindings;
8
+ if (!bindings || !ts.isNamedImports(bindings))
9
+ continue;
10
+ const element = bindings.elements.find((el) => (el.propertyName?.text ?? el.name.text) === importedName);
11
+ if (element)
12
+ return { localName: element.name.text, packageName: statement.moduleSpecifier.text };
13
+ }
14
+ return null;
15
+ }
16
+ export function collectClasses(sourceFile) {
17
+ const classes = [];
18
+ const visit = (node) => {
19
+ if (ts.isClassDeclaration(node))
20
+ classes.push(node);
21
+ ts.forEachChild(node, visit);
22
+ };
23
+ visit(sourceFile);
24
+ return classes;
25
+ }
26
+ export function isServiceProperty(member, serviceName) {
27
+ const initializer = member.initializer;
28
+ const injectsService = !!initializer &&
29
+ ts.isCallExpression(initializer) &&
30
+ ts.isIdentifier(initializer.expression) &&
31
+ initializer.expression.text === 'inject' &&
32
+ !!initializer.arguments[0] &&
33
+ ts.isIdentifier(initializer.arguments[0]) &&
34
+ initializer.arguments[0].text === serviceName;
35
+ return injectsService || isServiceType(member.type, serviceName);
36
+ }
37
+ export function isServiceParameter(param, serviceName) {
38
+ return ts.isIdentifier(param.name) && isServiceType(param.type, serviceName);
39
+ }
40
+ export function findServiceClasses(sourceFile, serviceName) {
41
+ return collectClasses(sourceFile).flatMap((classNode, index) => {
42
+ const fields = classNode.members.flatMap((member) => {
43
+ if (ts.isPropertyDeclaration(member) && ts.isIdentifier(member.name) && isServiceProperty(member, serviceName)) {
44
+ return [member.name.text];
45
+ }
46
+ if (ts.isConstructorDeclaration(member)) {
47
+ return member.parameters
48
+ .filter((param) => isServiceParameter(param, serviceName))
49
+ .map((param) => param.name.text);
50
+ }
51
+ return [];
52
+ });
53
+ return fields.length > 0 ? [{ index, fields }] : [];
54
+ });
55
+ }
56
+ export function applyReplacementsInClass(content, sourceFile, classNode, replacements) {
57
+ const start = classNode.getStart(sourceFile);
58
+ const end = classNode.getEnd();
59
+ return content.slice(0, start) + applyReplacements(content.slice(start, end), replacements) + content.slice(end);
60
+ }
61
+ export function removeServiceImport(sourceFile, content, importedName) {
62
+ for (const statement of sourceFile.statements) {
63
+ if (!ts.isImportDeclaration(statement))
64
+ continue;
65
+ const bindings = statement.importClause?.namedBindings;
66
+ if (!bindings || !ts.isNamedImports(bindings))
67
+ continue;
68
+ const elements = bindings.elements;
69
+ const index = elements.findIndex((el) => (el.propertyName?.text ?? el.name.text) === importedName);
70
+ if (index === -1)
71
+ continue;
72
+ if (elements.length === 1) {
73
+ const defaultImport = statement.importClause.name;
74
+ if (defaultImport)
75
+ return content.slice(0, defaultImport.getEnd()) + content.slice(bindings.getEnd());
76
+ let lineEnd = statement.getEnd();
77
+ while (lineEnd < content.length && content[lineEnd] !== '\n')
78
+ lineEnd++;
79
+ if (content[lineEnd] === '\n')
80
+ lineEnd++;
81
+ return content.slice(0, statement.getStart(sourceFile)) + content.slice(lineEnd);
82
+ }
83
+ const element = elements[index];
84
+ const next = elements[index + 1];
85
+ const previous = elements[index - 1];
86
+ const [start, end] = next
87
+ ? [element.getStart(sourceFile), next.getStart(sourceFile)]
88
+ : [previous.getEnd(), element.getEnd()];
89
+ return content.slice(0, start) + content.slice(end);
90
+ }
91
+ return content;
92
+ }
93
+ function isServiceType(type, serviceName) {
94
+ return !!type && ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && type.typeName.text === serviceName;
95
+ }
96
+ export function isServiceStillUsed(sourceFile, serviceName, serviceClasses) {
97
+ const classes = collectClasses(sourceFile);
98
+ const referencesField = serviceClasses.some(({ index, fields }) => containsIdentifier(classes[index], fields));
99
+ const referencesService = sourceFile.statements.some((statement) => !ts.isImportDeclaration(statement) && containsIdentifier(statement, [serviceName]));
100
+ return referencesField || referencesService;
101
+ }
102
+ function containsIdentifier(node, names) {
103
+ if (!node)
104
+ return false;
105
+ if (ts.isIdentifier(node) && names.includes(node.text))
106
+ return true;
107
+ return !!ts.forEachChild(node, (child) => containsIdentifier(child, names) || undefined);
108
+ }
@@ -0,0 +1,80 @@
1
+ import * as ts from 'typescript';
2
+ const MESSAGE = 'reads an observable made by toObservable(), which emits only after its first effect run. Read the signal directly or use computed().';
3
+ export function findSyncObservableConsumers(filePath, originalContent, content) {
4
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
5
+ const isAddedToObservable = (node) => ts.isCallExpression(node) &&
6
+ ts.isIdentifier(node.expression) &&
7
+ node.expression.text === 'toObservable' &&
8
+ !originalContent.includes(node.getText());
9
+ const containsAddedToObservable = (node) => isAddedToObservable(node) || (ts.forEachChild(node, containsAddedToObservable) ?? false);
10
+ const asyncNames = new Set();
11
+ const collectNames = (node) => {
12
+ if ((ts.isPropertyDeclaration(node) || ts.isVariableDeclaration(node)) &&
13
+ ts.isIdentifier(node.name) &&
14
+ node.initializer &&
15
+ containsAddedToObservable(node.initializer)) {
16
+ asyncNames.add(node.name.text);
17
+ }
18
+ ts.forEachChild(node, collectNames);
19
+ };
20
+ collectNames(sourceFile);
21
+ const isAsyncSource = (expression) => {
22
+ let root = expression;
23
+ while (ts.isCallExpression(root) && ts.isPropertyAccessExpression(root.expression)) {
24
+ root = root.expression.expression;
25
+ }
26
+ if (isAddedToObservable(root))
27
+ return true;
28
+ if (ts.isIdentifier(root))
29
+ return asyncNames.has(root.text);
30
+ return ts.isPropertyAccessExpression(root) && root.expression.kind === ts.SyntaxKind.ThisKeyword
31
+ ? asyncNames.has(root.name.text)
32
+ : false;
33
+ };
34
+ const findings = [];
35
+ const visit = (node) => {
36
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
37
+ const [source, options] = node.arguments;
38
+ const name = node.expression.text;
39
+ if (name === 'toSignal' && source && options && requiresSync(options) && isAsyncSource(source)) {
40
+ findings.push({ node, label: `toSignal(${flatText(source)}, { requireSync: true })` });
41
+ }
42
+ if (name === 'withLatestFrom') {
43
+ for (const argument of node.arguments.filter(isAsyncSource)) {
44
+ findings.push({ node: argument, label: `withLatestFrom(${flatText(argument)})` });
45
+ }
46
+ }
47
+ }
48
+ if (ts.isCallExpression(node) &&
49
+ ts.isPropertyAccessExpression(node.expression) &&
50
+ node.expression.name.text === 'pipe' &&
51
+ isAsyncSource(node.expression.expression)) {
52
+ for (const operator of node.arguments.filter(isFirstValueOperator)) {
53
+ findings.push({ node: operator, label: flatText(operator) });
54
+ }
55
+ }
56
+ ts.forEachChild(node, visit);
57
+ };
58
+ visit(sourceFile);
59
+ return findings
60
+ .sort((a, b) => a.node.getStart() - b.node.getStart())
61
+ .map(({ node, label }) => {
62
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
63
+ return `${filePath}:${line}: ${label} ${MESSAGE}`;
64
+ });
65
+ }
66
+ function requiresSync(options) {
67
+ return (ts.isObjectLiteralExpression(options) &&
68
+ options.properties.some((property) => ts.isPropertyAssignment(property) &&
69
+ property.name.getText() === 'requireSync' &&
70
+ property.initializer.kind === ts.SyntaxKind.TrueKeyword));
71
+ }
72
+ function isFirstValueOperator(operator) {
73
+ if (!ts.isCallExpression(operator) || !ts.isIdentifier(operator.expression))
74
+ return false;
75
+ const name = operator.expression.text;
76
+ return name === 'first' || (name === 'take' && operator.arguments[0]?.getText() === '1');
77
+ }
78
+ function flatText(node) {
79
+ return node.getText().replace(/\s+/g, ' ');
80
+ }