@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
@@ -0,0 +1,122 @@
1
+ import { formatFiles, joinPathFragments, logger, readProjectConfiguration, updateProjectConfiguration, visitNotIgnoredFiles, } from '@nx/devkit';
2
+ const SCRIPT_PATH = 'tools/generate-build-info.cjs';
3
+ const SCRIPT = `const { execSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const [, , outputPath, packageJsonPath] = process.argv;
8
+
9
+ const read = (command) => {
10
+ try {
11
+ return execSync(command, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
12
+ } catch {
13
+ return null;
14
+ }
15
+ };
16
+
17
+ const version = fs.existsSync(packageJsonPath)
18
+ ? JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')).version
19
+ : null;
20
+
21
+ const info = {
22
+ version: version ?? 'unknown',
23
+ sha: read('git rev-parse --short HEAD') ?? 'unknown',
24
+ branch: read('git rev-parse --abbrev-ref HEAD') ?? 'unknown',
25
+ builtAt: new Date().toISOString(),
26
+ };
27
+
28
+ const contents =
29
+ '// Generated by ' +
30
+ ${JSON.stringify(SCRIPT_PATH)} +
31
+ ' - do not edit.\\n\\nexport const APP_BUILD_INFO = ' +
32
+ JSON.stringify(info, null, 2) +
33
+ ' as const;\\n';
34
+
35
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
36
+ fs.writeFileSync(outputPath, contents);
37
+ `;
38
+ const PLACEHOLDER = `// Generated by ${SCRIPT_PATH} - do not edit.
39
+
40
+ export const APP_BUILD_INFO = {
41
+ version: 'unknown',
42
+ sha: 'unknown',
43
+ branch: 'unknown',
44
+ builtAt: '1970-01-01T00:00:00.000Z',
45
+ } as const;
46
+ `;
47
+ const relativeImport = (from, to) => {
48
+ const fromParts = from.split('/').slice(0, -1);
49
+ const toParts = to.replace(/\.ts$/, '').split('/');
50
+ while (fromParts.length && toParts.length && fromParts[0] === toParts[0]) {
51
+ fromParts.shift();
52
+ toParts.shift();
53
+ }
54
+ const up = fromParts.length ? fromParts.map(() => '..') : ['.'];
55
+ return [...up, ...toParts].join('/');
56
+ };
57
+ /** Wires the `provideQueryDevtools({ about })` call up to the generated constant, if it can be found. */
58
+ const wireProvider = (tree, projectRoot, outputPath) => {
59
+ const candidates = [];
60
+ visitNotIgnoredFiles(tree, projectRoot, (file) => {
61
+ if (!file.endsWith('.ts') ||
62
+ file.endsWith('.spec.ts') ||
63
+ file.endsWith('.test.ts') ||
64
+ file.endsWith('.stories.ts')) {
65
+ return;
66
+ }
67
+ const contents = tree.read(file, 'utf-8');
68
+ if (!contents?.includes('provideQueryDevtools('))
69
+ return;
70
+ candidates.push({ file, contents });
71
+ });
72
+ const alreadyWired = candidates.some(({ contents }) => /provideQueryDevtools\(\s*{[\s\S]*?about\s*:\s*APP_BUILD_INFO/.test(contents));
73
+ if (alreadyWired)
74
+ return true;
75
+ const candidate = candidates.find(({ contents }) => contents.includes('provideQueryDevtools()'));
76
+ if (!candidate) {
77
+ for (const { file } of candidates) {
78
+ logger.warn(`${file} already passes options to provideQueryDevtools() - add \`about: APP_BUILD_INFO\` to them yourself.`);
79
+ }
80
+ return candidates.length > 0;
81
+ }
82
+ const importPath = relativeImport(candidate.file, outputPath);
83
+ tree.write(candidate.file, `import { APP_BUILD_INFO } from '${importPath}';\n` +
84
+ candidate.contents.replace('provideQueryDevtools()', 'provideQueryDevtools({ about: APP_BUILD_INFO })'));
85
+ return true;
86
+ };
87
+ export const devtoolsAboutGenerator = async (tree, options) => {
88
+ const project = readProjectConfiguration(tree, options.project);
89
+ const outputPath = options.outputPath ?? joinPathFragments(project.sourceRoot ?? project.root, 'build-info.ts');
90
+ if (!tree.exists(SCRIPT_PATH))
91
+ tree.write(SCRIPT_PATH, SCRIPT);
92
+ if (!tree.exists(outputPath))
93
+ tree.write(outputPath, PLACEHOLDER);
94
+ const gitignore = tree.read('.gitignore', 'utf-8') ?? '';
95
+ // Regenerated on every build, and its SHA changes with every commit - committing it would put a
96
+ // one-line diff in every commit that says nothing about the change.
97
+ if (!gitignore.includes(outputPath)) {
98
+ tree.write('.gitignore', `${gitignore.trimEnd()}${gitignore.trim() ? '\n' : ''}${outputPath}\n`);
99
+ }
100
+ project.targets ??= {};
101
+ project.targets['build-info'] = {
102
+ executor: 'nx:run-commands',
103
+ options: {
104
+ commands: [`node ${SCRIPT_PATH} ${outputPath} ${joinPathFragments(project.root, 'package.json')}`],
105
+ },
106
+ };
107
+ for (const target of ['build', 'serve']) {
108
+ const config = project.targets[target];
109
+ if (!config)
110
+ continue;
111
+ config.dependsOn ??= [];
112
+ if (!config.dependsOn.includes('build-info'))
113
+ config.dependsOn.push('build-info');
114
+ }
115
+ updateProjectConfiguration(tree, options.project, project);
116
+ if (!wireProvider(tree, project.root, outputPath)) {
117
+ logger.warn(`No provideQueryDevtools() call found in ${project.root} - pass \`about: APP_BUILD_INFO\` to it yourself.`);
118
+ }
119
+ if (!options.skipFormat)
120
+ await formatFiles(tree);
121
+ };
122
+ export default devtoolsAboutGenerator;
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "devtools-about",
4
+ "title": "Query devtools About generator",
5
+ "description": "Generates the application's build info (version, commit SHA, branch, build time) and wires it into provideQueryDevtools({ about })",
6
+ "type": "object",
7
+ "properties": {
8
+ "project": {
9
+ "type": "string",
10
+ "description": "The application to wire up",
11
+ "$default": { "$source": "argv", "index": 0 },
12
+ "x-prompt": "Which application should report its build info?"
13
+ },
14
+ "outputPath": {
15
+ "type": "string",
16
+ "description": "Where to write the generated build-info file. Defaults to `<sourceRoot>/build-info.ts`."
17
+ },
18
+ "skipFormat": {
19
+ "type": "boolean",
20
+ "description": "Skip formatting files after generation",
21
+ "default": false
22
+ }
23
+ },
24
+ "required": ["project"]
25
+ }
@@ -1,14 +1,50 @@
1
1
  {
2
+ "$schema": "https://json.schemastore.org/nx-generators",
2
3
  "generators": {
3
4
  "migrate-to-v5": {
4
- "factory": "./migrate-to-v5/migration",
5
+ "implementation": "./migrate-to-v5/migration",
5
6
  "schema": "./migrate-to-v5/schema.json",
6
7
  "description": "Migrate to v5"
7
8
  },
8
- "tailwind-4-theme": {
9
- "factory": "./tailwind-4-theme/generator",
10
- "schema": "./tailwind-4-theme/schema.json",
11
- "description": "Generate theming for Tailwind CSS v4"
9
+ "migrate-provider-shape": {
10
+ "implementation": "./migrate-provider-shape/migration",
11
+ "schema": "./migrate-provider-shape/schema.json",
12
+ "description": "Rewrite destructured provider tuples into the tree-shakeable definition shape"
13
+ },
14
+ "migrate-create-destroy": {
15
+ "implementation": "./migrate-create-destroy/migration",
16
+ "schema": "./migrate-create-destroy/schema.json",
17
+ "description": "Rewrite takeUntil(this.destroy$) on a createDestroy() field into takeUntilDestroyed()"
18
+ },
19
+ "migrate-surface-interaction-swatch": {
20
+ "implementation": "./migrate-surface-interaction-swatch/migration",
21
+ "schema": "./migrate-surface-interaction-swatch/schema.json",
22
+ "description": "Rewrite surface theme interactionColor maps into the { color, onColor, inkColor } swatch shape"
23
+ },
24
+ "migrate-color-theme-variants": {
25
+ "implementation": "./migrate-color-theme-variants/migration",
26
+ "schema": "./migrate-color-theme-variants/schema.json",
27
+ "description": "Rewrite surface theme colorTheme and semanticColorThemes into colorThemeVariants"
28
+ },
29
+ "migrate-legacy-color-themes": {
30
+ "implementation": "./migrate-legacy-color-themes/migration",
31
+ "schema": "./migrate-legacy-color-themes/schema.json",
32
+ "description": "Rewrite provideColorThemes into provideColorThemesWithTailwind4 and list the Tailwind 4 setup that is left"
33
+ },
34
+ "tailwind-4-color-theme": {
35
+ "implementation": "./tailwind-4-color-theme/generator",
36
+ "schema": "./tailwind-4-color-theme/schema.json",
37
+ "description": "Generate color theming for Tailwind CSS v4"
38
+ },
39
+ "tailwind-4-surface-theme": {
40
+ "implementation": "./tailwind-4-surface-theme/generator",
41
+ "schema": "./tailwind-4-surface-theme/schema.json",
42
+ "description": "Generate surface theming for Tailwind CSS v4"
43
+ },
44
+ "devtools-about": {
45
+ "implementation": "./devtools-about/generator",
46
+ "schema": "./devtools-about/schema.json",
47
+ "description": "Generate the app's build info and wire it into provideQueryDevtools({ about })"
12
48
  }
13
49
  }
14
50
  }
@@ -0,0 +1,136 @@
1
+ import { Project, SyntaxKind } from 'ts-morph';
2
+ const createProject = () => new Project({
3
+ useInMemoryFileSystem: true,
4
+ compilerOptions: {
5
+ target: 99,
6
+ module: 99,
7
+ },
8
+ });
9
+ const findProperty = (literal, name) => {
10
+ const property = literal.getProperty(name);
11
+ return property?.isKind(SyntaxKind.PropertyAssignment) ? property : undefined;
12
+ };
13
+ const stringValue = (property) => {
14
+ const initializer = property?.getInitializer();
15
+ return initializer?.isKind(SyntaxKind.StringLiteral) || initializer?.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)
16
+ ? initializer.getLiteralText()
17
+ : undefined;
18
+ };
19
+ const literalsOf = (sourceFile) => sourceFile.getDescendantsOfKind(SyntaxKind.ObjectLiteralExpression);
20
+ export const collectColorThemesInFile = (filePath, content) => {
21
+ if (!content.includes('primary'))
22
+ return [];
23
+ return literalsOf(createProject().createSourceFile(filePath, content)).flatMap((literal) => {
24
+ const name = stringValue(findProperty(literal, 'name'));
25
+ if (!name || !literal.getProperty('primary'))
26
+ return [];
27
+ return [
28
+ {
29
+ name,
30
+ type: stringValue(findProperty(literal, 'type')),
31
+ isDefault: findProperty(literal, 'isDefault')?.getInitializer()?.getText() === 'true',
32
+ },
33
+ ];
34
+ });
35
+ };
36
+ const uniqueName = (names) => {
37
+ const unique = [...new Set(names)];
38
+ return unique.length === 1 ? unique[0] : undefined;
39
+ };
40
+ const quote = (value) => (/^[A-Za-z_$][\w$]*$/.test(value) ? value : `'${value}'`);
41
+ /**
42
+ * Rewrites a surface theme's deprecated `colorTheme` and `semanticColorThemes` into `colorThemeVariants`, keyed by
43
+ * the `isDefault` theme and the theme of each `type` in `themes`. Existing `colorThemeVariants` entries win, then
44
+ * `semanticColorThemes`, then `colorTheme`, the same order the runtime resolves them in.
45
+ */
46
+ export const migrateColorThemeVariantsInFile = (filePath, content, themes) => {
47
+ if (!content.includes('colorTheme') && !content.includes('semanticColorThemes')) {
48
+ return { changed: false, content, unresolved: [] };
49
+ }
50
+ const defaultName = uniqueName(themes.filter((theme) => theme.isDefault).map((theme) => theme.name));
51
+ const nameOfType = (type) => uniqueName(themes.filter((theme) => theme.type === type).map((theme) => theme.name));
52
+ const sourceFile = createProject().createSourceFile(filePath, content);
53
+ const unresolved = [];
54
+ let changed = false;
55
+ const surfaces = literalsOf(sourceFile).filter((literal) => literal.getProperty('elevation'));
56
+ for (const literal of surfaces) {
57
+ const colorTheme = findProperty(literal, 'colorTheme');
58
+ const semantic = findProperty(literal, 'semanticColorThemes');
59
+ if (!colorTheme && !semantic)
60
+ continue;
61
+ const surfaceName = stringValue(findProperty(literal, 'name')) ?? '<unnamed surface>';
62
+ const variants = findProperty(literal, 'colorThemeVariants');
63
+ const variantsLiteral = variants?.getInitializer();
64
+ if (variants && !variantsLiteral?.isKind(SyntaxKind.ObjectLiteralExpression)) {
65
+ unresolved.push(`${surfaceName}: colorThemeVariants is not an object literal`);
66
+ continue;
67
+ }
68
+ const existingKeys = new Set(variantsLiteral?.isKind(SyntaxKind.ObjectLiteralExpression)
69
+ ? variantsLiteral
70
+ .getProperties()
71
+ .flatMap((property) => property.isKind(SyntaxKind.PropertyAssignment) ? [property.getName().replace(/['"]/g, '')] : [])
72
+ : []);
73
+ const additions = new Map();
74
+ const toRemove = [];
75
+ const add = (key, value) => {
76
+ if (!existingKeys.has(key) && !additions.has(key))
77
+ additions.set(key, value);
78
+ };
79
+ let semanticResolved = true;
80
+ if (semantic) {
81
+ const semanticLiteral = semantic.getInitializer();
82
+ if (!semanticLiteral?.isKind(SyntaxKind.ObjectLiteralExpression)) {
83
+ semanticResolved = false;
84
+ unresolved.push(`${surfaceName}: semanticColorThemes is not an object literal`);
85
+ }
86
+ else {
87
+ const pending = [];
88
+ for (const property of semanticLiteral.getProperties()) {
89
+ const type = property.isKind(SyntaxKind.PropertyAssignment) ? property.getName().replace(/['"]/g, '') : '';
90
+ const value = property.isKind(SyntaxKind.PropertyAssignment) ? stringValue(property) : undefined;
91
+ const key = type ? nameOfType(type) : undefined;
92
+ if (!value || !key) {
93
+ semanticResolved = false;
94
+ unresolved.push(`${surfaceName}: semanticColorThemes entry ${property.getText()} has no single color theme of that type or no string value`);
95
+ continue;
96
+ }
97
+ pending.push([key, value]);
98
+ }
99
+ if (semanticResolved)
100
+ pending.forEach(([key, value]) => add(key, value));
101
+ }
102
+ }
103
+ let colorThemeResolved = true;
104
+ if (colorTheme) {
105
+ const value = stringValue(colorTheme);
106
+ if (!value || !defaultName) {
107
+ colorThemeResolved = false;
108
+ unresolved.push(`${surfaceName}: colorTheme ${colorTheme.getInitializer()?.getText()} needs a string value and exactly one isDefault color theme`);
109
+ }
110
+ else if (value !== defaultName) {
111
+ add(defaultName, value);
112
+ }
113
+ }
114
+ const added = [...additions];
115
+ if (semantic && semanticResolved) {
116
+ toRemove.push(semantic);
117
+ }
118
+ if (colorTheme && colorThemeResolved) {
119
+ toRemove.push(colorTheme);
120
+ }
121
+ if (toRemove.length === 0)
122
+ continue;
123
+ if (added.length > 0) {
124
+ const text = added.map(([key, value]) => `${quote(key)}: '${value}'`).join(', ');
125
+ if (variantsLiteral?.isKind(SyntaxKind.ObjectLiteralExpression)) {
126
+ variantsLiteral.addPropertyAssignments(added.map(([key, value]) => ({ name: quote(key), initializer: `'${value}'` })));
127
+ }
128
+ else {
129
+ toRemove.shift()?.replaceWithText(`colorThemeVariants: { ${text} }`);
130
+ }
131
+ }
132
+ toRemove.forEach((property) => property.remove());
133
+ changed = true;
134
+ }
135
+ return { changed, content: changed ? sourceFile.getFullText() : content, unresolved };
136
+ };
@@ -0,0 +1,46 @@
1
+ import { formatFiles, visitNotIgnoredFiles } from '@nx/devkit';
2
+ import { createMigrationScope } from '../migrate-provider-shape/migration-scope.js';
3
+ import { collectColorThemesInFile, migrateColorThemeVariantsInFile } from './color-theme-variants.js';
4
+ const isSource = (filePath) => filePath.endsWith('.ts') && !filePath.endsWith('.d.ts');
5
+ export default async function migrateColorThemeVariants(tree, schema) {
6
+ console.log('\nšŸ”„ Migrating surface colorTheme and semanticColorThemes to colorThemeVariants...');
7
+ const themes = [];
8
+ visitNotIgnoredFiles(tree, '', (filePath) => {
9
+ if (!isSource(filePath))
10
+ return;
11
+ const content = tree.read(filePath, 'utf-8');
12
+ if (content)
13
+ themes.push(...collectColorThemesInFile(filePath, content));
14
+ });
15
+ const defaults = themes.filter((theme) => theme.isDefault).map((theme) => theme.name);
16
+ console.log(` Color themes found: ${themes.map((theme) => theme.name).join(', ') || 'none'}`);
17
+ console.log(` Default color theme: ${defaults.join(', ') || 'none'}`);
18
+ const scope = createMigrationScope(tree, schema);
19
+ console.log(` Scope: ${scope.describe()}`);
20
+ let filesChanged = 0;
21
+ let unresolvedCount = 0;
22
+ scope.visit(tree, (filePath) => {
23
+ if (!isSource(filePath))
24
+ return;
25
+ const before = tree.read(filePath, 'utf-8');
26
+ if (!before)
27
+ return;
28
+ const result = migrateColorThemeVariantsInFile(filePath, before, themes);
29
+ for (const message of result.unresolved) {
30
+ unresolvedCount++;
31
+ console.warn(` ⚠ ${filePath}: ${message}`);
32
+ }
33
+ if (!result.changed)
34
+ return;
35
+ tree.write(filePath, result.content);
36
+ filesChanged++;
37
+ console.log(` āœ“ ${filePath}`);
38
+ });
39
+ console.log(filesChanged === 0 ? ' Nothing to migrate.\n' : `\nāœ… Migrated ${filesChanged} file(s).\n`);
40
+ if (unresolvedCount > 0) {
41
+ console.warn(` Review ${unresolvedCount} unresolved value(s) manually. They keep working until the next major.\n`);
42
+ }
43
+ if (!schema.skipFormat) {
44
+ await formatFiles(tree);
45
+ }
46
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "migrate-color-theme-variants",
4
+ "title": "Migrate surface colorTheme and semanticColorThemes to colorThemeVariants",
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
+ }