@typespec/compiler
Version:
TypeSpec compiler and standard library
216 lines • 8.33 kB
JavaScript
import { isCompilerFeatureName } from "../core/features.js";
import { createDiagnostic } from "../core/messages.js";
import { getBaseFileName, getDirectoryPath, isPathAbsolute, joinPaths, resolvePath, } from "../core/path-utils.js";
import { createJSONSchemaValidator } from "../core/schema-validator.js";
import { createSourceFile } from "../core/source-file.js";
import { NoTarget } from "../core/types.js";
import { doIO } from "../utils/io.js";
import { deepFreeze, omitUndefined } from "../utils/misc.js";
import { getLocationInYamlScript } from "../yaml/index.js";
import { parseYaml } from "../yaml/parser.js";
import { TypeSpecConfigJsonSchema } from "./config-schema.js";
export const TypeSpecConfigFilename = "tspconfig.yaml";
export const defaultConfig = deepFreeze({
outputDir: "{cwd}/tsp-output",
diagnostics: [],
});
/**
* Look for the project root by looking up until a `tspconfig.yaml` is found.
* @param path Path to the file or the folder to start looking
*/
export async function findTypeSpecConfigPath(host, path, lookup = true) {
// if the path is a file, return immediately
const stats = await doIO(() => host.stat(path), path, () => { }, { allowFileNotFound: true });
if (!stats) {
return undefined;
}
else if (stats.isFile()) {
return path;
}
let current = path;
// only recurse if the path is a directory and the flag was set to true (only for default case)
// otherwise, look in the specific directory for tspconfig.yaml ONLY
if (!lookup) {
current = `${path}/tspconfig.yaml`;
const stats = await doIO(() => host.stat(current), current, () => { }, { allowFileNotFound: true });
if (stats?.isFile()) {
return current;
}
return undefined;
}
else {
while (true) {
const pkgPath = await searchConfigFile(host, current, TypeSpecConfigFilename);
// if found either file in current folder, return it
if (pkgPath !== undefined) {
return pkgPath;
}
const parent = getDirectoryPath(current);
if (parent === current) {
return undefined;
}
current = parent;
}
}
}
/**
* Load the TypeSpec configuration for the provided path or directory
* @param host
* @param path
*/
export async function loadTypeSpecConfigForPath(host, path, errorIfNotFound = false, lookup = true) {
const typespecConfigPath = await findTypeSpecConfigPath(host, path, lookup);
if (typespecConfigPath === undefined) {
const projectRoot = getDirectoryPath(path);
const tsConfig = { ...structuredClone(defaultConfig), projectRoot: projectRoot };
if (errorIfNotFound) {
tsConfig.diagnostics.push(createDiagnostic({
code: "config-path-not-found",
format: {
path: path,
},
target: NoTarget,
}));
}
return tsConfig;
}
const tsConfig = await loadTypeSpecConfigFile(host, typespecConfigPath);
return tsConfig;
}
/**
* Load given file as a TypeSpec configuration
*/
export async function loadTypeSpecConfigFile(host, filePath) {
const config = await loadConfigFile(host, filePath, parseYaml);
if (config.diagnostics.length === 0 && config.extends) {
const extendPath = resolvePath(getDirectoryPath(filePath), config.extends);
const parent = await loadTypeSpecConfigFile(host, extendPath);
if (parent.diagnostics.length > 0) {
return {
...config,
diagnostics: parent.diagnostics,
};
}
return {
...parent,
...config,
};
}
return {
...structuredClone(defaultConfig),
...config,
};
}
const configValidator = createJSONSchemaValidator(TypeSpecConfigJsonSchema);
async function searchConfigFile(host, path, filename) {
const pkgPath = joinPaths(path, filename);
const stat = await doIO(() => host.stat(pkgPath), pkgPath, () => { });
return stat?.isFile() === true ? pkgPath : undefined;
}
async function loadConfigFile(host, filename, loadData) {
let diagnostics = [];
const reportDiagnostic = (d) => diagnostics.push(d);
const file = (await doIO(host.readFile, filename, reportDiagnostic)) ?? createSourceFile("", filename);
const [yamlScript, yamlDiagnostics] = loadData(file);
yamlDiagnostics.forEach((d) => reportDiagnostic(d));
let data = yamlScript.value;
if (data) {
diagnostics = diagnostics.concat(configValidator.validate(data, yamlScript));
}
if (!data || diagnostics.length > 0) {
// NOTE: Don't trust the data if there are errors and use default
// config. Otherwise, we may return an object that does not conform to
// TypeSpecConfig's typing.
data = structuredClone(defaultConfig);
}
// Validate project-specific constraints
if (data.kind === "project" && getBaseFileName(filename) !== TypeSpecConfigFilename) {
diagnostics.push(createDiagnostic({
code: "config-project-kind-filename",
format: { filename: getBaseFileName(filename) },
target: NoTarget,
}));
}
if (data.entrypoint !== undefined && data.kind !== "project") {
diagnostics.push(createDiagnostic({
code: "config-project-only-option",
format: { option: "entrypoint" },
target: NoTarget,
}));
}
if (data.features !== undefined && data.kind !== "project") {
diagnostics.push(createDiagnostic({
code: "config-project-only-option",
format: { option: "features" },
target: NoTarget,
}));
}
const features = Array.isArray(data.features) ? data.features : undefined;
if (data.kind === "project" && features !== undefined) {
for (const feature of features) {
if (!isCompilerFeatureName(feature)) {
diagnostics.push(createDiagnostic({
code: "config-unknown-feature",
format: { feature },
target: getLocationInYamlScript(yamlScript, ["features", feature]),
}));
}
}
}
const emit = data.emit;
const options = data.options;
return omitUndefined({
projectRoot: getDirectoryPath(filename),
file: yamlScript,
filename,
diagnostics,
extends: data.extends,
kind: data.kind,
entrypoint: data.entrypoint,
features,
environmentVariables: data["environment-variables"],
parameters: data.parameters,
outputDir: data["output-dir"] ?? "{cwd}/tsp-output",
warnAsError: data["warn-as-error"],
imports: data.imports,
trace: typeof data.trace === "string" ? [data.trace] : data.trace,
emit,
options,
linter: data.linter,
});
}
export function validateConfigPathsAbsolute(config) {
const diagnostics = [];
function checkPath(value, path) {
if (value === undefined) {
return;
}
const diagnostic = validatePathAbsolute(value, config.file ? { file: config.file, path } : NoTarget);
if (diagnostic) {
diagnostics.push(diagnostic);
}
}
checkPath(config.outputDir, ["output-dir"]);
for (const [emitterName, emitterOptions] of Object.entries(config.options ?? {})) {
checkPath(emitterOptions["emitter-output-dir"], ["options", emitterName, "emitter-output-dir"]);
}
return diagnostics;
}
function validatePathAbsolute(path, target) {
if (path.startsWith(".") || !isPathAbsolute(path)) {
return createDiagnostic({
code: "config-path-absolute",
format: { path },
target: target === NoTarget ? target : getLocationInYamlScript(target.file, target.path),
});
}
// if (path.includes("\\")) {
// return createDiagnostic({
// code: "path-unix-style",
// format: { path },
// target: target === NoTarget ? target : getLocationInYamlScript(target.file, target.path),
// });
// }
return undefined;
}
//# sourceMappingURL=config-loader.js.map