UNPKG

@typespec/compiler

Version:

TypeSpec compiler and standard library

393 lines 15.4 kB
import { realpath } from "fs/promises"; import { pathToFileURL } from "url"; import { compilerAssert } from "../core/diagnostics.js"; import { getEntityName } from "../core/helpers/type-name-utils.js"; import { NodeHost } from "../core/node-host.js"; import { getIdentifierContext, getNodeAtPosition } from "../core/parser.js"; import { getRelativePathFromDirectory, joinPaths, resolvePath } from "../core/path-utils.js"; import { compile as coreCompile } from "../core/program.js"; import { createSourceLoader } from "../core/source-loader.js"; import { NoTarget } from "../core/types.js"; import { resolveModule } from "../module-resolver/module-resolver.js"; import { NodePackageResolver } from "../module-resolver/node-package-resolver.js"; import { parseNodeModuleSpecifier } from "../module-resolver/utils.js"; import { $ } from "../typekit/index.js"; import { expectDiagnosticEmpty } from "./expect.js"; import { extractMarkers } from "./fourslash.js"; import { createTestFileSystem } from "./fs.js"; import { TemplateWithMarkers } from "./marked-template.js"; import { StandardTestLibrary, addTestLib } from "./test-compiler-host.js"; import { resolveVirtualPath } from "./test-utils.js"; export function createTester(base, options) { return createTesterInternal({ fs: once(() => createTesterFs(base, options)), libraries: options.libraries, }); } function once(fn) { let load; return () => { if (load) return load; load = fn(); return load; }; } async function createTesterFs(base, options) { const fs = createTestFileSystem(); const host = options.host ?? { ...NodeHost, // We want to keep the original path in the file map but we do still want to resolve the full path when loading JS to prevent duplicate imports. realpath: async (x) => x, getJsImport: async (path) => { return await import(pathToFileURL(await realpath(path)).href); }, }; const sl = await createSourceLoader(host); const selfName = JSON.parse((await host.readFile(resolvePath(base, "package.json"))).text).name; const moduleHost = { realpath: async (x) => x, stat: host.stat, readFile: async (path) => { const file = await host.readFile(path); return file.text; }, }; const nodePackageResolver = new NodePackageResolver(moduleHost); for (const lib of options.libraries) { const specifier = parseNodeModuleSpecifier(lib); if (specifier === null) { throw new Error(`Library imports must be bare module specifiers. Got: ${lib}`); } if (specifier.subPath !== "") { // eslint-disable-next-line no-console console.warn(`Warning: Defining a subpath '${lib}' of a library is unnecessary. Just import the library. Ignoring the subpath in '${lib}'`); continue; } const pkg = await nodePackageResolver.resolve(specifier.packageName, base); for (const key of Object.keys(pkg?.exports ?? {})) { const spec = resolvePath(specifier.packageName, key); await sl.importPath(spec, NoTarget, base); } // We also need to load the library js entrypoint for emitters and linters. const resolved = await resolveModule(moduleHost, lib, { baseDir: base, conditions: ["import", "default"], }); if (resolved.type === "module") { const virtualPath = computeRelativePath(lib, resolved.mainFile); fs.addJsFile(virtualPath, await host.getJsImport(resolved.mainFile)); fs.add(resolvePath("node_modules", lib, "package.json"), resolved.manifest.file.text); // Mount the library's own `tspconfig.yaml` (if any) so that features it opts into // (e.g. `auto-decorators`) are honored when compiling against the virtual file system. const tspconfigPath = resolvePath(resolved.path, "tspconfig.yaml"); try { const tspconfig = await host.readFile(tspconfigPath); fs.add(resolvePath("node_modules", lib, "tspconfig.yaml"), tspconfig.text); } catch { // No library tspconfig.yaml; nothing to mount. } } } await fs.addTypeSpecLibrary(StandardTestLibrary); function computeVirtualPath(file) { const context = sl.resolution.locationContexts.get(file); compilerAssert(context?.type === "library", `Unexpected: all source files should be in a library but ${file.path} was in '${context?.type}'`); return computeRelativePath(context.metadata.name, file.path); } function computeRelativePath(libName, realPath) { const relativePath = getRelativePathFromDirectory(base, realPath, false); if (libName === selfName) { return joinPaths("node_modules", selfName, relativePath); } else { return relativePath; } } for (const file of sl.resolution.sourceFiles.values()) { const relativePath = computeVirtualPath(file.file); fs.add(resolveVirtualPath(relativePath), file.file.text); } for (const file of sl.resolution.jsSourceFiles.values()) { const relativePath = computeVirtualPath(file.file); fs.addJsFile(resolveVirtualPath(relativePath), file.esmExports); } for (const [path, lib] of sl.resolution.loadedLibraries) { fs.add(resolvePath("node_modules", path, "package.json"), lib.manifest.file.text); } fs.freeze(); return fs; } function createTesterBuilder(params, create) { return { files, wrap, importLibraries, import: importFn, using, }; function files(files) { const fs = async () => { const fs = (await params.fs()).clone(); for (const [name, value] of Object.entries(files)) { fs.add(name, value); } fs.freeze(); return fs; }; return create({ ...params, fs, }); } function wrap(fn) { return create({ ...params, wraps: [...(params.wraps ?? []), fn], }); } function importLibraries() { return create({ ...params, imports: [...(params.imports ?? []), ...params.libraries], }); } function importFn(...imports) { return create({ ...params, imports: [...(params.imports ?? []), ...imports], }); } function using(...usings) { return create({ ...params, usings: [...(params.usings ?? []), ...usings], }); } } function createTesterInternal(params) { return { ...createCompilable(async (...args) => { const instance = await createTesterInstance(params); return instance.compileAndDiagnose(...args); }), ...createTesterBuilder(params, createTesterInternal), emit, createInstance, }; function emit(emitter, options) { return createEmitterTesterInternal({ ...params, emitter, compilerOptions: options ? { ...params.compilerOptions, options: { ...params.compilerOptions?.options, [emitter]: options, }, } : params.compilerOptions, }); } function createInstance() { return createTesterInstance(params); } } function createEmitterTesterInternal(params) { return { ...createCompilable(async (...args) => { const instance = await createEmitterTesterInstance(params); return instance.compileAndDiagnose(...args); }), ...createTesterBuilder(params, createEmitterTesterInternal), pipe: (cb) => { return createEmitterTesterInternal({ ...params, outputProcess: async (result) => { return params.outputProcess ? cb(params.outputProcess(result)) : cb(result); }, }); }, createInstance: () => createEmitterTesterInstance(params), }; } async function createEmitterTesterInstance(params) { const tester = await createTesterInstance(params); return { fs: tester.fs, ...createCompilable(compileAndDiagnose), get program() { return tester.program; }, get $() { return tester.$; }, }; async function compileAndDiagnose(code, options) { if (options?.compilerOptions?.emit !== undefined) { throw new Error("Cannot set emit in options."); } const resolvedOptions = { ...options, compilerOptions: { ...params.compilerOptions, ...options?.compilerOptions, outputDir: "tsp-output", emit: [params.emitter], }, }; const [result, diagnostics] = await tester.compileAndDiagnose(code, resolvedOptions); const outputs = {}; const outputDir = resolvedOptions.compilerOptions?.options?.[params.emitter]?.["emitter-output-dir"] ?? resolveVirtualPath(resolvePath("tsp-output", params.emitter)); for (const [name, value] of result.fs.fs) { if (name.startsWith(outputDir)) { const relativePath = name.slice(outputDir.length + 1); outputs[relativePath] = value; } } const prep = { ...result, outputs, }; const final = params.outputProcess ? params.outputProcess(prep) : prep; return [final, diagnostics]; } } async function createTesterInstance(params) { let savedProgram; let saved$; const fs = (await params.fs()).clone(); return { ...createCompilable(compileAndDiagnose), fs, get program() { if (!savedProgram) { throw new Error("Program not initialized. Call compile first."); } return savedProgram; }, get $() { if (!saved$) { throw new Error("Typekit not initialized. Call compile first."); } return saved$; }, }; function applyWraps(code, wraps) { for (const wrap of wraps) { code = wrap(code); } return code; } function addCode(fs, code) { const markerPositions = []; const markerConfigs = {}; function addTsp(filename, value) { const codeStr = TemplateWithMarkers.is(value) ? value.code : value; const actualCode = filename === "main.tsp" ? wrapMain(codeStr) : codeStr; const markers = extractMarkers(actualCode); for (const marker of markers) { markerPositions.push({ ...marker, filename }); } if (TemplateWithMarkers.is(value)) { for (const [markerName, markerConfig] of Object.entries(value.markers)) { if (markerConfig) { markerConfigs[markerName] = markerConfig; } } } fs.addTypeSpecFile(filename, actualCode); } const files = typeof code === "string" || TemplateWithMarkers.is(code) ? { "main.tsp": code } : code; for (const [name, value] of Object.entries(files)) { addTsp(name, value); } return { markerPositions, markerConfigs }; } function wrapMain(code) { const imports = (params.imports ?? []).map((x) => `import "${x}";`); const usings = (params.usings ?? []).map((x) => `using ${x};`); const actualCode = [ ...imports, ...usings, params.wraps ? applyWraps(code, params.wraps) : code, ].join("\n"); return actualCode; } async function compileAndDiagnose(code, options) { const typesCollected = addTestLib(fs); const { markerPositions, markerConfigs } = addCode(fs, code); const program = await coreCompile(fs.compilerHost, resolveVirtualPath("main.tsp"), options?.compilerOptions); savedProgram = program; saved$ = $(program); const entities = extractMarkedEntities(program, markerPositions, markerConfigs); return [ { program, $: saved$, fs, pos: Object.fromEntries(markerPositions.map((x) => [x.name, x])), ...typesCollected, ...entities, }, program.diagnostics, ]; } } function extractMarkedEntities(program, markerPositions, markerConfigs) { const entities = {}; for (const marker of markerPositions) { const file = program.sourceFiles.get(resolveVirtualPath(marker.filename)); if (!file) { throw new Error(`Couldn't find ${resolveVirtualPath(marker.filename)} in program`); } const { name, pos } = marker; const markerConfig = markerConfigs[name]; const node = getNodeAtPosition(file, pos); if (!node) { throw new Error(`Could not find node at ${pos}`); } const { node: contextNode } = getIdentifierContext(node); if (contextNode === undefined) { throw new Error(`Could not find context node for ${name} at ${pos}. File content: ${file.file.text}`); } const entity = program.checker.getTypeOrValueForNode(contextNode); if (entity === null) { throw new Error(`Expected ${name} to be of entity kind ${markerConfig?.entityKind} but got null (Means a value failed to resolve) at ${pos}`); } if (markerConfig) { const { entityKind, kind, valueKind } = markerConfig; if (entity.entityKind !== entityKind) { throw new Error(`Expected ${name} to be of entity kind ${entityKind} but got (${entity?.entityKind}) ${getEntityName(entity)} at ${pos}`); } if (entity.entityKind === "Type" && kind !== undefined && entity.kind !== kind) { throw new Error(`Expected ${name} to be of kind ${kind} but got (${entity.kind}) ${getEntityName(entity)} at ${pos}`); } else if (entity?.entityKind === "Value" && valueKind !== undefined && entity.valueKind !== valueKind) { throw new Error(`Expected ${name} to be of value kind ${valueKind} but got (${entity.valueKind}) ${getEntityName(entity)} at ${pos}`); } } entities[name] = entity; } return entities; } function createCompilable(fn) { return { compileAndDiagnose: fn, compile: async (...args) => { const [result, diagnostics] = await fn(...args); expectDiagnosticEmpty(diagnostics); return result; }, diagnose: async (...args) => { const [_, diagnostics] = await fn(...args); return diagnostics; }, }; } //# sourceMappingURL=tester.js.map