mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-21 05:50:55 +08:00
Merge pull request #67 from vercel-labs/sub-export
Refuse unresolved subscriptions exports
This commit is contained in:
@@ -151,6 +151,10 @@ export interface LibrarySidecarConfig {
|
||||
initExport: string;
|
||||
updateExport: string;
|
||||
subscriptionsExport: string;
|
||||
/** True only when `sidecar.subscriptions_export` is present in the
|
||||
* profile. Omission keeps subscriptions optional; a declared name is a
|
||||
* reference that must resolve to an exported function. */
|
||||
subscriptionsExportDeclared: boolean;
|
||||
/** The profile-selected source-tree hashing contract. v1 implements
|
||||
* exactly one: "module-graph" (wyhash-64, seed 0xc0de5eed, over the
|
||||
* length-prefixed sorted module graph — canonical root-relative POSIX
|
||||
@@ -175,6 +179,9 @@ export interface LibrarySidecarConfig {
|
||||
export interface LibraryProfile {
|
||||
profileFormat: 1;
|
||||
name: string;
|
||||
/** The profile file that produced this configuration. Semantic
|
||||
* designation errors anchor here, where the offending name is written. */
|
||||
profilePath: string;
|
||||
/** Resolved absolute path of the ONE entry module. */
|
||||
entry: string;
|
||||
/** The pinned emission — no fallback concept exists on the library path. */
|
||||
@@ -432,6 +439,7 @@ export function loadLibraryProfile(
|
||||
initExport: nameField("init_export", "init"),
|
||||
updateExport: nameField("update_export", "update"),
|
||||
subscriptionsExport: nameField("subscriptions_export", "subscriptions"),
|
||||
subscriptionsExportDeclared: s["subscriptions_export"] !== undefined,
|
||||
sourceHash: "module-graph",
|
||||
integerSlots,
|
||||
};
|
||||
@@ -562,6 +570,7 @@ export function loadLibraryProfile(
|
||||
profile: {
|
||||
profileFormat: 1,
|
||||
name,
|
||||
profilePath,
|
||||
entry,
|
||||
emission,
|
||||
prefix,
|
||||
|
||||
@@ -1456,6 +1456,16 @@ export function buildSidecar(input: SidecarBuildInput): SidecarBuildResult {
|
||||
const helperNames = new Set(helpers.map((h) => h.name));
|
||||
|
||||
// Shape flags from the designated entries' declared signatures.
|
||||
const subscriptionsFn = facts.functions.find((f) => f.name === config.subscriptionsExport);
|
||||
if (config.subscriptionsExportDeclared && subscriptionsFn === undefined) {
|
||||
throw new SidecarRefusal(
|
||||
libSidecarDiag(
|
||||
`'sidecar.subscriptions_export' designates export '${config.subscriptionsExport}', but the entry module exports no function by that name`,
|
||||
{ file: profile.profilePath, start: 0, end: 0 },
|
||||
`export a function named '${config.subscriptionsExport}' from the entry module, or omit 'sidecar.subscriptions_export' when the contract has no subscriptions entry`,
|
||||
),
|
||||
);
|
||||
}
|
||||
const returnsCmd = (which: "init" | "update", exportName: string): boolean => {
|
||||
const fn = facts.functions.find((f) => f.name === exportName);
|
||||
if (fn === undefined) {
|
||||
@@ -1473,7 +1483,7 @@ export function buildSidecar(input: SidecarBuildInput): SidecarBuildResult {
|
||||
};
|
||||
const initReturnsCmd = returnsCmd("init", config.initExport);
|
||||
const updateReturnsCmd = returnsCmd("update", config.updateExport);
|
||||
const hasSubscriptions = facts.functions.some((f) => f.name === config.subscriptionsExport);
|
||||
const hasSubscriptions = subscriptionsFn !== undefined;
|
||||
|
||||
// Unbound lists: model fields or helper entries (helpers are bindable
|
||||
// surface — the clarified rule) on the model side, arm names on the
|
||||
|
||||
@@ -837,7 +837,7 @@ async function sidecarRefusal(
|
||||
source: string,
|
||||
sidecarPatch: Record<string, unknown> = {},
|
||||
extraFiles: Record<string, string> = {},
|
||||
): Promise<{ code: string; message: string; hint?: string }[]> {
|
||||
): Promise<{ code: string; message: string; file: string; hint?: string }[]> {
|
||||
const outDir = join(cacheDir, `refusal-${refusalCounter++}`);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const entry = join(outDir, "lib.ts");
|
||||
@@ -872,7 +872,11 @@ async function sidecarRefusal(
|
||||
const result = await compileLibrary({ profilePath, outDir });
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("unreachable");
|
||||
return result.diagnostics.map((d) => (d.hint === undefined ? { code: d.code, message: d.message } : { code: d.code, message: d.message, hint: d.hint }));
|
||||
return result.diagnostics.map((d) => (
|
||||
d.hint === undefined
|
||||
? { code: d.code, message: d.message, file: d.loc.file }
|
||||
: { code: d.code, message: d.message, file: d.loc.file, hint: d.hint }
|
||||
));
|
||||
}
|
||||
|
||||
const REFUSAL_BASE = `export interface Model { count: number; }
|
||||
@@ -985,6 +989,19 @@ export function boot(): number {
|
||||
});
|
||||
|
||||
describe("SC4009: contract sidecar refusals", () => {
|
||||
test("an explicit subscriptions export naming nothing refuses at the profile", async () => {
|
||||
const diags = await sidecarRefusal(REFUSAL_BASE, { subscriptions_export: "watch" });
|
||||
expect(diags[0]!.code).toBe("SC4009");
|
||||
expect(diags[0]!.message).toContain("'sidecar.subscriptions_export'");
|
||||
expect(diags[0]!.message).toContain("'watch'");
|
||||
expect(diags[0]!.file).toMatch(/[/\\]profile\.json$/);
|
||||
});
|
||||
|
||||
test("an omitted subscriptions export remains optional", async () => {
|
||||
const doc = await sidecarProjection(REFUSAL_BASE);
|
||||
expect(doc.has_subscriptions).toBe(false);
|
||||
});
|
||||
|
||||
test("a model designation naming nothing refuses", async () => {
|
||||
const diags = await sidecarRefusal(REFUSAL_BASE, { model: "Nope" });
|
||||
expect(diags[0]!.code).toBe("SC4009");
|
||||
|
||||
@@ -239,6 +239,7 @@ describe("library profile sidecar section", () => {
|
||||
initExport: "init",
|
||||
updateExport: "update",
|
||||
subscriptionsExport: "subscriptions",
|
||||
subscriptionsExportDeclared: false,
|
||||
sourceHash: "module-graph",
|
||||
integerSlots: [],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user