@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,293 @@
1
+ # Testing
2
+
3
+ Conventions for writing tests in Smelt. Most of this is project-agnostic
4
+ guidance adapted from [betterspecs.org](https://www.betterspecs.org/) and
5
+ [bettertests.js.org](https://bettertests.js.org/).
6
+
7
+ The test runner is [Vitest](https://vitest.dev/) in Browser Mode via
8
+ [`@augeo/assay`](https://www.npmjs.com/package/@augeo/assay). Tests run against
9
+ the **built** theme directories (`sections/`, `snippets/`, `blocks/`), since
10
+ assay / LiquidJS cannot resolve the `@/...` source alias.
11
+
12
+ - [Describe Your Files](#describe-your-files)
13
+ - [When the Filename Doesn't Match the Exports / Multiple Exports](#when-the-filename-doesnt-match-the-exports--multiple-exports)
14
+ - [Nest Describes to Add Context](#nest-describes-to-add-context)
15
+ - [Use `it` to Describe the Expected Behavior](#use-it-to-describe-the-expected-behavior)
16
+ - [Single Expectation Tests](#single-expectation-tests)
17
+ - [Use Subjects](#use-subjects)
18
+ - [Before / After Hooks Should Be Single Concern](#before--after-hooks-should-be-single-concern)
19
+ - [Don't Use 'should'](#dont-use-should)
20
+ - [Describe the Expected Behavior / What the User Will See](#describe-the-expected-behavior--what-the-user-will-see)
21
+ - [Use Factories](#use-factories)
22
+ - [When to Mock](#when-to-mock)
23
+ - [Inspiration](#inspiration)
24
+
25
+ ## Describe Your Files
26
+
27
+ Usually a file should have a single named export that's named the same as the
28
+ containing file. In this case your root `describe` should be the exported
29
+ object.
30
+
31
+ #### Bad
32
+
33
+ ```ts
34
+ describe("a Hamburger Menu component", () => {});
35
+ describe("a function that describes sandwiches", () => {});
36
+ describe("a constant holding our ingredients", () => {});
37
+ ```
38
+
39
+ #### Good
40
+
41
+ ```ts
42
+ describe("<HamburgerMenu />", () => {});
43
+ describe("describeSandwiches()", () => {});
44
+ describe("ingredients", () => {});
45
+ ```
46
+
47
+ For Liquid components, name the describe after the component file:
48
+
49
+ ```ts
50
+ describe("button.liquid", () => {});
51
+ describe("hero.liquid", () => {});
52
+ ```
53
+
54
+ ### When the Filename Doesn't Match the Exports / Multiple Exports
55
+
56
+ If you have multiple exports in a file or the file name doesn't match the export
57
+ name, use a root-level describe for the file that's as short as possible while
58
+ still being descriptive, then nest describes per export.
59
+
60
+ #### Bad
61
+
62
+ ```ts
63
+ describe("addPickles()", () => {});
64
+ describe("removePickles()", () => {});
65
+ ```
66
+
67
+ #### Good
68
+
69
+ ```ts
70
+ describe("pickleManager", () => {
71
+ describe("addPickles()", () => {});
72
+ describe("removePickles()", () => {});
73
+ });
74
+ ```
75
+
76
+ ## Nest Describes to Add Context
77
+
78
+ Use nested describes to add context — they keep tests clear and well organized.
79
+ When describing a context, start its description with `when`, `as`, `with`, or
80
+ `without`.
81
+
82
+ Nested describes are also useful to group tests that share setup or mock
83
+ requirements. `beforeEach` is preferred for isolation; reach for `beforeAll`
84
+ only when setup is expensive.
85
+
86
+ #### Bad
87
+
88
+ ```ts
89
+ it("shows order details when logged in", () => {});
90
+ it("redirects to login when logged out", () => {});
91
+ ```
92
+
93
+ #### Good
94
+
95
+ ```ts
96
+ describe("when logged in", () => {
97
+ beforeEach(setupValidSession);
98
+
99
+ it("shows order details", () => {});
100
+ });
101
+
102
+ describe("when logged out", () => {
103
+ beforeEach(setupInvalidSession);
104
+
105
+ it("redirects to login", () => {});
106
+ });
107
+ ```
108
+
109
+ ## Use `it` to Describe the Expected Behavior
110
+
111
+ #### Bad
112
+
113
+ ```ts
114
+ test("constructs a sandwich", () => {});
115
+ ```
116
+
117
+ #### Good
118
+
119
+ ```ts
120
+ it("constructs a sandwich", () => {});
121
+ ```
122
+
123
+ ## Single Expectation Tests
124
+
125
+ Each test should specify one (and only one) behavior. Multiple expectations in
126
+ the same test usually signal you're specifying multiple behaviors — but there
127
+ are cases where multiple expectations support a single behavior.
128
+
129
+ When a test fails, it should be immediately clear what behavior is broken.
130
+
131
+ #### Bad
132
+
133
+ ```ts
134
+ it("renders the product card", async () => {
135
+ await render("product-card", { product });
136
+
137
+ await expect.element(page.getByText("Classic Tee")).toBeVisible();
138
+ await expect.element(page.getByText("$29.99")).toBeVisible();
139
+ });
140
+ ```
141
+
142
+ #### Good
143
+
144
+ ```ts
145
+ describe("content", () => {
146
+ beforeEach(() => render("product-card", { product }));
147
+
148
+ it("shows the product title", async () => {
149
+ await expect.element(page.getByText("Classic Tee")).toBeVisible();
150
+ });
151
+
152
+ it("shows the formatted price", async () => {
153
+ await expect.element(page.getByText("$29.99")).toBeVisible();
154
+ });
155
+ });
156
+ ```
157
+
158
+ Multiple expectations supporting one behavior are fine:
159
+
160
+ ```ts
161
+ it("formats discounts", async () => {
162
+ // Both expectations describe one behavior: the discount styling.
163
+ await expect.element(page.getByText("Sale")).toHaveClass("discount");
164
+ await expect.element(page.getByText("Sale")).not.toHaveClass("full-price");
165
+ });
166
+ ```
167
+
168
+ ## Use Subjects
169
+
170
+ If you have several tests related to the same subject, use a `describe` block to
171
+ DRY them up.
172
+
173
+ #### Bad
174
+
175
+ ```ts
176
+ it("shows the title", async () => {
177
+ await render("product-card", { product: { title: "Clubhouse" } });
178
+ await expect.element(page.getByText("Clubhouse")).toBeVisible();
179
+ });
180
+
181
+ it("shows the description", async () => {
182
+ await render("product-card", { product: { title: "Clubhouse" } });
183
+ await expect.element(page.getByText("A delicious sandwich")).toBeVisible();
184
+ });
185
+ ```
186
+
187
+ #### Good
188
+
189
+ ```ts
190
+ describe("content", () => {
191
+ beforeEach(() => render("product-card", { product: { title: "Clubhouse" } }));
192
+
193
+ it("shows the title", async () => {
194
+ await expect.element(page.getByText("Clubhouse")).toBeVisible();
195
+ });
196
+
197
+ it("shows the description", async () => {
198
+ await expect.element(page.getByText("A delicious sandwich")).toBeVisible();
199
+ });
200
+ });
201
+ ```
202
+
203
+ ## Before / After Hooks Should Be Single Concern
204
+
205
+ Lifecycle hooks should each relate to a single concern. Biome's
206
+ `noDuplicateTestHooks` rule is disabled in `biome.json` to allow this pattern.
207
+
208
+ #### Bad
209
+
210
+ ```ts
211
+ describe("with lettuce", () => {
212
+ beforeEach(() => {
213
+ sandwich.addLettuce();
214
+ restaurant.build(sandwich);
215
+ });
216
+ });
217
+ ```
218
+
219
+ #### Good
220
+
221
+ ```ts
222
+ describe("with lettuce", () => {
223
+ beforeEach(() => sandwich.addLettuce());
224
+ beforeEach(() => restaurant.build(sandwich));
225
+ });
226
+ ```
227
+
228
+ ## Don't Use 'should'
229
+
230
+ Don't use "should" when describing tests. Use the third person, present tense.
231
+
232
+ #### Bad
233
+
234
+ ```ts
235
+ it("should return a sandwich", () => {});
236
+ ```
237
+
238
+ #### Good
239
+
240
+ ```ts
241
+ it("returns a sandwich", () => {});
242
+ ```
243
+
244
+ ## Describe the Expected Behavior / What the User Will See
245
+
246
+ Describe and test the expected behavior, not implementation details. See
247
+ [Testing Library's guiding principles](https://testing-library.com/docs/guiding-principles).
248
+
249
+ #### Bad
250
+
251
+ ```ts
252
+ it("fetches order from the DB", () => {});
253
+ it("calls 'showToast'", () => {});
254
+ ```
255
+
256
+ #### Good
257
+
258
+ ```ts
259
+ it("returns the order's details", () => {});
260
+ it("shows a success message", () => {});
261
+ ```
262
+
263
+ ## Use Factories
264
+
265
+ Don't use fixtures — they're hard to control. Use factories instead to reduce
266
+ verbosity when creating test data.
267
+
268
+ ## When to Mock
269
+
270
+ Don't overuse mocks. They're useful when you need to isolate the interface of a
271
+ dependency. In a strongly-typed language, type-checking already gives you a lot
272
+ of that confidence — calling through to the real implementation is often fine
273
+ when it's cheap.
274
+
275
+ That said, asserting that a dependency was called (rather than relying on its
276
+ side effects) can be valuable: it decouples your test from the dependency's
277
+ implementation.
278
+
279
+ > Mock-based tests are more coupled to the interfaces in your system, while
280
+ > classical tests are more coupled to the implementation of an object's
281
+ > collaborators.
282
+ >
283
+ > —
284
+ > [Thoughts on Mocking](https://web.archive.org/web/20220612005103/http://myronmars.to/n/dev-blog/2012/06/thoughts-on-mocking)
285
+
286
+ For mocking nested `{% render %}` calls in Liquid tests, see the
287
+ [`@augeo/assay` mocking docs](https://github.com/seanhealy/assay/blob/main/docs/advanced-usage.md#mocking).
288
+
289
+ ## Inspiration
290
+
291
+ Most of this guidance comes from [betterspecs.org](https://www.betterspecs.org/)
292
+ — written for RSpec but a great guide for tests in general. Some additional
293
+ guidance from [bettertests.js.org](https://bettertests.js.org/).
@@ -0,0 +1,197 @@
1
+ ---
2
+ status: draft
3
+ ---
4
+
5
+ # Component-Owned Assets
6
+
7
+ Plan for letting a component own colocated binary assets (PNG, JPG, WebP, WOFF2,
8
+ anything that has to be a real file at a Shopify-resolvable URL). Companion to
9
+ [`build-spec.md`](./build-spec.md) — the build spec describes today's behavior;
10
+ this doc describes the shape and decisions for adding asset emission.
11
+
12
+ ## Goal
13
+
14
+ A consumer authoring an icon component should be able to put a `cart.png` next
15
+ to `icon.liquid` and reference it from Liquid without leaving the component
16
+ directory or mangling Shopify-flat asset names by hand. Layering should apply to
17
+ assets the same way it does to every other slot: a consumer dropping in a
18
+ same-named PNG shadows the baseline's.
19
+
20
+ Today the spec says `assets/` is "entirely hand-written in v1" — this plan flips
21
+ that constraint.
22
+
23
+ ## The authoring shape
24
+
25
+ A component opts into asset emission by adding an `assets/` subdirectory:
26
+
27
+ ```
28
+ src/components/icon/
29
+ ├── icon.liquid
30
+ ├── icon.css
31
+ └── assets/
32
+ ├── cart.png
33
+ ├── search.png
34
+ └── menu.png
35
+ ```
36
+
37
+ Build copies each file to:
38
+
39
+ ```
40
+ assets/built--components--icon--cart.png
41
+ assets/built--components--icon--search.png
42
+ assets/built--components--icon--menu.png
43
+ ```
44
+
45
+ The author writes assets the standard Shopify way — `asset_url` filter, the
46
+ output landing in the flat `assets/` directory the storefront actually serves
47
+ from.
48
+
49
+ ## Design Decisions
50
+
51
+ ### 1. Per-component `assets/` subdir, not loose siblings
52
+
53
+ A component's assets live under a dedicated `assets/` subdir, not as bare files
54
+ next to `<name>.liquid`.
55
+
56
+ - **Why:** explicit signal. The build doesn't have to guess "is this `.png` an
57
+ asset or some slot we haven't defined yet?" The convention is discoverable
58
+ from a single `ls` of the component directory.
59
+ - **Cost:** one extra directory level in source. Worth it for the clarity.
60
+
61
+ ### 2. Output naming reuses `built--<segments>--<filename>`
62
+
63
+ Same naming scheme as snippets/sections/blocks, just landing in `assets/`
64
+ instead of `sections/` / `snippets/` / `blocks/`. The final segment is the
65
+ asset's own filename (including extension), not the source component's directory
66
+ name.
67
+
68
+ - **Why:** consistency. The cleanup sweep, the prefix discipline, and the
69
+ layering keying all already work this way. No new naming convention to
70
+ remember.
71
+ - **Cost:** asset names are verbose (`built--components--icon--cart.png`), which
72
+ is exactly the ergonomics gap the alias rewriter below closes.
73
+
74
+ ### 3. Cleanup extends to `assets/`
75
+
76
+ The build's existing clean step sweeps `/^_?built--/` from `sections/`,
77
+ `snippets/`, and `blocks/`. Add `assets/` to that list — anything starting with
78
+ `built--` in the consumer's `assets/` directory gets cleaned and rewritten on
79
+ every build. Hand-written files without the prefix survive.
80
+
81
+ - **Why:** same additive-build guarantee that everywhere else honors. Removed
82
+ source assets disappear cleanly; hand-authored `assets/background.jpg` is
83
+ untouched.
84
+ - **Cost:** none meaningful. `assets/` is currently a do-not-touch zone; this
85
+ scopes the touch to the reserved prefix.
86
+
87
+ ### 4. Layering applies to assets
88
+
89
+ If both consumer and baseline have `src/components/icon/assets/cart.png`,
90
+ first-wins like every other slot. The consumer's PNG wins; the baseline's is
91
+ ignored.
92
+
93
+ - **Why:** consistent with how every other file resolves through the layer list.
94
+ A consumer can override a single icon by dropping a same-named PNG into their
95
+ tree.
96
+ - **Cost:** none. The resolver gets an extra slot to track, but the rule is the
97
+ rule.
98
+
99
+ ### 5. Single-pass alias rewriter (broader than today)
100
+
101
+ The existing rewriter only matches `'@/...'` and `'./...'` strings inside
102
+ `{% render %}` / `{% include %}` tags. To make asset references ergonomic
103
+ (`{{ '@/components/icon/assets/cart.png' | asset_url }}` instead of the mangled
104
+ `'built--components--icon--cart.png'`), the rewriter pass extends to **any
105
+ string literal containing an `@/` or `./` alias.**
106
+
107
+ The rule is: in every `.liquid` source file, find every `'@/...'` or `'./...'`
108
+ string literal, resolve it against the lookup map, replace it with the built
109
+ name. If a path doesn't resolve, error with file + line.
110
+
111
+ - **Why:** the realistic rate of false positives approaches zero. A `'@/...'`
112
+ string in a Liquid source file is almost certainly a Smelt alias — meaningless
113
+ to Shopify on its own. Considered and rejected: a separate `@asset/` alias
114
+ (splits the mental model), filter-context detection (allowlist of `asset_url`
115
+ / `asset_img_url` / etc. — narrow on purpose but more fragile than the simpler
116
+ design), and a catch-all validator (unnecessary if the rewriter is broad).
117
+ - **Cost:** a typo today silently passes through to runtime and Shopify fails
118
+ confusingly; with the broader rewriter it becomes a clean build-time error
119
+ pointing at the line. That's a behavior change — but an improvement, not a
120
+ regression. The other cosmetic case (a comment that happens to mention
121
+ `'@/components/button'` gets rewritten) is dead text in the output and doesn't
122
+ break anything.
123
+
124
+ ## Out of Scope
125
+
126
+ Things this plan deliberately does not address:
127
+
128
+ - **Transforms.** No image resizing, format conversion, or quality knobs. Just
129
+ copy. If we want a pipeline like Vite's image plugins, that's its own design
130
+ discussion.
131
+ - **Cache-busting / content hashing.** Shopify's CDN handles its own cache key
132
+ strategy from the asset URL. We don't append `?v=hash` or rename to
133
+ `built--…-abc123.png`.
134
+ - **Schema / metadata for assets.** No `<name>.schema.ts` equivalent — assets
135
+ are dumb files, not authoring artifacts that need types.
136
+ - **Asset-only "components."** A directory containing only `assets/` and no
137
+ `.liquid` is not a component. The build still requires a `.liquid` anchor to
138
+ register a component; assets are slots of an existing component.
139
+
140
+ ## Implementation Outline
141
+
142
+ A concrete-but-not-final list of where changes land:
143
+
144
+ 1. **`lib/resolver.ts`** — recognize each component's `assets/` subdir as a new
145
+ slot. Likely `assets?: ComponentSource[]` on `ResolvedComponent` (plural —
146
+ components can have many assets). Walk happens at the same time as the
147
+ other-slots resolution.
148
+ 2. **`lib/build/build.ts`**
149
+ - **`cleanBuiltOutputs`** — add `assets` to `TYPE_TO_OUTPUT_DIRECTORY`'s
150
+ sweep targets (or maintain a separate cleanup-roots list — see Open
151
+ Questions).
152
+ - **`buildComponent`** — for each asset in the component, copy from
153
+ `component.assets[i].path` to
154
+ `assets/built--<component.segments.join("--")>--<filename>`. Stream the
155
+ file rather than read-into-memory if it gets large.
156
+ - **`rewriteRenders`** — generalize to walk any `'@/...'` / `'./...'` string
157
+ in the source. Rename to `rewriteLiquidAliases` (or similar). The
158
+ render-tag-specific regex becomes a broader one. Existing render rewrites
159
+ continue to work because they're a subset.
160
+ - **`lookup`** — needs to contain asset entries too, so the rewriter can
161
+ resolve `'@/components/icon/assets/cart.png'` to its built name. Key shape
162
+ needs thought — assets are not just segments-joined; they have filenames
163
+ with extensions.
164
+ 3. **`docs/build-spec.md`**
165
+ - Remove `assets/` from the "build does not read or write" list.
166
+ - Document the `assets/` subdir convention.
167
+ - Update Output Naming section with the asset case.
168
+ - Update the alias-rewriter description to reflect the broader scope.
169
+ 4. **`example/`** — add a small demonstration. An icon component with one or two
170
+ PNGs is the canonical use case. Verify it builds, theme-check passes, and the
171
+ asset_url renders correctly via assay browser tests.
172
+ 5. **Tests** — extend `lib/build/build.test.ts` with:
173
+ - Asset emission to `assets/built--...`
174
+ - Cleanup of removed assets
175
+ - Layering (consumer shadows baseline)
176
+ - Alias rewrite in non-render contexts (`{{ '@/...' | asset_url }}`)
177
+ - Unresolved alias → build error
178
+
179
+ ## Open Questions
180
+
181
+ - **Lookup map keying.** Today the lookup is
182
+ `segments.join("/") → ResolvedComponent`. For assets, we'd want
183
+ `<component-segments>/assets/<filename>` to resolve directly. Either add asset
184
+ entries to the same map with a different key shape, or maintain a parallel
185
+ map. Probably the former, but worth confirming during implementation.
186
+ - **Sub-paths under `assets/`.** Should `assets/icons/cart.png` work, with the
187
+ built name being `built--components--icon--icons--cart.png`? Or do we require
188
+ a flat `assets/` directory? Flat is simpler; nested mirrors the existing
189
+ component-tree freedom. Probably allow nesting; cost is small.
190
+ - **Other-extension overlap.** A consumer with a `cart.png` and a `cart.svg` in
191
+ the same `assets/` directory — both copy fine, no conflict. But if someone
192
+ puts a `.liquid` in `assets/`, what happens? Defining "assets are not liquid"
193
+ by convention is cleanest; the build could ignore `.liquid` in `assets/` or
194
+ error. Defer.
195
+ - **`utilities/assets/`?** Could a utility module have its own assets? The spec
196
+ already says utilities have no Liquid output. Extending assets to utilities
197
+ would be possible but probably not worth the complexity for the first cut.