vite-plugin-react-shopify 2.1.0 → 2.2.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
@@ -394,32 +394,75 @@ import fs2 from "fs";
394
394
  import path6 from "path";
395
395
  import { createRequire } from "module";
396
396
 
397
- // src/ssg/hydration-fix.ts
397
+ // src/hydration-fix/index.ts
398
+ import { parseSync } from "oxc-parser";
399
+ import { walk } from "oxc-walker";
398
400
  var log4 = logger("hydration-fix");
399
401
  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}>`;
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
+ }
413
415
  }
414
- );
415
- fixed.push(replaced);
416
+ }
417
+ });
418
+ if (replacements.length === 0) {
419
+ return { result: source, fixCount: 0 };
416
420
  }
417
- if (fixCount > 0) {
418
- log4.warn(
419
- `auto-fixed ${fixCount} adjacent text+expression issue(s) in ${filePath}`
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;
421
465
  }
422
- return { result: fixed.join("\n"), fixCount };
423
466
  }
424
467
  function needsFix(content) {
425
468
  const trimmed = content.trim();
@@ -485,7 +528,8 @@ async function bundleEntry(entry, projectRoot, sourceDir) {
485
528
  if (fixCount2 > 0) {
486
529
  return { contents: result, loader: args.path.endsWith(".tsx") ? "tsx" : "jsx" };
487
530
  }
488
- } catch {
531
+ } catch (e) {
532
+ log5.debug("SSG hydration-fix failed for %s: %s", args.path, e);
489
533
  }
490
534
  return void 0;
491
535
  });
@@ -948,7 +992,7 @@ function shopifySSG(options) {
948
992
  if (id === "\0vite-plugin-shopify:runtime") {
949
993
  const exports = [
950
994
  `export { LiquidDataProvider, LiquidDataContext } from 'vite-plugin-shopify/runtime'`,
951
- `export { useLiquid, useLiquidValues, useSectionSettings, useBlockSettings, useSnippetParams, useBlockParams } from 'vite-plugin-shopify/runtime'`
995
+ `export { useLiquidValue, useLiquidValues, useSectionSettings, useBlockSettings, useSnippetParams, useBlockParams } from 'vite-plugin-shopify/runtime'`
952
996
  ];
953
997
  return exports.join("\n");
954
998
  }
@@ -980,6 +1024,25 @@ function writeImportMapSnippet(options) {
980
1024
  fs4.writeFileSync(snippetPath, content);
981
1025
  }
982
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
+ };
1044
+ }
1045
+
983
1046
  // src/index.ts
984
1047
  var vitePluginShopify = (options = {}) => {
985
1048
  const resolvedOptions = resolveOptions(options);
@@ -987,6 +1050,7 @@ var vitePluginShopify = (options = {}) => {
987
1050
  enableDebug();
988
1051
  }
989
1052
  return [
1053
+ hydrationFix(resolvedOptions),
990
1054
  shopifyConfig(resolvedOptions),
991
1055
  shopifyEntries(resolvedOptions),
992
1056
  shopifySSG(resolvedOptions)
@@ -1,13 +1,21 @@
1
1
  import * as react from 'react';
2
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
- };
3
+ declare function parseLiquidBoolean(value: string | boolean | undefined | null): boolean;
4
+ declare function parseLiquidNumber(value: string | number | undefined | null, defaultVal?: number): number;
5
+ type LiquidTypeMode = "string" | "number" | "boolean";
6
+ type Setter<T> = (val: T | ((prev: T) => T)) => void;
7
+ type ValueForMode<M extends LiquidTypeMode | undefined> = M extends "number" ? number : M extends "boolean" ? boolean : string | undefined;
8
+ declare function useLiquidValue(expr: string): [string | undefined, Setter<string | undefined>];
9
+ declare function useLiquidValue(expr: string, type: "string"): [string | undefined, Setter<string | undefined>];
10
+ declare function useLiquidValue(expr: string, type: "number"): [number, Setter<number>];
11
+ declare function useLiquidValue(expr: string, type: "boolean"): [boolean, Setter<boolean>];
12
+ type TypeModes<T extends Record<string, string>> = Partial<{
13
+ [K in keyof T & string]: LiquidTypeMode;
14
+ }>;
15
+ type InferValues<T extends Record<string, string>, Types extends TypeModes<T>> = {
16
+ [K in keyof T & string]: ValueForMode<Types[K]>;
10
17
  };
18
+ declare function useLiquidValues<T extends Record<string, string>, const Types extends TypeModes<T> = {}>(map: T, types?: Types): InferValues<T, Types>;
11
19
  declare function useSectionSettings(key: string): {
12
20
  value: string | undefined;
13
21
  };
@@ -21,12 +29,7 @@ declare function useBlockParams(key: string): {
21
29
  value: string | undefined;
22
30
  };
23
31
 
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
32
  declare const LiquidDataContext: react.Context<Record<string, any>>;
30
33
  declare const LiquidDataProvider: react.Provider<Record<string, any>>;
31
34
 
32
- export { LiquidDataContext, LiquidDataProvider, parseLiquidBoolean, parseLiquidNumber, useBlockParams, useBlockSettings, useLiquid, useLiquidValues, useSectionSettings, useSnippetParams };
35
+ export { LiquidDataContext, LiquidDataProvider, type LiquidTypeMode, parseLiquidBoolean, parseLiquidNumber, useBlockParams, useBlockSettings, useLiquidValue, useLiquidValues, useSectionSettings, useSnippetParams };
@@ -1,5 +1,5 @@
1
1
  // src/runtime/hooks.ts
2
- import { useContext } from "react";
2
+ import { useContext, useEffect, useState } from "react";
3
3
 
4
4
  // src/runtime/provider.ts
5
5
  import { createContext } from "react";
@@ -7,19 +7,19 @@ var LiquidDataContext = createContext({});
7
7
  var LiquidDataProvider = LiquidDataContext.Provider;
8
8
 
9
9
  // src/runtime/hooks.ts
10
- function useLiquid(expr) {
10
+ function useLiquidRaw(expr) {
11
11
  const data = useContext(LiquidDataContext);
12
12
  if (typeof globalThis.document === "undefined") {
13
13
  const tracker = globalThis.__shopify_ssg_liquid_track;
14
14
  if (tracker) tracker.add(expr);
15
- return { value: `{{ ${expr} }}` };
15
+ return `{{ ${expr} }}`;
16
16
  }
17
17
  if (Object.prototype.hasOwnProperty.call(data, expr)) {
18
- return { value: data[expr] };
18
+ return data[expr];
19
19
  }
20
- return { value: void 0 };
20
+ return void 0;
21
21
  }
22
- function useLiquidValues(map) {
22
+ function useLiquidRawValues(map) {
23
23
  const data = useContext(LiquidDataContext);
24
24
  if (typeof globalThis.document === "undefined") {
25
25
  const tracker = globalThis.__shopify_ssg_liquid_track;
@@ -28,25 +28,13 @@ function useLiquidValues(map) {
28
28
  if (tracker) tracker.add(expr);
29
29
  values2[key] = `{{ ${expr} }}`;
30
30
  }
31
- return { values: values2 };
31
+ return values2;
32
32
  }
33
33
  const values = {};
34
34
  for (const [key, expr] of Object.entries(map)) {
35
35
  values[key] = Object.prototype.hasOwnProperty.call(data, expr) ? data[expr] : void 0;
36
36
  }
37
- return { values };
38
- }
39
- function useSectionSettings(key) {
40
- return useLiquid(`section.settings.${key}`);
41
- }
42
- function useBlockSettings(key) {
43
- return useLiquid(`block.settings.${key}`);
44
- }
45
- function useSnippetParams(key) {
46
- return useLiquid(key);
47
- }
48
- function useBlockParams(key) {
49
- return useLiquid(key);
37
+ return values;
50
38
  }
51
39
  function parseLiquidBoolean(value) {
52
40
  if (typeof value === "boolean") return value;
@@ -59,6 +47,76 @@ function parseLiquidNumber(value, defaultVal = 0) {
59
47
  const num = Number(value);
60
48
  return Number.isNaN(num) ? defaultVal : num;
61
49
  }
50
+ function useLiquidValue(expr, type = "string") {
51
+ const raw = useLiquidRaw(expr);
52
+ const isSSR = typeof globalThis.document === "undefined";
53
+ let initialVal;
54
+ if (type === "boolean") {
55
+ initialVal = false;
56
+ } else if (type === "number") {
57
+ initialVal = isSSR ? raw : parseLiquidNumber(raw, 0);
58
+ } else {
59
+ initialVal = raw;
60
+ }
61
+ const [val, setVal] = useState(initialVal);
62
+ useEffect(() => {
63
+ if (type === "number") setVal(parseLiquidNumber(raw, 0));
64
+ else if (type === "boolean") setVal(parseLiquidBoolean(raw));
65
+ else setVal(raw);
66
+ }, [raw]);
67
+ return [val, setVal];
68
+ }
69
+ function useLiquidValues(map, types) {
70
+ const raw = useLiquidRawValues(map);
71
+ const keys = Object.keys(map);
72
+ const rawDep = keys.map((k) => raw[k]).join("\0");
73
+ const isSSR = typeof globalThis.document === "undefined";
74
+ const [parsed, setParsed] = useState(() => {
75
+ const vals = {};
76
+ for (const k of keys) {
77
+ const mode = types?.[k] ?? "string";
78
+ if (mode === "boolean") {
79
+ vals[k] = false;
80
+ } else if (mode === "number") {
81
+ vals[k] = isSSR ? raw[k] : parseLiquidNumber(raw[k], 0);
82
+ } else {
83
+ vals[k] = raw[k];
84
+ }
85
+ }
86
+ return vals;
87
+ });
88
+ useEffect(() => {
89
+ setParsed((prev) => {
90
+ let changed = false;
91
+ const next = { ...prev };
92
+ for (const k of keys) {
93
+ const mode = types?.[k] ?? "string";
94
+ let v;
95
+ if (mode === "number") v = parseLiquidNumber(raw[k], 0);
96
+ else if (mode === "boolean") v = parseLiquidBoolean(raw[k]);
97
+ else v = raw[k];
98
+ if (v !== prev[k]) {
99
+ next[k] = v;
100
+ changed = true;
101
+ }
102
+ }
103
+ return changed ? next : prev;
104
+ });
105
+ }, [rawDep]);
106
+ return parsed;
107
+ }
108
+ function useSectionSettings(key) {
109
+ return { value: useLiquidRaw(`section.settings.${key}`) };
110
+ }
111
+ function useBlockSettings(key) {
112
+ return { value: useLiquidRaw(`block.settings.${key}`) };
113
+ }
114
+ function useSnippetParams(key) {
115
+ return { value: useLiquidRaw(key) };
116
+ }
117
+ function useBlockParams(key) {
118
+ return { value: useLiquidRaw(key) };
119
+ }
62
120
  export {
63
121
  LiquidDataContext,
64
122
  LiquidDataProvider,
@@ -66,7 +124,7 @@ export {
66
124
  parseLiquidNumber,
67
125
  useBlockParams,
68
126
  useBlockSettings,
69
- useLiquid,
127
+ useLiquidValue,
70
128
  useLiquidValues,
71
129
  useSectionSettings,
72
130
  useSnippetParams
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-react-shopify",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Vite plugin for React Shopify themes",
5
5
  "files": [
6
6
  "dist"
@@ -19,7 +19,9 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "debug": "^4.4.0",
22
- "fast-glob": "^3.3.0"
22
+ "fast-glob": "^3.3.0",
23
+ "oxc-parser": "^0.133.0",
24
+ "oxc-walker": "^1.0.0"
23
25
  },
24
26
  "devDependencies": {
25
27
  "@types/debug": "^4.1.0",