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.
Files changed (3) hide show
  1. package/dist/index.d.ts +14 -11
  2. package/dist/index.js +520 -370
  3. 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
- type ShopifyBlockType = "template" | "section" | "block" | "snippet";
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: string;
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/plugin/options.ts
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/plugin/logger.ts
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/plugin/config.ts
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/plugin/entries.ts
149
+ // src/core/entries.ts
150
150
  import path4 from "path";
151
151
  import { normalizePath as normalizePath2 } from "vite";
152
152
 
153
- // src/plugin/ssg/scanner.ts
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: componentName }
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/plugin/entries.ts
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/plugin/ssg/index.ts
278
- import fs2 from "fs";
279
- import path7 from "path";
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/plugin/ssg/compiler.ts
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/plugin/ssg/post-process.ts
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(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#x27;/g, "'");
299
582
  }
300
583
 
301
- // src/plugin/ssg/liquid.ts
302
- import path5 from "path";
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/plugin/ssg/schema-gen.ts
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/plugin/ssg/liquid.ts
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
- function getOutputPath(entry, options) {
504
- const type = entry.meta.type ?? entry.targetType;
505
- const dirName = typeToDir(type);
506
- const fileName = resolveFileName(entry, type, options);
507
- return path5.join(options.themeRoot, dirName, fileName);
508
- }
509
- function typeToDir(type) {
510
- if (type === "snippet") return "snippets";
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 resolveFileName(entry, type, options) {
520
- if (options.outputName) {
521
- return options.outputName.replace(/\{type\}/g, type).replace(/\{kebab\}/g, entry.kebabName).replace(/\{pascal\}/g, entry.componentName).replace(/\{target\}/g, entry.targetType) + ".liquid";
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
- const prefix = options.prefix[type] ?? "react-";
524
- return `${prefix}${entry.kebabName}.liquid`;
868
+ return null;
525
869
  }
526
870
 
527
- // src/plugin/ssg/hydration-fix.ts
528
- var log4 = logger("hydration-fix");
529
- function autoFixAdjacentText(source, filePath) {
530
- let fixCount = 0;
531
- const lines = source.split("\n");
532
- const fixed = [];
533
- for (let i = 0; i < lines.length; i++) {
534
- const line = lines[i];
535
- const replaced = line.replace(
536
- /<(\w+)([^>]*?)>([^<]*?\{[^}]*\}[^<]*?)<\/\1>/g,
537
- (match, tagName, attrs, content) => {
538
- const trimmed = content.trim();
539
- if (!needsFix(trimmed)) return match;
540
- fixCount++;
541
- const tpl = trimmed.replace(/\{([^}]+)\}/g, "${$1}");
542
- return `<${tagName}${attrs}>{\`${tpl}\`}</${tagName}>`;
543
- }
544
- );
545
- fixed.push(replaced);
546
- }
547
- if (fixCount > 0) {
548
- log4.warn(
549
- `auto-fixed ${fixCount} adjacent text+expression issue(s) in ${filePath}`
550
- );
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
- return { result: fixed.join("\n"), fixCount };
553
- }
554
- function needsFix(content) {
555
- const trimmed = content.trim();
556
- if (!trimmed) return false;
557
- if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
558
- const inner = trimmed.slice(1, -1).trim();
559
- if (inner.startsWith("`") && inner.endsWith("`")) return false;
560
- if (inner.length > 0 && !/<[a-zA-Z]/.test(inner)) return false;
883
+ for (const w of warnings) {
884
+ log7.warn(w);
561
885
  }
562
- if (!/\{/.test(trimmed)) return false;
563
- if (/<[a-zA-Z]/.test(trimmed)) return false;
564
- return true;
886
+ return warnings;
565
887
  }
566
888
 
567
- // src/plugin/ssg/compiler.ts
568
- var log5 = logger("ssg:compiler");
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
- log5.debug("found %d entries to compile", entries.length);
573
- const projectRoot = path6.resolve(options.themeRoot);
574
- const sourceDir = path6.resolve(options.themeRoot, options.sourceCodeDir);
575
- const entryCssFiles = /* @__PURE__ */ new Map();
576
- const cssRefCount = /* @__PURE__ */ new Map();
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
- const cssFiles = entryCssFiles.get(entry.kebabName) || [];
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
- log5.error("Failed to compile %s:", entry.filePath, err);
903
+ log8.error("Failed to compile %s:", entry.filePath, err);
629
904
  }
630
905
  }
