@semi-solid/compiler 0.1.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/LICENSE +21 -0
- package/dist/chunk-BDCUB2QP.js +28 -0
- package/dist/chunk-HSIWLJX2.js +41 -0
- package/dist/chunk-X2A6LNIZ.js +429 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +9 -0
- package/dist/cli/config.d.ts +17 -0
- package/dist/cli/config.js +6 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +8 -0
- package/dist/index.d.ts +645 -0
- package/dist/index.js +2115 -0
- package/package.json +46 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2115 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VIRTUAL_LOCALE_MODULE,
|
|
3
|
+
resolveActiveLocalePath,
|
|
4
|
+
resolveLocaleFiles,
|
|
5
|
+
virtualLocaleIds
|
|
6
|
+
} from "./chunk-HSIWLJX2.js";
|
|
7
|
+
|
|
8
|
+
// src/plugin.ts
|
|
9
|
+
import fs2 from "fs";
|
|
10
|
+
import path3 from "path";
|
|
11
|
+
|
|
12
|
+
// src/tap-extract.ts
|
|
13
|
+
import { parseSync } from "oxc-parser";
|
|
14
|
+
import MagicString from "magic-string";
|
|
15
|
+
|
|
16
|
+
// src/ast-utils.ts
|
|
17
|
+
function isIdentifier(node) {
|
|
18
|
+
if (!node || typeof node !== "object") return false;
|
|
19
|
+
const n = node;
|
|
20
|
+
return n.type === "Identifier" && typeof n.name === "string";
|
|
21
|
+
}
|
|
22
|
+
function isStringLiteral(node) {
|
|
23
|
+
if (!node || typeof node !== "object") return false;
|
|
24
|
+
const n = node;
|
|
25
|
+
return n.type === "StringLiteral" && typeof n.value === "string" || n.type === "Literal" && typeof n.value === "string";
|
|
26
|
+
}
|
|
27
|
+
function isCallExpression(node) {
|
|
28
|
+
if (!node || typeof node !== "object") return false;
|
|
29
|
+
const n = node;
|
|
30
|
+
return n.type === "CallExpression" && Array.isArray(n.arguments);
|
|
31
|
+
}
|
|
32
|
+
function isVariableDeclarator(node) {
|
|
33
|
+
if (!node || typeof node !== "object") return false;
|
|
34
|
+
return node.type === "VariableDeclarator";
|
|
35
|
+
}
|
|
36
|
+
function isJSXElement(node) {
|
|
37
|
+
if (!node || typeof node !== "object") return false;
|
|
38
|
+
return node.type === "JSXElement";
|
|
39
|
+
}
|
|
40
|
+
function isJSXFragment(node) {
|
|
41
|
+
if (!node || typeof node !== "object") return false;
|
|
42
|
+
return node.type === "JSXFragment";
|
|
43
|
+
}
|
|
44
|
+
function isJSXText(node) {
|
|
45
|
+
if (!node || typeof node !== "object") return false;
|
|
46
|
+
return node.type === "JSXText";
|
|
47
|
+
}
|
|
48
|
+
function isJSXExpressionContainer(node) {
|
|
49
|
+
if (!node || typeof node !== "object") return false;
|
|
50
|
+
return node.type === "JSXExpressionContainer";
|
|
51
|
+
}
|
|
52
|
+
function isJSXAttribute(node) {
|
|
53
|
+
if (!node || typeof node !== "object") return false;
|
|
54
|
+
return node.type === "JSXAttribute";
|
|
55
|
+
}
|
|
56
|
+
function isJSXIdentifier(node) {
|
|
57
|
+
if (!node || typeof node !== "object") return false;
|
|
58
|
+
const n = node;
|
|
59
|
+
return n.type === "JSXIdentifier" && typeof n.name === "string";
|
|
60
|
+
}
|
|
61
|
+
function isJSXMemberExpression(node) {
|
|
62
|
+
if (!node || typeof node !== "object") return false;
|
|
63
|
+
return node.type === "JSXMemberExpression";
|
|
64
|
+
}
|
|
65
|
+
function isReturnStatement(node) {
|
|
66
|
+
if (!node || typeof node !== "object") return false;
|
|
67
|
+
return node.type === "ReturnStatement";
|
|
68
|
+
}
|
|
69
|
+
function isMemberExpression(node) {
|
|
70
|
+
if (!node || typeof node !== "object") return false;
|
|
71
|
+
const n = node;
|
|
72
|
+
return n.type === "MemberExpression" && !!n.object && !!n.property && typeof n.computed === "boolean";
|
|
73
|
+
}
|
|
74
|
+
function isUnaryExpression(node) {
|
|
75
|
+
if (!node || typeof node !== "object") return false;
|
|
76
|
+
const n = node;
|
|
77
|
+
return n.type === "UnaryExpression" && typeof n.operator === "string" && !!n.argument;
|
|
78
|
+
}
|
|
79
|
+
function isFunctionLike(node) {
|
|
80
|
+
if (!node || typeof node !== "object") return false;
|
|
81
|
+
const n = node;
|
|
82
|
+
return (n.type === "ArrowFunctionExpression" || n.type === "FunctionExpression") && Array.isArray(n.params) && !!n.body;
|
|
83
|
+
}
|
|
84
|
+
function unwrapTypeAssertions(node) {
|
|
85
|
+
let current = node;
|
|
86
|
+
while (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression") {
|
|
87
|
+
current = current.expression;
|
|
88
|
+
}
|
|
89
|
+
return current;
|
|
90
|
+
}
|
|
91
|
+
function isTapCall(node) {
|
|
92
|
+
if (!isCallExpression(node)) return false;
|
|
93
|
+
if (!isIdentifier(node.callee) || node.callee.name !== "tap") return false;
|
|
94
|
+
if (node.arguments.length < 2) return false;
|
|
95
|
+
if (!isStringLiteral(node.arguments[0])) return false;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
function isTapWhenCall(node) {
|
|
99
|
+
if (!isCallExpression(node)) return false;
|
|
100
|
+
if (!isIdentifier(node.callee) || node.callee.name !== "tapWhen") return false;
|
|
101
|
+
if (node.arguments.length < 3) return false;
|
|
102
|
+
if (!isStringLiteral(node.arguments[0])) return false;
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
function isTapPersonalizedCall(node) {
|
|
106
|
+
if (!isCallExpression(node)) return false;
|
|
107
|
+
if (!isIdentifier(node.callee) || node.callee.name !== "tapPersonalized") return false;
|
|
108
|
+
if (node.arguments.length < 3) return false;
|
|
109
|
+
if (!isStringLiteral(node.arguments[0])) return false;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
function isTapRemoteCall(node) {
|
|
113
|
+
if (!isCallExpression(node)) return false;
|
|
114
|
+
if (!isIdentifier(node.callee) || node.callee.name !== "tapRemote") return false;
|
|
115
|
+
if (node.arguments.length < 2) return false;
|
|
116
|
+
if (!isIdentifier(node.arguments[0])) return false;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
function walkAst(root, visitor, parent = null) {
|
|
120
|
+
if (!root || typeof root !== "object") return false;
|
|
121
|
+
const node = root;
|
|
122
|
+
if (typeof node.type !== "string") return false;
|
|
123
|
+
const result = visitor(node, parent);
|
|
124
|
+
if (result === "stop") return true;
|
|
125
|
+
for (const key of Object.keys(node)) {
|
|
126
|
+
if (key === "span" || key === "type") continue;
|
|
127
|
+
const child = node[key];
|
|
128
|
+
if (Array.isArray(child)) {
|
|
129
|
+
for (const item of child) {
|
|
130
|
+
if (item && typeof item === "object" && typeof item.type === "string") {
|
|
131
|
+
const stopped = walkAst(item, visitor, node);
|
|
132
|
+
if (stopped) return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else if (child && typeof child === "object" && typeof child.type === "string") {
|
|
136
|
+
const stopped = walkAst(child, visitor, node);
|
|
137
|
+
if (stopped) return true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
function getJSXTagName(nameNode) {
|
|
143
|
+
if (isJSXIdentifier(nameNode)) return nameNode.name;
|
|
144
|
+
if (isJSXMemberExpression(nameNode)) {
|
|
145
|
+
const obj = getJSXTagName(nameNode.object);
|
|
146
|
+
const prop = getJSXTagName(nameNode.property);
|
|
147
|
+
if (obj && prop) return `${obj}.${prop}`;
|
|
148
|
+
}
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
function isComponentTag(tagName) {
|
|
152
|
+
return tagName.charAt(0) === tagName.charAt(0).toUpperCase() && tagName.charAt(0) !== tagName.charAt(0).toLowerCase();
|
|
153
|
+
}
|
|
154
|
+
function toKebabCase(name) {
|
|
155
|
+
return name.replace(
|
|
156
|
+
/([A-Z])/g,
|
|
157
|
+
(_, char, offset) => offset > 0 ? `-${char.toLowerCase()}` : char.toLowerCase()
|
|
158
|
+
).replace(/[_\s]+/g, "-");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/tap-extract.ts
|
|
162
|
+
function hasSpan(node) {
|
|
163
|
+
return typeof node.start === "number" && typeof node.end === "number";
|
|
164
|
+
}
|
|
165
|
+
function extractTapMappings(source, filename = "component.tsx") {
|
|
166
|
+
const { program, errors } = parseSync(filename, source);
|
|
167
|
+
const warnings = [];
|
|
168
|
+
if (errors.length > 0) {
|
|
169
|
+
for (const err of errors) {
|
|
170
|
+
warnings.push(`Parse error in ${filename}: ${err.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const mappings = {};
|
|
174
|
+
const reactiveVars = /* @__PURE__ */ new Set();
|
|
175
|
+
const remoteComponents = /* @__PURE__ */ new Set();
|
|
176
|
+
const personalizedCalls = [];
|
|
177
|
+
const s = new MagicString(source);
|
|
178
|
+
let inlineCounter = 0;
|
|
179
|
+
let needsTapRemoteImport = false;
|
|
180
|
+
let needsPersonalizedImport = false;
|
|
181
|
+
walkAst(program, (node) => {
|
|
182
|
+
if (isVariableDeclarator(node)) {
|
|
183
|
+
const { id, init } = node;
|
|
184
|
+
if (!init || !isIdentifier(id)) return;
|
|
185
|
+
const inner = unwrapTypeAssertions(init);
|
|
186
|
+
const isTap = isTapCall(inner);
|
|
187
|
+
const isTapWhen = !isTap && isTapWhenCall(inner);
|
|
188
|
+
const isTapPersonalized = !isTap && !isTapWhen && isTapPersonalizedCall(inner);
|
|
189
|
+
const isTapRemote = !isTap && !isTapWhen && !isTapPersonalized && isTapRemoteCall(inner);
|
|
190
|
+
if (!isTap && !isTapWhen && !isTapPersonalized && !isTapRemote) return;
|
|
191
|
+
if (isTapPersonalized) {
|
|
192
|
+
const varName2 = id.name;
|
|
193
|
+
const urlLiteral = inner.arguments[0].value;
|
|
194
|
+
const paramsNode = inner.arguments[1];
|
|
195
|
+
const fallbackNode = inner.arguments[2];
|
|
196
|
+
const params = {};
|
|
197
|
+
if (paramsNode.type === "ObjectExpression" && Array.isArray(paramsNode.properties)) {
|
|
198
|
+
for (const prop of paramsNode.properties) {
|
|
199
|
+
if (prop.type !== "Property") continue;
|
|
200
|
+
const keyNode = prop.key;
|
|
201
|
+
const valueNode = prop.value;
|
|
202
|
+
const keyName = isIdentifier(keyNode) ? keyNode.name : keyNode.value;
|
|
203
|
+
const valueName = isIdentifier(valueNode) ? valueNode.name : keyName;
|
|
204
|
+
params[keyName] = valueName;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
personalizedCalls.push({ varName: varName2, url: urlLiteral, params });
|
|
208
|
+
needsPersonalizedImport = true;
|
|
209
|
+
if (hasSpan(paramsNode) && hasSpan(fallbackNode) && hasSpan(init)) {
|
|
210
|
+
const paramsSrc = source.slice(paramsNode.start, paramsNode.end);
|
|
211
|
+
const fallbackSrc = source.slice(fallbackNode.start, fallbackNode.end);
|
|
212
|
+
s.overwrite(
|
|
213
|
+
init.start,
|
|
214
|
+
init.end,
|
|
215
|
+
`createPersonalizedSignal(${JSON.stringify(urlLiteral)}, ${paramsSrc}, ${fallbackSrc})`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (isTapRemote) {
|
|
221
|
+
const compName = inner.arguments[0].name;
|
|
222
|
+
const urlNode = inner.arguments[1];
|
|
223
|
+
remoteComponents.add(compName);
|
|
224
|
+
needsTapRemoteImport = true;
|
|
225
|
+
if (hasSpan(urlNode) && hasSpan(init)) {
|
|
226
|
+
const kebabName = toKebabCase(compName);
|
|
227
|
+
const urlSrc = source.slice(urlNode.start, urlNode.end);
|
|
228
|
+
s.overwrite(
|
|
229
|
+
init.start,
|
|
230
|
+
init.end,
|
|
231
|
+
`__tapRemoteHtml("remote-${kebabName}", ${urlSrc})`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const varName = id.name;
|
|
237
|
+
const liquidExpr = inner.arguments[0].value;
|
|
238
|
+
validateLiquidExpr(liquidExpr, varName, warnings);
|
|
239
|
+
mappings[varName] = liquidExpr;
|
|
240
|
+
if (isTap) {
|
|
241
|
+
const fallbackNode = inner.arguments[1];
|
|
242
|
+
if (hasSpan(fallbackNode) && hasSpan(init)) {
|
|
243
|
+
const fallbackSrc = source.slice(fallbackNode.start, fallbackNode.end);
|
|
244
|
+
s.overwrite(init.start, init.end, fallbackSrc);
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
reactiveVars.add(varName);
|
|
248
|
+
const depsNode = inner.arguments[1];
|
|
249
|
+
const fallbackNode = inner.arguments[2];
|
|
250
|
+
if (hasSpan(depsNode) && hasSpan(fallbackNode) && hasSpan(init)) {
|
|
251
|
+
const depsSrc = source.slice(depsNode.start, depsNode.end);
|
|
252
|
+
const fallbackSrc = source.slice(fallbackNode.start, fallbackNode.end);
|
|
253
|
+
s.overwrite(
|
|
254
|
+
init.start,
|
|
255
|
+
init.end,
|
|
256
|
+
`createTapSignal(${JSON.stringify(varName)}, ${depsSrc}, ${fallbackSrc})`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (isJSXExpressionContainer(node)) {
|
|
263
|
+
const expr = node.expression;
|
|
264
|
+
const inner = unwrapTypeAssertions(expr);
|
|
265
|
+
if (isTapRemoteCall(inner)) {
|
|
266
|
+
const compName = inner.arguments[0].name;
|
|
267
|
+
const urlNode = inner.arguments[1];
|
|
268
|
+
remoteComponents.add(compName);
|
|
269
|
+
needsTapRemoteImport = true;
|
|
270
|
+
if (hasSpan(urlNode) && hasSpan(expr)) {
|
|
271
|
+
const kebabName = toKebabCase(compName);
|
|
272
|
+
const urlSrc = source.slice(urlNode.start, urlNode.end);
|
|
273
|
+
s.overwrite(
|
|
274
|
+
expr.start,
|
|
275
|
+
expr.end,
|
|
276
|
+
`__tapRemoteHtml("remote-${kebabName}", ${urlSrc})`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!isTapCall(inner)) return;
|
|
282
|
+
const liquidExpr = inner.arguments[0].value;
|
|
283
|
+
const fallbackNode = inner.arguments[1];
|
|
284
|
+
const syntheticName = `__tap_inline_${inlineCounter++}__`;
|
|
285
|
+
validateLiquidExpr(liquidExpr, "inline", warnings);
|
|
286
|
+
mappings[syntheticName] = liquidExpr;
|
|
287
|
+
if (hasSpan(fallbackNode) && hasSpan(expr)) {
|
|
288
|
+
const fallbackSrc = source.slice(fallbackNode.start, fallbackNode.end);
|
|
289
|
+
s.overwrite(expr.start, expr.end, fallbackSrc);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
if (reactiveVars.size > 0) {
|
|
294
|
+
s.prepend(`import { createTapSignal } from '$lib/runtime';
|
|
295
|
+
`);
|
|
296
|
+
}
|
|
297
|
+
if (needsPersonalizedImport) {
|
|
298
|
+
s.prepend(`import { createPersonalizedSignal } from '$lib/runtime';
|
|
299
|
+
`);
|
|
300
|
+
}
|
|
301
|
+
if (needsTapRemoteImport) {
|
|
302
|
+
s.prepend(`import { __tapRemoteHtml } from '$lib/runtime';
|
|
303
|
+
`);
|
|
304
|
+
}
|
|
305
|
+
const cleanedSource = s.toString();
|
|
306
|
+
const sourceMap = s.generateMap({ hires: true }).toString();
|
|
307
|
+
return { mappings, cleanedSource, sourceMap, warnings, reactiveVars, remoteComponents, personalizedCalls };
|
|
308
|
+
}
|
|
309
|
+
function validateLiquidExpr(expr, context, warnings) {
|
|
310
|
+
if (expr.includes("`")) {
|
|
311
|
+
warnings.push(
|
|
312
|
+
`tap() in "${context}": liquid expression contains a backtick. Use a plain string literal, not a template literal.`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// src/liquid-gen.ts
|
|
318
|
+
import { parseSync as parseSync2 } from "oxc-parser";
|
|
319
|
+
|
|
320
|
+
// src/control-flow.ts
|
|
321
|
+
function resolveShowCondition(whenExpr, mappings, loopVars, warnings) {
|
|
322
|
+
if (isIdentifier(whenExpr)) {
|
|
323
|
+
const liquidStr = mappings[whenExpr.name];
|
|
324
|
+
if (liquidStr) {
|
|
325
|
+
if (hasLiquidFilters(liquidStr)) {
|
|
326
|
+
warnings?.push(
|
|
327
|
+
`<Show when={${whenExpr.name}}>: tap() expression '${liquidStr}' contains Liquid filters which are not valid in {% if %} tag context. Use a separate tap() without filters for conditions (e.g. tap('{{ product.available }}', false)). Falling back to client-side rendering.`
|
|
328
|
+
);
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
return { liquidExpr: stripLiquidBraces(liquidStr), negated: false };
|
|
332
|
+
}
|
|
333
|
+
if (loopVars.has(whenExpr.name)) {
|
|
334
|
+
return { liquidExpr: whenExpr.name, negated: false };
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
if (isUnaryExpression(whenExpr) && whenExpr.operator === "!") {
|
|
339
|
+
const inner = resolveShowCondition(whenExpr.argument, mappings, loopVars, warnings);
|
|
340
|
+
if (inner) return { liquidExpr: inner.liquidExpr, negated: !inner.negated };
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const memberPath = resolveMemberPath(whenExpr, mappings, loopVars);
|
|
344
|
+
if (memberPath && memberPath.includes("|")) {
|
|
345
|
+
warnings?.push(
|
|
346
|
+
`<Show when={...}>: resolved expression '${memberPath}' contains Liquid filters which are not valid in {% if %} tag context. Falling back to client-side rendering.`
|
|
347
|
+
);
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
if (memberPath) return { liquidExpr: memberPath, negated: false };
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
function resolveForIteration(eachExpr, loopVarName, mappings, loopVars = /* @__PURE__ */ new Set(), warnings) {
|
|
354
|
+
if (isIdentifier(eachExpr)) {
|
|
355
|
+
const liquidStr = mappings[eachExpr.name];
|
|
356
|
+
if (!liquidStr) return null;
|
|
357
|
+
if (hasLiquidFilters(liquidStr)) {
|
|
358
|
+
warnings?.push(
|
|
359
|
+
`<For each={${eachExpr.name}}>: tap() expression '${liquidStr}' contains Liquid filters which are not valid in {% for %} tag context. Use a separate tap() without filters for collections. Falling back to client-side rendering.`
|
|
360
|
+
);
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
collection: stripLiquidBraces(liquidStr),
|
|
365
|
+
loopVar: loopVarName
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (isMemberExpression(eachExpr)) {
|
|
369
|
+
const memberPath = resolveMemberPath(eachExpr, mappings, loopVars);
|
|
370
|
+
if (memberPath && memberPath.includes("|")) {
|
|
371
|
+
warnings?.push(
|
|
372
|
+
`<For each={...}>: resolved expression '${memberPath}' contains Liquid filters which are not valid in {% for %} tag context. Falling back to client-side rendering.`
|
|
373
|
+
);
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
if (memberPath) {
|
|
377
|
+
return { collection: memberPath, loopVar: loopVarName };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
function resolveMemberPath(node, mappings, loopVars) {
|
|
383
|
+
if (isIdentifier(node)) {
|
|
384
|
+
if (loopVars.has(node.name)) return node.name;
|
|
385
|
+
if (mappings[node.name]) return stripLiquidBraces(mappings[node.name]);
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
if (isMemberExpression(node)) {
|
|
389
|
+
if (node.computed) return null;
|
|
390
|
+
const objPath = resolveMemberPath(node.object, mappings, loopVars);
|
|
391
|
+
if (!objPath) return null;
|
|
392
|
+
if (!isIdentifier(node.property)) return null;
|
|
393
|
+
return `${objPath}.${node.property.name}`;
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
function stripLiquidBraces(liquidStr) {
|
|
398
|
+
const trimmed = liquidStr.trim();
|
|
399
|
+
if (trimmed.startsWith("{{") && trimmed.endsWith("}}")) {
|
|
400
|
+
return trimmed.slice(2, -2).trim();
|
|
401
|
+
}
|
|
402
|
+
return trimmed;
|
|
403
|
+
}
|
|
404
|
+
function hasLiquidFilters(liquidStr) {
|
|
405
|
+
const bare = stripLiquidBraces(liquidStr);
|
|
406
|
+
return bare.includes("|");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/liquid-gen.ts
|
|
410
|
+
var TRANSPARENT_COMPONENTS = /* @__PURE__ */ new Set([
|
|
411
|
+
"Transition",
|
|
412
|
+
"TransitionGroup",
|
|
413
|
+
"ErrorBoundary",
|
|
414
|
+
"Suspense"
|
|
415
|
+
]);
|
|
416
|
+
var OPAQUE_COMPONENTS = /* @__PURE__ */ new Set([
|
|
417
|
+
"Portal",
|
|
418
|
+
"Dynamic"
|
|
419
|
+
]);
|
|
420
|
+
function generateLiquid(source, mappings, options) {
|
|
421
|
+
const filename = `${options.componentName}.tsx`;
|
|
422
|
+
const { program, errors } = parseSync2(filename, source);
|
|
423
|
+
if (errors.length > 0) {
|
|
424
|
+
const msgs = errors.map((e) => e.message).join(", ");
|
|
425
|
+
throw new Error(`Parse errors in ${filename}: ${msgs}`);
|
|
426
|
+
}
|
|
427
|
+
let jsxRoot = null;
|
|
428
|
+
walkAst(program, (node) => {
|
|
429
|
+
if (jsxRoot) return;
|
|
430
|
+
if (isReturnStatement(node) && node.argument) {
|
|
431
|
+
let candidate = node.argument;
|
|
432
|
+
if (candidate.type === "ParenthesizedExpression" && candidate.expression) {
|
|
433
|
+
candidate = candidate.expression;
|
|
434
|
+
}
|
|
435
|
+
if (isJSXElement(candidate) || isJSXFragment(candidate)) {
|
|
436
|
+
jsxRoot = candidate;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
if (!jsxRoot) {
|
|
441
|
+
throw new Error(
|
|
442
|
+
`No JSX return statement found in ${filename}. The component must return JSX directly from its function body.`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
const warnings = options.warnings ?? [];
|
|
446
|
+
const ctx = {
|
|
447
|
+
mappings,
|
|
448
|
+
indent: options.indent ?? " ",
|
|
449
|
+
source,
|
|
450
|
+
loopVars: /* @__PURE__ */ new Set(),
|
|
451
|
+
sectionComponents: options.sectionComponents ?? /* @__PURE__ */ new Set(),
|
|
452
|
+
hydration: options.dataProps != null ? {
|
|
453
|
+
componentName: options.componentName,
|
|
454
|
+
dataProps: options.dataProps,
|
|
455
|
+
dataSectionId: options.dataSectionId,
|
|
456
|
+
applied: false
|
|
457
|
+
} : null,
|
|
458
|
+
warnings
|
|
459
|
+
};
|
|
460
|
+
return renderNode(jsxRoot, ctx, 0).trim() + "\n";
|
|
461
|
+
}
|
|
462
|
+
function ind(ctx, depth) {
|
|
463
|
+
return ctx.indent.repeat(depth);
|
|
464
|
+
}
|
|
465
|
+
function withLoopVar(ctx, varName) {
|
|
466
|
+
return { ...ctx, loopVars: /* @__PURE__ */ new Set([...ctx.loopVars, varName]) };
|
|
467
|
+
}
|
|
468
|
+
function renderNode(node, ctx, depth) {
|
|
469
|
+
if (isJSXElement(node)) return renderElement(node, ctx, depth);
|
|
470
|
+
if (isJSXFragment(node)) return renderChildren(node.children, ctx, depth);
|
|
471
|
+
if (isJSXText(node)) return renderJSXText(node.value);
|
|
472
|
+
if (isJSXExpressionContainer(node)) {
|
|
473
|
+
return renderExpression(node.expression, ctx, depth);
|
|
474
|
+
}
|
|
475
|
+
return "";
|
|
476
|
+
}
|
|
477
|
+
function renderElement(element, ctx, depth) {
|
|
478
|
+
const opening = element.openingElement;
|
|
479
|
+
const tagName = getJSXTagName(opening.name);
|
|
480
|
+
if (!tagName) return `<!-- unknown tag -->`;
|
|
481
|
+
if (tagName === "Show") return renderShow(element, ctx, depth);
|
|
482
|
+
if (tagName === "For") return renderFor(element, ctx, depth);
|
|
483
|
+
if (tagName === "Match") return renderMatch(element, ctx, depth);
|
|
484
|
+
if (tagName === "Case") return `<!-- <Case> must appear inside <Match> -->`;
|
|
485
|
+
if (TRANSPARENT_COMPONENTS.has(tagName)) {
|
|
486
|
+
return renderChildren(element.children, ctx, depth);
|
|
487
|
+
}
|
|
488
|
+
if (OPAQUE_COMPONENTS.has(tagName)) {
|
|
489
|
+
return "";
|
|
490
|
+
}
|
|
491
|
+
if (isComponentTag(tagName)) {
|
|
492
|
+
return renderComponent(element, ctx, depth);
|
|
493
|
+
}
|
|
494
|
+
const attrs = renderAttributes(opening.attributes, ctx);
|
|
495
|
+
let hydrationAttrStr = "";
|
|
496
|
+
if (ctx.hydration && !ctx.hydration.applied) {
|
|
497
|
+
ctx.hydration.applied = true;
|
|
498
|
+
hydrationAttrStr = ` data-component="${ctx.hydration.componentName}" data-props='${ctx.hydration.dataProps}'`;
|
|
499
|
+
if (ctx.hydration.dataSectionId) {
|
|
500
|
+
hydrationAttrStr += ` data-section-id="${ctx.hydration.dataSectionId}"`;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
const attrStr = (attrs.length > 0 ? " " + attrs.join(" ") : "") + hydrationAttrStr;
|
|
504
|
+
const selfClosing = !!opening.selfClosing;
|
|
505
|
+
if (selfClosing) {
|
|
506
|
+
return `${ind(ctx, depth)}<${tagName}${attrStr} />`;
|
|
507
|
+
}
|
|
508
|
+
const children = element.children;
|
|
509
|
+
if (children.length === 0) {
|
|
510
|
+
return `${ind(ctx, depth)}<${tagName}${attrStr}></${tagName}>`;
|
|
511
|
+
}
|
|
512
|
+
const renderedChildren = children.map((child) => renderNode(child, ctx, depth + 1)).filter((s) => s.trim().length > 0);
|
|
513
|
+
if (renderedChildren.length === 1 && !renderedChildren[0].includes("\n")) {
|
|
514
|
+
const child = renderedChildren[0].trim();
|
|
515
|
+
return `${ind(ctx, depth)}<${tagName}${attrStr}>${child}</${tagName}>`;
|
|
516
|
+
}
|
|
517
|
+
const childLines = renderedChildren.map(
|
|
518
|
+
(s) => s.startsWith(ind(ctx, depth + 1)) ? s : `${ind(ctx, depth + 1)}${s.trim()}`
|
|
519
|
+
).join("\n");
|
|
520
|
+
return [
|
|
521
|
+
`${ind(ctx, depth)}<${tagName}${attrStr}>`,
|
|
522
|
+
childLines,
|
|
523
|
+
`${ind(ctx, depth)}</${tagName}>`
|
|
524
|
+
].join("\n");
|
|
525
|
+
}
|
|
526
|
+
function renderShow(element, ctx, depth) {
|
|
527
|
+
const attrs = element.openingElement.attributes;
|
|
528
|
+
const whenAttr = findJSXAttr(attrs, "when");
|
|
529
|
+
const fallbackAttr = findJSXAttr(attrs, "fallback");
|
|
530
|
+
if (!whenAttr) {
|
|
531
|
+
return `<!-- <Show> missing 'when' prop -->`;
|
|
532
|
+
}
|
|
533
|
+
const whenExpr = getJSXAttrExpr(whenAttr);
|
|
534
|
+
if (!whenExpr) {
|
|
535
|
+
return `<!-- <Show> 'when' prop must be a JSX expression -->`;
|
|
536
|
+
}
|
|
537
|
+
const condition = resolveShowCondition(whenExpr, ctx.mappings, ctx.loopVars, ctx.warnings);
|
|
538
|
+
if (!condition) {
|
|
539
|
+
return `<!-- <Show> condition is not Liquid-mapped \u2014 rendered client-side -->`;
|
|
540
|
+
}
|
|
541
|
+
const tag = condition.negated ? "unless" : "if";
|
|
542
|
+
const endTag = condition.negated ? "endunless" : "endif";
|
|
543
|
+
const thenContent = renderShowChildren(element.children, ctx, depth + 1);
|
|
544
|
+
if (!fallbackAttr) {
|
|
545
|
+
return [
|
|
546
|
+
`${ind(ctx, depth)}{% ${tag} ${condition.liquidExpr} %}`,
|
|
547
|
+
thenContent,
|
|
548
|
+
`${ind(ctx, depth)}{% ${endTag} %}`
|
|
549
|
+
].join("\n");
|
|
550
|
+
}
|
|
551
|
+
const fallbackContent = renderShowFallback(fallbackAttr, ctx, depth + 1);
|
|
552
|
+
return [
|
|
553
|
+
`${ind(ctx, depth)}{% ${tag} ${condition.liquidExpr} %}`,
|
|
554
|
+
thenContent,
|
|
555
|
+
`${ind(ctx, depth)}{% else %}`,
|
|
556
|
+
fallbackContent,
|
|
557
|
+
`${ind(ctx, depth)}{% ${endTag} %}`
|
|
558
|
+
].join("\n");
|
|
559
|
+
}
|
|
560
|
+
function renderShowChildren(children, ctx, depth) {
|
|
561
|
+
const parts = [];
|
|
562
|
+
for (const child of children) {
|
|
563
|
+
if (isJSXExpressionContainer(child)) {
|
|
564
|
+
const expr = child.expression;
|
|
565
|
+
if (isFunctionLike(expr)) {
|
|
566
|
+
const body = extractFunctionBody(expr);
|
|
567
|
+
if (body) {
|
|
568
|
+
parts.push(renderNode(body, ctx, depth));
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
const rendered = renderNode(child, ctx, depth);
|
|
574
|
+
if (rendered.trim()) parts.push(rendered);
|
|
575
|
+
}
|
|
576
|
+
return parts.map((s) => s.startsWith(ind(ctx, depth)) ? s : `${ind(ctx, depth)}${s.trim()}`).join("\n");
|
|
577
|
+
}
|
|
578
|
+
function renderShowFallback(fallbackAttr, ctx, depth) {
|
|
579
|
+
const value = fallbackAttr.value;
|
|
580
|
+
if (!value) return "";
|
|
581
|
+
if (isJSXExpressionContainer(value)) {
|
|
582
|
+
const expr = value.expression;
|
|
583
|
+
if (isJSXElement(expr) || isJSXFragment(expr)) {
|
|
584
|
+
const rendered = renderNode(expr, ctx, depth);
|
|
585
|
+
return rendered.startsWith(ind(ctx, depth)) ? rendered : `${ind(ctx, depth)}${rendered.trim()}`;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return "";
|
|
589
|
+
}
|
|
590
|
+
function renderFor(element, ctx, depth) {
|
|
591
|
+
const attrs = element.openingElement.attributes;
|
|
592
|
+
const eachAttr = findJSXAttr(attrs, "each");
|
|
593
|
+
if (!eachAttr) {
|
|
594
|
+
return `<!-- <For> missing 'each' prop -->`;
|
|
595
|
+
}
|
|
596
|
+
const eachExpr = getJSXAttrExpr(eachAttr);
|
|
597
|
+
if (!eachExpr) {
|
|
598
|
+
return `<!-- <For> 'each' prop must be a JSX expression -->`;
|
|
599
|
+
}
|
|
600
|
+
const children = element.children;
|
|
601
|
+
const funcContainer = children.find(
|
|
602
|
+
(c) => isJSXExpressionContainer(c) && isFunctionLike(c.expression)
|
|
603
|
+
);
|
|
604
|
+
if (!funcContainer) {
|
|
605
|
+
return `<!-- <For> children must be a render function: {(item) => <jsx>} -->`;
|
|
606
|
+
}
|
|
607
|
+
const renderFn = funcContainer.expression;
|
|
608
|
+
const params = renderFn.params;
|
|
609
|
+
if (params.length === 0 || !isIdentifier(params[0])) {
|
|
610
|
+
return `<!-- <For> render function must have at least one identifier parameter -->`;
|
|
611
|
+
}
|
|
612
|
+
const loopVarName = params[0].name;
|
|
613
|
+
const iteration = resolveForIteration(eachExpr, loopVarName, ctx.mappings, ctx.loopVars, ctx.warnings);
|
|
614
|
+
if (!iteration) {
|
|
615
|
+
return `<!-- <For> collection is not Liquid-mapped \u2014 rendered client-side -->`;
|
|
616
|
+
}
|
|
617
|
+
const bodyJSX = extractFunctionBody(renderFn);
|
|
618
|
+
if (!bodyJSX) {
|
|
619
|
+
return `<!-- <For> could not extract JSX body from render function -->`;
|
|
620
|
+
}
|
|
621
|
+
const loopCtx = withLoopVar(ctx, iteration.loopVar);
|
|
622
|
+
const bodyContent = renderNode(bodyJSX, loopCtx, depth + 1);
|
|
623
|
+
const indentedBody = bodyContent.startsWith(ind(ctx, depth + 1)) ? bodyContent : `${ind(ctx, depth + 1)}${bodyContent.trim()}`;
|
|
624
|
+
return [
|
|
625
|
+
`${ind(ctx, depth)}{% for ${iteration.loopVar} in ${iteration.collection} %}`,
|
|
626
|
+
indentedBody,
|
|
627
|
+
`${ind(ctx, depth)}{% endfor %}`
|
|
628
|
+
].join("\n");
|
|
629
|
+
}
|
|
630
|
+
function renderMatch(element, ctx, depth) {
|
|
631
|
+
const attrs = element.openingElement.attributes;
|
|
632
|
+
const onAttr = findJSXAttr(attrs, "on");
|
|
633
|
+
if (!onAttr) {
|
|
634
|
+
return `<!-- <Match> missing 'on' prop -->`;
|
|
635
|
+
}
|
|
636
|
+
const onExpr = getJSXAttrExpr(onAttr);
|
|
637
|
+
if (!onExpr) {
|
|
638
|
+
return `<!-- <Match> 'on' prop must be a JSX expression -->`;
|
|
639
|
+
}
|
|
640
|
+
let liquidPath = null;
|
|
641
|
+
const memberPath = resolveMemberPath(onExpr, ctx.mappings, ctx.loopVars);
|
|
642
|
+
if (memberPath) {
|
|
643
|
+
liquidPath = memberPath;
|
|
644
|
+
} else if (isIdentifier(onExpr)) {
|
|
645
|
+
const varName = onExpr.name;
|
|
646
|
+
if (ctx.mappings[varName]) {
|
|
647
|
+
liquidPath = stripLiquidBraces(ctx.mappings[varName]);
|
|
648
|
+
} else if (ctx.loopVars.has(varName)) {
|
|
649
|
+
liquidPath = varName;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (liquidPath && liquidPath.includes("|")) {
|
|
653
|
+
ctx.warnings.push(
|
|
654
|
+
`<Match on={...}>: resolved expression '${liquidPath}' contains Liquid filters which are not valid in {% case %} tag context. Falling back to client-side rendering.`
|
|
655
|
+
);
|
|
656
|
+
liquidPath = null;
|
|
657
|
+
}
|
|
658
|
+
if (!liquidPath) {
|
|
659
|
+
return `<!-- <Match> expression is not Liquid-mapped \u2014 rendered client-side -->`;
|
|
660
|
+
}
|
|
661
|
+
const children = element.children;
|
|
662
|
+
const caseParts = [];
|
|
663
|
+
for (const child of children) {
|
|
664
|
+
if (!isJSXElement(child)) continue;
|
|
665
|
+
const caseTag = getJSXTagName(child.openingElement.name);
|
|
666
|
+
if (caseTag !== "Case") continue;
|
|
667
|
+
const caseAttrs = child.openingElement.attributes;
|
|
668
|
+
const valueAttr = findJSXAttr(caseAttrs, "value");
|
|
669
|
+
if (!valueAttr) {
|
|
670
|
+
caseParts.push(`${ind(ctx, depth + 1)}<!-- <Case> missing 'value' prop -->`);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
let caseValue = null;
|
|
674
|
+
const rawValue = valueAttr.value;
|
|
675
|
+
if (rawValue) {
|
|
676
|
+
if (isStringLiteral(rawValue)) {
|
|
677
|
+
caseValue = rawValue.value;
|
|
678
|
+
} else if (isJSXExpressionContainer(rawValue)) {
|
|
679
|
+
const inner = rawValue.expression;
|
|
680
|
+
if (isStringLiteral(inner)) {
|
|
681
|
+
caseValue = inner.value;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (caseValue === null) {
|
|
686
|
+
caseParts.push(`${ind(ctx, depth + 1)}<!-- <Case> 'value' must be a string literal -->`);
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const caseChildren = child.children;
|
|
690
|
+
const renderedBody = caseChildren.map((c) => renderNode(c, ctx, depth + 2)).filter((s) => s.trim().length > 0);
|
|
691
|
+
const bodyLines = renderedBody.map(
|
|
692
|
+
(s) => s.startsWith(ind(ctx, depth + 2)) ? s : `${ind(ctx, depth + 2)}${s.trim()}`
|
|
693
|
+
).join("\n");
|
|
694
|
+
caseParts.push(
|
|
695
|
+
`${ind(ctx, depth + 1)}{% when '${caseValue}' %}` + (bodyLines ? `
|
|
696
|
+
${bodyLines}` : "")
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
if (caseParts.length === 0) {
|
|
700
|
+
return `${ind(ctx, depth)}{% case ${liquidPath} %}
|
|
701
|
+
${ind(ctx, depth)}{% endcase %}`;
|
|
702
|
+
}
|
|
703
|
+
return [
|
|
704
|
+
`${ind(ctx, depth)}{% case ${liquidPath} %}`,
|
|
705
|
+
...caseParts,
|
|
706
|
+
`${ind(ctx, depth)}{% endcase %}`
|
|
707
|
+
].join("\n");
|
|
708
|
+
}
|
|
709
|
+
function renderComponent(element, ctx, depth) {
|
|
710
|
+
const opening = element.openingElement;
|
|
711
|
+
const tagName = getJSXTagName(opening.name);
|
|
712
|
+
const snippetName = toKebabCase(tagName);
|
|
713
|
+
const attrs = opening.attributes;
|
|
714
|
+
if (ctx.sectionComponents.has(tagName)) {
|
|
715
|
+
return `${ind(ctx, depth)}{% section '${snippetName}' %}`;
|
|
716
|
+
}
|
|
717
|
+
const params = [];
|
|
718
|
+
for (const attr of attrs) {
|
|
719
|
+
if (!isJSXAttribute(attr)) continue;
|
|
720
|
+
const nameNode = attr.name;
|
|
721
|
+
if (!isJSXIdentifier(nameNode)) continue;
|
|
722
|
+
const propName = nameNode.name;
|
|
723
|
+
if (propName.startsWith("on") || propName === "ref" || propName === "key") continue;
|
|
724
|
+
const value = attr.value;
|
|
725
|
+
if (!value) continue;
|
|
726
|
+
if (isJSXExpressionContainer(value)) {
|
|
727
|
+
const expr = value.expression;
|
|
728
|
+
const liquidExpr = exprToLiquid(expr, ctx);
|
|
729
|
+
if (liquidExpr) {
|
|
730
|
+
params.push(`${propName}: ${stripLiquidBraces(liquidExpr)}`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const explicitPropNames = new Set(params.map((p) => p.split(":")[0].trim()));
|
|
735
|
+
const contextObjects = /* @__PURE__ */ new Set();
|
|
736
|
+
for (const liquidExpr of Object.values(ctx.mappings)) {
|
|
737
|
+
const m = liquidExpr.match(/\{\{\s*([a-z_]\w+)\./);
|
|
738
|
+
if (m) contextObjects.add(m[1]);
|
|
739
|
+
}
|
|
740
|
+
for (const obj of contextObjects) {
|
|
741
|
+
if (!explicitPropNames.has(obj)) {
|
|
742
|
+
params.push(`${obj}: ${obj}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (params.length === 0) {
|
|
746
|
+
return `${ind(ctx, depth)}{% render '${snippetName}' %}`;
|
|
747
|
+
}
|
|
748
|
+
return `${ind(ctx, depth)}{% render '${snippetName}', ${params.join(", ")} %}`;
|
|
749
|
+
}
|
|
750
|
+
function renderAttributes(attrs, ctx) {
|
|
751
|
+
const result = [];
|
|
752
|
+
for (const attr of attrs) {
|
|
753
|
+
if (attr.type === "JSXSpreadAttribute") {
|
|
754
|
+
const arg = attr.argument;
|
|
755
|
+
if (arg && isCallExpression(arg) && isIdentifier(arg.callee) && arg.callee.name === "blockAttrs") {
|
|
756
|
+
result.push("{{ block.shopify_attributes }}");
|
|
757
|
+
}
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (!isJSXAttribute(attr)) continue;
|
|
761
|
+
const nameNode = attr.name;
|
|
762
|
+
if (!isJSXIdentifier(nameNode)) continue;
|
|
763
|
+
const attrName = nameNode.name;
|
|
764
|
+
if (attrName.startsWith("on") || attrName === "ref" || attrName === "key" || attrName === "classList") continue;
|
|
765
|
+
const value = attr.value;
|
|
766
|
+
if (!value) {
|
|
767
|
+
result.push(attrName);
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (isStringLiteral(value)) {
|
|
771
|
+
result.push(`${attrName}="${escapeAttr(value.value)}"`);
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (isJSXExpressionContainer(value)) {
|
|
775
|
+
const expr = value.expression;
|
|
776
|
+
if (attrName === "class" && expr.type === "TemplateLiteral") {
|
|
777
|
+
const quasis = expr.quasis;
|
|
778
|
+
const staticParts = quasis.map((q) => {
|
|
779
|
+
const val = q.value;
|
|
780
|
+
return (val?.raw ?? "").trim();
|
|
781
|
+
}).filter((s) => s.length > 0).join(" ");
|
|
782
|
+
if (staticParts) {
|
|
783
|
+
result.push(`${attrName}="${escapeAttr(staticParts)}"`);
|
|
784
|
+
}
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
const liquidExpr = exprToLiquid(expr, ctx);
|
|
788
|
+
if (liquidExpr) {
|
|
789
|
+
result.push(`${attrName}="${sanitiseLiquidForAttr(liquidExpr)}"`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return result;
|
|
794
|
+
}
|
|
795
|
+
function renderExpression(expr, ctx, depth) {
|
|
796
|
+
if (isFunctionLike(expr)) {
|
|
797
|
+
const body = extractFunctionBody(expr);
|
|
798
|
+
if (body) return renderNode(body, ctx, depth);
|
|
799
|
+
return "";
|
|
800
|
+
}
|
|
801
|
+
const liquid = exprToLiquid(expr, ctx);
|
|
802
|
+
return liquid ?? "";
|
|
803
|
+
}
|
|
804
|
+
function exprToLiquid(expr, ctx) {
|
|
805
|
+
if (isTapCall(expr)) {
|
|
806
|
+
const liquidStr = expr.arguments[0].value;
|
|
807
|
+
return ensureLiquidOutput(liquidStr);
|
|
808
|
+
}
|
|
809
|
+
if (isIdentifier(expr) && ctx.loopVars.has(expr.name)) {
|
|
810
|
+
return `{{ ${expr.name} }}`;
|
|
811
|
+
}
|
|
812
|
+
if (isIdentifier(expr) && ctx.mappings[expr.name]) {
|
|
813
|
+
return ensureLiquidOutput(ctx.mappings[expr.name]);
|
|
814
|
+
}
|
|
815
|
+
if (isMemberExpression(expr) && isIdentifier(expr.object) && expr.object.name === "props" && !expr.computed && isIdentifier(expr.property) && expr.property.name === "children") {
|
|
816
|
+
return "{{ content_for_layout }}";
|
|
817
|
+
}
|
|
818
|
+
const memberPath = resolveMemberPath(expr, ctx.mappings, ctx.loopVars);
|
|
819
|
+
if (memberPath) return `{{ ${memberPath} }}`;
|
|
820
|
+
if (isCallExpression(expr) && isIdentifier(expr.callee) && expr.callee.name === "liquidRaw" && expr.arguments.length >= 1) {
|
|
821
|
+
const arg = expr.arguments[0];
|
|
822
|
+
if (isStringLiteral(arg)) {
|
|
823
|
+
return arg.value;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (isCallExpression(expr) && isIdentifier(expr.callee) && expr.callee.name === "t" && expr.arguments.length >= 1) {
|
|
827
|
+
const keyArg = expr.arguments[0];
|
|
828
|
+
if (isStringLiteral(keyArg)) {
|
|
829
|
+
return `{{ '${keyArg.value}' | t }}`;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (isCallExpression(expr) && isIdentifier(expr.callee) && expr.callee.name === "filter" && expr.arguments.length >= 2) {
|
|
833
|
+
const innerExpr = expr.arguments[0];
|
|
834
|
+
const filterNameArg = expr.arguments[1];
|
|
835
|
+
if (isStringLiteral(filterNameArg)) {
|
|
836
|
+
const innerLiquid = exprToLiquid(innerExpr, ctx);
|
|
837
|
+
if (innerLiquid) {
|
|
838
|
+
const barePath = stripLiquidBraces(innerLiquid);
|
|
839
|
+
const filterName = filterNameArg.value;
|
|
840
|
+
let argStr = "";
|
|
841
|
+
if (expr.arguments.length >= 3) {
|
|
842
|
+
const argsObj = expr.arguments[2];
|
|
843
|
+
if (argsObj.type === "ObjectExpression" && Array.isArray(argsObj.properties)) {
|
|
844
|
+
const pairs = argsObj.properties.filter((p) => p.type === "ObjectProperty" || p.type === "Property").map((p) => {
|
|
845
|
+
const key = isIdentifier(p.key) ? p.key.name : isStringLiteral(p.key) ? p.key.value : null;
|
|
846
|
+
if (!key) return null;
|
|
847
|
+
const val = p.value;
|
|
848
|
+
if (val.type === "Literal" && typeof val.value === "number") {
|
|
849
|
+
return `${key}: ${val.value}`;
|
|
850
|
+
}
|
|
851
|
+
if (isStringLiteral(val)) {
|
|
852
|
+
return `${key}: '${val.value}'`;
|
|
853
|
+
}
|
|
854
|
+
return null;
|
|
855
|
+
}).filter(Boolean);
|
|
856
|
+
if (pairs.length > 0) {
|
|
857
|
+
argStr = ": " + pairs.join(", ");
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return `{{ ${barePath} | ${filterName}${argStr} }}`;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
function extractFunctionBody(fnNode) {
|
|
868
|
+
const body = fnNode.body;
|
|
869
|
+
if (!body) return null;
|
|
870
|
+
if (isJSXElement(body) || isJSXFragment(body)) return body;
|
|
871
|
+
if (body.type === "ParenthesizedExpression" && body.expression) {
|
|
872
|
+
const inner = body.expression;
|
|
873
|
+
if (isJSXElement(inner) || isJSXFragment(inner)) return inner;
|
|
874
|
+
}
|
|
875
|
+
if (body.type === "BlockStatement" && Array.isArray(body.body)) {
|
|
876
|
+
for (const stmt of body.body) {
|
|
877
|
+
if (isReturnStatement(stmt) && stmt.argument) {
|
|
878
|
+
let arg = stmt.argument;
|
|
879
|
+
if (arg.type === "ParenthesizedExpression" && arg.expression) {
|
|
880
|
+
arg = arg.expression;
|
|
881
|
+
}
|
|
882
|
+
if (isJSXElement(arg) || isJSXFragment(arg)) return arg;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
function findJSXAttr(attrs, name) {
|
|
889
|
+
return attrs.find(
|
|
890
|
+
(a) => isJSXAttribute(a) && isJSXIdentifier(a.name) && a.name.name === name
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
function getJSXAttrExpr(attr) {
|
|
894
|
+
const value = attr.value;
|
|
895
|
+
if (!value) return null;
|
|
896
|
+
if (isJSXExpressionContainer(value)) return value.expression;
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
function ensureLiquidOutput(liquidStr) {
|
|
900
|
+
const trimmed = liquidStr.trim();
|
|
901
|
+
if (trimmed.startsWith("{{") || trimmed.startsWith("{%")) return trimmed;
|
|
902
|
+
return `{{ ${trimmed} }}`;
|
|
903
|
+
}
|
|
904
|
+
function sanitiseLiquidForAttr(liquidExpr) {
|
|
905
|
+
return liquidExpr.replace(/\{\{([^}]*)\}\}/g, (_, inner) => {
|
|
906
|
+
return `{{ ${inner.trim().replace(/"/g, "'")} }}`;
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
function renderChildren(children, ctx, depth) {
|
|
910
|
+
return children.map((child) => renderNode(child, ctx, depth)).filter((s) => s.trim().length > 0).join("\n");
|
|
911
|
+
}
|
|
912
|
+
function renderJSXText(raw) {
|
|
913
|
+
return raw.replace(/\s+/g, " ").trim();
|
|
914
|
+
}
|
|
915
|
+
function escapeAttr(value) {
|
|
916
|
+
return value.replace(/"/g, """);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/route-map.ts
|
|
920
|
+
import path from "path";
|
|
921
|
+
function resolveRoute(filePath, routesDir) {
|
|
922
|
+
const rel = path.relative(routesDir, filePath).replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
923
|
+
const normalized = rel.replace(/\\/g, "/");
|
|
924
|
+
if (/^\+layout$/.test(normalized)) {
|
|
925
|
+
return {
|
|
926
|
+
template: "_layout",
|
|
927
|
+
outputPath: "layout/theme.liquid",
|
|
928
|
+
context: ["shop", "cart", "request"],
|
|
929
|
+
isLayout: true
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
934
|
+
function isRouteFile(filePath, projectRoot) {
|
|
935
|
+
const routesDir = getRoutesDir(projectRoot);
|
|
936
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
937
|
+
const normalizedRoutesDir = routesDir.replace(/\\/g, "/");
|
|
938
|
+
return normalized.startsWith(normalizedRoutesDir + "/");
|
|
939
|
+
}
|
|
940
|
+
function getRoutesDir(projectRoot) {
|
|
941
|
+
return path.join(projectRoot, "src", "routes");
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/section-schema.ts
|
|
945
|
+
import { parseSync as parseSync3 } from "oxc-parser";
|
|
946
|
+
function extractSectionSchema(source) {
|
|
947
|
+
const { program, errors } = parseSync3("__schema__.tsx", source);
|
|
948
|
+
if (errors.length > 0) {
|
|
949
|
+
const msgs = errors.map((e) => e.message).join(", ");
|
|
950
|
+
throw new Error(`Parse errors while extracting schema: ${msgs}`);
|
|
951
|
+
}
|
|
952
|
+
let schemaValue = null;
|
|
953
|
+
walkAst(program, (node) => {
|
|
954
|
+
if (schemaValue) return;
|
|
955
|
+
if (node.type !== "ExportNamedDeclaration") return;
|
|
956
|
+
const decl = node.declaration;
|
|
957
|
+
if (!decl || decl.type !== "VariableDeclaration") return;
|
|
958
|
+
const declarations = decl.declarations;
|
|
959
|
+
for (const declarator of declarations) {
|
|
960
|
+
if (declarator.type !== "VariableDeclarator") continue;
|
|
961
|
+
const id = declarator.id;
|
|
962
|
+
if (id.type !== "Identifier" || id.name !== "schema") continue;
|
|
963
|
+
const init = declarator.init;
|
|
964
|
+
if (!init) continue;
|
|
965
|
+
const unwrapped = unwrapTypeAssertions(init);
|
|
966
|
+
if (unwrapped.type !== "ObjectExpression") continue;
|
|
967
|
+
schemaValue = unwrapped;
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
if (!schemaValue) return null;
|
|
972
|
+
return evaluateAstValue(schemaValue);
|
|
973
|
+
}
|
|
974
|
+
function formatSchemaTag(schemaObj) {
|
|
975
|
+
return `{% schema %}
|
|
976
|
+
${JSON.stringify(schemaObj, null, 2)}
|
|
977
|
+
{% endschema %}
|
|
978
|
+
`;
|
|
979
|
+
}
|
|
980
|
+
function evaluateAstValue(node) {
|
|
981
|
+
switch (node.type) {
|
|
982
|
+
// Literals (oxc native + ESTree compat)
|
|
983
|
+
case "Literal":
|
|
984
|
+
case "StringLiteral":
|
|
985
|
+
case "NumericLiteral":
|
|
986
|
+
case "BooleanLiteral": {
|
|
987
|
+
return node.value;
|
|
988
|
+
}
|
|
989
|
+
case "NullLiteral": {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
case "ObjectExpression": {
|
|
993
|
+
const result = {};
|
|
994
|
+
for (const prop of node.properties) {
|
|
995
|
+
if (prop.type !== "Property" && prop.type !== "ObjectProperty") {
|
|
996
|
+
throw new Error(
|
|
997
|
+
`Unsupported property type in schema object: ${prop.type}. Only plain properties are allowed.`
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
const key = resolvePropertyKey(prop.key);
|
|
1001
|
+
result[key] = evaluateAstValue(prop.value);
|
|
1002
|
+
}
|
|
1003
|
+
return result;
|
|
1004
|
+
}
|
|
1005
|
+
case "ArrayExpression": {
|
|
1006
|
+
const elements = node.elements;
|
|
1007
|
+
return elements.map((el) => {
|
|
1008
|
+
if (!el) return null;
|
|
1009
|
+
return evaluateAstValue(el);
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
case "UnaryExpression": {
|
|
1013
|
+
if (node.operator === "-") {
|
|
1014
|
+
const inner = evaluateAstValue(node.argument);
|
|
1015
|
+
if (typeof inner !== "number") {
|
|
1016
|
+
throw new Error(
|
|
1017
|
+
`UnaryExpression '-' applied to non-numeric value in schema.`
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
return -inner;
|
|
1021
|
+
}
|
|
1022
|
+
throw new Error(
|
|
1023
|
+
`Unsupported UnaryExpression operator '${node.operator}' in schema. Only '-' (negation) is supported.`
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
case "TemplateLiteral": {
|
|
1027
|
+
const quasis = node.quasis;
|
|
1028
|
+
const exprs = node.expressions;
|
|
1029
|
+
if (exprs.length > 0) {
|
|
1030
|
+
throw new Error(
|
|
1031
|
+
`TemplateLiteral with expressions is not supported in schema values. Use plain string literals instead.`
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
1034
|
+
return quasis.map((q) => q.value?.cooked ?? "").join("");
|
|
1035
|
+
}
|
|
1036
|
+
// TSAsExpression / TSSatisfiesExpression / TSNonNullExpression can appear
|
|
1037
|
+
// when the initialiser is `{ ... } as const satisfies SomeType`. Strip them.
|
|
1038
|
+
case "TSAsExpression":
|
|
1039
|
+
case "TSSatisfiesExpression":
|
|
1040
|
+
case "TSNonNullExpression": {
|
|
1041
|
+
return evaluateAstValue(unwrapTypeAssertions(node));
|
|
1042
|
+
}
|
|
1043
|
+
default: {
|
|
1044
|
+
throw new Error(
|
|
1045
|
+
`Unsupported AST node type '${node.type}' in schema value. Schema must contain only literals, objects, and arrays.`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function resolvePropertyKey(keyNode) {
|
|
1051
|
+
if (keyNode.type === "Identifier") {
|
|
1052
|
+
return keyNode.name;
|
|
1053
|
+
}
|
|
1054
|
+
if (keyNode.type === "Literal" || keyNode.type === "StringLiteral" || keyNode.type === "NumericLiteral") {
|
|
1055
|
+
return String(keyNode.value);
|
|
1056
|
+
}
|
|
1057
|
+
throw new Error(
|
|
1058
|
+
`Unsupported property key type '${keyNode.type}' in schema object.`
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/hydration.ts
|
|
1063
|
+
import { parseSync as parseSync4 } from "oxc-parser";
|
|
1064
|
+
function isInteractiveComponent(source) {
|
|
1065
|
+
if (/\bcreateSignal\b|\bcreateEffect\b/.test(source)) return true;
|
|
1066
|
+
if (/\bon[A-Z][a-zA-Z]*\s*=\s*\{/.test(source)) return true;
|
|
1067
|
+
if (/\btapWhen\b/.test(source)) return true;
|
|
1068
|
+
if (/\btapRemote\b/.test(source)) return true;
|
|
1069
|
+
if (/\btapPersonalized\b/.test(source)) return true;
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
function detectPropVars(source, mappings) {
|
|
1073
|
+
const { program, errors } = parseSync4("component.tsx", source);
|
|
1074
|
+
if (errors.length > 0) return [];
|
|
1075
|
+
const handlerExprs = [];
|
|
1076
|
+
walkAst(program, (node) => {
|
|
1077
|
+
if (!isJSXAttribute(node)) return;
|
|
1078
|
+
const nameNode = node.name;
|
|
1079
|
+
if (!isJSXIdentifier(nameNode)) return;
|
|
1080
|
+
const attrName = nameNode.name;
|
|
1081
|
+
if (attrName.length < 3) return;
|
|
1082
|
+
if (!attrName.startsWith("on")) return;
|
|
1083
|
+
if (attrName[2] !== attrName[2].toUpperCase()) return;
|
|
1084
|
+
const value = node.value;
|
|
1085
|
+
if (value && isJSXExpressionContainer(value)) {
|
|
1086
|
+
handlerExprs.push(value.expression);
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
if (handlerExprs.length === 0) return [];
|
|
1090
|
+
const namedFuncNodes = /* @__PURE__ */ new Map();
|
|
1091
|
+
walkAst(program, (node) => {
|
|
1092
|
+
if (node.type === "FunctionDeclaration" && node.id) {
|
|
1093
|
+
const name = node.id.name;
|
|
1094
|
+
namedFuncNodes.set(name, node);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (node.type === "VariableDeclarator" && node.id && isIdentifier(node.id) && node.init && isFunctionLike(node.init)) {
|
|
1098
|
+
const name = node.id.name;
|
|
1099
|
+
namedFuncNodes.set(name, node.init);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
const nodesToSearch = [];
|
|
1103
|
+
for (const expr of handlerExprs) {
|
|
1104
|
+
if (isIdentifier(expr)) {
|
|
1105
|
+
const funcNode = namedFuncNodes.get(expr.name);
|
|
1106
|
+
if (funcNode) nodesToSearch.push(funcNode);
|
|
1107
|
+
} else if (isFunctionLike(expr)) {
|
|
1108
|
+
nodesToSearch.push(expr);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const result = /* @__PURE__ */ new Set();
|
|
1112
|
+
for (const node of nodesToSearch) {
|
|
1113
|
+
walkAst(node, (n) => {
|
|
1114
|
+
if (isIdentifier(n)) {
|
|
1115
|
+
const name = n.name;
|
|
1116
|
+
if (mappings[name]) result.add(name);
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
return [...result];
|
|
1121
|
+
}
|
|
1122
|
+
function generateDataProps(propVars, mappings) {
|
|
1123
|
+
if (propVars.length === 0) return "{}";
|
|
1124
|
+
const entries = propVars.map((varName) => {
|
|
1125
|
+
const liquidExpr = mappings[varName];
|
|
1126
|
+
const bare = stripLiquidBraces(liquidExpr);
|
|
1127
|
+
return `"${varName}": ${serializeLiquidValue(bare)}`;
|
|
1128
|
+
});
|
|
1129
|
+
return `{ ${entries.join(", ")} }`;
|
|
1130
|
+
}
|
|
1131
|
+
function serializeLiquidValue(bare) {
|
|
1132
|
+
if (/^linklists\.\S+\.links$/.test(bare)) {
|
|
1133
|
+
return [
|
|
1134
|
+
`[{% for _l in ${bare} %}`,
|
|
1135
|
+
`{% unless forloop.first %},{% endunless %}`,
|
|
1136
|
+
`{"title":{{ _l.title | json }},`,
|
|
1137
|
+
`"url":{{ _l.url | json }},`,
|
|
1138
|
+
`"links":[{% for _c in _l.links %}`,
|
|
1139
|
+
`{% unless forloop.first %},{% endunless %}`,
|
|
1140
|
+
`{"title":{{ _c.title | json }},"url":{{ _c.url | json }}}`,
|
|
1141
|
+
`{% endfor %}]}`,
|
|
1142
|
+
`{% endfor %}]`
|
|
1143
|
+
].join("");
|
|
1144
|
+
}
|
|
1145
|
+
return `{{ ${bare} | json }}`;
|
|
1146
|
+
}
|
|
1147
|
+
function generateDataSection(propVars, mappings) {
|
|
1148
|
+
const entries = propVars.map((varName) => {
|
|
1149
|
+
const liquidExpr = mappings[varName];
|
|
1150
|
+
const bare = stripLiquidBraces(liquidExpr);
|
|
1151
|
+
return ` "${varName}": ${serializeLiquidValue(bare)}`;
|
|
1152
|
+
});
|
|
1153
|
+
return [
|
|
1154
|
+
'<script type="application/json">',
|
|
1155
|
+
"{",
|
|
1156
|
+
entries.join(",\n"),
|
|
1157
|
+
"}",
|
|
1158
|
+
"</script>",
|
|
1159
|
+
""
|
|
1160
|
+
].join("\n");
|
|
1161
|
+
}
|
|
1162
|
+
function generateHydrationEntry(components) {
|
|
1163
|
+
if (components.length === 0) {
|
|
1164
|
+
return "// No interactive components.\n";
|
|
1165
|
+
}
|
|
1166
|
+
const registryEntries = components.map((c) => ` '${c.name}': () => import('${c.importPath}'),`).join("\n");
|
|
1167
|
+
return [
|
|
1168
|
+
`// Generated by Semi-Solid \u2014 do not edit.`,
|
|
1169
|
+
`import { render } from 'solid-js/web';`,
|
|
1170
|
+
``,
|
|
1171
|
+
`const registry = {`,
|
|
1172
|
+
registryEntries,
|
|
1173
|
+
`};`,
|
|
1174
|
+
``,
|
|
1175
|
+
`document.querySelectorAll('[data-component]').forEach(async (el) => {`,
|
|
1176
|
+
` const name = el.getAttribute('data-component');`,
|
|
1177
|
+
` if (!registry[name]) return;`,
|
|
1178
|
+
` const props = JSON.parse(el.getAttribute('data-props') || '{}');`,
|
|
1179
|
+
` const { default: Component } = await registry[name]();`,
|
|
1180
|
+
` render(() => Component(props), el);`,
|
|
1181
|
+
`});`,
|
|
1182
|
+
``
|
|
1183
|
+
].join("\n");
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/validation.ts
|
|
1187
|
+
var GLOBAL_LIQUID_OBJECTS = /* @__PURE__ */ new Set([
|
|
1188
|
+
"shop",
|
|
1189
|
+
"settings",
|
|
1190
|
+
"request",
|
|
1191
|
+
"routes",
|
|
1192
|
+
"linklists",
|
|
1193
|
+
"content_for_header",
|
|
1194
|
+
"content_for_layout",
|
|
1195
|
+
"content_for_index",
|
|
1196
|
+
"pages",
|
|
1197
|
+
"blogs",
|
|
1198
|
+
"collections",
|
|
1199
|
+
"all_products",
|
|
1200
|
+
"customer",
|
|
1201
|
+
"localization",
|
|
1202
|
+
"predictive_search",
|
|
1203
|
+
"recommendations",
|
|
1204
|
+
"powered_by_link",
|
|
1205
|
+
"canonical_url",
|
|
1206
|
+
"section",
|
|
1207
|
+
// available in section files (section.settings.*, section.blocks)
|
|
1208
|
+
"block"
|
|
1209
|
+
// available inside {% for block in section.blocks %}
|
|
1210
|
+
]);
|
|
1211
|
+
function extractLiquidObjects(liquidExpr) {
|
|
1212
|
+
const stripped = stripLiquidBraces2(liquidExpr);
|
|
1213
|
+
if (!stripped) return [];
|
|
1214
|
+
const baseExpr = stripped.split("|")[0].trim();
|
|
1215
|
+
if (baseExpr.startsWith("'") || baseExpr.startsWith('"')) return [];
|
|
1216
|
+
const match = baseExpr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)/);
|
|
1217
|
+
if (!match) return [];
|
|
1218
|
+
return [match[1]];
|
|
1219
|
+
}
|
|
1220
|
+
function validateTapMappings(mappings, routeContext) {
|
|
1221
|
+
const available = /* @__PURE__ */ new Set([...routeContext, ...GLOBAL_LIQUID_OBJECTS]);
|
|
1222
|
+
const warnings = [];
|
|
1223
|
+
for (const [variable, liquidExpr] of Object.entries(mappings)) {
|
|
1224
|
+
const objects = extractLiquidObjects(liquidExpr);
|
|
1225
|
+
for (const obj of objects) {
|
|
1226
|
+
if (!available.has(obj)) {
|
|
1227
|
+
const contextList = routeContext.length > 0 ? routeContext.join(", ") : "(none)";
|
|
1228
|
+
warnings.push({
|
|
1229
|
+
type: "context_mismatch",
|
|
1230
|
+
message: `Variable '${variable}' uses '{{ ${obj}.* }}' which is not available in this route's Liquid context. Route context: [${contextList}]. Check that the tap() expression matches the Liquid objects for this template.`,
|
|
1231
|
+
variable,
|
|
1232
|
+
liquidExpr
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return warnings;
|
|
1238
|
+
}
|
|
1239
|
+
function validateUnusedMappings(mappings, liquidOutput) {
|
|
1240
|
+
const warnings = [];
|
|
1241
|
+
for (const [variable, liquidExpr] of Object.entries(mappings)) {
|
|
1242
|
+
if (variable.startsWith("__tap_inline_")) continue;
|
|
1243
|
+
const stripped = stripLiquidBraces2(liquidExpr);
|
|
1244
|
+
const baseExpr = stripped.split("|")[0].trim();
|
|
1245
|
+
if (baseExpr.startsWith("'") || baseExpr.startsWith('"')) continue;
|
|
1246
|
+
if (baseExpr && !liquidOutput.includes(baseExpr)) {
|
|
1247
|
+
warnings.push({
|
|
1248
|
+
type: "unused_mapping",
|
|
1249
|
+
message: `Variable '${variable}' was extracted from tap() but '${baseExpr}' does not appear in the generated Liquid output. It may be used only client-side (event handlers, reactive computations), which is fine \u2014 this is informational.`,
|
|
1250
|
+
variable,
|
|
1251
|
+
liquidExpr
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return warnings;
|
|
1256
|
+
}
|
|
1257
|
+
function generateManifest(brand, locale, files) {
|
|
1258
|
+
return {
|
|
1259
|
+
brand,
|
|
1260
|
+
locale,
|
|
1261
|
+
templates: [...files.templates].sort(),
|
|
1262
|
+
snippets: [...files.snippets].sort(),
|
|
1263
|
+
sections: [...files.sections ?? []].sort(),
|
|
1264
|
+
assets: [...files.assets].sort(),
|
|
1265
|
+
locales: [...files.locales].sort()
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
function stripLiquidBraces2(s) {
|
|
1269
|
+
const trimmed = s.trim();
|
|
1270
|
+
if (trimmed.startsWith("{{") && trimmed.endsWith("}}")) {
|
|
1271
|
+
return trimmed.slice(2, -2).trim();
|
|
1272
|
+
}
|
|
1273
|
+
return trimmed;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// src/css.ts
|
|
1277
|
+
import fs from "fs";
|
|
1278
|
+
import path2 from "path";
|
|
1279
|
+
function resolveCSSFiles(brand, projectRoot, existsSync = fs.existsSync) {
|
|
1280
|
+
const candidates = [
|
|
1281
|
+
{
|
|
1282
|
+
rel: path2.join("src", "brands", brand, "theme.css"),
|
|
1283
|
+
assetName: "theme.css"
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
rel: path2.join("src", "styles", "global.css"),
|
|
1287
|
+
assetName: "global.css"
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
rel: path2.join("src", "index.css"),
|
|
1291
|
+
assetName: "index.css"
|
|
1292
|
+
}
|
|
1293
|
+
];
|
|
1294
|
+
return candidates.map(({ rel, assetName }) => ({
|
|
1295
|
+
src: path2.join(projectRoot, rel),
|
|
1296
|
+
assetName
|
|
1297
|
+
})).filter(({ src }) => existsSync(src));
|
|
1298
|
+
}
|
|
1299
|
+
function generateStylesheetTag(assetName) {
|
|
1300
|
+
return `<link rel="stylesheet" href="{{ '${assetName}' | asset_url }}" media="all">`;
|
|
1301
|
+
}
|
|
1302
|
+
function generateScriptTag(assetName) {
|
|
1303
|
+
return `<script src="{{ '${assetName}' | asset_url }}" type="module"></script>`;
|
|
1304
|
+
}
|
|
1305
|
+
function generatePreconnectTag(baseUrl) {
|
|
1306
|
+
try {
|
|
1307
|
+
const origin = new URL(baseUrl).origin;
|
|
1308
|
+
return `<link rel="preconnect" href="${origin}" crossorigin>`;
|
|
1309
|
+
} catch {
|
|
1310
|
+
return `<link rel="preconnect" href="${baseUrl}" crossorigin>`;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
function generatePrefetchScript(calls, baseUrl) {
|
|
1314
|
+
if (calls.length === 0) return "";
|
|
1315
|
+
const iifes = [];
|
|
1316
|
+
for (const call of calls) {
|
|
1317
|
+
let fullUrl;
|
|
1318
|
+
if (/^https?:\/\//.test(call.url)) {
|
|
1319
|
+
fullUrl = call.url;
|
|
1320
|
+
} else {
|
|
1321
|
+
fullUrl = baseUrl.replace(/\/$/, "") + "/" + call.url.replace(/^\//, "");
|
|
1322
|
+
}
|
|
1323
|
+
const sortedKeys = Object.keys(call.params).sort();
|
|
1324
|
+
if (sortedKeys.length > 0) {
|
|
1325
|
+
const qs = sortedKeys.map((key) => {
|
|
1326
|
+
const tapVarName = call.params[key];
|
|
1327
|
+
const liquidExpr = call.componentMappings[tapVarName];
|
|
1328
|
+
if (!liquidExpr) {
|
|
1329
|
+
return `${encodeURIComponent(key)}=`;
|
|
1330
|
+
}
|
|
1331
|
+
const bare = liquidExpr.replace(/^\{\{\s*/, "").replace(/\s*\}\}$/, "");
|
|
1332
|
+
return `${encodeURIComponent(key)}={{ ${bare} | url_encode }}`;
|
|
1333
|
+
}).join("&");
|
|
1334
|
+
fullUrl += `?${qs}`;
|
|
1335
|
+
}
|
|
1336
|
+
iifes.push([
|
|
1337
|
+
`(function() {`,
|
|
1338
|
+
` var u = "${fullUrl}";`,
|
|
1339
|
+
` window.__p[u] = fetch(u).then(function(r) { return r.json(); });`,
|
|
1340
|
+
`})();`
|
|
1341
|
+
].join("\n"));
|
|
1342
|
+
}
|
|
1343
|
+
return [
|
|
1344
|
+
"<script>",
|
|
1345
|
+
"window.__p = window.__p || {};",
|
|
1346
|
+
...iifes,
|
|
1347
|
+
"</script>"
|
|
1348
|
+
].join("\n");
|
|
1349
|
+
}
|
|
1350
|
+
function generateAssetIncludes(cssAssets, jsAssets, personalization) {
|
|
1351
|
+
const lines = [];
|
|
1352
|
+
if (personalization?.preconnect && personalization.baseUrl) {
|
|
1353
|
+
lines.push(generatePreconnectTag(personalization.baseUrl));
|
|
1354
|
+
}
|
|
1355
|
+
lines.push(...cssAssets.map(generateStylesheetTag));
|
|
1356
|
+
lines.push(...jsAssets.map(generateScriptTag));
|
|
1357
|
+
if (personalization?.prefetch && personalization.calls.length > 0) {
|
|
1358
|
+
lines.push(generatePrefetchScript(personalization.calls, personalization.baseUrl));
|
|
1359
|
+
}
|
|
1360
|
+
return lines.join("\n");
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// src/plugin.ts
|
|
1364
|
+
var REQUIRED_THEME_FILES = {
|
|
1365
|
+
// layout/theme.liquid is generated from src/routes/+layout.tsx.
|
|
1366
|
+
// This minimal fallback ensures Shopify CLI never tries to delete the protected
|
|
1367
|
+
// remote file when a build error prevents generation of the real layout.
|
|
1368
|
+
"layout/theme.liquid": [
|
|
1369
|
+
`<!DOCTYPE html>`,
|
|
1370
|
+
`<html lang="{{ request.locale.iso_code }}">`,
|
|
1371
|
+
`<head>`,
|
|
1372
|
+
` <meta charset="utf-8">`,
|
|
1373
|
+
` {{ content_for_header }}`,
|
|
1374
|
+
`</head>`,
|
|
1375
|
+
`<body>`,
|
|
1376
|
+
` {{ content_for_layout }}`,
|
|
1377
|
+
`</body>`,
|
|
1378
|
+
`</html>`,
|
|
1379
|
+
``
|
|
1380
|
+
].join("\n"),
|
|
1381
|
+
// Every Shopify theme must have a gift card template.
|
|
1382
|
+
"templates/gift_card.liquid": [
|
|
1383
|
+
`<!DOCTYPE html>`,
|
|
1384
|
+
`<html lang="{{ request.locale.iso_code }}">`,
|
|
1385
|
+
`<head>`,
|
|
1386
|
+
` <meta charset="utf-8">`,
|
|
1387
|
+
` <meta name="viewport" content="width=device-width, initial-scale=1">`,
|
|
1388
|
+
` <title>{{ 'gift_cards.issued.title' | t }} \u2014 {{ shop.name }}</title>`,
|
|
1389
|
+
` {{ content_for_header }}`,
|
|
1390
|
+
` {% render 'theme-assets' %}`,
|
|
1391
|
+
`</head>`,
|
|
1392
|
+
`<body class="gift-card-page">`,
|
|
1393
|
+
` <header class="text-center py-8">`,
|
|
1394
|
+
` <a href="{{ shop.url }}">`,
|
|
1395
|
+
` <h1>{{ shop.name }}</h1>`,
|
|
1396
|
+
` </a>`,
|
|
1397
|
+
` </header>`,
|
|
1398
|
+
` <main class="gift-card max-w-md mx-auto px-4 text-center">`,
|
|
1399
|
+
` <h2>{{ 'gift_cards.issued.subtext' | t }}</h2>`,
|
|
1400
|
+
` {% if gift_card.enabled %}`,
|
|
1401
|
+
` <p class="gift-card__amount text-4xl font-bold my-4">{{ gift_card.initial_value | money }}</p>`,
|
|
1402
|
+
` {% if gift_card.balance != gift_card.initial_value %}`,
|
|
1403
|
+
` <p>{{ 'gift_cards.issued.remaining_html' | t: balance: gift_card.balance | money }}</p>`,
|
|
1404
|
+
` {% endif %}`,
|
|
1405
|
+
` <div class="gift-card__code my-6">`,
|
|
1406
|
+
` <input type="text" value="{{ gift_card.code | format_code }}" class="text-center border rounded px-4 py-3 w-full text-lg tracking-widest" readonly onfocus="this.select();">`,
|
|
1407
|
+
` </div>`,
|
|
1408
|
+
` {% if gift_card.pass_url %}`,
|
|
1409
|
+
` <a href="{{ gift_card.pass_url }}" class="inline-block mb-4">`,
|
|
1410
|
+
` <img src="{{ 'gift-card/add-to-apple-wallet.svg' | shopify_asset_url }}" alt="{{ 'gift_cards.issued.add_to_apple_wallet' | t }}" width="120">`,
|
|
1411
|
+
` </a>`,
|
|
1412
|
+
` {% endif %}`,
|
|
1413
|
+
` <p class="text-sm text-gray-500">{{ 'gift_cards.issued.expiry_html' | t: expires: gift_card.expires_on | date: "%B %d, %Y" }}</p>`,
|
|
1414
|
+
` {% else %}`,
|
|
1415
|
+
` <p>{{ 'gift_cards.issued.disabled' | t }}</p>`,
|
|
1416
|
+
` {% endif %}`,
|
|
1417
|
+
` <a href="{{ shop.url }}" class="inline-block mt-8 bg-primary text-white px-8 py-3 rounded">{{ 'gift_cards.issued.shop_link' | t }}</a>`,
|
|
1418
|
+
` </main>`,
|
|
1419
|
+
`</body>`,
|
|
1420
|
+
`</html>`,
|
|
1421
|
+
``
|
|
1422
|
+
].join("\n"),
|
|
1423
|
+
// Theme settings schema — typography, layout, and colors.
|
|
1424
|
+
"config/settings_schema.json": JSON.stringify(
|
|
1425
|
+
[
|
|
1426
|
+
{
|
|
1427
|
+
name: "theme_info",
|
|
1428
|
+
theme_name: "Semi-Solid",
|
|
1429
|
+
theme_version: "1.0.0",
|
|
1430
|
+
theme_author: "Semi-Solid",
|
|
1431
|
+
theme_documentation_url: "https://github.com/CarlR100/semi-solid",
|
|
1432
|
+
theme_support_url: "https://github.com/CarlR100/semi-solid"
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
name: "Colors",
|
|
1436
|
+
settings: [
|
|
1437
|
+
{ type: "color", id: "color_primary", label: "Primary", default: "#111827" },
|
|
1438
|
+
{ type: "color", id: "color_secondary", label: "Secondary", default: "#6b7280" },
|
|
1439
|
+
{ type: "color", id: "color_background", label: "Background", default: "#ffffff" },
|
|
1440
|
+
{ type: "color", id: "color_text", label: "Text", default: "#111827" }
|
|
1441
|
+
]
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
name: "Typography",
|
|
1445
|
+
settings: [
|
|
1446
|
+
{ type: "font_picker", id: "heading_font", label: "Heading font", default: "assistant_n4" },
|
|
1447
|
+
{ type: "font_picker", id: "body_font", label: "Body font", default: "assistant_n4" }
|
|
1448
|
+
]
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
name: "Layout",
|
|
1452
|
+
settings: [
|
|
1453
|
+
{ type: "range", id: "page_width", label: "Page width", default: 1200, min: 1e3, max: 1600, step: 100, unit: "px" }
|
|
1454
|
+
]
|
|
1455
|
+
}
|
|
1456
|
+
],
|
|
1457
|
+
null,
|
|
1458
|
+
2
|
|
1459
|
+
) + "\n",
|
|
1460
|
+
// Theme settings data (JSONC comment header for clarity).
|
|
1461
|
+
"config/settings_data.json": [
|
|
1462
|
+
"// This file is auto-generated. Edit config/settings_schema.json for schema.",
|
|
1463
|
+
'{ "current": {} }',
|
|
1464
|
+
""
|
|
1465
|
+
].join("\n")
|
|
1466
|
+
};
|
|
1467
|
+
function writeIfChanged(filePath, content) {
|
|
1468
|
+
try {
|
|
1469
|
+
if (fs2.readFileSync(filePath, "utf-8") === content) return false;
|
|
1470
|
+
} catch {
|
|
1471
|
+
}
|
|
1472
|
+
fs2.writeFileSync(filePath, content, "utf-8");
|
|
1473
|
+
return true;
|
|
1474
|
+
}
|
|
1475
|
+
function semiSolidPlugin(options) {
|
|
1476
|
+
const { brand, locale } = options;
|
|
1477
|
+
const outDir = options.outDir ?? `dist/${brand}/${locale}`;
|
|
1478
|
+
let resolvedOutDir = outDir;
|
|
1479
|
+
let projectRoot = process.cwd();
|
|
1480
|
+
const interactiveComponents = [];
|
|
1481
|
+
const interactiveComponentPaths = /* @__PURE__ */ new Map();
|
|
1482
|
+
const componentCategories = /* @__PURE__ */ new Map();
|
|
1483
|
+
const generatedTemplates = [];
|
|
1484
|
+
const generatedSnippets = [];
|
|
1485
|
+
const generatedSections = [];
|
|
1486
|
+
const generatedAssets = [];
|
|
1487
|
+
const sectionComponentNames = /* @__PURE__ */ new Set();
|
|
1488
|
+
const remoteWrapperSections = /* @__PURE__ */ new Set();
|
|
1489
|
+
const personalizedCallsCollected = [];
|
|
1490
|
+
let emittedCSSAssets = [];
|
|
1491
|
+
return {
|
|
1492
|
+
name: "semi-solid",
|
|
1493
|
+
// Run before vite-plugin-solid so we see the original JSX source, not
|
|
1494
|
+
// the already-compiled Solid runtime output.
|
|
1495
|
+
enforce: "pre",
|
|
1496
|
+
configResolved(config) {
|
|
1497
|
+
projectRoot = config.root;
|
|
1498
|
+
resolvedOutDir = path3.isAbsolute(outDir) ? outDir : path3.resolve(projectRoot, outDir);
|
|
1499
|
+
},
|
|
1500
|
+
// -------------------------------------------------------------------------
|
|
1501
|
+
// Pre-scan component directories so the virtual hydration-entry load() hook
|
|
1502
|
+
// has the full interactive component list before any module is processed.
|
|
1503
|
+
// Must run in buildStart (not configResolved) because projectRoot is set in
|
|
1504
|
+
// configResolved and we need it resolved first.
|
|
1505
|
+
// -------------------------------------------------------------------------
|
|
1506
|
+
buildStart() {
|
|
1507
|
+
interactiveComponentPaths.clear();
|
|
1508
|
+
sectionComponentNames.clear();
|
|
1509
|
+
remoteWrapperSections.clear();
|
|
1510
|
+
generatedTemplates.length = 0;
|
|
1511
|
+
generatedSnippets.length = 0;
|
|
1512
|
+
generatedSections.length = 0;
|
|
1513
|
+
generatedAssets.length = 0;
|
|
1514
|
+
interactiveComponents.length = 0;
|
|
1515
|
+
personalizedCallsCollected.length = 0;
|
|
1516
|
+
componentCategories.clear();
|
|
1517
|
+
const categories = ["snippets", "sections", "blocks"];
|
|
1518
|
+
for (const category of categories) {
|
|
1519
|
+
const scanDirs = [
|
|
1520
|
+
path3.join(projectRoot, "src", category),
|
|
1521
|
+
path3.join(projectRoot, "src", "brands", brand, category)
|
|
1522
|
+
];
|
|
1523
|
+
for (const dir of scanDirs) {
|
|
1524
|
+
if (!fs2.existsSync(dir)) continue;
|
|
1525
|
+
for (const file of fs2.readdirSync(dir)) {
|
|
1526
|
+
if (!/\.(tsx|jsx)$/.test(file)) continue;
|
|
1527
|
+
const filePath = path3.join(dir, file);
|
|
1528
|
+
try {
|
|
1529
|
+
const source = fs2.readFileSync(filePath, "utf-8");
|
|
1530
|
+
const name = path3.basename(file).replace(/\.(tsx|jsx)$/, "");
|
|
1531
|
+
componentCategories.set(name, category);
|
|
1532
|
+
if (isInteractiveComponent(source)) {
|
|
1533
|
+
interactiveComponentPaths.set(name, filePath);
|
|
1534
|
+
}
|
|
1535
|
+
try {
|
|
1536
|
+
const schema = extractSectionSchema(source);
|
|
1537
|
+
if (schema !== null) {
|
|
1538
|
+
sectionComponentNames.add(name);
|
|
1539
|
+
}
|
|
1540
|
+
} catch {
|
|
1541
|
+
}
|
|
1542
|
+
} catch {
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
},
|
|
1548
|
+
// -------------------------------------------------------------------------
|
|
1549
|
+
// Phase 5: virtual:semi-solid/locale — inlines the active locale JSON
|
|
1550
|
+
// into the JS bundle so the runtime t() function can call setTranslations.
|
|
1551
|
+
//
|
|
1552
|
+
// Usage in entry modules:
|
|
1553
|
+
// import translations from 'virtual:semi-solid/locale';
|
|
1554
|
+
// import { setTranslations } from '@semi-solid/runtime';
|
|
1555
|
+
// setTranslations(translations);
|
|
1556
|
+
// -------------------------------------------------------------------------
|
|
1557
|
+
resolveId(id) {
|
|
1558
|
+
if (id === "virtual:semi-solid/hydration-entry") return "\0virtual:semi-solid/hydration-entry";
|
|
1559
|
+
if (id === virtualLocaleIds.external) return virtualLocaleIds.internal;
|
|
1560
|
+
return null;
|
|
1561
|
+
},
|
|
1562
|
+
load(id) {
|
|
1563
|
+
if (id === "\0virtual:semi-solid/hydration-entry") {
|
|
1564
|
+
const componentNames = [...interactiveComponentPaths.keys()];
|
|
1565
|
+
if (componentNames.length === 0) {
|
|
1566
|
+
return "// No interactive components\n";
|
|
1567
|
+
}
|
|
1568
|
+
const personalizationLines = [];
|
|
1569
|
+
if (options.personalization?.baseUrl) {
|
|
1570
|
+
personalizationLines.push(
|
|
1571
|
+
"import { __setPersonalizationBaseUrl } from '$lib/tapPersonalized';"
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
const lines = [
|
|
1575
|
+
"import { render } from 'solid-js/web';",
|
|
1576
|
+
"import { __setSectionId } from '$lib/tapWhen';",
|
|
1577
|
+
"import { setTranslations } from '$lib/i18n';",
|
|
1578
|
+
"import __translations from 'virtual:semi-solid/locale';",
|
|
1579
|
+
...personalizationLines,
|
|
1580
|
+
...componentNames.map((n) => {
|
|
1581
|
+
const category = componentCategories.get(n) ?? "snippets";
|
|
1582
|
+
return `import ${n} from '$${category}/${n}';`;
|
|
1583
|
+
}),
|
|
1584
|
+
"",
|
|
1585
|
+
// Initialize translations before any component mounts so t() calls
|
|
1586
|
+
// resolve to actual translated strings instead of raw key names.
|
|
1587
|
+
"setTranslations(__translations);",
|
|
1588
|
+
"",
|
|
1589
|
+
// Set personalization base URL before any component mounts
|
|
1590
|
+
...options.personalization?.baseUrl ? [`__setPersonalizationBaseUrl(${JSON.stringify(options.personalization.baseUrl)});`, ""] : [],
|
|
1591
|
+
"const registry = {",
|
|
1592
|
+
...componentNames.map((n) => ` '${n}': ${n},`),
|
|
1593
|
+
"};",
|
|
1594
|
+
"",
|
|
1595
|
+
"document.querySelectorAll('[data-component]').forEach((el) => {",
|
|
1596
|
+
" const name = el.getAttribute('data-component');",
|
|
1597
|
+
" const Component = registry[name];",
|
|
1598
|
+
" if (!Component) return;",
|
|
1599
|
+
" const props = JSON.parse(el.getAttribute('data-props') || '{}');",
|
|
1600
|
+
" // Clear Liquid SSR content before mounting \u2014 SolidJS render() appends",
|
|
1601
|
+
" // rather than replaces, so without this the SSR HTML stays alongside",
|
|
1602
|
+
" // the newly mounted component, producing duplicate content.",
|
|
1603
|
+
" el.textContent = '';",
|
|
1604
|
+
" // Set the active section ID before render() so createTapSignal() can",
|
|
1605
|
+
" // capture it synchronously at component initialisation time.",
|
|
1606
|
+
" __setSectionId(el.getAttribute('data-section-id'));",
|
|
1607
|
+
" render(() => Component(props), el);",
|
|
1608
|
+
" __setSectionId(undefined);",
|
|
1609
|
+
"});"
|
|
1610
|
+
];
|
|
1611
|
+
return lines.join("\n") + "\n";
|
|
1612
|
+
}
|
|
1613
|
+
if (id !== virtualLocaleIds.internal) return null;
|
|
1614
|
+
const localePath = resolveActiveLocalePath(brand, locale, projectRoot);
|
|
1615
|
+
if (localePath) {
|
|
1616
|
+
const json = fs2.readFileSync(localePath, "utf-8");
|
|
1617
|
+
return `export default ${json};`;
|
|
1618
|
+
}
|
|
1619
|
+
return "export default {};";
|
|
1620
|
+
},
|
|
1621
|
+
// -------------------------------------------------------------------------
|
|
1622
|
+
// Capture CSS assets emitted by Vite/Tailwind so buildEnd can reference
|
|
1623
|
+
// the correct hashed filenames in snippets/theme-assets.liquid.
|
|
1624
|
+
// -------------------------------------------------------------------------
|
|
1625
|
+
generateBundle(_outputOptions, bundle) {
|
|
1626
|
+
emittedCSSAssets = [];
|
|
1627
|
+
for (const [fileName, asset] of Object.entries(bundle)) {
|
|
1628
|
+
if (asset.type === "asset" && fileName.endsWith(".css")) {
|
|
1629
|
+
emittedCSSAssets.push(fileName);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
},
|
|
1633
|
+
// -------------------------------------------------------------------------
|
|
1634
|
+
// Phase 5: copy locale JSON files to dist/{brand}/{locale}/locales/
|
|
1635
|
+
// Phase 6: write assets/theme.entry.js for interactive components
|
|
1636
|
+
//
|
|
1637
|
+
// NOTE: Must use closeBundle (not buildEnd) because CSS assets are emitted
|
|
1638
|
+
// during the output generation phase (generateBundle), which runs AFTER
|
|
1639
|
+
// buildEnd. closeBundle fires after all output has been written, so
|
|
1640
|
+
// emittedCSSAssets is fully populated when we need it here.
|
|
1641
|
+
// -------------------------------------------------------------------------
|
|
1642
|
+
closeBundle() {
|
|
1643
|
+
const pairs = resolveLocaleFiles(brand, locale, projectRoot, resolvedOutDir);
|
|
1644
|
+
for (const { src, dest } of pairs) {
|
|
1645
|
+
try {
|
|
1646
|
+
fs2.mkdirSync(path3.dirname(dest), { recursive: true });
|
|
1647
|
+
const localeContent = fs2.readFileSync(src, "utf-8");
|
|
1648
|
+
if (writeIfChanged(dest, localeContent)) {
|
|
1649
|
+
this.info(
|
|
1650
|
+
`semi-solid: copied locale ${path3.basename(src)} \u2192 ${path3.relative(projectRoot, dest)}`
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
} catch (err) {
|
|
1654
|
+
this.warn(
|
|
1655
|
+
`semi-solid: failed to copy locale file ${src}: ${err.message}`
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
const cssAssetNames = [];
|
|
1660
|
+
for (const cssPath of emittedCSSAssets) {
|
|
1661
|
+
const baseName = path3.basename(cssPath);
|
|
1662
|
+
cssAssetNames.push(baseName);
|
|
1663
|
+
generatedAssets.push(cssPath);
|
|
1664
|
+
this.info(`semi-solid: CSS asset from Vite \u2192 ${cssPath}`);
|
|
1665
|
+
}
|
|
1666
|
+
if (interactiveComponentPaths.size > 0) {
|
|
1667
|
+
generatedAssets.push("assets/theme.entry.js");
|
|
1668
|
+
this.info(
|
|
1669
|
+
`semi-solid: theme.entry.js bundled by Vite (${interactiveComponentPaths.size} interactive component${interactiveComponentPaths.size === 1 ? "" : "s"})`
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
for (const sectionName of sectionComponentNames) {
|
|
1673
|
+
const kebab = toKebabCase(sectionName);
|
|
1674
|
+
const expectedFile = `sections/${kebab}.liquid`;
|
|
1675
|
+
if (generatedSections.includes(expectedFile)) continue;
|
|
1676
|
+
const brandPath = path3.join(projectRoot, "src", "brands", brand, "sections", `${sectionName}.tsx`);
|
|
1677
|
+
const basePath = path3.join(projectRoot, "src", "sections", `${sectionName}.tsx`);
|
|
1678
|
+
const srcPath = fs2.existsSync(brandPath) ? brandPath : basePath;
|
|
1679
|
+
if (!fs2.existsSync(srcPath)) {
|
|
1680
|
+
this.warn(`semi-solid: section source not found for ${sectionName}`);
|
|
1681
|
+
continue;
|
|
1682
|
+
}
|
|
1683
|
+
try {
|
|
1684
|
+
const source = fs2.readFileSync(srcPath, "utf-8");
|
|
1685
|
+
const { mappings } = extractTapMappings(source, srcPath);
|
|
1686
|
+
const liquidWarnings = [];
|
|
1687
|
+
let liquidContent = generateLiquid(source, mappings, {
|
|
1688
|
+
componentName: sectionName,
|
|
1689
|
+
sectionComponents: sectionComponentNames,
|
|
1690
|
+
warnings: liquidWarnings
|
|
1691
|
+
});
|
|
1692
|
+
for (const w of liquidWarnings) {
|
|
1693
|
+
this.warn(`semi-solid: ${sectionName}: ${w}`);
|
|
1694
|
+
}
|
|
1695
|
+
const schema = extractSectionSchema(source);
|
|
1696
|
+
if (schema !== null) {
|
|
1697
|
+
liquidContent += "\n" + formatSchemaTag(schema);
|
|
1698
|
+
}
|
|
1699
|
+
const sectionsDir = path3.join(resolvedOutDir, "sections");
|
|
1700
|
+
fs2.mkdirSync(sectionsDir, { recursive: true });
|
|
1701
|
+
writeIfChanged(path3.join(sectionsDir, `${kebab}.liquid`), liquidContent);
|
|
1702
|
+
generatedSections.push(expectedFile);
|
|
1703
|
+
this.info(`semi-solid: wrote ${expectedFile} (section, closeBundle fallback)`);
|
|
1704
|
+
} catch (err) {
|
|
1705
|
+
this.warn(
|
|
1706
|
+
`semi-solid: failed to compile section ${sectionName}: ${err.message}`
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
for (const [relPath, content] of Object.entries(REQUIRED_THEME_FILES)) {
|
|
1711
|
+
const fullPath = path3.join(resolvedOutDir, relPath);
|
|
1712
|
+
const isFallback = relPath.startsWith("layout/") || relPath.startsWith("templates/");
|
|
1713
|
+
if (isFallback && fs2.existsSync(fullPath)) continue;
|
|
1714
|
+
try {
|
|
1715
|
+
fs2.mkdirSync(path3.dirname(fullPath), { recursive: true });
|
|
1716
|
+
if (writeIfChanged(fullPath, content)) {
|
|
1717
|
+
this.info(`semi-solid: wrote required file ${relPath}`);
|
|
1718
|
+
}
|
|
1719
|
+
} catch (err) {
|
|
1720
|
+
this.warn(
|
|
1721
|
+
`semi-solid: failed to write ${relPath}: ${err.message}`
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
const jsAssetNames = interactiveComponentPaths.size > 0 ? ["theme.entry.js"] : [];
|
|
1726
|
+
let personalizationAssets;
|
|
1727
|
+
if (options.personalization?.baseUrl && personalizedCallsCollected.length > 0) {
|
|
1728
|
+
personalizationAssets = {
|
|
1729
|
+
baseUrl: options.personalization.baseUrl,
|
|
1730
|
+
preconnect: options.personalization.preconnect ?? true,
|
|
1731
|
+
prefetch: options.personalization.prefetch ?? true,
|
|
1732
|
+
calls: personalizedCallsCollected
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
const hasPersonalization = !!personalizationAssets;
|
|
1736
|
+
if (cssAssetNames.length > 0 || jsAssetNames.length > 0 || hasPersonalization) {
|
|
1737
|
+
const assetSnippet = generateAssetIncludes(cssAssetNames, jsAssetNames, personalizationAssets) + "\n";
|
|
1738
|
+
const snippetsDir = path3.join(resolvedOutDir, "snippets");
|
|
1739
|
+
try {
|
|
1740
|
+
fs2.mkdirSync(snippetsDir, { recursive: true });
|
|
1741
|
+
const snippetPath = path3.join(snippetsDir, "theme-assets.liquid");
|
|
1742
|
+
writeIfChanged(snippetPath, assetSnippet);
|
|
1743
|
+
generatedSnippets.push("snippets/theme-assets.liquid");
|
|
1744
|
+
this.info(`semi-solid: wrote snippets/theme-assets.liquid`);
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
this.warn(
|
|
1747
|
+
`semi-solid: failed to write theme-assets.liquid: ${err.message}`
|
|
1748
|
+
);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
const baseTemplatesDir = path3.join(projectRoot, "src", "templates");
|
|
1752
|
+
const brandTemplatesDir = path3.join(projectRoot, "src", "brands", brand, "templates");
|
|
1753
|
+
const templateFileMap = /* @__PURE__ */ new Map();
|
|
1754
|
+
for (const dir of [baseTemplatesDir, brandTemplatesDir]) {
|
|
1755
|
+
if (!fs2.existsSync(dir)) continue;
|
|
1756
|
+
for (const file of fs2.readdirSync(dir)) {
|
|
1757
|
+
if (!file.endsWith(".json")) continue;
|
|
1758
|
+
templateFileMap.set(file, path3.join(dir, file));
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
if (templateFileMap.size > 0) {
|
|
1762
|
+
const destDir = path3.join(resolvedOutDir, "templates");
|
|
1763
|
+
fs2.mkdirSync(destDir, { recursive: true });
|
|
1764
|
+
for (const [file, srcPath] of templateFileMap) {
|
|
1765
|
+
const content = fs2.readFileSync(srcPath, "utf-8");
|
|
1766
|
+
writeIfChanged(path3.join(destDir, file), content);
|
|
1767
|
+
generatedTemplates.push(`templates/${file}`);
|
|
1768
|
+
const isBrandOverride = srcPath.startsWith(brandTemplatesDir);
|
|
1769
|
+
this.info(`semi-solid: copied templates/${file}${isBrandOverride ? ` (brand: ${brand})` : ""}`);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
const baseThemeDir = path3.join(projectRoot, "src", "theme");
|
|
1773
|
+
const brandThemeDir = path3.join(projectRoot, "src", "brands", brand, "theme");
|
|
1774
|
+
for (const themeDir of [baseThemeDir, brandThemeDir]) {
|
|
1775
|
+
if (!fs2.existsSync(themeDir)) continue;
|
|
1776
|
+
const copyThemeFiles = (dir, relBase) => {
|
|
1777
|
+
for (const entry of fs2.readdirSync(dir, { withFileTypes: true })) {
|
|
1778
|
+
const srcPath = path3.join(dir, entry.name);
|
|
1779
|
+
const relPath = path3.join(relBase, entry.name);
|
|
1780
|
+
if (entry.isDirectory()) {
|
|
1781
|
+
copyThemeFiles(srcPath, relPath);
|
|
1782
|
+
} else {
|
|
1783
|
+
const destPath = path3.join(resolvedOutDir, relPath);
|
|
1784
|
+
if (fs2.existsSync(destPath)) continue;
|
|
1785
|
+
try {
|
|
1786
|
+
fs2.mkdirSync(path3.dirname(destPath), { recursive: true });
|
|
1787
|
+
const content = fs2.readFileSync(srcPath, "utf-8");
|
|
1788
|
+
writeIfChanged(destPath, content);
|
|
1789
|
+
if (relPath.startsWith("sections")) generatedSections.push(relPath);
|
|
1790
|
+
else if (relPath.startsWith("snippets")) generatedSnippets.push(relPath);
|
|
1791
|
+
else if (relPath.startsWith("templates")) generatedTemplates.push(relPath);
|
|
1792
|
+
this.info(`semi-solid: copied theme file ${relPath}${themeDir === brandThemeDir ? ` (brand: ${brand})` : ""}`);
|
|
1793
|
+
} catch (err) {
|
|
1794
|
+
this.warn(`semi-solid: failed to copy theme file ${relPath}: ${err.message}`);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
copyThemeFiles(themeDir, "");
|
|
1800
|
+
}
|
|
1801
|
+
try {
|
|
1802
|
+
const localeFiles = resolveLocaleFiles(brand, locale, projectRoot, resolvedOutDir).map(({ dest }) => path3.relative(resolvedOutDir, dest));
|
|
1803
|
+
const manifest = generateManifest(brand, locale, {
|
|
1804
|
+
templates: generatedTemplates,
|
|
1805
|
+
snippets: generatedSnippets,
|
|
1806
|
+
sections: generatedSections,
|
|
1807
|
+
assets: generatedAssets,
|
|
1808
|
+
locales: localeFiles
|
|
1809
|
+
});
|
|
1810
|
+
fs2.mkdirSync(resolvedOutDir, { recursive: true });
|
|
1811
|
+
writeIfChanged(
|
|
1812
|
+
path3.join(resolvedOutDir, "manifest.json"),
|
|
1813
|
+
JSON.stringify(manifest, null, 2) + "\n"
|
|
1814
|
+
);
|
|
1815
|
+
this.info(`semi-solid: wrote manifest.json`);
|
|
1816
|
+
} catch (err) {
|
|
1817
|
+
this.warn(`semi-solid: failed to write manifest.json: ${err.message}`);
|
|
1818
|
+
}
|
|
1819
|
+
},
|
|
1820
|
+
transform(code, id) {
|
|
1821
|
+
if (!id.includes(`${path3.sep}src${path3.sep}`) && !id.includes("/src/")) {
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
if (!id.endsWith(".tsx") && !id.endsWith(".jsx")) {
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
const componentName = path3.basename(id).replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
1828
|
+
if (id.includes(`${path3.sep}routes${path3.sep}`) || id.includes("/routes/")) {
|
|
1829
|
+
const basename = path3.basename(id).replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
1830
|
+
if (basename !== "+layout") return null;
|
|
1831
|
+
const routesDir = getRoutesDir(projectRoot);
|
|
1832
|
+
const routeInfo = resolveRoute(id, routesDir);
|
|
1833
|
+
if (!routeInfo) {
|
|
1834
|
+
this.warn(`semi-solid: unrecognized route file: ${id}`);
|
|
1835
|
+
return null;
|
|
1836
|
+
}
|
|
1837
|
+
const { mappings: mappings2, cleanedSource: cleanedSource2, sourceMap: sourceMap2, warnings: warnings2 } = extractTapMappings(code, id);
|
|
1838
|
+
for (const warning of warnings2) {
|
|
1839
|
+
this.warn(warning);
|
|
1840
|
+
}
|
|
1841
|
+
for (const warn of validateTapMappings(mappings2, routeInfo.context)) {
|
|
1842
|
+
this.warn(`semi-solid: ${warn.message}`);
|
|
1843
|
+
}
|
|
1844
|
+
const liquidWarnings = [];
|
|
1845
|
+
let liquidContent2;
|
|
1846
|
+
try {
|
|
1847
|
+
liquidContent2 = generateLiquid(code, mappings2, {
|
|
1848
|
+
componentName,
|
|
1849
|
+
sectionComponents: sectionComponentNames,
|
|
1850
|
+
warnings: liquidWarnings
|
|
1851
|
+
});
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
this.warn(
|
|
1854
|
+
`semi-solid: failed to generate liquid for ${id}: ${err.message}`
|
|
1855
|
+
);
|
|
1856
|
+
return { code: cleanedSource2, map: sourceMap2 };
|
|
1857
|
+
}
|
|
1858
|
+
for (const w of liquidWarnings) {
|
|
1859
|
+
this.warn(`semi-solid: ${componentName}: ${w}`);
|
|
1860
|
+
}
|
|
1861
|
+
for (const warn of validateUnusedMappings(mappings2, liquidContent2)) {
|
|
1862
|
+
this.warn(`semi-solid: ${warn.message}`);
|
|
1863
|
+
}
|
|
1864
|
+
const outputPath = path3.join(resolvedOutDir, routeInfo.outputPath);
|
|
1865
|
+
try {
|
|
1866
|
+
fs2.mkdirSync(path3.dirname(outputPath), { recursive: true });
|
|
1867
|
+
writeIfChanged(outputPath, liquidContent2);
|
|
1868
|
+
generatedTemplates.push(routeInfo.outputPath);
|
|
1869
|
+
this.info(`semi-solid: wrote ${path3.relative(projectRoot, outputPath)}`);
|
|
1870
|
+
} catch (err) {
|
|
1871
|
+
this.warn(
|
|
1872
|
+
`semi-solid: failed to write liquid for ${id}: ${err.message}`
|
|
1873
|
+
);
|
|
1874
|
+
}
|
|
1875
|
+
return { code: cleanedSource2, map: sourceMap2 };
|
|
1876
|
+
}
|
|
1877
|
+
const { mappings, cleanedSource, sourceMap, warnings, reactiveVars, remoteComponents, personalizedCalls } = extractTapMappings(code, id);
|
|
1878
|
+
for (const warning of warnings) {
|
|
1879
|
+
this.warn(warning);
|
|
1880
|
+
}
|
|
1881
|
+
for (const call of personalizedCalls) {
|
|
1882
|
+
personalizedCallsCollected.push({
|
|
1883
|
+
url: call.url,
|
|
1884
|
+
params: call.params,
|
|
1885
|
+
componentMappings: mappings
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
for (const compName of remoteComponents) {
|
|
1889
|
+
const wrapperName = `remote-${toKebabCase(compName)}`;
|
|
1890
|
+
if (!remoteWrapperSections.has(wrapperName)) {
|
|
1891
|
+
remoteWrapperSections.add(wrapperName);
|
|
1892
|
+
const wrapperContent = `{% render '${toKebabCase(compName)}' %}
|
|
1893
|
+
`;
|
|
1894
|
+
const sectionsDir = path3.join(resolvedOutDir, "sections");
|
|
1895
|
+
try {
|
|
1896
|
+
fs2.mkdirSync(sectionsDir, { recursive: true });
|
|
1897
|
+
writeIfChanged(path3.join(sectionsDir, `${wrapperName}.liquid`), wrapperContent);
|
|
1898
|
+
generatedSections.push(`sections/${wrapperName}.liquid`);
|
|
1899
|
+
this.info(`semi-solid: wrote sections/${wrapperName}.liquid (remote wrapper)`);
|
|
1900
|
+
} catch (err) {
|
|
1901
|
+
this.warn(
|
|
1902
|
+
`semi-solid: failed to write remote wrapper section ${wrapperName}: ${err.message}`
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
if (Object.keys(mappings).length === 0) {
|
|
1908
|
+
return null;
|
|
1909
|
+
}
|
|
1910
|
+
const kebabName = toKebabCase(componentName);
|
|
1911
|
+
let dataProps;
|
|
1912
|
+
let dataSectionId;
|
|
1913
|
+
if (isInteractiveComponent(code)) {
|
|
1914
|
+
const propVars = Object.keys(mappings);
|
|
1915
|
+
dataProps = generateDataProps(propVars, mappings);
|
|
1916
|
+
interactiveComponents.push({
|
|
1917
|
+
name: componentName,
|
|
1918
|
+
importPath: `./${kebabName}.js`
|
|
1919
|
+
});
|
|
1920
|
+
if (reactiveVars.size > 0) {
|
|
1921
|
+
dataSectionId = `${kebabName}-data`;
|
|
1922
|
+
const dataSectionContent = generateDataSection(propVars, mappings);
|
|
1923
|
+
const sectionsDir = path3.join(resolvedOutDir, "sections");
|
|
1924
|
+
try {
|
|
1925
|
+
fs2.mkdirSync(sectionsDir, { recursive: true });
|
|
1926
|
+
const dataSectionPath = path3.join(sectionsDir, `${dataSectionId}.liquid`);
|
|
1927
|
+
writeIfChanged(dataSectionPath, dataSectionContent);
|
|
1928
|
+
generatedSnippets.push(`sections/${dataSectionId}.liquid`);
|
|
1929
|
+
this.info(`semi-solid: wrote sections/${dataSectionId}.liquid`);
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
this.warn(
|
|
1932
|
+
`semi-solid: failed to write data section for ${id}: ${err.message}`
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
const liquidFileName = kebabName + ".liquid";
|
|
1938
|
+
const liquidWarnings2 = [];
|
|
1939
|
+
let liquidContent;
|
|
1940
|
+
try {
|
|
1941
|
+
liquidContent = generateLiquid(code, mappings, {
|
|
1942
|
+
componentName,
|
|
1943
|
+
dataProps,
|
|
1944
|
+
dataSectionId,
|
|
1945
|
+
sectionComponents: sectionComponentNames,
|
|
1946
|
+
warnings: liquidWarnings2
|
|
1947
|
+
});
|
|
1948
|
+
} catch (err) {
|
|
1949
|
+
this.warn(
|
|
1950
|
+
`semi-solid: failed to generate liquid for ${id}: ${err.message}`
|
|
1951
|
+
);
|
|
1952
|
+
return { code: cleanedSource, map: sourceMap };
|
|
1953
|
+
}
|
|
1954
|
+
for (const w of liquidWarnings2) {
|
|
1955
|
+
this.warn(`semi-solid: ${componentName}: ${w}`);
|
|
1956
|
+
}
|
|
1957
|
+
for (const warn of validateUnusedMappings(mappings, liquidContent)) {
|
|
1958
|
+
this.warn(`semi-solid: ${warn.message}`);
|
|
1959
|
+
}
|
|
1960
|
+
let isSection = false;
|
|
1961
|
+
try {
|
|
1962
|
+
const schema = extractSectionSchema(code);
|
|
1963
|
+
if (schema !== null) {
|
|
1964
|
+
isSection = true;
|
|
1965
|
+
liquidContent += "\n" + formatSchemaTag(schema);
|
|
1966
|
+
sectionComponentNames.add(componentName);
|
|
1967
|
+
const sectionsDir = path3.join(resolvedOutDir, "sections");
|
|
1968
|
+
fs2.mkdirSync(sectionsDir, { recursive: true });
|
|
1969
|
+
const liquidPath = path3.join(sectionsDir, liquidFileName);
|
|
1970
|
+
writeIfChanged(liquidPath, liquidContent);
|
|
1971
|
+
generatedSections.push(`sections/${liquidFileName}`);
|
|
1972
|
+
this.info(`semi-solid: wrote sections/${liquidFileName} (section)`);
|
|
1973
|
+
}
|
|
1974
|
+
} catch (err) {
|
|
1975
|
+
this.warn(
|
|
1976
|
+
`semi-solid: failed to extract/write section schema for ${id}: ${err.message}`
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
if (!isSection) {
|
|
1980
|
+
const snippetsDir = path3.join(resolvedOutDir, "snippets");
|
|
1981
|
+
try {
|
|
1982
|
+
fs2.mkdirSync(snippetsDir, { recursive: true });
|
|
1983
|
+
const liquidPath = path3.join(snippetsDir, liquidFileName);
|
|
1984
|
+
writeIfChanged(liquidPath, liquidContent);
|
|
1985
|
+
generatedSnippets.push(`snippets/${liquidFileName}`);
|
|
1986
|
+
this.info(`semi-solid: wrote ${path3.relative(projectRoot, liquidPath)}`);
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
this.warn(
|
|
1989
|
+
`semi-solid: failed to write liquid for ${id}: ${err.message}`
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
return {
|
|
1994
|
+
code: cleanedSource,
|
|
1995
|
+
map: sourceMap
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// src/hash.ts
|
|
2002
|
+
import { createHash } from "crypto";
|
|
2003
|
+
function hashContent(content) {
|
|
2004
|
+
return createHash("sha256").update(content, "utf-8").digest("hex").slice(0, 8);
|
|
2005
|
+
}
|
|
2006
|
+
function versionedName(basename, hash) {
|
|
2007
|
+
const lastDot = basename.lastIndexOf(".");
|
|
2008
|
+
if (lastDot === -1) return `${basename}-${hash}`;
|
|
2009
|
+
return `${basename.slice(0, lastDot)}-${hash}${basename.slice(lastDot)}`;
|
|
2010
|
+
}
|
|
2011
|
+
function parseVersionedName(filename) {
|
|
2012
|
+
const match = filename.match(/^(.+)-([0-9a-f]{8})(\.[^.]+)$/);
|
|
2013
|
+
if (!match) return null;
|
|
2014
|
+
return { name: match[1] + match[3], hash: match[2] };
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// src/brand-resolve.ts
|
|
2018
|
+
import fs3 from "fs";
|
|
2019
|
+
import path4 from "path";
|
|
2020
|
+
var EXTENSIONS = [".tsx", ".ts", ".jsx", ".js"];
|
|
2021
|
+
var CATEGORY_PREFIXES = {
|
|
2022
|
+
"$snippets/": "snippets",
|
|
2023
|
+
"$sections/": "sections",
|
|
2024
|
+
"$blocks/": "blocks"
|
|
2025
|
+
};
|
|
2026
|
+
function resolveCategoryBrandPath(componentPath, category, brand, projectRoot, existsSync = fs3.existsSync) {
|
|
2027
|
+
const nameNoExt = componentPath.replace(/\.(tsx|jsx|ts|js)$/, "");
|
|
2028
|
+
const overrideBase = path4.join(
|
|
2029
|
+
projectRoot,
|
|
2030
|
+
"src",
|
|
2031
|
+
"brands",
|
|
2032
|
+
brand,
|
|
2033
|
+
category,
|
|
2034
|
+
nameNoExt
|
|
2035
|
+
);
|
|
2036
|
+
for (const ext of EXTENSIONS) {
|
|
2037
|
+
const candidate = overrideBase + ext;
|
|
2038
|
+
if (existsSync(candidate)) return candidate;
|
|
2039
|
+
}
|
|
2040
|
+
const baseComponentBase = path4.join(
|
|
2041
|
+
projectRoot,
|
|
2042
|
+
"src",
|
|
2043
|
+
category,
|
|
2044
|
+
nameNoExt
|
|
2045
|
+
);
|
|
2046
|
+
for (const ext of EXTENSIONS) {
|
|
2047
|
+
const candidate = baseComponentBase + ext;
|
|
2048
|
+
if (existsSync(candidate)) return candidate;
|
|
2049
|
+
}
|
|
2050
|
+
return null;
|
|
2051
|
+
}
|
|
2052
|
+
function resolveBrandPath(componentPath, brand, projectRoot, existsSync = fs3.existsSync) {
|
|
2053
|
+
for (const category of ["snippets", "sections", "blocks"]) {
|
|
2054
|
+
const result = resolveCategoryBrandPath(componentPath, category, brand, projectRoot, existsSync);
|
|
2055
|
+
if (result) return result;
|
|
2056
|
+
}
|
|
2057
|
+
return null;
|
|
2058
|
+
}
|
|
2059
|
+
function createBrandResolver(brand, projectRoot) {
|
|
2060
|
+
let resolvedRoot = projectRoot ?? process.cwd();
|
|
2061
|
+
return {
|
|
2062
|
+
name: "semi-solid-brand-resolve",
|
|
2063
|
+
configResolved(config) {
|
|
2064
|
+
if (!projectRoot) {
|
|
2065
|
+
resolvedRoot = config.root;
|
|
2066
|
+
}
|
|
2067
|
+
},
|
|
2068
|
+
resolveId(source) {
|
|
2069
|
+
for (const [prefix, category] of Object.entries(CATEGORY_PREFIXES)) {
|
|
2070
|
+
if (source.startsWith(prefix)) {
|
|
2071
|
+
const componentPath = source.slice(prefix.length);
|
|
2072
|
+
return resolveCategoryBrandPath(componentPath, category, brand, resolvedRoot);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return null;
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
export {
|
|
2080
|
+
GLOBAL_LIQUID_OBJECTS,
|
|
2081
|
+
VIRTUAL_LOCALE_MODULE,
|
|
2082
|
+
createBrandResolver,
|
|
2083
|
+
detectPropVars,
|
|
2084
|
+
extractLiquidObjects,
|
|
2085
|
+
extractTapMappings,
|
|
2086
|
+
generateAssetIncludes,
|
|
2087
|
+
generateDataProps,
|
|
2088
|
+
generateHydrationEntry,
|
|
2089
|
+
generateLiquid,
|
|
2090
|
+
generateManifest,
|
|
2091
|
+
generatePreconnectTag,
|
|
2092
|
+
generatePrefetchScript,
|
|
2093
|
+
generateScriptTag,
|
|
2094
|
+
generateStylesheetTag,
|
|
2095
|
+
getRoutesDir,
|
|
2096
|
+
hashContent,
|
|
2097
|
+
isInteractiveComponent,
|
|
2098
|
+
isRouteFile,
|
|
2099
|
+
parseVersionedName,
|
|
2100
|
+
resolveActiveLocalePath,
|
|
2101
|
+
resolveBrandPath,
|
|
2102
|
+
resolveCSSFiles,
|
|
2103
|
+
resolveForIteration,
|
|
2104
|
+
resolveLocaleFiles,
|
|
2105
|
+
resolveMemberPath,
|
|
2106
|
+
resolveRoute,
|
|
2107
|
+
resolveShowCondition,
|
|
2108
|
+
semiSolidPlugin,
|
|
2109
|
+
stripLiquidBraces,
|
|
2110
|
+
toKebabCase,
|
|
2111
|
+
validateTapMappings,
|
|
2112
|
+
validateUnusedMappings,
|
|
2113
|
+
versionedName,
|
|
2114
|
+
virtualLocaleIds
|
|
2115
|
+
};
|