vite-plugin-react-shopify 2.0.0 → 2.1.1
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/dist/index.d.ts +14 -11
- package/dist/index.js +520 -370
- package/package.json +15 -13
package/dist/index.d.ts
CHANGED
|
@@ -24,14 +24,14 @@ interface ImportMapOptions {
|
|
|
24
24
|
react?: string;
|
|
25
25
|
reactDomClient?: string;
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
type SettingValue = string | number | boolean;
|
|
29
|
-
type InputSettings = Record<string, SettingValue>;
|
|
27
|
+
|
|
30
28
|
interface BaseSettingSchema {
|
|
31
29
|
id: string;
|
|
32
30
|
label: string;
|
|
33
31
|
info?: string;
|
|
34
32
|
}
|
|
33
|
+
type SettingValue = string | number | boolean;
|
|
34
|
+
type InputSettings = Record<string, SettingValue>;
|
|
35
35
|
interface CheckboxSetting extends BaseSettingSchema {
|
|
36
36
|
type: "checkbox";
|
|
37
37
|
default?: boolean;
|
|
@@ -230,9 +230,18 @@ type EmptyDefaultsExist<T extends readonly any[]> = true extends {
|
|
|
230
230
|
[K in keyof T]: IsEmptyStringDefault<T[K]>;
|
|
231
231
|
}[number] ? true : false;
|
|
232
232
|
type AssertNoEmptyDefaults<T extends readonly SettingSchema[]> = EmptyDefaultsExist<T> extends true ? never : true;
|
|
233
|
+
type ValueForType<T extends string> = T extends "checkbox" ? boolean : T extends "number" | "range" ? number : string;
|
|
234
|
+
type InferSettings<T extends readonly {
|
|
235
|
+
type: string;
|
|
236
|
+
id: string;
|
|
237
|
+
}[]> = {
|
|
238
|
+
[K in T[number] as K["id"]]: ValueForType<K["type"]>;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
type ShopifyBlockType = "template" | "section" | "block" | "snippet";
|
|
233
242
|
interface ShopifyMeta {
|
|
234
243
|
type?: ShopifyBlockType;
|
|
235
|
-
name
|
|
244
|
+
name?: string;
|
|
236
245
|
tag?: string;
|
|
237
246
|
class?: string;
|
|
238
247
|
limit?: number;
|
|
@@ -262,6 +271,7 @@ interface PresetBlock {
|
|
|
262
271
|
settings?: InputSettings;
|
|
263
272
|
blocks?: PresetBlock[];
|
|
264
273
|
}
|
|
274
|
+
|
|
265
275
|
interface SSGEntry {
|
|
266
276
|
filePath: string;
|
|
267
277
|
componentName: string;
|
|
@@ -269,13 +279,6 @@ interface SSGEntry {
|
|
|
269
279
|
targetType: ShopifyBlockType;
|
|
270
280
|
meta: Required<Pick<ShopifyMeta, "name">> & ShopifyMeta;
|
|
271
281
|
}
|
|
272
|
-
type ValueForType<T extends string> = T extends "checkbox" ? boolean : T extends "number" | "range" ? number : string;
|
|
273
|
-
type InferSettings<T extends readonly {
|
|
274
|
-
type: string;
|
|
275
|
-
id: string;
|
|
276
|
-
}[]> = {
|
|
277
|
-
[K in T[number] as K["id"]]: ValueForType<K["type"]>;
|
|
278
|
-
};
|
|
279
282
|
|
|
280
283
|
declare const vitePluginShopify: (options?: Options) => Plugin[];
|
|
281
284
|
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/core/options.ts
|
|
2
2
|
import path from "path";
|
|
3
3
|
var defaultPrefix = {
|
|
4
4
|
template: "page.react-",
|
|
@@ -45,7 +45,7 @@ var resolveOptions = (options = {}) => {
|
|
|
45
45
|
};
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
// src/
|
|
48
|
+
// src/core/logger.ts
|
|
49
49
|
import createDebugger from "debug";
|
|
50
50
|
var NAMESPACE = "vite-plugin-shopify";
|
|
51
51
|
var _debugEnabled = false;
|
|
@@ -67,7 +67,7 @@ function logger(ns) {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// src/
|
|
70
|
+
// src/core/config.ts
|
|
71
71
|
import path2 from "path";
|
|
72
72
|
var log = logger("config");
|
|
73
73
|
function isWatchMode() {
|
|
@@ -146,11 +146,11 @@ function shopifyConfig(options) {
|
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
// src/
|
|
149
|
+
// src/core/entries.ts
|
|
150
150
|
import path4 from "path";
|
|
151
151
|
import { normalizePath as normalizePath2 } from "vite";
|
|
152
152
|
|
|
153
|
-
// src/
|
|
153
|
+
// src/ssg/scanner.ts
|
|
154
154
|
import path3 from "path";
|
|
155
155
|
import glob from "fast-glob";
|
|
156
156
|
import { normalizePath } from "vite";
|
|
@@ -177,7 +177,7 @@ function scanEntries(options) {
|
|
|
177
177
|
componentName,
|
|
178
178
|
kebabName,
|
|
179
179
|
targetType,
|
|
180
|
-
meta: { name:
|
|
180
|
+
meta: { name: deriveName(fileName) }
|
|
181
181
|
});
|
|
182
182
|
}
|
|
183
183
|
}
|
|
@@ -186,8 +186,64 @@ function scanEntries(options) {
|
|
|
186
186
|
function toKebabCase(str) {
|
|
187
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();
|
|
188
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
|
+
}
|
|
193
|
+
|
|
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
|
+
}
|
|
189
245
|
|
|
190
|
-
// src/
|
|
246
|
+
// src/core/entries.ts
|
|
191
247
|
var log2 = logger("entries");
|
|
192
248
|
function shopifyEntries(options) {
|
|
193
249
|
let entries = [];
|
|
@@ -223,67 +279,294 @@ function shopifyEntries(options) {
|
|
|
223
279
|
if (!entry) return;
|
|
224
280
|
const sourceDir = path4.resolve(options.themeRoot, options.sourceCodeDir);
|
|
225
281
|
const componentRel = normalizePath2(path4.relative(sourceDir, entry.filePath));
|
|
226
|
-
return
|
|
227
|
-
`import { createElement } from 'react'`,
|
|
228
|
-
`import Component from '~/${componentRel}'`,
|
|
229
|
-
`import { hydrateRoot } from 'react-dom/client'`,
|
|
230
|
-
`import { LiquidDataProvider } from 'vite-plugin-react-shopify/runtime'`,
|
|
231
|
-
``,
|
|
232
|
-
`const SELECTOR = '[data-ssg-component="${kebabName}"]'`,
|
|
233
|
-
`const roots = new Map()`,
|
|
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
|
-
``,
|
|
241
|
-
`function hydrate(el) {`,
|
|
242
|
-
` const h = el.querySelector(':scope > [data-ssg-hydrate]') || (el.matches('[data-ssg-hydrate]') ? el : null)`,
|
|
243
|
-
` if (!h || roots.has(h)) return`,
|
|
244
|
-
` const liquidData = readLiquidData(el)`,
|
|
245
|
-
` roots.set(h, hydrateRoot(h, createElement(LiquidDataProvider, { value: liquidData }, createElement(Component))))`,
|
|
246
|
-
`}`,
|
|
247
|
-
``,
|
|
248
|
-
`function unmount(el) {`,
|
|
249
|
-
` const h = el.querySelector(':scope > [data-ssg-hydrate]') || (el.matches('[data-ssg-hydrate]') ? el : null)`,
|
|
250
|
-
` if (h && roots.has(h)) { roots.get(h).unmount(); roots.delete(h) }`,
|
|
251
|
-
`}`,
|
|
252
|
-
``,
|
|
253
|
-
`function scan(target) {`,
|
|
254
|
-
` if (target.matches?.(SELECTOR)) hydrate(target)`,
|
|
255
|
-
` target.querySelectorAll(SELECTOR).forEach(hydrate)`,
|
|
256
|
-
`}`,
|
|
257
|
-
``,
|
|
258
|
-
`function sweep(target) {`,
|
|
259
|
-
` if (target.matches?.(SELECTOR)) unmount(target)`,
|
|
260
|
-
` target.querySelectorAll(SELECTOR).forEach(unmount)`,
|
|
261
|
-
`}`,
|
|
262
|
-
``,
|
|
263
|
-
`scan(document)`,
|
|
264
|
-
``,
|
|
265
|
-
`document.addEventListener('shopify:section:load', (e) => {`,
|
|
266
|
-
` scan(e.target)`,
|
|
267
|
-
`})`,
|
|
268
|
-
``,
|
|
269
|
-
`document.addEventListener('shopify:section:unload', (e) => {`,
|
|
270
|
-
` sweep(e.target)`,
|
|
271
|
-
`})`
|
|
272
|
-
].join("\n");
|
|
282
|
+
return generateEntryModule(entry, componentRel);
|
|
273
283
|
}
|
|
274
284
|
};
|
|
275
285
|
}
|
|
276
286
|
|
|
277
|
-
// src/
|
|
278
|
-
import
|
|
279
|
-
import
|
|
287
|
+
// src/ssg/index.ts
|
|
288
|
+
import fs4 from "fs";
|
|
289
|
+
import path10 from "path";
|
|
290
|
+
|
|
291
|
+
// src/ssg/compiler.ts
|
|
292
|
+
import fs3 from "fs";
|
|
293
|
+
import path9 from "path";
|
|
280
294
|
|
|
281
|
-
// src/
|
|
295
|
+
// src/ssg/css-manager.ts
|
|
282
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";
|
|
283
394
|
import path6 from "path";
|
|
284
395
|
import { createRequire } from "module";
|
|
285
396
|
|
|
286
|
-
// src/
|
|
397
|
+
// src/hydration-fix/index.ts
|
|
398
|
+
import { parseSync } from "oxc-parser";
|
|
399
|
+
import { walk } from "oxc-walker";
|
|
400
|
+
var log4 = logger("hydration-fix");
|
|
401
|
+
function autoFixAdjacentText(source, filePath) {
|
|
402
|
+
const parseResult = parseSync(filePath, source);
|
|
403
|
+
if (parseResult.errors.length > 0) {
|
|
404
|
+
log4.debug("OXC parse errors for %s, skipping hydration fix", filePath);
|
|
405
|
+
return { result: source, fixCount: 0 };
|
|
406
|
+
}
|
|
407
|
+
const replacements = [];
|
|
408
|
+
walk(parseResult.program, {
|
|
409
|
+
enter(node) {
|
|
410
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") {
|
|
411
|
+
const children = node.children;
|
|
412
|
+
if (children.length > 0) {
|
|
413
|
+
processChildren(children, source, replacements);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
if (replacements.length === 0) {
|
|
419
|
+
return { result: source, fixCount: 0 };
|
|
420
|
+
}
|
|
421
|
+
replacements.sort((a, b) => b.start - a.start);
|
|
422
|
+
let fixed = source;
|
|
423
|
+
for (const { start, end, replacement } of replacements) {
|
|
424
|
+
fixed = fixed.slice(0, start) + replacement + fixed.slice(end);
|
|
425
|
+
}
|
|
426
|
+
log4.warn(
|
|
427
|
+
`auto-fixed ${replacements.length} adjacent text+expression issue(s) in ${filePath}`
|
|
428
|
+
);
|
|
429
|
+
return { result: fixed, fixCount: replacements.length };
|
|
430
|
+
}
|
|
431
|
+
function processChildren(children, source, replacements) {
|
|
432
|
+
let i = 0;
|
|
433
|
+
while (i < children.length) {
|
|
434
|
+
if (children[i].type !== "JSXText" && children[i].type !== "JSXExpressionContainer") {
|
|
435
|
+
i++;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
let runEnd = i;
|
|
439
|
+
let hasText = children[i].type === "JSXText";
|
|
440
|
+
let hasExpr = children[i].type === "JSXExpressionContainer";
|
|
441
|
+
while (runEnd + 1 < children.length && (children[runEnd + 1].type === "JSXText" || children[runEnd + 1].type === "JSXExpressionContainer")) {
|
|
442
|
+
runEnd++;
|
|
443
|
+
if (children[runEnd].type === "JSXText") hasText = true;
|
|
444
|
+
if (children[runEnd].type === "JSXExpressionContainer") hasExpr = true;
|
|
445
|
+
}
|
|
446
|
+
if (!hasText || !hasExpr) {
|
|
447
|
+
i = runEnd + 1;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const sliceStart = children[i].start;
|
|
451
|
+
const sliceEnd = children[runEnd].end;
|
|
452
|
+
const runText = source.slice(sliceStart, sliceEnd);
|
|
453
|
+
const trimmed = runText.trim();
|
|
454
|
+
if (!needsFix(trimmed)) {
|
|
455
|
+
i = runEnd + 1;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
const tpl = trimmed.replace(/\{([^}]+)\}/g, "${$1}");
|
|
459
|
+
replacements.push({
|
|
460
|
+
start: sliceStart,
|
|
461
|
+
end: sliceEnd,
|
|
462
|
+
replacement: `{\`${tpl}\`}`
|
|
463
|
+
});
|
|
464
|
+
i = runEnd + 1;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function needsFix(content) {
|
|
468
|
+
const trimmed = content.trim();
|
|
469
|
+
if (!trimmed) return false;
|
|
470
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
471
|
+
const inner = trimmed.slice(1, -1).trim();
|
|
472
|
+
if (inner.startsWith("`") && inner.endsWith("`")) return false;
|
|
473
|
+
if (inner.length > 0 && !/<[a-zA-Z]/.test(inner)) return false;
|
|
474
|
+
}
|
|
475
|
+
if (!/\{/.test(trimmed)) return false;
|
|
476
|
+
if (/<[a-zA-Z]/.test(trimmed)) return false;
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/ssg/bundler.ts
|
|
481
|
+
var log5 = logger("ssg:bundler");
|
|
482
|
+
async function bundleEntry(entry, projectRoot, sourceDir) {
|
|
483
|
+
const projectRequire = createRequire(path6.join(projectRoot, "package.json"));
|
|
484
|
+
let esbuild;
|
|
485
|
+
try {
|
|
486
|
+
esbuild = projectRequire("esbuild");
|
|
487
|
+
} catch {
|
|
488
|
+
log5.warn("esbuild not found, skipping SSR for %s", entry.kebabName);
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const sourceCode = fs2.readFileSync(entry.filePath, "utf-8");
|
|
492
|
+
const { result: fixedSource, fixCount } = autoFixAdjacentText(sourceCode, entry.filePath);
|
|
493
|
+
const finalSource = fixCount > 0 ? fixedSource : sourceCode;
|
|
494
|
+
const ts = Date.now();
|
|
495
|
+
const tmpDir = path6.join(sourceDir, ".ssg-tmp");
|
|
496
|
+
fs2.mkdirSync(tmpDir, { recursive: true });
|
|
497
|
+
const tmpFile = path6.join(tmpDir, `.ssg-entry-${ts}.mjs`);
|
|
498
|
+
log5.debug("bundling %s via esbuild", entry.kebabName);
|
|
499
|
+
const startBundled = Date.now();
|
|
500
|
+
await esbuild.build({
|
|
501
|
+
stdin: {
|
|
502
|
+
contents: finalSource,
|
|
503
|
+
resolveDir: path6.dirname(entry.filePath),
|
|
504
|
+
loader: path6.extname(entry.filePath).slice(1)
|
|
505
|
+
},
|
|
506
|
+
outfile: tmpFile,
|
|
507
|
+
bundle: true,
|
|
508
|
+
format: "esm",
|
|
509
|
+
jsx: "automatic",
|
|
510
|
+
platform: "node",
|
|
511
|
+
external: [
|
|
512
|
+
"react",
|
|
513
|
+
"react-dom",
|
|
514
|
+
"react-dom/*",
|
|
515
|
+
"vite-plugin-react-shopify",
|
|
516
|
+
"vite-plugin-react-shopify/*"
|
|
517
|
+
],
|
|
518
|
+
write: true,
|
|
519
|
+
allowOverwrite: true,
|
|
520
|
+
plugins: [
|
|
521
|
+
{
|
|
522
|
+
name: "ssg-hydration-fix",
|
|
523
|
+
setup(build) {
|
|
524
|
+
build.onLoad({ filter: /\.(tsx|jsx)$/ }, (args) => {
|
|
525
|
+
try {
|
|
526
|
+
const source = fs2.readFileSync(args.path, "utf-8");
|
|
527
|
+
const { result, fixCount: fixCount2 } = autoFixAdjacentText(source, args.path);
|
|
528
|
+
if (fixCount2 > 0) {
|
|
529
|
+
return { contents: result, loader: args.path.endsWith(".tsx") ? "tsx" : "jsx" };
|
|
530
|
+
}
|
|
531
|
+
} catch (e) {
|
|
532
|
+
log5.debug("SSG hydration-fix failed for %s: %s", args.path, e);
|
|
533
|
+
}
|
|
534
|
+
return void 0;
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: "ssg-strip-css",
|
|
540
|
+
setup(build) {
|
|
541
|
+
build.onResolve({ filter: /\.module\.css$/ }, (args) => ({
|
|
542
|
+
namespace: "ssg-css-module",
|
|
543
|
+
path: args.path
|
|
544
|
+
}));
|
|
545
|
+
build.onResolve({ filter: /\.css$/ }, (args) => ({
|
|
546
|
+
namespace: "ssg-css-plain",
|
|
547
|
+
path: args.path
|
|
548
|
+
}));
|
|
549
|
+
build.onLoad({ filter: /.*/, namespace: "ssg-css-module" }, () => ({
|
|
550
|
+
contents: "export default new Proxy({},{get:(_,k)=>k});",
|
|
551
|
+
loader: "js"
|
|
552
|
+
}));
|
|
553
|
+
build.onLoad({ filter: /.*/, namespace: "ssg-css-plain" }, () => ({
|
|
554
|
+
contents: "",
|
|
555
|
+
loader: "js"
|
|
556
|
+
}));
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
]
|
|
560
|
+
});
|
|
561
|
+
log5.debug("esbuild bundle took %dms", Date.now() - startBundled);
|
|
562
|
+
return { tmpFile };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/ssg/renderer.ts
|
|
566
|
+
import path7 from "path";
|
|
567
|
+
import { createRequire as createRequire2 } from "module";
|
|
568
|
+
|
|
569
|
+
// src/ssg/post-process.ts
|
|
287
570
|
var VOID_ELEMENTS = /<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)([^>]*)\/>/g;
|
|
288
571
|
function normalizeVoidElements(html) {
|
|
289
572
|
return html.replace(VOID_ELEMENTS, "<$1$2>");
|
|
@@ -298,21 +581,63 @@ function unwrapHtmlEntities(html) {
|
|
|
298
581
|
return html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
299
582
|
}
|
|
300
583
|
|
|
301
|
-
// src/
|
|
302
|
-
|
|
584
|
+
// src/ssg/renderer.ts
|
|
585
|
+
function pathToFileURL(filePath) {
|
|
586
|
+
const absPath = path7.resolve(filePath);
|
|
587
|
+
if (process.platform === "win32") {
|
|
588
|
+
return "file:///" + absPath.replace(/\\/g, "/");
|
|
589
|
+
}
|
|
590
|
+
return "file://" + absPath;
|
|
591
|
+
}
|
|
592
|
+
var log6 = logger("ssg:renderer");
|
|
593
|
+
function renderEntry(tmpFile, entry, projectRoot) {
|
|
594
|
+
return import(pathToFileURL(tmpFile)).then((mod) => {
|
|
595
|
+
const Component = mod.default;
|
|
596
|
+
const shopifyMeta = mod.shopifyMeta;
|
|
597
|
+
if (!Component) {
|
|
598
|
+
log6.warn("No default export found in %s, skipping", entry.filePath);
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
if (shopifyMeta) {
|
|
602
|
+
entry.meta = { ...entry.meta, ...shopifyMeta, name: shopifyMeta.name ?? entry.meta.name };
|
|
603
|
+
}
|
|
604
|
+
const projectRequire = createRequire2(path7.join(projectRoot, "package.json"));
|
|
605
|
+
let createElement;
|
|
606
|
+
let renderToStaticMarkup;
|
|
607
|
+
try {
|
|
608
|
+
createElement = projectRequire("react").createElement;
|
|
609
|
+
renderToStaticMarkup = projectRequire("react-dom/server").renderToStaticMarkup;
|
|
610
|
+
} catch {
|
|
611
|
+
log6.warn("react/react-dom not found, skipping SSR for %s", entry.kebabName);
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
globalThis.__shopify_ssg_target = entry.targetType;
|
|
615
|
+
const trackedExpressions = /* @__PURE__ */ new Set();
|
|
616
|
+
globalThis.__shopify_ssg_liquid_track = trackedExpressions;
|
|
617
|
+
const element = createElement(Component);
|
|
618
|
+
let html = renderToStaticMarkup(element);
|
|
619
|
+
delete globalThis.__shopify_ssg_liquid_track;
|
|
620
|
+
html = normalizeVoidElements(html);
|
|
621
|
+
html = normalizeStyleAttributes(html);
|
|
622
|
+
html = unwrapHtmlEntities(html);
|
|
623
|
+
return { html, trackedExpressions, entryMeta: entry.meta };
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
function resolveScriptAsset(kebabName, manifest) {
|
|
627
|
+
const manifestKey = `shopify:entry:${kebabName}`;
|
|
628
|
+
const entryChunk = manifest[manifestKey];
|
|
629
|
+
if (!entryChunk) return null;
|
|
630
|
+
const file = entryChunk.file;
|
|
631
|
+
if (!file) return null;
|
|
632
|
+
return path7.basename(file);
|
|
633
|
+
}
|
|
303
634
|
|
|
304
|
-
// src/
|
|
305
|
-
var log3 = logger("schema-gen");
|
|
635
|
+
// src/ssg/schema.ts
|
|
306
636
|
function serializeSetting(setting) {
|
|
307
637
|
const s = { type: setting.type };
|
|
308
638
|
if ("id" in setting) s.id = setting.id;
|
|
309
639
|
if ("label" in setting) s.label = setting.label;
|
|
310
640
|
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
|
-
}
|
|
316
641
|
s.default = setting.default;
|
|
317
642
|
}
|
|
318
643
|
if ("info" in setting && setting.info) s.info = setting.info;
|
|
@@ -375,7 +700,33 @@ ${json}
|
|
|
375
700
|
`;
|
|
376
701
|
}
|
|
377
702
|
|
|
378
|
-
// src/
|
|
703
|
+
// src/ssg/liquid-paths.ts
|
|
704
|
+
import path8 from "path";
|
|
705
|
+
function getOutputPath(entry, options) {
|
|
706
|
+
const type = entry.meta.type ?? entry.targetType;
|
|
707
|
+
const dirName = typeToDir(type);
|
|
708
|
+
const fileName = resolveFileName(entry, type, options);
|
|
709
|
+
return path8.join(options.themeRoot, dirName, fileName);
|
|
710
|
+
}
|
|
711
|
+
function typeToDir(type) {
|
|
712
|
+
if (type === "snippet") return "snippets";
|
|
713
|
+
if (type === "block") return "blocks";
|
|
714
|
+
return `${type}s`;
|
|
715
|
+
}
|
|
716
|
+
function getAssetRelativePath(buildDir, filename) {
|
|
717
|
+
if (!buildDir.startsWith("assets/")) return filename;
|
|
718
|
+
const prefix = buildDir.slice("assets/".length);
|
|
719
|
+
return prefix ? `${prefix}/${filename}` : filename;
|
|
720
|
+
}
|
|
721
|
+
function resolveFileName(entry, type, options) {
|
|
722
|
+
if (options.outputName) {
|
|
723
|
+
return options.outputName.replace(/\{type\}/g, type).replace(/\{kebab\}/g, entry.kebabName).replace(/\{pascal\}/g, entry.componentName).replace(/\{target\}/g, entry.targetType) + ".liquid";
|
|
724
|
+
}
|
|
725
|
+
const prefix = options.prefix[type] ?? "react-";
|
|
726
|
+
return `${prefix}${entry.kebabName}.liquid`;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// src/ssg/liquid-assembler.ts
|
|
379
730
|
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";
|
|
380
731
|
function assembleLiquidFile(html, entry, scriptAsset, cssContents, options, trackedExpressions = []) {
|
|
381
732
|
const type = entry.meta.type ?? entry.targetType;
|
|
@@ -500,251 +851,88 @@ function buildSnippet(html, entry, trackedExpressions) {
|
|
|
500
851
|
);
|
|
501
852
|
return lines;
|
|
502
853
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (type === "block") return "blocks";
|
|
512
|
-
return `${type}s`;
|
|
513
|
-
}
|
|
514
|
-
function getAssetRelativePath(buildDir, filename) {
|
|
515
|
-
if (!buildDir.startsWith("assets/")) return filename;
|
|
516
|
-
const prefix = buildDir.slice("assets/".length);
|
|
517
|
-
return prefix ? `${prefix}/${filename}` : filename;
|
|
854
|
+
|
|
855
|
+
// src/validate/rules.ts
|
|
856
|
+
var MAX_NAME_LENGTH = 25;
|
|
857
|
+
function checkNameLength(meta, kebabName) {
|
|
858
|
+
if (meta.name.length > MAX_NAME_LENGTH) {
|
|
859
|
+
return `[${kebabName}] shopifyMeta.name "${meta.name}" is ${meta.name.length} chars (Shopify limit: ${MAX_NAME_LENGTH})`;
|
|
860
|
+
}
|
|
861
|
+
return null;
|
|
518
862
|
}
|
|
519
|
-
function
|
|
520
|
-
if (
|
|
521
|
-
|
|
863
|
+
function checkEmptyStringDefault(setting) {
|
|
864
|
+
if (setting.default === "") {
|
|
865
|
+
const label = "id" in setting && setting.id ? setting.id : "(no id)";
|
|
866
|
+
return `Setting "${label}" (type: ${setting.type}) has empty string default`;
|
|
522
867
|
}
|
|
523
|
-
|
|
524
|
-
return `${prefix}${entry.kebabName}.liquid`;
|
|
868
|
+
return null;
|
|
525
869
|
}
|
|
526
870
|
|
|
527
|
-
// src/
|
|
528
|
-
var
|
|
529
|
-
function
|
|
530
|
-
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
);
|
|
871
|
+
// src/validate/index.ts
|
|
872
|
+
var log7 = logger("validate");
|
|
873
|
+
function validateShopifyMeta(meta, context) {
|
|
874
|
+
const warnings = [];
|
|
875
|
+
const nameWarning = checkNameLength(meta, context.kebabName);
|
|
876
|
+
if (nameWarning) warnings.push(nameWarning);
|
|
877
|
+
if (meta.settings) {
|
|
878
|
+
for (const s of meta.settings) {
|
|
879
|
+
const w = checkEmptyStringDefault(s);
|
|
880
|
+
if (w) warnings.push(w);
|
|
881
|
+
}
|
|
551
882
|
}
|
|
552
|
-
|
|
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;
|
|
883
|
+
for (const w of warnings) {
|
|
884
|
+
log7.warn(w);
|
|
561
885
|
}
|
|
562
|
-
|
|
563
|
-
if (/<[a-zA-Z]/.test(trimmed)) return false;
|
|
564
|
-
return true;
|
|
886
|
+
return warnings;
|
|
565
887
|
}
|
|
566
888
|
|
|
567
|
-
// src/
|
|
568
|
-
var
|
|
889
|
+
// src/ssg/compiler.ts
|
|
890
|
+
var log8 = logger("ssg:compiler");
|
|
569
891
|
async function compileAllEntries(options, manifest) {
|
|
570
892
|
const entries = scanEntries(options);
|
|
571
893
|
if (entries.length === 0) return;
|
|
572
|
-
|
|
573
|
-
const projectRoot =
|
|
574
|
-
const sourceDir =
|
|
575
|
-
const entryCssFiles
|
|
576
|
-
const
|
|
577
|
-
for (const entry of entries) {
|
|
578
|
-
const manifestKey = `shopify:entry:${entry.kebabName}`;
|
|
579
|
-
const files = collectCssFiles(manifestKey, manifest);
|
|
580
|
-
entryCssFiles.set(entry.kebabName, files);
|
|
581
|
-
for (const f of files) {
|
|
582
|
-
cssRefCount.set(f, (cssRefCount.get(f) || 0) + 1);
|
|
583
|
-
}
|
|
584
|
-
log5.debug("entry %s has %d CSS files", entry.kebabName, files.length);
|
|
585
|
-
}
|
|
586
|
-
const cssSnippetMap = /* @__PURE__ */ new Map();
|
|
587
|
-
for (const [cssFile, count] of cssRefCount) {
|
|
588
|
-
if (count > 1) {
|
|
589
|
-
const snippetName = `${options.ssg.cssPrefix}-${getCssBaseName(cssFile)}`;
|
|
590
|
-
cssSnippetMap.set(cssFile, snippetName);
|
|
591
|
-
const snippetPath = path6.join(
|
|
592
|
-
path6.resolve(options.themeRoot),
|
|
593
|
-
"snippets",
|
|
594
|
-
`${snippetName}.liquid`
|
|
595
|
-
);
|
|
596
|
-
const cssPath = path6.join(
|
|
597
|
-
path6.resolve(options.themeRoot, options.buildDir),
|
|
598
|
-
cssFile
|
|
599
|
-
);
|
|
600
|
-
try {
|
|
601
|
-
const cssContent = fs.readFileSync(cssPath, "utf-8");
|
|
602
|
-
fs.mkdirSync(path6.dirname(snippetPath), { recursive: true });
|
|
603
|
-
fs.writeFileSync(snippetPath, `{% stylesheet %}
|
|
604
|
-
${cssContent.trim()}
|
|
605
|
-
{% endstylesheet %}
|
|
606
|
-
`);
|
|
607
|
-
log5.debug("generated shared CSS snippet %s (used by %d entries)", snippetName, count);
|
|
608
|
-
} catch {
|
|
609
|
-
log5.warn("failed to write CSS snippet for %s", cssFile);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
}
|
|
894
|
+
log8.debug("found %d entries to compile", entries.length);
|
|
895
|
+
const projectRoot = path9.resolve(options.themeRoot);
|
|
896
|
+
const sourceDir = path9.resolve(options.themeRoot, options.sourceCodeDir);
|
|
897
|
+
const { entryCssFiles, cssRefCount } = analyzeCssDistribution(entries, manifest);
|
|
898
|
+
const cssSnippetMap = generateSharedCssSnippets(cssRefCount, options);
|
|
613
899
|
for (const entry of entries) {
|
|
614
900
|
try {
|
|
615
|
-
|
|
616
|
-
const cssSnippets = cssFiles.filter((f) => cssSnippetMap.has(f)).map((f) => cssSnippetMap.get(f));
|
|
617
|
-
const cssInlineFiles = cssFiles.filter((f) => !cssSnippetMap.has(f));
|
|
618
|
-
const cssInline = readCssFileContents(cssInlineFiles, options.buildDir, options.themeRoot);
|
|
619
|
-
log5.debug(
|
|
620
|
-
"compiling %s (type=%s, css inline=%d, css snippets=%d)",
|
|
621
|
-
entry.kebabName,
|
|
622
|
-
entry.targetType,
|
|
623
|
-
cssInline.length,
|
|
624
|
-
cssSnippets.length
|
|
625
|
-
);
|
|
626
|
-
await compileEntry(entry, options, manifest, projectRoot, sourceDir, cssInline, cssSnippets);
|
|
901
|
+
await compileEntry(entry, options, manifest, projectRoot, sourceDir, entryCssFiles, cssSnippetMap);
|
|
627
902
|
} catch (err) {
|
|
628
|
-
|
|
903
|
+
log8.error("Failed to compile %s:", entry.filePath, err);
|
|
629
904
|
}
|
|
630
905
|
}
|
|
631
|
-
|
|
632
|
-
const tmpDir =
|
|
906
|
+
log8.info("Compiled %d entries", entries.length);
|
|
907
|
+
const tmpDir = path9.join(sourceDir, ".ssg-tmp");
|
|
633
908
|
try {
|
|
634
|
-
|
|
909
|
+
fs3.rmSync(tmpDir, { recursive: true, force: true });
|
|
635
910
|
} catch {
|
|
636
911
|
}
|
|
637
912
|
}
|
|
638
|
-
async function compileEntry(entry, options, manifest, projectRoot, sourceDir,
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
let renderToStaticMarkup;
|
|
642
|
-
try {
|
|
643
|
-
createElement = projectRequire("react").createElement;
|
|
644
|
-
renderToStaticMarkup = projectRequire("react-dom/server").renderToStaticMarkup;
|
|
645
|
-
} catch {
|
|
646
|
-
log5.warn("react/react-dom not found, skipping SSR for %s", entry.kebabName);
|
|
647
|
-
return;
|
|
648
|
-
}
|
|
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;
|
|
652
|
-
let esbuild;
|
|
653
|
-
try {
|
|
654
|
-
esbuild = projectRequire("esbuild");
|
|
655
|
-
} catch {
|
|
656
|
-
log5.warn("esbuild not found, skipping SSR for %s", entry.kebabName);
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
const ts = Date.now();
|
|
660
|
-
const tmpDir = path6.join(sourceDir, ".ssg-tmp");
|
|
661
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
662
|
-
const tmpFile = path6.join(tmpDir, `.ssg-entry-${ts}.mjs`);
|
|
663
|
-
log5.debug("bundling %s via esbuild", entry.kebabName);
|
|
664
|
-
const startBundled = Date.now();
|
|
665
|
-
await esbuild.build({
|
|
666
|
-
stdin: {
|
|
667
|
-
contents: finalSource,
|
|
668
|
-
resolveDir: path6.dirname(entry.filePath),
|
|
669
|
-
loader: path6.extname(entry.filePath).slice(1)
|
|
670
|
-
},
|
|
671
|
-
outfile: tmpFile,
|
|
672
|
-
bundle: true,
|
|
673
|
-
format: "esm",
|
|
674
|
-
jsx: "automatic",
|
|
675
|
-
platform: "node",
|
|
676
|
-
external: [
|
|
677
|
-
"react",
|
|
678
|
-
"react-dom",
|
|
679
|
-
"react-dom/*",
|
|
680
|
-
"vite-plugin-react-shopify",
|
|
681
|
-
"vite-plugin-react-shopify/*"
|
|
682
|
-
],
|
|
683
|
-
write: true,
|
|
684
|
-
allowOverwrite: true,
|
|
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
|
-
},
|
|
702
|
-
{
|
|
703
|
-
name: "ssg-strip-css",
|
|
704
|
-
setup(build) {
|
|
705
|
-
build.onResolve({ filter: /\.module\.css$/ }, (args) => ({
|
|
706
|
-
namespace: "ssg-css-module",
|
|
707
|
-
path: args.path
|
|
708
|
-
}));
|
|
709
|
-
build.onResolve({ filter: /\.css$/ }, (args) => ({
|
|
710
|
-
namespace: "ssg-css-plain",
|
|
711
|
-
path: args.path
|
|
712
|
-
}));
|
|
713
|
-
build.onLoad({ filter: /.*/, namespace: "ssg-css-module" }, () => ({
|
|
714
|
-
contents: "export default new Proxy({},{get:(_,k)=>k});",
|
|
715
|
-
loader: "js"
|
|
716
|
-
}));
|
|
717
|
-
build.onLoad({ filter: /.*/, namespace: "ssg-css-plain" }, () => ({
|
|
718
|
-
contents: "",
|
|
719
|
-
loader: "js"
|
|
720
|
-
}));
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
]
|
|
724
|
-
});
|
|
725
|
-
log5.debug("esbuild bundle took %dms", Date.now() - startBundled);
|
|
913
|
+
async function compileEntry(entry, options, manifest, projectRoot, sourceDir, entryCssFiles, cssSnippetMap) {
|
|
914
|
+
const bundleResult = await bundleEntry(entry, projectRoot, sourceDir);
|
|
915
|
+
if (!bundleResult) return;
|
|
726
916
|
try {
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
let html = renderToStaticMarkup(element);
|
|
742
|
-
delete globalThis.__shopify_ssg_liquid_track;
|
|
743
|
-
html = normalizeVoidElements(html);
|
|
744
|
-
html = normalizeStyleAttributes(html);
|
|
745
|
-
html = unwrapHtmlEntities(html);
|
|
917
|
+
const renderResult = await renderEntry(bundleResult.tmpFile, entry, projectRoot);
|
|
918
|
+
if (!renderResult) return;
|
|
919
|
+
const { html, trackedExpressions } = renderResult;
|
|
920
|
+
validateShopifyMeta(entry.meta, { kebabName: entry.kebabName, filePath: entry.filePath });
|
|
921
|
+
const cssFiles = entryCssFiles.get(entry.kebabName) || [];
|
|
922
|
+
const { inline: cssInlineFiles, snippets: cssSnippets } = categorizeCss(cssFiles, cssSnippetMap);
|
|
923
|
+
const cssInline = readCssFileContents(cssInlineFiles, options.buildDir, options.themeRoot);
|
|
924
|
+
log8.debug(
|
|
925
|
+
"compiling %s (type=%s, css inline=%d, css snippets=%d)",
|
|
926
|
+
entry.kebabName,
|
|
927
|
+
entry.targetType,
|
|
928
|
+
cssInline.length,
|
|
929
|
+
cssSnippets.length
|
|
930
|
+
);
|
|
746
931
|
const scriptAsset = resolveScriptAsset(entry.kebabName, manifest);
|
|
747
|
-
const liquidContent = assembleLiquidFile(html, entry, scriptAsset, {
|
|
932
|
+
const liquidContent = assembleLiquidFile(html, entry, scriptAsset, {
|
|
933
|
+
inline: cssInline,
|
|
934
|
+
snippets: cssSnippets
|
|
935
|
+
}, {
|
|
748
936
|
prefix: options.ssg.prefix,
|
|
749
937
|
outputName: options.ssg.outputName || void 0,
|
|
750
938
|
buildDir: options.buildDir
|
|
@@ -754,79 +942,21 @@ async function compileEntry(entry, options, manifest, projectRoot, sourceDir, cs
|
|
|
754
942
|
outputName: options.ssg.outputName || void 0,
|
|
755
943
|
themeRoot: options.themeRoot
|
|
756
944
|
});
|
|
757
|
-
const dir =
|
|
758
|
-
if (!
|
|
759
|
-
|
|
945
|
+
const dir = path9.dirname(outputPath);
|
|
946
|
+
if (!fs3.existsSync(dir)) {
|
|
947
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
760
948
|
}
|
|
761
|
-
|
|
949
|
+
fs3.writeFileSync(outputPath, liquidContent);
|
|
762
950
|
} finally {
|
|
763
951
|
try {
|
|
764
|
-
|
|
952
|
+
fs3.unlinkSync(bundleResult.tmpFile);
|
|
765
953
|
} catch {
|
|
766
954
|
}
|
|
767
955
|
}
|
|
768
956
|
}
|
|
769
|
-
function resolveScriptAsset(kebabName, manifest) {
|
|
770
|
-
const manifestKey = `shopify:entry:${kebabName}`;
|
|
771
|
-
const entryChunk = manifest[manifestKey];
|
|
772
|
-
if (!entryChunk) return null;
|
|
773
|
-
const file = entryChunk.file;
|
|
774
|
-
if (!file) return null;
|
|
775
|
-
return path6.basename(file);
|
|
776
|
-
}
|
|
777
|
-
function collectCssFiles(manifestKey, manifest) {
|
|
778
|
-
const collected = /* @__PURE__ */ new Set();
|
|
779
|
-
const visited = /* @__PURE__ */ new Set();
|
|
780
|
-
collectCssFilesRecursive(manifestKey, manifest, collected, visited);
|
|
781
|
-
return [...collected];
|
|
782
|
-
}
|
|
783
|
-
function collectCssFilesRecursive(chunkKey, manifest, collected, visited) {
|
|
784
|
-
if (visited.has(chunkKey)) return;
|
|
785
|
-
visited.add(chunkKey);
|
|
786
|
-
const chunk = manifest[chunkKey];
|
|
787
|
-
if (!chunk) return;
|
|
788
|
-
if (chunk.css && Array.isArray(chunk.css)) {
|
|
789
|
-
for (const cssFile of chunk.css) {
|
|
790
|
-
collected.add(cssFile);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
if (chunk.imports && Array.isArray(chunk.imports)) {
|
|
794
|
-
for (const imported of chunk.imports) {
|
|
795
|
-
collectCssFilesRecursive(imported, manifest, collected, visited);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
function readCssFileContents(cssFiles, buildDir, themeRoot) {
|
|
800
|
-
const assetsDir = path6.resolve(themeRoot, buildDir);
|
|
801
|
-
return cssFiles.map((file) => {
|
|
802
|
-
try {
|
|
803
|
-
return fs.readFileSync(path6.join(assetsDir, file), "utf-8");
|
|
804
|
-
} catch {
|
|
805
|
-
return "";
|
|
806
|
-
}
|
|
807
|
-
}).filter(Boolean);
|
|
808
|
-
}
|
|
809
|
-
function getCssBaseName(cssFile) {
|
|
810
|
-
const name = cssFile.replace(/\.css$/, "");
|
|
811
|
-
const lastHyphen = name.lastIndexOf("-");
|
|
812
|
-
if (lastHyphen > 0) {
|
|
813
|
-
const possibleHash = name.slice(lastHyphen + 1);
|
|
814
|
-
if (/^[A-Za-z0-9_-]{8,}$/.test(possibleHash)) {
|
|
815
|
-
return name.slice(0, lastHyphen);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
return name;
|
|
819
|
-
}
|
|
820
|
-
function pathToFileURL(filePath) {
|
|
821
|
-
const absPath = path6.resolve(filePath);
|
|
822
|
-
if (process.platform === "win32") {
|
|
823
|
-
return "file:///" + absPath.replace(/\\/g, "/");
|
|
824
|
-
}
|
|
825
|
-
return "file://" + absPath;
|
|
826
|
-
}
|
|
827
957
|
|
|
828
|
-
// src/
|
|
829
|
-
var
|
|
958
|
+
// src/ssg/index.ts
|
|
959
|
+
var log9 = logger("ssg");
|
|
830
960
|
function shopifySSG(options) {
|
|
831
961
|
return {
|
|
832
962
|
name: "vite-plugin-shopify:ssg",
|
|
@@ -835,23 +965,23 @@ function shopifySSG(options) {
|
|
|
835
965
|
return {};
|
|
836
966
|
},
|
|
837
967
|
async closeBundle() {
|
|
838
|
-
const manifestPath =
|
|
968
|
+
const manifestPath = path10.resolve(
|
|
839
969
|
options.themeRoot,
|
|
840
970
|
options.buildDir,
|
|
841
971
|
".vite",
|
|
842
972
|
"manifest.json"
|
|
843
973
|
);
|
|
844
|
-
if (!
|
|
845
|
-
|
|
974
|
+
if (!fs4.existsSync(manifestPath)) {
|
|
975
|
+
log9.warn("No manifest.json found, skipping SSG");
|
|
846
976
|
return;
|
|
847
977
|
}
|
|
848
|
-
|
|
849
|
-
const manifest = JSON.parse(
|
|
850
|
-
|
|
978
|
+
log9.debug("reading manifest from %s", manifestPath);
|
|
979
|
+
const manifest = JSON.parse(fs4.readFileSync(manifestPath, "utf-8"));
|
|
980
|
+
log9.info("Starting SSG compilation...");
|
|
851
981
|
await compileAllEntries(options, manifest);
|
|
852
|
-
|
|
982
|
+
log9.info("SSG compilation complete");
|
|
853
983
|
writeImportMapSnippet(options);
|
|
854
|
-
|
|
984
|
+
log9.debug("wrote import map snippet");
|
|
855
985
|
},
|
|
856
986
|
resolveId(id) {
|
|
857
987
|
if (id === "vite-plugin-shopify/runtime") {
|
|
@@ -870,7 +1000,7 @@ function shopifySSG(options) {
|
|
|
870
1000
|
};
|
|
871
1001
|
}
|
|
872
1002
|
function writeImportMapSnippet(options) {
|
|
873
|
-
const snippetPath =
|
|
1003
|
+
const snippetPath = path10.resolve(
|
|
874
1004
|
options.themeRoot,
|
|
875
1005
|
"snippets",
|
|
876
1006
|
options.snippetFile
|
|
@@ -887,11 +1017,30 @@ function writeImportMapSnippet(options) {
|
|
|
887
1017
|
"</script>",
|
|
888
1018
|
""
|
|
889
1019
|
].join("\n");
|
|
890
|
-
const dir =
|
|
891
|
-
if (!
|
|
892
|
-
|
|
1020
|
+
const dir = path10.dirname(snippetPath);
|
|
1021
|
+
if (!fs4.existsSync(dir)) {
|
|
1022
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
893
1023
|
}
|
|
894
|
-
|
|
1024
|
+
fs4.writeFileSync(snippetPath, content);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// src/hydration-fix/vite-plugin.ts
|
|
1028
|
+
import path11 from "path";
|
|
1029
|
+
function hydrationFix(options) {
|
|
1030
|
+
const sourceDir = path11.resolve(options.themeRoot, options.sourceCodeDir);
|
|
1031
|
+
return {
|
|
1032
|
+
name: "vite-plugin-shopify:hydration-fix",
|
|
1033
|
+
enforce: "pre",
|
|
1034
|
+
transform(code, id) {
|
|
1035
|
+
if (!/\.(tsx|jsx)$/.test(id)) return;
|
|
1036
|
+
if (!id.startsWith(sourceDir)) return;
|
|
1037
|
+
const { result, fixCount } = autoFixAdjacentText(code, id);
|
|
1038
|
+
if (fixCount > 0) {
|
|
1039
|
+
return result;
|
|
1040
|
+
}
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
895
1044
|
}
|
|
896
1045
|
|
|
897
1046
|
// src/index.ts
|
|
@@ -901,6 +1050,7 @@ var vitePluginShopify = (options = {}) => {
|
|
|
901
1050
|
enableDebug();
|
|
902
1051
|
}
|
|
903
1052
|
return [
|
|
1053
|
+
hydrationFix(resolvedOptions),
|
|
904
1054
|
shopifyConfig(resolvedOptions),
|
|
905
1055
|
shopifyEntries(resolvedOptions),
|
|
906
1056
|
shopifySSG(resolvedOptions)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-react-shopify",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Vite plugin for React Shopify themes",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -17,19 +17,11 @@
|
|
|
17
17
|
"default": "./dist/runtime/index.js"
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
|
-
"scripts": {
|
|
21
|
-
"dev": "tsup --watch",
|
|
22
|
-
"build": "tsup && cp dev-server-index.html dist/",
|
|
23
|
-
"typecheck": "tsc --noEmit",
|
|
24
|
-
"test": "vitest run",
|
|
25
|
-
"release": "bumpp && pnpm publish",
|
|
26
|
-
"release:patch": "bumpp --patch",
|
|
27
|
-
"release:minor": "bumpp --minor",
|
|
28
|
-
"release:major": "bumpp --major"
|
|
29
|
-
},
|
|
30
20
|
"dependencies": {
|
|
31
21
|
"debug": "^4.4.0",
|
|
32
|
-
"fast-glob": "^3.3.0"
|
|
22
|
+
"fast-glob": "^3.3.0",
|
|
23
|
+
"oxc-parser": "^0.133.0",
|
|
24
|
+
"oxc-walker": "^1.0.0"
|
|
33
25
|
},
|
|
34
26
|
"devDependencies": {
|
|
35
27
|
"@types/debug": "^4.1.0",
|
|
@@ -42,5 +34,15 @@
|
|
|
42
34
|
},
|
|
43
35
|
"peerDependencies": {
|
|
44
36
|
"vite": "^8.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"dev": "tsup --watch",
|
|
40
|
+
"build": "tsup && cp dev-server-index.html dist/",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"release": "bumpp && pnpm publish",
|
|
44
|
+
"release:patch": "bumpp --patch",
|
|
45
|
+
"release:minor": "bumpp --minor",
|
|
46
|
+
"release:major": "bumpp --major"
|
|
45
47
|
}
|
|
46
|
-
}
|
|
48
|
+
}
|