vite-plugin-react-shopify 2.0.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/dist/index.d.ts +14 -11
- package/dist/index.js +456 -370
- package/package.json +12 -12
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,250 @@ 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/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
|
+
|
|
525
|
+
// src/ssg/post-process.ts
|
|
287
526
|
var VOID_ELEMENTS = /<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)([^>]*)\/>/g;
|
|
288
527
|
function normalizeVoidElements(html) {
|
|
289
528
|
return html.replace(VOID_ELEMENTS, "<$1$2>");
|
|
@@ -298,21 +537,63 @@ function unwrapHtmlEntities(html) {
|
|
|
298
537
|
return html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'");
|
|
299
538
|
}
|
|
300
539
|
|
|
301
|
-
// src/
|
|
302
|
-
|
|
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
|
+
}
|
|
303
590
|
|
|
304
|
-
// src/
|
|
305
|
-
var log3 = logger("schema-gen");
|
|
591
|
+
// src/ssg/schema.ts
|
|
306
592
|
function serializeSetting(setting) {
|
|
307
593
|
const s = { type: setting.type };
|
|
308
594
|
if ("id" in setting) s.id = setting.id;
|
|
309
595
|
if ("label" in setting) s.label = setting.label;
|
|
310
596
|
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
597
|
s.default = setting.default;
|
|
317
598
|
}
|
|
318
599
|
if ("info" in setting && setting.info) s.info = setting.info;
|
|
@@ -375,7 +656,33 @@ ${json}
|
|
|
375
656
|
`;
|
|
376
657
|
}
|
|
377
658
|
|
|
378
|
-
// src/
|
|
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
|
|
379
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";
|
|
380
687
|
function assembleLiquidFile(html, entry, scriptAsset, cssContents, options, trackedExpressions = []) {
|
|
381
688
|
const type = entry.meta.type ?? entry.targetType;
|
|
@@ -500,251 +807,88 @@ function buildSnippet(html, entry, trackedExpressions) {
|
|
|
500
807
|
);
|
|
501
808
|
return lines;
|
|
502
809
|
}
|
|
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;
|
|
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;
|
|
518
818
|
}
|
|
519
|
-
function
|
|
520
|
-
if (
|
|
521
|
-
|
|
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`;
|
|
522
823
|
}
|
|
523
|
-
|
|
524
|
-
return `${prefix}${entry.kebabName}.liquid`;
|
|
824
|
+
return null;
|
|
525
825
|
}
|
|
526
826
|
|
|
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
|
-
);
|
|
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
|
+
}
|
|
551
838
|
}
|
|
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;
|
|
839
|
+
for (const w of warnings) {
|
|
840
|
+
log7.warn(w);
|
|
561
841
|
}
|
|
562
|
-
|
|
563
|
-
if (/<[a-zA-Z]/.test(trimmed)) return false;
|
|
564
|
-
return true;
|
|
842
|
+
return warnings;
|
|
565
843
|
}
|
|
566
844
|
|
|
567
|
-
// src/
|
|
568
|
-
var
|
|
845
|
+
// src/ssg/compiler.ts
|
|
846
|
+
var log8 = logger("ssg:compiler");
|
|
569
847
|
async function compileAllEntries(options, manifest) {
|
|
570
848
|
const entries = scanEntries(options);
|
|
571
849
|
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
|
-
}
|
|
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);
|
|
613
855
|
for (const entry of entries) {
|
|
614
856
|
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);
|
|
857
|
+
await compileEntry(entry, options, manifest, projectRoot, sourceDir, entryCssFiles, cssSnippetMap);
|
|
627
858
|
} catch (err) {
|
|
628
|
-
|
|
859
|
+
log8.error("Failed to compile %s:", entry.filePath, err);
|
|
629
860
|
}
|
|
630
861
|
}
|
|
631
|
-
|
|
632
|
-
const tmpDir =
|
|
862
|
+
log8.info("Compiled %d entries", entries.length);
|
|
863
|
+
const tmpDir = path9.join(sourceDir, ".ssg-tmp");
|
|
633
864
|
try {
|
|
634
|
-
|
|
865
|
+
fs3.rmSync(tmpDir, { recursive: true, force: true });
|
|
635
866
|
} catch {
|
|
636
867
|
}
|
|
637
868
|
}
|
|
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);
|
|
869
|
+
async function compileEntry(entry, options, manifest, projectRoot, sourceDir, entryCssFiles, cssSnippetMap) {
|
|
870
|
+
const bundleResult = await bundleEntry(entry, projectRoot, sourceDir);
|
|
871
|
+
if (!bundleResult) return;
|
|
726
872
|
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);
|
|
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
|
+
);
|
|
746
887
|
const scriptAsset = resolveScriptAsset(entry.kebabName, manifest);
|
|
747
|
-
const liquidContent = assembleLiquidFile(html, entry, scriptAsset, {
|
|
888
|
+
const liquidContent = assembleLiquidFile(html, entry, scriptAsset, {
|
|
889
|
+
inline: cssInline,
|
|
890
|
+
snippets: cssSnippets
|
|
891
|
+
}, {
|
|
748
892
|
prefix: options.ssg.prefix,
|
|
749
893
|
outputName: options.ssg.outputName || void 0,
|
|
750
894
|
buildDir: options.buildDir
|
|
@@ -754,79 +898,21 @@ async function compileEntry(entry, options, manifest, projectRoot, sourceDir, cs
|
|
|
754
898
|
outputName: options.ssg.outputName || void 0,
|
|
755
899
|
themeRoot: options.themeRoot
|
|
756
900
|
});
|
|
757
|
-
const dir =
|
|
758
|
-
if (!
|
|
759
|
-
|
|
901
|
+
const dir = path9.dirname(outputPath);
|
|
902
|
+
if (!fs3.existsSync(dir)) {
|
|
903
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
760
904
|
}
|
|
761
|
-
|
|
905
|
+
fs3.writeFileSync(outputPath, liquidContent);
|
|
762
906
|
} finally {
|
|
763
907
|
try {
|
|
764
|
-
|
|
765
|
-
} catch {
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
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");
|
|
908
|
+
fs3.unlinkSync(bundleResult.tmpFile);
|
|
804
909
|
} 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
910
|
}
|
|
817
911
|
}
|
|
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
912
|
}
|
|
827
913
|
|
|
828
|
-
// src/
|
|
829
|
-
var
|
|
914
|
+
// src/ssg/index.ts
|
|
915
|
+
var log9 = logger("ssg");
|
|
830
916
|
function shopifySSG(options) {
|
|
831
917
|
return {
|
|
832
918
|
name: "vite-plugin-shopify:ssg",
|
|
@@ -835,23 +921,23 @@ function shopifySSG(options) {
|
|
|
835
921
|
return {};
|
|
836
922
|
},
|
|
837
923
|
async closeBundle() {
|
|
838
|
-
const manifestPath =
|
|
924
|
+
const manifestPath = path10.resolve(
|
|
839
925
|
options.themeRoot,
|
|
840
926
|
options.buildDir,
|
|
841
927
|
".vite",
|
|
842
928
|
"manifest.json"
|
|
843
929
|
);
|
|
844
|
-
if (!
|
|
845
|
-
|
|
930
|
+
if (!fs4.existsSync(manifestPath)) {
|
|
931
|
+
log9.warn("No manifest.json found, skipping SSG");
|
|
846
932
|
return;
|
|
847
933
|
}
|
|
848
|
-
|
|
849
|
-
const manifest = JSON.parse(
|
|
850
|
-
|
|
934
|
+
log9.debug("reading manifest from %s", manifestPath);
|
|
935
|
+
const manifest = JSON.parse(fs4.readFileSync(manifestPath, "utf-8"));
|
|
936
|
+
log9.info("Starting SSG compilation...");
|
|
851
937
|
await compileAllEntries(options, manifest);
|
|
852
|
-
|
|
938
|
+
log9.info("SSG compilation complete");
|
|
853
939
|
writeImportMapSnippet(options);
|
|
854
|
-
|
|
940
|
+
log9.debug("wrote import map snippet");
|
|
855
941
|
},
|
|
856
942
|
resolveId(id) {
|
|
857
943
|
if (id === "vite-plugin-shopify/runtime") {
|
|
@@ -870,7 +956,7 @@ function shopifySSG(options) {
|
|
|
870
956
|
};
|
|
871
957
|
}
|
|
872
958
|
function writeImportMapSnippet(options) {
|
|
873
|
-
const snippetPath =
|
|
959
|
+
const snippetPath = path10.resolve(
|
|
874
960
|
options.themeRoot,
|
|
875
961
|
"snippets",
|
|
876
962
|
options.snippetFile
|
|
@@ -887,11 +973,11 @@ function writeImportMapSnippet(options) {
|
|
|
887
973
|
"</script>",
|
|
888
974
|
""
|
|
889
975
|
].join("\n");
|
|
890
|
-
const dir =
|
|
891
|
-
if (!
|
|
892
|
-
|
|
976
|
+
const dir = path10.dirname(snippetPath);
|
|
977
|
+
if (!fs4.existsSync(dir)) {
|
|
978
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
893
979
|
}
|
|
894
|
-
|
|
980
|
+
fs4.writeFileSync(snippetPath, content);
|
|
895
981
|
}
|
|
896
982
|
|
|
897
983
|
// src/index.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-react-shopify",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Vite plugin for React Shopify themes",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -17,16 +17,6 @@
|
|
|
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
22
|
"fast-glob": "^3.3.0"
|
|
@@ -42,5 +32,15 @@
|
|
|
42
32
|
},
|
|
43
33
|
"peerDependencies": {
|
|
44
34
|
"vite": "^8.0.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"build": "tsup && cp dev-server-index.html dist/",
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"release": "bumpp && pnpm publish",
|
|
42
|
+
"release:patch": "bumpp --patch",
|
|
43
|
+
"release:minor": "bumpp --minor",
|
|
44
|
+
"release:major": "bumpp --major"
|
|
45
45
|
}
|
|
46
|
-
}
|
|
46
|
+
}
|