@augeo/smelt 1.2.2

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.
Files changed (152) hide show
  1. package/.claude/settings.json +11 -0
  2. package/.github/workflows/verify.yml +64 -0
  3. package/.gitmodules +3 -0
  4. package/.prettierignore +25 -0
  5. package/.prettierrc.cjs +9 -0
  6. package/.zed/settings.json +21 -0
  7. package/AGENTS.md +232 -0
  8. package/LICENSE +21 -0
  9. package/README.md +266 -0
  10. package/biome.json +58 -0
  11. package/dist/cli.d.mts +1 -0
  12. package/dist/cli.mjs +350 -0
  13. package/dist/schema.d.mts +265 -0
  14. package/dist/schema.mjs +21 -0
  15. package/docs/TESTING.md +293 -0
  16. package/docs/assets-plan.md +197 -0
  17. package/docs/build-spec.md +466 -0
  18. package/docs/library-conversion-plan.md +419 -0
  19. package/example/.gitattributes +7 -0
  20. package/example/.shopifyignore +28 -0
  21. package/example/.theme-check.yml +7 -0
  22. package/example/blocks/_built--sections--hero--blocks--feature.liquid +52 -0
  23. package/example/config/settings_schema.json +10 -0
  24. package/example/layout/theme.liquid +25 -0
  25. package/example/locales/en.default.json +1 -0
  26. package/example/package-lock.json +51 -0
  27. package/example/package.json +20 -0
  28. package/example/sections/built--sections--hero.liquid +83 -0
  29. package/example/snippets/built--components--button.liquid +38 -0
  30. package/example/snippets/built--components--card.liquid +33 -0
  31. package/example/src/components/button/button.css +13 -0
  32. package/example/src/components/card/card.css +16 -0
  33. package/example/src/components/card/card.liquid +9 -0
  34. package/example/src/sections/hero/blocks/feature/feature.css +11 -0
  35. package/example/src/sections/hero/blocks/feature/feature.liquid +9 -0
  36. package/example/src/sections/hero/blocks/feature/feature.schema.ts +14 -0
  37. package/example/src/sections/hero/hero.css +15 -0
  38. package/example/src/sections/hero/hero.liquid +16 -0
  39. package/example/src/sections/hero/hero.schema.ts +26 -0
  40. package/example/src/sections/hero/hero.test.ts +43 -0
  41. package/example/src/utilities/labels.ts +5 -0
  42. package/example/templates/index.liquid +1 -0
  43. package/example/tsconfig.json +10 -0
  44. package/example/vitest.config.ts +6 -0
  45. package/lib/build/build.test.ts +475 -0
  46. package/lib/build/build.ts +314 -0
  47. package/lib/build/command.ts +27 -0
  48. package/lib/build/index.ts +1 -0
  49. package/lib/cli.ts +17 -0
  50. package/lib/dev/command.ts +25 -0
  51. package/lib/dev/index.ts +1 -0
  52. package/lib/dev/watch.ts +52 -0
  53. package/lib/resolver.test.ts +275 -0
  54. package/lib/resolver.ts +156 -0
  55. package/lib/schema.ts +37 -0
  56. package/package.json +59 -0
  57. package/scripts/codegen-schema.ts +66 -0
  58. package/src/components/button/button.css +13 -0
  59. package/src/components/button/button.liquid +5 -0
  60. package/src/components/button/button.ts +5 -0
  61. package/src/tsconfig.json +10 -0
  62. package/tests/example.test.ts +101 -0
  63. package/tsconfig.json +20 -0
  64. package/tsdown.config.ts +14 -0
  65. package/vendor/theme-liquid-docs/.gitattributes +10 -0
  66. package/vendor/theme-liquid-docs/.github/CODEOWNERS +1 -0
  67. package/vendor/theme-liquid-docs/.github/CODE_OF_CONDUCT.md +73 -0
  68. package/vendor/theme-liquid-docs/.github/ISSUE_TEMPLATE/bug_report.md +17 -0
  69. package/vendor/theme-liquid-docs/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
  70. package/vendor/theme-liquid-docs/.github/dependabot.yaml +6 -0
  71. package/vendor/theme-liquid-docs/.github/workflows/ci.yml +33 -0
  72. package/vendor/theme-liquid-docs/.github/workflows/cla.yml +27 -0
  73. package/vendor/theme-liquid-docs/.github/workflows/shopify-dev-preview-automation.yml +86 -0
  74. package/vendor/theme-liquid-docs/.github/workflows/update-latest.yml +56 -0
  75. package/vendor/theme-liquid-docs/.prettierrc.json +16 -0
  76. package/vendor/theme-liquid-docs/.vscode/settings.json +28 -0
  77. package/vendor/theme-liquid-docs/LICENSE.md +7 -0
  78. package/vendor/theme-liquid-docs/README.md +48 -0
  79. package/vendor/theme-liquid-docs/ai/claude/CLAUDE.md +1485 -0
  80. package/vendor/theme-liquid-docs/ai/cursor/rules/assets.mdc +15 -0
  81. package/vendor/theme-liquid-docs/ai/cursor/rules/blocks.mdc +339 -0
  82. package/vendor/theme-liquid-docs/ai/cursor/rules/examples/block-example-group.mdc +103 -0
  83. package/vendor/theme-liquid-docs/ai/cursor/rules/examples/block-example-text.mdc +59 -0
  84. package/vendor/theme-liquid-docs/ai/cursor/rules/examples/section-example.mdc +61 -0
  85. package/vendor/theme-liquid-docs/ai/cursor/rules/examples/snippet-example.mdc +72 -0
  86. package/vendor/theme-liquid-docs/ai/cursor/rules/liquid.mdc +837 -0
  87. package/vendor/theme-liquid-docs/ai/cursor/rules/locales.mdc +100 -0
  88. package/vendor/theme-liquid-docs/ai/cursor/rules/localization.mdc +67 -0
  89. package/vendor/theme-liquid-docs/ai/cursor/rules/mcp.mdc +2 -0
  90. package/vendor/theme-liquid-docs/ai/cursor/rules/schemas.mdc +184 -0
  91. package/vendor/theme-liquid-docs/ai/cursor/rules/sections.mdc +84 -0
  92. package/vendor/theme-liquid-docs/ai/cursor/rules/settings-schema.mdc +51 -0
  93. package/vendor/theme-liquid-docs/ai/cursor/rules/snippets.mdc +119 -0
  94. package/vendor/theme-liquid-docs/ai/github/copilot-instructions.md +1485 -0
  95. package/vendor/theme-liquid-docs/ai/liquid.mdc +638 -0
  96. package/vendor/theme-liquid-docs/data/filters.json +6148 -0
  97. package/vendor/theme-liquid-docs/data/latest.json +2 -0
  98. package/vendor/theme-liquid-docs/data/objects.json +20594 -0
  99. package/vendor/theme-liquid-docs/data/shopify_system_translations.json +2586 -0
  100. package/vendor/theme-liquid-docs/data/tags.json +1276 -0
  101. package/vendor/theme-liquid-docs/package.json +20 -0
  102. package/vendor/theme-liquid-docs/schemas/manifest_schema.json +31 -0
  103. package/vendor/theme-liquid-docs/schemas/manifest_theme.json +19 -0
  104. package/vendor/theme-liquid-docs/schemas/manifest_theme_app_extension.json +10 -0
  105. package/vendor/theme-liquid-docs/schemas/theme/app_block_entry.json +13 -0
  106. package/vendor/theme-liquid-docs/schemas/theme/default_setting_values.json +24 -0
  107. package/vendor/theme-liquid-docs/schemas/theme/local_block_entry.json +25 -0
  108. package/vendor/theme-liquid-docs/schemas/theme/preset.json +72 -0
  109. package/vendor/theme-liquid-docs/schemas/theme/preset_blocks.json +91 -0
  110. package/vendor/theme-liquid-docs/schemas/theme/section.json +208 -0
  111. package/vendor/theme-liquid-docs/schemas/theme/setting.json +1413 -0
  112. package/vendor/theme-liquid-docs/schemas/theme/settings.json +10 -0
  113. package/vendor/theme-liquid-docs/schemas/theme/targetted_block_entry.json +15 -0
  114. package/vendor/theme-liquid-docs/schemas/theme/theme_block.json +91 -0
  115. package/vendor/theme-liquid-docs/schemas/theme/theme_block_entry.json +14 -0
  116. package/vendor/theme-liquid-docs/schemas/theme/theme_settings.json +83 -0
  117. package/vendor/theme-liquid-docs/schemas/theme/translations.json +63 -0
  118. package/vendor/theme-liquid-docs/schemas/update/update_extension_schema_v1.json +186 -0
  119. package/vendor/theme-liquid-docs/tests/fixtures/section-nested-blocks.json +18 -0
  120. package/vendor/theme-liquid-docs/tests/fixtures/section-schema-1.json +90 -0
  121. package/vendor/theme-liquid-docs/tests/fixtures/section-schema-2.json +201 -0
  122. package/vendor/theme-liquid-docs/tests/fixtures/section-schema-3.json +29 -0
  123. package/vendor/theme-liquid-docs/tests/fixtures/section-schema-4.json +315 -0
  124. package/vendor/theme-liquid-docs/tests/fixtures/section-schema-5.json +114 -0
  125. package/vendor/theme-liquid-docs/tests/fixtures/section-schema-6.json +63 -0
  126. package/vendor/theme-liquid-docs/tests/fixtures/section-schema-conditional-settings.json +145 -0
  127. package/vendor/theme-liquid-docs/tests/fixtures/section-schema-preset-blocks-as-hash.json +60 -0
  128. package/vendor/theme-liquid-docs/tests/fixtures/section-schema-static-block-preset.json +76 -0
  129. package/vendor/theme-liquid-docs/tests/fixtures/section-settings.json +34 -0
  130. package/vendor/theme-liquid-docs/tests/fixtures/theme-block-1.json +234 -0
  131. package/vendor/theme-liquid-docs/tests/fixtures/theme-block-2.json +253 -0
  132. package/vendor/theme-liquid-docs/tests/fixtures/theme-block-basics.json +48 -0
  133. package/vendor/theme-liquid-docs/tests/fixtures/theme-block-conditional-settings.json +202 -0
  134. package/vendor/theme-liquid-docs/tests/fixtures/theme-block-presets-as-hash.json +50 -0
  135. package/vendor/theme-liquid-docs/tests/fixtures/theme-block-settings.json +34 -0
  136. package/vendor/theme-liquid-docs/tests/fixtures/theme-settings-all-settings.json +313 -0
  137. package/vendor/theme-liquid-docs/tests/fixtures/theme-settings-dawn.json +1469 -0
  138. package/vendor/theme-liquid-docs/tests/fixtures/theme-settings-metadata.json +10 -0
  139. package/vendor/theme-liquid-docs/tests/fixtures/translations-1.json +14 -0
  140. package/vendor/theme-liquid-docs/tests/section.spec.ts +367 -0
  141. package/vendor/theme-liquid-docs/tests/test-constants.ts +58 -0
  142. package/vendor/theme-liquid-docs/tests/test-helpers.ts +104 -0
  143. package/vendor/theme-liquid-docs/tests/theme-settings/color_palette.spec.ts +184 -0
  144. package/vendor/theme-liquid-docs/tests/theme-settings/color_scheme_group.spec.ts +143 -0
  145. package/vendor/theme-liquid-docs/tests/theme-settings/general.spec.ts +192 -0
  146. package/vendor/theme-liquid-docs/tests/theme-settings/metaobject.spec.ts +94 -0
  147. package/vendor/theme-liquid-docs/tests/theme-settings/resource_list.spec.ts +58 -0
  148. package/vendor/theme-liquid-docs/tests/theme-settings/theme-metadata.spec.ts +59 -0
  149. package/vendor/theme-liquid-docs/tests/theme_block.spec.ts +266 -0
  150. package/vendor/theme-liquid-docs/tests/translations_schema.spec.ts +31 -0
  151. package/vendor/theme-liquid-docs/yarn.lock +543 -0
  152. package/vitest.config.ts +7 -0
