feat(core): expose model-declared windows to TypeScript (#351)

* feat(core): expose model-declared windows to TypeScript

- Add canonical TypeScript window descriptors with close-policy and close-command routing.
- Compile and hot-reload label-addressed secondary-window markup in generated launchers.
- Cover quit/hide behavior end to end and port the TypeScript system-monitor settings window.

* fix(core): harden TypeScript window declarations

* fix(core): validate TypeScript window views

* fix(core): validate returned window descriptors

* fix(core): harden TypeScript window contracts
This commit is contained in:
Chris Tate
2026-08-14 09:30:18 -05:00
committed by GitHub
parent e924d7fcac
commit 0ecdc7d2e9
37 changed files with 1602 additions and 36 deletions
+25
View File
@@ -84,6 +84,31 @@ export function utf8Bytes(s: string): Uint8Array {
export type Msgish = { readonly kind: string };
import { type WindowDescriptor, type WindowDescriptorSpec } from "./events.ts";
export function windowDescriptor(spec: WindowDescriptorSpec): WindowDescriptor {
return {
label: spec.label,
canvasLabel: spec.canvasLabel,
title: spec.title ?? new Uint8Array(0),
width: spec.width ?? 480,
height: spec.height ?? 360,
x: spec.x ?? null,
y: spec.y ?? null,
resizable: spec.resizable ?? true,
minWidth: spec.minWidth ?? 0,
minHeight: spec.minHeight ?? 0,
titlebar: spec.titlebar ?? "standard",
transparent: spec.transparent ?? false,
alwaysOnTop: spec.alwaysOnTop ?? false,
clickThrough: spec.clickThrough ?? false,
activateOnShow: spec.activateOnShow ?? true,
allowsFullscreen: spec.allowsFullscreen ?? true,
closePolicy: spec.closePolicy ?? "quit",
onCloseCommand: spec.onCloseCommand ?? new Uint8Array(0),
};
}
/** Cooperative cancellation capability supplied by generated service hosts. */
export interface ServiceCancellation {
readonly cancelled: () => boolean;
+2
View File
@@ -3,6 +3,8 @@ export declare function utf8Bytes(s: string): Uint8Array;
export type Msgish = {
readonly kind: string;
};
import { type WindowDescriptor, type WindowDescriptorSpec } from "./events.js";
export declare function windowDescriptor(spec: WindowDescriptorSpec): WindowDescriptor;
/** Cooperative cancellation capability supplied by generated service hosts. */
export interface ServiceCancellation {
/** True after Cmd.cancel or the operation deadline requests cancellation. */
+26
View File
@@ -375,6 +375,32 @@ export function utf8Bytes(s: string): Uint8Array {
/// Every app Msg is a discriminated union on a string `kind` tag.
export type Msgish = { readonly kind: string };
import { type WindowDescriptor, type WindowDescriptorSpec } from "./events.ts";
/// Fill the canonical defaults for a model-declared secondary window.
export function windowDescriptor(spec: WindowDescriptorSpec): WindowDescriptor {
return {
label: spec.label,
canvasLabel: spec.canvasLabel,
title: spec.title ?? new Uint8Array(0),
width: spec.width ?? 480,
height: spec.height ?? 360,
x: spec.x ?? null,
y: spec.y ?? null,
resizable: spec.resizable ?? true,
minWidth: spec.minWidth ?? 0,
minHeight: spec.minHeight ?? 0,
titlebar: spec.titlebar ?? "standard",
transparent: spec.transparent ?? false,
alwaysOnTop: spec.alwaysOnTop ?? false,
clickThrough: spec.clickThrough ?? false,
activateOnShow: spec.activateOnShow ?? true,
allowsFullscreen: spec.allowsFullscreen ?? true,
closePolicy: spec.closePolicy ?? "quit",
onCloseCommand: spec.onCloseCommand ?? new Uint8Array(0),
};
}
/** Cooperative cancellation capability supplied by generated service hosts. */
export interface ServiceCancellation {
/** True after Cmd.cancel or the operation deadline requests cancellation. */
+42
View File
@@ -46,6 +46,48 @@ export interface StatusItemDescriptor {
readonly presentation: StatusItemPresentation;
readonly items: readonly StatusItemMenuItem[];
}
export type WindowClosePolicy = "quit" | "hide";
export type WindowTitlebarStyle = "standard" | "hidden_inset" | "hidden_inset_tall" | "chromeless";
export interface WindowDescriptorSpec {
readonly label: Uint8Array;
readonly canvasLabel: Uint8Array;
readonly title?: Uint8Array;
readonly width?: number;
readonly height?: number;
readonly x?: number | null;
readonly y?: number | null;
readonly resizable?: boolean;
readonly minWidth?: number;
readonly minHeight?: number;
readonly titlebar?: WindowTitlebarStyle;
readonly transparent?: boolean;
readonly alwaysOnTop?: boolean;
readonly clickThrough?: boolean;
readonly activateOnShow?: boolean;
readonly allowsFullscreen?: boolean;
readonly closePolicy?: WindowClosePolicy;
readonly onCloseCommand?: Uint8Array;
}
export interface WindowDescriptor {
readonly label: Uint8Array;
readonly canvasLabel: Uint8Array;
readonly title: Uint8Array;
readonly width: number;
readonly height: number;
readonly x: number | null;
readonly y: number | null;
readonly resizable: boolean;
readonly minWidth: number;
readonly minHeight: number;
readonly titlebar: WindowTitlebarStyle;
readonly transparent: boolean;
readonly alwaysOnTop: boolean;
readonly clickThrough: boolean;
readonly activateOnShow: boolean;
readonly allowsFullscreen: boolean;
readonly closePolicy: WindowClosePolicy;
readonly onCloseCommand: Uint8Array;
}
export interface ScrollState {
readonly offsetX: number;
readonly offsetY: number;
+56
View File
@@ -112,6 +112,62 @@ export interface StatusItemDescriptor {
readonly items: readonly StatusItemMenuItem[];
}
/// What the user's close affordance does for a model-declared secondary
/// window. `quit` really closes it and routes `onCloseCommand`; `hide` keeps
/// the native window and view tree alive for `Cmd.showWindow(label)`.
export type WindowClosePolicy = "quit" | "hide";
export type WindowTitlebarStyle = "standard" | "hidden_inset" | "hidden_inset_tall" | "chromeless";
/// Author-facing input to `windowDescriptor`; omitted fields receive the
/// same defaults as UiApp.WindowDescriptor.
export interface WindowDescriptorSpec {
readonly label: Uint8Array;
readonly canvasLabel: Uint8Array;
readonly title?: Uint8Array;
readonly width?: number;
readonly height?: number;
readonly x?: number | null;
readonly y?: number | null;
readonly resizable?: boolean;
readonly minWidth?: number;
readonly minHeight?: number;
readonly titlebar?: WindowTitlebarStyle;
readonly transparent?: boolean;
readonly alwaysOnTop?: boolean;
readonly clickThrough?: boolean;
readonly activateOnShow?: boolean;
readonly allowsFullscreen?: boolean;
readonly closePolicy?: WindowClosePolicy;
readonly onCloseCommand?: Uint8Array;
}
/// One independently reconciled secondary window returned by
/// `windows(model)`. The generated launcher compiles
/// `src/windows/<label>.native` as its view; presence is liveness. Construct
/// entries with `windowDescriptor` and spell `label` as a literal
/// `asciiBytes("<label>")` so check/build can prove that root exists.
export interface WindowDescriptor {
readonly label: Uint8Array;
readonly canvasLabel: Uint8Array;
readonly title: Uint8Array;
readonly width: number;
readonly height: number;
readonly x: number | null;
readonly y: number | null;
readonly resizable: boolean;
readonly minWidth: number;
readonly minHeight: number;
readonly titlebar: WindowTitlebarStyle;
readonly transparent: boolean;
readonly alwaysOnTop: boolean;
readonly clickThrough: boolean;
readonly activateOnShow: boolean;
readonly allowsFullscreen: boolean;
readonly closePolicy: WindowClosePolicy;
readonly onCloseCommand: Uint8Array;
}
/// The scroll-state mirror markup's `on-scroll` matches structurally: a
/// record of exactly these eight numeric fields — the TWO-AXIS shape, one
/// offset/velocity/viewport/content quartet per axis. Offsets and extents
+333 -1
View File
@@ -462,6 +462,7 @@ export class SubsetChecker {
private readonly permissions: Set<string>;
private readonly persistRoutes: PersistRoutes | undefined;
private readonly sdkCorePath: string;
private readonly windowViews: ReadonlySet<string> | undefined;
private usesPersist = false;
private usesStore = false;
private usesSqlite = false;
@@ -476,6 +477,7 @@ export class SubsetChecker {
permissions: readonly string[] = [],
persistRoutes?: PersistRoutes,
sdkCorePath: string = sdkCoreModulePath,
windowViews?: readonly string[],
) {
this.tast = tast;
this.table = table;
@@ -487,6 +489,7 @@ export class SubsetChecker {
this.permissions = new Set(permissions);
this.persistRoutes = persistRoutes;
this.sdkCorePath = sdkCorePath;
this.windowViews = windowViews === undefined ? undefined : new Set(windowViews);
}
check(): CheckResult {
@@ -506,6 +509,7 @@ export class SubsetChecker {
this.checkThemePackHelper();
this.checkStatusItemHelper();
this.checkStatusItemsHelper();
this.checkWindowsHelper();
this.checkViewUnbound();
this.checkReservedContractConsts();
this.checkValueRecordAliases();
@@ -995,6 +999,254 @@ export class SubsetChecker {
}
}
/// One entry-module function exported under its own wiring name. The
/// entry-contract pass separately teaches re-exports and renames; this
/// query lets related channels prove that the generated launcher will
/// actually have the named callback available.
private entryExportedFunction(name: string): ts.FunctionDeclaration | null {
for (const stmt of this.entry.statements) {
if (ts.isFunctionDeclaration(stmt) && stmt.name?.text === name && hasExportModifier(stmt)) return stmt;
}
for (const binding of exportListBindings(this.tast, this.entry)) {
if (
binding.exportedName === name &&
!binding.renamed &&
binding.target !== null &&
binding.target !== undefined &&
ts.isFunctionDeclaration(binding.target) &&
binding.target.getSourceFile() === this.entry
) {
return binding.target;
}
}
return null;
}
/// The generated launcher owns a closed, comptime-compiled registry keyed
/// by direct `src/windows/<label>.native` stems. Require every possible
/// descriptor to declare that identity as a literal at its canonical
/// constructor call, then prove the root exists before compilation. This
/// turns a would-be first-render panic into a source diagnostic in both
/// `native check` and every build.
private checkWindowViewRegistry(windowsDecl: ts.FunctionDeclaration): void {
if (this.windowViews === undefined) return;
type ProducedKind = "collection" | "descriptor";
let sawConstructor = false;
let productionErrors = 0;
const validatingCollections = new Set<ts.FunctionDeclaration>();
const validatingDescriptors = new Set<ts.FunctionDeclaration>();
const validatedCollections = new Set<ts.FunctionDeclaration>();
const validatedDescriptors = new Set<ts.FunctionDeclaration>();
const unwrap = (expression: ts.Expression): ts.Expression => {
let current = expression;
while (
ts.isParenthesizedExpression(current) ||
ts.isAsExpression(current) ||
ts.isSatisfiesExpression(current) ||
ts.isNonNullExpression(current)
) {
current = current.expression;
}
return current;
};
const reportProduction = (node: ts.Node, kind: ProducedKind): void => {
productionErrors += 1;
this.report(
"NS1033",
kind === "collection"
? "Every value returned by `windows` must be an array literal of direct `windowDescriptor(...)` results (factored constructor helpers are fine); copied, spread, or mutable descriptor collections can replace the statically registered label."
: "Every window returned by `windows` must flow directly from `windowDescriptor({...})` (or a helper whose own returns do); copying, spreading, or rebuilding a descriptor can replace its statically registered label.",
node,
);
};
const appFunctionTarget = (call: ts.CallExpression): ts.FunctionDeclaration | null => {
if (!ts.isIdentifier(call.expression) && !ts.isPropertyAccessExpression(call.expression)) return null;
const target = this.tast.declarationOf(
ts.isIdentifier(call.expression) ? call.expression : call.expression.name,
);
return target && ts.isFunctionDeclaration(target) && this.fileSet.has(target.getSourceFile()) ? target : null;
};
const returnExpressions = (fn: ts.FunctionDeclaration): ts.Expression[] => {
const returns: ts.Expression[] = [];
const visit = (node: ts.Node): void => {
if (node !== fn && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node))) return;
if (ts.isReturnStatement(node)) {
if (node.expression) returns.push(node.expression);
return;
}
ts.forEachChild(node, visit);
};
if (fn.body) visit(fn.body);
return returns;
};
const validateConstructor = (call: ts.CallExpression): void => {
sawConstructor = true;
const spec = call.arguments[0];
if (!spec || !ts.isObjectLiteralExpression(unwrap(spec))) {
productionErrors += 1;
this.report(
"NS1033",
"A model-declared window must pass an inline object to `windowDescriptor`, with a static `label: asciiBytes(\"<label>\")` property selecting its compiled view.",
spec ?? call,
);
return;
}
const object = unwrap(spec) as ts.ObjectLiteralExpression;
const labelProp = object.properties.find((prop): prop is ts.PropertyAssignment =>
ts.isPropertyAssignment(prop) &&
((ts.isIdentifier(prop.name) && prop.name.text === "label") ||
(ts.isStringLiteral(prop.name) && prop.name.text === "label")),
);
const labelIndex = labelProp === undefined ? -1 : object.properties.indexOf(labelProp);
if (object.properties.some((property, index) => ts.isSpreadAssignment(property) && index > labelIndex)) {
productionErrors += 1;
this.report(
"NS1033",
"A `windowDescriptor` spec cannot spread fields after its literal label: that spread can replace the identity after the registry check. Move the literal label after every spread.",
object,
);
return;
}
const labelExpr = labelProp?.initializer;
const labelArg = labelExpr && ts.isCallExpression(labelExpr) &&
this.sdkRootFunctionName(labelExpr.expression) === "asciiBytes"
? labelExpr.arguments[0]
: undefined;
if (!labelArg || !ts.isStringLiteral(labelArg)) {
productionErrors += 1;
this.report(
"NS1033",
"A model-declared window label must be a static `asciiBytes(\"<label>\")` property inside `windowDescriptor({...})`; that literal selects `src/windows/<label>.native` at build time.",
labelProp ?? object,
);
return;
}
const label = labelArg.text;
if (
label.length === 0 ||
label.length > 64 ||
label === "." ||
label === ".." ||
label.includes("\0") ||
label.includes("/") ||
label.includes("\\")
) {
productionErrors += 1;
this.report(
"NS1033",
"A model-declared window label must be a non-empty filename stem of at most 64 ASCII bytes, without path separators or NUL bytes.",
labelArg,
);
return;
}
if (!this.windowViews.has(labelArg.text)) {
productionErrors += 1;
this.report(
"NS1033",
`Window label \`${labelArg.text}\` has no compiled view; add \`src/windows/${labelArg.text}.native\` or change the descriptor label to an existing root.`,
labelArg,
);
}
const propertyNamed = (name: string): ts.PropertyAssignment | undefined =>
object.properties.find((prop): prop is ts.PropertyAssignment =>
ts.isPropertyAssignment(prop) &&
((ts.isIdentifier(prop.name) && prop.name.text === name) ||
(ts.isStringLiteral(prop.name) && prop.name.text === name)),
);
const closePolicy = propertyNamed("closePolicy")?.initializer;
const alwaysHides = closePolicy !== undefined && ts.isStringLiteral(unwrap(closePolicy)) && unwrap(closePolicy).text === "hide";
const closeCommand = propertyNamed("onCloseCommand")?.initializer;
const closeCommandArg = closeCommand !== undefined && ts.isCallExpression(unwrap(closeCommand)) &&
this.sdkRootFunctionName((unwrap(closeCommand) as ts.CallExpression).expression) === "asciiBytes"
? (unwrap(closeCommand) as ts.CallExpression).arguments[0]
: undefined;
const definitelyEmptyCloseCommand = closeCommandArg !== undefined && ts.isStringLiteral(closeCommandArg) && closeCommandArg.text.length === 0;
if (
closeCommand !== undefined &&
!definitelyEmptyCloseCommand &&
!alwaysHides &&
this.entryExportedFunction("commandMsg") === null
) {
productionErrors += 1;
this.report(
"NS1033",
"A quit-close `onCloseCommand` requires `commandMsg(name: string): Msg | null`; otherwise the generated launcher cannot map the command to the Msg that removes the window declaration.",
closeCommand,
);
}
};
const validateFunction = (fn: ts.FunctionDeclaration, kind: ProducedKind): void => {
const validating = kind === "collection" ? validatingCollections : validatingDescriptors;
const validated = kind === "collection" ? validatedCollections : validatedDescriptors;
if (validated.has(fn) || validating.has(fn)) return;
validating.add(fn);
const returns = returnExpressions(fn);
if (returns.length === 0) reportProduction(fn.name ?? fn, kind);
for (const expression of returns) validateExpression(expression, kind);
validating.delete(fn);
validated.add(fn);
};
const validateExpression = (raw: ts.Expression, kind: ProducedKind): void => {
const expression = unwrap(raw);
if (ts.isConditionalExpression(expression)) {
validateExpression(expression.whenTrue, kind);
validateExpression(expression.whenFalse, kind);
return;
}
if (kind === "collection" && ts.isArrayLiteralExpression(expression)) {
for (const element of expression.elements) {
if (ts.isSpreadElement(element)) validateExpression(element.expression, "collection");
else validateExpression(element, "descriptor");
}
return;
}
if (kind === "descriptor" && ts.isIdentifier(expression)) {
const declaration = this.tast.declarationOf(expression);
if (
declaration &&
ts.isVariableDeclaration(declaration) &&
declaration.initializer &&
ts.isVariableDeclarationList(declaration.parent) &&
(declaration.parent.flags & ts.NodeFlags.Const) !== 0
) {
validateExpression(declaration.initializer, "descriptor");
return;
}
}
if (ts.isCallExpression(expression)) {
if (kind === "descriptor" && this.sdkRootFunctionName(expression.expression) === "windowDescriptor") {
validateConstructor(expression);
return;
}
const helper = appFunctionTarget(expression);
if (helper) {
validateFunction(helper, kind);
return;
}
}
reportProduction(expression, kind);
};
validateFunction(windowsDecl, "collection");
if (!sawConstructor && productionErrors === 0) {
this.report(
"NS1033",
"`windows` must construct each possible entry with `windowDescriptor({ label: asciiBytes(\"<label>\"), ... })` so the generated launcher can prove its `src/windows/<label>.native` view exists.",
windowsDecl.name ?? windowsDecl,
);
}
}
private checkStatusItemsHelper(): void {
let decl: ts.FunctionDeclaration | null = null;
for (const stmt of this.entry.statements) {
@@ -1108,6 +1360,86 @@ export class SubsetChecker {
}
}
/// `windows(model)` is the TypeScript launcher's model-declared secondary
/// window set. Keep the descriptor exact: the Zig adapter projects it into
/// UiApp.WindowDescriptor, including close-command routing and closePolicy.
private checkWindowsHelper(): void {
let decl: ts.FunctionDeclaration | null = null;
for (const stmt of this.entry.statements) {
if (ts.isFunctionDeclaration(stmt) && stmt.name?.text === "windows" && hasExportModifier(stmt)) {
decl = stmt;
break;
}
}
if (decl === null) {
for (const binding of exportListBindings(this.tast, this.entry)) {
if (
binding.exportedName === "windows" &&
binding.target !== null &&
binding.target !== undefined &&
ts.isFunctionDeclaration(binding.target) &&
binding.target.getSourceFile() === this.entry
) {
decl = binding.target;
break;
}
}
}
if (decl === null) return;
const helper = this.table.modelHelperDecls().find(
(candidate) => candidate.name === "windows" && candidate.decl === decl,
);
if (helper === undefined || decl.type === undefined) {
this.report(
"NS1033",
"`windows` must be a single-Model-parameter helper with an explicit `readonly WindowDescriptor[]` return type.",
decl.name ?? decl,
);
return;
}
const returns = this.table.resolveTypeNode(decl.type);
const descriptorType = returns.k === "slice" && returns.elem.k === "struct" ? returns.elem : null;
const descriptor = descriptorType === null ? undefined : this.table.structs.get(descriptorType.name);
const names = descriptor?.fields.map((field) => field.tsName).sort() ?? [];
const field = (name: string) => descriptor?.fields.find((candidate) => candidate.tsName === name);
const numeric = (name: string): boolean => {
const candidate = field(name);
return candidate !== undefined && ["number", "i64", "f64", "numAlias"].includes(candidate.type.k);
};
const enumMembersAre = (name: string, expected: readonly string[]): boolean => {
const candidate = field(name);
if (candidate?.type.k !== "enum") return false;
const found = this.table.enums.get(candidate.type.name)?.members.slice().sort() ?? [];
return found.join(",") === expected.slice().sort().join(",");
};
const optionalNumeric = (name: string): boolean => {
const candidate = field(name);
return candidate?.type.k === "optional" && ["number", "i64", "f64", "numAlias"].includes(candidate.type.inner.k);
};
const valid =
names.join(",") === "activateOnShow,allowsFullscreen,alwaysOnTop,canvasLabel,clickThrough,closePolicy,height,label,minHeight,minWidth,onCloseCommand,resizable,title,titlebar,transparent,width,x,y" &&
field("label")?.type.k === "bytes" &&
field("canvasLabel")?.type.k === "bytes" &&
field("title")?.type.k === "bytes" &&
numeric("width") && numeric("height") && optionalNumeric("x") && optionalNumeric("y") &&
field("resizable")?.type.k === "bool" && numeric("minWidth") && numeric("minHeight") &&
enumMembersAre("titlebar", ["standard", "hidden_inset", "hidden_inset_tall", "chromeless"]) &&
field("transparent")?.type.k === "bool" && field("alwaysOnTop")?.type.k === "bool" &&
field("clickThrough")?.type.k === "bool" && field("activateOnShow")?.type.k === "bool" &&
field("allowsFullscreen")?.type.k === "bool" && enumMembersAre("closePolicy", ["quit", "hide"]) &&
field("onCloseCommand")?.type.k === "bytes";
if (!valid) {
const shape = descriptor?.fields.map((candidate) => `${candidate.tsName}:${candidate.type.k}${candidate.type.k === "optional" ? `(${candidate.type.inner.k}${candidate.type.inner.k === "union" ? `:${candidate.type.inner.name}` : ""})` : candidate.type.k === "enum" ? `:${candidate.type.name}` : ""}`).join(", ") ?? `missing descriptor (return=${returns.k}${returns.k === "slice" ? ` elem=${returns.elem.k}` : ""})`;
this.report(
"NS1033",
`\`windows\` must return \`readonly WindowDescriptor[]\`; import \`WindowDescriptor\` from \`@native-sdk/core/events\` and construct entries with \`windowDescriptor(...)\`. Resolved: ${shape}`,
decl.type,
);
return;
}
this.checkWindowViewRegistry(decl);
}
/// NS1032 — `export const viewUnbound = [...] as const`: the dead-state
/// lint opt-out. Every entry must be a string literal naming a Model
/// field, an exported model helper, or a Msg kind; the emitter routes the
@@ -1539,7 +1871,7 @@ export class SubsetChecker {
/// entry points, but the exports themselves live in the entry module.
private static readonly entryOnlyExports = new Set([
"update", "initialModel", "subscriptions", "migrate",
"commandMsg", "keyMsg", "frameMsg", "pinchMsg", "dropMsg", "appearanceMsg", "chromeMsg", "envMsgs", "themePack", "statusItem", "statusItems",
"commandMsg", "keyMsg", "frameMsg", "pinchMsg", "dropMsg", "appearanceMsg", "chromeMsg", "envMsgs", "themePack", "statusItem", "statusItems", "windows",
"viewUnbound", "modelUnbound", "msgUnbound",
]);
+13
View File
@@ -35,6 +35,8 @@ function main(argv: string[]): number {
let persistNone: string | null = null;
let persistErr: string | null = null;
let sdkCorePath: string | null = null;
let windowViewsEnabled = false;
const windowViews: string[] = [];
const capabilities: string[] = [];
const permissions: string[] = [];
const servicePackages: ServicePackage[] = [];
@@ -109,6 +111,16 @@ function main(argv: string[]): number {
console.error("--sdk-core requires a generated core.ts path");
return 2;
}
} else if (args[i] === "--window-views") {
windowViewsEnabled = true;
} else if (args[i] === "--window-view") {
windowViewsEnabled = true;
const label = args[++i] ?? null;
if (label === null || label.length === 0) {
console.error("--window-view requires a non-empty label");
return 2;
}
windowViews.push(label);
} else if (args[i] === "-o" || args[i] === "--out") {
console.error(
"-o named the removed TS-to-Zig emitter (v0.7.0 removed it): TypeScript cores compile through the external core compiler now, and this CLI checks the core and emits its contract sidecar (--contract). Drop the flag.",
@@ -148,6 +160,7 @@ function main(argv: string[]): number {
persistStatePath: persistStatePath ?? undefined,
persistRoutes,
sdkCorePath: sdkCorePath ?? undefined,
windowViews: windowViewsEnabled ? windowViews : undefined,
};
const result = checkFile(entry, options);
for (const e of result.typeErrors) console.error(e);
+1
View File
@@ -463,6 +463,7 @@ class ContractEmitter {
// making every app repeat it in `viewUnbound`.
if (helperNames.includes("statusItem") && !model.includes("statusItem")) model.push("statusItem");
if (helperNames.includes("statusItems") && !model.includes("statusItems")) model.push("statusItems");
if (helperNames.includes("windows") && !model.includes("windows")) model.push("windows");
return { model, msg };
}
+11 -1
View File
@@ -91,6 +91,8 @@ let appId = "app";
const servicePackages: { name: string; version: string; content_hash: string }[] = [];
const capabilities = new Set<string>();
const permissions = new Set<string>();
let windowViewsEnabled = false;
const windowViews: string[] = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === "--script") script = args[++i] ?? null;
else if (args[i] === "--capability") {
@@ -103,6 +105,13 @@ for (let i = 0; i < args.length; i++) {
if (permission === null) usage();
permissions.add(permission);
}
else if (args[i] === "--window-views") windowViewsEnabled = true;
else if (args[i] === "--window-view") {
windowViewsEnabled = true;
const label = args[++i] ?? null;
if (label === null || label.length === 0) usage();
windowViews.push(label);
}
else if (args[i] === "--persist-ok") persistOk = args[++i] ?? null;
else if (args[i] === "--persist-none") persistNone = args[++i] ?? null;
else if (args[i] === "--persist-err") persistErr = args[++i] ?? null;
@@ -143,7 +152,7 @@ if (serviceCwd !== null) {
const persistRouteCount = [persistOk, persistNone, persistErr].filter((route) => route !== null).length;
if (persistRouteCount !== 0 && persistRouteCount !== 3) usage();
let checked: ReturnType<typeof checkFile> | null = null;
if (capabilities.size > 0 || permissions.size > 0 || (persistOk !== null && persistNone !== null && persistErr !== null) || fs.existsSync(path.join(path.dirname(path.resolve(entry)), "services"))) {
if (capabilities.size > 0 || permissions.size > 0 || windowViewsEnabled || (persistOk !== null && persistNone !== null && persistErr !== null) || fs.existsSync(path.join(path.dirname(path.resolve(entry)), "services"))) {
// Type information is erased by the time this module imports the app core.
// Run the frontend inside the watched process so every node --watch restart
// revalidates manifest-owned routes against the newly edited Msg union.
@@ -154,6 +163,7 @@ if (capabilities.size > 0 || permissions.size > 0 || (persistOk !== null && pers
servicesContract: true,
servicePackages,
sdkCorePath: sdkCore ?? undefined,
windowViews: windowViewsEnabled ? windowViews : undefined,
});
for (const error of checked.typeErrors) console.error(error);
for (const diagnostic of checked.diagnostics) console.error(formatDiagnostic(diagnostic));
+2 -2
View File
@@ -257,8 +257,8 @@ export const rules = {
NS1033: {
id: "NS1033",
title: "wiring exports match their runtime shapes",
fix: "Declare the channel exactly: `commandMsg(name: string)` / `keyMsg(key: KeyEvent)` / `frameMsg(model: Model, frame: FrameEvent)` / `pinchMsg(pinch: PinchEvent)` / `dropMsg(drop: FileDropEvent)` returning `Msg | null`; `themePack(model: Model): ThemePack`; singular `statusItem(model: Model): StatusItemState` or collection `statusItems(model: Model): readonly StatusItemDescriptor[]`; `appearanceMsg` / `chromeMsg` naming an arm with that channel's record shape; `envMsgs` entries targeting one-`Uint8Array`-field arms; and persistence ok/none routes naming void arms while err names a one-`Uint8Array`-field arm. Import canonical records from `@native-sdk/core/events`.",
why: "The generated wiring builds host events, persistence restore results, model-derived theme selection, and the live menu-bar status item structurally from your declarations at build time; a wrong shape would otherwise surface as a Zig compile error inside generated code instead of a teaching diagnostic here.",
fix: "Declare the channel exactly: `commandMsg(name: string)` / `keyMsg(key: KeyEvent)` / `frameMsg(model: Model, frame: FrameEvent)` / `pinchMsg(pinch: PinchEvent)` / `dropMsg(drop: FileDropEvent)` returning `Msg | null`; `themePack(model: Model): ThemePack`; singular `statusItem(model: Model): StatusItemState` or collection `statusItems(model: Model): readonly StatusItemDescriptor[]`; `windows(model: Model): readonly WindowDescriptor[]`, with each entry constructed by `windowDescriptor` and a literal `label: asciiBytes(\"name\")` matching `src/windows/name.native`; `appearanceMsg` / `chromeMsg` naming an arm with that channel's record shape; `envMsgs` entries targeting one-`Uint8Array`-field arms; and persistence ok/none routes naming void arms while err names a one-`Uint8Array`-field arm. Import canonical records from the SDK modules.",
why: "The generated wiring builds host events, persistence restore results, model-derived theme/status/window declarations, and their typed callbacks structurally at build time; a wrong shape would otherwise surface as a Zig compile error inside generated code instead of a teaching diagnostic here.",
class: "guarantee",
},
NS1034: {
+6
View File
@@ -49,6 +49,11 @@ export interface FrontendOptions {
/// Generated @native-sdk/core surface carrying declared SQLite query
/// constructors. Omitted for non-relational apps and direct checker tests.
readonly sdkCorePath?: string;
/// Statically compiled secondary-window view labels discovered from direct
/// `src/windows/<label>.native` children. Undefined keeps core-only checker
/// use independent of an app tree; an empty set means app validation is
/// active and the app supplies no secondary-window roots.
readonly windowViews?: readonly string[];
}
export interface FrontendResult {
@@ -204,6 +209,7 @@ export function checkFile(entry: string, options: FrontendOptions = {}): Fronten
options.permissions ?? [],
options.persistRoutes,
options.sdkCorePath,
options.windowViews,
);
const checkResult = checker.check();
if (checkResult.diagnostics.length > 0) {
+24
View File
@@ -549,6 +549,30 @@ export class IntInference {
}
}
}
// `windows(model)` returns host-consumed geometry. Those number slots
// are canvas points, so whole-valued descriptors must not accidentally
// specialize the canonical WindowDescriptor fields to i64 — another
// model may return fractional positions or dimensions through the same
// SDK record. Mark the descriptor's immediate number fields as boundary
// values, exactly like a host-constructed event record in reverse.
if (stmt.name?.text === "windows" && stmt.type) {
const returned = this.table.resolveTypeNode(stmt.type);
if (returned.k === "slice" && returned.elem.k === "struct") {
const descriptor = this.table.structs.get(returned.elem.name);
if (descriptor) {
for (const f of descriptor.fields) {
const numeric = f.type.k === "number" || (f.type.k === "optional" && f.type.inner.k === "number");
if (!numeric) continue;
const slot = this.slots.get(f.decl);
if (slot) {
slot.external = true;
slot.hostBoundary = true;
slot.proven = false;
}
}
}
}
}
}
}
+17 -1
View File
@@ -1,6 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import { asciiBytes, utf8Bytes } from "../sdk/core.ts";
import { asciiBytes, utf8Bytes, windowDescriptor } from "../sdk/core.ts";
test("asciiBytes returns exact ASCII and rejects Unicode", () => {
assert.deepEqual([...asciiBytes("app.refresh")], [...new TextEncoder().encode("app.refresh")]);
@@ -22,3 +22,19 @@ test("utf8Bytes matches TextEncoder for BMP, astral, and lone-surrogate text", (
assert.deepEqual([...utf8Bytes(value)], [...new TextEncoder().encode(value)], JSON.stringify(value));
}
});
test("windowDescriptor fills canonical window defaults", () => {
const descriptor = windowDescriptor({
label: asciiBytes("settings"),
canvasLabel: asciiBytes("settings-canvas"),
titlebar: "chromeless",
closePolicy: "hide",
});
assert.deepEqual([...descriptor.label], [...asciiBytes("settings")]);
assert.equal(descriptor.width, 480);
assert.equal(descriptor.height, 360);
assert.equal(descriptor.resizable, true);
assert.equal(descriptor.titlebar, "chromeless");
assert.equal(descriptor.closePolicy, "hide");
assert.equal(descriptor.onCloseCommand.length, 0);
});
+234
View File
@@ -1267,6 +1267,240 @@ export function statusItems(model: Model): readonly StatusItemDescriptor[] { ret
assert.ok(ruleIds(mutuallyExclusive).includes("NS1033"), `got ${ruleIds(mutuallyExclusive)}`);
});
test("NS1033 windows is the canonical model-declared secondary-window collection", () => {
const cleanSource = `
import { asciiBytes, windowDescriptor } from "@native-sdk/core";
import { type WindowDescriptor } from "@native-sdk/core/events";
export interface Model { readonly settingsOpen: boolean; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false }; }
export function update(model: Model, msg: Msg): Model { return model; }
export function windows(model: Model): readonly WindowDescriptor[] {
if (!model.settingsOpen) return [];
return [windowDescriptor({ label: asciiBytes("settings"), canvasLabel: asciiBytes("settings-canvas"), titlebar: "chromeless", transparent: true, closePolicy: "hide", onCloseCommand: asciiBytes("settings.closed") })];
}
`;
const clean = check(cleanSource, { windowViews: ["settings"] });
assert.equal(clean.ok, true, clean.diagnostics.map((d) => d.message).join("\n"));
assert.ok(!ruleIds(clean).includes("NS1033"), `got ${ruleIds(clean)}`);
const missingView = check(cleanSource, { windowViews: [] });
assert.equal(missingView.ok, false);
assert.match(missingView.diagnostics.map((d) => d.message).join("\n"), /src\/windows\/settings\.native/);
const mismatchedView = check(cleanSource, { windowViews: ["preferences"] });
assert.equal(mismatchedView.ok, false);
assert.match(mismatchedView.diagnostics.map((d) => d.message).join("\n"), /src\/windows\/settings\.native/);
const tooLongLabel = "a".repeat(65);
const oversized = check(cleanSource.replaceAll("settings", tooLongLabel), { windowViews: [tooLongLabel] });
assert.equal(oversized.ok, false);
assert.match(oversized.diagnostics.map((d) => d.message).join("\n"), /at most 64 ASCII bytes/);
const missingCommandMapper = check(`
import { asciiBytes, windowDescriptor } from "@native-sdk/core";
import { type WindowDescriptor } from "@native-sdk/core/events";
export interface Model { readonly settingsOpen: boolean; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false }; }
export function update(model: Model, msg: Msg): Model { return model; }
export function windows(model: Model): readonly WindowDescriptor[] {
if (!model.settingsOpen) return [];
return [windowDescriptor({
label: asciiBytes("settings"),
canvasLabel: asciiBytes("settings-canvas"),
onCloseCommand: asciiBytes("settings.closed"),
})];
}
`, { windowViews: ["settings"] });
assert.equal(missingCommandMapper.ok, false);
assert.match(missingCommandMapper.diagnostics.map((d) => d.message).join("\n"), /requires `commandMsg/);
const hiddenWithoutMapper = check(`
import { asciiBytes, windowDescriptor } from "@native-sdk/core";
import { type WindowDescriptor } from "@native-sdk/core/events";
export interface Model { readonly settingsOpen: boolean; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false }; }
export function update(model: Model, msg: Msg): Model { return model; }
export function windows(model: Model): readonly WindowDescriptor[] {
return model.settingsOpen ? [windowDescriptor({
label: asciiBytes("settings"),
canvasLabel: asciiBytes("settings-canvas"),
closePolicy: "hide",
onCloseCommand: asciiBytes("unused.while.hidden"),
})] : [];
}
`, { windowViews: ["settings"] });
assert.equal(hiddenWithoutMapper.ok, true, hiddenWithoutMapper.diagnostics.map((d) => d.message).join("\n"));
const rootTypeReexport = check(`
import { asciiBytes, windowDescriptor, type WindowDescriptor } from "@native-sdk/core";
export interface Model { readonly settingsOpen: boolean; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false }; }
export function update(model: Model, msg: Msg): Model { return model; }
export function windows(model: Model): readonly WindowDescriptor[] {
return [windowDescriptor({ label: asciiBytes("settings"), canvasLabel: asciiBytes("settings-canvas") })];
}
`, { windowViews: ["settings"] });
assert.equal(rootTypeReexport.ok, false);
assert.match(rootTypeReexport.typeErrors.join("\n"), /WindowDescriptor/);
const dynamicLabel = check(`
import { asciiBytes, windowDescriptor } from "@native-sdk/core";
import { type WindowDescriptor } from "@native-sdk/core/events";
export interface Model { readonly settingsOpen: boolean; readonly label: Uint8Array; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false, label: asciiBytes("settings") }; }
export function update(model: Model, msg: Msg): Model { return model; }
export function windows(model: Model): readonly WindowDescriptor[] {
if (!model.settingsOpen) return [];
return [windowDescriptor({ label: model.label, canvasLabel: asciiBytes("settings-canvas") })];
}
`, { windowViews: ["settings"] });
assert.equal(dynamicLabel.ok, false);
assert.match(dynamicLabel.diagnostics.map((d) => d.message).join("\n"), /static `asciiBytes/);
const spreadSpec = check(`
import { asciiBytes, windowDescriptor } from "@native-sdk/core";
import { type WindowDescriptor } from "@native-sdk/core/events";
export interface Model { readonly settingsOpen: boolean; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false }; }
export function update(model: Model, msg: Msg): Model { return model; }
export function windows(model: Model): readonly WindowDescriptor[] {
if (!model.settingsOpen) return [];
const common = { canvasLabel: asciiBytes("settings-canvas") };
return [windowDescriptor({ label: asciiBytes("settings"), ...common })];
}
`, { windowViews: ["settings"] });
assert.equal(spreadSpec.ok, false);
assert.match(spreadSpec.diagnostics.map((d) => d.message).join("\n"), /cannot spread fields after its literal label/);
const overwrittenLabel = check(`
import { asciiBytes, windowDescriptor } from "@native-sdk/core";
import { type WindowDescriptor } from "@native-sdk/core/events";
export interface Model { readonly settingsOpen: boolean; readonly label: Uint8Array; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false, label: asciiBytes("missing") }; }
export function update(model: Model, msg: Msg): Model { return model; }
export function windows(model: Model): readonly WindowDescriptor[] {
if (!model.settingsOpen) return [];
const base = windowDescriptor({ label: asciiBytes("settings"), canvasLabel: asciiBytes("settings-canvas") });
return [{ ...base, label: model.label }];
}
`, { windowViews: ["settings"] });
assert.equal(overwrittenLabel.ok, false);
assert.match(overwrittenLabel.diagnostics.map((d) => d.message).join("\n"), /flow directly from `windowDescriptor/);
const helperOverwrite = check(`
import { asciiBytes, windowDescriptor } from "@native-sdk/core";
import { type WindowDescriptor } from "@native-sdk/core/events";
export interface Model { readonly settingsOpen: boolean; readonly label: Uint8Array; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false, label: asciiBytes("missing") }; }
export function update(model: Model, msg: Msg): Model { return model; }
function unsafeWindow(model: Model): WindowDescriptor {
const base = windowDescriptor({ label: asciiBytes("settings"), canvasLabel: asciiBytes("settings-canvas") });
return { ...base, label: model.label };
}
export function windows(model: Model): readonly WindowDescriptor[] {
if (!model.settingsOpen) return [];
return [unsafeWindow(model)];
}
`, { windowViews: ["settings"] });
assert.equal(helperOverwrite.ok, false);
assert.match(helperOverwrite.diagnostics.map((d) => d.message).join("\n"), /flow directly from `windowDescriptor/);
const unrelatedConstructor = check(`
import { asciiBytes, windowDescriptor } from "@native-sdk/core";
import { type WindowDescriptor } from "@native-sdk/core/events";
export interface Model { readonly settingsOpen: boolean; readonly label: Uint8Array; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false, label: asciiBytes("missing") }; }
export function update(model: Model, msg: Msg): Model { return model; }
function unusedWindow(): WindowDescriptor {
return windowDescriptor({ label: asciiBytes("settings"), canvasLabel: asciiBytes("settings-canvas") });
}
export function windows(model: Model): readonly WindowDescriptor[] {
if (!model.settingsOpen) return [];
return [{ ...unusedWindow(), label: model.label }];
}
`, { windowViews: ["settings"] });
assert.equal(unrelatedConstructor.ok, false);
assert.match(unrelatedConstructor.diagnostics.map((d) => d.message).join("\n"), /flow directly from `windowDescriptor/);
const factored = check(`
import { asciiBytes, windowDescriptor } from "@native-sdk/core";
import { type WindowDescriptor } from "@native-sdk/core/events";
export interface Model { readonly settingsOpen: boolean; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false }; }
export function update(model: Model, msg: Msg): Model { return model; }
function settingsWindow(): WindowDescriptor {
return windowDescriptor({ label: asciiBytes("settings"), canvasLabel: asciiBytes("settings-canvas") });
}
export function windows(model: Model): readonly WindowDescriptor[] {
if (!model.settingsOpen) return [];
return [settingsWindow()];
}
`, { windowViews: ["settings"] });
assert.equal(factored.ok, true, factored.diagnostics.map((d) => d.message).join("\n"));
const constAlias = check(`
import { asciiBytes, windowDescriptor } from "@native-sdk/core";
import { type WindowDescriptor } from "@native-sdk/core/events";
export interface Model { readonly settingsOpen: boolean; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false }; }
export function update(model: Model, msg: Msg): Model { return model; }
export function windows(model: Model): readonly WindowDescriptor[] {
if (!model.settingsOpen) return [];
const common = { canvasLabel: asciiBytes("settings-canvas") };
const window = windowDescriptor({
...common,
label: asciiBytes("settings"),
});
return [window];
}
`, { windowViews: ["settings"] });
assert.equal(constAlias.ok, true, constAlias.diagnostics.map((d) => d.message).join("\n"));
const namespaced = checkFiles({
"core.ts": `
import * as shell from "./windows.ts";
import { type WindowDescriptor } from "@native-sdk/core/events";
export interface Model { readonly settingsOpen: boolean; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false }; }
export function update(model: Model, msg: Msg): Model { return model; }
export function windows(model: Model): readonly WindowDescriptor[] {
if (!model.settingsOpen) return [];
return [shell.settingsWindow()];
}
`,
"windows.ts": `
import { asciiBytes, windowDescriptor } from "@native-sdk/core";
import { type WindowDescriptor } from "@native-sdk/core/events";
export function settingsWindow(): WindowDescriptor {
return windowDescriptor({ label: asciiBytes("settings"), canvasLabel: asciiBytes("settings-canvas") });
}
`,
}, { windowViews: ["settings"] });
assert.equal(namespaced.ok, true, namespaced.diagnostics.map((d) => d.message).join("\n"));
const wrong = checkOnly(`
export interface BadWindow { readonly label: Uint8Array; readonly closePolicy: boolean; }
export interface Model { readonly settingsOpen: boolean; }
export type Msg = { readonly kind: "open" } | { readonly kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false }; }
export function update(model: Model, msg: Msg): Model { return model; }
export function windows(model: Model): readonly BadWindow[] { return []; }
`);
assert.ok(ruleIds(wrong).includes("NS1033"), `got ${ruleIds(wrong)}`);
});
test("NS1061: value-record aliases refuse the shapes value storage cannot carry", () => {
// The model root is reference storage by contract.
const root = checkOnly(`
+16
View File
@@ -221,6 +221,22 @@ export function statusItems(model: Model): readonly StatusItemDescriptor[] { ret
assert.ok(structs.includes("StatusItemModifiers"), `structs: ${structs.join(", ")}`);
});
test("windows is projected as a launcher-bound descriptor slice", () => {
const doc = contractOf(`
import { type WindowDescriptor } from "@native-sdk/core/events";
export interface Model { settingsOpen: boolean; }
export type Msg = { kind: "open" } | { kind: "closed" };
export function initialModel(): Model { return { settingsOpen: false }; }
export function update(model: Model, msg: Msg): Model { return model; }
export function windows(model: Model): readonly WindowDescriptor[] { return []; }
`);
const helpers = doc.model_helpers as { name: string; returns: unknown }[];
assert.deepEqual(helpers.map((helper) => helper.name), ["windows"]);
assert.deepEqual(doc.model_unbound, ["windows"]);
const structs = (doc.types as { structs: { name: string }[] }).structs.map((record) => record.name);
assert.ok(structs.includes("WindowDescriptor"), `structs: ${structs.join(", ")}`);
});
test("Cmd.fetch accepts a line-stream route with bytes and status arms", () => {
const doc = contractOf(`
import { Cmd, asciiBytes } from "@native-sdk/core";
@@ -31,7 +31,7 @@ if (process.env.SCRIPTC_TARGET !== "x86_64-windows-gnu") { console.error("wrong
if (!(process.env.PATH ?? "").startsWith(${JSON.stringify(zigDir)})) { console.error("zig directory missing from PATH front"); process.exit(9); }
const output = process.argv[process.argv.indexOf("-o") + 1];
fs.writeFileSync(output + ".lib.a", "target archive bytes");
fs.writeFileSync("core.contract.json", JSON.stringify({ build_id: "cross-target" }));
fs.writeFileSync("core.contract.json", JSON.stringify({ build_id: "cross-target", model_unbound: [], msg: { unbound: [] } }));
`);
const script = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "scripts", "run_external_core_compiler.mjs");
const archive = path.join(root, "libfixture_core.a");
@@ -216,7 +216,7 @@ if (process.env.SCRIPTC_CC !== "zigcc") { console.error("mobile compile missing
if (process.env.SCRIPTC_TARGET !== "aarch64-apple-ios-simulator") { console.error("mobile compile got SCRIPTC_TARGET=" + process.env.SCRIPTC_TARGET); process.exit(9); }
const output = process.argv[process.argv.indexOf("-o") + 1];
fs.writeFileSync(output + ".lib.a", "ios simulator archive bytes");
fs.writeFileSync("core.contract.json", JSON.stringify({ build_id: "ios-simulator" }));
fs.writeFileSync("core.contract.json", JSON.stringify({ build_id: "ios-simulator", model_unbound: [], msg: { unbound: [] } }));
`);
const archive = path.join(root, "libfixture_core.a");
const env = { ...process.env };
@@ -263,7 +263,7 @@ if (process.argv.includes("-v")) { console.log("0.0.29"); process.exit(0); }
if (process.env.SCRIPTC_CC !== undefined || process.env.SCRIPTC_TARGET !== undefined) { console.error("native compile received cross environment"); process.exit(9); }
const output = process.argv[process.argv.indexOf("-o") + 1];
fs.writeFileSync(output + ".lib.a", "native msvc archive bytes");
fs.writeFileSync("core.contract.json", JSON.stringify({ build_id: "native-msvc" }));
fs.writeFileSync("core.contract.json", JSON.stringify({ build_id: "native-msvc", model_unbound: [], msg: { unbound: [] } }));
`);
const script = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "scripts", "run_external_core_compiler.mjs");
const archive = path.join(root, "libfixture_core.a");