vite-plugin-react-shopify 1.1.0 → 2.0.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/README.md +205 -288
- package/dist/index.d.ts +22 -3
- package/dist/index.js +225 -112
- package/dist/runtime/index.d.ts +32 -0
- package/dist/runtime/index.js +73 -0
- package/package.json +3 -15
- package/dist/runtime/Liquid.client.d.ts +0 -6
- package/dist/runtime/Liquid.client.js +0 -7
- package/dist/runtime/Liquid.d.ts +0 -11
- package/dist/runtime/Liquid.js +0 -10
- package/dist/runtime/settings.d.ts +0 -8
- package/dist/runtime/settings.js +0 -44
package/dist/index.js
CHANGED
|
@@ -1,31 +1,38 @@
|
|
|
1
|
-
// src/options.ts
|
|
1
|
+
// src/plugin/options.ts
|
|
2
2
|
import path from "path";
|
|
3
|
-
var defaultImportMap = {
|
|
4
|
-
react: "https://esm.sh/react@19",
|
|
5
|
-
reactDomClient: "https://esm.sh/react-dom@19/client"
|
|
6
|
-
};
|
|
7
3
|
var defaultPrefix = {
|
|
8
4
|
template: "page.react-",
|
|
9
5
|
section: "react-",
|
|
10
|
-
block: "react-"
|
|
6
|
+
block: "react-",
|
|
7
|
+
snippet: "react-"
|
|
11
8
|
};
|
|
9
|
+
function assetRef(buildDir, filename) {
|
|
10
|
+
if (buildDir === "assets") return filename;
|
|
11
|
+
const sub = buildDir.startsWith("assets/") ? buildDir.slice(7) : buildDir;
|
|
12
|
+
return `${sub}/${filename}`;
|
|
13
|
+
}
|
|
14
|
+
function liquidAssetUrl(ref) {
|
|
15
|
+
return `{{ '${ref}' | asset_url }}`;
|
|
16
|
+
}
|
|
12
17
|
var resolveOptions = (options = {}) => {
|
|
13
18
|
const themeRoot = options.themeRoot ?? "./";
|
|
14
19
|
const sourceCodeDir = options.sourceCodeDir ?? "frontend";
|
|
15
20
|
const snippetFile = options.snippetFile ?? "shopify-importmap.liquid";
|
|
16
21
|
const buildDir = options.buildDir ?? "assets";
|
|
17
22
|
const ssg = {
|
|
18
|
-
directories: options.ssg?.directories ?? ["sections", "blocks", "templates"],
|
|
23
|
+
directories: options.ssg?.directories ?? ["sections", "blocks", "templates", "snippets"],
|
|
19
24
|
prefix: {
|
|
20
25
|
template: options.ssg?.prefix?.template ?? defaultPrefix.template,
|
|
21
26
|
section: options.ssg?.prefix?.section ?? defaultPrefix.section,
|
|
22
|
-
block: options.ssg?.prefix?.block ?? defaultPrefix.block
|
|
27
|
+
block: options.ssg?.prefix?.block ?? defaultPrefix.block,
|
|
28
|
+
snippet: options.ssg?.prefix?.snippet ?? defaultPrefix.snippet
|
|
23
29
|
},
|
|
24
|
-
outputName: options.ssg?.outputName ?? ""
|
|
30
|
+
outputName: options.ssg?.outputName ?? "",
|
|
31
|
+
cssPrefix: options.ssg?.cssPrefix ?? "css"
|
|
25
32
|
};
|
|
26
33
|
const importMap = {
|
|
27
|
-
react: options.importMap?.react ??
|
|
28
|
-
reactDomClient: options.importMap?.reactDomClient ??
|
|
34
|
+
react: options.importMap?.react ?? liquidAssetUrl(assetRef(buildDir, "react.js")),
|
|
35
|
+
reactDomClient: options.importMap?.reactDomClient ?? liquidAssetUrl(assetRef(buildDir, "react-dom.js"))
|
|
29
36
|
};
|
|
30
37
|
return {
|
|
31
38
|
themeRoot: path.resolve(themeRoot),
|
|
@@ -33,13 +40,12 @@ var resolveOptions = (options = {}) => {
|
|
|
33
40
|
snippetFile,
|
|
34
41
|
buildDir,
|
|
35
42
|
debug: options.debug ?? false,
|
|
36
|
-
hash: options.hash ?? false,
|
|
37
43
|
ssg,
|
|
38
44
|
importMap
|
|
39
45
|
};
|
|
40
46
|
};
|
|
41
47
|
|
|
42
|
-
// src/logger.ts
|
|
48
|
+
// src/plugin/logger.ts
|
|
43
49
|
import createDebugger from "debug";
|
|
44
50
|
var NAMESPACE = "vite-plugin-shopify";
|
|
45
51
|
var _debugEnabled = false;
|
|
@@ -61,7 +67,7 @@ function logger(ns) {
|
|
|
61
67
|
};
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
// src/config.ts
|
|
70
|
+
// src/plugin/config.ts
|
|
65
71
|
import path2 from "path";
|
|
66
72
|
var log = logger("config");
|
|
67
73
|
function isWatchMode() {
|
|
@@ -73,32 +79,38 @@ function shopifyConfig(options) {
|
|
|
73
79
|
config(config) {
|
|
74
80
|
const sourceDirAbs = path2.resolve(options.themeRoot, options.sourceCodeDir);
|
|
75
81
|
const watch = isWatchMode();
|
|
76
|
-
|
|
77
|
-
const chunkFileNames = options.hash ? "[name]-[hash].js" : "[name].js";
|
|
78
|
-
const assetFileNames = options.hash ? "[name]-[hash][extname]" : "[name][extname]";
|
|
79
|
-
log.debug("hash=%s watch=%s", options.hash, watch);
|
|
82
|
+
log.debug("watch=%s", watch);
|
|
80
83
|
const generated = {
|
|
81
84
|
base: config.base ?? "./",
|
|
82
85
|
publicDir: config.publicDir ?? false,
|
|
83
86
|
build: {
|
|
84
87
|
outDir: config.build?.outDir ?? path2.join(options.themeRoot, options.buildDir),
|
|
85
88
|
assetsDir: config.build?.assetsDir ?? "",
|
|
86
|
-
emptyOutDir: config.build?.emptyOutDir ??
|
|
89
|
+
emptyOutDir: config.build?.emptyOutDir ?? true,
|
|
87
90
|
manifest: config.build?.manifest ?? true,
|
|
88
91
|
minify: config.build?.minify ?? (watch || options.debug ? false : void 0),
|
|
89
92
|
sourcemap: config.build?.sourcemap ?? (watch || options.debug ? "inline" : void 0),
|
|
90
|
-
|
|
91
|
-
...config.build?.rollupOptions,
|
|
92
|
-
external: [
|
|
93
|
-
...Array.isArray(config.build?.rollupOptions?.external) ? config.build.rollupOptions.external : [],
|
|
94
|
-
"react",
|
|
95
|
-
"react-dom/client"
|
|
96
|
-
],
|
|
93
|
+
rolldownOptions: {
|
|
94
|
+
...config.build?.rolldownOptions ?? config.build?.rollupOptions,
|
|
95
|
+
external: Array.isArray((config.build?.rolldownOptions ?? config.build?.rollupOptions)?.external) ? (config.build?.rolldownOptions ?? config.build?.rollupOptions).external : [],
|
|
97
96
|
output: {
|
|
98
|
-
...config.build?.rollupOptions?.output,
|
|
99
|
-
entryFileNames,
|
|
100
|
-
chunkFileNames
|
|
101
|
-
|
|
97
|
+
...(config.build?.rolldownOptions ?? config.build?.rollupOptions)?.output,
|
|
98
|
+
entryFileNames: "[name]-[hash].js",
|
|
99
|
+
chunkFileNames(chunkInfo) {
|
|
100
|
+
if (["react", "react-dom"].includes(chunkInfo.name)) {
|
|
101
|
+
return `${chunkInfo.name}.js`;
|
|
102
|
+
}
|
|
103
|
+
return "[name]-[hash].js";
|
|
104
|
+
},
|
|
105
|
+
assetFileNames: "[name]-[hash][extname]",
|
|
106
|
+
manualChunks(id) {
|
|
107
|
+
if (id.includes("/node_modules/react-dom/")) {
|
|
108
|
+
return "react-dom";
|
|
109
|
+
}
|
|
110
|
+
if (id.includes("/node_modules/react/") || id.includes("/node_modules/scheduler/")) {
|
|
111
|
+
return "react";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
102
114
|
}
|
|
103
115
|
}
|
|
104
116
|
},
|
|
@@ -134,18 +146,19 @@ function shopifyConfig(options) {
|
|
|
134
146
|
};
|
|
135
147
|
}
|
|
136
148
|
|
|
137
|
-
// src/entries.ts
|
|
149
|
+
// src/plugin/entries.ts
|
|
138
150
|
import path4 from "path";
|
|
139
151
|
import { normalizePath as normalizePath2 } from "vite";
|
|
140
152
|
|
|
141
|
-
// src/ssg/scanner.ts
|
|
153
|
+
// src/plugin/ssg/scanner.ts
|
|
142
154
|
import path3 from "path";
|
|
143
155
|
import glob from "fast-glob";
|
|
144
156
|
import { normalizePath } from "vite";
|
|
145
157
|
var TYPE_BY_DIR = {
|
|
146
158
|
templates: "template",
|
|
147
159
|
sections: "section",
|
|
148
|
-
blocks: "block"
|
|
160
|
+
blocks: "block",
|
|
161
|
+
snippets: "snippet"
|
|
149
162
|
};
|
|
150
163
|
function scanEntries(options) {
|
|
151
164
|
const sourceDir = path3.resolve(options.themeRoot, options.sourceCodeDir);
|
|
@@ -174,7 +187,7 @@ function toKebabCase(str) {
|
|
|
174
187
|
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
175
188
|
}
|
|
176
189
|
|
|
177
|
-
// src/entries.ts
|
|
190
|
+
// src/plugin/entries.ts
|
|
178
191
|
var log2 = logger("entries");
|
|
179
192
|
function shopifyEntries(options) {
|
|
180
193
|
let entries = [];
|
|
@@ -214,20 +227,22 @@ function shopifyEntries(options) {
|
|
|
214
227
|
`import { createElement } from 'react'`,
|
|
215
228
|
`import Component from '~/${componentRel}'`,
|
|
216
229
|
`import { hydrateRoot } from 'react-dom/client'`,
|
|
217
|
-
`import {
|
|
218
|
-
`import { ParamsProvider } from 'vite-plugin-react-shopify/runtime/settings'`,
|
|
230
|
+
`import { LiquidDataProvider } from 'vite-plugin-react-shopify/runtime'`,
|
|
219
231
|
``,
|
|
220
232
|
`const SELECTOR = '[data-ssg-component="${kebabName}"]'`,
|
|
221
233
|
`const roots = new Map()`,
|
|
222
234
|
``,
|
|
235
|
+
`function readLiquidData(el) {`,
|
|
236
|
+
` const script = el.querySelector(':scope > script[data-ssg-liquid]')`,
|
|
237
|
+
` if (!script) return {}`,
|
|
238
|
+
` try { return JSON.parse(script.textContent || '{}') } catch { return {} }`,
|
|
239
|
+
`}`,
|
|
240
|
+
``,
|
|
223
241
|
`function hydrate(el) {`,
|
|
224
242
|
` const h = el.querySelector(':scope > [data-ssg-hydrate]') || (el.matches('[data-ssg-hydrate]') ? el : null)`,
|
|
225
243
|
` if (!h || roots.has(h)) return`,
|
|
226
|
-
` const
|
|
227
|
-
`
|
|
228
|
-
` const paramsEl = el.querySelector(':scope > script[data-ssg-params]')`,
|
|
229
|
-
` const params = paramsEl ? JSON.parse(paramsEl.textContent || '{}') : {}`,
|
|
230
|
-
` roots.set(h, hydrateRoot(h, createElement(SettingsProvider, { value: props }, createElement(ParamsProvider, { value: params }, createElement(Component)))))`,
|
|
244
|
+
` const liquidData = readLiquidData(el)`,
|
|
245
|
+
` roots.set(h, hydrateRoot(h, createElement(LiquidDataProvider, { value: liquidData }, createElement(Component))))`,
|
|
231
246
|
`}`,
|
|
232
247
|
``,
|
|
233
248
|
`function unmount(el) {`,
|
|
@@ -259,30 +274,45 @@ function shopifyEntries(options) {
|
|
|
259
274
|
};
|
|
260
275
|
}
|
|
261
276
|
|
|
262
|
-
// src/ssg/index.ts
|
|
277
|
+
// src/plugin/ssg/index.ts
|
|
263
278
|
import fs2 from "fs";
|
|
264
279
|
import path7 from "path";
|
|
265
280
|
|
|
266
|
-
// src/ssg/compiler.ts
|
|
281
|
+
// src/plugin/ssg/compiler.ts
|
|
267
282
|
import fs from "fs";
|
|
268
283
|
import path6 from "path";
|
|
269
284
|
import { createRequire } from "module";
|
|
270
285
|
|
|
271
|
-
// src/ssg/post-process.ts
|
|
272
|
-
var
|
|
273
|
-
function
|
|
274
|
-
return html.replace(
|
|
286
|
+
// src/plugin/ssg/post-process.ts
|
|
287
|
+
var VOID_ELEMENTS = /<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)([^>]*)\/>/g;
|
|
288
|
+
function normalizeVoidElements(html) {
|
|
289
|
+
return html.replace(VOID_ELEMENTS, "<$1$2>");
|
|
290
|
+
}
|
|
291
|
+
function normalizeStyleAttributes(html) {
|
|
292
|
+
return html.replace(/ style="([^"]+)"/g, (_match, content) => {
|
|
293
|
+
const normalized = content.replace(/:(\S)/g, ": $1").replace(/;\s*$/, "");
|
|
294
|
+
return ` style="${normalized};"`;
|
|
295
|
+
});
|
|
275
296
|
}
|
|
276
297
|
function unwrapHtmlEntities(html) {
|
|
277
298
|
return html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
278
299
|
}
|
|
279
300
|
|
|
280
|
-
// src/ssg/
|
|
301
|
+
// src/plugin/ssg/liquid.ts
|
|
302
|
+
import path5 from "path";
|
|
303
|
+
|
|
304
|
+
// src/plugin/ssg/schema-gen.ts
|
|
305
|
+
var log3 = logger("schema-gen");
|
|
281
306
|
function serializeSetting(setting) {
|
|
282
307
|
const s = { type: setting.type };
|
|
283
308
|
if ("id" in setting) s.id = setting.id;
|
|
284
309
|
if ("label" in setting) s.label = setting.label;
|
|
285
310
|
if ("default" in setting && setting.default !== void 0) {
|
|
311
|
+
if (setting.default === "") {
|
|
312
|
+
log3.warn(
|
|
313
|
+
`Setting "${"id" in setting ? setting.id : "(no id)"}" has empty string default. Use a non-empty string or remove the default.`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
286
316
|
s.default = setting.default;
|
|
287
317
|
}
|
|
288
318
|
if ("info" in setting && setting.info) s.info = setting.info;
|
|
@@ -345,10 +375,9 @@ ${json}
|
|
|
345
375
|
`;
|
|
346
376
|
}
|
|
347
377
|
|
|
348
|
-
// src/ssg/liquid.ts
|
|
349
|
-
import path5 from "path";
|
|
378
|
+
// src/plugin/ssg/liquid.ts
|
|
350
379
|
var DISCLAIMER = "{% comment %}\n IMPORTANT: This file is automatically generated by vite-plugin-shopify.\n Do not attempt to modify this file directly, as any changes will be overwritten by the next build.\n{% endcomment %}\n";
|
|
351
|
-
function assembleLiquidFile(html, entry, scriptAsset, cssContents, options) {
|
|
380
|
+
function assembleLiquidFile(html, entry, scriptAsset, cssContents, options, trackedExpressions = []) {
|
|
352
381
|
const type = entry.meta.type ?? entry.targetType;
|
|
353
382
|
const parts = [DISCLAIMER];
|
|
354
383
|
switch (type) {
|
|
@@ -356,13 +385,16 @@ function assembleLiquidFile(html, entry, scriptAsset, cssContents, options) {
|
|
|
356
385
|
parts.push(html);
|
|
357
386
|
break;
|
|
358
387
|
case "section":
|
|
359
|
-
parts.push(...buildSection(html, entry));
|
|
388
|
+
parts.push(...buildSection(html, entry, trackedExpressions));
|
|
360
389
|
break;
|
|
361
390
|
case "block":
|
|
362
|
-
parts.push(...buildBlock(html, entry));
|
|
391
|
+
parts.push(...buildBlock(html, entry, trackedExpressions));
|
|
392
|
+
break;
|
|
393
|
+
case "snippet":
|
|
394
|
+
parts.push(...buildSnippet(html, entry, trackedExpressions));
|
|
363
395
|
break;
|
|
364
396
|
default:
|
|
365
|
-
parts.push(...buildSection(html, entry));
|
|
397
|
+
parts.push(...buildSection(html, entry, trackedExpressions));
|
|
366
398
|
break;
|
|
367
399
|
}
|
|
368
400
|
for (const snippet of cssContents.snippets) {
|
|
@@ -383,21 +415,27 @@ function assembleLiquidFile(html, entry, scriptAsset, cssContents, options) {
|
|
|
383
415
|
`<script type="module" src="{{ '${assetPath}' | asset_url }}"></script>`
|
|
384
416
|
);
|
|
385
417
|
}
|
|
386
|
-
|
|
418
|
+
if (type !== "snippet") {
|
|
419
|
+
parts.push(generateSchema(entry.meta));
|
|
420
|
+
}
|
|
387
421
|
return parts.join("\n") + "\n";
|
|
388
422
|
}
|
|
389
423
|
var hasBlocks = (entry) => !!entry.meta.blocks && entry.meta.blocks.length > 0;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
424
|
+
function buildLiquidBridge(trackedExpressions) {
|
|
425
|
+
if (trackedExpressions.length === 0) return "";
|
|
426
|
+
const entries = trackedExpressions.map((expr, i) => {
|
|
427
|
+
const comma = i < trackedExpressions.length - 1 ? "," : "";
|
|
428
|
+
return ` "${expr}": {{ ${expr} | json }}${comma}`;
|
|
429
|
+
});
|
|
430
|
+
return [
|
|
431
|
+
' <script type="application/json" data-ssg-liquid>',
|
|
432
|
+
" {",
|
|
433
|
+
entries.join("\n"),
|
|
434
|
+
" }",
|
|
435
|
+
" </script>"
|
|
436
|
+
].join("\n");
|
|
399
437
|
}
|
|
400
|
-
function buildSection(html, entry) {
|
|
438
|
+
function buildSection(html, entry, trackedExpressions) {
|
|
401
439
|
const tag = entry.meta.tag ?? "div";
|
|
402
440
|
const cls = entry.meta.class ?? "";
|
|
403
441
|
const lines = [
|
|
@@ -408,23 +446,17 @@ function buildSection(html, entry) {
|
|
|
408
446
|
` data-ssg-component="${entry.kebabName}"`
|
|
409
447
|
];
|
|
410
448
|
if (cls) lines.push(` class="${cls}"`);
|
|
449
|
+
lines.push(`>`);
|
|
450
|
+
const liquidBridge = buildLiquidBridge(trackedExpressions);
|
|
451
|
+
if (liquidBridge) lines.push(liquidBridge);
|
|
411
452
|
lines.push(
|
|
412
|
-
|
|
413
|
-
SETTINGS_SECTION
|
|
414
|
-
);
|
|
415
|
-
if (entry.meta.params?.length) {
|
|
416
|
-
lines.push(buildParamsBridge(entry.meta.params));
|
|
417
|
-
}
|
|
418
|
-
lines.push(
|
|
419
|
-
` <div data-ssg-hydrate>`,
|
|
420
|
-
` ${html}`,
|
|
421
|
-
` </div>`
|
|
453
|
+
` <div data-ssg-hydrate>${html}</div>`
|
|
422
454
|
);
|
|
423
455
|
if (hasBlocks(entry)) lines.push(` {% content_for 'blocks' %}`);
|
|
424
456
|
lines.push(`</${tag}>`);
|
|
425
457
|
return lines;
|
|
426
458
|
}
|
|
427
|
-
function buildBlock(html, entry) {
|
|
459
|
+
function buildBlock(html, entry, trackedExpressions) {
|
|
428
460
|
const tag = entry.meta.tag ?? "div";
|
|
429
461
|
const cls = entry.meta.class ?? "";
|
|
430
462
|
const lines = [
|
|
@@ -442,27 +474,43 @@ function buildBlock(html, entry) {
|
|
|
442
474
|
if (cls) lines.push(` class="${cls}"`);
|
|
443
475
|
lines.push(
|
|
444
476
|
` {{ block.shopify_attributes }}`,
|
|
445
|
-
|
|
446
|
-
SETTINGS_BLOCK
|
|
477
|
+
`>`
|
|
447
478
|
);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
479
|
+
const liquidBridge = buildLiquidBridge(trackedExpressions);
|
|
480
|
+
if (liquidBridge) lines.push(liquidBridge);
|
|
451
481
|
lines.push(
|
|
452
|
-
` <div data-ssg-hydrate
|
|
453
|
-
` ${html}`,
|
|
454
|
-
` </div>`
|
|
482
|
+
` <div data-ssg-hydrate>${html}</div>`
|
|
455
483
|
);
|
|
456
484
|
if (hasBlocks(entry)) lines.push(` {% content_for 'blocks' %}`);
|
|
457
485
|
lines.push(`</${tag}>`);
|
|
458
486
|
return lines;
|
|
459
487
|
}
|
|
488
|
+
function buildSnippet(html, entry, trackedExpressions) {
|
|
489
|
+
const lines = [
|
|
490
|
+
"",
|
|
491
|
+
`<div data-ssg-component="${entry.kebabName}">`
|
|
492
|
+
];
|
|
493
|
+
const liquidBridge = buildLiquidBridge(trackedExpressions);
|
|
494
|
+
if (liquidBridge) lines.push(liquidBridge);
|
|
495
|
+
lines.push(
|
|
496
|
+
` <div data-ssg-hydrate>`,
|
|
497
|
+
` ${html}`,
|
|
498
|
+
` </div>`,
|
|
499
|
+
`</div>`
|
|
500
|
+
);
|
|
501
|
+
return lines;
|
|
502
|
+
}
|
|
460
503
|
function getOutputPath(entry, options) {
|
|
461
504
|
const type = entry.meta.type ?? entry.targetType;
|
|
462
|
-
const dirName = type
|
|
505
|
+
const dirName = typeToDir(type);
|
|
463
506
|
const fileName = resolveFileName(entry, type, options);
|
|
464
507
|
return path5.join(options.themeRoot, dirName, fileName);
|
|
465
508
|
}
|
|
509
|
+
function typeToDir(type) {
|
|
510
|
+
if (type === "snippet") return "snippets";
|
|
511
|
+
if (type === "block") return "blocks";
|
|
512
|
+
return `${type}s`;
|
|
513
|
+
}
|
|
466
514
|
function getAssetRelativePath(buildDir, filename) {
|
|
467
515
|
if (!buildDir.startsWith("assets/")) return filename;
|
|
468
516
|
const prefix = buildDir.slice("assets/".length);
|
|
@@ -476,13 +524,52 @@ function resolveFileName(entry, type, options) {
|
|
|
476
524
|
return `${prefix}${entry.kebabName}.liquid`;
|
|
477
525
|
}
|
|
478
526
|
|
|
479
|
-
// src/ssg/
|
|
480
|
-
var
|
|
481
|
-
|
|
527
|
+
// src/plugin/ssg/hydration-fix.ts
|
|
528
|
+
var log4 = logger("hydration-fix");
|
|
529
|
+
function autoFixAdjacentText(source, filePath) {
|
|
530
|
+
let fixCount = 0;
|
|
531
|
+
const lines = source.split("\n");
|
|
532
|
+
const fixed = [];
|
|
533
|
+
for (let i = 0; i < lines.length; i++) {
|
|
534
|
+
const line = lines[i];
|
|
535
|
+
const replaced = line.replace(
|
|
536
|
+
/<(\w+)([^>]*?)>([^<]*?\{[^}]*\}[^<]*?)<\/\1>/g,
|
|
537
|
+
(match, tagName, attrs, content) => {
|
|
538
|
+
const trimmed = content.trim();
|
|
539
|
+
if (!needsFix(trimmed)) return match;
|
|
540
|
+
fixCount++;
|
|
541
|
+
const tpl = trimmed.replace(/\{([^}]+)\}/g, "${$1}");
|
|
542
|
+
return `<${tagName}${attrs}>{\`${tpl}\`}</${tagName}>`;
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
fixed.push(replaced);
|
|
546
|
+
}
|
|
547
|
+
if (fixCount > 0) {
|
|
548
|
+
log4.warn(
|
|
549
|
+
`auto-fixed ${fixCount} adjacent text+expression issue(s) in ${filePath}`
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
return { result: fixed.join("\n"), fixCount };
|
|
553
|
+
}
|
|
554
|
+
function needsFix(content) {
|
|
555
|
+
const trimmed = content.trim();
|
|
556
|
+
if (!trimmed) return false;
|
|
557
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
558
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
559
|
+
if (inner.startsWith("`") && inner.endsWith("`")) return false;
|
|
560
|
+
if (inner.length > 0 && !/<[a-zA-Z]/.test(inner)) return false;
|
|
561
|
+
}
|
|
562
|
+
if (!/\{/.test(trimmed)) return false;
|
|
563
|
+
if (/<[a-zA-Z]/.test(trimmed)) return false;
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/plugin/ssg/compiler.ts
|
|
568
|
+
var log5 = logger("ssg:compiler");
|
|
482
569
|
async function compileAllEntries(options, manifest) {
|
|
483
570
|
const entries = scanEntries(options);
|
|
484
571
|
if (entries.length === 0) return;
|
|
485
|
-
|
|
572
|
+
log5.debug("found %d entries to compile", entries.length);
|
|
486
573
|
const projectRoot = path6.resolve(options.themeRoot);
|
|
487
574
|
const sourceDir = path6.resolve(options.themeRoot, options.sourceCodeDir);
|
|
488
575
|
const entryCssFiles = /* @__PURE__ */ new Map();
|
|
@@ -494,12 +581,12 @@ async function compileAllEntries(options, manifest) {
|
|
|
494
581
|
for (const f of files) {
|
|
495
582
|
cssRefCount.set(f, (cssRefCount.get(f) || 0) + 1);
|
|
496
583
|
}
|
|
497
|
-
|
|
584
|
+
log5.debug("entry %s has %d CSS files", entry.kebabName, files.length);
|
|
498
585
|
}
|
|
499
586
|
const cssSnippetMap = /* @__PURE__ */ new Map();
|
|
500
587
|
for (const [cssFile, count] of cssRefCount) {
|
|
501
588
|
if (count > 1) {
|
|
502
|
-
const snippetName = `${
|
|
589
|
+
const snippetName = `${options.ssg.cssPrefix}-${getCssBaseName(cssFile)}`;
|
|
503
590
|
cssSnippetMap.set(cssFile, snippetName);
|
|
504
591
|
const snippetPath = path6.join(
|
|
505
592
|
path6.resolve(options.themeRoot),
|
|
@@ -517,9 +604,9 @@ async function compileAllEntries(options, manifest) {
|
|
|
517
604
|
${cssContent.trim()}
|
|
518
605
|
{% endstylesheet %}
|
|
519
606
|
`);
|
|
520
|
-
|
|
607
|
+
log5.debug("generated shared CSS snippet %s (used by %d entries)", snippetName, count);
|
|
521
608
|
} catch {
|
|
522
|
-
|
|
609
|
+
log5.warn("failed to write CSS snippet for %s", cssFile);
|
|
523
610
|
}
|
|
524
611
|
}
|
|
525
612
|
}
|
|
@@ -529,7 +616,7 @@ ${cssContent.trim()}
|
|
|
529
616
|
const cssSnippets = cssFiles.filter((f) => cssSnippetMap.has(f)).map((f) => cssSnippetMap.get(f));
|
|
530
617
|
const cssInlineFiles = cssFiles.filter((f) => !cssSnippetMap.has(f));
|
|
531
618
|
const cssInline = readCssFileContents(cssInlineFiles, options.buildDir, options.themeRoot);
|
|
532
|
-
|
|
619
|
+
log5.debug(
|
|
533
620
|
"compiling %s (type=%s, css inline=%d, css snippets=%d)",
|
|
534
621
|
entry.kebabName,
|
|
535
622
|
entry.targetType,
|
|
@@ -538,10 +625,10 @@ ${cssContent.trim()}
|
|
|
538
625
|
);
|
|
539
626
|
await compileEntry(entry, options, manifest, projectRoot, sourceDir, cssInline, cssSnippets);
|
|
540
627
|
} catch (err) {
|
|
541
|
-
|
|
628
|
+
log5.error("Failed to compile %s:", entry.filePath, err);
|
|
542
629
|
}
|
|
543
630
|
}
|
|
544
|
-
|
|
631
|
+
log5.info("Compiled %d entries", entries.length);
|
|
545
632
|
const tmpDir = path6.join(sourceDir, ".ssg-tmp");
|
|
546
633
|
try {
|
|
547
634
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
@@ -556,26 +643,28 @@ async function compileEntry(entry, options, manifest, projectRoot, sourceDir, cs
|
|
|
556
643
|
createElement = projectRequire("react").createElement;
|
|
557
644
|
renderToStaticMarkup = projectRequire("react-dom/server").renderToStaticMarkup;
|
|
558
645
|
} catch {
|
|
559
|
-
|
|
646
|
+
log5.warn("react/react-dom not found, skipping SSR for %s", entry.kebabName);
|
|
560
647
|
return;
|
|
561
648
|
}
|
|
562
649
|
const sourceCode = fs.readFileSync(entry.filePath, "utf-8");
|
|
650
|
+
const { result: fixedSource, fixCount } = autoFixAdjacentText(sourceCode, entry.filePath);
|
|
651
|
+
const finalSource = fixCount > 0 ? fixedSource : sourceCode;
|
|
563
652
|
let esbuild;
|
|
564
653
|
try {
|
|
565
654
|
esbuild = projectRequire("esbuild");
|
|
566
655
|
} catch {
|
|
567
|
-
|
|
656
|
+
log5.warn("esbuild not found, skipping SSR for %s", entry.kebabName);
|
|
568
657
|
return;
|
|
569
658
|
}
|
|
570
659
|
const ts = Date.now();
|
|
571
660
|
const tmpDir = path6.join(sourceDir, ".ssg-tmp");
|
|
572
661
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
573
662
|
const tmpFile = path6.join(tmpDir, `.ssg-entry-${ts}.mjs`);
|
|
574
|
-
|
|
663
|
+
log5.debug("bundling %s via esbuild", entry.kebabName);
|
|
575
664
|
const startBundled = Date.now();
|
|
576
665
|
await esbuild.build({
|
|
577
666
|
stdin: {
|
|
578
|
-
contents:
|
|
667
|
+
contents: finalSource,
|
|
579
668
|
resolveDir: path6.dirname(entry.filePath),
|
|
580
669
|
loader: path6.extname(entry.filePath).slice(1)
|
|
581
670
|
},
|
|
@@ -594,6 +683,22 @@ async function compileEntry(entry, options, manifest, projectRoot, sourceDir, cs
|
|
|
594
683
|
write: true,
|
|
595
684
|
allowOverwrite: true,
|
|
596
685
|
plugins: [
|
|
686
|
+
{
|
|
687
|
+
name: "ssg-hydration-fix",
|
|
688
|
+
setup(build) {
|
|
689
|
+
build.onLoad({ filter: /\.(tsx|jsx)$/ }, (args) => {
|
|
690
|
+
try {
|
|
691
|
+
const source = fs.readFileSync(args.path, "utf-8");
|
|
692
|
+
const { result, fixCount: fixCount2 } = autoFixAdjacentText(source, args.path);
|
|
693
|
+
if (fixCount2 > 0) {
|
|
694
|
+
return { contents: result, loader: args.path.endsWith(".tsx") ? "tsx" : "jsx" };
|
|
695
|
+
}
|
|
696
|
+
} catch {
|
|
697
|
+
}
|
|
698
|
+
return void 0;
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
},
|
|
597
702
|
{
|
|
598
703
|
name: "ssg-strip-css",
|
|
599
704
|
setup(build) {
|
|
@@ -617,29 +722,33 @@ async function compileEntry(entry, options, manifest, projectRoot, sourceDir, cs
|
|
|
617
722
|
}
|
|
618
723
|
]
|
|
619
724
|
});
|
|
620
|
-
|
|
725
|
+
log5.debug("esbuild bundle took %dms", Date.now() - startBundled);
|
|
621
726
|
try {
|
|
622
727
|
const mod = await import(pathToFileURL(tmpFile));
|
|
623
728
|
const Component = mod.default;
|
|
624
729
|
const shopifyMeta = mod.shopifyMeta;
|
|
625
730
|
if (!Component) {
|
|
626
|
-
|
|
731
|
+
log5.warn("No default export found in %s, skipping", entry.filePath);
|
|
627
732
|
return;
|
|
628
733
|
}
|
|
629
734
|
if (shopifyMeta) {
|
|
630
735
|
entry.meta = { ...entry.meta, ...shopifyMeta };
|
|
631
736
|
}
|
|
632
737
|
globalThis.__shopify_ssg_target = entry.targetType;
|
|
738
|
+
const trackedExpressions = /* @__PURE__ */ new Set();
|
|
739
|
+
globalThis.__shopify_ssg_liquid_track = trackedExpressions;
|
|
633
740
|
const element = createElement(Component);
|
|
634
741
|
let html = renderToStaticMarkup(element);
|
|
635
|
-
|
|
742
|
+
delete globalThis.__shopify_ssg_liquid_track;
|
|
743
|
+
html = normalizeVoidElements(html);
|
|
744
|
+
html = normalizeStyleAttributes(html);
|
|
636
745
|
html = unwrapHtmlEntities(html);
|
|
637
746
|
const scriptAsset = resolveScriptAsset(entry.kebabName, manifest);
|
|
638
747
|
const liquidContent = assembleLiquidFile(html, entry, scriptAsset, { inline: cssInline, snippets: cssSnippets }, {
|
|
639
748
|
prefix: options.ssg.prefix,
|
|
640
749
|
outputName: options.ssg.outputName || void 0,
|
|
641
750
|
buildDir: options.buildDir
|
|
642
|
-
});
|
|
751
|
+
}, [...trackedExpressions]);
|
|
643
752
|
const outputPath = getOutputPath(entry, {
|
|
644
753
|
prefix: options.ssg.prefix,
|
|
645
754
|
outputName: options.ssg.outputName || void 0,
|
|
@@ -716,8 +825,8 @@ function pathToFileURL(filePath) {
|
|
|
716
825
|
return "file://" + absPath;
|
|
717
826
|
}
|
|
718
827
|
|
|
719
|
-
// src/ssg/index.ts
|
|
720
|
-
var
|
|
828
|
+
// src/plugin/ssg/index.ts
|
|
829
|
+
var log6 = logger("ssg");
|
|
721
830
|
function shopifySSG(options) {
|
|
722
831
|
return {
|
|
723
832
|
name: "vite-plugin-shopify:ssg",
|
|
@@ -733,16 +842,16 @@ function shopifySSG(options) {
|
|
|
733
842
|
"manifest.json"
|
|
734
843
|
);
|
|
735
844
|
if (!fs2.existsSync(manifestPath)) {
|
|
736
|
-
|
|
845
|
+
log6.warn("No manifest.json found, skipping SSG");
|
|
737
846
|
return;
|
|
738
847
|
}
|
|
739
|
-
|
|
848
|
+
log6.debug("reading manifest from %s", manifestPath);
|
|
740
849
|
const manifest = JSON.parse(fs2.readFileSync(manifestPath, "utf-8"));
|
|
741
|
-
|
|
850
|
+
log6.info("Starting SSG compilation...");
|
|
742
851
|
await compileAllEntries(options, manifest);
|
|
743
|
-
|
|
852
|
+
log6.info("SSG compilation complete");
|
|
744
853
|
writeImportMapSnippet(options);
|
|
745
|
-
|
|
854
|
+
log6.debug("wrote import map snippet");
|
|
746
855
|
},
|
|
747
856
|
resolveId(id) {
|
|
748
857
|
if (id === "vite-plugin-shopify/runtime") {
|
|
@@ -751,7 +860,11 @@ function shopifySSG(options) {
|
|
|
751
860
|
},
|
|
752
861
|
load(id) {
|
|
753
862
|
if (id === "\0vite-plugin-shopify:runtime") {
|
|
754
|
-
|
|
863
|
+
const exports = [
|
|
864
|
+
`export { LiquidDataProvider, LiquidDataContext } from 'vite-plugin-shopify/runtime'`,
|
|
865
|
+
`export { useLiquid, useLiquidValues, useSectionSettings, useBlockSettings, useSnippetParams, useBlockParams } from 'vite-plugin-shopify/runtime'`
|
|
866
|
+
];
|
|
867
|
+
return exports.join("\n");
|
|
755
868
|
}
|
|
756
869
|
}
|
|
757
870
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
declare function useLiquid(expr: string): {
|
|
4
|
+
value: string | undefined;
|
|
5
|
+
};
|
|
6
|
+
declare function useLiquidValues<T extends Record<string, string>>(map: T): {
|
|
7
|
+
values: {
|
|
8
|
+
[K in keyof T]: string | undefined;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
declare function useSectionSettings(key: string): {
|
|
12
|
+
value: string | undefined;
|
|
13
|
+
};
|
|
14
|
+
declare function useBlockSettings(key: string): {
|
|
15
|
+
value: string | undefined;
|
|
16
|
+
};
|
|
17
|
+
declare function useSnippetParams(key: string): {
|
|
18
|
+
value: string | undefined;
|
|
19
|
+
};
|
|
20
|
+
declare function useBlockParams(key: string): {
|
|
21
|
+
value: string | undefined;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** SSR-safe boolean parser: treats Liquid expression strings as truthy, real booleans as-is */
|
|
25
|
+
declare function parseLiquidBoolean(value: string | boolean | undefined | null): boolean;
|
|
26
|
+
/** SSR-safe number parser: returns defaultVal for unparseable SSR placeholders */
|
|
27
|
+
declare function parseLiquidNumber(value: string | number | undefined | null, defaultVal?: number): number;
|
|
28
|
+
|
|
29
|
+
declare const LiquidDataContext: react.Context<Record<string, any>>;
|
|
30
|
+
declare const LiquidDataProvider: react.Provider<Record<string, any>>;
|
|
31
|
+
|
|
32
|
+
export { LiquidDataContext, LiquidDataProvider, parseLiquidBoolean, parseLiquidNumber, useBlockParams, useBlockSettings, useLiquid, useLiquidValues, useSectionSettings, useSnippetParams };
|