Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(): supports slots in template #4532

Merged
merged 1 commit into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ module.exports = [
},
{
path: "packages/brick-kit/dist/index.esm.js",
limit: "131 KB",
limit: "135 KB",
},
{
path: "packages/brick-types/dist/index.esm.js",
Expand Down
45 changes: 32 additions & 13 deletions packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
RuntimeBrickConf,
SlotsConfOfBricks,
CustomTemplate,
type SlotConfOfBricks,
type BrickConf,
} from "@next-core/brick-types";
import { RuntimeBrick } from "../BrickNode";
import {
Expand All @@ -16,23 +18,22 @@
} from "./internalInterfaces";
import { isMergeableProperty, isVariableProperty } from "./assertions";
import { collectRefsInTemplate } from "./collectRefsInTemplate";
import {
customTemplateRegistry,
RuntimeBrickConfWithTplSymbols,
} from "./constants";
import { customTemplateRegistry } from "./constants";
import { collectMergeBases } from "./collectMergeBases";
import { CustomTemplateContext } from "./CustomTemplateContext";
import { setupUseBrickInTemplate } from "./setupUseBrickInTemplate";
import { setupTemplateProxy } from "./setupTemplateProxy";
import { collectWidgetContract } from "../CollectContracts";
import type { LocationContext } from "../LocationContext";
import { replaceSlotWithSlottedBricks } from "./replaceSlotWithSlottedBricks";

export interface ProxyContext {
reversedProxies: ReversedProxies;
templateProperties: Record<string, unknown>;
externalSlots: SlotsConfOfBricks;
templateContextId: string;
proxyBrick: RuntimeBrick;
usedSlots: Set<string>;
}

interface ReversedProxies {
Expand Down Expand Up @@ -219,39 +220,57 @@
externalSlots: externalSlots as SlotsConfOfBricks,
templateContextId: tplContext.id,
proxyBrick,
usedSlots: new Set(),
};

newBrickConf.slots = {
"": {
type: "bricks",
bricks: bricks.map((item) => expandBrickInTemplate(item, proxyContext)),
bricks: bricks.flatMap((item) =>
expandBrickInTemplate(item, proxyContext)
),
},
};

return newBrickConf;
}

function expandBrickInTemplate(
brickConfInTemplate: BrickConfInTemplate,
proxyContext: ProxyContext
): RuntimeBrickConfWithTplSymbols {
brickConfInTemplate: BrickConf,
proxyContext: ProxyContext,
markSlotted?: () => void
): BrickConf | BrickConf[] {
// Ignore `if: null` to make `looseCheckIf` working.
if (brickConfInTemplate.if === null) {
delete brickConfInTemplate.if;
}

if (brickConfInTemplate.brick === "slot") {
markSlotted?.();
return replaceSlotWithSlottedBricks(

Check warning on line 250 in packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts

View check run for this annotation

Codecov / codecov/patch

packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts#L249-L250

Added lines #L249 - L250 were not covered by tests
brickConfInTemplate,
proxyContext,
expandBrickInTemplate
);
}

const {
ref,
slots: slotsInTemplate,
...restBrickConfInTemplate
} = brickConfInTemplate;
} = brickConfInTemplate as BrickConfInTemplate;

