Build composable Camino packages with explicit type and source dependencies
This commit is contained in:
+56
-4
@@ -9,7 +9,9 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
|
||||
if (pkg.components?.length) throw Error("Declared components require checked component build settings");
|
||||
return [];
|
||||
}
|
||||
const declarations = pkg.components ?? [];
|
||||
const declarations = config.group
|
||||
? (pkg.components ?? []).filter((entry) => Object.hasOwn(settings.entries, entry.id))
|
||||
: (pkg.components ?? []);
|
||||
const entries = Object.entries(settings.entries);
|
||||
if (
|
||||
entries.length !== declarations.length ||
|
||||
@@ -86,6 +88,46 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
|
||||
await fs.writeFile(path.join(output, "share/quixos/generated/component-assets.d.ts"), assets);
|
||||
generated.set("component-client", { file: path.join(work, bindingPath), source: bindings });
|
||||
generated.set("component-assets", { file: path.join(work, assetPath), source: assets });
|
||||
// Public props are type-only inputs owned by imported interface resources.
|
||||
// Augment the generated nominal registry without importing an implementation.
|
||||
const propsImports = [],
|
||||
propsMembers = [],
|
||||
seenProps = new Set();
|
||||
for (const root of settings.contracts ?? []) {
|
||||
const companion = JSON.parse(await fs.readFile(path.join(root, "react-contracts.json"), "utf8"));
|
||||
if (companion.schemaVersion !== 1) throw Error("Unsupported React props companion");
|
||||
const contracts = plan.definition.workspace.interfaceImports.filter(
|
||||
(iface) =>
|
||||
iface.revisionId === companion.interfaceRevisionId ||
|
||||
iface.application?.definitionId === companion.interfaceRevisionId,
|
||||
);
|
||||
if (!contracts.length) continue;
|
||||
for (const [memberId, entry] of Object.entries(companion.members)) {
|
||||
if (!/^[$A-Z_a-z][$\w]*$/.test(entry.export)) throw Error("Invalid props type export");
|
||||
const alias = `Props${propsImports.length}`;
|
||||
propsImports.push(
|
||||
`import type {${entry.export} as ${alias}} from ${JSON.stringify(path.join(root, "types", compiler.artifactPath(entry.path)))};`,
|
||||
);
|
||||
for (const iface of contracts) {
|
||||
if (
|
||||
!(iface.template?.members ?? iface.members).some(
|
||||
(member) => member.kind === "module" && member.id === memberId,
|
||||
)
|
||||
)
|
||||
throw Error("Props companion names an unknown module member");
|
||||
const key = JSON.stringify([iface.revisionId, memberId]);
|
||||
if (seenProps.has(key)) throw Error("Duplicate React props companion for " + key);
|
||||
seenProps.add(key);
|
||||
propsMembers.push(`${JSON.stringify(key)}: ${alias};`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const propsPath = "component-props.d.ts";
|
||||
await fs.writeFile(
|
||||
path.join(work, propsPath),
|
||||
propsImports.join("\n") +
|
||||
`\nexport {};\ndeclare module ${JSON.stringify("./" + bindingPath.replace(/\.ts$/, ".js"))} { interface ModuleProps {${propsMembers.join("\n")}} }\n`,
|
||||
);
|
||||
const components = [];
|
||||
for (const [id, entry] of entries) {
|
||||
const declaration = declarations.find((component) => component.id === id),
|
||||
@@ -93,10 +135,14 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
|
||||
if (!/^[$A-Z_a-z][$\w]*$/.test(entry.export)) throw Error("Invalid component export name");
|
||||
const key = digest(Buffer.from(id)).slice(0, 24),
|
||||
directory = "share/quixos/components/" + key;
|
||||
const witness = path.join(work, "component-check-" + key + ".ts");
|
||||
const witness = path.join(work, "component-check-" + key + ".tsx");
|
||||
const module = pkg.modules.find((module) => module.id === id);
|
||||
await fs.writeFile(
|
||||
witness,
|
||||
`import type {Components} from ${JSON.stringify("./" + bindingPath.replace(/\.ts$/, ".js"))};import {${entry.export} as Component} from ${JSON.stringify("./" + source.replace(/\.[cm]?tsx?$/, ".js"))};const checked:Components[${JSON.stringify(declaration.displayName)}]=Component;export default checked;\n`,
|
||||
`import type {Modules} from ${JSON.stringify("./" + bindingPath.replace(/\.ts$/, ".js"))};import {${entry.export} as Component} from ${JSON.stringify("./" + source.replace(/\.[cm]?tsx?$/, ".js"))};const checked:Modules[${JSON.stringify(declaration.displayName)}]=Component;export default checked;\n` +
|
||||
(module.contract === "org.quixos.react.subject-only/1"
|
||||
? `import type {ComponentProps} from "@quixos/camino-react";declare const subject:ComponentProps<${JSON.stringify(declaration.subject)}>['subject'];const mount = <Component subject={subject}/>;\n`
|
||||
: ""),
|
||||
);
|
||||
execFileSync(
|
||||
config.node,
|
||||
@@ -118,15 +164,21 @@ export async function buildComponents({ config, compiler, plan, pkg, output, rec
|
||||
"react-jsx",
|
||||
path.basename(witness),
|
||||
assetPath,
|
||||
propsPath,
|
||||
],
|
||||
{ cwd: work, stdio: "inherit" },
|
||||
);
|
||||
const metadata = path.join(work, "component-meta-" + key + ".json");
|
||||
const facade = "component-entry-" + key + ".ts";
|
||||
await fs.writeFile(
|
||||
path.join(work, facade),
|
||||
`export {${entry.export}} from ${JSON.stringify("./" + source.replace(/\.[cm]?tsx?$/, ".js"))};\n`,
|
||||
);
|
||||
await fs.mkdir(path.join(output, directory), { recursive: true });
|
||||
execFileSync(
|
||||
config.esbuild,
|
||||
[
|
||||
source,
|
||||
facade,
|
||||
"--bundle",
|
||||
"--platform=browser",
|
||||
"--format=esm",
|
||||
|
||||
Reference in New Issue
Block a user