@semi-solid/compiler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,645 @@
1
+ import { Plugin } from 'vite';
2
+ export { BrandConfig, SemiSolidConfig } from './cli/config.js';
3
+
4
+ /**
5
+ * plugin.ts
6
+ *
7
+ * Main Vite plugin entry point for Semi-Solid.
8
+ *
9
+ * During the build, this plugin intercepts .tsx/.jsx files and:
10
+ * 1. Extracts tap() mappings
11
+ * 2. Generates a .liquid file and writes it to the output directory
12
+ * 3. Returns the cleaned source code (tap() replaced by fallbacks)
13
+ * for Vite/SolidJS to compile into the JS bundle
14
+ *
15
+ * Phase 1 handles: components in src/components/ → snippets/*.liquid
16
+ * Phase 3 will add: routes in src/routes/ → templates/*.liquid
17
+ */
18
+
19
+ interface SemiSolidOptions {
20
+ /** Brand identifier, e.g. 'brand-a' */
21
+ brand: string;
22
+ /** Locale identifier, e.g. 'en' */
23
+ locale: string;
24
+ /**
25
+ * Output directory for the Shopify theme.
26
+ * Defaults to `dist/${brand}/${locale}`.
27
+ */
28
+ outDir?: string;
29
+ /**
30
+ * Phase 12: external server personalization.
31
+ * When configured, tapPersonalized() calls generate preconnect and prefetch
32
+ * tags so the fetch starts as soon as the HTML parser hits <head>.
33
+ */
34
+ personalization?: {
35
+ baseUrl: string;
36
+ preconnect?: boolean;
37
+ prefetch?: boolean;
38
+ };
39
+ }
40
+ declare function semiSolidPlugin(options: SemiSolidOptions): Plugin;
41
+
42
+ /**
43
+ * tap-extract.ts
44
+ *
45
+ * Extracts tap() mappings from a component's source and produces a
46
+ * cleaned version of the source suitable for the client-side JS bundle
47
+ * (where tap() calls are replaced by their fallback values).
48
+ *
49
+ * Uses oxc-parser for fast AST parsing and magic-string for
50
+ * source-map-preserving replacements.
51
+ */
52
+ interface TapMapping {
53
+ /** Variable name (or __tap_inline_N__ for inline calls) → Liquid expression */
54
+ [variableName: string]: string;
55
+ }
56
+ interface PersonalizedCallInfo {
57
+ /** LHS variable name */
58
+ varName: string;
59
+ /** API endpoint URL (first arg) */
60
+ url: string;
61
+ /** paramKey → tap variable name used as the param value */
62
+ params: Record<string, string>;
63
+ }
64
+ interface TapExtractResult {
65
+ /** Map of variable name → liquid expression string */
66
+ mappings: TapMapping;
67
+ /** Source with tap() calls replaced by fallbacks, for the JS bundle */
68
+ cleanedSource: string;
69
+ /** Source map for the cleaned source */
70
+ sourceMap: string;
71
+ /** Any warnings generated during extraction */
72
+ warnings: string[];
73
+ /**
74
+ * Variable names bound via tapWhen() — these need a data section so the
75
+ * runtime can re-fetch their values when the dep signals change.
76
+ */
77
+ reactiveVars: Set<string>;
78
+ /**
79
+ * PascalCase component names referenced via tapRemote() — the plugin
80
+ * generates a wrapper section for each so Shopify can render the snippet.
81
+ */
82
+ remoteComponents: Set<string>;
83
+ /**
84
+ * tapPersonalized() calls found in this component — each describes an
85
+ * external API endpoint, its param-to-tap-variable mapping, and the LHS
86
+ * variable name.
87
+ */
88
+ personalizedCalls: PersonalizedCallInfo[];
89
+ }
90
+ declare function extractTapMappings(source: string, filename?: string): TapExtractResult;
91
+
92
+ /**
93
+ * css.ts
94
+ *
95
+ * Phase 7: CSS handling for Shopify theme builds.
96
+ *
97
+ * Resolves brand-specific and global CSS entry points, and generates
98
+ * the Liquid asset include tags needed in layout/theme.liquid.
99
+ *
100
+ * Shopify theme CSS lives in assets/ and is referenced via a direct <link> tag
101
+ * (not stylesheet_tag, which defers loading and causes FOUC):
102
+ * <link rel="stylesheet" href="{{ 'theme.css' | asset_url }}" media="all">
103
+ *
104
+ * JS assets use the modern module pattern (script_tag is deprecated):
105
+ * <script src="{{ 'theme.entry.js' | asset_url }}" type="module"></script>
106
+ */
107
+
108
+ interface CSSFile {
109
+ /** Absolute path to the CSS source file. */
110
+ src: string;
111
+ /** Filename the file will have inside the Shopify theme's assets/ directory. */
112
+ assetName: string;
113
+ }
114
+ /**
115
+ * Resolves CSS files for a brand build.
116
+ *
117
+ * Looks for (in precedence order):
118
+ * 1. src/brands/{brand}/theme.css → assets/theme.css (brand-specific styles)
119
+ * 2. src/styles/global.css → assets/global.css (shared global CSS)
120
+ * 3. src/index.css → assets/index.css (Tailwind entry, if present)
121
+ *
122
+ * All files that exist are returned — multiple CSS files are allowed.
123
+ */
124
+ declare function resolveCSSFiles(brand: string, projectRoot: string, existsSync?: (p: string) => boolean): CSSFile[];
125
+ /**
126
+ * Generates a render-blocking CSS include for a Shopify theme asset.
127
+ *
128
+ * Uses a direct <link> tag instead of the `| stylesheet_tag` Liquid filter.
129
+ * Shopify's stylesheet_tag defers loading with media="print" + onload swap,
130
+ * which causes a flash of unstyled content (FOUC) and layout shift.
131
+ * A plain <link rel="stylesheet"> is render-blocking by default, ensuring
132
+ * styles are applied before the first paint.
133
+ *
134
+ * Output: <link rel="stylesheet" href="{{ 'theme.css' | asset_url }}" media="all">
135
+ */
136
+ declare function generateStylesheetTag(assetName: string): string;
137
+ /**
138
+ * Generates a modern Shopify Liquid script include for a JS asset.
139
+ * Uses type="module" for ES modules. The deprecated `script_tag` filter
140
+ * is avoided — Shopify's own guidance recommends explicit <script> tags.
141
+ *
142
+ * Output: <script src="{{ 'theme.entry.js' | asset_url }}" type="module"></script>
143
+ */
144
+ declare function generateScriptTag(assetName: string): string;
145
+ interface PersonalizationAssetOptions {
146
+ baseUrl: string;
147
+ preconnect: boolean;
148
+ prefetch: boolean;
149
+ calls: Array<{
150
+ url: string;
151
+ params: Record<string, string>;
152
+ componentMappings: TapMapping;
153
+ }>;
154
+ }
155
+ /**
156
+ * Generates a `<link rel="preconnect">` tag for an external API origin.
157
+ * Extracts the origin (scheme + host) from the full URL.
158
+ */
159
+ declare function generatePreconnectTag(baseUrl: string): string;
160
+ /**
161
+ * Generates an inline `<script>` that prefetches personalized data.
162
+ *
163
+ * For each call, builds a Liquid-powered URL with `| url_encode` on each
164
+ * param value, wraps in an IIFE that stores the fetch promise on
165
+ * `window.__p[url]`.
166
+ *
167
+ * Param keys are sorted alphabetically to match the runtime's `buildUrl()`.
168
+ */
169
+ declare function generatePrefetchScript(calls: PersonalizationAssetOptions['calls'], baseUrl: string): string;
170
+ /**
171
+ * Generates a block of Liquid asset includes (CSS stylesheets + JS modules)
172
+ * suitable for insertion in the <head> of layout/theme.liquid.
173
+ *
174
+ * Ordering: preconnect → CSS → JS → prefetch script.
175
+ * Backward compatible — omitting personalization produces the same output.
176
+ */
177
+ declare function generateAssetIncludes(cssAssets: string[], jsAssets: string[], personalization?: PersonalizationAssetOptions): string;
178
+
179
+ /**
180
+ * hash.ts
181
+ *
182
+ * Phase 7: Content hashing for asset cache busting.
183
+ *
184
+ * Shopify themes are served from a CDN with long-lived cache headers.
185
+ * Including a content hash in asset filenames ensures browsers and CDNs
186
+ * pick up changes immediately after a new theme is published.
187
+ *
188
+ * The 8-character hex hash format matches Vite's default for JS chunks,
189
+ * keeping all assets consistent.
190
+ */
191
+ /**
192
+ * Computes a short content hash for cache busting.
193
+ *
194
+ * Returns the first 8 hex characters of the SHA-256 digest of the input.
195
+ * The same input always produces the same output (deterministic).
196
+ *
197
+ * hashContent('hello') → 'aaf4c61d' (example)
198
+ */
199
+ declare function hashContent(content: string): string;
200
+ /**
201
+ * Produces a versioned asset filename by inserting the hash before the
202
+ * file extension — consistent with Vite's chunk naming convention.
203
+ *
204
+ * versionedName('theme.css', 'abc12345') → 'theme-abc12345.css'
205
+ * versionedName('theme.entry.js', 'abc12345') → 'theme.entry-abc12345.js'
206
+ * versionedName('logo', 'abc12345') → 'logo-abc12345'
207
+ */
208
+ declare function versionedName(basename: string, hash: string): string;
209
+ /**
210
+ * Parses a versioned filename produced by `versionedName()` back into
211
+ * its original base name and hash.
212
+ *
213
+ * Returns null if the filename does not match the versioned format.
214
+ *
215
+ * parseVersionedName('theme-abc12345.css') → { name: 'theme.css', hash: 'abc12345' }
216
+ * parseVersionedName('theme.entry-abc12345.js') → { name: 'theme.entry.js', hash: 'abc12345' }
217
+ * parseVersionedName('theme.css') → null
218
+ */
219
+ declare function parseVersionedName(filename: string): {
220
+ name: string;
221
+ hash: string;
222
+ } | null;
223
+
224
+ /**
225
+ * validation.ts
226
+ *
227
+ * Phase 7: Build validation and manifest generation.
228
+ *
229
+ * Provides utilities to catch common mistakes at build time:
230
+ * - Using Liquid objects not available in the current route's context
231
+ * - tap() variables extracted but never rendered into the Liquid output
232
+ *
233
+ * Also generates a build manifest (manifest.json) summarising all emitted files.
234
+ */
235
+
236
+ /**
237
+ * Shopify Liquid objects that are available in every template context.
238
+ * These do not need to appear in each route's `context` array to be valid.
239
+ * See: https://shopify.dev/docs/api/liquid/objects
240
+ */
241
+ declare const GLOBAL_LIQUID_OBJECTS: ReadonlySet<string>;
242
+ type WarningType = 'context_mismatch' | 'unused_mapping';
243
+ interface ValidationWarning {
244
+ type: WarningType;
245
+ /** Human-readable description of the issue. */
246
+ message: string;
247
+ /** The tap()-mapped variable name (component source identifier). */
248
+ variable: string;
249
+ /** The full Liquid expression string from the tap() call. */
250
+ liquidExpr: string;
251
+ }
252
+ interface BuildManifest {
253
+ brand: string;
254
+ locale: string;
255
+ /** Relative paths of Shopify template files written. */
256
+ templates: string[];
257
+ /** Relative paths of Shopify snippet files written. */
258
+ snippets: string[];
259
+ /** Relative paths of Shopify section files written. */
260
+ sections: string[];
261
+ /** Relative paths of JS/CSS asset files written. */
262
+ assets: string[];
263
+ /** Relative paths of locale JSON files copied. */
264
+ locales: string[];
265
+ }
266
+ /**
267
+ * Extracts the top-level Liquid object names referenced in a tap() mapping value.
268
+ *
269
+ * Examples:
270
+ * '{{ product.title }}' → ['product']
271
+ * '{{ product.price | money }}' → ['product']
272
+ * '{{ shop.name }}' → ['shop']
273
+ * "{{ 'product.add_to_cart' | t }}" → [] (string literal, no object)
274
+ * 'product' → ['product'] (bare object ref)
275
+ * '{{ cart.item_count }}' → ['cart']
276
+ */
277
+ declare function extractLiquidObjects(liquidExpr: string): string[];
278
+ /**
279
+ * Returns a warning for each tap() mapping that references a Liquid object
280
+ * not available in the given route context (and not in GLOBAL_LIQUID_OBJECTS).
281
+ *
282
+ * Example: using `{{ product.title }}` in an index route (which only has
283
+ * `shop` in its context) would produce a context_mismatch warning.
284
+ */
285
+ declare function validateTapMappings(mappings: TapMapping, routeContext: string[]): ValidationWarning[];
286
+ /**
287
+ * Returns a warning for each tap() mapping whose Liquid path does not appear
288
+ * anywhere in the generated Liquid output.
289
+ *
290
+ * A mapping is "unused in liquid" when the variable was extracted from tap()
291
+ * but its JSX usage was never compiled to a Liquid expression — for example,
292
+ * because it is used only inside a client-side event handler.
293
+ *
294
+ * Note: variables that appear in data-props (e.g. for hydration) ARE counted
295
+ * as "used" because their Liquid path appears in the data-props attribute.
296
+ *
297
+ * Skips synthetic inline tap() variable names (prefixed with __tap_inline_).
298
+ */
299
+ declare function validateUnusedMappings(mappings: TapMapping, liquidOutput: string): ValidationWarning[];
300
+ /**
301
+ * Generates a build manifest object summarising all files emitted during
302
+ * this build. Written to `manifest.json` in the theme output directory.
303
+ */
304
+ declare function generateManifest(brand: string, locale: string, files: {
305
+ templates: string[];
306
+ snippets: string[];
307
+ sections?: string[];
308
+ assets: string[];
309
+ locales: string[];
310
+ }): BuildManifest;
311
+
312
+ /**
313
+ * hydration.ts
314
+ *
315
+ * Phase 6: Hydration Loader
316
+ *
317
+ * Provides utilities for detecting interactive SolidJS components and
318
+ * generating the hydration entry point for client-side island hydration.
319
+ */
320
+
321
+ /**
322
+ * Returns true if the component source is interactive:
323
+ * - Uses createSignal or createEffect (SolidJS reactive primitives)
324
+ * - Has any on* JSX event handler attributes (onClick, onChange, etc.)
325
+ * - Uses tapWhen() (reactive tap — implies createTapSignal at runtime)
326
+ */
327
+ declare function isInteractiveComponent(source: string): boolean;
328
+ /**
329
+ * Finds tap-mapped variable names that are used inside event handler
330
+ * function bodies. These are the variables the JS bundle needs at
331
+ * runtime (e.g. the product handle to call Shopify's cart API).
332
+ *
333
+ * Walks on* JSX attribute values and the bodies of any named functions
334
+ * they reference, collecting identifier names that are keys in `mappings`.
335
+ */
336
+ declare function detectPropVars(source: string, mappings: TapMapping): string[];
337
+ /**
338
+ * Builds the data-props attribute value for a hydrated component.
339
+ *
340
+ * Each tap-mapped prop is serialized with the | json Liquid filter so
341
+ * the server-rendered value is available to the client JS bundle.
342
+ *
343
+ * Some Shopify objects (e.g. linklist links) don't support | json, so
344
+ * we detect those patterns and generate manual Liquid serialization.
345
+ *
346
+ * Example output for propVars = ['handle']:
347
+ * { "handle": {{ product.handle | json }} }
348
+ */
349
+ declare function generateDataProps(propVars: string[], mappings: TapMapping): string;
350
+ /**
351
+ * Generates the content of `assets/theme.entry.js` — the Shopify theme
352
+ * entry script that mounts interactive components on the client.
353
+ *
354
+ * Uses SolidJS's render() (not hydrate()) because the DOM was produced by
355
+ * Shopify Liquid, not SolidJS's renderToString(). hydrate() requires
356
+ * SolidJS hydration markers that Liquid never emits; render() mounts fresh.
357
+ */
358
+ declare function generateHydrationEntry(components: Array<{
359
+ name: string;
360
+ importPath: string;
361
+ }>): string;
362
+
363
+ /**
364
+ * liquid-gen.ts
365
+ *
366
+ * Generates a .liquid snippet from a SolidJS component's source code
367
+ * by walking the JSX AST and emitting Liquid equivalents.
368
+ *
369
+ * Phase 1: plain HTML, tap()-mapped variables in text/attributes, t() calls.
370
+ * Phase 2: <Show> → {% if %} / {% unless %}, <For> → {% for %}.
371
+ * Phase 3 (TODO): component imports → {% render 'snippet' %}.
372
+ */
373
+
374
+ interface LiquidGenOptions {
375
+ /** Component name, e.g. 'ProductCard'. Used in error messages. */
376
+ componentName: string;
377
+ /** Indent string for pretty-printing. Default: two spaces. */
378
+ indent?: string;
379
+ /**
380
+ * Pre-computed data-props attribute value for client-side hydration.
381
+ * When set, the root HTML element receives data-component and data-props
382
+ * attributes so SolidJS's render() can target the SSR-rendered element.
383
+ * Generate this value with generateDataProps() from hydration.ts.
384
+ */
385
+ dataProps?: string;
386
+ /**
387
+ * Section ID for the companion JSON data section, e.g. 'product-card-data'.
388
+ * When set, the root HTML element also receives a data-section-id attribute
389
+ * so the hydration entry can pass it to createTapSignal() for tapWhen() calls.
390
+ */
391
+ dataSectionId?: string;
392
+ /**
393
+ * PascalCase names of components that are Shopify sections.
394
+ * When set, <ComponentName /> emits {% section 'component-name' %} instead
395
+ * of {% render 'component-name' %}.
396
+ */
397
+ sectionComponents?: Set<string>;
398
+ /**
399
+ * Mutable array that accumulates build-time warnings during Liquid generation.
400
+ * Callers can inspect this after generateLiquid() returns to surface warnings
401
+ * via the build tool (e.g. Vite/Rollup this.warn()).
402
+ */
403
+ warnings?: string[];
404
+ }
405
+ declare function generateLiquid(source: string, mappings: TapMapping, options: LiquidGenOptions): string;
406
+
407
+ /**
408
+ * Lightweight helpers for navigating the oxc-parser AST.
409
+ *
410
+ * oxc-parser returns an ESTree-compatible AST but with some differences
411
+ * from Babel. The helpers here are intentionally defensive so they work
412
+ * across minor oxc version variations.
413
+ */
414
+ interface AstNode {
415
+ type: string;
416
+ /** Byte offset of the node start in the source string (oxc-parser native) */
417
+ start?: number;
418
+ /** Byte offset of the node end in the source string (oxc-parser native) */
419
+ end?: number;
420
+ [key: string]: unknown;
421
+ }
422
+ /**
423
+ * Converts PascalCase or camelCase to kebab-case.
424
+ * e.g. ProductCard → product-card, CartDrawer → cart-drawer
425
+ */
426
+ declare function toKebabCase(name: string): string;
427
+
428
+ /**
429
+ * control-flow.ts
430
+ *
431
+ * Utilities for resolving SolidJS control-flow components (<Show>, <For>)
432
+ * to their Liquid equivalents.
433
+ *
434
+ * These functions operate on oxc-parser AST nodes and are consumed by
435
+ * liquid-gen.ts during JSX → Liquid compilation.
436
+ */
437
+
438
+ interface ShowCondition {
439
+ /** The bare Liquid expression, e.g. 'product.available' or 'item.in_stock' */
440
+ liquidExpr: string;
441
+ /**
442
+ * When true, use {% unless %} / {% endunless %}.
443
+ * Produced by <Show when={!condition}>.
444
+ */
445
+ negated: boolean;
446
+ }
447
+ interface ForIteration {
448
+ /** The Liquid collection expression, e.g. 'product.images' */
449
+ collection: string;
450
+ /** The loop variable name taken from the JSX render function param */
451
+ loopVar: string;
452
+ }
453
+ /**
454
+ * Resolves the `when` expression from `<Show when={expr}>` to a Liquid condition.
455
+ *
456
+ * Returns null when the condition is purely client-side (e.g. a signal call,
457
+ * a complex expression, or an identifier that is not tap()-mapped).
458
+ *
459
+ * Supported patterns:
460
+ * when={tapMapped} → { liquidExpr: 'product.available', negated: false }
461
+ * when={!tapMapped} → { liquidExpr: 'product.available', negated: true }
462
+ * when={loopVar} → { liquidExpr: 'item', negated: false }
463
+ * when={loopVar.prop} → { liquidExpr: 'item.in_stock', negated: false }
464
+ * when={!loopVar.prop} → { liquidExpr: 'item.in_stock', negated: true }
465
+ */
466
+ declare function resolveShowCondition(whenExpr: AstNode, mappings: TapMapping, loopVars: Set<string>, warnings?: string[]): ShowCondition | null;
467
+ /**
468
+ * Resolves the `each` expression from `<For each={expr}>` to a ForIteration.
469
+ *
470
+ * Returns null if the collection is not tap()-mapped (i.e. it's a purely
471
+ * client-side array).
472
+ *
473
+ * @param eachExpr The AST node for the `each` attribute value
474
+ * @param loopVarName The render-function parameter name (the loop variable)
475
+ * @param mappings tap() mappings from the component
476
+ */
477
+ declare function resolveForIteration(eachExpr: AstNode, loopVarName: string, mappings: TapMapping, loopVars?: Set<string>, warnings?: string[]): ForIteration | null;
478
+ /**
479
+ * Walks a (possibly nested) MemberExpression and builds a dot-separated
480
+ * Liquid path when the root identifier is a loop variable or tap()-mapped object.
481
+ *
482
+ * Examples:
483
+ * image (loop var) → 'image'
484
+ * image.url (loop var + prop) → 'image.url'
485
+ * image.assets.src (deeper) → 'image.assets.src'
486
+ * product (tap mapped 'product') → 'product'
487
+ *
488
+ * Returns null for:
489
+ * - Computed accesses (arr[0], obj[key])
490
+ * - Root identifiers that are neither loop vars nor tap-mapped
491
+ */
492
+ declare function resolveMemberPath(node: AstNode, mappings: TapMapping, loopVars: Set<string>): string | null;
493
+ /**
494
+ * Strips `{{ }}` braces from a Liquid expression string so it can be used
495
+ * in tag context ({% if expr %}, {% for x in expr %}).
496
+ *
497
+ * '{{ product.available }}' → 'product.available'
498
+ * 'product.images' → 'product.images' (already plain)
499
+ * '{% raw tag %}' → returned unchanged
500
+ */
501
+ declare function stripLiquidBraces(liquidStr: string): string;
502
+
503
+ /**
504
+ * route-map.ts
505
+ *
506
+ * Maps src/routes/+layout.tsx to Shopify layout/theme.liquid.
507
+ *
508
+ * Page templates are now JSON files in templates/ (copied directly to dist).
509
+ * Only the layout file needs the tap-extract → liquid-gen pipeline.
510
+ */
511
+ interface RouteInfo {
512
+ /** Shopify template identifier, e.g. '_layout' */
513
+ template: string;
514
+ /** Relative output path from the theme root, e.g. 'layout/theme.liquid' */
515
+ outputPath: string;
516
+ /** Liquid context objects available in this template */
517
+ context: string[];
518
+ /** Whether this is the layout file (+layout.tsx → layout/theme.liquid) */
519
+ isLayout: boolean;
520
+ }
521
+ /**
522
+ * Given a route file's absolute path and the routes directory, returns
523
+ * the RouteInfo describing the Shopify output for that file.
524
+ *
525
+ * Only +layout files are recognized. All other route files return null
526
+ * since page templates are now JSON files in templates/.
527
+ */
528
+ declare function resolveRoute(filePath: string, routesDir: string): RouteInfo | null;
529
+ /**
530
+ * Returns true if the file is inside src/routes/.
531
+ */
532
+ declare function isRouteFile(filePath: string, projectRoot: string): boolean;
533
+ /**
534
+ * Returns the absolute path to the routes directory for a project root.
535
+ */
536
+ declare function getRoutesDir(projectRoot: string): string;
537
+
538
+ /**
539
+ * brand-resolve.ts
540
+ *
541
+ * Vite plugin that resolves `$snippets/`, `$sections/`, and `$blocks/` imports
542
+ * to either:
543
+ * 1. src/brands/{brand}/{category}/ComponentName.tsx — brand-specific override
544
+ * 2. src/{category}/ComponentName.tsx — base component fallback
545
+ *
546
+ * Resolution stops at the first match. If neither exists, the plugin returns
547
+ * null and Vite continues with its normal alias/module resolution.
548
+ *
549
+ * Usage in vite.config.ts (must come BEFORE solidPlugin):
550
+ * plugins: [createBrandResolver('brand-a'), solidPlugin(), semiSolidPlugin(...)]
551
+ *
552
+ * Component imports opt into brand resolution by using category prefixes:
553
+ * import ProductCard from '$snippets/ProductCard'; // ← brand-resolved
554
+ * import ProductSection from '$sections/ProductSection'; // ← brand-resolved
555
+ * import ImageGallery from '$blocks/ImageGallery'; // ← brand-resolved
556
+ * import Something from '../relative/path'; // ← not resolved here
557
+ */
558
+
559
+ /**
560
+ * Resolves a component name by searching across all categories.
561
+ *
562
+ * Used by the plugin's buildStart to locate components without knowing
563
+ * which category they belong to.
564
+ *
565
+ * @param componentPath - The component name, e.g. 'ProductCard'
566
+ * @param brand - Brand identifier, e.g. 'brand-a'
567
+ * @param projectRoot - Absolute project root path
568
+ * @param existsSync - File existence check (defaults to fs.existsSync)
569
+ */
570
+ declare function resolveBrandPath(componentPath: string, brand: string, projectRoot: string, existsSync?: (p: string) => boolean): string | null;
571
+ /**
572
+ * Creates a Vite plugin that performs brand-aware component resolution for
573
+ * all `$snippets/*`, `$sections/*`, and `$blocks/*` imports.
574
+ *
575
+ * @param brand - Brand identifier, e.g. 'brand-a'
576
+ * @param projectRoot - Optional: explicit project root (mainly for testing)
577
+ */
578
+ declare function createBrandResolver(brand: string, projectRoot?: string): Plugin;
579
+
580
+ /**
581
+ * i18n.ts
582
+ *
583
+ * Utilities for locale file resolution during the build step.
584
+ *
585
+ * Shopify theme locales directory conventions:
586
+ * locales/
587
+ * en.default.json ← the locale this theme build is primary for
588
+ * fr.json ← additional locales understood by the storefront
589
+ * de.json
590
+ *
591
+ * Source locale files live at:
592
+ * src/brands/{brand}/i18n/{locale}.json
593
+ *
594
+ * The `resolveLocaleFiles` function returns the {src, dest} pairs so
595
+ * that the plugin can copy them without knowing filesystem details
596
+ * itself — the pure function form also makes unit-testing trivial.
597
+ *
598
+ * The `VIRTUAL_LOCALE_MODULE` id is provided to the Vite plugin so
599
+ * JS bundles can import the locale data at build time:
600
+ *
601
+ * import translations from 'virtual:semi-solid/locale';
602
+ * setTranslations(translations);
603
+ */
604
+ /** Vite virtual module id for the active locale JSON. */
605
+ declare const VIRTUAL_LOCALE_MODULE = "virtual:semi-solid/locale";
606
+ interface LocaleFilePair {
607
+ /** Absolute path to the source JSON in src/brands/{brand}/i18n/ */
608
+ src: string;
609
+ /** Absolute path to the destination inside the Shopify locales/ dir */
610
+ dest: string;
611
+ }
612
+ /**
613
+ * Determines all locale file copy pairs for a brand+locale build.
614
+ *
615
+ * The active `locale` is written as `{locale}.default.json` in the
616
+ * output; all other JSON files in the i18n directory keep their name
617
+ * (e.g. `fr.json` stays `fr.json`).
618
+ *
619
+ * Returns an empty array if the i18n source directory doesn't exist.
620
+ *
621
+ * @param brand - Brand identifier, e.g. 'brand-a'
622
+ * @param locale - Active build locale, e.g. 'en'
623
+ * @param projectRoot - Absolute path to the project root
624
+ * @param outDir - Absolute path to the Shopify theme output directory
625
+ * @param existsSync - Directory existence check (injectable for testing)
626
+ * @param readdirSync - Directory listing returning filenames (injectable)
627
+ */
628
+ declare function resolveLocaleFiles(brand: string, locale: string, projectRoot: string, outDir: string, existsSync?: (p: string) => boolean, readdirSync?: (dir: string) => string[]): LocaleFilePair[];
629
+ /**
630
+ * Returns the absolute path to the active locale JSON file for a build,
631
+ * or null if it doesn't exist.
632
+ *
633
+ * Used by the plugin's virtual module to inline translations into the bundle.
634
+ */
635
+ declare function resolveActiveLocalePath(brand: string, locale: string, projectRoot: string, existsSync?: (p: string) => boolean): string | null;
636
+ /**
637
+ * Returns the two virtual-module ids for use in a Vite plugin's
638
+ * `resolveId` and `load` hooks.
639
+ */
640
+ declare const virtualLocaleIds: {
641
+ readonly external: "virtual:semi-solid/locale";
642
+ readonly internal: string;
643
+ };
644
+
645
+ export { type BuildManifest, type CSSFile, type ForIteration, GLOBAL_LIQUID_OBJECTS, type LiquidGenOptions, type LocaleFilePair, type PersonalizationAssetOptions, type PersonalizedCallInfo, type RouteInfo, type SemiSolidOptions, type ShowCondition, type TapExtractResult, type TapMapping, VIRTUAL_LOCALE_MODULE, type ValidationWarning, type WarningType, createBrandResolver, detectPropVars, extractLiquidObjects, extractTapMappings, generateAssetIncludes, generateDataProps, generateHydrationEntry, generateLiquid, generateManifest, generatePreconnectTag, generatePrefetchScript, generateScriptTag, generateStylesheetTag, getRoutesDir, hashContent, isInteractiveComponent, isRouteFile, parseVersionedName, resolveActiveLocalePath, resolveBrandPath, resolveCSSFiles, resolveForIteration, resolveLocaleFiles, resolveMemberPath, resolveRoute, resolveShowCondition, semiSolidPlugin, stripLiquidBraces, toKebabCase, validateTapMappings, validateUnusedMappings, versionedName, virtualLocaleIds };