// Implemented locally rather than imported from @wrnexus/core: compiler modules // are bundled into the Node-only VS Code extension, which contains no other // packages, so a runtime import of core would break the editor compiler. const HTML_ESCAPES: Record = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; function escapeHtml(value: string): string { return value.replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]!); } const SAFE_ISLAND_NAME = /^[A-Za-z0-9_-]+$/; function isSafeIslandName(name: string): boolean { return SAFE_ISLAND_NAME.test(name); } export type IslandStrategy = "only" | "load" | "visible" | "idle"; export interface IslandDiagnostic { code: "WRN-ISLAND-PROPS"; message: string; severity: "error"; } const STRATEGIES: Record = { "client:only": "only", "client:load": "load", "client:visible": "visible", "client:idle": "idle", }; export function parseIslandStrategy(directives: string[]): IslandStrategy { for (const directive of directives) { const match = STRATEGIES[directive]; if (match) return match; } return "only"; } function unsupportedProp(value: unknown): boolean { const type = typeof value; if (type === "function" || type === "symbol" || type === "bigint" || type === "undefined") { return true; } if (value === null || type !== "object") return false; if (Array.isArray(value)) return (value as unknown[]).some(unsupportedProp); const proto = Object.getPrototypeOf(value); if (proto !== Object.prototype && proto !== null) return true; return Object.values(value as Record).some(unsupportedProp); } /** * Interprets an island attribute value with JSX semantics. * * `title="Revenue"` is a string, `start={3}` is a number, `flag` alone is * `true`. Without this every prop arrives as a string, so `start={3}` would be * `"3"` and arithmetic in the island silently concatenates. * * Returns `dynamic` for a `{…}` value that is not JSON: such expressions are * evaluated at runtime and cannot cross the serialization boundary. */ export function islandPropValue(raw: string | undefined): { value: unknown } | { dynamic: string } { if (raw === undefined || raw === "") return { value: true }; const expression = /^\{([\s\S]*)\}$/.exec(raw); if (!expression) return { value: raw }; const inner = expression[1]!.trim(); try { return { value: JSON.parse(inner) as unknown }; } catch { return { dynamic: inner }; } } export function serializeIslandProps( componentName: string, props: Record, ): { json: string } | { diagnostic: IslandDiagnostic } { const offenders = Object.entries(props) .filter(([, value]) => unsupportedProp(value)) .map(([key]) => key); if (offenders.length > 0) { return { diagnostic: { code: "WRN-ISLAND-PROPS", severity: "error", message: `Island '${componentName}' received non-serializable prop(s): ${offenders.join(", ")}. ` + `Island props cross a serialization boundary and must be JSON-safe ` + `(no functions, symbols, bigints, undefined, or class instances).`, }, }; } return { json: JSON.stringify(props) }; } export function renderIslandMarker(input: { name: string; strategy: IslandStrategy; propsJson: string; }): string { // The name becomes a path segment when the browser fetches // /__wrnexus/island/.js, so reuse the framework's conservative charset // rather than relying on escaping alone. if (!isSafeIslandName(input.name)) { throw new Error( `Island name '${input.name}' is not a safe identifier. ` + `Island names may only contain letters, digits, underscores, and hyphens.`, ); } return ( `
` ); } /** * Local binding names introduced by island imports. * * Codegen sees only component tag names, so it needs the set of names that came * from `.tsx` imports to tell an island apart from a `.wrn` component. */ export function islandNamesFrom( imports: Array<{ kind?: "island"; declaration: { defaultImport?: string } }>, ): Set { const names = new Set(); for (const entry of imports) { if (entry.kind !== "island") continue; const local = entry.declaration.defaultImport; if (local) names.add(local); } return names; }