@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.
- package/.claude/settings.json +11 -0
- package/.github/workflows/verify.yml +64 -0
- package/.gitmodules +3 -0
- package/.prettierignore +25 -0
- package/.prettierrc.cjs +9 -0
- package/.zed/settings.json +21 -0
- package/AGENTS.md +232 -0
- package/LICENSE +21 -0
- package/README.md +266 -0
- package/biome.json +58 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +350 -0
- package/dist/schema.d.mts +265 -0
- package/dist/schema.mjs +21 -0
- package/docs/TESTING.md +293 -0
- package/docs/assets-plan.md +197 -0
- package/docs/build-spec.md +466 -0
- package/docs/library-conversion-plan.md +419 -0
- package/example/.gitattributes +7 -0
- package/example/.shopifyignore +28 -0
- package/example/.theme-check.yml +7 -0
- package/example/blocks/_built--sections--hero--blocks--feature.liquid +52 -0
- package/example/config/settings_schema.json +10 -0
- package/example/layout/theme.liquid +25 -0
- package/example/locales/en.default.json +1 -0
- package/example/package-lock.json +51 -0
- package/example/package.json +20 -0
- package/example/sections/built--sections--hero.liquid +83 -0
- package/example/snippets/built--components--button.liquid +38 -0
- package/example/snippets/built--components--card.liquid +33 -0
- package/example/src/components/button/button.css +13 -0
- package/example/src/components/card/card.css +16 -0
- package/example/src/components/card/card.liquid +9 -0
- package/example/src/sections/hero/blocks/feature/feature.css +11 -0
- package/example/src/sections/hero/blocks/feature/feature.liquid +9 -0
- package/example/src/sections/hero/blocks/feature/feature.schema.ts +14 -0
- package/example/src/sections/hero/hero.css +15 -0
- package/example/src/sections/hero/hero.liquid +16 -0
- package/example/src/sections/hero/hero.schema.ts +26 -0
- package/example/src/sections/hero/hero.test.ts +43 -0
- package/example/src/utilities/labels.ts +5 -0
- package/example/templates/index.liquid +1 -0
- package/example/tsconfig.json +10 -0
- package/example/vitest.config.ts +6 -0
- package/lib/build/build.test.ts +475 -0
- package/lib/build/build.ts +314 -0
- package/lib/build/command.ts +27 -0
- package/lib/build/index.ts +1 -0
- package/lib/cli.ts +17 -0
- package/lib/dev/command.ts +25 -0
- package/lib/dev/index.ts +1 -0
- package/lib/dev/watch.ts +52 -0
- package/lib/resolver.test.ts +275 -0
- package/lib/resolver.ts +156 -0
- package/lib/schema.ts +37 -0
- package/package.json +59 -0
- package/scripts/codegen-schema.ts +66 -0
- package/src/components/button/button.css +13 -0
- package/src/components/button/button.liquid +5 -0
- package/src/components/button/button.ts +5 -0
- package/src/tsconfig.json +10 -0
- package/tests/example.test.ts +101 -0
- package/tsconfig.json +20 -0
- package/tsdown.config.ts +14 -0
- package/vendor/theme-liquid-docs/.gitattributes +10 -0
- package/vendor/theme-liquid-docs/.github/CODEOWNERS +1 -0
- package/vendor/theme-liquid-docs/.github/CODE_OF_CONDUCT.md +73 -0
- package/vendor/theme-liquid-docs/.github/ISSUE_TEMPLATE/bug_report.md +17 -0
- package/vendor/theme-liquid-docs/.github/ISSUE_TEMPLATE/feature_request.md +17 -0
- package/vendor/theme-liquid-docs/.github/dependabot.yaml +6 -0
- package/vendor/theme-liquid-docs/.github/workflows/ci.yml +33 -0
- package/vendor/theme-liquid-docs/.github/workflows/cla.yml +27 -0
- package/vendor/theme-liquid-docs/.github/workflows/shopify-dev-preview-automation.yml +86 -0
- package/vendor/theme-liquid-docs/.github/workflows/update-latest.yml +56 -0
- package/vendor/theme-liquid-docs/.prettierrc.json +16 -0
- package/vendor/theme-liquid-docs/.vscode/settings.json +28 -0
- package/vendor/theme-liquid-docs/LICENSE.md +7 -0
- package/vendor/theme-liquid-docs/README.md +48 -0
- package/vendor/theme-liquid-docs/ai/claude/CLAUDE.md +1485 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/assets.mdc +15 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/blocks.mdc +339 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/examples/block-example-group.mdc +103 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/examples/block-example-text.mdc +59 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/examples/section-example.mdc +61 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/examples/snippet-example.mdc +72 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/liquid.mdc +837 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/locales.mdc +100 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/localization.mdc +67 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/mcp.mdc +2 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/schemas.mdc +184 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/sections.mdc +84 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/settings-schema.mdc +51 -0
- package/vendor/theme-liquid-docs/ai/cursor/rules/snippets.mdc +119 -0
- package/vendor/theme-liquid-docs/ai/github/copilot-instructions.md +1485 -0
- package/vendor/theme-liquid-docs/ai/liquid.mdc +638 -0
- package/vendor/theme-liquid-docs/data/filters.json +6148 -0
- package/vendor/theme-liquid-docs/data/latest.json +2 -0
- package/vendor/theme-liquid-docs/data/objects.json +20594 -0
- package/vendor/theme-liquid-docs/data/shopify_system_translations.json +2586 -0
- package/vendor/theme-liquid-docs/data/tags.json +1276 -0
- package/vendor/theme-liquid-docs/package.json +20 -0
- package/vendor/theme-liquid-docs/schemas/manifest_schema.json +31 -0
- package/vendor/theme-liquid-docs/schemas/manifest_theme.json +19 -0
- package/vendor/theme-liquid-docs/schemas/manifest_theme_app_extension.json +10 -0
- package/vendor/theme-liquid-docs/schemas/theme/app_block_entry.json +13 -0
- package/vendor/theme-liquid-docs/schemas/theme/default_setting_values.json +24 -0
- package/vendor/theme-liquid-docs/schemas/theme/local_block_entry.json +25 -0
- package/vendor/theme-liquid-docs/schemas/theme/preset.json +72 -0
- package/vendor/theme-liquid-docs/schemas/theme/preset_blocks.json +91 -0
- package/vendor/theme-liquid-docs/schemas/theme/section.json +208 -0
- package/vendor/theme-liquid-docs/schemas/theme/setting.json +1413 -0
- package/vendor/theme-liquid-docs/schemas/theme/settings.json +10 -0
- package/vendor/theme-liquid-docs/schemas/theme/targetted_block_entry.json +15 -0
- package/vendor/theme-liquid-docs/schemas/theme/theme_block.json +91 -0
- package/vendor/theme-liquid-docs/schemas/theme/theme_block_entry.json +14 -0
- package/vendor/theme-liquid-docs/schemas/theme/theme_settings.json +83 -0
- package/vendor/theme-liquid-docs/schemas/theme/translations.json +63 -0
- package/vendor/theme-liquid-docs/schemas/update/update_extension_schema_v1.json +186 -0
- package/vendor/theme-liquid-docs/tests/fixtures/section-nested-blocks.json +18 -0
- package/vendor/theme-liquid-docs/tests/fixtures/section-schema-1.json +90 -0
- package/vendor/theme-liquid-docs/tests/fixtures/section-schema-2.json +201 -0
- package/vendor/theme-liquid-docs/tests/fixtures/section-schema-3.json +29 -0
- package/vendor/theme-liquid-docs/tests/fixtures/section-schema-4.json +315 -0
- package/vendor/theme-liquid-docs/tests/fixtures/section-schema-5.json +114 -0
- package/vendor/theme-liquid-docs/tests/fixtures/section-schema-6.json +63 -0
- package/vendor/theme-liquid-docs/tests/fixtures/section-schema-conditional-settings.json +145 -0
- package/vendor/theme-liquid-docs/tests/fixtures/section-schema-preset-blocks-as-hash.json +60 -0
- package/vendor/theme-liquid-docs/tests/fixtures/section-schema-static-block-preset.json +76 -0
- package/vendor/theme-liquid-docs/tests/fixtures/section-settings.json +34 -0
- package/vendor/theme-liquid-docs/tests/fixtures/theme-block-1.json +234 -0
- package/vendor/theme-liquid-docs/tests/fixtures/theme-block-2.json +253 -0
- package/vendor/theme-liquid-docs/tests/fixtures/theme-block-basics.json +48 -0
- package/vendor/theme-liquid-docs/tests/fixtures/theme-block-conditional-settings.json +202 -0
- package/vendor/theme-liquid-docs/tests/fixtures/theme-block-presets-as-hash.json +50 -0
- package/vendor/theme-liquid-docs/tests/fixtures/theme-block-settings.json +34 -0
- package/vendor/theme-liquid-docs/tests/fixtures/theme-settings-all-settings.json +313 -0
- package/vendor/theme-liquid-docs/tests/fixtures/theme-settings-dawn.json +1469 -0
- package/vendor/theme-liquid-docs/tests/fixtures/theme-settings-metadata.json +10 -0
- package/vendor/theme-liquid-docs/tests/fixtures/translations-1.json +14 -0
- package/vendor/theme-liquid-docs/tests/section.spec.ts +367 -0
- package/vendor/theme-liquid-docs/tests/test-constants.ts +58 -0
- package/vendor/theme-liquid-docs/tests/test-helpers.ts +104 -0
- package/vendor/theme-liquid-docs/tests/theme-settings/color_palette.spec.ts +184 -0
- package/vendor/theme-liquid-docs/tests/theme-settings/color_scheme_group.spec.ts +143 -0
- package/vendor/theme-liquid-docs/tests/theme-settings/general.spec.ts +192 -0
- package/vendor/theme-liquid-docs/tests/theme-settings/metaobject.spec.ts +94 -0
- package/vendor/theme-liquid-docs/tests/theme-settings/resource_list.spec.ts +58 -0
- package/vendor/theme-liquid-docs/tests/theme-settings/theme-metadata.spec.ts +59 -0
- package/vendor/theme-liquid-docs/tests/theme_block.spec.ts +266 -0
- package/vendor/theme-liquid-docs/tests/translations_schema.spec.ts +31 -0
- package/vendor/theme-liquid-docs/yarn.lock +543 -0
- package/vitest.config.ts +7 -0
package/docs/TESTING.md
ADDED
|
@@ -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.
|