631
- log5.info("Compiled %d entries", entries.length);
632
- const tmpDir = path6.join(sourceDir, ".ssg-tmp");
906
+ log8.info("Compiled %d entries", entries.length);
907
+ const tmpDir = path9.join(sourceDir, ".ssg-tmp");
633
908
  try {
634
- fs.rmSync(tmpDir, { recursive: true, force: true });
909
+ fs3.rmSync(tmpDir, { recursive: true, force: true });
635
910
  } catch {
636
911
  }
637
912
  }
638
- async function compileEntry(entry, options, manifest, projectRoot, sourceDir, cssInline, cssSnippets) {
639
- const projectRequire = createRequire(path6.join(projectRoot, "package.json"));
640
- let createElement;
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 mod = await import(pathToFileURL(tmpFile));
728
- const Component = mod.default;
729
- const shopifyMeta = mod.shopifyMeta;
730
- if (!Component) {
731
- log5.warn("No default export found in %s, skipping", entry.filePath);
732
- return;
733
- }
734
- if (shopifyMeta) {
735
- entry.meta = { ...entry.meta, ...shopifyMeta };
736
- }
737
- globalThis.__shopify_ssg_target = entry.targetType;
738
- const trackedExpressions = /* @__PURE__ */ new Set();
739
- globalThis.__shopify_ssg_liquid_track = trackedExpressions;
740
- const element = createElement(Component);
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, { inline: cssInline, snippets: cssSnippets }, {
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 = path6.dirname(outputPath);
758
- if (!fs.existsSync(dir)) {
759
- fs.mkdirSync(dir, { recursive: true });
945
+ const dir = path9.dirname(outputPath);
946
+ if (!fs3.existsSync(dir)) {
947
+ fs3.mkdirSync(dir, { recursive: true });
760
948
  }
761
- fs.writeFileSync(outputPath, liquidContent);
949
+ fs3.writeFileSync(outputPath, liquidContent);
762
950
  } finally {
763
951
  try {
764
- fs.unlinkSync(tmpFile);
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/plugin/ssg/index.ts
829
- var log6 = logger("ssg");
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 = path7.resolve(
968
+ const manifestPath = path10.resolve(
839
969
  options.themeRoot,
840
970
  options.buildDir,
841
971
  ".vite",
842
972
  "manifest.json"
843
973
  );
844
- if (!fs2.existsSync(manifestPath)) {
845
- log6.warn("No manifest.json found, skipping SSG");
974
+ if (!fs4.existsSync(manifestPath)) {
975
+ log9.warn("No manifest.json found, skipping SSG");
846
976
  return;
847
977
  }
848
- log6.debug("reading manifest from %s", manifestPath);
849
- const manifest = JSON.parse(fs2.readFileSync(manifestPath, "utf-8"));
850
- log6.info("Starting SSG compilation...");
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
- log6.info("SSG compilation complete");
982
+ log9.info("SSG compilation complete");
853
983
  writeImportMapSnippet(options);
854
- log6.debug("wrote import map snippet");
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 = path7.resolve(
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 = path7.dirname(snippetPath);
891
- if (!fs2.existsSync(dir)) {
892
- fs2.mkdirSync(dir, { recursive: true });
1020
+ const dir = path10.dirname(snippetPath);
1021
+ if (!fs4.existsSync(dir)) {
1022
+ fs4.mkdirSync(dir, { recursive: true });
893
1023
  }
894
- fs2.writeFileSync(snippetPath, content);
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.0.0",
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
+ }