@typespec/compiler
Version:
TypeSpec Compiler Preview
283 lines • 10.8 kB
JavaScript
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