const slots: SlotsConfOfBricks = Object.fromEntries(
let slotted = false;
const markChild = (): void => {
slotted = true;

Check warning on line 265 in packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts

View check run for this annotation

Codecov / codecov/patch

packages/brick-kit/src/core/CustomTemplates/expandCustomTemplate.ts#L265

Added line #L265 was not covered by tests
};
const slots = Object.fromEntries<SlotConfOfBricks>(
Object.entries(slotsInTemplate ?? {}).map(([slotName, slotConf]) => [
slotName,
{
type: "bricks",
bricks: (slotConf.bricks ?? []).map((item) =>
expandBrickInTemplate(item, proxyContext)
bricks: (slotConf.bricks ?? []).flatMap((item) =>
expandBrickInTemplate(item, proxyContext, markChild)
),
},
])
Expand All @@ -264,6 +283,6 @@
proxyContext
),
slots,
...setupTemplateProxy(proxyContext, ref, slots),
...setupTemplateProxy(proxyContext, ref, slots, slotted),
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { BrickConf } from "@next-core/brick-types";
import { identity } from "lodash";
import { replaceSlotWithSlottedBricks } from "./replaceSlotWithSlottedBricks";
import type { ProxyContext } from "./expandCustomTemplate";

describe("replaceSlotWithSlottedBricks", () => {
let mockHostContext: ProxyContext;

beforeEach(() => {
mockHostContext = {
proxyBrick: {
type: "tpl-test",
},
usedSlots: new Set(),
} as unknown as ProxyContext;
});

it("should throw an error if 'if' is used in a slot", () => {
const brickConf = { brick: "slot", if: false };
expect(() =>
replaceSlotWithSlottedBricks(brickConf, mockHostContext, identity)
).toThrow(
`Can not use "if" in a slot currently, check your template "tpl-test"`
);
const brickConf2 = { brick: "slot", if: "<% true %>" };
expect(() =>
replaceSlotWithSlottedBricks(brickConf2, mockHostContext, identity)
).toThrow(
`Can not use "if" in a slot currently, check your template "tpl-test"`
);
});

it("should throw an error if slot name is an expression", () => {
const brickConf = {
brick: "slot",
properties: { name: "<% 'abc' %>" },
} as BrickConf;
expect(() =>
replaceSlotWithSlottedBricks(brickConf, mockHostContext, identity)
).toThrow(
`Can not use an expression as slot name "<% 'abc' %>" currently, check your template "tpl-test"`
);
});

it("should throw an error if slot name is repeated", () => {
const brickConf = {
brick: "div",
properties: { name: "repeated-slot" },
} as BrickConf;
mockHostContext.usedSlots.add("repeated-slot");
expect(() =>
replaceSlotWithSlottedBricks(brickConf, mockHostContext, identity)
).toThrow(
`Can not have multiple slots with the same name "repeated-slot", check your template "tpl-test"`
);
});

it("should return external bricks if available", () => {
const brickConf = {
brick: "slot",
properties: { name: "slot1" },
} as BrickConf;
mockHostContext.externalSlots = {
slot1: { type: "bricks", bricks: [{ brick: "h1" }, { brick: "h2" }] },
};
const result = replaceSlotWithSlottedBricks(
brickConf,
mockHostContext,
identity
);
expect(result).toEqual([{ brick: "h1" }, { brick: "h2" }]);
});

it("should return expanded default slots if no external bricks", () => {
const brickConf = {
brick: "slot",
properties: { name: "slot1" },
slots: {
"": {
type: "bricks",
bricks: [
{
brick: "p",
},
],
},
oops: {
type: "bricks",
bricks: [
{
brick: "hr",
},
],
},
},
} as BrickConf;
const expandMock = jest.fn().mockImplementation((item) => [item]);
const result = replaceSlotWithSlottedBricks(
brickConf,
mockHostContext,
expandMock
);
expect(result).toEqual([{ brick: "p" }]);
expect(expandMock).toHaveBeenCalledTimes(1);
});

it("no default bricks nor external bricks", () => {
const brickConf = {
brick: "slot",
properties: { name: "slot1" },
} as BrickConf;
const result = replaceSlotWithSlottedBricks(
brickConf,
mockHostContext,
identity
);
expect(result).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type {
BrickConf,
BrickConfInTemplate,
UseSingleBrickConf,
} from "@next-core/brick-types";
import { hasOwnProperty } from "@next-core/brick-utils";
import { isEvaluable } from "@next-core/cook";
import type { ProxyContext } from "./expandCustomTemplate";

export function replaceSlotWithSlottedBricks<
T extends BrickConf | UseSingleBrickConf
>(
brickConf: T,
proxyContext: ProxyContext,
expand: (item: T, proxyContext: ProxyContext) => T | T[]
): T[] {
// Currently, no support for `if` in a slot.
if (
(brickConf.if != null && !brickConf.if) ||
typeof brickConf.if === "string"
) {
throw new Error(
`Can not use "if" in a slot currently, check your template "${proxyContext.proxyBrick.type}"`
);
}

const slot = String(brickConf.properties?.name ?? "");

// Currently, no support for expression as slot name.
if (isEvaluable(slot)) {
throw new Error(
`Can not use an expression as slot name "${slot}" currently, check your template "${proxyContext.proxyBrick.type}"`
);
}

// Do not repeat the same slot name in a template.
if (proxyContext.usedSlots.has(slot)) {
throw new Error(
`Can not have multiple slots with the same name "${slot}", check your template "${proxyContext.proxyBrick.type}"`
);
}
proxyContext.usedSlots.add(slot);

if (
proxyContext.externalSlots &&
hasOwnProperty(proxyContext.externalSlots, slot)
) {
const insertBricks = proxyContext.externalSlots[slot].bricks ?? [];
if (insertBricks.length > 0) {
return insertBricks as T[];
}
}

return ((brickConf as BrickConfInTemplate).slots?.[""]?.bricks ?? []).flatMap(
(item) => expand(item as T, proxyContext)
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
export function setupTemplateProxy(
proxyContext: Partial<ProxyContext>,
ref: string,
slots: SlotsConfOfBricks
slots: SlotsConfOfBricks,
slotted?: boolean
): RuntimeBrickConfOfTplSymbols {
const computedPropsFromProxy: Record<string, unknown> = {};
let refForProxy: RefForProxy;
Expand Down Expand Up @@ -89,6 +90,12 @@
const quasisMap = new Map<string, BrickConf[][]>();

if (reversedProxies.slots.has(ref)) {
if (slotted) {
throw new Error(

Check warning on line 94 in packages/brick-kit/src/core/CustomTemplates/setupTemplateProxy.ts

View check run for this annotation

Codecov / codecov/patch

packages/brick-kit/src/core/CustomTemplates/setupTemplateProxy.ts#L94

Added line #L94 was not covered by tests
`Can not have proxied slot ref when the ref target has a slot element child, check your template "${proxyBrick.type}" and ref "${ref}"`
);
}

for (const item of reversedProxies.slots.get(ref)) {
if (!quasisMap.has(item.refSlot)) {
const quasis: BrickConf[][] = [];
Expand Down
Loading