vite-plugin-react-shopify 1.1.0 → 2.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/README.md +205 -288
- package/dist/index.d.ts +35 -13
- package/dist/index.js +558 -359
- package/dist/runtime/index.d.ts +32 -0
- package/dist/runtime/index.js +73 -0
- package/package.json +14 -26
- 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/core/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/core/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/core/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,7 +146,7 @@ function shopifyConfig(options) {
|
|
|
134
146
|
};
|
|
135
147
|
}
|
|
136
148
|
|
|
137
|
-
// src/entries.ts
|
|
149
|
+
// src/core/entries.ts
|
|
138
150
|
import path4 from "path";
|
|
139
151
|
import { normalizePath as normalizePath2 } from "vite";
|
|
140
152
|
|
|
@@ -145,7 +157,8 @@ 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);
|
|
@@ -164,7 +177,7 @@ function scanEntries(options) {
|
|
|
164
177
|
componentName,
|
|
165
178
|
kebabName,
|
|
166
179
|
targetType,
|
|
167
|
-
meta: { name:
|
|
180
|
+
meta: { name: deriveName(fileName) }
|
|
168
181
|
});
|
|
169
182
|
}
|
|
170
183
|
}
|
|
@@ -173,8 +186,64 @@ function scanEntries(options) {
|
|
|
173
186
|
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
|
}
|
|
189
|
+
function deriveName(fileName) {
|
|
190
|
+
const readable = fileName.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2").replace(/[-_]/g, " ").replace(/\s+/g, " ").trim();
|
|
191
|
+
return readable.length > 25 ? readable.slice(0, 25) : readable;
|
|
192
|
+
}
|
|
176
193
|
|
|
177
|
-
// src/
|
|
194
|
+
// src/core/entry-template.ts
|
|
195
|
+
function generateEntryModule(entry, componentRel) {
|
|
196
|
+
const { kebabName } = entry;
|
|
197
|
+
return [
|
|
198
|
+
`import { createElement } from 'react'`,
|
|
199
|
+
`import Component from '~/${componentRel}'`,
|
|
200
|
+
`import { hydrateRoot } from 'react-dom/client'`,
|
|
201
|
+
`import { LiquidDataProvider } from 'vite-plugin-react-shopify/runtime'`,
|
|
202
|
+
``,
|
|
203
|
+
`const SELECTOR = '[data-ssg-component="${kebabName}"]'`,
|
|
204
|
+
`const roots = new Map()`,
|
|
205
|
+
``,
|
|
206
|
+
`function readLiquidData(el) {`,
|
|
207
|
+
` const script = el.querySelector(':scope > script[data-ssg-liquid]')`,
|
|
208
|
+
` if (!script) return {}`,
|
|
209
|
+
` try { return JSON.parse(script.textContent || '{}') } catch { return {} }`,
|
|
210
|
+
`}`,
|
|
211
|
+
``,
|
|
212
|
+
`function hydrate(el) {`,
|
|
213
|
+
` const h = el.querySelector(':scope > [data-ssg-hydrate]') || (el.matches('[data-ssg-hydrate]') ? el : null)`,
|
|
214
|
+
` if (!h || roots.has(h)) return`,
|
|
215
|
+
` const liquidData = readLiquidData(el)`,
|
|
216
|
+
` roots.set(h, hydrateRoot(h, createElement(LiquidDataProvider, { value: liquidData }, createElement(Component))))`,
|
|
217
|
+
`}`,
|
|
218
|
+
``,
|
|
219
|
+
`function unmount(el) {`,
|
|
220
|
+
` const h = el.querySelector(':scope > [data-ssg-hydrate]') || (el.matches('[data-ssg-hydrate]') ? el : null)`,
|
|
221
|
+
` if (h && roots.has(h)) { roots.get(h).unmount(); roots.delete(h) }`,
|
|
222
|
+
`}`,
|
|
223
|
+
``,
|
|
224
|
+
`function scan(target) {`,
|
|
225
|
+
` if (target.matches?.(SELECTOR)) hydrate(target)`,
|
|
226
|
+
` target.querySelectorAll(SELECTOR).forEach(hydrate)`,
|
|
227
|
+
`}`,
|
|
228
|
+
``,
|
|
229
|
+
`function sweep(target) {`,
|
|
230
|
+
` if (target.matches?.(SELECTOR)) unmount(target)`,
|
|
231
|
+
` target.querySelectorAll(SELECTOR).forEach(unmount)`,
|
|
232
|
+
`}`,
|
|
233
|
+
``,
|
|
234
|
+
`scan(document)`,
|
|
235
|
+
``,
|
|
236
|
+
`document.addEventListener('shopify:section:load', (e) => {`,
|
|
237
|
+
` scan(e.target)`,
|
|
238
|
+
`})`,
|
|
239
|
+
``,
|
|
240
|
+
`document.addEventListener('shopify:section:unload', (e) => {`,
|
|
241
|
+
` sweep(e.target)`,
|
|
242
|
+
`})`
|
|
243
|
+
].join("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/core/entries.ts
|
|
178
247
|
var log2 = logger("entries");
|
|
179
248
|
function shopifyEntries(options) {
|
|
180
249
|
let entries = [];
|
|
@@ -210,74 +279,316 @@ function shopifyEntries(options) {
|
|
|
210
279
|
if (!entry) return;
|
|
211
280
|
const sourceDir = path4.resolve(options.themeRoot, options.sourceCodeDir);
|
|
212
281
|
const componentRel = normalizePath2(path4.relative(sourceDir, entry.filePath));
|
|
213
|
-
return
|
|
214
|
-
`import { createElement } from 'react'`,
|
|
215
|
-
`import Component from '~/${componentRel}'`,
|
|
216
|
-
`import { hydrateRoot } from 'react-dom/client'`,
|
|
217
|
-
`import { SettingsProvider } from 'vite-plugin-react-shopify/runtime/settings'`,
|
|
218
|
-
`import { ParamsProvider } from 'vite-plugin-react-shopify/runtime/settings'`,
|
|
219
|
-
``,
|
|
220
|
-
`const SELECTOR = '[data-ssg-component="${kebabName}"]'`,
|
|
221
|
-
`const roots = new Map()`,
|
|
222
|
-
``,
|
|
223
|
-
`function hydrate(el) {`,
|
|
224
|
-
` const h = el.querySelector(':scope > [data-ssg-hydrate]') || (el.matches('[data-ssg-hydrate]') ? el : null)`,
|
|
225
|
-
` if (!h || roots.has(h)) return`,
|
|
226
|
-
` const propsEl = el.querySelector(':scope > script[data-ssg-props]')`,
|
|
227
|
-
` const props = propsEl ? JSON.parse(propsEl.textContent || '{}') : {}`,
|
|
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)))))`,
|
|
231
|
-
`}`,
|
|
232
|
-
``,
|
|
233
|
-
`function unmount(el) {`,
|
|
234
|
-
` const h = el.querySelector(':scope > [data-ssg-hydrate]') || (el.matches('[data-ssg-hydrate]') ? el : null)`,
|
|
235
|
-
` if (h && roots.has(h)) { roots.get(h).unmount(); roots.delete(h) }`,
|
|
236
|
-
`}`,
|
|
237
|
-
``,
|
|
238
|
-
`function scan(target) {`,
|
|
239
|
-
` if (target.matches?.(SELECTOR)) hydrate(target)`,
|
|
240
|
-
` target.querySelectorAll(SELECTOR).forEach(hydrate)`,
|
|
241
|
-
`}`,
|
|
242
|
-
``,
|
|
243
|
-
`function sweep(target) {`,
|
|
244
|
-
` if (target.matches?.(SELECTOR)) unmount(target)`,
|
|
245
|
-
` target.querySelectorAll(SELECTOR).forEach(unmount)`,
|
|
246
|
-
`}`,
|
|
247
|
-
``,
|
|
248
|
-
`scan(document)`,
|
|
249
|
-
``,
|
|
250
|
-
`document.addEventListener('shopify:section:load', (e) => {`,
|
|
251
|
-
` scan(e.target)`,
|
|
252
|
-
`})`,
|
|
253
|
-
``,
|
|
254
|
-
`document.addEventListener('shopify:section:unload', (e) => {`,
|
|
255
|
-
` sweep(e.target)`,
|
|
256
|
-
`})`
|
|
257
|
-
].join("\n");
|
|
282
|
+
return generateEntryModule(entry, componentRel);
|
|
258
283
|
}
|
|
259
284
|
};
|
|
260
285
|
}
|
|
261
286
|
|
|
262
287
|
// src/ssg/index.ts
|
|
263
|
-
import
|
|
264
|
-
import
|
|
288
|
+
import fs4 from "fs";
|
|
289
|
+
import path10 from "path";
|
|
265
290
|
|
|
266
291
|
// src/ssg/compiler.ts
|
|
292
|
+
import fs3 from "fs";
|
|
293
|
+
import path9 from "path";
|
|
294
|
+
|
|
295
|
+
// src/ssg/css-manager.ts
|
|
267
296
|
import fs from "fs";
|
|
297
|
+
import path5 from "path";
|
|
298
|
+
var log3 = logger("ssg:css");
|
|
299
|
+
function collectCssFiles(manifestKey, manifest) {
|
|
300
|
+
const collected = /* @__PURE__ */ new Set();
|
|
301
|
+
const visited = /* @__PURE__ */ new Set();
|
|
302
|
+
collectCssFilesRecursive(manifestKey, manifest, collected, visited);
|
|
303
|
+
return [...collected];
|
|
304
|
+
}
|
|
305
|
+
function collectCssFilesRecursive(chunkKey, manifest, collected, visited) {
|
|
306
|
+
if (visited.has(chunkKey)) return;
|
|
307
|
+
visited.add(chunkKey);
|
|
308
|
+
const chunk = manifest[chunkKey];
|
|
309
|
+
if (!chunk) return;
|
|
310
|
+
if (chunk.css && Array.isArray(chunk.css)) {
|
|
311
|
+
for (const cssFile of chunk.css) {
|
|
312
|
+
collected.add(cssFile);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (chunk.imports && Array.isArray(chunk.imports)) {
|
|
316
|
+
for (const imported of chunk.imports) {
|
|
317
|
+
collectCssFilesRecursive(imported, manifest, collected, visited);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
function readCssFileContents(cssFiles, buildDir, themeRoot) {
|
|
322
|
+
const assetsDir = path5.resolve(themeRoot, buildDir);
|
|
323
|
+
return cssFiles.map((file) => {
|
|
324
|
+
try {
|
|
325
|
+
return fs.readFileSync(path5.join(assetsDir, file), "utf-8");
|
|
326
|
+
} catch {
|
|
327
|
+
return "";
|
|
328
|
+
}
|
|
329
|
+
}).filter(Boolean);
|
|
330
|
+
}
|
|
331
|
+
function getCssBaseName(cssFile) {
|
|
332
|
+
const name = cssFile.replace(/\.css$/, "");
|
|
333
|
+
const lastHyphen = name.lastIndexOf("-");
|
|
334
|
+
if (lastHyphen > 0) {
|
|
335
|
+
const possibleHash = name.slice(lastHyphen + 1);
|
|
336
|
+
if (/^[A-Za-z0-9_-]{8,}$/.test(possibleHash)) {
|
|
337
|
+
return name.slice(0, lastHyphen);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return name;
|
|
341
|
+
}
|
|
342
|
+
function analyzeCssDistribution(entries, manifest) {
|
|
343
|
+
const entryCssFiles = /* @__PURE__ */ new Map();
|
|
344
|
+
const cssRefCount = /* @__PURE__ */ new Map();
|
|
345
|
+
for (const entry of entries) {
|
|
346
|
+
const manifestKey = `shopify:entry:${entry.kebabName}`;
|
|
347
|
+
const files = collectCssFiles(manifestKey, manifest);
|
|
348
|
+
entryCssFiles.set(entry.kebabName, files);
|
|
349
|
+
for (const f of files) {
|
|
350
|
+
cssRefCount.set(f, (cssRefCount.get(f) || 0) + 1);
|
|
351
|
+
}
|
|
352
|
+
log3.debug("entry %s has %d CSS files", entry.kebabName, files.length);
|
|
353
|
+
}
|
|
354
|
+
return { entryCssFiles, cssRefCount };
|
|
355
|
+
}
|
|
356
|
+
function generateSharedCssSnippets(cssRefCount, options) {
|
|
357
|
+
const cssSnippetMap = /* @__PURE__ */ new Map();
|
|
358
|
+
for (const [cssFile, count] of cssRefCount) {
|
|
359
|
+
if (count > 1) {
|
|
360
|
+
const snippetName = `${options.ssg.cssPrefix}-${getCssBaseName(cssFile)}`;
|
|
361
|
+
cssSnippetMap.set(cssFile, snippetName);
|
|
362
|
+
const snippetPath = path5.join(
|
|
363
|
+
path5.resolve(options.themeRoot),
|
|
364
|
+
"snippets",
|
|
365
|
+
`${snippetName}.liquid`
|
|
366
|
+
);
|
|
367
|
+
const cssPath = path5.join(
|
|
368
|
+
path5.resolve(options.themeRoot, options.buildDir),
|
|
369
|
+
cssFile
|
|
370
|
+
);
|
|
371
|
+
try {
|
|
372
|
+
const cssContent = fs.readFileSync(cssPath, "utf-8");
|
|
373
|
+
fs.mkdirSync(path5.dirname(snippetPath), { recursive: true });
|
|
374
|
+
fs.writeFileSync(snippetPath, `{% stylesheet %}
|
|
375
|
+
${cssContent.trim()}
|
|
376
|
+
{% endstylesheet %}
|
|
377
|
+
`);
|
|
378
|
+
log3.debug("generated shared CSS snippet %s (used by %d entries)", snippetName, count);
|
|
379
|
+
} catch {
|
|
380
|
+
log3.warn("failed to write CSS snippet for %s", cssFile);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return cssSnippetMap;
|
|
385
|
+
}
|
|
386
|
+
function categorizeCss(cssFiles, cssSnippetMap) {
|
|
387
|
+
const snippets = cssFiles.filter((f) => cssSnippetMap.has(f)).map((f) => cssSnippetMap.get(f));
|
|
388
|
+
const inlineFiles = cssFiles.filter((f) => !cssSnippetMap.has(f));
|
|
389
|
+
return { inline: inlineFiles, snippets };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// src/ssg/bundler.ts
|
|
393
|
+
import fs2 from "fs";
|
|
268
394
|
import path6 from "path";
|
|
269
395
|
import { createRequire } from "module";
|
|
270
396
|
|
|
397
|
+
// src/ssg/hydration-fix.ts
|
|
398
|
+
var log4 = logger("hydration-fix");
|
|
399
|
+
function autoFixAdjacentText(source, filePath) {
|
|
400
|
+
let fixCount = 0;
|
|
401
|
+
const lines = source.split("\n");
|
|
402
|
+
const fixed = [];
|
|
403
|
+
for (let i = 0; i < lines.length; i++) {
|
|
404
|
+
const line = lines[i];
|
|
405
|
+
const replaced = line.replace(
|
|
406
|
+
/<(\w+)([^>]*?)>([^<]*?\{[^}]*\}[^<]*?)<\/\1>/g,
|
|
407
|
+
(match, tagName, attrs, content) => {
|
|
408
|
+
const trimmed = content.trim();
|
|
409
|
+
if (!needsFix(trimmed)) return match;
|
|
410
|
+
fixCount++;
|
|
411
|
+
const tpl = trimmed.replace(/\{([^}]+)\}/g, "${$1}");
|
|
412
|
+
return `<${tagName}${attrs}>{\`${tpl}\`}</${tagName}>`;
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
fixed.push(replaced);
|
|
416
|
+
}
|
|
417
|
+
if (fixCount > 0) {
|
|
418
|
+
log4.warn(
|
|
419
|
+
`auto-fixed ${fixCount} adjacent text+expression issue(s) in ${filePath}`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
return { result: fixed.join("\n"), fixCount };
|
|
423
|
+
}
|
|
424
|
+
function needsFix(content) {
|
|
425
|
+
const trimmed = content.trim();
|
|
426
|
+
if (!trimmed) return false;
|
|
427
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
428
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
429
|
+
if (inner.startsWith("`") && inner.endsWith("`")) return false;
|
|
430
|
+
if (inner.length > 0 && !/<[a-zA-Z]/.test(inner)) return false;
|
|
431
|
+
}
|
|
432
|
+
if (!/\{/.test(trimmed)) return false;
|
|
433
|
+
if (/<[a-zA-Z]/.test(trimmed)) return false;
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/ssg/bundler.ts
|
|
438
|
+
var log5 = logger("ssg:bundler");
|
|
439
|
+
async function bundleEntry(entry, projectRoot, sourceDir) {
|
|
440
|
+
const projectRequire = createRequire(path6.join(projectRoot, "package.json"));
|
|
441
|
+
let esbuild;
|
|
442
|
+
try {
|
|
443
|
+
esbuild = projectRequire("esbuild");
|
|
444
|
+
} catch {
|
|
445
|
+
log5.warn("esbuild not found, skipping SSR for %s", entry.kebabName);
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
const sourceCode = fs2.readFileSync(entry.filePath, "utf-8");
|
|
449
|
+
const { result: fixedSource, fixCount } = autoFixAdjacentText(sourceCode, entry.filePath);
|
|
450
|
+
const finalSource = fixCount > 0 ? fixedSource : sourceCode;
|
|
451
|
+
const ts = Date.now();
|
|
452
|
+
const tmpDir = path6.join(sourceDir, ".ssg-tmp");
|
|
453
|
+
fs2.mkdirSync(tmpDir, { recursive: true });
|
|
454
|
+
const tmpFile = path6.join(tmpDir, `.ssg-entry-${ts}.mjs`);
|
|
455
|
+
log5.debug("bundling %s via esbuild", entry.kebabName);
|
|
456
|
+
const startBundled = Date.now();
|
|
457
|
+
await esbuild.build({
|
|
458
|
+
stdin: {
|
|
459
|
+
contents: finalSource,
|
|
460
|
+
resolveDir: path6.dirname(entry.filePath),
|
|
461
|
+
loader: path6.extname(entry.filePath).slice(1)
|
|
462
|
+
},
|
|
463
|
+
outfile: tmpFile,
|
|
464
|
+
bundle: true,
|
|
465
|
+
format: "esm",
|
|
466
|
+
jsx: "automatic",
|
|
467
|
+
platform: "node",
|
|
468
|
+
external: [
|
|
469
|
+
"react",
|
|
470
|
+
"react-dom",
|
|
471
|
+
"react-dom/*",
|
|
472
|
+
"vite-plugin-react-shopify",
|
|
473
|
+
"vite-plugin-react-shopify/*"
|
|
474
|
+
],
|
|
475
|
+
write: true,
|
|
476
|
+
allowOverwrite: true,
|
|
477
|
+
plugins: [
|
|
478
|
+
{
|
|
479
|
+
name: "ssg-hydration-fix",
|
|
480
|
+
setup(build) {
|
|
481
|
+
build.onLoad({ filter: /\.(tsx|jsx)$/ }, (args) => {
|
|
482
|
+
try {
|
|
483
|
+
const source = fs2.readFileSync(args.path, "utf-8");
|
|
484
|
+
const { result, fixCount: fixCount2 } = autoFixAdjacentText(source, args.path);
|
|
485
|
+
if (fixCount2 > 0) {
|
|
486
|
+
return { contents: result, loader: args.path.endsWith(".tsx") ? "tsx" : "jsx" };
|
|
487
|
+
}
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
return void 0;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: "ssg-strip-css",
|
|
496
|
+
setup(build) {
|
|
497
|
+
build.onResolve({ filter: /\.module\.css$/ }, (args) => ({
|
|
498
|
+
namespace: "ssg-css-module",
|
|
499
|
+
path: args.path
|
|
500
|
+
}));
|
|
501
|
+
build.onResolve({ filter: /\.css$/ }, (args) => ({
|
|
502
|
+
namespace: "ssg-css-plain",
|
|
503
|
+
path: args.path
|
|
504
|
+
}));
|
|
505
|
+
build.onLoad({ filter: /.*/, namespace: "ssg-css-module" }, () => ({
|
|
506
|
+
contents: "export default new Proxy({},{get:(_,k)=>k});",
|
|
507
|
+
loader: "js"
|
|
508
|
+
}));
|
|
509
|
+
build.onLoad({ filter: /.*/, namespace: "ssg-css-plain" }, () => ({
|
|
510
|
+
contents: "",
|
|
511
|
+
loader: "js"
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
]
|
|
516
|
+
});
|
|
517
|
+
log5.debug("esbuild bundle took %dms", Date.now() - startBundled);
|
|
518
|
+
return { tmpFile };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/ssg/renderer.ts
|
|
522
|
+
import path7 from "path";
|
|
523
|
+
import { createRequire as createRequire2 } from "module";
|
|
524
|
+
|
|
271
525
|
// src/ssg/post-process.ts
|
|
272
|
-
var
|
|
273
|
-
function
|
|
274
|
-
return html.replace(
|
|
526
|
+
var VOID_ELEMENTS = /<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)([^>]*)\/>/g;
|
|
527
|
+
function normalizeVoidElements(html) {
|
|
528
|
+
return html.replace(VOID_ELEMENTS, "<$1$2>");
|
|
529
|
+
}
|
|
530
|
+
function normalizeStyleAttributes(html) {
|
|
531
|
+
return html.replace(/ style="([^"]+)"/g, (_match, content) => {
|
|
532
|
+
const normalized = content.replace(/:(\S)/g, ": $1").replace(/;\s*$/, "");
|
|
533
|
+
return ` style="${normalized};"`;
|
|
534
|
+
});
|
|
275
535
|
}
|
|
276
536
|
function unwrapHtmlEntities(html) {
|
|
277
537
|
return html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
278
538
|
}
|
|
279
539
|
|
|
280
|
-
// src/ssg/
|
|
540
|
+
// src/ssg/renderer.ts
|
|
541
|
+
function pathToFileURL(filePath) {
|
|
542
|
+
const absPath = path7.resolve(filePath);
|
|
543
|
+
if (process.platform === "win32") {
|
|
544
|
+
return "file:///" + absPath.replace(/\\/g, "/");
|
|
545
|
+
}
|
|
546
|
+
return "file://" + absPath;
|
|
547
|
+
}
|
|
548
|
+
var log6 = logger("ssg:renderer");
|
|
549
|
+
function renderEntry(tmpFile, entry, projectRoot) {
|
|
550
|
+
return import(pathToFileURL(tmpFile)).then((mod) => {
|
|
551
|
+
const Component = mod.default;
|
|
552
|
+
const shopifyMeta = mod.shopifyMeta;
|
|
553
|
+
if (!Component) {
|
|
554
|
+
log6.warn("No default export found in %s, skipping", entry.filePath);
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
if (shopifyMeta) {
|
|
558
|
+
entry.meta = { ...entry.meta, ...shopifyMeta, name: shopifyMeta.name ?? entry.meta.name };
|
|
559
|
+
}
|
|
560
|
+
const projectRequire = createRequire2(path7.join(projectRoot, "package.json"));
|
|
561
|
+
let createElement;
|
|
562
|
+
let renderToStaticMarkup;
|
|
563
|
+
try {
|
|
564
|
+
createElement = projectRequire("react").createElement;
|
|
565
|
+
renderToStaticMarkup = projectRequire("react-dom/server").renderToStaticMarkup;
|
|
566
|
+
} catch {
|
|
567
|
+
log6.warn("react/react-dom not found, skipping SSR for %s", entry.kebabName);
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
globalThis.__shopify_ssg_target = entry.targetType;
|
|
571
|
+
const trackedExpressions = /* @__PURE__ */ new Set();
|
|
572
|
+
globalThis.__shopify_ssg_liquid_track = trackedExpressions;
|
|
573
|
+
const element = createElement(Component);
|
|
574
|
+
let html = renderToStaticMarkup(element);
|
|
575
|
+
delete globalThis.__shopify_ssg_liquid_track;
|
|
576
|
+
html = normalizeVoidElements(html);
|
|
577
|
+
html = normalizeStyleAttributes(html);
|
|
578
|
+
html = unwrapHtmlEntities(html);
|
|
579
|
+
return { html, trackedExpressions, entryMeta: entry.meta };
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function resolveScriptAsset(kebabName, manifest) {
|
|
583
|
+
const manifestKey = `shopify:entry:${kebabName}`;
|
|
584
|
+
const entryChunk = manifest[manifestKey];
|
|
585
|
+
if (!entryChunk) return null;
|
|
586
|
+
const file = entryChunk.file;
|
|
587
|
+
if (!file) return null;
|
|
588
|
+
return path7.basename(file);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/ssg/schema.ts
|
|
281
592
|
function serializeSetting(setting) {
|
|
282
593
|
const s = { type: setting.type };
|
|
283
594
|
if ("id" in setting) s.id = setting.id;
|
|
@@ -345,10 +656,35 @@ ${json}
|
|
|
345
656
|
`;
|
|
346
657
|
}
|
|
347
658
|
|
|
348
|
-
// src/ssg/liquid.ts
|
|
349
|
-
import
|
|
659
|
+
// src/ssg/liquid-paths.ts
|
|
660
|
+
import path8 from "path";
|
|
661
|
+
function getOutputPath(entry, options) {
|
|
662
|
+
const type = entry.meta.type ?? entry.targetType;
|
|
663
|
+
const dirName = typeToDir(type);
|
|
664
|
+
const fileName = resolveFileName(entry, type, options);
|
|
665
|
+
return path8.join(options.themeRoot, dirName, fileName);
|
|
666
|
+
}
|
|
667
|
+
function typeToDir(type) {
|
|
668
|
+
if (type === "snippet") return "snippets";
|
|
669
|
+
if (type === "block") return "blocks";
|
|
670
|
+
return `${type}s`;
|
|
671
|
+
}
|
|
672
|
+
function getAssetRelativePath(buildDir, filename) {
|
|
673
|
+
if (!buildDir.startsWith("assets/")) return filename;
|
|
674
|
+
const prefix = buildDir.slice("assets/".length);
|
|
675
|
+
return prefix ? `${prefix}/${filename}` : filename;
|
|
676
|
+
}
|
|
677
|
+
function resolveFileName(entry, type, options) {
|
|
678
|
+
if (options.outputName) {
|
|
679
|
+
return options.outputName.replace(/\{type\}/g, type).replace(/\{kebab\}/g, entry.kebabName).replace(/\{pascal\}/g, entry.componentName).replace(/\{target\}/g, entry.targetType) + ".liquid";
|
|
680
|
+
}
|
|
681
|
+
const prefix = options.prefix[type] ?? "react-";
|
|
682
|
+
return `${prefix}${entry.kebabName}.liquid`;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// src/ssg/liquid-assembler.ts
|
|
350
686
|
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) {
|
|
687
|
+
function assembleLiquidFile(html, entry, scriptAsset, cssContents, options, trackedExpressions = []) {
|
|
352
688
|
const type = entry.meta.type ?? entry.targetType;
|
|
353
689
|
const parts = [DISCLAIMER];
|
|
354
690
|
switch (type) {
|
|
@@ -356,13 +692,16 @@ function assembleLiquidFile(html, entry, scriptAsset, cssContents, options) {
|
|
|
356
692
|
parts.push(html);
|
|
357
693
|
break;
|
|
358
694
|
case "section":
|
|
359
|
-
parts.push(...buildSection(html, entry));
|
|
695
|
+
parts.push(...buildSection(html, entry, trackedExpressions));
|
|
360
696
|
break;
|
|
361
697
|
case "block":
|
|
362
|
-
parts.push(...buildBlock(html, entry));
|
|
698
|
+
parts.push(...buildBlock(html, entry, trackedExpressions));
|
|
699
|
+
break;
|
|
700
|
+
case "snippet":
|
|
701
|
+
parts.push(...buildSnippet(html, entry, trackedExpressions));
|
|
363
702
|
break;
|
|
364
703
|
default:
|
|
365
|
-
parts.push(...buildSection(html, entry));
|
|
704
|
+
parts.push(...buildSection(html, entry, trackedExpressions));
|
|
366
705
|
break;
|
|
367
706
|
}
|
|
368
707
|
for (const snippet of cssContents.snippets) {
|
|
@@ -383,21 +722,27 @@ function assembleLiquidFile(html, entry, scriptAsset, cssContents, options) {
|
|
|
383
722
|
`<script type="module" src="{{ '${assetPath}' | asset_url }}"></script>`
|
|
384
723
|
);
|
|
385
724
|
}
|
|
386
|
-
|
|
725
|
+
if (type !== "snippet") {
|
|
726
|
+
parts.push(generateSchema(entry.meta));
|
|
727
|
+
}
|
|
387
728
|
return parts.join("\n") + "\n";
|
|
388
729
|
}
|
|
389
730
|
var hasBlocks = (entry) => !!entry.meta.blocks && entry.meta.blocks.length > 0;
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
731
|
+
function buildLiquidBridge(trackedExpressions) {
|
|
732
|
+
if (trackedExpressions.length === 0) return "";
|
|
733
|
+
const entries = trackedExpressions.map((expr, i) => {
|
|
734
|
+
const comma = i < trackedExpressions.length - 1 ? "," : "";
|
|
735
|
+
return ` "${expr}": {{ ${expr} | json }}${comma}`;
|
|
736
|
+
});
|
|
737
|
+
return [
|
|
738
|
+
' <script type="application/json" data-ssg-liquid>',
|
|
739
|
+
" {",
|
|
740
|
+
entries.join("\n"),
|
|
741
|
+
" }",
|
|
742
|
+
" </script>"
|
|
743
|
+
].join("\n");
|
|
399
744
|
}
|
|
400
|
-
function buildSection(html, entry) {
|
|
745
|
+
function buildSection(html, entry, trackedExpressions) {
|
|
401
746
|
const tag = entry.meta.tag ?? "div";
|
|
402
747
|
const cls = entry.meta.class ?? "";
|
|
403
748
|
const lines = [
|
|
@@ -408,23 +753,17 @@ function buildSection(html, entry) {
|
|
|
408
753
|
` data-ssg-component="${entry.kebabName}"`
|
|
409
754
|
];
|
|
410
755
|
if (cls) lines.push(` class="${cls}"`);
|
|
756
|
+
lines.push(`>`);
|
|
757
|
+
const liquidBridge = buildLiquidBridge(trackedExpressions);
|
|
758
|
+
if (liquidBridge) lines.push(liquidBridge);
|
|
411
759
|
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>`
|
|
760
|
+
` <div data-ssg-hydrate>${html}</div>`
|
|
422
761
|
);
|
|
423
762
|
if (hasBlocks(entry)) lines.push(` {% content_for 'blocks' %}`);
|
|
424
763
|
lines.push(`</${tag}>`);
|
|
425
764
|
return lines;
|
|
426
765
|
}
|
|
427
|
-
function buildBlock(html, entry) {
|
|
766
|
+
function buildBlock(html, entry, trackedExpressions) {
|
|
428
767
|
const tag = entry.meta.tag ?? "div";
|
|
429
768
|
const cls = entry.meta.class ?? "";
|
|
430
769
|
const lines = [
|
|
@@ -442,282 +781,138 @@ function buildBlock(html, entry) {
|
|
|
442
781
|
if (cls) lines.push(` class="${cls}"`);
|
|
443
782
|
lines.push(
|
|
444
783
|
` {{ block.shopify_attributes }}`,
|
|
445
|
-
|
|
446
|
-
SETTINGS_BLOCK
|
|
784
|
+
`>`
|
|
447
785
|
);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
786
|
+
const liquidBridge = buildLiquidBridge(trackedExpressions);
|
|
787
|
+
if (liquidBridge) lines.push(liquidBridge);
|
|
451
788
|
lines.push(
|
|
452
|
-
` <div data-ssg-hydrate
|
|
453
|
-
` ${html}`,
|
|
454
|
-
` </div>`
|
|
789
|
+
` <div data-ssg-hydrate>${html}</div>`
|
|
455
790
|
);
|
|
456
791
|
if (hasBlocks(entry)) lines.push(` {% content_for 'blocks' %}`);
|
|
457
792
|
lines.push(`</${tag}>`);
|
|
458
793
|
return lines;
|
|
459
794
|
}
|
|
460
|
-
function
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
795
|
+
function buildSnippet(html, entry, trackedExpressions) {
|
|
796
|
+
const lines = [
|
|
797
|
+
"",
|
|
798
|
+
`<div data-ssg-component="${entry.kebabName}">`
|
|
799
|
+
];
|
|
800
|
+
const liquidBridge = buildLiquidBridge(trackedExpressions);
|
|
801
|
+
if (liquidBridge) lines.push(liquidBridge);
|
|
802
|
+
lines.push(
|
|
803
|
+
` <div data-ssg-hydrate>`,
|
|
804
|
+
` ${html}`,
|
|
805
|
+
` </div>`,
|
|
806
|
+
`</div>`
|
|
807
|
+
);
|
|
808
|
+
return lines;
|
|
465
809
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
810
|
+
|
|
811
|
+
// src/validate/rules.ts
|
|
812
|
+
var MAX_NAME_LENGTH = 25;
|
|
813
|
+
function checkNameLength(meta, kebabName) {
|
|
814
|
+
if (meta.name.length > MAX_NAME_LENGTH) {
|
|
815
|
+
return `[${kebabName}] shopifyMeta.name "${meta.name}" is ${meta.name.length} chars (Shopify limit: ${MAX_NAME_LENGTH})`;
|
|
816
|
+
}
|
|
817
|
+
return null;
|
|
470
818
|
}
|
|
471
|
-
function
|
|
472
|
-
if (
|
|
473
|
-
|
|
819
|
+
function checkEmptyStringDefault(setting) {
|
|
820
|
+
if (setting.default === "") {
|
|
821
|
+
const label = "id" in setting && setting.id ? setting.id : "(no id)";
|
|
822
|
+
return `Setting "${label}" (type: ${setting.type}) has empty string default`;
|
|
474
823
|
}
|
|
475
|
-
|
|
476
|
-
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/validate/index.ts
|
|
828
|
+
var log7 = logger("validate");
|
|
829
|
+
function validateShopifyMeta(meta, context) {
|
|
830
|
+
const warnings = [];
|
|
831
|
+
const nameWarning = checkNameLength(meta, context.kebabName);
|
|
832
|
+
if (nameWarning) warnings.push(nameWarning);
|
|
833
|
+
if (meta.settings) {
|
|
834
|
+
for (const s of meta.settings) {
|
|
835
|
+
const w = checkEmptyStringDefault(s);
|
|
836
|
+
if (w) warnings.push(w);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
for (const w of warnings) {
|
|
840
|
+
log7.warn(w);
|
|
841
|
+
}
|
|
842
|
+
return warnings;
|
|
477
843
|
}
|
|
478
844
|
|
|
479
845
|
// src/ssg/compiler.ts
|
|
480
|
-
var
|
|
481
|
-
var SNIPPET_PREFIX = "react-css";
|
|
846
|
+
var log8 = logger("ssg:compiler");
|
|
482
847
|
async function compileAllEntries(options, manifest) {
|
|
483
848
|
const entries = scanEntries(options);
|
|
484
849
|
if (entries.length === 0) return;
|
|
485
|
-
|
|
486
|
-
const projectRoot =
|
|
487
|
-
const sourceDir =
|
|
488
|
-
const entryCssFiles
|
|
489
|
-
const
|
|
490
|
-
for (const entry of entries) {
|
|
491
|
-
const manifestKey = `shopify:entry:${entry.kebabName}`;
|
|
492
|
-
const files = collectCssFiles(manifestKey, manifest);
|
|
493
|
-
entryCssFiles.set(entry.kebabName, files);
|
|
494
|
-
for (const f of files) {
|
|
495
|
-
cssRefCount.set(f, (cssRefCount.get(f) || 0) + 1);
|
|
496
|
-
}
|
|
497
|
-
log3.debug("entry %s has %d CSS files", entry.kebabName, files.length);
|
|
498
|
-
}
|
|
499
|
-
const cssSnippetMap = /* @__PURE__ */ new Map();
|
|
500
|
-
for (const [cssFile, count] of cssRefCount) {
|
|
501
|
-
if (count > 1) {
|
|
502
|
-
const snippetName = `${SNIPPET_PREFIX}-${getCssBaseName(cssFile)}`;
|
|
503
|
-
cssSnippetMap.set(cssFile, snippetName);
|
|
504
|
-
const snippetPath = path6.join(
|
|
505
|
-
path6.resolve(options.themeRoot),
|
|
506
|
-
"snippets",
|
|
507
|
-
`${snippetName}.liquid`
|
|
508
|
-
);
|
|
509
|
-
const cssPath = path6.join(
|
|
510
|
-
path6.resolve(options.themeRoot, options.buildDir),
|
|
511
|
-
cssFile
|
|
512
|
-
);
|
|
513
|
-
try {
|
|
514
|
-
const cssContent = fs.readFileSync(cssPath, "utf-8");
|
|
515
|
-
fs.mkdirSync(path6.dirname(snippetPath), { recursive: true });
|
|
516
|
-
fs.writeFileSync(snippetPath, `{% stylesheet %}
|
|
517
|
-
${cssContent.trim()}
|
|
518
|
-
{% endstylesheet %}
|
|
519
|
-
`);
|
|
520
|
-
log3.debug("generated shared CSS snippet %s (used by %d entries)", snippetName, count);
|
|
521
|
-
} catch {
|
|
522
|
-
log3.warn("failed to write CSS snippet for %s", cssFile);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
850
|
+
log8.debug("found %d entries to compile", entries.length);
|
|
851
|
+
const projectRoot = path9.resolve(options.themeRoot);
|
|
852
|
+
const sourceDir = path9.resolve(options.themeRoot, options.sourceCodeDir);
|
|
853
|
+
const { entryCssFiles, cssRefCount } = analyzeCssDistribution(entries, manifest);
|
|
854
|
+
const cssSnippetMap = generateSharedCssSnippets(cssRefCount, options);
|
|
526
855
|
for (const entry of entries) {
|
|
527
856
|
try {
|
|
528
|
-
|
|
529
|
-
const cssSnippets = cssFiles.filter((f) => cssSnippetMap.has(f)).map((f) => cssSnippetMap.get(f));
|
|
530
|
-
const cssInlineFiles = cssFiles.filter((f) => !cssSnippetMap.has(f));
|
|
531
|
-
const cssInline = readCssFileContents(cssInlineFiles, options.buildDir, options.themeRoot);
|
|
532
|
-
log3.debug(
|
|
533
|
-
"compiling %s (type=%s, css inline=%d, css snippets=%d)",
|
|
534
|
-
entry.kebabName,
|
|
535
|
-
entry.targetType,
|
|
536
|
-
cssInline.length,
|
|
537
|
-
cssSnippets.length
|
|
538
|
-
);
|
|
539
|
-
await compileEntry(entry, options, manifest, projectRoot, sourceDir, cssInline, cssSnippets);
|
|
857
|
+
await compileEntry(entry, options, manifest, projectRoot, sourceDir, entryCssFiles, cssSnippetMap);
|
|
540
858
|
} catch (err) {
|
|
541
|
-
|
|
859
|
+
log8.error("Failed to compile %s:", entry.filePath, err);
|
|
542
860
|
}
|
|
543
861
|
}
|
|
544
|
-
|
|
545
|
-
const tmpDir =
|
|
862
|
+
log8.info("Compiled %d entries", entries.length);
|
|
863
|
+
const tmpDir = path9.join(sourceDir, ".ssg-tmp");
|
|
546
864
|
try {
|
|
547
|
-
|
|
865
|
+
fs3.rmSync(tmpDir, { recursive: true, force: true });
|
|
548
866
|
} catch {
|
|
549
867
|
}
|
|
550
868
|
}
|
|
551
|
-
async function compileEntry(entry, options, manifest, projectRoot, sourceDir,
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
let renderToStaticMarkup;
|
|
555
|
-
try {
|
|
556
|
-
createElement = projectRequire("react").createElement;
|
|
557
|
-
renderToStaticMarkup = projectRequire("react-dom/server").renderToStaticMarkup;
|
|
558
|
-
} catch {
|
|
559
|
-
log3.warn("react/react-dom not found, skipping SSR for %s", entry.kebabName);
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
const sourceCode = fs.readFileSync(entry.filePath, "utf-8");
|
|
563
|
-
let esbuild;
|
|
564
|
-
try {
|
|
565
|
-
esbuild = projectRequire("esbuild");
|
|
566
|
-
} catch {
|
|
567
|
-
log3.warn("esbuild not found, skipping SSR for %s", entry.kebabName);
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
const ts = Date.now();
|
|
571
|
-
const tmpDir = path6.join(sourceDir, ".ssg-tmp");
|
|
572
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
573
|
-
const tmpFile = path6.join(tmpDir, `.ssg-entry-${ts}.mjs`);
|
|
574
|
-
log3.debug("bundling %s via esbuild", entry.kebabName);
|
|
575
|
-
const startBundled = Date.now();
|
|
576
|
-
await esbuild.build({
|
|
577
|
-
stdin: {
|
|
578
|
-
contents: sourceCode,
|
|
579
|
-
resolveDir: path6.dirname(entry.filePath),
|
|
580
|
-
loader: path6.extname(entry.filePath).slice(1)
|
|
581
|
-
},
|
|
582
|
-
outfile: tmpFile,
|
|
583
|
-
bundle: true,
|
|
584
|
-
format: "esm",
|
|
585
|
-
jsx: "automatic",
|
|
586
|
-
platform: "node",
|
|
587
|
-
external: [
|
|
588
|
-
"react",
|
|
589
|
-
"react-dom",
|
|
590
|
-
"react-dom/*",
|
|
591
|
-
"vite-plugin-react-shopify",
|
|
592
|
-
"vite-plugin-react-shopify/*"
|
|
593
|
-
],
|
|
594
|
-
write: true,
|
|
595
|
-
allowOverwrite: true,
|
|
596
|
-
plugins: [
|
|
597
|
-
{
|
|
598
|
-
name: "ssg-strip-css",
|
|
599
|
-
setup(build) {
|
|
600
|
-
build.onResolve({ filter: /\.module\.css$/ }, (args) => ({
|
|
601
|
-
namespace: "ssg-css-module",
|
|
602
|
-
path: args.path
|
|
603
|
-
}));
|
|
604
|
-
build.onResolve({ filter: /\.css$/ }, (args) => ({
|
|
605
|
-
namespace: "ssg-css-plain",
|
|
606
|
-
path: args.path
|
|
607
|
-
}));
|
|
608
|
-
build.onLoad({ filter: /.*/, namespace: "ssg-css-module" }, () => ({
|
|
609
|
-
contents: "export default new Proxy({},{get:(_,k)=>k});",
|
|
610
|
-
loader: "js"
|
|
611
|
-
}));
|
|
612
|
-
build.onLoad({ filter: /.*/, namespace: "ssg-css-plain" }, () => ({
|
|
613
|
-
contents: "",
|
|
614
|
-
loader: "js"
|
|
615
|
-
}));
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
]
|
|
619
|
-
});
|
|
620
|
-
log3.debug("esbuild bundle took %dms", Date.now() - startBundled);
|
|
869
|
+
async function compileEntry(entry, options, manifest, projectRoot, sourceDir, entryCssFiles, cssSnippetMap) {
|
|
870
|
+
const bundleResult = await bundleEntry(entry, projectRoot, sourceDir);
|
|
871
|
+
if (!bundleResult) return;
|
|
621
872
|
try {
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
const
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
html = unwrapHtmlEntities(html);
|
|
873
|
+
const renderResult = await renderEntry(bundleResult.tmpFile, entry, projectRoot);
|
|
874
|
+
if (!renderResult) return;
|
|
875
|
+
const { html, trackedExpressions } = renderResult;
|
|
876
|
+
validateShopifyMeta(entry.meta, { kebabName: entry.kebabName, filePath: entry.filePath });
|
|
877
|
+
const cssFiles = entryCssFiles.get(entry.kebabName) || [];
|
|
878
|
+
const { inline: cssInlineFiles, snippets: cssSnippets } = categorizeCss(cssFiles, cssSnippetMap);
|
|
879
|
+
const cssInline = readCssFileContents(cssInlineFiles, options.buildDir, options.themeRoot);
|
|
880
|
+
log8.debug(
|
|
881
|
+
"compiling %s (type=%s, css inline=%d, css snippets=%d)",
|
|
882
|
+
entry.kebabName,
|
|
883
|
+
entry.targetType,
|
|
884
|
+
cssInline.length,
|
|
885
|
+
cssSnippets.length
|
|
886
|
+
);
|
|
637
887
|
const scriptAsset = resolveScriptAsset(entry.kebabName, manifest);
|
|
638
|
-
const liquidContent = assembleLiquidFile(html, entry, scriptAsset, {
|
|
888
|
+
const liquidContent = assembleLiquidFile(html, entry, scriptAsset, {
|
|
889
|
+
inline: cssInline,
|
|
890
|
+
snippets: cssSnippets
|
|
891
|
+
}, {
|
|
639
892
|
prefix: options.ssg.prefix,
|
|
640
893
|
outputName: options.ssg.outputName || void 0,
|
|
641
894
|
buildDir: options.buildDir
|
|
642
|
-
});
|
|
895
|
+
}, [...trackedExpressions]);
|
|
643
896
|
const outputPath = getOutputPath(entry, {
|
|
644
897
|
prefix: options.ssg.prefix,
|
|
645
898
|
outputName: options.ssg.outputName || void 0,
|
|
646
899
|
themeRoot: options.themeRoot
|
|
647
900
|
});
|
|
648
|
-
const dir =
|
|
649
|
-
if (!
|
|
650
|
-
|
|
901
|
+
const dir = path9.dirname(outputPath);
|
|
902
|
+
if (!fs3.existsSync(dir)) {
|
|
903
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
651
904
|
}
|
|
652
|
-
|
|
905
|
+
fs3.writeFileSync(outputPath, liquidContent);
|
|
653
906
|
} finally {
|
|
654
907
|
try {
|
|
655
|
-
|
|
656
|
-
} catch {
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
function resolveScriptAsset(kebabName, manifest) {
|
|
661
|
-
const manifestKey = `shopify:entry:${kebabName}`;
|
|
662
|
-
const entryChunk = manifest[manifestKey];
|
|
663
|
-
if (!entryChunk) return null;
|
|
664
|
-
const file = entryChunk.file;
|
|
665
|
-
if (!file) return null;
|
|
666
|
-
return path6.basename(file);
|
|
667
|
-
}
|
|
668
|
-
function collectCssFiles(manifestKey, manifest) {
|
|
669
|
-
const collected = /* @__PURE__ */ new Set();
|
|
670
|
-
const visited = /* @__PURE__ */ new Set();
|
|
671
|
-
collectCssFilesRecursive(manifestKey, manifest, collected, visited);
|
|
672
|
-
return [...collected];
|
|
673
|
-
}
|
|
674
|
-
function collectCssFilesRecursive(chunkKey, manifest, collected, visited) {
|
|
675
|
-
if (visited.has(chunkKey)) return;
|
|
676
|
-
visited.add(chunkKey);
|
|
677
|
-
const chunk = manifest[chunkKey];
|
|
678
|
-
if (!chunk) return;
|
|
679
|
-
if (chunk.css && Array.isArray(chunk.css)) {
|
|
680
|
-
for (const cssFile of chunk.css) {
|
|
681
|
-
collected.add(cssFile);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
if (chunk.imports && Array.isArray(chunk.imports)) {
|
|
685
|
-
for (const imported of chunk.imports) {
|
|
686
|
-
collectCssFilesRecursive(imported, manifest, collected, visited);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
function readCssFileContents(cssFiles, buildDir, themeRoot) {
|
|
691
|
-
const assetsDir = path6.resolve(themeRoot, buildDir);
|
|
692
|
-
return cssFiles.map((file) => {
|
|
693
|
-
try {
|
|
694
|
-
return fs.readFileSync(path6.join(assetsDir, file), "utf-8");
|
|
908
|
+
fs3.unlinkSync(bundleResult.tmpFile);
|
|
695
909
|
} catch {
|
|
696
|
-
return "";
|
|
697
910
|
}
|
|
698
|
-
}).filter(Boolean);
|
|
699
|
-
}
|
|
700
|
-
function getCssBaseName(cssFile) {
|
|
701
|
-
const name = cssFile.replace(/\.css$/, "");
|
|
702
|
-
const lastHyphen = name.lastIndexOf("-");
|
|
703
|
-
if (lastHyphen > 0) {
|
|
704
|
-
const possibleHash = name.slice(lastHyphen + 1);
|
|
705
|
-
if (/^[A-Za-z0-9_-]{8,}$/.test(possibleHash)) {
|
|
706
|
-
return name.slice(0, lastHyphen);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
return name;
|
|
710
|
-
}
|
|
711
|
-
function pathToFileURL(filePath) {
|
|
712
|
-
const absPath = path6.resolve(filePath);
|
|
713
|
-
if (process.platform === "win32") {
|
|
714
|
-
return "file:///" + absPath.replace(/\\/g, "/");
|
|
715
911
|
}
|
|
716
|
-
return "file://" + absPath;
|
|
717
912
|
}
|
|
718
913
|
|
|
719
914
|
// src/ssg/index.ts
|
|
720
|
-
var
|
|
915
|
+
var log9 = logger("ssg");
|
|
721
916
|
function shopifySSG(options) {
|
|
722
917
|
return {
|
|
723
918
|
name: "vite-plugin-shopify:ssg",
|
|
@@ -726,23 +921,23 @@ function shopifySSG(options) {
|
|
|
726
921
|
return {};
|
|
727
922
|
},
|
|
728
923
|
async closeBundle() {
|
|
729
|
-
const manifestPath =
|
|
924
|
+
const manifestPath = path10.resolve(
|
|
730
925
|
options.themeRoot,
|
|
731
926
|
options.buildDir,
|
|
732
927
|
".vite",
|
|
733
928
|
"manifest.json"
|
|
734
929
|
);
|
|
735
|
-
if (!
|
|
736
|
-
|
|
930
|
+
if (!fs4.existsSync(manifestPath)) {
|
|
931
|
+
log9.warn("No manifest.json found, skipping SSG");
|
|
737
932
|
return;
|
|
738
933
|
}
|
|
739
|
-
|
|
740
|
-
const manifest = JSON.parse(
|
|
741
|
-
|
|
934
|
+
log9.debug("reading manifest from %s", manifestPath);
|
|
935
|
+
const manifest = JSON.parse(fs4.readFileSync(manifestPath, "utf-8"));
|
|
936
|
+
log9.info("Starting SSG compilation...");
|
|
742
937
|
await compileAllEntries(options, manifest);
|
|
743
|
-
|
|
938
|
+
log9.info("SSG compilation complete");
|
|
744
939
|
writeImportMapSnippet(options);
|
|
745
|
-
|
|
940
|
+
log9.debug("wrote import map snippet");
|
|
746
941
|
},
|
|
747
942
|
resolveId(id) {
|
|
748
943
|
if (id === "vite-plugin-shopify/runtime") {
|
|
@@ -751,13 +946,17 @@ function shopifySSG(options) {
|
|
|
751
946
|
},
|
|
752
947
|
load(id) {
|
|
753
948
|
if (id === "\0vite-plugin-shopify:runtime") {
|
|
754
|
-
|
|
949
|
+
const exports = [
|
|
950
|
+
`export { LiquidDataProvider, LiquidDataContext } from 'vite-plugin-shopify/runtime'`,
|
|
951
|
+
`export { useLiquid, useLiquidValues, useSectionSettings, useBlockSettings, useSnippetParams, useBlockParams } from 'vite-plugin-shopify/runtime'`
|
|
952
|
+
];
|
|
953
|
+
return exports.join("\n");
|
|
755
954
|
}
|
|
756
955
|
}
|
|
757
956
|
};
|
|
758
957
|
}
|
|
759
958
|
function writeImportMapSnippet(options) {
|
|
760
|
-
const snippetPath =
|
|
959
|
+
const snippetPath = path10.resolve(
|
|
761
960
|
options.themeRoot,
|
|
762
961
|
"snippets",
|
|
763
962
|
options.snippetFile
|
|
@@ -774,11 +973,11 @@ function writeImportMapSnippet(options) {
|
|
|
774
973
|
"</script>",
|
|
775
974
|
""
|
|
776
975
|
].join("\n");
|
|
777
|
-
const dir =
|
|
778
|
-
if (!
|
|
779
|
-
|
|
976
|
+
const dir = path10.dirname(snippetPath);
|
|
977
|
+
if (!fs4.existsSync(dir)) {
|
|
978
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
780
979
|
}
|
|
781
|
-
|
|
980
|
+
fs4.writeFileSync(snippetPath, content);
|
|
782
981
|
}
|
|
783
982
|
|
|
784
983
|
// src/index.ts
|