@qds.dev/tools 0.11.2 → 0.13.0
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/lib/linter/qds-internal.d.ts +204 -1
- package/lib/linter/qds.d.ts +59 -0
- package/lib/linter/qds.unit.d.ts +1 -0
- package/lib/linter/rule-tester.d.ts +23 -1
- package/lib/playground/prop-extraction.d.ts +6 -1
- package/lib/playground/prop-extraction.qwik.mjs +68 -9
- package/lib/playground/scenario-injection.qwik.mjs +41 -8
- package/lib/rolldown/as-child.d.ts +6 -5
- package/lib/rolldown/as-child.qwik.mjs +52 -91
- package/lib/rolldown/index.d.ts +3 -2
- package/lib/rolldown/index.qwik.mjs +2 -3
- package/lib/rolldown/inject-component-types.qwik.mjs +1 -1
- package/lib/rolldown/inline-asset.qwik.mjs +6 -6
- package/lib/rolldown/inline-css.qwik.mjs +1 -1
- package/lib/rolldown/qds-types.d.ts +41 -0
- package/lib/rolldown/qds.d.ts +5 -0
- package/lib/rolldown/qds.qwik.mjs +147 -0
- package/lib/rolldown/qds.unit.d.ts +1 -0
- package/lib/rolldown/ui-types.d.ts +42 -0
- package/lib/rolldown/ui.d.ts +12 -0
- package/lib/rolldown/ui.qwik.mjs +445 -0
- package/lib/rolldown/ui.unit.d.ts +1 -0
- package/lib/utils/icons/transform/mdx.d.ts +3 -11
- package/lib/utils/icons/transform/mdx.qwik.mjs +14 -20
- package/lib/utils/icons/transform/tsx.d.ts +3 -12
- package/lib/utils/icons/transform/tsx.qwik.mjs +28 -37
- package/lib/utils/index.qwik.mjs +5 -5
- package/lib/utils/transform-dts.qwik.mjs +1 -1
- package/lib/vite/index.d.ts +2 -2
- package/lib/vite/index.qwik.mjs +2 -3
- package/lib/vite/minify-content.qwik.mjs +1 -1
- package/linter/qds-internal.ts +707 -0
- package/linter/qds-internal.unit.ts +399 -0
- package/linter/qds.ts +300 -0
- package/linter/qds.unit.ts +158 -0
- package/linter/rule-tester.ts +395 -0
- package/package.json +8 -7
- package/lib/rolldown/icons.qwik.mjs +0 -107
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { anyOf, createRegExp, exactly } from "magic-regexp";
|
|
2
|
+
import MagicString from "magic-string";
|
|
3
|
+
import { parseSync } from "oxc-parser";
|
|
4
|
+
import { walk } from "oxc-walker";
|
|
5
|
+
|
|
6
|
+
//#region rolldown/ui.ts
|
|
7
|
+
const deriveContextField = (getterName) => getterName.slice(3, 4).toLowerCase() + getterName.slice(4);
|
|
8
|
+
function generateComponentName(namespace, contextField, index) {
|
|
9
|
+
return `_Qds${namespace[0].toUpperCase() + namespace.slice(1)}${contextField[0].toUpperCase() + contextField.slice(1)}${index}`;
|
|
10
|
+
}
|
|
11
|
+
function collectUniqueNamespaces(target) {
|
|
12
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13
|
+
const namespaces = [];
|
|
14
|
+
for (const ref of target.stateRefs) {
|
|
15
|
+
if (seen.has(ref.namespace.localName)) continue;
|
|
16
|
+
seen.add(ref.namespace.localName);
|
|
17
|
+
namespaces.push(ref.namespace);
|
|
18
|
+
}
|
|
19
|
+
for (const alias of target.aliasRefs) {
|
|
20
|
+
if (seen.has(alias.namespace.localName)) continue;
|
|
21
|
+
seen.add(alias.namespace.localName);
|
|
22
|
+
namespaces.push(alias.namespace);
|
|
23
|
+
}
|
|
24
|
+
return namespaces;
|
|
25
|
+
}
|
|
26
|
+
function getContextVarName(namespace, isSingle) {
|
|
27
|
+
return isSingle ? "ctx" : `${namespace.localName}Ctx`;
|
|
28
|
+
}
|
|
29
|
+
function isInsideDollarBoundary(ancestors) {
|
|
30
|
+
return ancestors.some((ancestor) => {
|
|
31
|
+
if (ancestor.type !== "CallExpression") return false;
|
|
32
|
+
const callee = ancestor.callee;
|
|
33
|
+
if (callee.type === "Identifier") return callee.name.endsWith("$") && callee.name !== "component$";
|
|
34
|
+
if (callee.type === "MemberExpression" && !callee.computed) return callee.property.name.endsWith("$");
|
|
35
|
+
return false;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function getFirstRef(target) {
|
|
39
|
+
if (target.stateRefs.length > 0) return {
|
|
40
|
+
namespace: target.stateRefs[0].namespace,
|
|
41
|
+
contextField: target.stateRefs[0].contextField
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
namespace: target.aliasRefs[0].namespace,
|
|
45
|
+
contextField: target.aliasRefs[0].contextField
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function rewriteExpression(source, expressionStart, expressionEnd, stateRefs, aliasRefs, isSingle) {
|
|
49
|
+
const replacements = [];
|
|
50
|
+
for (const ref of stateRefs) {
|
|
51
|
+
const ctxVar = isSingle ? "ctx" : `${ref.namespace.localName}Ctx`;
|
|
52
|
+
replacements.push({
|
|
53
|
+
start: ref.start - expressionStart,
|
|
54
|
+
end: ref.end - expressionStart,
|
|
55
|
+
text: `${ctxVar}.${ref.contextField}`
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
59
|
+
let expr = source.slice(expressionStart, expressionEnd);
|
|
60
|
+
for (const r of replacements) expr = expr.slice(0, r.start) + r.text + expr.slice(r.end);
|
|
61
|
+
const seenAliases = /* @__PURE__ */ new Set();
|
|
62
|
+
for (const alias of aliasRefs) {
|
|
63
|
+
if (seenAliases.has(alias.localName)) continue;
|
|
64
|
+
seenAliases.add(alias.localName);
|
|
65
|
+
const replacement = `${isSingle ? "ctx" : `${alias.namespace.localName}Ctx`}.${alias.contextField}`;
|
|
66
|
+
expr = expr.replace(new RegExp(`\\b${alias.localName}\\b`, "g"), replacement);
|
|
67
|
+
}
|
|
68
|
+
return expr;
|
|
69
|
+
}
|
|
70
|
+
function buildUseContextLines(uniqueNamespaces) {
|
|
71
|
+
const isSingle = uniqueNamespaces.length === 1;
|
|
72
|
+
return uniqueNamespaces.map((ns) => {
|
|
73
|
+
return ` const ${getContextVarName(ns, isSingle)} = useContext(${ns.localName}.contextId);`;
|
|
74
|
+
}).join("\n");
|
|
75
|
+
}
|
|
76
|
+
function generateExpressionComponent(name, uniqueNamespaces, rewrittenExpression) {
|
|
77
|
+
return `\nconst ${name} = component$(() => {\n${buildUseContextLines(uniqueNamespaces)}\n return (\n <>\n {${rewrittenExpression}}\n </>\n );\n});\n`;
|
|
78
|
+
}
|
|
79
|
+
function generateElementComponent(name, uniqueNamespaces, rewrittenElement) {
|
|
80
|
+
return `\nconst ${name} = component$(() => {\n${buildUseContextLines(uniqueNamespaces)}\n return (\n ${rewrittenElement}\n );\n});\n`;
|
|
81
|
+
}
|
|
82
|
+
function generateHookComponent(name, uniqueNamespaces, rewrittenHook) {
|
|
83
|
+
return `const ${name} = component$(() => {\n${buildUseContextLines(uniqueNamespaces)}\n${rewrittenHook}\n return <></>;\n});\n`;
|
|
84
|
+
}
|
|
85
|
+
function generateChainedHookComponent(name, uniqueNamespaces, rewrittenHook, consumingJsx) {
|
|
86
|
+
return `const ${name} = component$(() => {\n${buildUseContextLines(uniqueNamespaces)}\n${rewrittenHook}\n return (\n ${consumingJsx}\n );\n});\n`;
|
|
87
|
+
}
|
|
88
|
+
const HOOK_NAMES = new Set([
|
|
89
|
+
"useTask$",
|
|
90
|
+
"useVisibleTask$",
|
|
91
|
+
"useComputed$"
|
|
92
|
+
]);
|
|
93
|
+
function filterToInnermostConsumers(ranges) {
|
|
94
|
+
const toRemove = /* @__PURE__ */ new Set();
|
|
95
|
+
for (let i = 0; i < ranges.length; i++) for (let j = 0; j < ranges.length; j++) {
|
|
96
|
+
if (i === j) continue;
|
|
97
|
+
if (ranges[i].start <= ranges[j].start && ranges[i].end >= ranges[j].end) toRemove.add(i);
|
|
98
|
+
}
|
|
99
|
+
for (let i = ranges.length - 1; i >= 0; i--) if (toRemove.has(i)) ranges.splice(i, 1);
|
|
100
|
+
}
|
|
101
|
+
function collectUniqueNamespacesFromRefs(stateRefs, aliasRefs) {
|
|
102
|
+
const seen = /* @__PURE__ */ new Set();
|
|
103
|
+
const namespaces = [];
|
|
104
|
+
for (const ref of stateRefs) {
|
|
105
|
+
if (seen.has(ref.namespace.localName)) continue;
|
|
106
|
+
seen.add(ref.namespace.localName);
|
|
107
|
+
namespaces.push(ref.namespace);
|
|
108
|
+
}
|
|
109
|
+
for (const alias of aliasRefs) {
|
|
110
|
+
if (seen.has(alias.namespace.localName)) continue;
|
|
111
|
+
seen.add(alias.namespace.localName);
|
|
112
|
+
namespaces.push(alias.namespace);
|
|
113
|
+
}
|
|
114
|
+
return namespaces;
|
|
115
|
+
}
|
|
116
|
+
function uiTransformCore(code, id, ast, s, importSources, debug, warn) {
|
|
117
|
+
const ancestors = [];
|
|
118
|
+
debug("processing", id);
|
|
119
|
+
const boundNamespaces = /* @__PURE__ */ new Map();
|
|
120
|
+
const stateRefs = /* @__PURE__ */ new Map();
|
|
121
|
+
const aliasBindings = /* @__PURE__ */ new Map();
|
|
122
|
+
const transformTargets = [];
|
|
123
|
+
const pendingWarnings = [];
|
|
124
|
+
const collectImportsAndBindings = function(node) {
|
|
125
|
+
if (node.type === "ImportDeclaration" && importSources.includes(node.source.value)) for (const specifier of node.specifiers) {
|
|
126
|
+
if (specifier.type !== "ImportSpecifier") continue;
|
|
127
|
+
const importedName = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
128
|
+
const localName = specifier.local.name;
|
|
129
|
+
boundNamespaces.set(localName, {
|
|
130
|
+
localName,
|
|
131
|
+
importedName
|
|
132
|
+
});
|
|
133
|
+
debug("detected namespace", localName, "->", importedName);
|
|
134
|
+
}
|
|
135
|
+
if (node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier" && boundNamespaces.has(node.object.name) && node.property.name.startsWith("get")) {
|
|
136
|
+
const namespace = boundNamespaces.get(node.object.name);
|
|
137
|
+
const getterName = node.property.name;
|
|
138
|
+
stateRefs.set(String(node.start), {
|
|
139
|
+
node,
|
|
140
|
+
namespace,
|
|
141
|
+
getterName,
|
|
142
|
+
contextField: deriveContextField(getterName),
|
|
143
|
+
start: node.start,
|
|
144
|
+
end: node.end
|
|
145
|
+
});
|
|
146
|
+
debug("state ref", getterName, "->", deriveContextField(getterName));
|
|
147
|
+
}
|
|
148
|
+
if (node.type === "VariableDeclarator" && node.id.type === "Identifier" && node.init !== null && node.init.type === "MemberExpression" && !node.init.computed && node.init.object.type === "Identifier" && boundNamespaces.has(node.init.object.name) && node.init.property.name.startsWith("get")) {
|
|
149
|
+
const getterName = node.init.property.name;
|
|
150
|
+
aliasBindings.set(node.id.name, {
|
|
151
|
+
localName: node.id.name,
|
|
152
|
+
namespace: boundNamespaces.get(node.init.object.name),
|
|
153
|
+
getterName,
|
|
154
|
+
contextField: deriveContextField(getterName)
|
|
155
|
+
});
|
|
156
|
+
debug("alias binding (direct)", node.id.name, "->", getterName);
|
|
157
|
+
}
|
|
158
|
+
if (node.type === "VariableDeclarator" && node.id.type === "ObjectPattern" && node.init !== null && node.init.type === "Identifier" && boundNamespaces.has(node.init.name)) {
|
|
159
|
+
const namespace = boundNamespaces.get(node.init.name);
|
|
160
|
+
for (const prop of node.id.properties) {
|
|
161
|
+
if (prop.type !== "Property") continue;
|
|
162
|
+
if (prop.key.type !== "Identifier") continue;
|
|
163
|
+
const keyName = prop.key.name;
|
|
164
|
+
if (!keyName.startsWith("get")) continue;
|
|
165
|
+
if (prop.value.type === "ObjectPattern") throw new Error(`[vite-plugin-qds-ui] Nested destructuring of QDS state getter '${keyName}' from '${namespace.localName}' is not supported. Use simple destructuring: const { ${keyName} } = ${namespace.localName}`);
|
|
166
|
+
if (prop.value.type !== "Identifier") continue;
|
|
167
|
+
aliasBindings.set(prop.value.name, {
|
|
168
|
+
localName: prop.value.name,
|
|
169
|
+
namespace,
|
|
170
|
+
getterName: keyName,
|
|
171
|
+
contextField: deriveContextField(keyName)
|
|
172
|
+
});
|
|
173
|
+
debug("alias binding (destructure)", prop.value.name, "->", keyName, prop.shorthand ? "(shorthand)" : "(renamed)");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
walk(ast, { enter: collectImportsAndBindings });
|
|
178
|
+
if (boundNamespaces.size === 0) return { changed: false };
|
|
179
|
+
if (stateRefs.size === 0 && aliasBindings.size === 0) return { changed: false };
|
|
180
|
+
const elementTargetRanges = [];
|
|
181
|
+
function collectRefsInRange(rangeStart, rangeEnd, rootNode) {
|
|
182
|
+
const foundStateRefs = [];
|
|
183
|
+
for (const ref of stateRefs.values()) if (ref.start >= rangeStart && ref.end <= rangeEnd) foundStateRefs.push(ref);
|
|
184
|
+
const foundAliasRefs = [];
|
|
185
|
+
walk(rootNode, { enter(innerNode) {
|
|
186
|
+
if (innerNode.type === "Identifier" && aliasBindings.has(innerNode.name)) foundAliasRefs.push(aliasBindings.get(innerNode.name));
|
|
187
|
+
} });
|
|
188
|
+
return {
|
|
189
|
+
stateRefs: foundStateRefs,
|
|
190
|
+
aliasRefs: foundAliasRefs
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function hasGetterInAttributes(element) {
|
|
194
|
+
for (const attr of element.openingElement.attributes) {
|
|
195
|
+
if (attr.type !== "JSXAttribute") continue;
|
|
196
|
+
if (!attr.value) continue;
|
|
197
|
+
if (attr.value.type !== "JSXExpressionContainer") continue;
|
|
198
|
+
if (attr.value.expression.type === "JSXEmptyExpression") continue;
|
|
199
|
+
const { stateRefs: attrStateRefs, aliasRefs: attrAliasRefs } = collectRefsInRange(attr.value.start, attr.value.end, attr.value.expression);
|
|
200
|
+
if (attrStateRefs.length > 0 || attrAliasRefs.length > 0) return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
walk(ast, {
|
|
205
|
+
enter(node) {
|
|
206
|
+
ancestors.push(node);
|
|
207
|
+
if (node.type === "JSXElement") {
|
|
208
|
+
const jsxElement = node;
|
|
209
|
+
if (elementTargetRanges.some((range) => range.start <= jsxElement.start && range.end >= jsxElement.end)) return;
|
|
210
|
+
if (isInsideDollarBoundary(ancestors)) return;
|
|
211
|
+
if (!hasGetterInAttributes(jsxElement)) return;
|
|
212
|
+
const { stateRefs: elementStateRefs, aliasRefs: elementAliasRefs } = collectRefsInRange(jsxElement.start, jsxElement.end, jsxElement);
|
|
213
|
+
if (elementStateRefs.length === 0 && elementAliasRefs.length === 0) return;
|
|
214
|
+
elementTargetRanges.push({
|
|
215
|
+
start: jsxElement.start,
|
|
216
|
+
end: jsxElement.end
|
|
217
|
+
});
|
|
218
|
+
transformTargets.push({
|
|
219
|
+
kind: "element",
|
|
220
|
+
stateRefs: elementStateRefs,
|
|
221
|
+
aliasRefs: elementAliasRefs,
|
|
222
|
+
start: jsxElement.start,
|
|
223
|
+
end: jsxElement.end
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (node.type === "JSXExpressionContainer") {
|
|
227
|
+
if (node.expression.type === "JSXEmptyExpression") return;
|
|
228
|
+
const containerStart = node.start;
|
|
229
|
+
const containerEnd = node.end;
|
|
230
|
+
if (isInsideDollarBoundary(ancestors)) {
|
|
231
|
+
const { stateRefs: containerStateRefs, aliasRefs: containerAliasRefs } = collectRefsInRange(containerStart, containerEnd, node.expression);
|
|
232
|
+
if (containerStateRefs.length > 0 || containerAliasRefs.length > 0) pendingWarnings.push("[vite-plugin-qds-ui] QDS state access found inside a $() boundary. The compiler cannot transform state access inside event handlers. Use the state through a JSX expression instead.");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (ancestors[ancestors.length - 2]?.type === "JSXAttribute") return;
|
|
236
|
+
if (transformTargets.some((target) => target.start <= containerStart && target.end >= containerEnd)) return;
|
|
237
|
+
const { stateRefs: containerStateRefs, aliasRefs: containerAliasRefs } = collectRefsInRange(containerStart, containerEnd, node.expression);
|
|
238
|
+
if (containerStateRefs.length === 0 && containerAliasRefs.length === 0) return;
|
|
239
|
+
transformTargets.push({
|
|
240
|
+
kind: "expression",
|
|
241
|
+
container: node,
|
|
242
|
+
stateRefs: containerStateRefs,
|
|
243
|
+
aliasRefs: containerAliasRefs,
|
|
244
|
+
start: containerStart,
|
|
245
|
+
end: containerEnd
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
leave() {
|
|
250
|
+
ancestors.pop();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
for (const warning of pendingWarnings) warn?.(warning);
|
|
254
|
+
const collectedHookTargets = [];
|
|
255
|
+
walk(ast, { enter(node) {
|
|
256
|
+
if (node.type !== "CallExpression") return;
|
|
257
|
+
if (node.callee.type !== "Identifier" || node.callee.name !== "component$") return;
|
|
258
|
+
if (!node.arguments.length) return;
|
|
259
|
+
const fnArg = node.arguments[0];
|
|
260
|
+
const fnBody = fnArg.body;
|
|
261
|
+
if (!fnBody || fnBody.type !== "BlockStatement") return;
|
|
262
|
+
const bodyStmts = fnBody.body;
|
|
263
|
+
const rootPositions = /* @__PURE__ */ new Map();
|
|
264
|
+
walk(fnArg, { enter(inner) {
|
|
265
|
+
if (inner.type !== "JSXElement") return;
|
|
266
|
+
const opening = inner.openingElement;
|
|
267
|
+
if (opening.name.type !== "JSXMemberExpression") return;
|
|
268
|
+
const nsObj = opening.name.object;
|
|
269
|
+
const nsProp = opening.name.property;
|
|
270
|
+
if (nsObj.type === "JSXIdentifier" && nsProp.type === "JSXIdentifier" && nsProp.name === "root" && boundNamespaces.has(nsObj.name)) rootPositions.set(nsObj.name, opening.end);
|
|
271
|
+
} });
|
|
272
|
+
let returnPos = -1;
|
|
273
|
+
let returnNode = null;
|
|
274
|
+
for (const bStmt of bodyStmts) if (bStmt.type === "ReturnStatement") {
|
|
275
|
+
returnPos = bStmt.start;
|
|
276
|
+
returnNode = bStmt;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
const aliasDeclRanges = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const bStmt of bodyStmts) {
|
|
281
|
+
if (bStmt.type !== "VariableDeclaration") continue;
|
|
282
|
+
for (const decl of bStmt.declarations) {
|
|
283
|
+
if (decl.id.type !== "ObjectPattern" || decl.init?.type !== "Identifier") continue;
|
|
284
|
+
if (!boundNamespaces.has(decl.init.name)) continue;
|
|
285
|
+
for (const prop of decl.id.properties) {
|
|
286
|
+
if (prop.type !== "Property" || prop.key.type !== "Identifier") continue;
|
|
287
|
+
if (!prop.key.name.startsWith("get")) continue;
|
|
288
|
+
const localName = prop.value.type === "Identifier" ? prop.value.name : prop.key.name;
|
|
289
|
+
aliasDeclRanges.set(localName, {
|
|
290
|
+
start: bStmt.start,
|
|
291
|
+
end: bStmt.end
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
for (const bStmt of bodyStmts) {
|
|
297
|
+
let expr;
|
|
298
|
+
let producedBinding = null;
|
|
299
|
+
if (bStmt.type === "ExpressionStatement") {
|
|
300
|
+
const exprStmt = bStmt.expression;
|
|
301
|
+
if (exprStmt.type !== "CallExpression") continue;
|
|
302
|
+
if (exprStmt.callee.type !== "Identifier" || !HOOK_NAMES.has(exprStmt.callee.name)) continue;
|
|
303
|
+
expr = exprStmt;
|
|
304
|
+
} else if (bStmt.type === "VariableDeclaration") {
|
|
305
|
+
const decls = bStmt.declarations;
|
|
306
|
+
if (decls.length !== 1) continue;
|
|
307
|
+
const decl = decls[0];
|
|
308
|
+
if (!decl.init || decl.init.type !== "CallExpression") continue;
|
|
309
|
+
if (decl.init.callee.type !== "Identifier" || !HOOK_NAMES.has(decl.init.callee.name)) continue;
|
|
310
|
+
if (decl.id.type !== "Identifier") continue;
|
|
311
|
+
expr = decl.init;
|
|
312
|
+
producedBinding = decl.id.name;
|
|
313
|
+
} else continue;
|
|
314
|
+
const callExpr = expr;
|
|
315
|
+
const hookStateRefs = [];
|
|
316
|
+
for (const ref of stateRefs.values()) if (ref.start >= callExpr.start && ref.end <= callExpr.end) hookStateRefs.push(ref);
|
|
317
|
+
const hookAliasRefs = [];
|
|
318
|
+
const seen = /* @__PURE__ */ new Set();
|
|
319
|
+
walk(bStmt, { enter(inner) {
|
|
320
|
+
if (inner.type !== "Identifier") return;
|
|
321
|
+
if (aliasBindings.has(inner.name) && !seen.has(inner.name)) {
|
|
322
|
+
seen.add(inner.name);
|
|
323
|
+
hookAliasRefs.push(aliasBindings.get(inner.name));
|
|
324
|
+
}
|
|
325
|
+
} });
|
|
326
|
+
if (hookStateRefs.length === 0 && hookAliasRefs.length === 0) continue;
|
|
327
|
+
const primary = hookStateRefs.length > 0 ? hookStateRefs[0] : {
|
|
328
|
+
namespace: hookAliasRefs[0].namespace,
|
|
329
|
+
contextField: hookAliasRefs[0].contextField
|
|
330
|
+
};
|
|
331
|
+
const rootEnd = rootPositions.get(primary.namespace.localName);
|
|
332
|
+
if (rootEnd === void 0) {
|
|
333
|
+
warn?.(`[vite-plugin-qds-ui] Getter used in ${expr.callee.name} but no <${primary.namespace.localName}.root> found in JSX return. Move this into a component rendered inside <${primary.namespace.localName}.root>.`);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
let aliasDeclRange = null;
|
|
337
|
+
for (const alias of hookAliasRefs) {
|
|
338
|
+
const range = aliasDeclRanges.get(alias.localName);
|
|
339
|
+
if (range) {
|
|
340
|
+
aliasDeclRange = range;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const jsxConsumers = [];
|
|
345
|
+
if (producedBinding !== null && returnNode !== null) {
|
|
346
|
+
walk(returnNode, { enter(inner) {
|
|
347
|
+
if (inner.type !== "JSXElement") return;
|
|
348
|
+
const jsxEl = inner;
|
|
349
|
+
let hasProducedRef = false;
|
|
350
|
+
walk(jsxEl, { enter(deepNode) {
|
|
351
|
+
if (deepNode.type !== "Identifier") return;
|
|
352
|
+
if (deepNode.name === producedBinding) hasProducedRef = true;
|
|
353
|
+
} });
|
|
354
|
+
if (hasProducedRef) jsxConsumers.push({
|
|
355
|
+
start: jsxEl.start,
|
|
356
|
+
end: jsxEl.end
|
|
357
|
+
});
|
|
358
|
+
} });
|
|
359
|
+
filterToInnermostConsumers(jsxConsumers);
|
|
360
|
+
}
|
|
361
|
+
collectedHookTargets.push({
|
|
362
|
+
statementStart: bStmt.start,
|
|
363
|
+
statementEnd: bStmt.end,
|
|
364
|
+
stateRefs: hookStateRefs,
|
|
365
|
+
aliasRefs: hookAliasRefs,
|
|
366
|
+
rootOpeningEnd: rootEnd,
|
|
367
|
+
returnStart: returnPos,
|
|
368
|
+
aliasDeclarationRange: aliasDeclRange,
|
|
369
|
+
producedBinding,
|
|
370
|
+
jsxConsumers
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
} });
|
|
374
|
+
if (transformTargets.length === 0 && collectedHookTargets.length === 0) return { changed: false };
|
|
375
|
+
const generated = transformTargets.map((target, index) => {
|
|
376
|
+
const uniqueNamespaces = collectUniqueNamespaces(target);
|
|
377
|
+
const firstRef = getFirstRef(target);
|
|
378
|
+
const name = generateComponentName(firstRef.namespace.localName, firstRef.contextField, index);
|
|
379
|
+
const isSingle = uniqueNamespaces.length === 1;
|
|
380
|
+
if (target.kind === "element") return {
|
|
381
|
+
name,
|
|
382
|
+
code: generateElementComponent(name, uniqueNamespaces, rewriteExpression(code, target.start, target.end, target.stateRefs, target.aliasRefs, isSingle))
|
|
383
|
+
};
|
|
384
|
+
return {
|
|
385
|
+
name,
|
|
386
|
+
code: generateExpressionComponent(name, uniqueNamespaces, rewriteExpression(code, target.container.expression.start, target.container.expression.end, target.stateRefs, target.aliasRefs, isSingle))
|
|
387
|
+
};
|
|
388
|
+
});
|
|
389
|
+
let lastImportEnd = 0;
|
|
390
|
+
for (const node of ast.body) if (node.type === "ImportDeclaration") lastImportEnd = Math.max(lastImportEnd, node.end);
|
|
391
|
+
debug("generated", generated.length, "components, insert at", lastImportEnd);
|
|
392
|
+
for (let i = 0; i < transformTargets.length; i++) s.overwrite(transformTargets[i].start, transformTargets[i].end, `<${generated[i].name} />`);
|
|
393
|
+
const coreNeeded = new Set(["component$", "useContext"]);
|
|
394
|
+
for (const node of ast.body) {
|
|
395
|
+
if (node.type !== "ImportDeclaration") continue;
|
|
396
|
+
if (code.slice(node.source.start + 1, node.source.end - 1) !== "@qwik.dev/core") continue;
|
|
397
|
+
if (node.importKind === "type") continue;
|
|
398
|
+
for (const spec of node.specifiers) {
|
|
399
|
+
if (spec.type !== "ImportSpecifier") continue;
|
|
400
|
+
const importedName = spec.imported.type === "Identifier" ? spec.imported.name : spec.imported.value;
|
|
401
|
+
coreNeeded.delete(importedName);
|
|
402
|
+
}
|
|
403
|
+
if (coreNeeded.size === 0) continue;
|
|
404
|
+
const lastSpec = node.specifiers[node.specifiers.length - 1];
|
|
405
|
+
if (lastSpec) {
|
|
406
|
+
s.appendLeft(lastSpec.end, `, ${[...coreNeeded].join(", ")}`);
|
|
407
|
+
coreNeeded.clear();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (coreNeeded.size > 0) s.appendLeft(lastImportEnd, `\nimport { ${[...coreNeeded].join(", ")} } from "@qwik.dev/core";`);
|
|
411
|
+
for (const g of generated) s.appendLeft(lastImportEnd, g.code);
|
|
412
|
+
const removedRanges = /* @__PURE__ */ new Set();
|
|
413
|
+
for (let hi = collectedHookTargets.length - 1; hi >= 0; hi--) {
|
|
414
|
+
const ht = collectedHookTargets[hi];
|
|
415
|
+
const uniqueNs = collectUniqueNamespacesFromRefs(ht.stateRefs, ht.aliasRefs);
|
|
416
|
+
const primary = ht.stateRefs.length > 0 ? ht.stateRefs[0] : {
|
|
417
|
+
namespace: ht.aliasRefs[0].namespace,
|
|
418
|
+
contextField: ht.aliasRefs[0].contextField
|
|
419
|
+
};
|
|
420
|
+
const compName = generateComponentName(primary.namespace.localName, primary.contextField, transformTargets.length + hi);
|
|
421
|
+
const isSingle = uniqueNs.length === 1;
|
|
422
|
+
const rewrittenHook = rewriteExpression(code, ht.statementStart, ht.statementEnd, ht.stateRefs, ht.aliasRefs, isSingle);
|
|
423
|
+
const isChained = ht.producedBinding !== null && ht.jsxConsumers.length > 0;
|
|
424
|
+
let hookComponentCode;
|
|
425
|
+
if (isChained) {
|
|
426
|
+
const consumers = ht.jsxConsumers;
|
|
427
|
+
hookComponentCode = generateChainedHookComponent(compName, uniqueNs, rewrittenHook, consumers.length === 1 ? code.slice(consumers[0].start, consumers[0].end) : `<>\n ${consumers.map((c) => code.slice(c.start, c.end)).join("\n ")}\n </>`);
|
|
428
|
+
} else hookComponentCode = generateHookComponent(compName, uniqueNs, rewrittenHook);
|
|
429
|
+
s.remove(ht.statementStart, ht.statementEnd);
|
|
430
|
+
if (ht.aliasDeclarationRange) {
|
|
431
|
+
const key = `${ht.aliasDeclarationRange.start}-${ht.aliasDeclarationRange.end}`;
|
|
432
|
+
if (!removedRanges.has(key)) {
|
|
433
|
+
removedRanges.add(key);
|
|
434
|
+
s.remove(ht.aliasDeclarationRange.start, ht.aliasDeclarationRange.end);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (isChained) for (const consumer of ht.jsxConsumers) s.remove(consumer.start, consumer.end);
|
|
438
|
+
s.appendLeft(ht.returnStart, hookComponentCode);
|
|
439
|
+
s.appendLeft(ht.rootOpeningEnd, `\n<${compName} />`);
|
|
440
|
+
}
|
|
441
|
+
return { changed: true };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
//#endregion
|
|
445
|
+
export { uiTransformCore };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -11,17 +11,9 @@ export declare function extractMDXAttributes(jsxNode: MDXJSXElement): {
|
|
|
11
11
|
titleProp?: string;
|
|
12
12
|
descriptionProp?: string;
|
|
13
13
|
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* @param id - File ID
|
|
18
|
-
* @param importSources - Sources to scan for imports
|
|
19
|
-
* @param availableCollections - Set of available collection names
|
|
20
|
-
* @param collectionNames - Map of alias to collection name
|
|
21
|
-
* @param packs - Custom packs configuration
|
|
22
|
-
* @param debug - Debug logging function
|
|
23
|
-
* @returns Transformation result or null
|
|
24
|
-
*/
|
|
14
|
+
export declare function transformMDXFileCore(code: string, id: string, s: MagicString, importSources: string[], availableCollections: Set<string>, collectionNames: Map<string, string>, packs: PacksMap | undefined, debug: (message: string, ...data: unknown[]) => void): {
|
|
15
|
+
changed: boolean;
|
|
16
|
+
};
|
|
25
17
|
export declare function transformMDXFile(code: string, id: string, importSources: string[], availableCollections: Set<string>, collectionNames: Map<string, string>, packs: PacksMap | undefined, debug: (message: string, ...data: unknown[]) => void): {
|
|
26
18
|
code: string;
|
|
27
19
|
map: ReturnType<MagicString["generateMap"]>;
|
|
@@ -40,27 +40,16 @@ import remarkMdx from "remark-mdx";
|
|
|
40
40
|
descriptionProp
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
* Transform MDX file using remark-mdx for semantic analysis
|
|
45
|
-
* @param code - Original MDX source code
|
|
46
|
-
* @param id - File ID
|
|
47
|
-
* @param importSources - Sources to scan for imports
|
|
48
|
-
* @param availableCollections - Set of available collection names
|
|
49
|
-
* @param collectionNames - Map of alias to collection name
|
|
50
|
-
* @param packs - Custom packs configuration
|
|
51
|
-
* @param debug - Debug logging function
|
|
52
|
-
* @returns Transformation result or null
|
|
53
|
-
*/ function transformMDXFile(code, id, importSources, availableCollections, collectionNames, packs, debug) {
|
|
43
|
+
function transformMDXFileCore(code, id, s, importSources, availableCollections, collectionNames, packs, debug) {
|
|
54
44
|
try {
|
|
55
45
|
debug(`[MDX] Parsing ${id}`);
|
|
56
46
|
const mdast = remark().use(remarkMdx).parse(code);
|
|
57
47
|
const aliasToPack = extractMDXImportAliases(mdast, importSources, availableCollections, collectionNames, packs, debug);
|
|
58
48
|
if (aliasToPack.size === 0) {
|
|
59
49
|
debug(`[MDX] No icon imports found in ${id}`);
|
|
60
|
-
return
|
|
50
|
+
return { changed: false };
|
|
61
51
|
}
|
|
62
52
|
debug("[MDX] Found icon imports:", Array.from(aliasToPack.entries()));
|
|
63
|
-
const s = new MagicString(code);
|
|
64
53
|
const ctx = {
|
|
65
54
|
usedImports: /* @__PURE__ */ new Set(),
|
|
66
55
|
importVars: /* @__PURE__ */ new Set(),
|
|
@@ -102,7 +91,7 @@ import remarkMdx from "remark-mdx";
|
|
|
102
91
|
} });
|
|
103
92
|
if (!hasChanges) {
|
|
104
93
|
debug(`[MDX] No icon elements found in ${id}`);
|
|
105
|
-
return
|
|
94
|
+
return { changed: false };
|
|
106
95
|
}
|
|
107
96
|
const importStatements = Array.from(ctx.usedImports).map((virtualId) => {
|
|
108
97
|
return `import ${ctx.virtualToVar.get(virtualId)} from '${virtualId}';`;
|
|
@@ -117,15 +106,20 @@ import remarkMdx from "remark-mdx";
|
|
|
117
106
|
}
|
|
118
107
|
s.appendLeft(insertPos, `${importStatements}\n`);
|
|
119
108
|
debug(`[MDX] Transformation complete for ${id}`);
|
|
120
|
-
return {
|
|
121
|
-
code: s.toString(),
|
|
122
|
-
map: s.generateMap({ hires: true })
|
|
123
|
-
};
|
|
109
|
+
return { changed: true };
|
|
124
110
|
} catch (error) {
|
|
125
111
|
debug("[MDX] Error transforming:", error);
|
|
126
|
-
return
|
|
112
|
+
return { changed: false };
|
|
127
113
|
}
|
|
128
114
|
}
|
|
115
|
+
function transformMDXFile(code, id, importSources, availableCollections, collectionNames, packs, debug) {
|
|
116
|
+
const s = new MagicString(code);
|
|
117
|
+
if (!transformMDXFileCore(code, id, s, importSources, availableCollections, collectionNames, packs, debug).changed) return null;
|
|
118
|
+
return {
|
|
119
|
+
code: s.toString(),
|
|
120
|
+
map: s.generateMap({ hires: true })
|
|
121
|
+
};
|
|
122
|
+
}
|
|
129
123
|
|
|
130
124
|
//#endregion
|
|
131
|
-
export { extractMDXAttributes, transformMDXFile };
|
|
125
|
+
export { extractMDXAttributes, transformMDXFile, transformMDXFileCore };
|
|
@@ -33,18 +33,9 @@ export declare function findIconElements(ast: Program, aliasToPack: Map<string,
|
|
|
33
33
|
* @returns True if transformation was applied
|
|
34
34
|
*/
|
|
35
35
|
export declare function transformIconElement(elem: JSXElement, s: MagicString, source: string, aliasToPack: Map<string, string>, collectionNames: Map<string, string>, packs: PacksMap | undefined, ctx: TransformContext, debug: (message: string, ...data: unknown[]) => void): boolean;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* @param id - File ID
|
|
40
|
-
* @param ast - Parsed AST
|
|
41
|
-
* @param aliasToPack - Map of aliases to pack names
|
|
42
|
-
* @param collectionNames - Map of alias to collection name
|
|
43
|
-
* @param availableCollections - Set of available collection names
|
|
44
|
-
* @param packs - Custom packs configuration
|
|
45
|
-
* @param debug - Debug logging function
|
|
46
|
-
* @returns Transformation result or null
|
|
47
|
-
*/
|
|
36
|
+
export declare function transformTSXFileCore(code: string, id: string, ast: Program, s: MagicString, aliasToPack: Map<string, string>, collectionNames: Map<string, string>, availableCollections: Set<string>, packs: PacksMap | undefined, debug: (message: string, ...data: unknown[]) => void): {
|
|
37
|
+
changed: boolean;
|
|
38
|
+
};
|
|
48
39
|
export declare function transformTSXFile(code: string, id: string, ast: Program, aliasToPack: Map<string, string>, collectionNames: Map<string, string>, availableCollections: Set<string>, packs: PacksMap | undefined, debug: (message: string, ...data: unknown[]) => void): {
|
|
49
40
|
code: string;
|
|
50
41
|
map: ReturnType<MagicString["generateMap"]>;
|
|
@@ -108,27 +108,15 @@ import { walk } from "oxc-walker";
|
|
|
108
108
|
debug(`[TRANSFORM_ICON] Successfully transformed ${alias}.${iconName} to SVG JSX`);
|
|
109
109
|
return true;
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
* Transform TSX/JSX file
|
|
113
|
-
* @param code - Original source code
|
|
114
|
-
* @param id - File ID
|
|
115
|
-
* @param ast - Parsed AST
|
|
116
|
-
* @param aliasToPack - Map of aliases to pack names
|
|
117
|
-
* @param collectionNames - Map of alias to collection name
|
|
118
|
-
* @param availableCollections - Set of available collection names
|
|
119
|
-
* @param packs - Custom packs configuration
|
|
120
|
-
* @param debug - Debug logging function
|
|
121
|
-
* @returns Transformation result or null
|
|
122
|
-
*/ function transformTSXFile(code, id, ast, aliasToPack, collectionNames, availableCollections, packs, debug) {
|
|
111
|
+
function transformTSXFileCore(code, id, ast, s, aliasToPack, collectionNames, availableCollections, packs, debug) {
|
|
123
112
|
debug(`[TRANSFORM] Processing ${id} with ${aliasToPack.size} aliases:`, Array.from(aliasToPack.entries()));
|
|
124
113
|
debug(`[DEBUG] collectionNames map: ${JSON.stringify(Array.from(collectionNames.entries()))}`);
|
|
125
114
|
const iconElements = findIconElements(ast, aliasToPack, collectionNames, availableCollections);
|
|
126
115
|
if (iconElements.length === 0) {
|
|
127
116
|
debug(`[TRANSFORM] No icon elements found in ${id}`);
|
|
128
|
-
return
|
|
117
|
+
return { changed: false };
|
|
129
118
|
}
|
|
130
119
|
debug(`[TRANSFORM] Found ${iconElements.length} icon elements in ${id}`);
|
|
131
|
-
const s = new MagicString(code);
|
|
132
120
|
const ctx = {
|
|
133
121
|
usedImports: /* @__PURE__ */ new Set(),
|
|
134
122
|
importVars: /* @__PURE__ */ new Set(),
|
|
@@ -136,31 +124,34 @@ import { walk } from "oxc-walker";
|
|
|
136
124
|
};
|
|
137
125
|
let hasChanges = false;
|
|
138
126
|
for (let i = iconElements.length - 1; i >= 0; i--) if (transformIconElement(iconElements[i], s, code, aliasToPack, collectionNames, packs, ctx, debug)) hasChanges = true;
|
|
139
|
-
if (hasChanges) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
127
|
+
if (!hasChanges) {
|
|
128
|
+
debug(`[TRANSFORM] No changes made to ${id}`);
|
|
129
|
+
return { changed: false };
|
|
130
|
+
}
|
|
131
|
+
if (ctx.usedImports.size > 0) {
|
|
132
|
+
const virtualImports = `${Array.from(ctx.usedImports).map((virtualId) => {
|
|
133
|
+
return `import ${ctx.virtualToVar.get(virtualId)} from '${virtualId}';`;
|
|
134
|
+
}).join("\n")}\n`;
|
|
135
|
+
let insertPos = 0;
|
|
136
|
+
let importCount = 0;
|
|
137
|
+
for (const node of ast.body) if (node.type === "ImportDeclaration") {
|
|
138
|
+
insertPos = Math.max(insertPos, node.end);
|
|
139
|
+
importCount++;
|
|
152
140
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
debug(`[TRANSFORM] Transformation successful for ${id}, returning transformed code`);
|
|
156
|
-
return {
|
|
157
|
-
code: resultCode,
|
|
158
|
-
map: s.generateMap({ hires: true })
|
|
159
|
-
};
|
|
141
|
+
debug(`Found ${importCount} imports, inserting at position ${insertPos}`);
|
|
142
|
+
s.appendLeft(insertPos, `\n${virtualImports.trimEnd()}\n`);
|
|
160
143
|
}
|
|
161
|
-
debug(`[TRANSFORM]
|
|
162
|
-
return
|
|
144
|
+
debug(`[TRANSFORM] Transformation successful for ${id}`);
|
|
145
|
+
return { changed: true };
|
|
146
|
+
}
|
|
147
|
+
function transformTSXFile(code, id, ast, aliasToPack, collectionNames, availableCollections, packs, debug) {
|
|
148
|
+
const s = new MagicString(code);
|
|
149
|
+
if (!transformTSXFileCore(code, id, ast, s, aliasToPack, collectionNames, availableCollections, packs, debug).changed) return null;
|
|
150
|
+
return {
|
|
151
|
+
code: s.toString(),
|
|
152
|
+
map: s.generateMap({ hires: true })
|
|
153
|
+
};
|
|
163
154
|
}
|
|
164
155
|
|
|
165
156
|
//#endregion
|
|
166
|
-
export { findIconElements, isIconElement, transformIconElement, transformTSXFile };
|
|
157
|
+
export { findIconElements, isIconElement, transformIconElement, transformTSXFile, transformTSXFileCore };
|