UNPKG

@typespec/compiler

Version:

TypeSpec Compiler Preview

283 lines 10.8 kB
import assert from "assert"; import { readdir, readFile, stat } from "fs/promises"; import { globby } from "globby"; import { join } from "path"; import { fileURLToPath, pathToFileURL } from "url"; import { logDiagnostics, logVerboseTestOutput } from "../core/diagnostics.js"; import { createLogger } from "../core/logger/logger.js"; import { NodeHost } from "../core/node-host.js"; import { getAnyExtensionFromPath, resolvePath } from "../core/path-utils.js"; import { compile as compileProgram } from "../core/program.js"; import { createSourceFile, getSourceFileKindFromExt } from "../index.js"; import { createStringMap } from "../utils/misc.js"; import { expectDiagnosticEmpty } from "./expect.js"; import { createTestWrapper, findTestPackageRoot, resolveVirtualPath } from "./test-utils.js"; import { TestHostError, } from "./types.js"; function createTestCompilerHost(virtualFs, jsImports, options) { const libDirs = [resolveVirtualPath(".tsp/lib/std")]; if (!options?.excludeTestLib) { libDirs.push(resolveVirtualPath(".tsp/test-lib")); } return { async readUrl(url) { const contents = virtualFs.get(url); if (contents === undefined) { throw new TestHostError(`File ${url} not found.`, "ENOENT"); } return createSourceFile(contents, url); }, async readFile(path) { path = resolveVirtualPath(path); const contents = virtualFs.get(path); if (contents === undefined) { throw new TestHostError(`File ${path} not found.`, "ENOENT"); } return createSourceFile(contents, path); }, async writeFile(path, content) { path = resolveVirtualPath(path); virtualFs.set(path, content); }, async readDir(path) { path = resolveVirtualPath(path); const fileFolder = [...virtualFs.keys()] .filter((x) => x.startsWith(`${path}/`)) .map((x) => x.replace(`${path}/`, "")) .map((x) => { const index = x.indexOf("/"); return index !== -1 ? x.substring(0, index) : x; }); return [...new Set(fileFolder)]; }, async rm(path, options) { path = resolveVirtualPath(path); if (options.recursive && !virtualFs.has(path)) { for (const key of virtualFs.keys()) { if (key.startsWith(`${path}/`)) { virtualFs.delete(key); } } } else { virtualFs.delete(path); } }, getLibDirs() { return libDirs; }, getExecutionRoot() { return resolveVirtualPath(".tsp"); }, async getJsImport(path) { path = resolveVirtualPath(path); const module = jsImports.get(path); if (module === undefined) { throw new TestHostError(`Module ${path} not found`, "ERR_MODULE_NOT_FOUND"); } return module; }, async stat(path) { path = resolveVirtualPath(path); if (virtualFs.has(path)) { return { isDirectory() { return false; }, isFile() { return true; }, }; } for (const fsPath of virtualFs.keys()) { if (fsPath.startsWith(path) && fsPath !== path) { return { isDirectory() { return true; }, isFile() { return false; }, }; } } throw new TestHostError(`File ${path} not found`, "ENOENT"); }, // symlinks not supported in test-host async realpath(path) { return path; }, getSourceFileKind: getSourceFileKindFromExt, logSink: { log: NodeHost.logSink.log }, mkdirp: async (path) => path, fileURLToPath, pathToFileURL(path) { return pathToFileURL(path).href; }, ...options?.compilerHostOverrides, }; } export async function createTestFileSystem(options) { const virtualFs = createStringMap(!!options?.caseInsensitiveFileSystem); const jsImports = createStringMap(!!options?.caseInsensitiveFileSystem); const compilerHost = createTestCompilerHost(virtualFs, jsImports, options); return { addTypeSpecFile, addJsFile, addRealTypeSpecFile, addRealJsFile, addRealFolder, addTypeSpecLibrary, compilerHost, fs: virtualFs, }; function addTypeSpecFile(path, contents) { virtualFs.set(resolveVirtualPath(path), contents); } function addJsFile(path, contents) { const key = resolveVirtualPath(path); virtualFs.set(key, ""); // don't need contents jsImports.set(key, new Promise((r) => r(contents))); } async function addRealTypeSpecFile(path, existingPath) { virtualFs.set(resolveVirtualPath(path), await readFile(existingPath, "utf8")); } async function addRealFolder(folder, existingFolder) { const entries = await readdir(existingFolder); for (const entry of entries) { const existingPath = join(existingFolder, entry); const virtualPath = join(folder, entry); const s = await stat(existingPath); if (s.isFile()) { if (existingPath.endsWith(".js")) { await addRealJsFile(virtualPath, existingPath); } else { await addRealTypeSpecFile(virtualPath, existingPath); } } if (s.isDirectory()) { await addRealFolder(virtualPath, existingPath); } } } async function addRealJsFile(path, existingPath) { const key = resolveVirtualPath(path); const exports = await import(pathToFileURL(existingPath).href); virtualFs.set(key, ""); jsImports.set(key, exports); } async function addTypeSpecLibrary(testLibrary) { for (const { realDir, pattern, virtualPath } of testLibrary.files) { const lookupDir = resolvePath(testLibrary.packageRoot, realDir); const entries = await findFilesFromPattern(lookupDir, pattern); for (const entry of entries) { const fileRealPath = resolvePath(lookupDir, entry); const fileVirtualPath = resolveVirtualPath(virtualPath, entry); switch (getAnyExtensionFromPath(fileRealPath)) { case ".tsp": case ".json": const contents = await readFile(fileRealPath, "utf-8"); addTypeSpecFile(fileVirtualPath, contents); break; case ".js": case ".mjs": await addRealJsFile(fileVirtualPath, fileRealPath); break; } } } } } export const StandardTestLibrary = { name: "@typespec/compiler", packageRoot: await findTestPackageRoot(import.meta.url), files: [ { virtualPath: "./.tsp/dist/src/lib", realDir: "./dist/src/lib", pattern: "**" }, { virtualPath: "./.tsp/lib", realDir: "./lib", pattern: "**" }, ], }; export async function createTestHost(config = {}) { const testHost = await createTestHostInternal(); await testHost.addTypeSpecLibrary(StandardTestLibrary); if (config.libraries) { for (const library of config.libraries) { await testHost.addTypeSpecLibrary(library); } } return testHost; } export async function createTestRunner(host) { const testHost = host ?? (await createTestHost()); return createTestWrapper(testHost); } async function createTestHostInternal() { let program; const libraries = []; const testTypes = {}; const fileSystem = await createTestFileSystem(); // add test decorators fileSystem.addTypeSpecFile(".tsp/test-lib/main.tsp", 'import "./test.js";'); fileSystem.addJsFile(".tsp/test-lib/test.js", { namespace: "TypeSpec", $test(_, target, nameLiteral) { let name = nameLiteral?.value; if (!name) { if (target.kind === "Model" || target.kind === "Scalar" || target.kind === "Namespace" || target.kind === "Enum" || target.kind === "Operation" || target.kind === "ModelProperty" || target.kind === "EnumMember" || target.kind === "Interface" || (target.kind === "Union" && !target.expression)) { name = target.name; } else { throw new Error("Need to specify a name for test type"); } } testTypes[name] = target; }, }); return { ...fileSystem, addTypeSpecLibrary: async (lib) => { if (lib !== StandardTestLibrary) { libraries.push(lib); } await fileSystem.addTypeSpecLibrary(lib); }, compile, diagnose, compileAndDiagnose, testTypes, libraries, get program() { assert(program, "Program cannot be accessed without calling compile, diagnose, or compileAndDiagnose."); return program; }, }; async function compile(main, options = {}) { const [testTypes, diagnostics] = await compileAndDiagnose(main, options); expectDiagnosticEmpty(diagnostics); return testTypes; } async function diagnose(main, options = {}) { const [, diagnostics] = await compileAndDiagnose(main, options); return diagnostics; } async function compileAndDiagnose(mainFile, options = {}) { const p = await compileProgram(fileSystem.compilerHost, resolveVirtualPath(mainFile), options); program = p; logVerboseTestOutput((log) => logDiagnostics(p.diagnostics, createLogger({ sink: fileSystem.compilerHost.logSink }))); return [testTypes, p.diagnostics]; } } export async function findFilesFromPattern(directory, pattern) { return globby(pattern, { cwd: directory, onlyFiles: true, }); } //# sourceMappingURL=test-host.js.map