@craft-ts/dev-tools 0.7.0-beta.13
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/LICENSE +21 -0
- package/README.md +154 -0
- package/generators.json +26 -0
- package/package.json +119 -0
- package/src/bin/craft-architecture-check.d.ts +2 -0
- package/src/bin/craft-architecture-check.js +51 -0
- package/src/bin/craft-architecture-check.js.map +1 -0
- package/src/bin/craft-brand.d.ts +2 -0
- package/src/bin/craft-brand.js +9 -0
- package/src/bin/craft-brand.js.map +1 -0
- package/src/bin/craft-graph.d.ts +2 -0
- package/src/bin/craft-graph.js +72 -0
- package/src/bin/craft-graph.js.map +1 -0
- package/src/bin/craft-migrate-architecture.d.ts +2 -0
- package/src/bin/craft-migrate-architecture.js +64 -0
- package/src/bin/craft-migrate-architecture.js.map +1 -0
- package/src/bin/craft-migrate-components.d.ts +2 -0
- package/src/bin/craft-migrate-components.js +67 -0
- package/src/bin/craft-migrate-components.js.map +1 -0
- package/src/bin/craft-migrate-primitives.d.ts +2 -0
- package/src/bin/craft-migrate-primitives.js +71 -0
- package/src/bin/craft-migrate-primitives.js.map +1 -0
- package/src/bin/craft-migrate-routes.d.ts +2 -0
- package/src/bin/craft-migrate-routes.js +77 -0
- package/src/bin/craft-migrate-routes.js.map +1 -0
- package/src/bin/craft-migrate-services.d.ts +2 -0
- package/src/bin/craft-migrate-services.js +75 -0
- package/src/bin/craft-migrate-services.js.map +1 -0
- package/src/bin/craft-migrate-template.d.ts +2 -0
- package/src/bin/craft-migrate-template.js +68 -0
- package/src/bin/craft-migrate-template.js.map +1 -0
- package/src/bin/craft-migrate.d.ts +2 -0
- package/src/bin/craft-migrate.js +91 -0
- package/src/bin/craft-migrate.js.map +1 -0
- package/src/bin/craft.d.ts +2 -0
- package/src/bin/craft.js +318 -0
- package/src/bin/craft.js.map +1 -0
- package/src/eslint-rules/anchor-has-href.cjs +35 -0
- package/src/eslint-rules/app-start-registry-match.cjs +355 -0
- package/src/eslint-rules/button-has-type.cjs +50 -0
- package/src/eslint-rules/control-has-accessible-name.cjs +56 -0
- package/src/eslint-rules/craft-component-name-match.cjs +8 -0
- package/src/eslint-rules/craft-computed-name-match.cjs +63 -0
- package/src/eslint-rules/craft-css-token-registry.cjs +17 -0
- package/src/eslint-rules/craft-css-var-naming.cjs +18 -0
- package/src/eslint-rules/craft-css-vars-contract.cjs +34 -0
- package/src/eslint-rules/craft-directive-name-match.cjs +8 -0
- package/src/eslint-rules/craft-method-name-match.cjs +8 -0
- package/src/eslint-rules/craft-name-match-utils.cjs +179 -0
- package/src/eslint-rules/craft-primitive-context.cjs +46 -0
- package/src/eslint-rules/craft-signal-source-name-match.cjs +8 -0
- package/src/eslint-rules/craft-source-name-match.cjs +8 -0
- package/src/eslint-rules/craft-styles-scope-safe.cjs +25 -0
- package/src/eslint-rules/css-rule-utils.cjs +109 -0
- package/src/eslint-rules/global-exception-registry-match.cjs +562 -0
- package/src/eslint-rules/heading-has-content.cjs +31 -0
- package/src/eslint-rules/html-helpers.cjs +115 -0
- package/src/eslint-rules/hyperscript-walk.cjs +249 -0
- package/src/eslint-rules/iframe-has-title.cjs +26 -0
- package/src/eslint-rules/img-has-alt.cjs +28 -0
- package/src/eslint-rules/index.cjs +209 -0
- package/src/eslint-rules/index.d.cts +5 -0
- package/src/eslint-rules/label-has-associated-control.cjs +67 -0
- package/src/eslint-rules/no-async-await.cjs +46 -0
- package/src/eslint-rules/no-craft-computed-side-effects.cjs +301 -0
- package/src/eslint-rules/no-craft-use-in-template.cjs +89 -0
- package/src/eslint-rules/no-craft-use.cjs +42 -0
- package/src/eslint-rules/no-direct-temporal-globals.cjs +73 -0
- package/src/eslint-rules/no-effect-in-params.cjs +265 -0
- package/src/eslint-rules/no-effect-outside-loaders.cjs +3 -0
- package/src/eslint-rules/no-ephemeral-template-form-state.cjs +131 -0
- package/src/eslint-rules/no-hardcoded-design-values.cjs +15 -0
- package/src/eslint-rules/no-heading-level-skip.cjs +101 -0
- package/src/eslint-rules/no-imperative-craft-resource-trigger.cjs +291 -0
- package/src/eslint-rules/no-important-in-component-styles.cjs +12 -0
- package/src/eslint-rules/no-injection-token.cjs +173 -0
- package/src/eslint-rules/no-invalid-insertion-pipe.cjs +162 -0
- package/src/eslint-rules/no-manual-route-provider-list.cjs +85 -0
- package/src/eslint-rules/no-noninteractive-element-interactions.cjs +66 -0
- package/src/eslint-rules/no-positive-tabindex.cjs +44 -0
- package/src/eslint-rules/no-redundant-primitive-insertion.cjs +361 -0
- package/src/eslint-rules/no-render-writes.cjs +106 -0
- package/src/eslint-rules/no-throw.cjs +132 -0
- package/src/eslint-rules/no-transition-actions.cjs +133 -0
- package/src/eslint-rules/no-type-assertions-in-template.cjs +211 -0
- package/src/eslint-rules/no-widened-route-provider-context.cjs +39 -0
- package/src/eslint-rules/prefer-browser-boundaries.cjs +92 -0
- package/src/eslint-rules/prefer-craft-http-transport.cjs +103 -0
- package/src/eslint-rules/prefer-craft-template-blocks.cjs +492 -0
- package/src/eslint-rules/prefer-direct-yieldable-callback.cjs +105 -0
- package/src/eslint-rules/prefer-named-html-helpers.cjs +33 -0
- package/src/eslint-rules/prefer-query-method-over-state-trigger.cjs +282 -0
- package/src/eslint-rules/prefer-relative-heading.cjs +43 -0
- package/src/eslint-rules/provide-host-name-match-component.cjs +455 -0
- package/src/eslint-rules/recommended-config.cjs +77 -0
- package/src/eslint-rules/require-assert-exhaustive-route-exceptions.cjs +259 -0
- package/src/eslint-rules/require-cascade-route-di-check.cjs +223 -0
- package/src/eslint-rules/require-child-route-mount-check.cjs +224 -0
- package/src/eslint-rules/require-component-monitoring.cjs +212 -0
- package/src/eslint-rules/require-craft-exception-handler.cjs +142 -0
- package/src/eslint-rules/require-craft-method-for-yieldable-callback.cjs +236 -0
- package/src/eslint-rules/require-craft-resource-trigger-yield.cjs +209 -0
- package/src/eslint-rules/require-effect-adapters.cjs +86 -0
- package/src/eslint-rules/require-exception-component-di-check.cjs +314 -0
- package/src/eslint-rules/require-focus-visible.cjs +36 -0
- package/src/eslint-rules/require-interactive-local-name.cjs +105 -0
- package/src/eslint-rules/require-lazy-load-with-retry.cjs +148 -0
- package/src/eslint-rules/require-outlet-heading-section.cjs +70 -0
- package/src/eslint-rules/require-pending-component-di-check.cjs +490 -0
- package/src/eslint-rules/require-primitive-context.cjs +156 -0
- package/src/eslint-rules/require-primitive-derived-property.cjs +598 -0
- package/src/eslint-rules/require-primitive-generator-unwrap.cjs +274 -0
- package/src/eslint-rules/require-reactive-template-bindings.cjs +305 -0
- package/src/eslint-rules/require-reduced-motion.cjs +38 -0
- package/src/eslint-rules/require-route-heading-outline.cjs +166 -0
- package/src/eslint-rules/require-yieldable-insertion-write.cjs +98 -0
- package/src/eslint-rules/require-yieldable-reactive-read.cjs +102 -0
- package/src/eslint-rules/require-yieldable-template-method.cjs +424 -0
- package/src/eslint-rules/role-has-required-aria.cjs +38 -0
- package/src/eslint-rules/server-function-client-match.cjs +283 -0
- package/src/eslint-rules/target-blank-noopener.cjs +31 -0
- package/src/eslint-rules/template-element-name-unique.cjs +124 -0
- package/src/eslint-rules/valid-aria.cjs +38 -0
- package/src/generators/route/compat.d.ts +3 -0
- package/src/generators/route/compat.js +6 -0
- package/src/generators/route/compat.js.map +1 -0
- package/src/generators/route/generator.d.ts +31 -0
- package/src/generators/route/generator.js +402 -0
- package/src/generators/route/generator.js.map +1 -0
- package/src/generators/route/schema.json +50 -0
- package/src/generators/route/split-schema.json +19 -0
- package/src/index.d.ts +21 -0
- package/src/index.js +23 -0
- package/src/index.js.map +1 -0
- package/src/scripts/angular-brand-codemod.d.ts +139 -0
- package/src/scripts/angular-brand-codemod.js +2318 -0
- package/src/scripts/angular-brand-codemod.js.map +1 -0
- package/src/scripts/angular-brand-codemod.ts +3985 -0
- package/src/scripts/architecture/migrate-architecture.d.ts +14 -0
- package/src/scripts/architecture/migrate-architecture.js +534 -0
- package/src/scripts/architecture/migrate-architecture.js.map +1 -0
- package/src/scripts/architecture/migrate-architecture.ts +653 -0
- package/src/scripts/architecture-graph.d.ts +368 -0
- package/src/scripts/architecture-graph.js +2191 -0
- package/src/scripts/architecture-graph.js.map +1 -0
- package/src/scripts/architecture-graph.ts +3136 -0
- package/src/scripts/components/migrate-components.d.ts +20 -0
- package/src/scripts/components/migrate-components.js +163 -0
- package/src/scripts/components/migrate-components.js.map +1 -0
- package/src/scripts/components/migrate-components.ts +227 -0
- package/src/scripts/components/migration-diagnostic.d.ts +7 -0
- package/src/scripts/components/migration-diagnostic.js +2 -0
- package/src/scripts/components/migration-diagnostic.js.map +1 -0
- package/src/scripts/components/migration-diagnostic.ts +8 -0
- package/src/scripts/create/create-project.d.ts +20 -0
- package/src/scripts/create/create-project.js +783 -0
- package/src/scripts/create/create-project.js.map +1 -0
- package/src/scripts/create/create-project.ts +849 -0
- package/src/scripts/data-flow-graph.d.ts +36 -0
- package/src/scripts/data-flow-graph.js +186 -0
- package/src/scripts/data-flow-graph.js.map +1 -0
- package/src/scripts/data-flow-graph.ts +240 -0
- package/src/scripts/dependency-graph.d.ts +152 -0
- package/src/scripts/dependency-graph.js +4456 -0
- package/src/scripts/dependency-graph.js.map +1 -0
- package/src/scripts/dependency-graph.ts +5908 -0
- package/src/scripts/effect-dependency-graph.d.ts +89 -0
- package/src/scripts/effect-dependency-graph.js +487 -0
- package/src/scripts/effect-dependency-graph.js.map +1 -0
- package/src/scripts/effect-dependency-graph.ts +630 -0
- package/src/scripts/migrate.d.ts +37 -0
- package/src/scripts/migrate.js +98 -0
- package/src/scripts/migrate.js.map +1 -0
- package/src/scripts/migrate.ts +162 -0
- package/src/scripts/migration-workspace.d.ts +2 -0
- package/src/scripts/migration-workspace.js +67 -0
- package/src/scripts/migration-workspace.js.map +1 -0
- package/src/scripts/migration-workspace.ts +93 -0
- package/src/scripts/primitives/migrate-named-primitives.d.ts +50 -0
- package/src/scripts/primitives/migrate-named-primitives.js +487 -0
- package/src/scripts/primitives/migrate-named-primitives.js.map +1 -0
- package/src/scripts/primitives/migrate-named-primitives.ts +627 -0
- package/src/scripts/primitives/migrate-primitive-generators.d.ts +30 -0
- package/src/scripts/primitives/migrate-primitive-generators.js +309 -0
- package/src/scripts/primitives/migrate-primitive-generators.js.map +1 -0
- package/src/scripts/primitives/migrate-primitive-generators.ts +410 -0
- package/src/scripts/primitives/migrate-primitives.d.ts +27 -0
- package/src/scripts/primitives/migrate-primitives.js +427 -0
- package/src/scripts/primitives/migrate-primitives.js.map +1 -0
- package/src/scripts/primitives/migrate-primitives.ts +655 -0
- package/src/scripts/primitives/migrate-yieldable-reactive-reads.d.ts +23 -0
- package/src/scripts/primitives/migrate-yieldable-reactive-reads.js +382 -0
- package/src/scripts/primitives/migrate-yieldable-reactive-reads.js.map +1 -0
- package/src/scripts/primitives/migrate-yieldable-reactive-reads.ts +499 -0
- package/src/scripts/primitives/migration-diagnostic.d.ts +8 -0
- package/src/scripts/primitives/migration-diagnostic.js +2 -0
- package/src/scripts/primitives/migration-diagnostic.js.map +1 -0
- package/src/scripts/primitives/migration-diagnostic.ts +15 -0
- package/src/scripts/routes/migrate-routes.d.ts +34 -0
- package/src/scripts/routes/migrate-routes.js +657 -0
- package/src/scripts/routes/migrate-routes.js.map +1 -0
- package/src/scripts/routes/migrate-routes.ts +895 -0
- package/src/scripts/routes/migration-diagnostic.d.ts +16 -0
- package/src/scripts/routes/migration-diagnostic.js +2 -0
- package/src/scripts/routes/migration-diagnostic.js.map +1 -0
- package/src/scripts/routes/migration-diagnostic.ts +25 -0
- package/src/scripts/routes/route-command.d.ts +75 -0
- package/src/scripts/routes/route-command.js +1169 -0
- package/src/scripts/routes/route-command.js.map +1 -0
- package/src/scripts/routes/route-command.ts +1807 -0
- package/src/scripts/routes/verify-routes.d.ts +60 -0
- package/src/scripts/routes/verify-routes.js +1121 -0
- package/src/scripts/routes/verify-routes.js.map +1 -0
- package/src/scripts/routes/verify-routes.ts +1511 -0
- package/src/scripts/services/config.d.ts +2 -0
- package/src/scripts/services/config.js +38 -0
- package/src/scripts/services/config.js.map +1 -0
- package/src/scripts/services/config.ts +60 -0
- package/src/scripts/services/migrate-services.d.ts +29 -0
- package/src/scripts/services/migrate-services.js +924 -0
- package/src/scripts/services/migrate-services.js.map +1 -0
- package/src/scripts/services/migrate-services.ts +1348 -0
- package/src/scripts/services/migration-diagnostic.d.ts +8 -0
- package/src/scripts/services/migration-diagnostic.js +2 -0
- package/src/scripts/services/migration-diagnostic.js.map +1 -0
- package/src/scripts/services/migration-diagnostic.ts +28 -0
- package/src/scripts/template/migrate-template.d.ts +16 -0
- package/src/scripts/template/migrate-template.js +349 -0
- package/src/scripts/template/migrate-template.js.map +1 -0
- package/src/scripts/template/migrate-template.ts +418 -0
- package/src/template-migration.d.ts +1 -0
- package/src/template-migration.js +2 -0
- package/src/template-migration.js.map +1 -0
- package/src/testing.d.ts +19 -0
- package/src/testing.js +2 -0
- package/src/testing.js.map +1 -0
- package/src/tsconfig.codemod.json +13 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
export type TemplateMigrationDiagnostic = {
|
|
2
|
+
readonly code: 'UNSUPPORTED_ANGULAR_SYNTAX' | 'MALFORMED_TEMPLATE';
|
|
3
|
+
readonly message: string;
|
|
4
|
+
readonly offset?: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type TemplateMigrationOptions = {
|
|
8
|
+
readonly componentName?: string;
|
|
9
|
+
readonly includeImport?: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type TemplateMigrationResult = {
|
|
13
|
+
readonly code: string;
|
|
14
|
+
readonly imports: readonly string[];
|
|
15
|
+
readonly diagnostics: readonly TemplateMigrationDiagnostic[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type HtmlNode = HtmlElement | HtmlText;
|
|
19
|
+
type HtmlElement = {
|
|
20
|
+
readonly kind: 'element';
|
|
21
|
+
readonly tag: string;
|
|
22
|
+
readonly attributes: readonly HtmlAttribute[];
|
|
23
|
+
readonly children: readonly HtmlNode[];
|
|
24
|
+
};
|
|
25
|
+
type HtmlText = { readonly kind: 'text'; readonly value: string };
|
|
26
|
+
type HtmlAttribute = { readonly name: string; readonly value: string | true };
|
|
27
|
+
|
|
28
|
+
const nativeHelpers = new Set([
|
|
29
|
+
'a',
|
|
30
|
+
'article',
|
|
31
|
+
'aside',
|
|
32
|
+
'button',
|
|
33
|
+
'div',
|
|
34
|
+
'footer',
|
|
35
|
+
'form',
|
|
36
|
+
'h1',
|
|
37
|
+
'h2',
|
|
38
|
+
'h3',
|
|
39
|
+
'h4',
|
|
40
|
+
'h5',
|
|
41
|
+
'h6',
|
|
42
|
+
'header',
|
|
43
|
+
'iframe',
|
|
44
|
+
'img',
|
|
45
|
+
'input',
|
|
46
|
+
'label',
|
|
47
|
+
'legend',
|
|
48
|
+
'li',
|
|
49
|
+
'main',
|
|
50
|
+
'nav',
|
|
51
|
+
'ol',
|
|
52
|
+
'option',
|
|
53
|
+
'p',
|
|
54
|
+
'section',
|
|
55
|
+
'select',
|
|
56
|
+
'small',
|
|
57
|
+
'span',
|
|
58
|
+
'strong',
|
|
59
|
+
'svg',
|
|
60
|
+
'table',
|
|
61
|
+
'tbody',
|
|
62
|
+
'td',
|
|
63
|
+
'textarea',
|
|
64
|
+
'th',
|
|
65
|
+
'thead',
|
|
66
|
+
'tr',
|
|
67
|
+
'ul',
|
|
68
|
+
'dialog',
|
|
69
|
+
'fieldset',
|
|
70
|
+
'figure',
|
|
71
|
+
'figcaption',
|
|
72
|
+
'caption',
|
|
73
|
+
'area',
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
const voidTags = new Set([
|
|
77
|
+
'area',
|
|
78
|
+
'base',
|
|
79
|
+
'br',
|
|
80
|
+
'col',
|
|
81
|
+
'embed',
|
|
82
|
+
'hr',
|
|
83
|
+
'img',
|
|
84
|
+
'input',
|
|
85
|
+
'link',
|
|
86
|
+
'meta',
|
|
87
|
+
'param',
|
|
88
|
+
'source',
|
|
89
|
+
'track',
|
|
90
|
+
'wbr',
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
/** Converts an HTML/Web Component snippet into a Craft template expression. */
|
|
94
|
+
export function migrateTemplateToCraft(
|
|
95
|
+
source: string,
|
|
96
|
+
options: TemplateMigrationOptions = {},
|
|
97
|
+
): TemplateMigrationResult {
|
|
98
|
+
const diagnostics: TemplateMigrationDiagnostic[] = [];
|
|
99
|
+
const roots = new HtmlSnippetParser(source, diagnostics).parse();
|
|
100
|
+
const imports = new Set<string>();
|
|
101
|
+
const expression = renderNodes(roots, imports, diagnostics, 0);
|
|
102
|
+
const componentName = options.componentName?.trim();
|
|
103
|
+
const body = componentName
|
|
104
|
+
? renderComponent(componentName, expression, imports)
|
|
105
|
+
: `({}) => ${expression}`;
|
|
106
|
+
const importLine =
|
|
107
|
+
options.includeImport !== false && imports.size > 0
|
|
108
|
+
? `import { ${[...imports].sort().join(', ')} } from '@craft-ts/component';\n\n`
|
|
109
|
+
: '';
|
|
110
|
+
return {
|
|
111
|
+
code: `${importLine}${body}\n`,
|
|
112
|
+
imports: [...imports].sort(),
|
|
113
|
+
diagnostics,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function renderComponent(
|
|
118
|
+
name: string,
|
|
119
|
+
expression: string,
|
|
120
|
+
imports: Set<string>,
|
|
121
|
+
): string {
|
|
122
|
+
imports.add('craftComponent');
|
|
123
|
+
const identifier = safeIdentifier(name, 'PastedTemplate');
|
|
124
|
+
return `export const ${identifier} = craftComponent(\n '${escapeSingleQuoted(name)}',\n {},\n () => ({}),\n () => ${expression},\n);`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function renderNodes(
|
|
128
|
+
nodes: readonly HtmlNode[],
|
|
129
|
+
imports: Set<string>,
|
|
130
|
+
diagnostics: TemplateMigrationDiagnostic[],
|
|
131
|
+
level: number,
|
|
132
|
+
): string {
|
|
133
|
+
const meaningful = nodes.filter(
|
|
134
|
+
(node) => node.kind !== 'text' || node.value.trim().length > 0,
|
|
135
|
+
);
|
|
136
|
+
if (meaningful.length === 0) return '[]';
|
|
137
|
+
if (meaningful.length === 1) {
|
|
138
|
+
return renderNode(meaningful[0], imports, diagnostics, level);
|
|
139
|
+
}
|
|
140
|
+
const indent = ' '.repeat(level);
|
|
141
|
+
const childIndent = ' '.repeat(level + 1);
|
|
142
|
+
return `[\n${meaningful
|
|
143
|
+
.map(
|
|
144
|
+
(node) =>
|
|
145
|
+
`${childIndent}${renderNode(node, imports, diagnostics, level + 1)},`,
|
|
146
|
+
)
|
|
147
|
+
.join('\n')}\n${indent}]`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function renderNode(
|
|
151
|
+
node: HtmlNode,
|
|
152
|
+
imports: Set<string>,
|
|
153
|
+
diagnostics: TemplateMigrationDiagnostic[],
|
|
154
|
+
level: number,
|
|
155
|
+
): string {
|
|
156
|
+
if (node.kind === 'text') return renderText(node.value);
|
|
157
|
+
const tag = node.tag.toLowerCase();
|
|
158
|
+
const helper = nativeHelpers.has(tag) ? tag : 'customElement';
|
|
159
|
+
imports.add(helper);
|
|
160
|
+
const props = renderAttributes(node.attributes, diagnostics);
|
|
161
|
+
const children = renderNodes(node.children, imports, diagnostics, level + 1);
|
|
162
|
+
const hasChildren = node.children.some(
|
|
163
|
+
(child) => child.kind !== 'text' || child.value.trim().length > 0,
|
|
164
|
+
);
|
|
165
|
+
if (helper === 'customElement') {
|
|
166
|
+
const tagArgument = `'${escapeSingleQuoted(tag)}'`;
|
|
167
|
+
return hasChildren
|
|
168
|
+
? `customElement(${tagArgument}${props ? `, ${props}` : ', {}'}, ${children})`
|
|
169
|
+
: `customElement(${tagArgument}${props ? `, ${props}` : ''})`;
|
|
170
|
+
}
|
|
171
|
+
return hasChildren
|
|
172
|
+
? `${helper}(${props ? `${props}, ` : ''}${children})`
|
|
173
|
+
: `${helper}(${props || ''})`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderAttributes(
|
|
177
|
+
attributes: readonly HtmlAttribute[],
|
|
178
|
+
diagnostics: TemplateMigrationDiagnostic[],
|
|
179
|
+
): string {
|
|
180
|
+
if (attributes.length === 0) return '';
|
|
181
|
+
const entries: string[] = [];
|
|
182
|
+
for (const attribute of attributes) {
|
|
183
|
+
const name = attribute.name;
|
|
184
|
+
if (name.startsWith('*') || name.startsWith('@')) {
|
|
185
|
+
diagnostics.push({
|
|
186
|
+
code: 'UNSUPPORTED_ANGULAR_SYNTAX',
|
|
187
|
+
message: `Structural Angular syntax '${name}' needs a manual ifBlock/each conversion.`,
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const bracketed = /^\[([^\]]+)\]$/.exec(name);
|
|
192
|
+
const event = /^\(([^)]+)\)$/.exec(name);
|
|
193
|
+
const key = bracketed?.[1] ?? event?.[1] ?? name;
|
|
194
|
+
const outputKey = quoteObjectKey(key);
|
|
195
|
+
if (event) {
|
|
196
|
+
const expression =
|
|
197
|
+
attribute.value === true ? 'undefined' : String(attribute.value);
|
|
198
|
+
entries.push(`${outputKey}: (event) => ${expression}`);
|
|
199
|
+
} else if (bracketed) {
|
|
200
|
+
const expression =
|
|
201
|
+
attribute.value === true ? 'undefined' : String(attribute.value);
|
|
202
|
+
entries.push(`${outputKey}: () => ${expression}`);
|
|
203
|
+
} else if (attribute.value === true) {
|
|
204
|
+
entries.push(`${outputKey}: true`);
|
|
205
|
+
} else {
|
|
206
|
+
const value = String(attribute.value);
|
|
207
|
+
entries.push(
|
|
208
|
+
`${outputKey}: ${interpolationExpression(value) ?? quote(value)}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return entries.length > 0 ? `{ ${entries.join(', ')} }` : '';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function renderText(value: string): string {
|
|
216
|
+
const decoded = decodeEntities(value);
|
|
217
|
+
return interpolationExpression(decoded) ?? quote(decoded);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function interpolationExpression(value: string): string | undefined {
|
|
221
|
+
const matches = [...value.matchAll(/{{\s*([\s\S]*?)\s*}}/g)];
|
|
222
|
+
if (matches.length === 0) return undefined;
|
|
223
|
+
if (matches.length === 1 && matches[0][0] === value) {
|
|
224
|
+
return `() => ${matches[0][1].trim()}`;
|
|
225
|
+
}
|
|
226
|
+
let template = value.replace(/`/g, '\\`');
|
|
227
|
+
for (const match of matches) {
|
|
228
|
+
template = template.replace(match[0], `\${${match[1].trim()}}`);
|
|
229
|
+
}
|
|
230
|
+
return `() => \`${template}\``;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function quote(value: string): string {
|
|
234
|
+
return `'${escapeSingleQuoted(value)}'`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function quoteObjectKey(value: string): string {
|
|
238
|
+
return /^[A-Za-z_$][\w$]*$/.test(value) ? value : quote(value);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function safeIdentifier(value: string, fallback: string): string {
|
|
242
|
+
const words = value
|
|
243
|
+
.replace(/[^A-Za-z0-9_$]/g, ' ')
|
|
244
|
+
.trim()
|
|
245
|
+
.split(/\s+/)
|
|
246
|
+
.filter(Boolean);
|
|
247
|
+
const result = words
|
|
248
|
+
.map((word, index) =>
|
|
249
|
+
index === 0 ? word : `${word[0]?.toUpperCase() ?? ''}${word.slice(1)}`,
|
|
250
|
+
)
|
|
251
|
+
.join('');
|
|
252
|
+
return /^[A-Za-z_$]/.test(result) ? result : fallback;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function escapeSingleQuoted(value: string): string {
|
|
256
|
+
return value
|
|
257
|
+
.replace(/\\/g, '\\\\')
|
|
258
|
+
.replace(/'/g, "\\'")
|
|
259
|
+
.replace(/\r?\n/g, '\\n');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function decodeEntities(value: string): string {
|
|
263
|
+
return value
|
|
264
|
+
.replace(/&/gi, '&')
|
|
265
|
+
.replace(/</gi, '<')
|
|
266
|
+
.replace(/>/gi, '>')
|
|
267
|
+
.replace(/"/gi, '"')
|
|
268
|
+
.replace(/'/gi, "'");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
class HtmlSnippetParser {
|
|
272
|
+
private index = 0;
|
|
273
|
+
|
|
274
|
+
constructor(
|
|
275
|
+
private readonly source: string,
|
|
276
|
+
private readonly diagnostics: TemplateMigrationDiagnostic[],
|
|
277
|
+
) {}
|
|
278
|
+
|
|
279
|
+
parse(): HtmlNode[] {
|
|
280
|
+
return this.parseNodes();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private parseNodes(stopTag?: string): HtmlNode[] {
|
|
284
|
+
const nodes: HtmlNode[] = [];
|
|
285
|
+
let textStart = this.index;
|
|
286
|
+
while (this.index < this.source.length) {
|
|
287
|
+
if (this.source.startsWith('<!--', this.index)) {
|
|
288
|
+
this.flushText(nodes, textStart);
|
|
289
|
+
const end = this.source.indexOf('-->', this.index + 4);
|
|
290
|
+
this.index = end < 0 ? this.source.length : end + 3;
|
|
291
|
+
textStart = this.index;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (this.source[this.index] !== '<') {
|
|
295
|
+
this.index += 1;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const closing = /^<\/\s*([\w:-]+)[^>]*>/.exec(
|
|
299
|
+
this.source.slice(this.index),
|
|
300
|
+
);
|
|
301
|
+
if (closing) {
|
|
302
|
+
this.flushText(nodes, textStart);
|
|
303
|
+
this.index += closing[0].length;
|
|
304
|
+
if (stopTag && closing[1].toLowerCase() !== stopTag) {
|
|
305
|
+
this.diagnostics.push({
|
|
306
|
+
code: 'MALFORMED_TEMPLATE',
|
|
307
|
+
message: `Unexpected closing tag </${closing[1]}>.`,
|
|
308
|
+
offset: this.index - closing[0].length,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
return nodes;
|
|
312
|
+
}
|
|
313
|
+
this.flushText(nodes, textStart);
|
|
314
|
+
const opening = this.readOpeningTag();
|
|
315
|
+
if (!opening) {
|
|
316
|
+
this.index += 1;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const children =
|
|
320
|
+
opening.selfClosing || voidTags.has(opening.tag)
|
|
321
|
+
? []
|
|
322
|
+
: this.parseNodes(opening.tag);
|
|
323
|
+
nodes.push({
|
|
324
|
+
kind: 'element',
|
|
325
|
+
tag: opening.tag,
|
|
326
|
+
attributes: opening.attributes,
|
|
327
|
+
children,
|
|
328
|
+
});
|
|
329
|
+
textStart = this.index;
|
|
330
|
+
}
|
|
331
|
+
this.flushText(nodes, textStart);
|
|
332
|
+
if (stopTag) {
|
|
333
|
+
this.diagnostics.push({
|
|
334
|
+
code: 'MALFORMED_TEMPLATE',
|
|
335
|
+
message: `Missing closing tag </${stopTag}>.`,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return nodes;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private flushText(nodes: HtmlNode[], start: number): void {
|
|
342
|
+
if (this.index > start)
|
|
343
|
+
nodes.push({ kind: 'text', value: this.source.slice(start, this.index) });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private readOpeningTag():
|
|
347
|
+
| {
|
|
348
|
+
readonly tag: string;
|
|
349
|
+
readonly attributes: readonly HtmlAttribute[];
|
|
350
|
+
readonly selfClosing: boolean;
|
|
351
|
+
}
|
|
352
|
+
| undefined {
|
|
353
|
+
const start = this.index;
|
|
354
|
+
if (!/^<[A-Za-z][\w:-]*/.test(this.source.slice(start))) return undefined;
|
|
355
|
+
let cursor = start + 1;
|
|
356
|
+
while (/[\w:-]/.test(this.source[cursor] ?? '')) cursor += 1;
|
|
357
|
+
const tag = this.source.slice(start + 1, cursor).toLowerCase();
|
|
358
|
+
const end = findTagEnd(this.source, cursor);
|
|
359
|
+
if (end < 0) {
|
|
360
|
+
this.diagnostics.push({
|
|
361
|
+
code: 'MALFORMED_TEMPLATE',
|
|
362
|
+
message: `Missing closing '>' for <${tag}>.`,
|
|
363
|
+
offset: start,
|
|
364
|
+
});
|
|
365
|
+
this.index = this.source.length;
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
const rawAttributes = this.source.slice(cursor, end);
|
|
369
|
+
this.index = end + 1;
|
|
370
|
+
return {
|
|
371
|
+
tag,
|
|
372
|
+
attributes: parseAttributes(rawAttributes),
|
|
373
|
+
selfClosing: /\/\s*$/.test(rawAttributes),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function findTagEnd(source: string, start: number): number {
|
|
379
|
+
let quoteCharacter = '';
|
|
380
|
+
for (let index = start; index < source.length; index += 1) {
|
|
381
|
+
const character = source[index];
|
|
382
|
+
if (quoteCharacter) {
|
|
383
|
+
if (character === quoteCharacter) quoteCharacter = '';
|
|
384
|
+
} else if (character === '"' || character === "'")
|
|
385
|
+
quoteCharacter = character;
|
|
386
|
+
else if (character === '>') return index;
|
|
387
|
+
}
|
|
388
|
+
return -1;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function parseAttributes(source: string): HtmlAttribute[] {
|
|
392
|
+
const attributes: HtmlAttribute[] = [];
|
|
393
|
+
let index = 0;
|
|
394
|
+
while (index < source.length) {
|
|
395
|
+
while (/\s/.test(source[index] ?? '') || source[index] === '/') index += 1;
|
|
396
|
+
if (index >= source.length) break;
|
|
397
|
+
const nameStart = index;
|
|
398
|
+
while (!/[\s=/>]/.test(source[index] ?? '')) index += 1;
|
|
399
|
+
const name = source.slice(nameStart, index);
|
|
400
|
+
while (/\s/.test(source[index] ?? '')) index += 1;
|
|
401
|
+
if (source[index] !== '=') {
|
|
402
|
+
attributes.push({ name, value: true });
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
index += 1;
|
|
406
|
+
while (/\s/.test(source[index] ?? '')) index += 1;
|
|
407
|
+
const quoteCharacter =
|
|
408
|
+
source[index] === '"' || source[index] === "'" ? source[index++] : '';
|
|
409
|
+
const valueStart = index;
|
|
410
|
+
if (quoteCharacter)
|
|
411
|
+
while (index < source.length && source[index] !== quoteCharacter)
|
|
412
|
+
index += 1;
|
|
413
|
+
else while (!/[\s>]/.test(source[index] ?? '')) index += 1;
|
|
414
|
+
attributes.push({ name, value: source.slice(valueStart, index) });
|
|
415
|
+
if (quoteCharacter && source[index] === quoteCharacter) index += 1;
|
|
416
|
+
}
|
|
417
|
+
return attributes;
|
|
418
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './scripts/template/migrate-template.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template-migration.js","sourceRoot":"","sources":["../../../../libs/dev-tools/src/template-migration.ts"],"names":[],"mappings":"AAAA,cAAc,wCAAwC,CAAC"}
|
package/src/testing.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level testing helpers.
|
|
3
|
+
*
|
|
4
|
+
* Published with dev-tools so applications can import the type assertions
|
|
5
|
+
* without depending on an internal workspace package:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import type { Equal, Expect } from '@craft-ts/dev-tools/testing';
|
|
9
|
+
* ```
|
|
10
|
+
*
|
|
11
|
+
* These are types only — nothing is emitted at runtime.
|
|
12
|
+
*/
|
|
13
|
+
type Prettify<T> = {
|
|
14
|
+
[K in keyof T]: T[K];
|
|
15
|
+
} & {};
|
|
16
|
+
export type Expect<T extends true> = T;
|
|
17
|
+
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false;
|
|
18
|
+
export type PrettifyEqual<X, Y, XP = Prettify<X>, YP = Prettify<Y>> = (<T>() => T extends XP ? 1 : 2) extends <T>() => T extends YP ? 1 : 2 ? true : false;
|
|
19
|
+
export {};
|
package/src/testing.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","sourceRoot":"","sources":["../../../../libs/dev-tools/src/testing.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.lib.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"allowSyntheticDefaultImports": true
|
|
10
|
+
},
|
|
11
|
+
"include": ["**/*.ts"],
|
|
12
|
+
"exclude": ["**/*.spec.ts", "**/*.test.ts"]
|
|
13
|
+
}
|