···
const __dirname = dirname(fileURLToPath(import.meta.url));
+
// Get all tsp and json files
+
function getAllFiles(dir, baseDir = dir) {
const entries = readdirSync(dir);
···
const stat = statSync(fullPath);
if (stat.isDirectory()) {
+
files.push(...getAllFiles(fullPath, baseDir));
+
} else if (entry.endsWith(".tsp") || entry.endsWith(".json")) {
files.push(relative(baseDir, fullPath));
···
+
// Extract all refs from JSON (recursively search for strings with #)
+
function extractRefsFromJson(obj, refs = new Map()) {
+
if (typeof obj === "string") {
+
// Match pattern like "foo.bar#baz" or "foo.barCamel#baz" (must have # to be a ref)
+
const match = obj.match(/^([a-z][a-zA-Z.]+)#([a-z][a-zA-Z]*)$/);
+
const modelName = def.charAt(0).toUpperCase() + def.slice(1);
+
refs.set(ns, new Set());
+
refs.get(ns).add(modelName);
+
// Also match plain namespace refs like "foo.bar.baz" or "foo.bar.bazCamel" (must have at least 2 dots)
+
const nsMatch = obj.match(/^([a-z][a-zA-Z]*(?:\.[a-z][a-zA-Z]*){2,})$/);
+
refs.set(ns, new Set());
+
refs.get(ns).add("Main");
+
} else if (Array.isArray(obj)) {
+
for (const item of obj) {
+
extractRefsFromJson(item, refs);
+
} else if (obj && typeof obj === "object") {
+
for (const value of Object.values(obj)) {
+
extractRefsFromJson(value, refs);
+
const integrationDir = join(__dirname, "../../emitter/test/integration");
+
// Get all test suite directories
+
const testSuites = readdirSync(integrationDir).filter((name) => {
+
const fullPath = join(integrationDir, name);
+
return statSync(fullPath).isDirectory() && !name.startsWith(".");
+
// Build lexicons with refs extracted from JSON
+
const lexicons = new Map(); // namespace -> { file, content, refs, suite }
+
// Process all test suites
+
for (const suite of testSuites) {
+
const inputDir = join(integrationDir, suite, "input");
+
const outputDir = join(integrationDir, suite, "output");
+
const inputFiles = getAllFiles(inputDir).filter((f) => f.endsWith(".tsp"));
+
for (const file of inputFiles) {
+
const fullPath = join(inputDir, file);
+
const content = readFileSync(fullPath, "utf-8");
+
const namespace = file.replace(/\.tsp$/, "").replace(/\//g, ".");
+
// Find corresponding JSON output
+
const jsonFile = file.replace(/\.tsp$/, ".json");
+
const jsonPath = join(outputDir, jsonFile);
+
const jsonContent = readFileSync(jsonPath, "utf-8");
+
const jsonData = JSON.parse(jsonContent);
+
const refs = extractRefsFromJson(jsonData);
+
lexicons.set(namespace, { file, content, refs, suite });
+
// TypeSpec reserved keywords that need escaping
+
const TYPESPEC_KEYWORDS = new Set([
+
// Escape a namespace part if it's a reserved keyword
+
function escapeNamespacePart(part) {
+
return TYPESPEC_KEYWORDS.has(part) ? `\`${part}\`` : part;
+
// Escape a full namespace path
+
function escapeNamespace(namespace) {
+
return namespace.split(".").map(escapeNamespacePart).join(".");
+
// Get the JSON for a lexicon to check its definitions
+
function getLexiconJson(namespace) {
+
const lexicon = lexicons.get(namespace);
+
if (!lexicon) return null;
+
lexicon.file.replace(".tsp", ".json"),
+
return JSON.parse(readFileSync(jsonPath, "utf-8"));
+
// Check if a definition in JSON is a token
+
function isToken(lexiconJson, defName) {
+
if (!lexiconJson || !lexiconJson.defs) return false;
+
const def = lexiconJson.defs[defName];
+
return def && def.type === "token";
+
// Bundle a lexicon with stubs for referenced types (from JSON)
+
function bundleLexicon(namespace) {
+
const mainLexicon = lexicons.get(namespace);
+
if (!mainLexicon) return "";
+
let bundled = mainLexicon.content;
+
// Add stubs from refs extracted from JSON output (excluding self-references)
+
if (mainLexicon.refs.size > 0) {
+
let hasExternalRefs = false;
+
for (const [ns] of mainLexicon.refs) {
+
if (ns !== namespace) {
+
hasExternalRefs = true;
+
bundled += "\n// --- Externals ---\n";
+
for (const [ns, models] of mainLexicon.refs) {
+
// Skip if this is the current namespace
+
if (ns === namespace) continue;
+
// Get the JSON for this referenced namespace to check for tokens
+
const refJson = getLexiconJson(ns);
+
const escapedNs = escapeNamespace(ns);
+
bundled += `\n@external\nnamespace ${escapedNs} {\n`;
+
for (const model of models) {
+
// Check if this definition exists in the JSON and is a token
+
const defName = model.charAt(0).toLowerCase() + model.slice(1);
+
if (refJson && isToken(refJson, defName)) {
+
bundled += ` @token model ${model} { }\n`;
+
bundled += ` model ${model} { }\n`;