Extract source resolution logic into its own source loader (#4324)
This will allow other tools to be able to reuse the typespec compiler source resolution logic(Figure out all the imported files) without doing a full compile and cleans up the program.ts which is doing a lot of things. Usage ```ts const loader = createSourceLoader(host); loader.importFile(resolvePath(cwd, "main.tsp", {type: "project"})); loader.importPath("./foo.tsp", NoTarget, cwd, {type: "project"} ); loader.resolution.sourceFiles // Tsp source files loaded loader.resolution.jsSourceFiles // Js source file loaded ```
This commit is contained in:
Родитель
dde8dc0ca7
Коммит
d2ac995842
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
|
||||
changeKind: fix
|
||||
packages:
|
||||
- "@typespec/compiler"
|
||||
---
|
||||
|
||||
API: Extract source resolution logic into its own source loader
|
|
@ -2,23 +2,12 @@ import { EmitterOptions } from "../config/types.js";
|
|||
import { createAssetEmitter } from "../emitter-framework/asset-emitter.js";
|
||||
import { validateEncodedNamesConflicts } from "../lib/encoded-names.js";
|
||||
import { MANIFEST } from "../manifest.js";
|
||||
import {
|
||||
deepEquals,
|
||||
doIO,
|
||||
findProjectRoot,
|
||||
isDefined,
|
||||
mapEquals,
|
||||
mutate,
|
||||
resolveTspMain,
|
||||
} from "../utils/misc.js";
|
||||
import { deepEquals, findProjectRoot, isDefined, mapEquals, mutate } from "../utils/misc.js";
|
||||
import { createBinder } from "./binder.js";
|
||||
import { Checker, createChecker } from "./checker.js";
|
||||
import { createSuppressCodeFix } from "./compiler-code-fixes/suppress.codefix.js";
|
||||
import { compilerAssert } from "./diagnostics.js";
|
||||
import {
|
||||
resolveTypeSpecEntrypoint,
|
||||
resolveTypeSpecEntrypointForDir,
|
||||
} from "./entrypoint-resolution.js";
|
||||
import { resolveTypeSpecEntrypoint } from "./entrypoint-resolution.js";
|
||||
import { ExternalError } from "./external-error.js";
|
||||
import { getLibraryUrlsLoaded } from "./library.js";
|
||||
import { createLinter, resolveLinterDefinition } from "./linter.js";
|
||||
|
@ -33,15 +22,14 @@ import {
|
|||
resolveModule,
|
||||
} from "./module-resolver.js";
|
||||
import { CompilerOptions } from "./options.js";
|
||||
import { isImportStatement, parse, parseStandaloneTypeReference } from "./parser.js";
|
||||
import { parse, parseStandaloneTypeReference } from "./parser.js";
|
||||
import { getDirectoryPath, joinPaths, resolvePath } from "./path-utils.js";
|
||||
import { createProjector } from "./projector.js";
|
||||
import { createSourceFile } from "./source-file.js";
|
||||
import { SourceLoader, SourceResolution, createSourceLoader, loadJsFile } from "./source-loader.js";
|
||||
import { StateMap, StateSet, createStateAccessors } from "./state-accessors.js";
|
||||
import {
|
||||
CompilerHost,
|
||||
Diagnostic,
|
||||
DiagnosticTarget,
|
||||
Directive,
|
||||
DirectiveExpressionNode,
|
||||
EmitContext,
|
||||
|
@ -56,7 +44,6 @@ import {
|
|||
Namespace,
|
||||
NoTarget,
|
||||
Node,
|
||||
NodeFlags,
|
||||
ProjectionApplication,
|
||||
Projector,
|
||||
SourceFile,
|
||||
|
@ -150,12 +137,10 @@ export async function compile(
|
|||
const stateMaps = new Map<symbol, StateMap>();
|
||||
const stateSets = new Map<symbol, StateSet>();
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
const seenSourceFiles = new Set<string>();
|
||||
const duplicateSymbols = new Set<Sym>();
|
||||
const emitters: EmitterRef[] = [];
|
||||
const requireImports = new Map<string, string>();
|
||||
const loadedLibraries = new Map<string, TypeSpecLibraryReference>();
|
||||
const sourceFileLocationContexts = new WeakMap<SourceFile, LocationContext>();
|
||||
let sourceResolution: SourceResolution;
|
||||
let error = false;
|
||||
let continueToNextStage = true;
|
||||
|
||||
|
@ -201,27 +186,12 @@ export async function compile(
|
|||
}
|
||||
const binder = createBinder(program);
|
||||
|
||||
await loadIntrinsicTypes();
|
||||
if (!options?.nostdlib) {
|
||||
await loadStandardLibrary();
|
||||
}
|
||||
|
||||
// Load additional imports prior to compilation
|
||||
if (resolvedMain && options.additionalImports) {
|
||||
const importScript = options.additionalImports.map((i) => `import "${i}";`).join("\n");
|
||||
const sourceFile = createSourceFile(
|
||||
importScript,
|
||||
joinPaths(getDirectoryPath(resolvedMain), `__additional_imports`)
|
||||
);
|
||||
sourceFileLocationContexts.set(sourceFile, { type: "project" });
|
||||
await loadTypeSpecScript(sourceFile);
|
||||
}
|
||||
|
||||
if (resolvedMain) {
|
||||
await loadMain(resolvedMain);
|
||||
} else {
|
||||
if (resolvedMain === undefined) {
|
||||
return program;
|
||||
}
|
||||
await checkForCompilerVersionMismatch(resolvedMain);
|
||||
|
||||
await loadSources(resolvedMain);
|
||||
|
||||
const basedir = getDirectoryPath(resolvedMain);
|
||||
|
||||
|
@ -292,7 +262,7 @@ export async function compile(
|
|||
}
|
||||
}
|
||||
|
||||
const libraries = new Map([...loadedLibraries.entries()]);
|
||||
const libraries = new Map([...sourceResolution.loadedLibraries.entries()]);
|
||||
const incompatibleLibraries = new Map<string, TypeSpecLibraryReference[]>();
|
||||
for (const root of loadedRoots) {
|
||||
const packageJsonPath = joinPaths(root, "package.json");
|
||||
|
@ -328,105 +298,58 @@ export async function compile(
|
|||
}
|
||||
}
|
||||
|
||||
async function loadIntrinsicTypes() {
|
||||
async function loadSources(entrypoint: string) {
|
||||
const sourceLoader = await createSourceLoader(host, {
|
||||
parseOptions: options.parseOptions,
|
||||
getCachedScript: (file) =>
|
||||
oldProgram?.sourceFiles.get(file.path) ?? host.parseCache?.get(file),
|
||||
});
|
||||
|
||||
// intrinsic.tsp
|
||||
await loadIntrinsicTypes(sourceLoader);
|
||||
|
||||
// standard library
|
||||
if (!options?.nostdlib) {
|
||||
await loadStandardLibrary(sourceLoader);
|
||||
}
|
||||
|
||||
// main entrypoint
|
||||
await sourceLoader.importFile(entrypoint, { type: "project" }, "entrypoint");
|
||||
|
||||
// additional imports
|
||||
for (const additionalImport of options?.additionalImports ?? []) {
|
||||
await sourceLoader.importPath(additionalImport, NoTarget, getDirectoryPath(entrypoint), {
|
||||
type: "project",
|
||||
});
|
||||
}
|
||||
|
||||
sourceResolution = sourceLoader.resolution;
|
||||
|
||||
program.sourceFiles = sourceResolution.sourceFiles;
|
||||
program.jsSourceFiles = sourceResolution.jsSourceFiles;
|
||||
|
||||
// Bind
|
||||
for (const file of sourceResolution.sourceFiles.values()) {
|
||||
binder.bindSourceFile(file);
|
||||
}
|
||||
for (const jsFile of sourceResolution.jsSourceFiles.values()) {
|
||||
binder.bindJsSourceFile(jsFile);
|
||||
}
|
||||
program.reportDiagnostics(sourceResolution.diagnostics);
|
||||
}
|
||||
|
||||
async function loadIntrinsicTypes(loader: SourceLoader) {
|
||||
const locationContext: LocationContext = { type: "compiler" };
|
||||
await loadTypeSpecFile(
|
||||
return loader.importFile(
|
||||
resolvePath(host.getExecutionRoot(), "lib/intrinsics.tsp"),
|
||||
locationContext,
|
||||
NoTarget
|
||||
locationContext
|
||||
);
|
||||
}
|
||||
|
||||
async function loadStandardLibrary() {
|
||||
async function loadStandardLibrary(loader: SourceLoader) {
|
||||
const locationContext: LocationContext = { type: "compiler" };
|
||||
for (const dir of host.getLibDirs()) {
|
||||
await loadDirectory(dir, locationContext, NoTarget);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDirectory(
|
||||
dir: string,
|
||||
locationContext: LocationContext,
|
||||
diagnosticTarget: DiagnosticTarget | typeof NoTarget
|
||||
): Promise<string> {
|
||||
const mainFile = await resolveTypeSpecEntrypointForDir(host, dir, reportDiagnostic);
|
||||
await loadTypeSpecFile(mainFile, locationContext, diagnosticTarget);
|
||||
return mainFile;
|
||||
}
|
||||
|
||||
async function loadTypeSpecFile(
|
||||
path: string,
|
||||
locationContext: LocationContext,
|
||||
diagnosticTarget: DiagnosticTarget | typeof NoTarget
|
||||
) {
|
||||
if (seenSourceFiles.has(path)) {
|
||||
return;
|
||||
}
|
||||
seenSourceFiles.add(path);
|
||||
|
||||
const file = await doIO(host.readFile, path, program.reportDiagnostic, {
|
||||
diagnosticTarget,
|
||||
});
|
||||
|
||||
if (file) {
|
||||
sourceFileLocationContexts.set(file, locationContext);
|
||||
await loadTypeSpecScript(file);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadJsFile(
|
||||
path: string,
|
||||
locationContext: LocationContext,
|
||||
diagnosticTarget: DiagnosticTarget | typeof NoTarget
|
||||
): Promise<JsSourceFileNode | undefined> {
|
||||
const sourceFile = program.jsSourceFiles.get(path);
|
||||
if (sourceFile !== undefined) {
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
const file = createSourceFile("", path);
|
||||
sourceFileLocationContexts.set(file, locationContext);
|
||||
const exports = await doIO(host.getJsImport, path, program.reportDiagnostic, {
|
||||
diagnosticTarget,
|
||||
jsDiagnosticTarget: { file, pos: 0, end: 0 },
|
||||
});
|
||||
|
||||
if (!exports) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
kind: SyntaxKind.JsSourceFile,
|
||||
id: {
|
||||
kind: SyntaxKind.Identifier,
|
||||
sv: "",
|
||||
pos: 0,
|
||||
end: 0,
|
||||
symbol: undefined!,
|
||||
flags: NodeFlags.Synthetic,
|
||||
},
|
||||
esmExports: exports,
|
||||
file,
|
||||
namespaceSymbols: [],
|
||||
symbol: undefined!,
|
||||
pos: 0,
|
||||
end: 0,
|
||||
flags: NodeFlags.None,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the Javascript files decorator and lifecycle hooks.
|
||||
*/
|
||||
async function importJsFile(
|
||||
path: string,
|
||||
locationContext: LocationContext,
|
||||
diagnosticTarget: DiagnosticTarget | typeof NoTarget
|
||||
) {
|
||||
const file = await loadJsFile(path, locationContext, diagnosticTarget);
|
||||
if (file !== undefined) {
|
||||
program.jsSourceFiles.set(path, file);
|
||||
binder.bindJsSourceFile(file);
|
||||
await loader.importFile(resolvePath(dir, "main.tsp"), locationContext);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -441,7 +364,6 @@ export async function compile(
|
|||
program.reportDiagnostics(script.parseDiagnostics);
|
||||
program.sourceFiles.set(file.path, script);
|
||||
binder.bindSourceFile(script);
|
||||
await loadScriptImports(script);
|
||||
return script;
|
||||
}
|
||||
|
||||
|
@ -455,75 +377,12 @@ export async function compile(
|
|||
return script;
|
||||
}
|
||||
|
||||
async function loadScriptImports(file: TypeSpecScriptNode) {
|
||||
// collect imports
|
||||
const basedir = getDirectoryPath(file.file.path);
|
||||
await loadImports(
|
||||
file.statements.filter(isImportStatement).map((x) => ({ path: x.path.value, target: x })),
|
||||
basedir,
|
||||
getSourceFileLocationContext(file.file)
|
||||
);
|
||||
}
|
||||
|
||||
function getSourceFileLocationContext(sourcefile: SourceFile): LocationContext {
|
||||
const locationContext = sourceFileLocationContexts.get(sourcefile);
|
||||
const locationContext = sourceResolution.locationContexts.get(sourcefile);
|
||||
compilerAssert(locationContext, "SourceFile should have a declaration locationContext.");
|
||||
return locationContext;
|
||||
}
|
||||
|
||||
async function loadImports(
|
||||
imports: Array<{ path: string; target: DiagnosticTarget | typeof NoTarget }>,
|
||||
relativeTo: string,
|
||||
locationContext: LocationContext
|
||||
) {
|
||||
// collect imports
|
||||
for (const { path, target } of imports) {
|
||||
await loadImport(path, target, relativeTo, locationContext);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadImport(
|
||||
path: string,
|
||||
target: DiagnosticTarget | typeof NoTarget,
|
||||
relativeTo: string,
|
||||
locationContext: LocationContext
|
||||
) {
|
||||
const library = await resolveTypeSpecLibrary(path, relativeTo, target);
|
||||
if (library === undefined) {
|
||||
return;
|
||||
}
|
||||
if (library.type === "module") {
|
||||
loadedLibraries.set(library.manifest.name, {
|
||||
path: library.path,
|
||||
manifest: library.manifest,
|
||||
});
|
||||
trace("import-resolution.library", `Loading library "${path}" from "${library.mainFile}"`);
|
||||
|
||||
const metadata = computeModuleMetadata(library);
|
||||
locationContext = {
|
||||
type: "library",
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
const importFilePath = library.type === "module" ? library.mainFile : library.path;
|
||||
|
||||
const isDirectory = (await host.stat(importFilePath)).isDirectory();
|
||||
if (isDirectory) {
|
||||
return await loadDirectory(importFilePath, locationContext, target);
|
||||
}
|
||||
|
||||
const sourceFileKind = host.getSourceFileKind(importFilePath);
|
||||
|
||||
switch (sourceFileKind) {
|
||||
case "js":
|
||||
return await importJsFile(importFilePath, locationContext, target);
|
||||
case "typespec":
|
||||
return await loadTypeSpecFile(importFilePath, locationContext, target);
|
||||
default:
|
||||
program.reportDiagnostic(createDiagnostic({ code: "invalid-import", target }));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEmitters(
|
||||
basedir: string,
|
||||
emitterNameOrPaths: string[],
|
||||
|
@ -558,9 +417,9 @@ export async function compile(
|
|||
}
|
||||
|
||||
const entrypoint = module.type === "file" ? module.path : module.mainFile;
|
||||
const file = await loadJsFile(entrypoint, locationContext, NoTarget);
|
||||
const [file, jsDiagnostics] = await loadJsFile(host, entrypoint, NoTarget);
|
||||
|
||||
return [{ module, entrypoint: file }, []];
|
||||
return [{ module, entrypoint: file }, jsDiagnostics];
|
||||
}
|
||||
|
||||
async function loadLibrary(
|
||||
|
@ -746,7 +605,7 @@ export async function compile(
|
|||
|
||||
function validateRequiredImports() {
|
||||
for (const [requiredImport, emitterName] of requireImports) {
|
||||
if (!loadedLibraries.has(requiredImport)) {
|
||||
if (!sourceResolution.loadedLibraries.has(requiredImport)) {
|
||||
program.reportDiagnostic(
|
||||
createDiagnostic({
|
||||
code: "missing-import",
|
||||
|
@ -758,47 +617,6 @@ export async function compile(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* resolves a module specifier like "myLib" to an absolute path where we can find the main of
|
||||
* that module, e.g. "/typespec/node_modules/myLib/main.tsp".
|
||||
*/
|
||||
async function resolveTypeSpecLibrary(
|
||||
specifier: string,
|
||||
baseDir: string,
|
||||
target: DiagnosticTarget | typeof NoTarget
|
||||
): Promise<ModuleResolutionResult | undefined> {
|
||||
try {
|
||||
return await resolveModule(getResolveModuleHost(), specifier, {
|
||||
baseDir,
|
||||
directoryIndexFiles: ["main.tsp", "index.mjs", "index.js"],
|
||||
resolveMain(pkg) {
|
||||
// this lets us follow node resolve semantics more-or-less exactly
|
||||
// but using tspMain instead of main.
|
||||
return resolveTspMain(pkg) ?? pkg.main;
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e.code === "MODULE_NOT_FOUND") {
|
||||
program.reportDiagnostic(
|
||||
createDiagnostic({ code: "import-not-found", format: { path: specifier }, target })
|
||||
);
|
||||
return undefined;
|
||||
} else if (e.code === "INVALID_MAIN") {
|
||||
program.reportDiagnostic(
|
||||
createDiagnostic({
|
||||
code: "library-invalid",
|
||||
format: { path: specifier },
|
||||
messageId: "tspMain",
|
||||
target,
|
||||
})
|
||||
);
|
||||
return undefined;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* resolves a module specifier like "myLib" to an absolute path where we can find the main of
|
||||
* that module, e.g. "/typespec/node_modules/myLib/dist/lib.js".
|
||||
|
@ -850,26 +668,6 @@ export async function compile(
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the main file from the given path
|
||||
* @param mainPath Absolute path to the main file.
|
||||
*/
|
||||
async function loadMain(mainPath: string): Promise<void> {
|
||||
await checkForCompilerVersionMismatch(mainPath);
|
||||
|
||||
const sourceFileKind = host.getSourceFileKind(mainPath);
|
||||
|
||||
const locationContext: LocationContext = { type: "project" };
|
||||
switch (sourceFileKind) {
|
||||
case "js":
|
||||
return await importJsFile(mainPath, locationContext, NoTarget);
|
||||
case "typespec":
|
||||
return await loadTypeSpecFile(mainPath, locationContext, NoTarget);
|
||||
default:
|
||||
program.reportDiagnostic(createDiagnostic({ code: "invalid-main", target: NoTarget }));
|
||||
}
|
||||
}
|
||||
|
||||
// It's important that we use the compiler version that resolves locally
|
||||
// from the input TypeSpec source location. Otherwise, there will be undefined
|
||||
// runtime behavior when decorators and handlers expect a
|
||||
|
|
|
@ -0,0 +1,374 @@
|
|||
import { deepEquals, doIO, resolveTspMain } from "../utils/misc.js";
|
||||
import { compilerAssert, createDiagnosticCollector } from "./diagnostics.js";
|
||||
import { resolveTypeSpecEntrypointForDir } from "./entrypoint-resolution.js";
|
||||
import { createDiagnostic } from "./messages.js";
|
||||
import {
|
||||
ModuleResolutionResult,
|
||||
NodePackage,
|
||||
ResolvedModule,
|
||||
resolveModule,
|
||||
ResolveModuleHost,
|
||||
} from "./module-resolver.js";
|
||||
import { isImportStatement, parse } from "./parser.js";
|
||||
import { getDirectoryPath } from "./path-utils.js";
|
||||
import { createSourceFile } from "./source-file.js";
|
||||
import {
|
||||
DiagnosticTarget,
|
||||
ModuleLibraryMetadata,
|
||||
NodeFlags,
|
||||
NoTarget,
|
||||
ParseOptions,
|
||||
SourceFile,
|
||||
SyntaxKind,
|
||||
Tracer,
|
||||
type CompilerHost,
|
||||
type Diagnostic,
|
||||
type JsSourceFileNode,
|
||||
type LocationContext,
|
||||
type TypeSpecScriptNode,
|
||||
} from "./types.js";
|
||||
|
||||
export interface SourceResolution {
|
||||
/** TypeSpec source files */
|
||||
readonly sourceFiles: Map<string, TypeSpecScriptNode>;
|
||||
|
||||
/** Javascript source files(Entrypoint only) */
|
||||
readonly jsSourceFiles: Map<string, JsSourceFileNode>;
|
||||
|
||||
readonly locationContexts: WeakMap<SourceFile, LocationContext>;
|
||||
readonly loadedLibraries: Map<string, TypeSpecLibraryReference>;
|
||||
|
||||
readonly diagnostics: readonly Diagnostic[];
|
||||
}
|
||||
|
||||
interface TypeSpecLibraryReference {
|
||||
path: string;
|
||||
manifest: NodePackage;
|
||||
}
|
||||
|
||||
export interface LoadSourceOptions {
|
||||
readonly parseOptions?: ParseOptions;
|
||||
readonly tracer?: Tracer;
|
||||
getCachedScript?: (file: SourceFile) => TypeSpecScriptNode | undefined;
|
||||
}
|
||||
|
||||
export interface SourceLoader {
|
||||
importFile(
|
||||
path: string,
|
||||
locationContext?: LocationContext,
|
||||
kind?: "import" | "entrypoint"
|
||||
): Promise<void>;
|
||||
importPath(
|
||||
path: string,
|
||||
target: DiagnosticTarget | typeof NoTarget,
|
||||
relativeTo: string,
|
||||
locationContext?: LocationContext
|
||||
): Promise<void>;
|
||||
readonly resolution: SourceResolution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a TypeSpec source loader. This will be able to resolve and load TypeSpec and JS files.
|
||||
* @param host Compiler host
|
||||
* @param options Loading options
|
||||
*/
|
||||
export async function createSourceLoader(
|
||||
host: CompilerHost,
|
||||
options?: LoadSourceOptions
|
||||
): Promise<SourceLoader> {
|
||||
const diagnostics = createDiagnosticCollector();
|
||||
const tracer = options?.tracer;
|
||||
const seenSourceFiles = new Set<string>();
|
||||
const sourceFileLocationContexts = new WeakMap<SourceFile, LocationContext>();
|
||||
const sourceFiles = new Map<string, TypeSpecScriptNode>();
|
||||
const jsSourceFiles = new Map<string, JsSourceFileNode>();
|
||||
const loadedLibraries = new Map<string, TypeSpecLibraryReference>();
|
||||
|
||||
async function importFile(
|
||||
path: string,
|
||||
locationContext: LocationContext,
|
||||
kind: "import" | "entrypoint" = "import"
|
||||
) {
|
||||
const sourceFileKind = host.getSourceFileKind(path);
|
||||
|
||||
switch (sourceFileKind) {
|
||||
case "js":
|
||||
await importJsFile(path, locationContext, NoTarget);
|
||||
break;
|
||||
case "typespec":
|
||||
await loadTypeSpecFile(path, locationContext, NoTarget);
|
||||
break;
|
||||
default:
|
||||
diagnostics.add(
|
||||
createDiagnostic({
|
||||
code: kind === "import" ? "invalid-import" : "invalid-main",
|
||||
target: NoTarget,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
importFile,
|
||||
importPath,
|
||||
resolution: {
|
||||
sourceFiles,
|
||||
jsSourceFiles,
|
||||
locationContexts: sourceFileLocationContexts,
|
||||
loadedLibraries: loadedLibraries,
|
||||
diagnostics: diagnostics.diagnostics,
|
||||
},
|
||||
};
|
||||
|
||||
async function loadTypeSpecFile(
|
||||
path: string,
|
||||
locationContext: LocationContext,
|
||||
diagnosticTarget: DiagnosticTarget | typeof NoTarget
|
||||
) {
|
||||
if (seenSourceFiles.has(path)) {
|
||||
return;
|
||||
}
|
||||
seenSourceFiles.add(path);
|
||||
|
||||
const file = await doIO(host.readFile, path, (x) => diagnostics.add(x), {
|
||||
diagnosticTarget,
|
||||
});
|
||||
|
||||
if (file) {
|
||||
sourceFileLocationContexts.set(file, locationContext);
|
||||
await loadTypeSpecScript(file);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTypeSpecScript(file: SourceFile): Promise<TypeSpecScriptNode> {
|
||||
// This is not a diagnostic because the compiler should never reuse the same path.
|
||||
// It's the caller's responsibility to use unique paths.
|
||||
if (sourceFiles.has(file.path)) {
|
||||
throw new RangeError("Duplicate script path: " + file.path);
|
||||
}
|
||||
|
||||
const script = parseOrReuse(file);
|
||||
for (const diagnostic of script.parseDiagnostics) {
|
||||
diagnostics.add(diagnostic);
|
||||
}
|
||||
|
||||
sourceFiles.set(file.path, script);
|
||||
await loadScriptImports(script);
|
||||
return script;
|
||||
}
|
||||
|
||||
function parseOrReuse(file: SourceFile): TypeSpecScriptNode {
|
||||
if (options?.getCachedScript) {
|
||||
const old = options.getCachedScript(file);
|
||||
if (old?.file === file && deepEquals(old.parseOptions, options.parseOptions)) {
|
||||
return old;
|
||||
}
|
||||
}
|
||||
const script = parse(file, options?.parseOptions);
|
||||
host.parseCache?.set(file, script);
|
||||
return script;
|
||||
}
|
||||
|
||||
async function loadScriptImports(file: TypeSpecScriptNode) {
|
||||
// collect imports
|
||||
const basedir = getDirectoryPath(file.file.path);
|
||||
await loadImports(
|
||||
file.statements.filter(isImportStatement).map((x) => ({ path: x.path.value, target: x })),
|
||||
basedir,
|
||||
getSourceFileLocationContext(file.file)
|
||||
);
|
||||
}
|
||||
|
||||
function getSourceFileLocationContext(sourcefile: SourceFile): LocationContext {
|
||||
const locationContext = sourceFileLocationContexts.get(sourcefile);
|
||||
compilerAssert(locationContext, "SourceFile should have a declaration locationContext.");
|
||||
return locationContext;
|
||||
}
|
||||
|
||||
async function loadImports(
|
||||
imports: Array<{ path: string; target: DiagnosticTarget | typeof NoTarget }>,
|
||||
relativeTo: string,
|
||||
locationContext: LocationContext
|
||||
) {
|
||||
// collect imports
|
||||
for (const { path, target } of imports) {
|
||||
await importPath(path, target, relativeTo, locationContext);
|
||||
}
|
||||
}
|
||||
|
||||
async function importPath(
|
||||
path: string,
|
||||
target: DiagnosticTarget | typeof NoTarget,
|
||||
relativeTo: string,
|
||||
locationContext: LocationContext = { type: "project" }
|
||||
) {
|
||||
const library = await resolveTypeSpecLibrary(path, relativeTo, target);
|
||||
if (library === undefined) {
|
||||
return;
|
||||
}
|
||||
if (library.type === "module") {
|
||||
loadedLibraries.set(library.manifest.name, {
|
||||
path: library.path,
|
||||
manifest: library.manifest,
|
||||
});
|
||||
tracer?.trace(
|
||||
"import-resolution.library",
|
||||
`Loading library "${path}" from "${library.mainFile}"`
|
||||
);
|
||||
|
||||
const metadata = computeModuleMetadata(library);
|
||||
locationContext = {
|
||||
type: "library",
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
const importFilePath = library.type === "module" ? library.mainFile : library.path;
|
||||
|
||||
const isDirectory = (await host.stat(importFilePath)).isDirectory();
|
||||
if (isDirectory) {
|
||||
await loadDirectory(importFilePath, locationContext, target);
|
||||
return;
|
||||
}
|
||||
|
||||
return importFile(importFilePath, locationContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* resolves a module specifier like "myLib" to an absolute path where we can find the main of
|
||||
* that module, e.g. "/typespec/node_modules/myLib/main.tsp".
|
||||
*/
|
||||
async function resolveTypeSpecLibrary(
|
||||
specifier: string,
|
||||
baseDir: string,
|
||||
target: DiagnosticTarget | typeof NoTarget
|
||||
): Promise<ModuleResolutionResult | undefined> {
|
||||
try {
|
||||
return await resolveModule(getResolveModuleHost(), specifier, {
|
||||
baseDir,
|
||||
directoryIndexFiles: ["main.tsp", "index.mjs", "index.js"],
|
||||
resolveMain(pkg) {
|
||||
// this lets us follow node resolve semantics more-or-less exactly
|
||||
// but using tspMain instead of main.
|
||||
return resolveTspMain(pkg) ?? pkg.main;
|
||||
},
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (e.code === "MODULE_NOT_FOUND") {
|
||||
diagnostics.add(
|
||||
createDiagnostic({ code: "import-not-found", format: { path: specifier }, target })
|
||||
);
|
||||
return undefined;
|
||||
} else if (e.code === "INVALID_MAIN") {
|
||||
diagnostics.add(
|
||||
createDiagnostic({
|
||||
code: "library-invalid",
|
||||
format: { path: specifier },
|
||||
messageId: "tspMain",
|
||||
target,
|
||||
})
|
||||
);
|
||||
return undefined;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDirectory(
|
||||
dir: string,
|
||||
locationContext: LocationContext,
|
||||
diagnosticTarget: DiagnosticTarget | typeof NoTarget
|
||||
): Promise<string> {
|
||||
const mainFile = await resolveTypeSpecEntrypointForDir(host, dir, (x) => diagnostics.add(x));
|
||||
await loadTypeSpecFile(mainFile, locationContext, diagnosticTarget);
|
||||
return mainFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the Javascript files decorator and lifecycle hooks.
|
||||
*/
|
||||
async function importJsFile(
|
||||
path: string,
|
||||
locationContext: LocationContext,
|
||||
diagnosticTarget: DiagnosticTarget | typeof NoTarget
|
||||
) {
|
||||
const sourceFile = jsSourceFiles.get(path);
|
||||
if (sourceFile !== undefined) {
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
const file = diagnostics.pipe(await loadJsFile(host, path, diagnosticTarget));
|
||||
if (file !== undefined) {
|
||||
sourceFileLocationContexts.set(file.file, locationContext);
|
||||
jsSourceFiles.set(path, file);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
function getResolveModuleHost(): ResolveModuleHost {
|
||||
return {
|
||||
realpath: host.realpath,
|
||||
stat: host.stat,
|
||||
readFile: async (path) => {
|
||||
const file = await host.readFile(path);
|
||||
return file.text;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function computeModuleMetadata(module: ResolvedModule): ModuleLibraryMetadata {
|
||||
const metadata: ModuleLibraryMetadata = {
|
||||
type: "module",
|
||||
name: module.manifest.name,
|
||||
};
|
||||
|
||||
if (module.manifest.homepage) {
|
||||
metadata.homepage = module.manifest.homepage;
|
||||
}
|
||||
if (module.manifest.bugs?.url) {
|
||||
metadata.bugs = { url: module.manifest.bugs?.url };
|
||||
}
|
||||
if (module.manifest.version) {
|
||||
metadata.version = module.manifest.version;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export async function loadJsFile(
|
||||
host: CompilerHost,
|
||||
path: string,
|
||||
diagnosticTarget: DiagnosticTarget | typeof NoTarget
|
||||
): Promise<[JsSourceFileNode | undefined, readonly Diagnostic[]]> {
|
||||
const file = createSourceFile("", path);
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
const exports = await doIO(host.getJsImport, path, (x) => diagnostics.push(x), {
|
||||
diagnosticTarget,
|
||||
jsDiagnosticTarget: { file, pos: 0, end: 0 },
|
||||
});
|
||||
|
||||
if (!exports) {
|
||||
return [undefined, diagnostics];
|
||||
}
|
||||
|
||||
const node: JsSourceFileNode = {
|
||||
kind: SyntaxKind.JsSourceFile,
|
||||
id: {
|
||||
kind: SyntaxKind.Identifier,
|
||||
sv: "",
|
||||
pos: 0,
|
||||
end: 0,
|
||||
symbol: undefined!,
|
||||
flags: NodeFlags.Synthetic,
|
||||
},
|
||||
esmExports: exports,
|
||||
file,
|
||||
namespaceSymbols: [],
|
||||
symbol: undefined!,
|
||||
pos: 0,
|
||||
end: 0,
|
||||
flags: NodeFlags.None,
|
||||
};
|
||||
return [node, diagnostics];
|
||||
}
|
Загрузка…
Ссылка в новой задаче