@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,490 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { IndentationText, Node, Project, QuoteKind, SyntaxKind } = require('ts-morph');
|
|
4
|
+
|
|
5
|
+
const projectCache = new Map();
|
|
6
|
+
|
|
7
|
+
const ROUTES_FACTORY = 'craftRoutes';
|
|
8
|
+
const ROUTE_FN = 'craftRoute';
|
|
9
|
+
const PENDING_PROP = 'pendingComponent';
|
|
10
|
+
const VT_PROP = 'withLoaderViewTransitionImage';
|
|
11
|
+
const CHECK_TYPE = 'RouteCheckedDI';
|
|
12
|
+
const CASCADE_TYPE = 'ValidateCascadeRoutesFile';
|
|
13
|
+
const CAN_RUN = 'CanRun';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ensures every route with a lazy `pendingComponent: () => import('./x')` is
|
|
17
|
+
* verified with the per-component, O(1) `RouteCheckedDI` check (setup.md
|
|
18
|
+
* "Escape hatch"). The aggregated cascade never sees the pending component, so
|
|
19
|
+
* its DI must be checked directly — and `RouteCheckedDI` is used (not a second
|
|
20
|
+
* aggregated pass) to stay off the instantiation-count budget.
|
|
21
|
+
*
|
|
22
|
+
* Autofix generates the whole block:
|
|
23
|
+
*
|
|
24
|
+
* type _Check<Feature>PendingDI = RouteCheckedDI<
|
|
25
|
+
* import('./x').GenDeps_<DefaultExportClass>,
|
|
26
|
+
* '<CollectionParam>Params' | '<Collection><Route>ViewTransition', // route auto-provides these
|
|
27
|
+
* <ParentValues>, // borrowed from the cascade check
|
|
28
|
+
* 'pending component: <path>'
|
|
29
|
+
* >;
|
|
30
|
+
* type _CanRun<Feature>Pending = CanRun<_Check<Feature>PendingDI>;
|
|
31
|
+
*
|
|
32
|
+
* The `GenDeps_*` alias is read from the skeleton module, the available service
|
|
33
|
+
* names are derived from the route's path params + (when present) its
|
|
34
|
+
* `withLoaderViewTransitionImage` payload, and the parent context is taken from
|
|
35
|
+
* the collection's own `ValidateCascadeRoutesFile<…, typeof xRoutes>`. If that
|
|
36
|
+
* cascade check is absent (so the context can't be inferred), the rule reports
|
|
37
|
+
* without a fix.
|
|
38
|
+
*/
|
|
39
|
+
module.exports = {
|
|
40
|
+
meta: {
|
|
41
|
+
type: 'problem',
|
|
42
|
+
docs: {
|
|
43
|
+
description:
|
|
44
|
+
'Ensure every route with a lazy pendingComponent is verified with RouteCheckedDI(...).',
|
|
45
|
+
},
|
|
46
|
+
fixable: 'code',
|
|
47
|
+
schema: [],
|
|
48
|
+
},
|
|
49
|
+
create(context) {
|
|
50
|
+
return {
|
|
51
|
+
'Program:exit'() {
|
|
52
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
53
|
+
const filePath = getFilePath(context);
|
|
54
|
+
if (!filePath || !filePath.endsWith('.ts')) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const text = sourceCode.getText();
|
|
59
|
+
if (!text.includes(PENDING_PROP)) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sourceFile = getProjectSourceFile(
|
|
64
|
+
getProject(getCwd(context)),
|
|
65
|
+
filePath,
|
|
66
|
+
text,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const collections = collectCollections(sourceFile, filePath);
|
|
70
|
+
const pendingRoutes = collections.flatMap((c) => c.pendingRoutes);
|
|
71
|
+
if (pendingRoutes.length === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const checkedFirstArgs = collectCheckedGenDeps(sourceFile);
|
|
76
|
+
const cascadeContexts = collectCascadeContexts(sourceFile);
|
|
77
|
+
|
|
78
|
+
const issues = pendingRoutes.filter(
|
|
79
|
+
(route) => !checkedFirstArgs.has(normalize(route.genDepsRef)),
|
|
80
|
+
);
|
|
81
|
+
if (issues.length === 0) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const reportLoc = getNodeLoc(sourceCode, issues[0].reportNode);
|
|
86
|
+
|
|
87
|
+
let fixable = true;
|
|
88
|
+
for (const issue of issues) {
|
|
89
|
+
const ctx = cascadeContexts.get(`typeof ${issue.routesName}`);
|
|
90
|
+
if (!ctx) {
|
|
91
|
+
fixable = false;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
appendCheck(sourceFile, issue, ctx);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (fixable) {
|
|
98
|
+
ensureImports(sourceFile, [CHECK_TYPE, CAN_RUN]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const fixedText = sourceFile.getFullText();
|
|
102
|
+
const message = `craftRoute(s) with a pendingComponent must be verified with ${CHECK_TYPE}(): ${issues
|
|
103
|
+
.map((i) => i.path)
|
|
104
|
+
.join(', ')}`;
|
|
105
|
+
|
|
106
|
+
if (!fixable || fixedText === text) {
|
|
107
|
+
context.report({ loc: reportLoc, message });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
context.report({
|
|
112
|
+
loc: reportLoc,
|
|
113
|
+
message,
|
|
114
|
+
fix(fixer) {
|
|
115
|
+
return fixer.replaceTextRange([0, text.length], fixedText);
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function collectCollections(sourceFile, filePath) {
|
|
124
|
+
const collections = [];
|
|
125
|
+
|
|
126
|
+
for (const call of sourceFile.getDescendantsOfKind(
|
|
127
|
+
SyntaxKind.CallExpression,
|
|
128
|
+
)) {
|
|
129
|
+
if (call.getExpression().getText() !== ROUTES_FACTORY) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const collectionName = getStringArg(call, 0);
|
|
134
|
+
const routesArray = call.getArguments()[1];
|
|
135
|
+
if (!collectionName || !routesArray) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const routesName = getRoutesBindingName(call);
|
|
140
|
+
if (!routesName) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const pendingRoutes = [];
|
|
145
|
+
for (const element of getArrayElements(routesArray)) {
|
|
146
|
+
const route = readRoute(element);
|
|
147
|
+
if (!route) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const importSpecifier = getLazyImportSpecifier(route.object, PENDING_PROP);
|
|
152
|
+
if (!importSpecifier) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const genDepsName = resolveGenDepsName(filePath, importSpecifier);
|
|
157
|
+
if (!genDepsName) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pendingRoutes.push({
|
|
162
|
+
routesName,
|
|
163
|
+
path: route.path,
|
|
164
|
+
genDepsRef: `import('${importSpecifier}').${genDepsName}`,
|
|
165
|
+
availableNames: deriveAvailableNames(
|
|
166
|
+
collectionName,
|
|
167
|
+
route.path,
|
|
168
|
+
hasProperty(route.object, VT_PROP),
|
|
169
|
+
),
|
|
170
|
+
suffix: toPascalCase(routesName.replace(/Routes$/, '')),
|
|
171
|
+
reportNode: getPropertyAssignment(route.object, PENDING_PROP),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
collections.push({ collectionName, routesName, pendingRoutes });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return collections;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// A route element is either `craftRoute('<path>', { … })` or a `{ path: '<path>', … }`
|
|
182
|
+
// object literal. Returns the path string and the route's object literal.
|
|
183
|
+
function readRoute(element) {
|
|
184
|
+
if (Node.isCallExpression(element) && element.getExpression().getText() === ROUTE_FN) {
|
|
185
|
+
const pathArg = getStringArg(element, 0);
|
|
186
|
+
const object = element.getArguments()[1];
|
|
187
|
+
if (pathArg !== undefined && object && Node.isObjectLiteralExpression(object)) {
|
|
188
|
+
return { path: pathArg, object };
|
|
189
|
+
}
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (Node.isObjectLiteralExpression(element)) {
|
|
194
|
+
const pathProp = getPropertyAssignment(element, 'path');
|
|
195
|
+
const initializer = pathProp?.getInitializer();
|
|
196
|
+
if (initializer && Node.isStringLiteral(initializer)) {
|
|
197
|
+
return { path: initializer.getLiteralText(), object: element };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// `${Collection}${Param}Params` for each `:param`, plus
|
|
205
|
+
// `${Collection}${RouteBase}ViewTransition` when the route declares a payload.
|
|
206
|
+
function deriveAvailableNames(collectionName, routePath, hasViewTransition) {
|
|
207
|
+
const collection = toPascalCase(collectionName);
|
|
208
|
+
const names = [];
|
|
209
|
+
|
|
210
|
+
for (const segment of routePath.split('/')) {
|
|
211
|
+
if (segment.startsWith(':')) {
|
|
212
|
+
const param = segment.slice(1).replace(/\?$/, '');
|
|
213
|
+
names.push(`${collection}${toPascalCase(param)}Params`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (hasViewTransition) {
|
|
218
|
+
names.push(`${collection}${routeBaseServiceName(routePath)}ViewTransition`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return names;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function routeBaseServiceName(routePath) {
|
|
225
|
+
const name = routePath
|
|
226
|
+
.split('/')
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
.map((segment) =>
|
|
229
|
+
segment === '**'
|
|
230
|
+
? 'Wildcard'
|
|
231
|
+
: toPascalCase(segment.replace(/^:/, '').replace(/\?$/, '')),
|
|
232
|
+
)
|
|
233
|
+
.join('');
|
|
234
|
+
return name || 'Root';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function getArrayElements(node) {
|
|
238
|
+
return Node.isArrayLiteralExpression(node) ? node.getElements() : [];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getStringArg(call, index) {
|
|
242
|
+
const arg = call.getArguments()[index];
|
|
243
|
+
return arg && Node.isStringLiteral(arg) ? arg.getLiteralText() : undefined;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function getPropertyAssignment(objectLiteral, name) {
|
|
247
|
+
if (!Node.isObjectLiteralExpression(objectLiteral)) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
const property = objectLiteral
|
|
251
|
+
.getProperties()
|
|
252
|
+
.find(
|
|
253
|
+
(candidate) =>
|
|
254
|
+
Node.isPropertyAssignment(candidate) && candidate.getName() === name,
|
|
255
|
+
);
|
|
256
|
+
return Node.isPropertyAssignment(property) ? property : undefined;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function hasProperty(objectLiteral, name) {
|
|
260
|
+
return getPropertyAssignment(objectLiteral, name) !== undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getLazyImportSpecifier(objectLiteral, propertyName) {
|
|
264
|
+
const property = getPropertyAssignment(objectLiteral, propertyName);
|
|
265
|
+
const initializer = property?.getInitializer();
|
|
266
|
+
if (!initializer || !Node.isArrowFunction(initializer)) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const body = initializer.getBody();
|
|
271
|
+
const callExpression = Node.isCallExpression(body) ? body : undefined;
|
|
272
|
+
if (
|
|
273
|
+
!callExpression ||
|
|
274
|
+
callExpression.getExpression().getKind() !== SyntaxKind.ImportKeyword
|
|
275
|
+
) {
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const [argument] = callExpression.getArguments();
|
|
280
|
+
return argument && Node.isStringLiteral(argument)
|
|
281
|
+
? argument.getLiteralText()
|
|
282
|
+
: undefined;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function getRoutesBindingName(call) {
|
|
286
|
+
const declaration = call.getFirstAncestorByKind(
|
|
287
|
+
SyntaxKind.VariableDeclaration,
|
|
288
|
+
);
|
|
289
|
+
const nameNode = declaration?.getNameNode();
|
|
290
|
+
if (!nameNode || nameNode.getKind() !== SyntaxKind.ObjectBindingPattern) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const elements = nameNode.getElements();
|
|
295
|
+
const expected =
|
|
296
|
+
getStringArg(call, 0) !== undefined ? `${getStringArg(call, 0)}Routes` : undefined;
|
|
297
|
+
if (expected) {
|
|
298
|
+
const match = elements.find(
|
|
299
|
+
(element) =>
|
|
300
|
+
(element.getPropertyNameNode()?.getText() ?? element.getName()) ===
|
|
301
|
+
expected,
|
|
302
|
+
);
|
|
303
|
+
if (match) {
|
|
304
|
+
return match.getName();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const routesElements = elements.filter((element) =>
|
|
309
|
+
(element.getPropertyNameNode()?.getText() ?? element.getName()).endsWith(
|
|
310
|
+
'Routes',
|
|
311
|
+
),
|
|
312
|
+
);
|
|
313
|
+
return routesElements.length === 1 ? routesElements[0].getName() : undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// First type-argument text of every `RouteCheckedDI<First, …>` already present.
|
|
317
|
+
function collectCheckedGenDeps(sourceFile) {
|
|
318
|
+
const checked = new Set();
|
|
319
|
+
for (const reference of sourceFile.getDescendantsOfKind(
|
|
320
|
+
SyntaxKind.TypeReference,
|
|
321
|
+
)) {
|
|
322
|
+
if (reference.getTypeName().getText() !== CHECK_TYPE) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const first = reference.getTypeArguments()[0];
|
|
326
|
+
if (first) {
|
|
327
|
+
checked.add(normalize(first.getText()));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return checked;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// `typeof xRoutes` -> "ParentNames, ParentValues" from the cascade check.
|
|
334
|
+
function collectCascadeContexts(sourceFile) {
|
|
335
|
+
const contexts = new Map();
|
|
336
|
+
for (const reference of sourceFile.getDescendantsOfKind(
|
|
337
|
+
SyntaxKind.TypeReference,
|
|
338
|
+
)) {
|
|
339
|
+
if (reference.getTypeName().getText() !== CASCADE_TYPE) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const args = reference.getTypeArguments();
|
|
343
|
+
if (args.length >= 3) {
|
|
344
|
+
contexts.set(args[2].getText(), {
|
|
345
|
+
names: args[0].getText(),
|
|
346
|
+
values: args[1].getText(),
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return contexts;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function appendCheck(sourceFile, issue, parentContext) {
|
|
354
|
+
const quoted = issue.availableNames.map((name) => `'${name}'`);
|
|
355
|
+
const available =
|
|
356
|
+
parentContext.names === 'never'
|
|
357
|
+
? quoted.join(' | ') || 'never'
|
|
358
|
+
: [parentContext.names, ...quoted].join(' | ');
|
|
359
|
+
|
|
360
|
+
sourceFile.addStatements((writer) => {
|
|
361
|
+
writer.blankLine();
|
|
362
|
+
writer.writeLine(`type _Check${issue.suffix}PendingDI = ${CHECK_TYPE}<`);
|
|
363
|
+
writer.writeLine(` ${issue.genDepsRef},`);
|
|
364
|
+
writer.writeLine(` ${available},`);
|
|
365
|
+
writer.writeLine(` ${parentContext.values},`);
|
|
366
|
+
writer.writeLine(` 'pending component: ${issue.path}'`);
|
|
367
|
+
writer.writeLine('>;');
|
|
368
|
+
writer.writeLine(
|
|
369
|
+
`type _CanRun${issue.suffix}Pending = ${CAN_RUN}<_Check${issue.suffix}PendingDI>;`,
|
|
370
|
+
);
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function resolveGenDepsName(fromFilePath, importSpecifier) {
|
|
375
|
+
if (!importSpecifier.startsWith('.')) {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
const resolvedBase = path.resolve(path.dirname(fromFilePath), importSpecifier);
|
|
379
|
+
const candidates = [
|
|
380
|
+
`${resolvedBase}.ts`,
|
|
381
|
+
`${resolvedBase}.tsx`,
|
|
382
|
+
path.join(resolvedBase, 'index.ts'),
|
|
383
|
+
];
|
|
384
|
+
const targetPath = candidates.find((candidate) => fs.existsSync(candidate));
|
|
385
|
+
if (!targetPath) {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const targetText = fs.readFileSync(targetPath, 'utf8');
|
|
390
|
+
const names = [...targetText.matchAll(/export\s+type\s+(GenDeps_\w+)/g)].map(
|
|
391
|
+
(match) => match[1],
|
|
392
|
+
);
|
|
393
|
+
if (names.length === 0) {
|
|
394
|
+
return undefined;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const defaultClass = targetText.match(/export\s+default\s+class\s+(\w+)/);
|
|
398
|
+
if (defaultClass) {
|
|
399
|
+
const preferred = `GenDeps_${defaultClass[1]}`;
|
|
400
|
+
if (names.includes(preferred)) {
|
|
401
|
+
return preferred;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return names[0];
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function ensureImports(sourceFile, names) {
|
|
408
|
+
const craftImport = sourceFile
|
|
409
|
+
.getImportDeclarations()
|
|
410
|
+
.find((imp) => imp.getModuleSpecifierValue() === '@craft-ts/core');
|
|
411
|
+
if (!craftImport) {
|
|
412
|
+
sourceFile.addImportDeclaration({
|
|
413
|
+
moduleSpecifier: '@craft-ts/core',
|
|
414
|
+
namedImports: names.map((name) => ({ name, isTypeOnly: true })),
|
|
415
|
+
});
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const existing = new Set(
|
|
419
|
+
craftImport.getNamedImports().map((ni) => ni.getName()),
|
|
420
|
+
);
|
|
421
|
+
for (const name of names) {
|
|
422
|
+
if (!existing.has(name)) {
|
|
423
|
+
craftImport.addNamedImport({ name, isTypeOnly: true });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function getProject(cwd) {
|
|
429
|
+
let project = projectCache.get(cwd);
|
|
430
|
+
if (project) {
|
|
431
|
+
return project;
|
|
432
|
+
}
|
|
433
|
+
const manipulationSettings = {
|
|
434
|
+
indentationText: IndentationText.TwoSpaces,
|
|
435
|
+
quoteKind: QuoteKind.Single,
|
|
436
|
+
};
|
|
437
|
+
const tsConfigFilePath = path.join(cwd, 'tsconfig.json');
|
|
438
|
+
project = fs.existsSync(tsConfigFilePath)
|
|
439
|
+
? new Project({ tsConfigFilePath, manipulationSettings })
|
|
440
|
+
: new Project({
|
|
441
|
+
compilerOptions: { experimentalDecorators: true, target: 9 },
|
|
442
|
+
manipulationSettings,
|
|
443
|
+
});
|
|
444
|
+
projectCache.set(cwd, project);
|
|
445
|
+
return project;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function getProjectSourceFile(project, filePath, text) {
|
|
449
|
+
const normalizedPath = path.resolve(filePath);
|
|
450
|
+
const existing = project.getSourceFile(normalizedPath);
|
|
451
|
+
if (existing) {
|
|
452
|
+
existing.replaceWithText(text);
|
|
453
|
+
return existing;
|
|
454
|
+
}
|
|
455
|
+
const added = project.addSourceFileAtPathIfExists(normalizedPath);
|
|
456
|
+
if (added) {
|
|
457
|
+
added.replaceWithText(text);
|
|
458
|
+
return added;
|
|
459
|
+
}
|
|
460
|
+
return project.createSourceFile(normalizedPath, text, { overwrite: true });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function getFilePath(context) {
|
|
464
|
+
const filePath = context.filename ?? context.getFilename();
|
|
465
|
+
return !filePath || filePath === '<input>' ? undefined : filePath;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function getCwd(context) {
|
|
469
|
+
return context.cwd ?? process.cwd();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function getNodeLoc(sourceCode, node) {
|
|
473
|
+
return {
|
|
474
|
+
start: sourceCode.getLocFromIndex(node.getStart()),
|
|
475
|
+
end: sourceCode.getLocFromIndex(node.getEnd()),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function toPascalCase(value) {
|
|
480
|
+
return value
|
|
481
|
+
.replace(/^:/, '')
|
|
482
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
483
|
+
.filter(Boolean)
|
|
484
|
+
.map((segment) => segment[0].toUpperCase() + segment.slice(1))
|
|
485
|
+
.join('');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function normalize(text) {
|
|
489
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
490
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const PRIMITIVES = new Set([
|
|
2
|
+
'asyncProcess',
|
|
3
|
+
'craftPipe',
|
|
4
|
+
'mutation',
|
|
5
|
+
'query',
|
|
6
|
+
'queryParam',
|
|
7
|
+
'queryParams',
|
|
8
|
+
'state',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const ALLOWED_HOSTS = new Set([
|
|
12
|
+
'craftComponent',
|
|
13
|
+
'craftGen',
|
|
14
|
+
'craftRoute',
|
|
15
|
+
'craftRoutes',
|
|
16
|
+
'craftService',
|
|
17
|
+
'toCraftService',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const CORE_IMPORTS = new Set([
|
|
21
|
+
'asyncProcess',
|
|
22
|
+
'craftGen',
|
|
23
|
+
'craftPipe',
|
|
24
|
+
'craftRoute',
|
|
25
|
+
'craftRoutes',
|
|
26
|
+
'craftService',
|
|
27
|
+
'mutation',
|
|
28
|
+
'query',
|
|
29
|
+
'queryParam',
|
|
30
|
+
'queryParams',
|
|
31
|
+
'state',
|
|
32
|
+
'toCraftService',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const COMPONENT_IMPORTS = new Set(['craftComponent']);
|
|
36
|
+
const CORE_MODULES = new Set(['@craft-ts/core']);
|
|
37
|
+
const COMPONENT_MODULES = new Set(['@craft-ts/component']);
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
meta: {
|
|
41
|
+
type: 'problem',
|
|
42
|
+
docs: {
|
|
43
|
+
description:
|
|
44
|
+
'Require Craft primitive creation calls to run inside an Angular-aware Craft host.',
|
|
45
|
+
},
|
|
46
|
+
schema: [],
|
|
47
|
+
messages: {
|
|
48
|
+
primitiveContext:
|
|
49
|
+
"'{{name}}(...)' must be created inside craftComponent, craftService, craftGen, craftRoute, or craftRoutes; module-level primitive creation can fail without an injection context.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
create(context) {
|
|
53
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
CallExpression(node) {
|
|
57
|
+
if (
|
|
58
|
+
node.callee.type !== 'Identifier' ||
|
|
59
|
+
node.arguments.length === 0 ||
|
|
60
|
+
!PRIMITIVES.has(node.callee.name) ||
|
|
61
|
+
!resolvesToImportedName(
|
|
62
|
+
node.callee,
|
|
63
|
+
sourceCode,
|
|
64
|
+
CORE_IMPORTS,
|
|
65
|
+
CORE_MODULES,
|
|
66
|
+
) ||
|
|
67
|
+
isInsideAllowedHost(node, sourceCode)
|
|
68
|
+
) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
context.report({
|
|
73
|
+
node,
|
|
74
|
+
messageId: 'primitiveContext',
|
|
75
|
+
data: { name: node.callee.name },
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function isInsideAllowedHost(node, sourceCode) {
|
|
83
|
+
let current = node.parent;
|
|
84
|
+
while (current) {
|
|
85
|
+
if (
|
|
86
|
+
current.type === 'CallExpression' &&
|
|
87
|
+
current.callee.type === 'Identifier' &&
|
|
88
|
+
isAllowedHostImport(current.callee, sourceCode)
|
|
89
|
+
) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
current = current.parent;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isAllowedHostImport(identifier, sourceCode) {
|
|
98
|
+
const allowedNames =
|
|
99
|
+
identifier.name === 'craftComponent'
|
|
100
|
+
? COMPONENT_IMPORTS
|
|
101
|
+
: CORE_IMPORTS;
|
|
102
|
+
const allowedModules =
|
|
103
|
+
identifier.name === 'craftComponent'
|
|
104
|
+
? COMPONENT_MODULES
|
|
105
|
+
: CORE_MODULES;
|
|
106
|
+
return (
|
|
107
|
+
ALLOWED_HOSTS.has(identifier.name) &&
|
|
108
|
+
resolvesToImportedName(
|
|
109
|
+
identifier,
|
|
110
|
+
sourceCode,
|
|
111
|
+
allowedNames,
|
|
112
|
+
allowedModules,
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function resolvesToImportedName(
|
|
118
|
+
identifier,
|
|
119
|
+
sourceCode,
|
|
120
|
+
importedNames,
|
|
121
|
+
moduleNames,
|
|
122
|
+
) {
|
|
123
|
+
const scope = sourceCode.getScope(identifier);
|
|
124
|
+
let current = scope;
|
|
125
|
+
|
|
126
|
+
while (current) {
|
|
127
|
+
const variable = current.variables.find(
|
|
128
|
+
(candidate) => candidate.name === identifier.name,
|
|
129
|
+
);
|
|
130
|
+
if (variable) {
|
|
131
|
+
return variable.defs.some(
|
|
132
|
+
(definition) =>
|
|
133
|
+
definition.type === 'ImportBinding' &&
|
|
134
|
+
importedNames.has(importedName(definition)) &&
|
|
135
|
+
moduleNames.has(definition.parent?.source?.value),
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
current = current.upper;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function importedName(definition) {
|
|
145
|
+
if (definition.node?.type === 'ImportSpecifier') {
|
|
146
|
+
return definition.node.imported.type === 'Identifier'
|
|
147
|
+
? definition.node.imported.name
|
|
148
|
+
: definition.node.imported.value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (definition.node?.type === 'ImportDefaultSpecifier') {
|
|
152
|
+
return 'default';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|