@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/build/build.test.ts
CHANGED
|
@@ -436,6 +436,188 @@ describe("build()", () => {
|
|
|
436
436
|
);
|
|
437
437
|
});
|
|
438
438
|
});
|
|
439
|
+
|
|
440
|
+
describe("when a component has a mechanical block face", () => {
|
|
441
|
+
beforeEach(async () => {
|
|
442
|
+
await writeComponent(baseline, "components/image", {
|
|
443
|
+
liquid: "<img>",
|
|
444
|
+
css: ".img {}",
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
beforeEach(async () => {
|
|
448
|
+
await writeFace(baseline, "components/image", {
|
|
449
|
+
schema:
|
|
450
|
+
"export const schema = { name: 'Image', settings: [{ type: 'image_picker', id: 'src', label: 'Image' }, { type: 'checkbox', id: 'rounded', label: 'Rounded' }] };",
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
beforeEach(async () => {
|
|
454
|
+
await build({ layers: [consumer.layer, baseline.layer] });
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
let face: string;
|
|
458
|
+
beforeEach(async () => {
|
|
459
|
+
face = await readFile(
|
|
460
|
+
join(consumer.path, "blocks/built--components--image.liquid"),
|
|
461
|
+
"utf-8",
|
|
462
|
+
);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("emits the face into blocks/ named after the component", () => {
|
|
466
|
+
expect(face).toBeTruthy();
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("still emits the underlying snippet", async () => {
|
|
470
|
+
const snippet = await readFile(
|
|
471
|
+
join(consumer.path, "snippets/built--components--image.liquid"),
|
|
472
|
+
"utf-8",
|
|
473
|
+
);
|
|
474
|
+
expect(snippet).toContain("<img>");
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("renders the underlying component by its built name", () => {
|
|
478
|
+
expect(face).toContain("{% render 'built--components--image',");
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("maps each setting id to a render arg of the same name", () => {
|
|
482
|
+
expect(face).toContain("src: block.settings.src,");
|
|
483
|
+
expect(face).toContain("rounded: block.settings.rounded,");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it("passes shopify_attributes through", () => {
|
|
487
|
+
expect(face).toContain("shopify_attributes: block.shopify_attributes");
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("injects the face schema", () => {
|
|
491
|
+
expect(face).toContain("{% schema %}");
|
|
492
|
+
expect(face).toContain('"name": "Image"');
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe("when a mechanical face has no underlying snippet", () => {
|
|
497
|
+
beforeEach(async () => {
|
|
498
|
+
await writeFace(baseline, "components/orphan", {
|
|
499
|
+
schema: "export const schema = { name: 'Orphan' };",
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it("fails with a face-specific error, not a generic render miss", async () => {
|
|
504
|
+
await expect(
|
|
505
|
+
build({ layers: [consumer.layer, baseline.layer] }),
|
|
506
|
+
).rejects.toThrow(/no underlying snippet/);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
describe("when a component has an override block face", () => {
|
|
511
|
+
beforeEach(async () => {
|
|
512
|
+
await writeComponent(baseline, "components/stack", {
|
|
513
|
+
liquid: "<div>stack</div>",
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
beforeEach(async () => {
|
|
517
|
+
await writeFace(baseline, "components/stack", {
|
|
518
|
+
liquid:
|
|
519
|
+
"{% render '@/components/stack', children: block.blocks, shopify_attributes: block.shopify_attributes %}",
|
|
520
|
+
schema: "export const schema = { name: 'Stack' };",
|
|
521
|
+
css: ".stack-block {}",
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
beforeEach(async () => {
|
|
525
|
+
await build({ layers: [consumer.layer, baseline.layer] });
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
let face: string;
|
|
529
|
+
beforeEach(async () => {
|
|
530
|
+
face = await readFile(
|
|
531
|
+
join(consumer.path, "blocks/built--components--stack.liquid"),
|
|
532
|
+
"utf-8",
|
|
533
|
+
);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
it("uses the hand-written body verbatim (with renders rewritten)", () => {
|
|
537
|
+
expect(face).toContain("children: block.blocks");
|
|
538
|
+
expect(face).toContain("{% render 'built--components--stack',");
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("inlines the face's own css", () => {
|
|
542
|
+
expect(face).toContain(".stack-block {}");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it("injects the face schema", () => {
|
|
546
|
+
expect(face).toContain('"name": "Stack"');
|
|
547
|
+
});
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
describe("when a block face owns private child blocks", () => {
|
|
551
|
+
beforeEach(async () => {
|
|
552
|
+
await writeComponent(baseline, "components/tabs", {
|
|
553
|
+
liquid: "<div>tabs</div>",
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
beforeEach(async () => {
|
|
557
|
+
await writeFace(baseline, "components/tabs", {
|
|
558
|
+
liquid: "{% render '@/components/tabs', children: block.blocks %}",
|
|
559
|
+
schema: "export const schema = { name: 'Tabs' };",
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
beforeEach(async () => {
|
|
563
|
+
await writeComponent(baseline, "components/tabs/block/blocks/tab", {
|
|
564
|
+
liquid: "<div>tab</div>",
|
|
565
|
+
schema: "export const schema = { name: 'Tab' };",
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
beforeEach(async () => {
|
|
569
|
+
await build({ layers: [consumer.layer, baseline.layer] });
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("emits the child as a private block named through the face", async () => {
|
|
573
|
+
const child = await readFile(
|
|
574
|
+
join(
|
|
575
|
+
consumer.path,
|
|
576
|
+
"blocks/_built--components--tabs--blocks--tab.liquid",
|
|
577
|
+
),
|
|
578
|
+
"utf-8",
|
|
579
|
+
);
|
|
580
|
+
expect(child).toContain("<div>tab</div>");
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it("auto-merges the private child into the face's blocks array", async () => {
|
|
584
|
+
const face = await readFile(
|
|
585
|
+
join(consumer.path, "blocks/built--components--tabs.liquid"),
|
|
586
|
+
"utf-8",
|
|
587
|
+
);
|
|
588
|
+
expect(face).toContain('"type": "_built--components--tabs--blocks--tab"');
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
describe("when a `block/` dir sits under a real block (not a snippet)", () => {
|
|
593
|
+
beforeEach(async () => {
|
|
594
|
+
await writeComponent(baseline, "blocks/promo", {
|
|
595
|
+
liquid: "<aside>promo</aside>",
|
|
596
|
+
schema: "export const schema = { name: 'Promo' };",
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
beforeEach(async () => {
|
|
600
|
+
// A face marker here would collide with promo's own type:segments key.
|
|
601
|
+
// Faces are components-only, so this must be ignored, not silently drop
|
|
602
|
+
// the block.
|
|
603
|
+
await writeFace(baseline, "blocks/promo", {
|
|
604
|
+
schema: "export const schema = { name: 'ShouldBeIgnored' };",
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
beforeEach(async () => {
|
|
608
|
+
await build({ layers: [consumer.layer, baseline.layer] });
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("still emits the block, unshadowed by a spurious face", async () => {
|
|
612
|
+
const promo = await readFile(
|
|
613
|
+
join(consumer.path, "blocks/built--blocks--promo.liquid"),
|
|
614
|
+
"utf-8",
|
|
615
|
+
);
|
|
616
|
+
expect(promo).toContain("<aside>promo</aside>");
|
|
617
|
+
expect(promo).toContain('"name": "Promo"');
|
|
618
|
+
expect(promo).not.toContain("ShouldBeIgnored");
|
|
619
|
+
});
|
|
620
|
+
});
|
|
439
621
|
});
|
|
440
622
|
|
|
441
623
|
async function makeFixture(tempRoot: string, name: string): Promise<Fixture> {
|
|
@@ -473,3 +655,28 @@ async function writeComponent(
|
|
|
473
655
|
),
|
|
474
656
|
);
|
|
475
657
|
}
|
|
658
|
+
|
|
659
|
+
// Writes a component's block face: files named after the component but located
|
|
660
|
+
// in its `block/` subdir (e.g. src/components/image/block/image.schema.ts).
|
|
661
|
+
async function writeFace(
|
|
662
|
+
fixture: Fixture,
|
|
663
|
+
segments: string,
|
|
664
|
+
slots: { liquid?: string; ts?: string; css?: string; schema?: string },
|
|
665
|
+
): Promise<void> {
|
|
666
|
+
const parts = segments.split("/");
|
|
667
|
+
const last = parts[parts.length - 1];
|
|
668
|
+
const directory = join(fixture.path, "src", ...parts, "block");
|
|
669
|
+
await mkdir(directory, { recursive: true });
|
|
670
|
+
await Promise.all(
|
|
671
|
+
Object.entries({
|
|
672
|
+
[`${last}.liquid`]: slots.liquid,
|
|
673
|
+
[`${last}.ts`]: slots.ts,
|
|
674
|
+
[`${last}.css`]: slots.css,
|
|
675
|
+
[`${last}.schema.ts`]: slots.schema,
|
|
676
|
+
})
|
|
677
|
+
.filter(([, content]) => content !== undefined)
|
|
678
|
+
.map(([filename, content]) =>
|
|
679
|
+
writeFile(join(directory, filename), content ?? ""),
|
|
680
|
+
),
|
|
681
|
+
);
|
|
682
|
+
}
|
package/lib/build/build.ts
CHANGED
|
@@ -43,8 +43,15 @@ export async function build(context: BuildContext): Promise<number> {
|
|
|
43
43
|
const components = await resolveLayers(context.layers);
|
|
44
44
|
await cleanBuiltOutputs(outputRoot);
|
|
45
45
|
|
|
46
|
+
// Keyed by segments alone. A block face shares its snippet's segments (the
|
|
47
|
+
// `block/` marker is zero-segment), so faces are excluded here — they are
|
|
48
|
+
// never a render target (you render the underlying snippet) nor a private
|
|
49
|
+
// child, so neither consumer of this map needs them, and dropping them keeps
|
|
50
|
+
// the remaining keys unique.
|
|
46
51
|
const lookup = new Map(
|
|
47
|
-
components
|
|
52
|
+
components
|
|
53
|
+
.filter((component) => !component.face)
|
|
54
|
+
.map((component) => [component.segments.join("/"), component]),
|
|
48
55
|
);
|
|
49
56
|
await Promise.all(
|
|
50
57
|
components.map((component) =>
|
|
@@ -75,14 +82,40 @@ async function buildComponent(
|
|
|
75
82
|
lookup: Map<string, ResolvedComponent>,
|
|
76
83
|
outputRoot: string,
|
|
77
84
|
): Promise<void> {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
const schema = component.schema
|
|
86
|
+
? await loadSchema(component.schema)
|
|
87
|
+
: undefined;
|
|
81
88
|
|
|
82
|
-
|
|
89
|
+
let body: string;
|
|
90
|
+
if (component.liquid) {
|
|
91
|
+
const source = await readFile(component.liquid.path, "utf-8");
|
|
92
|
+
assertNoInlineSchema(source, component);
|
|
93
|
+
body = rewriteRenders(source, component, lookup);
|
|
94
|
+
} else {
|
|
95
|
+
// Mechanical block face: no hand-written body, so synthesize the wrapper
|
|
96
|
+
// render from the schema's settings (see docs/build-spec.md "Block faces").
|
|
97
|
+
// A mechanical face renders its underlying snippet — fail clearly here if
|
|
98
|
+
// that snippet is missing, rather than deferring to a generic
|
|
99
|
+
// "Cannot resolve render" from the alias rewrite.
|
|
100
|
+
const snippetKey = component.segments.join("/");
|
|
101
|
+
if (!lookup.has(snippetKey)) {
|
|
102
|
+
const name = component.segments[component.segments.length - 1];
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Block face '${snippetKey}' has no underlying snippet to render — ` +
|
|
105
|
+
`a mechanical face wraps its component. Add ${name}.liquid, or ` +
|
|
106
|
+
`author a block/${name}.liquid override.`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
body = rewriteRenders(
|
|
110
|
+
generateFaceBody(component, schema),
|
|
111
|
+
component,
|
|
112
|
+
lookup,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const sections = [banner(anchorSourceOf(component)), body.trimEnd()];
|
|
83
117
|
|
|
84
|
-
if (
|
|
85
|
-
const schema = await loadSchema(component.schema);
|
|
118
|
+
if (schema !== undefined) {
|
|
86
119
|
const merged = mergePrivateBlocks(schema, component, lookup);
|
|
87
120
|
sections.push(renderSchemaBlock(merged));
|
|
88
121
|
}
|
|
@@ -114,7 +147,7 @@ function assertNoInlineSchema(
|
|
|
114
147
|
if (!/\{%-?\s*schema\s*-?%\}/.test(source)) return;
|
|
115
148
|
const last = component.segments[component.segments.length - 1];
|
|
116
149
|
throw new Error(
|
|
117
|
-
`Inline {% schema %} block in ${
|
|
150
|
+
`Inline {% schema %} block in ${sourcePathFor(anchorSourceOf(component))}. ` +
|
|
118
151
|
`Schemas must live in a sibling ${last}.schema.ts file — import ` +
|
|
119
152
|
`{ defineSchemaSection } (or defineSchemaBlock) from "@augeo/smelt/schema".`,
|
|
120
153
|
);
|
|
@@ -207,19 +240,60 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
207
240
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
208
241
|
}
|
|
209
242
|
|
|
210
|
-
function banner(
|
|
243
|
+
function banner(source: ComponentSource): string {
|
|
211
244
|
return `{%- comment -%}
|
|
212
|
-
GENERATED FROM ${
|
|
245
|
+
GENERATED FROM ${sourcePathFor(source)} — do not edit this file directly.
|
|
213
246
|
Edit the source and run \`npm run build\`.
|
|
214
247
|
{%- endcomment -%}`;
|
|
215
248
|
}
|
|
216
249
|
|
|
217
|
-
function
|
|
218
|
-
const relativePath = relative(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
250
|
+
function sourcePathFor(source: ComponentSource): string {
|
|
251
|
+
const relativePath = relative(source.layer.path, source.path);
|
|
252
|
+
return `${source.layer.name}/${relativePath}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The slot a built file traces back to: the hand-written liquid when present,
|
|
257
|
+
* else the schema (a mechanical block face is anchored on its schema alone).
|
|
258
|
+
*/
|
|
259
|
+
function anchorSourceOf(component: ResolvedComponent): ComponentSource {
|
|
260
|
+
const anchor = component.liquid ?? component.schema;
|
|
261
|
+
if (!anchor) {
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Component '${component.segments.join("/")}' has neither a liquid nor a schema file.`,
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
return anchor;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Synthesize a block face's wrapper body: render the underlying component,
|
|
271
|
+
* mapping each schema setting to a render arg of the same name (`id == arg`)
|
|
272
|
+
* and passing `shopify_attributes` through. The `@/...` render is rewritten to
|
|
273
|
+
* the built name by the caller. For non-mechanical wrappers (derived props,
|
|
274
|
+
* `block.blocks`, …) author a `block/<name>.liquid` override instead.
|
|
275
|
+
*/
|
|
276
|
+
function generateFaceBody(
|
|
277
|
+
component: ResolvedComponent,
|
|
278
|
+
schema: unknown,
|
|
279
|
+
): string {
|
|
280
|
+
const settings =
|
|
281
|
+
isPlainObject(schema) && Array.isArray(schema.settings)
|
|
282
|
+
? schema.settings
|
|
283
|
+
: [];
|
|
284
|
+
const settingArgs = settings
|
|
285
|
+
.filter(
|
|
286
|
+
(setting): setting is { id: string } =>
|
|
287
|
+
isPlainObject(setting) && typeof setting.id === "string",
|
|
288
|
+
)
|
|
289
|
+
.map((setting) => `\t${setting.id}: block.settings.${setting.id},`);
|
|
290
|
+
const target = `@/${component.segments.join("/")}`;
|
|
291
|
+
return [
|
|
292
|
+
`{% render '${target}',`,
|
|
293
|
+
...settingArgs,
|
|
294
|
+
"\tshopify_attributes: block.shopify_attributes",
|
|
295
|
+
"%}",
|
|
296
|
+
].join("\n");
|
|
223
297
|
}
|
|
224
298
|
|
|
225
299
|
function wrapEmbedded(tag: "stylesheet" | "javascript", body: string): string {
|
|
@@ -244,7 +318,7 @@ function rewriteRenders(
|
|
|
244
318
|
const target = lookup.get(lookupKey);
|
|
245
319
|
if (!target) {
|
|
246
320
|
throw new Error(
|
|
247
|
-
`Cannot resolve render '${importPath}' (looked up '${lookupKey}') from ${component.
|
|
321
|
+
`Cannot resolve render '${importPath}' (looked up '${lookupKey}') from ${anchorSourceOf(component).path}`,
|
|
248
322
|
);
|
|
249
323
|
}
|
|
250
324
|
return `${prefix}${builtNameOf(target)}${suffix}`;
|
|
@@ -263,7 +337,7 @@ function resolveImport(
|
|
|
263
337
|
// `../` is intentionally not supported — see docs/build-spec.md.
|
|
264
338
|
if (importPath.includes("../")) {
|
|
265
339
|
throw new Error(
|
|
266
|
-
`'../' is not supported in render path: '${importPath}' (in ${component.
|
|
340
|
+
`'../' is not supported in render path: '${importPath}' (in ${anchorSourceOf(component).path})`,
|
|
267
341
|
);
|
|
268
342
|
}
|
|
269
343
|
const remaining = importPath.slice(2).split("/").filter(Boolean);
|
package/lib/resolver.test.ts
CHANGED
|
@@ -52,7 +52,7 @@ describe("resolveLayers()", () => {
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
it("anchors the liquid slot against the baseline", () => {
|
|
55
|
-
expect(resolved[0].liquid
|
|
55
|
+
expect(resolved[0].liquid?.layer).toBe(baseline.layer);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
it("provides the ts slot from the baseline", () => {
|
|
@@ -97,6 +97,70 @@ describe("resolveLayers()", () => {
|
|
|
97
97
|
});
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
describe("with a component that has a block/ face", () => {
|
|
101
|
+
beforeEach(async () => {
|
|
102
|
+
await writeComponent(baseline, "components/image", {
|
|
103
|
+
liquid: "<img>",
|
|
104
|
+
});
|
|
105
|
+
await writeFace(baseline, "components/image", {
|
|
106
|
+
schema: "export const schema = { name: 'Image' };",
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
let resolved: ResolvedComponent[];
|
|
111
|
+
beforeEach(async () => {
|
|
112
|
+
resolved = await resolveLayers([baseline.layer]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("resolves the snippet and the face as two components", () => {
|
|
116
|
+
expect(resolved).toHaveLength(2);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("emits the face as a blocks-typed component with the parent's segments", () => {
|
|
120
|
+
const face = resolved.find((component) => component.face);
|
|
121
|
+
expect(face?.type).toBe("blocks");
|
|
122
|
+
expect(face?.segments).toEqual(["components", "image"]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("anchors the mechanical face on its schema, not a liquid", () => {
|
|
126
|
+
const face = resolved.find((component) => component.face);
|
|
127
|
+
expect(face?.liquid).toBeUndefined();
|
|
128
|
+
expect(face?.schema?.layer).toBe(baseline.layer);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("does not flag the underlying snippet as a face", () => {
|
|
132
|
+
const snippet = resolved.find(
|
|
133
|
+
(component) => component.type === "components",
|
|
134
|
+
);
|
|
135
|
+
expect(snippet?.face).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("with a block/ dir under a section (not a snippet)", () => {
|
|
140
|
+
beforeEach(async () => {
|
|
141
|
+
await writeComponent(baseline, "sections/hero", {
|
|
142
|
+
liquid: "<section></section>",
|
|
143
|
+
});
|
|
144
|
+
await writeFace(baseline, "sections/hero", {
|
|
145
|
+
schema: "export const schema = { name: 'Nope' };",
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
let resolved: ResolvedComponent[];
|
|
150
|
+
beforeEach(async () => {
|
|
151
|
+
resolved = await resolveLayers([baseline.layer]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("does not produce a face (faces are components-only)", () => {
|
|
155
|
+
expect(resolved.some((component) => component.face)).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("leaves the section as the only resolved component", () => {
|
|
159
|
+
expect(resolved).toHaveLength(1);
|
|
160
|
+
expect(resolved[0].type).toBe("sections");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
100
164
|
describe("with a consumer layer ahead of the baseline", () => {
|
|
101
165
|
describe("when the consumer shadows only the css slot", () => {
|
|
102
166
|
beforeEach(async () => {
|
|
@@ -119,7 +183,7 @@ describe("resolveLayers()", () => {
|
|
|
119
183
|
});
|
|
120
184
|
|
|
121
185
|
it("takes the liquid slot from the baseline", () => {
|
|
122
|
-
expect(button.liquid
|
|
186
|
+
expect(button.liquid?.layer).toBe(baseline.layer);
|
|
123
187
|
});
|
|
124
188
|
|
|
125
189
|
it("takes the ts slot from the baseline", () => {
|
|
@@ -153,7 +217,7 @@ describe("resolveLayers()", () => {
|
|
|
153
217
|
});
|
|
154
218
|
|
|
155
219
|
it("takes the liquid slot from the consumer", () => {
|
|
156
|
-
expect(button.liquid
|
|
220
|
+
expect(button.liquid?.layer).toBe(consumer.layer);
|
|
157
221
|
});
|
|
158
222
|
|
|
159
223
|
it("takes the ts slot from the consumer", () => {
|
|
@@ -189,7 +253,7 @@ describe("resolveLayers()", () => {
|
|
|
189
253
|
});
|
|
190
254
|
|
|
191
255
|
it("still anchors the liquid slot against the baseline", () => {
|
|
192
|
-
expect(button.liquid
|
|
256
|
+
expect(button.liquid?.layer).toBe(baseline.layer);
|
|
193
257
|
});
|
|
194
258
|
});
|
|
195
259
|
|
|
@@ -273,3 +337,26 @@ async function writeComponent(
|
|
|
273
337
|
),
|
|
274
338
|
);
|
|
275
339
|
}
|
|
340
|
+
|
|
341
|
+
// Writes a block face: files named after the component but in its `block/`
|
|
342
|
+
// subdir (e.g. src/components/image/block/image.schema.ts).
|
|
343
|
+
async function writeFace(
|
|
344
|
+
fixture: Fixture,
|
|
345
|
+
segments: string,
|
|
346
|
+
slots: { liquid?: string; schema?: string },
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
const parts = segments.split("/");
|
|
349
|
+
const last = parts[parts.length - 1];
|
|
350
|
+
const directory = join(fixture.path, "src", ...parts, "block");
|
|
351
|
+
await mkdir(directory, { recursive: true });
|
|
352
|
+
await Promise.all(
|
|
353
|
+
Object.entries({
|
|
354
|
+
[`${last}.liquid`]: slots.liquid,
|
|
355
|
+
[`${last}.schema.ts`]: slots.schema,
|
|
356
|
+
})
|
|
357
|
+
.filter(([, content]) => content !== undefined)
|
|
358
|
+
.map(([filename, content]) =>
|
|
359
|
+
writeFile(join(directory, filename), content ?? ""),
|
|
360
|
+
),
|
|
361
|
+
);
|
|
362
|
+
}
|