import-plan.ts (1745B)
1 import type { ModuleStage } from './module-stage.js'; 2 import { sourceText } from './compiler.js'; 3 export interface ImportPlan { source: string; statement: string; existing?: { alias: string; start: number; end: number }; } 4 // Header meaning comes from the native compiler. Insert at the beginning, which 5 // preserves every original character and keeps the directive in the header. 6 export function importPlan(source: string, stage: ModuleStage, manifest: string, alias: string): ImportPlan { 7 if (!/^[a-f0-9]{64}$/u.test(manifest)) throw new Error('Invalid module manifest'); 8 if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(alias) || alias.length > 128) throw new Error('Use a namespace starting with a letter or underscore, followed by letters, digits or underscores.'); 9 const statement = `!import ${manifest} as ${alias}\n`; 10 const existing = stage.imports.find(([ref]) => ref === manifest); 11 if (existing) { 12 // Locate a display selection in the compiler-masked header; this has no role 13 // in parsing or compiling source. Fall back to selecting the whole source. 14 let offset = 0, range = { start: 0, end: source.length }; 15 const masked = stage.body.split('\n'); 16 for (const [i, line] of source.split('\n').entries()) { 17 if (line !== masked[i] && line.trimStart().startsWith('!import ') && line.includes(manifest)) { range = { start: offset, end: offset + line.length }; break; } 18 offset += line.length + 1; 19 } 20 return { source, statement, existing: { alias: existing[1], ...range } }; 21 } 22 if (stage.imports.some(([, name]) => name === alias)) throw new Error(`Namespace ${alias} is already used by another import.`); 23 const next = statement + source; 24 sourceText(next); 25 return { source: next, statement }; 26 }