@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.
- package/LICENSE +21 -0
- package/dist/chunk-BDCUB2QP.js +28 -0
- package/dist/chunk-HSIWLJX2.js +41 -0
- package/dist/chunk-X2A6LNIZ.js +429 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +9 -0
- package/dist/cli/config.d.ts +17 -0
- package/dist/cli/config.js +6 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +8 -0
- package/dist/index.d.ts +645 -0
- package/dist/index.js +2115 -0
- package/package.json +46 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|