@augeo/smelt 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/verify.yml +23 -10
- package/AGENTS.md +7 -3
- package/README.md +123 -23
- package/dist/cli.mjs +84 -39
- package/dist/schema.d.mts +8 -6
- package/dist/schema.mjs +6 -4
- package/docs/build-spec.md +105 -7
- package/docs/js-modules-plan.md +278 -0
- package/example/blocks/built--components--card.liquid +34 -0
- package/example/snippets/built--components--card.liquid +1 -1
- package/example/src/components/card/block/card.schema.ts +14 -0
- package/example/src/components/card/card.liquid +1 -1
- package/lib/build/build.test.ts +207 -0
- package/lib/build/build.ts +92 -18
- package/lib/resolver.test.ts +91 -4
- package/lib/resolver.ts +121 -36
- package/lib/schema.ts +8 -10
- package/package.json +1 -1
- package/docs/library-conversion-plan.md +0 -419
package/lib/resolver.ts
CHANGED
|
@@ -22,48 +22,85 @@ export interface ComponentSource {
|
|
|
22
22
|
export interface ResolvedComponent {
|
|
23
23
|
/** Component kind — determines the output directory. */
|
|
24
24
|
type: ComponentType;
|
|
25
|
-
/** Path segments under `src
|
|
25
|
+
/** Path segments under `src/` for naming, e.g. `["components", "button"]`. */
|
|
26
26
|
segments: string[];
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The anchor liquid. Present for every authored component and for an
|
|
29
|
+
* override block face (`block/<name>.liquid`). A *mechanical* block face —
|
|
30
|
+
* a `block/` dir with only a `<name>.schema.ts` — has no liquid; the engine
|
|
31
|
+
* synthesizes its body from the schema's settings.
|
|
32
|
+
*/
|
|
33
|
+
liquid?: ComponentSource;
|
|
29
34
|
/** Optional sibling slots; each may come from a different layer. */
|
|
30
35
|
ts?: ComponentSource;
|
|
31
36
|
css?: ComponentSource;
|
|
32
37
|
test?: ComponentSource;
|
|
33
|
-
/** Section schema authored in TS; executed at build time. */
|
|
38
|
+
/** Section/block schema authored in TS; executed at build time. */
|
|
34
39
|
schema?: ComponentSource;
|
|
40
|
+
/**
|
|
41
|
+
* True when this component is the block face of another component — emitted
|
|
42
|
+
* from a singular `block/` directory. Its `segments` are the parent's (the
|
|
43
|
+
* `block` marker is zero-segment), but its `type` is `"blocks"`.
|
|
44
|
+
*/
|
|
45
|
+
face?: boolean;
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
const TYPE_NAMES = ["sections", "blocks", "components"] as const;
|
|
38
49
|
export type ComponentType = (typeof TYPE_NAMES)[number];
|
|
39
50
|
|
|
51
|
+
// A singular `block/` directory inside a `src/components/*` snippet declares
|
|
52
|
+
// that snippet's block face (see docs/build-spec.md "Block faces"). It is a
|
|
53
|
+
// *zero-segment* marker: it retargets the type to "blocks" and reroots file
|
|
54
|
+
// lookup inside `block/`, but contributes nothing to the naming segments.
|
|
55
|
+
// Distinct from the plural `blocks/` type marker, which holds private child
|
|
56
|
+
// blocks. Faces are components-only — see the guard in walkType.
|
|
57
|
+
const FACE_MARKER = "block";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Internal walk result. `dir` is the real path under `src/` (it includes face
|
|
61
|
+
* markers); `segments` is the naming key (it does not). For everything except
|
|
62
|
+
* faces and their descendants the two are identical.
|
|
63
|
+
*/
|
|
64
|
+
interface Descriptor {
|
|
65
|
+
type: ComponentType;
|
|
66
|
+
segments: string[];
|
|
67
|
+
dir: string[];
|
|
68
|
+
face: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
40
71
|
export async function resolveLayers(
|
|
41
72
|
layers: Layer[],
|
|
42
73
|
): Promise<ResolvedComponent[]> {
|
|
43
74
|
const perLayerComponents = await Promise.all(layers.map(walkLayer));
|
|
44
|
-
const allKeys = new Map<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
>();
|
|
48
|
-
perLayerComponents.flat().forEach(({ type, segments }) => {
|
|
49
|
-
const key = `${type}:${segments.join("/")}`;
|
|
75
|
+
const allKeys = new Map<string, Descriptor>();
|
|
76
|
+
perLayerComponents.flat().forEach((descriptor) => {
|
|
77
|
+
const key = `${descriptor.type}:${descriptor.segments.join("/")}`;
|
|
50
78
|
if (!allKeys.has(key)) {
|
|
51
|
-
allKeys.set(key,
|
|
79
|
+
allKeys.set(key, descriptor);
|
|
52
80
|
}
|
|
53
81
|
});
|
|
54
82
|
|
|
55
83
|
const resolved = await Promise.all(
|
|
56
84
|
Array.from(allKeys.values()).map(
|
|
57
|
-
async (
|
|
85
|
+
async (descriptor): Promise<ResolvedComponent | undefined> => {
|
|
86
|
+
const anchor = descriptor.segments[descriptor.segments.length - 1];
|
|
58
87
|
const [liquid, ts, css, test, schema] = await Promise.all([
|
|
59
|
-
firstExisting(layers,
|
|
60
|
-
firstExisting(layers,
|
|
61
|
-
firstExisting(layers,
|
|
62
|
-
firstExisting(layers,
|
|
63
|
-
firstExisting(layers,
|
|
88
|
+
firstExisting(layers, descriptor.dir, anchor, ".liquid"),
|
|
89
|
+
firstExisting(layers, descriptor.dir, anchor, ".ts"),
|
|
90
|
+
firstExisting(layers, descriptor.dir, anchor, ".css"),
|
|
91
|
+
firstExisting(layers, descriptor.dir, anchor, ".test.ts"),
|
|
92
|
+
firstExisting(layers, descriptor.dir, anchor, ".schema.ts"),
|
|
64
93
|
]);
|
|
65
|
-
|
|
66
|
-
|
|
94
|
+
// A normal component needs a liquid anchor. A block face may instead
|
|
95
|
+
// be anchored on its schema alone (the mechanical case) — the engine
|
|
96
|
+
// generates the wrapper body from it.
|
|
97
|
+
if (!liquid && !(descriptor.face && schema)) return undefined;
|
|
98
|
+
const component: ResolvedComponent = {
|
|
99
|
+
type: descriptor.type,
|
|
100
|
+
segments: descriptor.segments,
|
|
101
|
+
};
|
|
102
|
+
if (descriptor.face) component.face = true;
|
|
103
|
+
if (liquid) component.liquid = liquid;
|
|
67
104
|
if (ts) component.ts = ts;
|
|
68
105
|
if (css) component.css = css;
|
|
69
106
|
if (test) component.test = test;
|
|
@@ -78,16 +115,14 @@ export async function resolveLayers(
|
|
|
78
115
|
);
|
|
79
116
|
}
|
|
80
117
|
|
|
81
|
-
async function walkLayer(
|
|
82
|
-
layer: Layer,
|
|
83
|
-
): Promise<Array<{ type: ComponentType; segments: string[] }>> {
|
|
118
|
+
async function walkLayer(layer: Layer): Promise<Descriptor[]> {
|
|
84
119
|
const sourceRoot = join(layer.path, "src");
|
|
85
120
|
if (!(await exists(sourceRoot))) return [];
|
|
86
121
|
const perType = await Promise.all(
|
|
87
122
|
TYPE_NAMES.map(async (type) => {
|
|
88
123
|
const typeDir = join(sourceRoot, type);
|
|
89
124
|
if (!(await exists(typeDir))) return [];
|
|
90
|
-
return walkType(typeDir, type, [type]);
|
|
125
|
+
return walkType(typeDir, type, [type], [type]);
|
|
91
126
|
}),
|
|
92
127
|
);
|
|
93
128
|
return perType.flat();
|
|
@@ -97,30 +132,80 @@ async function walkType(
|
|
|
97
132
|
directory: string,
|
|
98
133
|
type: ComponentType,
|
|
99
134
|
segments: string[],
|
|
100
|
-
|
|
135
|
+
dir: string[],
|
|
136
|
+
): Promise<Descriptor[]> {
|
|
101
137
|
const entries = await readdir(directory, { withFileTypes: true });
|
|
102
138
|
const directories = entries.filter((entry) => entry.isDirectory());
|
|
139
|
+
// We're "inside a component" (rather than at a type root) when the last
|
|
140
|
+
// segment names a component rather than a type. Only there is `block/` a
|
|
141
|
+
// face marker — at a type root it's just a component literally named "block".
|
|
142
|
+
const inComponentContext = !(TYPE_NAMES as readonly string[]).includes(
|
|
143
|
+
segments[segments.length - 1] ?? "",
|
|
144
|
+
);
|
|
103
145
|
|
|
104
146
|
const nested = await Promise.all(
|
|
105
147
|
directories.map(async (entry) => {
|
|
106
148
|
const subdirectory = join(directory, entry.name);
|
|
107
149
|
|
|
108
|
-
// Nested type marker (e.g. `components/` under a section
|
|
109
|
-
// and namespace.
|
|
150
|
+
// Nested type marker (e.g. `components/` under a section, or the plural
|
|
151
|
+
// `blocks/` holding private children) shifts type and namespace.
|
|
110
152
|
if ((TYPE_NAMES as readonly string[]).includes(entry.name)) {
|
|
111
|
-
return walkType(
|
|
112
|
-
|
|
113
|
-
entry.name,
|
|
114
|
-
|
|
153
|
+
return walkType(
|
|
154
|
+
subdirectory,
|
|
155
|
+
entry.name as ComponentType,
|
|
156
|
+
[...segments, entry.name],
|
|
157
|
+
[...dir, entry.name],
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Singular `block/` face marker: zero-segment. Retarget type to
|
|
162
|
+
// "blocks" and reroot file lookup inside `block/`, but leave the naming
|
|
163
|
+
// segments alone. Empty `block/` dirs resolve to nothing and are dropped.
|
|
164
|
+
// Only snippets (`type === "components"`) get a face — a section or
|
|
165
|
+
// block sprouting one would share its parent's `type:segments` key and
|
|
166
|
+
// collide. (At a type root `inComponentContext` is false, so this can't
|
|
167
|
+
// fire on a top-level component literally named "block".)
|
|
168
|
+
if (
|
|
169
|
+
entry.name === FACE_MARKER &&
|
|
170
|
+
inComponentContext &&
|
|
171
|
+
type === "components"
|
|
172
|
+
) {
|
|
173
|
+
const face: Descriptor = {
|
|
174
|
+
type: "blocks",
|
|
175
|
+
segments: [...segments],
|
|
176
|
+
dir: [...dir, entry.name],
|
|
177
|
+
face: true,
|
|
178
|
+
};
|
|
179
|
+
const deeper = await walkType(
|
|
180
|
+
subdirectory,
|
|
181
|
+
type,
|
|
182
|
+
[...segments],
|
|
183
|
+
[...dir, entry.name],
|
|
184
|
+
);
|
|
185
|
+
return [face, ...deeper];
|
|
115
186
|
}
|
|
116
187
|
|
|
188
|
+
// Nested component.
|
|
117
189
|
const componentSegments = [...segments, entry.name];
|
|
190
|
+
const componentDir = [...dir, entry.name];
|
|
118
191
|
const liquidPath = join(subdirectory, `${entry.name}.liquid`);
|
|
119
|
-
const here = (await exists(liquidPath))
|
|
120
|
-
? [
|
|
192
|
+
const here: Descriptor[] = (await exists(liquidPath))
|
|
193
|
+
? [
|
|
194
|
+
{
|
|
195
|
+
type,
|
|
196
|
+
segments: componentSegments,
|
|
197
|
+
dir: componentDir,
|
|
198
|
+
face: false,
|
|
199
|
+
},
|
|
200
|
+
]
|
|
121
201
|
: [];
|
|
122
202
|
|
|
123
|
-
const deeper = await walkType(
|
|
203
|
+
const deeper = await walkType(
|
|
204
|
+
subdirectory,
|
|
205
|
+
type,
|
|
206
|
+
componentSegments,
|
|
207
|
+
componentDir,
|
|
208
|
+
);
|
|
124
209
|
return [...here, ...deeper];
|
|
125
210
|
}),
|
|
126
211
|
);
|
|
@@ -130,11 +215,11 @@ async function walkType(
|
|
|
130
215
|
|
|
131
216
|
async function firstExisting(
|
|
132
217
|
layers: Layer[],
|
|
133
|
-
|
|
218
|
+
dir: string[],
|
|
219
|
+
anchor: string,
|
|
134
220
|
extension: string,
|
|
135
221
|
): Promise<ComponentSource | undefined> {
|
|
136
|
-
const
|
|
137
|
-
const relativePath = join("src", ...segments, `${last}${extension}`);
|
|
222
|
+
const relativePath = join("src", ...dir, `${anchor}${extension}`);
|
|
138
223
|
const candidates = await Promise.all(
|
|
139
224
|
layers.map(async (layer) => ({
|
|
140
225
|
layer,
|
package/lib/schema.ts
CHANGED
|
@@ -13,25 +13,23 @@ import type { ThemeBlockSchema } from "./schema-block.generated.ts";
|
|
|
13
13
|
import type { SectionSchema } from "./schema-section.generated.ts";
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Identity helper for authoring a Shopify section schema.
|
|
17
|
-
*
|
|
18
|
-
* for
|
|
16
|
+
* Identity helper for authoring a Shopify section schema. Typing the parameter
|
|
17
|
+
* as `SectionSchema` directly (rather than a generic `<const T>`) gives the
|
|
18
|
+
* author both autocomplete for `type` values, `id`s, etc. and excess-property
|
|
19
|
+
* checking — a bare type parameter would silently accept unknown keys.
|
|
19
20
|
*
|
|
20
21
|
* Use in `<name>.schema.ts` files under `src/sections/`.
|
|
21
22
|
*/
|
|
22
|
-
export function defineSchemaSection
|
|
23
|
-
schema: T,
|
|
24
|
-
): T {
|
|
23
|
+
export function defineSchemaSection(schema: SectionSchema): SectionSchema {
|
|
25
24
|
return schema;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
|
-
* Identity helper for authoring a Shopify theme-block schema.
|
|
28
|
+
* Identity helper for authoring a Shopify theme-block schema. See
|
|
29
|
+
* {@link defineSchemaSection} for why the parameter is typed directly.
|
|
30
30
|
*
|
|
31
31
|
* Use in `<name>.schema.ts` files under `src/blocks/`.
|
|
32
32
|
*/
|
|
33
|
-
export function defineSchemaBlock
|
|
34
|
-
schema: T,
|
|
35
|
-
): T {
|
|
33
|
+
export function defineSchemaBlock(schema: ThemeBlockSchema): ThemeBlockSchema {
|
|
36
34
|
return schema;
|
|
37
35
|
}
|