vite-plugin-react-shopify 1.1.0 → 2.0.0

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