@@ -0,0 +1,466 @@
1
+ ---
2
+ status: draft
3
+ ---
4
+
5
+ # Smelt Build Pipeline
6
+
7
+ Spec for the source-to-theme compiler that turns the `src/` authoring tree into
8
+ the flat, namespaced files Shopify expects.
9
+
10
+ The build is the only thing this document describes. Theme-wide concerns
11
+ (initial scaffolding choices, deployment workflow, store configuration) live
12
+ elsewhere or are out of scope.
13
+
14
+ ## Goal
15
+
16
+ Let theme authors work in colocated components (Liquid + TS + CSS + tests in one
17
+ directory) and have a build step emit the flat, namespaced output Shopify
18
+ expects. The build is **additive**: hand-written files in `sections/`,
19
+ `snippets/`, etc. are never touched — the compiler only writes files prefixed
20
+ `built--` (or `_built--` for private theme blocks).
21
+
22
+ ## Inputs and Outputs
23
+
24
+ The build runs in a **consumer theme's** repo. It reads from an ordered list of
25
+ **layers** — directories that each contain a
26
+ `src/{sections,blocks,components, utilities}/` tree — and writes the merged
27
+ output into Shopify's flat directories at the consumer's repo root.
28
+
29
+ In this repository the consumer is `example/`, so paths shown below as `src/…`
30
+ and `sections/…` are physically at `example/src/…` and `example/sections/…`. The
31
+ pipeline itself is identical either way.
32
+
33
+ ```
34
+ <layer>/src/ # one such tree per layer
35
+ ├── sections/ # → sections/built--*.liquid
36
+ ├── blocks/ # → blocks/built--*.liquid
37
+ ├── components/ # → snippets/built--*.liquid
38
+ └── utilities/ # no Liquid output; bundled into importers
39
+
40
+ sections/ # outputs (mixed with hand-written) — at the
41
+ snippets/ # consumer's repo root, written by the build
42
+ blocks/
43
+ ```
44
+
45
+ The default layer list is `[<consumer>, "@augeo/smelt"]` — the consumer's own
46
+ tree first, then Smelt's baseline. See [Layer Resolution](#layer-resolution)
47
+ below for how files merge across layers.
48
+
49
+ Directories the build does **not** read or write:
50
+
51
+ - `config/`, `layout/`, `templates/`, `locales/`, `assets/` — entirely
52
+ hand-written in v1.
53
+ - Any file in an output directory that lacks the `built--` prefix.
54
+
55
+ ## Component Model
56
+
57
+ A **component** is a directory with at minimum a `.liquid` file matching the
58
+ directory name. Optional sibling files extend it:
59
+
60
+ ```
61
+ src/components/button/
62
+ ├── button.liquid # required — Liquid template
63
+ ├── button.ts # optional — inlined as {% javascript %}
64
+ ├── button.css # optional — inlined as {% stylesheet %}
65
+ ├── button.test.ts # optional — vitest test (via @augeo/assay)
66
+ └── props.ts # optional — typed schema (future)
67
+ ```
68
+
69
+ Component types differ only by their source directory and output target:
70
+
71
+ | Source dir | Output dir | Notes |
72
+ | ----------------- | ----------- | ---------------------------------------- |
73
+ | `src/sections/` | `sections/` | Renderable as Shopify sections |
74
+ | `src/blocks/` | `blocks/` | Theme blocks |
75
+ | `src/components/` | `snippets/` | Reusable across sections/blocks/snippets |
76
+ | `src/utilities/` | (none) | TS/CSS-only; bundled into importers |
77
+
78
+ Nested components are supported and namespaced into the output name (see
79
+ [Output Naming](#output-naming)).
80
+
81
+ ## Layer Resolution
82
+
83
+ The build accepts an ordered list of **layers**; each layer is a directory
84
+ containing its own `src/{sections,blocks,components,utilities}/` tree. The
85
+ default layers, in order, are:
86
+
87
+ 1. **The consumer's layer** — `process.cwd()`. The consumer's own `src/`.
88
+ 2. **`@augeo/smelt`** — the published package's `src/`, the baseline.
89
+
90
+ For every component (keyed by `type` + path segments), each of the five file
91
+ slots — `liquid`, `ts`, `css`, `test.ts`, `schema.ts` — is resolved
92
+ **independently**: the first layer that has the file wins, the later layers are
93
+ shadowed.
94
+
95
+ A consumer who drops just `src/components/button/button.css` into their tree
96
+ inherits the baseline's `button.liquid` and `button.ts` and overrides only the
97
+ styles. The same shadowing rule applies to all five slots.
98
+
99
+ A component is included in the build if **any** layer has its `liquid` slot
100
+ filled. A consumer-only `.css` for a component that doesn't exist in any layer
101
+ is dead code (no liquid anchor) and is silently ignored.
102
+
103
+ ## Authoring Conventions
104
+
105
+ ### Liquid imports — `@/` and `./` aliases
106
+
107
+ Inside `.liquid` source files, reference other source components via one of two
108
+ aliases (modeled on Next.js / TypeScript path conventions):
109
+
110
+ - **`@/...`** — rooted at `src/`. Use for anything outside the current
111
+ component's subtree.
112
+ - **`./...`** — relative to the current source file's directory. Use for
113
+ references into the current component's nested children. **Descending only —
114
+ `../` is not supported.** If you need to reach a sibling-of-parent, use `@/`;
115
+ if you find yourself wanting `../` repeatedly, the nested component should
116
+ probably be hoisted to a shared location.
117
+
118
+ <!-- prettier-ignore -->
119
+ ```liquid
120
+ {# inside src/sections/hero/hero.liquid: #}
121
+
122
+ {% render './components/foo' %} {# nested child of hero #}
123
+ {% render '@/components/button' %} {# top-level shared component #}
124
+ ```
125
+
126
+ Both rewrite to namespaced output names:
127
+
128
+ ```liquid
129
+ {% render 'built--sections--hero--components--foo' %}
130
+ {% render 'built--components--button' %}
131
+ ```
132
+
133
+ Hand-written snippets remain referenceable by their bare name as usual:
134
+
135
+ ```liquid
136
+ {% render 'icon-cart' %} {# unchanged — resolves to snippets/icon-cart.liquid #}
137
+ ```
138
+
139
+ ### TS / CSS — colocated and implicit
140
+
141
+ If `<name>.ts` exists alongside `<name>.liquid`, it is the component's
142
+ JavaScript entry point. The bundled output is inlined into a `{% javascript %}`
143
+ tag at the end of the built `.liquid` file. Same for `<name>.css` →
144
+ `{% stylesheet %}`.
145
+
146
+ Each component owns its own `{% javascript %}` / `{% stylesheet %}` block.
147
+ Shopify supports these tags in sections, blocks, and snippets (one per file) and
148
+ handles per-file asset deduplication at runtime.
149
+
150
+ ### Cross-component imports — utilities only
151
+
152
+ A component's TS may import from `src/utilities/` and those will be bundled into
153
+ the component's IIFE. Importing another _component's_ TS
154
+ (`src/components/foo/foo.ts`) is **not supported** — foo is rendered as its own
155
+ snippet with its own script tag; the DOM already gets foo's behavior when
156
+ `{% render 'foo' %}` runs.
157
+
158
+ ### Section / block schemas — `<name>.schema.ts`
159
+
160
+ Sections and theme blocks can author their schema as a TypeScript file alongside
161
+ the component:
162
+
163
+ ```ts
164
+ // example/src/sections/hero/hero.schema.ts
165
+ import { defineSchemaSection } from "@augeo/smelt/schema";
166
+
167
+ export const schema = defineSchemaSection({
168
+ name: "Hero",
169
+ settings: [
170
+ { type: "text", id: "heading", label: "Heading", default: "Welcome" },
171
+ ],
172
+ presets: [{ name: "Hero" }],
173
+ });
174
+ ```
175
+
176
+ Conventions:
177
+
178
+ - **Filename:** `<name>.schema.ts`.
179
+ - **Required export:** a named `schema` constant. Other named exports are fine
180
+ and ignored.
181
+ - **Helper per component kind:** `defineSchemaSection` for `src/sections/*`,
182
+ `defineSchemaBlock` for `src/blocks/*`. Each is an identity function whose
183
+ generic preserves literal types so the schema author gets autocomplete for
184
+ `type` / `id` / setting shapes.
185
+ - **Types are codegen'd** from Shopify's authoritative JSON Schemas in
186
+ `vendor/theme-liquid-docs`. Update via
187
+ `npm run docs:update && npm run schema:codegen`.
188
+
189
+ At build time the engine executes the schema file (esbuild bundle → `import()`),
190
+ takes the `schema` export, `JSON.stringify`s it, and injects a
191
+ `{% schema %} … {% endschema %}` block into the built `.liquid`.
192
+
193
+ **Schemas may only live in `<name>.schema.ts` files.** Inline `{% schema %}`
194
+ blocks in `.liquid` source files are a build error — the engine refuses to emit
195
+ and points at the expected sibling filename. This keeps a single source of truth
196
+ and avoids surprises when shadowing across layers.
197
+
198
+ Shared settings (padding, alignment, etc.) compose through plain TS imports:
199
+
200
+ ```ts
201
+ import { paddingSettings } from "@/utilities/schemas/padding";
202
+
203
+ export const schema = defineSchemaSection({
204
+ name: "Hero",
205
+ settings: [
206
+ { type: "text", id: "heading", label: "Heading" },
207
+ ...paddingSettings,
208
+ ],
209
+ });
210
+ ```
211
+
212
+ ### Private blocks — nested `blocks/`
213
+
214
+ Shopify treats theme block files prefixed with `_` as **private**: hidden from
215
+ the merchant's block picker, renderable only via the parent's
216
+ `{% content_for "blocks" %}`. Smelt expresses this structurally — a `blocks/`
217
+ directory nested under a section or block emits its children with the `_` prefix
218
+ on the built filename, mirroring the parent/child relationship that private
219
+ blocks already imply in Shopify.
220
+
221
+ ```
222
+ src/sections/hero/
223
+ ├── hero.liquid
224
+ ├── hero.schema.ts
225
+ └── blocks/
226
+ └── headline/
227
+ ├── headline.liquid
228
+ └── headline.schema.ts
229
+ ```
230
+
231
+ Compiles to:
232
+
233
+ ```
234
+ sections/built--sections--hero.liquid
235
+ blocks/_built--sections--hero--blocks--headline.liquid
236
+ ```
237
+
238
+ Top-level `src/blocks/*` files remain public (no `_` prefix). The trigger is
239
+ "this block was reached through a nested `blocks/` type marker" — i.e.
240
+ `component.type === "blocks"` AND a `"blocks"` segment appears at any position
241
+ after the root (`component.segments.slice(1).includes("blocks")`). That covers
242
+ both a section/component owning private blocks (`sections/hero/blocks/headline`)
243
+ and a public block owning its own private blocks (`blocks/promo/blocks/item`).
244
+ The rule applies recursively: a private block can have its own `blocks/` subdir,
245
+ producing privately-scoped grandchildren that walk the same way.
246
+
247
+ The parent section or block's schema auto-merges these private children into its
248
+ `blocks: [...]` array — discovered children sorted by directory name and
249
+ prepended, any blocks the author explicitly lists (e.g. globally-shared block
250
+ types) appended after. No helper call required.
251
+
252
+ ## Build Pipeline
253
+
254
+ The engine lives in `lib/build/build.ts` and is invoked by `lib/cli.ts` (a citty
255
+ CLI bundled by tsdown to `dist/cli.mjs` — the published `smelt` bin). From a
256
+ consumer's repo the commands are `smelt build` (one-shot compile) and
257
+ `smelt dev` (build then watch — see below). From this repo, `npm run build`
258
+ rebuilds the bin and `npm run test:all` runs the full integration chain against
259
+ `example/`. The engine itself:
260
+
261
+ 1. **Resolve layers** — walk each layer's `src/` tree and produce a merged
262
+ `Map<segments, ResolvedComponent>` where each component's `liquid` / `ts` /
263
+ `css` / `test` / `schema` slots are independently first-wins (see
264
+ [Layer Resolution](#layer-resolution)).
265
+ 2. **Clean previous output** — sweep any file matching `/^_?built--/` from
266
+ `sections/`, `snippets/`, and `blocks/`. Output is regenerated from scratch
267
+ on every build, so removed source components disappear cleanly. Hand-written
268
+ files without the prefix are untouched.
269
+ 3. **For each component** (anything with a `.liquid`):
270
+ - Prepend a `{%- comment -%}` banner naming the source file, for tracing.
271
+ - Bundle `<name>.ts` (esbuild, IIFE, utilities inlined) → string.
272
+ - Read `<name>.css` as-is (no transform today — see TODO).
273
+ - Execute `<name>.schema.ts` — esbuild → `data:` URL → dynamic `import()`,
274
+ capturing the named `schema` export. esbuild auto-discovers the consumer's
275
+ `tsconfig.json` so `@/...` (and any custom path aliases) resolve the way
276
+ `tsc` sees them.
277
+ - Auto-merge any immediate private `blocks/` children into the loaded
278
+ schema's `blocks: [...]` array (see
279
+ [Private blocks](#private-blocks--nested-blocks)).
280
+ - Transform `<name>.liquid`:
281
+ - **Error** if the source `.liquid` contains an inline `{% schema %}` block
282
+ — schemas must live in `<name>.schema.ts`.
283
+ - Rewrite `{% render '@/...' %}` and `{% render './...' %}` to namespaced
284
+ output names (private blocks get the `_built--` form).
285
+ - Append `{% schema %}…{% endschema %}` block if schema present.
286
+ - Append `{% stylesheet %}…{% endstylesheet %}` block if CSS present.
287
+ - Append `{% javascript %}…{% endjavascript %}` block if TS present.
288
+ - Emit to the matching output directory under the namespaced name.
289
+
290
+ The build never touches files in output directories that lack the `built--` /
291
+ `_built--` prefix.
292
+
293
+ **Watch mode (`smelt dev`)** re-runs the whole pipeline on any source file
294
+ change, debounced ~100ms. Build failures log and keep the watcher alive. Today
295
+ that's a full rebuild every time — see TODO for the dependency-graph-aware
296
+ version.
297
+
298
+ ## Output Naming
299
+
300
+ Source path → output name:
301
+
302
+ ```
303
+ src/components/button/button.liquid
304
+ → snippets/built--components--button.liquid
305
+
306
+ src/sections/hero/hero.liquid
307
+ → sections/built--sections--hero.liquid
308
+
309
+ src/sections/hero/components/foo/foo.liquid
310
+ → snippets/built--sections--hero--components--foo.liquid
311
+
312
+ src/blocks/promo/promo.liquid
313
+ → blocks/built--blocks--promo.liquid
314
+ ```
315
+
316
+ Rules:
317
+
318
+ - Prefix `built--` marks compiler output: anything matching `/^_?built--/` in
319
+ `sections/`, `snippets/`, or `blocks/` is the build's territory — cleaned and
320
+ rewritten every build. Hand-written files in those directories are untouched.
321
+ - Path segments under `src/` are joined with `--`.
322
+ - Final segment (directory name) appears once; the matching `.liquid` filename
323
+ is not duplicated.
324
+ - Sections, blocks, and components-of-components all flatten into the same set
325
+ of Shopify-flat directories.
326
+ - Private blocks (reached through a nested `blocks/` marker) get an additional
327
+ `_` prefix on the filename — see
328
+ [Private blocks](#private-blocks--nested-blocks).
329
+
330
+ ## Worked Example
331
+
332
+ A fuller picture showing source files, nested components, and the compiled
333
+ output sitting alongside hand-written files.
334
+
335
+ ### Source (`src/`)
336
+
337
+ ```
338
+ src/
339
+ ├── sections/
340
+ │ └── hero/
341
+ │ ├── hero.liquid
342
+ │ ├── hero.css
343
+ │ ├── hero.ts
344
+ │ ├── hero.test.ts
345
+ │ ├── components/
346
+ │ │ └── foo/
347
+ │ │ ├── foo.liquid
348
+ │ │ ├── foo.css
349
+ │ │ ├── foo.ts
350
+ │ │ └── foo.test.ts
351
+ │ └── blocks/
352
+ │ └── headline/ # private — see Private blocks section
353
+ │ ├── headline.liquid
354
+ │ └── headline.schema.ts
355
+ ├── blocks/
356
+ ├── components/
357
+ │ └── button/
358
+ │ ├── props.ts # typed prop schema (future — see Open Questions)
359
+ │ ├── button.liquid
360
+ │ ├── button.css
361
+ │ ├── button.ts
362
+ │ └── button.test.ts
363
+ └── utilities/
364
+ ```
365
+
366
+ ### Compiled output (Shopify-flat dirs)
367
+
368
+ ```
369
+ sections/
370
+ ├── built--sections--hero.liquid # ← src/sections/hero/
371
+ └── old_legacy_section.liquid # hand-written, preserved
372
+
373
+ snippets/
374
+ ├── built--sections--hero--components--foo.liquid # nested under hero
375
+ ├── built--components--button.liquid # top-level component
376
+ └── icon-cart.liquid # hand-written, preserved
377
+
378
+ blocks/
379
+ └── _built--sections--hero--blocks--headline.liquid # private to hero
380
+
381
+ assets/
382
+ └── (hand-written static files; build does not write here in v1)
383
+ ```
384
+
385
+ Notes:
386
+
387
+ - Co-located `hero.ts` / `hero.css` are inlined into
388
+ `built--sections--hero.liquid` as `{% javascript %}` / `{% stylesheet %}`
389
+ blocks — no separate files in `assets/`.
390
+ - `built--` is the only namespace the compiler writes. Files without the prefix
391
+ (`old_legacy_section.liquid`, `icon-cart.liquid`) are treated as hand-written
392
+ and survive rebuilds untouched.
393
+ - The nested `hero/components/foo/` directory illustrates that components scoped
394
+ to a single section live inside it and flatten into a path-encoded snippet
395
+ name on output.
396
+
397
+ ## Testing
398
+
399
+ Tests use [`@augeo/assay`](https://www.npmjs.com/package/@augeo/assay) — Vitest
400
+ browser mode + liquidjs + Playwright.
401
+
402
+ - `<name>.test.ts` lives beside `<name>.liquid`.
403
+ - Tests run against the **built** theme directories (Liquid in `sections/`,
404
+ `snippets/`, `blocks/`), not against `src/` directly, since assay / liquidjs
405
+ cannot resolve the `@/...` alias.
406
+ - `npm test` runs `build` first, then `vitest`.
407
+
408
+ Vitest config uses the assay preset:
409
+
410
+ ```ts
411
+ import { assayPreset } from "@augeo/assay/preset";
412
+
413
+ export default assayPreset({
414
+ liquidPaths: ["./sections", "./snippets", "./blocks"],
415
+ assetsPath: "./assets",
416
+ });
417
+ ```
418
+
419
+ ## Decisions Made (and Why)
420
+
421
+ - **`{% javascript %}` / `{% stylesheet %}` inlining over standalone
422
+ `assets/section-*.js` files.** Compiled output is inlined into the
423
+ per-component tag; Shopify owns the asset generation and runtime dedup. Less
424
+ file proliferation in `assets/`. An earlier objection — that these tags were
425
+ section-only — was based on outdated docs; current Shopify supports them in
426
+ sections, blocks, and snippets (one tag of each kind per file).
427
+ - **No cross-component TS imports.** Each component is its own bundle. Sharing
428
+ happens through `src/utilities/`, which has no Liquid output and is purely a
429
+ code-sharing layer.
430
+ - **Tests run against built output.** Avoids reimplementing the `@/...` resolver
431
+ inside the test runner. `npm test` builds first, then runs vitest.
432
+ - **Build is additive, not destructive.** The compiler only writes files
433
+ prefixed `built--`. Hand-written files in output directories are preserved
434
+ across rebuilds, allowing gradual migration into `src/`.
435
+
436
+ ## Open Questions
437
+
438
+ - `src/components/<x>/props.ts` — typed prop schema for non-section/block
439
+ components (snippets), declaring the variables a `{% render %}` call must
440
+ pass. Distinct from `<name>.schema.ts`, which describes Shopify's
441
+ merchant-facing settings for sections and theme blocks. Possibly with runtime
442
+ validation injected into the built snippet for dev mode. Defer.
443
+ - LSP / editor support for the `@/...` alias (go-to-definition). Defer.
444
+
445
+ ## TODO
446
+
447
+ Deferred work — things we've decided we want to address eventually but aren't
448
+ scoped or designed yet.
449
+
450
+ - **Utility deduplication across component bundles.** Currently each component's
451
+ IIFE inlines its imported utilities, so a utility used by N components ships N
452
+ times. Acceptable for a small theme; revisit when measured page weight
453
+ justifies the design work. Candidate approaches include a single shared
454
+ `assets/built--utilities.js` bundle exposed on a global, ES modules via an
455
+ importmap, or per-utility snippet guards.
456
+
457
+ - **CSS processing.** Today `<name>.css` is inlined as-is. A real pipeline would
458
+ run something like [lightningcss](https://lightningcss.dev/) or
459
+ [postcss](https://postcss.org/) for autoprefixing, nesting, custom-property
460
+ fallbacks, and source-of-truth normalization across components. Defer until
461
+ someone authoring components asks for a feature that needs it.
462
+
463
+ - **Dependency-graph-aware watch.** `smelt dev` currently re-runs the whole
464
+ pipeline on any source change. A smarter loop would track which components
465
+ import which utilities and rebuild only the affected set — important once
466
+ builds get slow enough that full-rebuild latency hurts the authoring loop.