vite-plugin-react-shopify 2.2.3 → 2.2.6

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  import { Plugin } from 'vite';
2
2
 
3
+ /**
4
+ * @file User-facing configuration options for the Vite plugin.
5
+ *
6
+ * All fields are optional with sensible defaults applied at resolution time
7
+ * by {@link resolveOptions} (core/options.ts).
8
+ */
9
+ /** Top-level plugin options passed to `vitePluginShopify()`. */
3
10
  interface Options {
4
11
  themeRoot?: string;
5
12
  sourceCodeDir?: string;
@@ -9,6 +16,7 @@ interface Options {
9
16
  ssg?: SSGOptions;
10
17
  importMap?: ImportMapOptions;
11
18
  }
19
+ /** Static Site Generation configuration. */
12
20
  interface SSGOptions {
13
21
  directories?: string[];
14
22
  prefix?: {
@@ -20,11 +28,20 @@ interface SSGOptions {
20
28
  outputName?: string;
21
29
  cssPrefix?: string;
22
30
  }
31
+ /** CDN URLs for the import map snippet injected into the theme. */
23
32
  interface ImportMapOptions {
24
33
  react?: string;
25
34
  reactDomClient?: string;
26
35
  }
27
36
 
37
+ /**
38
+ * @file TypeScript type definitions for Shopify theme setting schemas.
39
+ *
40
+ * Covers every Shopify setting type (text, checkbox, color, image_picker,
41
+ * etc.) as well as informational sidebar types (header, paragraph). Also
42
+ * includes utility types for type-level validation ({@link AssertNoEmptyDefaults})
43
+ * and schema inference ({@link InferSettings}).
44
+ */
28
45
  interface BaseSettingSchema {
29
46
  id: string;
30
47
  label: string;
@@ -238,39 +255,126 @@ type InferSettings<T extends readonly {
238
255
  [K in T[number] as K["id"]]: ValueForType<K["type"]>;
239
256
  };
240
257
 
258
+ /**
259
+ * @file Shopify block/section metadata types.
260
+ *
261
+ * Defines {@link ShopifyMeta} — the shape of the `shopifyMeta` export from
262
+ * React components — along with supporting types for presets and block
263
+ * definitions. This metadata drives schema generation and Liquid output.
264
+ */
265
+
266
+ /** The four Shopify theme component categories. */
241
267
  type ShopifyBlockType = "template" | "section" | "block" | "snippet";
268
+ /**
269
+ * A block definition within a section's `blocks` attribute.
270
+ *
271
+ * Per the Shopify section schema, each block entry supports the following:
272
+ *
273
+ * | Attribute | Required | Description |
274
+ * | ---------- | -------- | -------------------------------------------------------------------- |
275
+ * | `type` | Yes | Free-form block type identifier. |
276
+ * | `name` | No | Block name shown as the block title in the theme editor. Auto-derived from `type` (e.g. `text-block` → `Text Block`) if omitted. |
277
+ * | `limit` | No | Max number of blocks of this type that can be used. |
278
+ * | `settings` | No | Input or sidebar settings exposed to the merchant for this block. |
279
+ */
280
+ interface BlockDefinition {
281
+ type: string;
282
+ name?: string;
283
+ limit?: number;
284
+ settings?: SettingSchema[];
285
+ }
286
+ /**
287
+ * Template/group scope filter used by `enabled_on` / `disabled_on`.
288
+ *
289
+ * Per the Shopify section schema, at least one of `templates` or `groups`
290
+ * must be provided. `enabled_on` and `disabled_on` are mutually exclusive —
291
+ * a section may declare one but not both.
292
+ *
293
+ * | Attribute | Description |
294
+ * | ----------- | ---------------------------------------------------------------------------------------- |
295
+ * | `templates` | Page types the section is restricted to (or excluded from). Use `["*"]` for all. |
296
+ * | `groups` | Section group types: `header`, `footer`, `aside`, or `custom.<NAME>`. Use `["*"]` for all. |
297
+ */
298
+ interface TemplateScope {
299
+ templates?: string[];
300
+ groups?: string[];
301
+ }
302
+ /**
303
+ * Section-level translation overrides for the schema.
304
+ *
305
+ * Each entry maps a language code (e.g. `"en"`, `"fr"`) to a flat key/value
306
+ * translation map. When emitted, translations are accessed in Liquid via
307
+ * the `t` filter using the key `sections.<section-name>.<key>`.
308
+ */
309
+ type SectionLocales = Record<string, Record<string, string>>;
310
+ /**
311
+ * Metadata for a Shopify section, block, snippet, or template.
312
+ *
313
+ * Exported as `shopifyMeta` from the React component file. Drives the
314
+ * generated {% schema %} block and influences Liquid wrapper output.
315
+ */
242
316
  interface ShopifyMeta {
243
317
  type?: ShopifyBlockType;
244
318
  name?: string;
245
- tag?: string;
319
+ /**
320
+ * HTML wrapper tag. Use `null` to render without a wrapper (blocks only).
321
+ *
322
+ * For sections Shopify only accepts a fixed set of tags (`article`,
323
+ * `aside`, `div`, `footer`, `header`, `section`); for blocks any string
324
+ * up to 50 chars is accepted.
325
+ */
326
+ tag?: string | null;
246
327
  class?: string;
247
328
  limit?: number;
248
329
  params?: string[];
249
330
  settings?: SettingSchema[];
250
- blocks?: {
251
- type: string;
252
- name?: string;
253
- settings?: SettingSchema[];
254
- }[];
331
+ blocks?: BlockDefinition[];
255
332
  max_blocks?: number;
256
333
  presets?: PresetDefinition[];
257
- enabled_on?: Record<string, string>[];
258
- disabled_on?: Record<string, string>[];
259
- templates?: string[];
260
- }
334
+ /**
335
+ * Default configuration used when a section is statically rendered. Has
336
+ * the same shape as a {@link PresetDefinition}.
337
+ */
338
+ default?: PresetDefinition;
339
+ /**
340
+ * Inline translation overrides for the section, scoped to the
341
+ * theme editor's **Sections** tab.
342
+ */
343
+ locales?: SectionLocales;
344
+ enabled_on?: TemplateScope;
345
+ disabled_on?: TemplateScope;
346
+ }
347
+ /** A theme editor preset definition within the schema. */
261
348
  interface PresetDefinition {
262
349
  name: string;
263
350
  category?: string;
264
351
  settings?: InputSettings;
265
352
  blocks?: PresetBlock[];
266
353
  }
267
- interface PresetBlock {
354
+ interface CommonBlockPreset {
268
355
  type: string;
269
- id?: string;
270
- static?: boolean;
356
+ name?: string;
271
357
  settings?: InputSettings;
272
358
  blocks?: PresetBlock[];
273
359
  }
360
+ interface ShopifyNormalBlockPreset extends CommonBlockPreset {
361
+ id?: never;
362
+ static?: never;
363
+ }
364
+ interface ShopifyStaticBlockPreset extends CommonBlockPreset {
365
+ id: string;
366
+ static: true;
367
+ }
368
+ /** A nested block preset inside a parent preset. */
369
+ type PresetBlock = ShopifyNormalBlockPreset | ShopifyStaticBlockPreset;
370
+
371
+ /**
372
+ * @file SSG entry type representing a discovered React component target.
373
+ *
374
+ * Each entry maps to one Liquid output file (section, block, snippet, or
375
+ * template). The {@link SSGEntry.meta} field carries {@link ShopifyMeta}
376
+ * merged with auto-derived defaults.
377
+ */
274
378
 
275
379
  interface SSGEntry {
276
380
  filePath: string;
@@ -280,6 +384,29 @@ interface SSGEntry {
280
384
  meta: Required<Pick<ShopifyMeta, "name">> & ShopifyMeta;
281
385
  }
282
386
 
387
+ /**
388
+ * @file Main entry point for the `vite-plugin-react-shopify` package.
389
+ *
390
+ * Composes four Vite plugins (hydration-fix, config, entries, SSG) into
391
+ * a single array returned to the user's `vite.config.ts`. Also re-exports
392
+ * all public types for consumers.
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * // vite.config.ts
397
+ * import vitePluginShopify from 'vite-plugin-react-shopify'
398
+ * export default {
399
+ * plugins: [vitePluginShopify({ themeRoot: '.', sourceCodeDir: 'frontend' })],
400
+ * }
401
+ * ```
402
+ */
403
+
404
+ /**
405
+ * Create the complete set of Vite plugins for React Shopify theme development.
406
+ *
407
+ * @param options User configuration (all fields optional).
408
+ * @returns An array of Vite plugin instances.
409
+ */
283
410
  declare const vitePluginShopify: (options?: Options) => Plugin[];
284
411
 
285
- export { type ArticleListSetting, type ArticleSetting, type AssertNoEmptyDefaults, type BlogSetting, type CheckboxSetting, type CollectionListSetting, type CollectionSetting, type ColorBackgroundSetting, type ColorSchemeGroupSetting, type ColorSchemeRole, type ColorSchemeSetting, type ColorSetting, type FontPickerSetting, type HeaderSetting, type HtmlSetting, type ImagePickerSetting, type ImportMapOptions, type InferSettings, type InlineRichtextSetting, type InputSettingSchema, type InputSettings, type LineBreakSetting, type LinkListSetting, type LiquidSetting, type MetaobjectListSetting, type MetaobjectSetting, type NumberSetting, type Options, type PageSetting, type ParagraphSetting, type PresetBlock, type PresetDefinition, type ProductListSetting, type ProductSetting, type RadioSetting, type RangeSetting, type RichtextSetting, type SSGEntry, type SSGOptions, type SchemaSetting, type SelectSetting, type SettingSchema, type SettingType, type SettingValue, type ShopifyBlockType, type ShopifyMeta, type SidebarSetting, type TextAlignmentSetting, type TextSetting, type TextareaSetting, type UrlSetting, type VideoSetting, type VideoUrlSetting, vitePluginShopify as default };
412
+ export { type ArticleListSetting, type ArticleSetting, type AssertNoEmptyDefaults, type BlockDefinition, type BlogSetting, type CheckboxSetting, type CollectionListSetting, type CollectionSetting, type ColorBackgroundSetting, type ColorSchemeGroupSetting, type ColorSchemeRole, type ColorSchemeSetting, type ColorSetting, type FontPickerSetting, type HeaderSetting, type HtmlSetting, type ImagePickerSetting, type ImportMapOptions, type InferSettings, type InlineRichtextSetting, type InputSettingSchema, type InputSettings, type LineBreakSetting, type LinkListSetting, type LiquidSetting, type MetaobjectListSetting, type MetaobjectSetting, type NumberSetting, type Options, type PageSetting, type ParagraphSetting, type PresetBlock, type PresetDefinition, type ProductListSetting, type ProductSetting, type RadioSetting, type RangeSetting, type RichtextSetting, type SSGEntry, type SSGOptions, type SchemaSetting, type SectionLocales, type SelectSetting, type SettingSchema, type SettingType, type SettingValue, type ShopifyBlockType, type ShopifyMeta, type SidebarSetting, type TemplateScope, type TextAlignmentSetting, type TextSetting, type TextareaSetting, type UrlSetting, type VideoSetting, type VideoUrlSetting, vitePluginShopify as default };
package/dist/index.js CHANGED
@@ -154,6 +154,24 @@ import { normalizePath as normalizePath2 } from "vite";
154
154
  import path3 from "path";
155
155
  import glob from "fast-glob";
156
156
  import { normalizePath } from "vite";
157
+
158
+ // src/validate/rules.ts
159
+ var MAX_NAME_LENGTH = 25;
160
+ function checkNameLength(meta, kebabName) {
161
+ if (meta.name.length > MAX_NAME_LENGTH) {
162
+ return `[${kebabName}] shopifyMeta.name "${meta.name}" is ${meta.name.length} chars (Shopify limit: ${MAX_NAME_LENGTH})`;
163
+ }
164
+ return null;
165
+ }
166
+ function checkEmptyStringDefault(setting) {
167
+ if (setting.default === "") {
168
+ const label = "id" in setting && setting.id ? setting.id : "(no id)";
169
+ return `Setting "${label}" (type: ${setting.type}) has empty string default`;
170
+ }
171
+ return null;
172
+ }
173
+
174
+ // src/ssg/scanner.ts
157
175
  var TYPE_BY_DIR = {
158
176
  templates: "template",
159
177
  sections: "section",
@@ -188,7 +206,7 @@ function toKebabCase(str) {
188
206
  }
189
207
  function deriveName(fileName) {
190
208
  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;
209
+ return readable.length > MAX_NAME_LENGTH ? readable.slice(0, MAX_NAME_LENGTH) : readable;
192
210
  }
193
211
 
194
212
  // src/core/entry-template.ts
@@ -295,98 +313,73 @@ import path9 from "path";
295
313
  // src/ssg/css-manager.ts
296
314
  import fs from "fs";
297
315
  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
316
  function analyzeCssDistribution(entries, manifest) {
343
317
  const entryCssFiles = /* @__PURE__ */ new Map();
344
318
  const cssRefCount = /* @__PURE__ */ new Map();
345
319
  for (const entry of entries) {
346
320
  const manifestKey = `shopify:entry:${entry.kebabName}`;
347
- const files = collectCssFiles(manifestKey, manifest);
348
- entryCssFiles.set(entry.kebabName, files);
349
- for (const f of files) {
321
+ const chunk = manifest[manifestKey];
322
+ if (!chunk) continue;
323
+ const cssFiles = collectCssFiles(chunk, manifest, /* @__PURE__ */ new Set());
324
+ entryCssFiles.set(entry.kebabName, cssFiles);
325
+ for (const f of cssFiles) {
350
326
  cssRefCount.set(f, (cssRefCount.get(f) || 0) + 1);
351
327
  }
352
- log3.debug("entry %s has %d CSS files", entry.kebabName, files.length);
353
328
  }
354
329
  return { entryCssFiles, cssRefCount };
355
330
  }
356
331
  function generateSharedCssSnippets(cssRefCount, options) {
357
332
  const cssSnippetMap = /* @__PURE__ */ new Map();
333
+ const snippetsDir = path5.resolve(options.themeRoot, "snippets");
334
+ fs.mkdirSync(snippetsDir, { recursive: true });
358
335
  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()}
336
+ if (count < 2) continue;
337
+ const cssName = path5.basename(cssFile, path5.extname(cssFile));
338
+ const snippetName = `${options.ssg.cssPrefix || "css-"}${cssName}`;
339
+ const cssContent = readCssFile(cssFile, options.buildDir, options.themeRoot);
340
+ fs.writeFileSync(
341
+ path5.join(snippetsDir, `${snippetName}.liquid`),
342
+ `{% stylesheet %}
343
+ ${cssContent}
376
344
  {% 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
- }
345
+ `
346
+ );
347
+ cssSnippetMap.set(cssFile, snippetName);
383
348
  }
384
349
  return cssSnippetMap;
385
350
  }
386
351
  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 };
352
+ const inline = [];
353
+ const snippets = [];
354
+ for (const f of cssFiles) {
355
+ const snippetName = cssSnippetMap.get(f);
356
+ if (snippetName) {
357
+ snippets.push(snippetName);
358
+ } else {
359
+ inline.push(f);
360
+ }
361
+ }
362
+ return { inline, snippets };
363
+ }
364
+ function readCssFileContents(cssFiles, buildDir, themeRoot) {
365
+ return cssFiles.map((f) => readCssFile(f, buildDir, themeRoot));
366
+ }
367
+ function readCssFile(cssFile, buildDir, themeRoot) {
368
+ return fs.readFileSync(path5.resolve(themeRoot, buildDir, cssFile), "utf-8");
369
+ }
370
+ function collectCssFiles(chunk, manifest, visited) {
371
+ if (visited.has(chunk.file)) return [];
372
+ visited.add(chunk.file);
373
+ const css = [...chunk.css || []];
374
+ if (chunk.imports) {
375
+ for (const imp of chunk.imports) {
376
+ const child = manifest[imp];
377
+ if (child) {
378
+ css.push(...collectCssFiles(child, manifest, visited));
379
+ }
380
+ }
381
+ }
382
+ return css;
390
383
  }
391
384
 
392
385
  // src/ssg/bundler.ts
@@ -397,11 +390,11 @@ import { createRequire } from "module";
397
390
  // src/hydration-fix/index.ts
398
391
  import { parseSync } from "oxc-parser";
399
392
  import { walk } from "oxc-walker";
400
- var log4 = logger("hydration-fix");
393
+ var log3 = logger("hydration-fix");
401
394
  function autoFixAdjacentText(source, filePath) {
402
395
  const parseResult = parseSync(filePath, source);
403
396
  if (parseResult.errors.length > 0) {
404
- log4.debug("OXC parse errors for %s, skipping hydration fix", filePath);
397
+ log3.debug("OXC parse errors for %s, skipping hydration fix", filePath);
405
398
  return { result: source, fixCount: 0 };
406
399
  }
407
400
  const replacements = [];
@@ -423,7 +416,7 @@ function autoFixAdjacentText(source, filePath) {
423
416
  for (const { start, end, replacement } of replacements) {
424
417
  fixed = fixed.slice(0, start) + replacement + fixed.slice(end);
425
418
  }
426
- log4.warn(
419
+ log3.warn(
427
420
  `auto-fixed ${replacements.length} adjacent text+expression issue(s) in ${filePath}`
428
421
  );
429
422
  return { result: fixed, fixCount: replacements.length };
@@ -478,14 +471,14 @@ function needsFix(content) {
478
471
  }
479
472
 
480
473
  // src/ssg/bundler.ts
481
- var log5 = logger("ssg:bundler");
474
+ var log4 = logger("ssg:bundler");
482
475
  async function bundleEntry(entry, projectRoot, sourceDir) {
483
476
  const projectRequire = createRequire(path6.join(projectRoot, "package.json"));
484
477
  let esbuild;
485
478
  try {
486
479
  esbuild = projectRequire("esbuild");
487
480
  } catch {
488
- log5.warn("esbuild not found, skipping SSR for %s", entry.kebabName);
481
+ log4.warn("esbuild not found, skipping SSR for %s", entry.kebabName);
489
482
  return null;
490
483
  }
491
484
  const sourceCode = fs2.readFileSync(entry.filePath, "utf-8");
@@ -495,7 +488,7 @@ async function bundleEntry(entry, projectRoot, sourceDir) {
495
488
  const tmpDir = path6.join(sourceDir, ".ssg-tmp");
496
489
  fs2.mkdirSync(tmpDir, { recursive: true });
497
490
  const tmpFile = path6.join(tmpDir, `.ssg-entry-${ts}.mjs`);
498
- log5.debug("bundling %s via esbuild", entry.kebabName);
491
+ log4.debug("bundling %s via esbuild", entry.kebabName);
499
492
  const startBundled = Date.now();
500
493
  await esbuild.build({
501
494
  stdin: {
@@ -519,6 +512,7 @@ async function bundleEntry(entry, projectRoot, sourceDir) {
519
512
  allowOverwrite: true,
520
513
  plugins: [
521
514
  {
515
+ // Re-apply hydration fix to all TSX/JSX files loaded during bundle
522
516
  name: "ssg-hydration-fix",
523
517
  setup(build) {
524
518
  build.onLoad({ filter: /\.(tsx|jsx)$/ }, (args) => {
@@ -529,13 +523,14 @@ async function bundleEntry(entry, projectRoot, sourceDir) {
529
523
  return { contents: result, loader: args.path.endsWith(".tsx") ? "tsx" : "jsx" };
530
524
  }
531
525
  } catch (e) {
532
- log5.debug("SSG hydration-fix failed for %s: %s", args.path, e);
526
+ log4.debug("SSG hydration-fix failed for %s: %s", args.path, e);
533
527
  }
534
528
  return void 0;
535
529
  });
536
530
  }
537
531
  },
538
532
  {
533
+ // Strip CSS imports — not needed for SSR, replaced by Liquid stylesheet
539
534
  name: "ssg-strip-css",
540
535
  setup(build) {
541
536
  build.onResolve({ filter: /\.module\.css$/ }, (args) => ({
@@ -558,7 +553,7 @@ async function bundleEntry(entry, projectRoot, sourceDir) {
558
553
  }
559
554
  ]
560
555
  });
561
- log5.debug("esbuild bundle took %dms", Date.now() - startBundled);
556
+ log4.debug("esbuild bundle took %dms", Date.now() - startBundled);
562
557
  return { tmpFile };
563
558
  }
564
559
 
@@ -589,7 +584,7 @@ function pathToFileURL(filePath) {
589
584
  }
590
585
  return "file://" + absPath;
591
586
  }
592
- var log6 = logger("ssg:renderer");
587
+ var log5 = logger("ssg:renderer");
593
588
  var DEFAULT_LIQUID_FILTERS = {
594
589
  textarea: " | newline_to_br",
595
590
  image_picker: " | img_url: 'master'"
@@ -610,7 +605,7 @@ function renderEntry(tmpFile, entry, projectRoot) {
610
605
  const Component = mod.default;
611
606
  const shopifyMeta = mod.shopifyMeta;
612
607
  if (!Component) {
613
- log6.warn("No default export found in %s, skipping", entry.filePath);
608
+ log5.warn("No default export found in %s, skipping", entry.filePath);
614
609
  return null;
615
610
  }
616
611
  if (shopifyMeta) {
@@ -623,7 +618,7 @@ function renderEntry(tmpFile, entry, projectRoot) {
623
618
  createElement = projectRequire("react").createElement;
624
619
  renderToStaticMarkup = projectRequire("react-dom/server").renderToStaticMarkup;
625
620
  } catch {
626
- log6.warn("react/react-dom not found, skipping SSR for %s", entry.kebabName);
621
+ log5.warn("react/react-dom not found, skipping SSR for %s", entry.kebabName);
627
622
  return null;
628
623
  }
629
624
  globalThis.__shopify_ssg_target = entry.targetType;
@@ -655,71 +650,76 @@ function resolveScriptAsset(kebabName, manifest) {
655
650
  }
656
651
 
657
652
  // src/ssg/schema.ts
658
- function serializeSetting(setting) {
659
- const s = { type: setting.type };
660
- if ("id" in setting) s.id = setting.id;
661
- if ("label" in setting) s.label = setting.label;
662
- if ("default" in setting && setting.default !== void 0) {
663
- s.default = setting.default;
664
- }
665
- if ("info" in setting && setting.info) s.info = setting.info;
666
- if ("placeholder" in setting && setting.placeholder) {
667
- s.placeholder = setting.placeholder;
668
- }
669
- if ("options" in setting && setting.options) s.options = setting.options;
670
- if ("min" in setting && setting.min !== void 0) s.min = setting.min;
671
- if ("max" in setting && setting.max !== void 0) s.max = setting.max;
672
- if ("step" in setting && setting.step !== void 0) s.step = setting.step;
673
- if ("unit" in setting && setting.unit) s.unit = setting.unit;
674
- if ("accept" in setting && setting.accept) s.accept = setting.accept;
675
- if ("metaobject_type" in setting && setting.metaobject_type) {
676
- s.metaobject_type = setting.metaobject_type;
677
- }
678
- if ("limit" in setting && setting.limit !== void 0) s.limit = setting.limit;
679
- if ("content" in setting && setting.content) s.content = setting.content;
680
- if ("definition" in setting && setting.definition) {
681
- s.definition = setting.definition.map(serializeSetting);
682
- }
683
- if ("role" in setting && setting.role) s.role = setting.role;
684
- return s;
685
- }
686
653
  function generateSchema(meta) {
687
- const schema = {
688
- name: meta.name
689
- };
690
- if (meta.tag) schema.tag = meta.tag;
691
- if (meta.class) schema.class = meta.class;
692
- if (meta.limit !== void 0) schema.limit = meta.limit;
693
- if (meta.max_blocks !== void 0) schema.max_blocks = meta.max_blocks;
694
- if (meta.settings && meta.settings.length > 0) {
695
- schema.settings = meta.settings.map(serializeSetting);
696
- }
697
- if (meta.blocks && meta.blocks.length > 0) {
698
- schema.blocks = meta.blocks.map((block) => {
699
- const b = { type: block.type };
700
- if (block.name) b.name = block.name;
701
- if (block.settings) b.settings = block.settings;
702
- return b;
703
- });
704
- }
705
- if (meta.presets && meta.presets.length > 0) {
706
- schema.presets = meta.presets.map((preset) => {
707
- const p = { name: preset.name };
708
- if (preset.category) p.category = preset.category;
709
- if (preset.settings) p.settings = preset.settings;
710
- if (preset.blocks) p.blocks = preset.blocks;
711
- return p;
712
- });
713
- }
714
- if (meta.enabled_on) schema.enabled_on = meta.enabled_on;
715
- if (meta.disabled_on) schema.disabled_on = meta.disabled_on;
716
- if (meta.templates) schema.templates = meta.templates;
654
+ const schema = buildSchema(meta);
717
655
  const json = JSON.stringify(schema, null, 2);
718
656
  return `
719
657
  {% schema %}
720
658
  ${json}
721
- {% endschema %}
722
- `;
659
+ {% endschema %}`;
660
+ }
661
+ function buildSchema(meta) {
662
+ return {
663
+ name: meta.name ?? "",
664
+ ...meta.tag !== void 0 ? { tag: meta.tag } : {},
665
+ class: meta.class ?? "",
666
+ limit: meta.limit,
667
+ ...meta.max_blocks != null ? { max_blocks: meta.max_blocks } : {},
668
+ settings: meta.settings || [],
669
+ blocks: meta.blocks ? meta.blocks.map((b) => serializeBlockDefinition(b)) : void 0,
670
+ presets: meta.presets ? meta.presets.map((p) => serializePreset(p)) : void 0,
671
+ ...meta.default ? { default: serializePreset(meta.default) } : {},
672
+ ...meta.locales ? { locales: meta.locales } : {},
673
+ ...meta.enabled_on ? { enabled_on: meta.enabled_on } : {},
674
+ ...meta.disabled_on ? { disabled_on: meta.disabled_on } : {}
675
+ };
676
+ }
677
+ function defaultBlockName(type) {
678
+ const name = type.replace(/^@/, "").split(/[-_\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
679
+ return name.length > MAX_NAME_LENGTH ? name.slice(0, MAX_NAME_LENGTH) : name;
680
+ }
681
+ function serializeBlockDefinition(block) {
682
+ const { type, name, limit, settings } = block;
683
+ return {
684
+ type,
685
+ name: name ?? defaultBlockName(type),
686
+ ...limit != null ? { limit } : {},
687
+ ...settings ? { settings } : {}
688
+ };
689
+ }
690
+ function serializeInputSettings(settings) {
691
+ if (!settings) return void 0;
692
+ const out = {};
693
+ for (const [key, value] of Object.entries(settings)) {
694
+ out[key] = value === "" ? void 0 : value;
695
+ }
696
+ return out;
697
+ }
698
+ function serializePresetBlock(block) {
699
+ const p = { type: block.type };
700
+ if (block.name) p.name = block.name;
701
+ if (block.static) {
702
+ p.static = true;
703
+ if (block.id) p.id = block.id;
704
+ }
705
+ const settings = serializeInputSettings(
706
+ "settings" in block ? block.settings : void 0
707
+ );
708
+ if (settings) p.settings = settings;
709
+ if (!block.static && block.blocks?.length) {
710
+ p.blocks = block.blocks.map(serializePresetBlock);
711
+ }
712
+ return p;
713
+ }
714
+ function serializePreset(preset) {
715
+ const obj = { name: preset.name };
716
+ if (preset.category) obj.category = preset.category;
717
+ const settings = serializeInputSettings(preset.settings);
718
+ if (settings) obj.settings = settings;
719
+ if (preset.blocks?.length) {
720
+ obj.blocks = preset.blocks.map(serializePresetBlock);
721
+ }
722
+ return obj;
723
723
  }
724
724
 
725
725
  // src/ssg/liquid-paths.ts
@@ -878,24 +878,8 @@ function buildSnippet(html, entry, trackedExpressions, liquidPrepend = "") {
878
878
  return lines;
879
879
  }
880
880
 
881
- // src/validate/rules.ts
882
- var MAX_NAME_LENGTH = 25;
883
- function checkNameLength(meta, kebabName) {
884
- if (meta.name.length > MAX_NAME_LENGTH) {
885
- return `[${kebabName}] shopifyMeta.name "${meta.name}" is ${meta.name.length} chars (Shopify limit: ${MAX_NAME_LENGTH})`;
886
- }
887
- return null;
888
- }
889
- function checkEmptyStringDefault(setting) {
890
- if (setting.default === "") {
891
- const label = "id" in setting && setting.id ? setting.id : "(no id)";
892
- return `Setting "${label}" (type: ${setting.type}) has empty string default`;
893
- }
894
- return null;
895
- }
896
-
897
881
  // src/validate/index.ts
898
- var log7 = logger("validate");
882
+ var log6 = logger("validate");
899
883
  function validateShopifyMeta(meta, context) {
900
884
  const warnings = [];
901
885
  const nameWarning = checkNameLength(meta, context.kebabName);
@@ -910,17 +894,17 @@ function validateShopifyMeta(meta, context) {
910
894
  }
911
895
  }
912
896
  for (const w of warnings) {
913
- log7.warn(w);
897
+ log6.warn(w);
914
898
  }
915
899
  return warnings;
916
900
  }
917
901
 
918
902
  // src/ssg/compiler.ts
919
- var log8 = logger("ssg:compiler");
903
+ var log7 = logger("ssg:compiler");
920
904
  async function compileAllEntries(options, manifest) {
921
905
  const entries = scanEntries(options);
922
906
  if (entries.length === 0) return;
923
- log8.debug("found %d entries to compile", entries.length);
907
+ log7.debug("found %d entries to compile", entries.length);
924
908
  const projectRoot = path9.resolve(options.themeRoot);
925
909
  const sourceDir = path9.resolve(options.themeRoot, options.sourceCodeDir);
926
910
  const { entryCssFiles, cssRefCount } = analyzeCssDistribution(entries, manifest);
@@ -929,10 +913,10 @@ async function compileAllEntries(options, manifest) {
929
913
  try {
930
914
  await compileEntry(entry, options, manifest, projectRoot, sourceDir, entryCssFiles, cssSnippetMap);
931
915
  } catch (err) {
932
- log8.error("Failed to compile %s:", entry.filePath, err);
916
+ log7.error("Failed to compile %s:", entry.filePath, err);
933
917
  }
934
918
  }
935
- log8.info("Compiled %d entries", entries.length);
919
+ log7.info("Compiled %d entries", entries.length);
936
920
  const tmpDir = path9.join(sourceDir, ".ssg-tmp");
937
921
  try {
938
922
  fs3.rmSync(tmpDir, { recursive: true, force: true });
@@ -950,7 +934,7 @@ async function compileEntry(entry, options, manifest, projectRoot, sourceDir, en
950
934
  const cssFiles = entryCssFiles.get(entry.kebabName) || [];
951
935
  const { inline: cssInlineFiles, snippets: cssSnippets } = categorizeCss(cssFiles, cssSnippetMap);
952
936
  const cssInline = readCssFileContents(cssInlineFiles, options.buildDir, options.themeRoot);
953
- log8.debug(
937
+ log7.debug(
954
938
  "compiling %s (type=%s, css inline=%d, css snippets=%d)",
955
939
  entry.kebabName,
956
940
  entry.targetType,
@@ -985,7 +969,7 @@ async function compileEntry(entry, options, manifest, projectRoot, sourceDir, en
985
969
  }
986
970
 
987
971
  // src/ssg/index.ts
988
- var log9 = logger("ssg");
972
+ var log8 = logger("ssg");
989
973
  function shopifySSG(options) {
990
974
  return {
991
975
  name: "vite-plugin-shopify:ssg",
@@ -1001,16 +985,16 @@ function shopifySSG(options) {
1001
985
  "manifest.json"
1002
986
  );
1003
987
  if (!fs4.existsSync(manifestPath)) {
1004
- log9.warn("No manifest.json found, skipping SSG");
988
+ log8.warn("No manifest.json found, skipping SSG");
1005
989
  return;
1006
990
  }
1007
- log9.debug("reading manifest from %s", manifestPath);
991
+ log8.debug("reading manifest from %s", manifestPath);
1008
992
  const manifest = JSON.parse(fs4.readFileSync(manifestPath, "utf-8"));
1009
- log9.info("Starting SSG compilation...");
993
+ log8.info("Starting SSG compilation...");
1010
994
  await compileAllEntries(options, manifest);
1011
- log9.info("SSG compilation complete");
995
+ log8.info("SSG compilation complete");
1012
996
  writeImportMapSnippet(options);
1013
- log9.debug("wrote import map snippet");
997
+ log8.debug("wrote import map snippet");
1014
998
  },
1015
999
  resolveId(id) {
1016
1000
  if (id === "vite-plugin-shopify/runtime") {
@@ -1,35 +1,104 @@
1
1
  import * as react from 'react';
2
2
 
3
+ /**
4
+ * Parses a Liquid value into a boolean following Shopify truthiness rules:
5
+ * `false`, `""`, `"0"`, `undefined`, and `null` are falsy; everything else is
6
+ * truthy.
7
+ */
3
8
  declare function parseLiquidBoolean(value: string | boolean | undefined | null): boolean;
9
+ /**
10
+ * Parses a Liquid value into a number, falling back to `defaultVal` (default 0)
11
+ * when the value is `undefined`, `null`, or unparseable.
12
+ */
4
13
  declare function parseLiquidNumber(value: string | number | undefined | null, defaultVal?: number): number;
14
+ /** Type mode hint for `useLiquidValue` — controls how the raw string is coerced. */
5
15
  type LiquidTypeMode = "string" | "number" | "boolean";
6
16
  type Setter<T> = (val: T | ((prev: T) => T)) => void;
7
17
  type ValueForMode<M extends LiquidTypeMode | undefined> = M extends "number" ? number : M extends "boolean" ? boolean : string | undefined;
18
+ /**
19
+ * Reads a single Liquid setting and returns a React state tuple.
20
+ *
21
+ * Overloads allow compile-time type narrowing based on the `type` argument:
22
+ * - No `type` or `"string"` → `[string | undefined, Setter]`
23
+ * - `"number"` → `[number, Setter]`
24
+ * - `"boolean"` → `[boolean, Setter]`
25
+ *
26
+ * @param expr - Liquid expression to read (e.g. `"section.settings.title"`).
27
+ * @param type - Type coercion mode: `"string"` (default), `"number"`, or `"boolean"`.
28
+ */
8
29
  declare function useLiquidValue(expr: string): [string | undefined, Setter<string | undefined>];
9
30
  declare function useLiquidValue(expr: string, type: "string"): [string | undefined, Setter<string | undefined>];
10
31
  declare function useLiquidValue(expr: string, type: "number"): [number, Setter<number>];
11
32
  declare function useLiquidValue(expr: string, type: "boolean"): [boolean, Setter<boolean>];
33
+ /** Maps individual keys in a Liquid expression map to a type mode (string/number/boolean). */
12
34
  type TypeModes<T extends Record<string, string>> = Partial<{
13
35
  [K in keyof T & string]: LiquidTypeMode;
14
36
  }>;
37
+ /** Infers the resolved value types for each key in a Liquid expression map. */
15
38
  type InferValues<T extends Record<string, string>, Types extends TypeModes<T>> = {
16
39
  [K in keyof T & string]: ValueForMode<Types[K]>;
17
40
  };
41
+ /**
42
+ * Reads multiple Liquid settings simultaneously and returns a single state object.
43
+ *
44
+ * Like `useLiquidValue` but batched — takes a name-to-expression mapping and an
45
+ * optional name-to-type-mode mapping. Returns a plain object with the resolved
46
+ * (and type-coerced) values.
47
+ *
48
+ * @param map - Object mapping keys to Liquid expression strings.
49
+ * @param types - Optional per-key type mode overrides (default: `"string"`).
50
+ */
18
51
  declare function useLiquidValues<T extends Record<string, string>, const Types extends TypeModes<T> = {}>(map: T, types?: Types): InferValues<T, Types>;
52
+ /**
53
+ * Reads a section-level setting value.
54
+ *
55
+ * Equivalent to `useLiquidRaw(\`section.settings.${key}\`)`.
56
+ */
19
57
  declare function useSectionSettings(key: string): {
20
58
  value: string | undefined;
21
59
  };
60
+ /**
61
+ * Reads a block-level setting value.
62
+ *
63
+ * Equivalent to `useLiquidRaw(\`block.settings.${key}\`)`.
64
+ */
22
65
  declare function useBlockSettings(key: string): {
23
66
  value: string | undefined;
24
67
  };
68
+ /**
69
+ * Reads a snippet parameter by name.
70
+ */
25
71
  declare function useSnippetParams(key: string): {
26
72
  value: string | undefined;
27
73
  };
74
+ /**
75
+ * Reads a block parameter by name.
76
+ */
28
77
  declare function useBlockParams(key: string): {
29
78
  value: string | undefined;
30
79
  };
80
+ /**
81
+ * Registers a block of raw Liquid code for injection into the generated
82
+ * `.liquid` file. During SSR the code (and any `{{ expr }}` references it
83
+ * contains) is accumulated in global registries; on the client it is a no-op.
84
+ *
85
+ * The registered Liquid code is injected **before** the JSON bridge in the
86
+ * assembled output so that any variables it defines are available for the
87
+ * bridge's `json` filter.
88
+ *
89
+ * @param code - Raw Liquid template code (may include `{{ }}` expressions).
90
+ * @returns Always returns an empty string — the Liquid code is rendered
91
+ * server-side only.
92
+ */
31
93
  declare function useLiquidBlock(code: string): string;
32
94
 
95
+ /**
96
+ * @file React context provider for Liquid data consumed by runtime hooks.
97
+ *
98
+ * The `LiquidDataContext` carries server-rendered Liquid values (from the JSON
99
+ * bridge) down to {@link useLiquidValue} and related hooks during client-side
100
+ * hydration. Without this provider, hooks return `undefined` on the client.
101
+ */
33
102
  declare const LiquidDataContext: react.Context<Record<string, any>>;
34
103
  declare const LiquidDataProvider: react.Provider<Record<string, any>>;
35
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-react-shopify",
3
- "version": "2.2.3",
3
+ "version": "2.2.6",
4
4
  "description": "Vite plugin for React Shopify themes",
5
5
  "files": [
6
6
  "dist"
@@ -17,6 +17,16 @@
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
+ },
20
30
  "dependencies": {
21
31
  "debug": "^4.4.0",
22
32
  "fast-glob": "^3.3.0",
@@ -34,15 +44,5 @@
34
44
  },
35
45
  "peerDependencies": {
36
46
  "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"
47
47
  }
48
- }
48
+ }