diff --git a/.chronus/changes/expose-init-internals-2025-2-8-13-21-58.md b/.chronus/changes/expose-init-internals-2025-2-8-13-21-58.md new file mode 100644 index 00000000000..98f98fef0e6 --- /dev/null +++ b/.chronus/changes/expose-init-internals-2025-2-8-13-21-58.md @@ -0,0 +1,6 @@ +--- +changeKind: internal +packages: + - "@typespec/bundler" +--- + diff --git a/.chronus/changes/expose-init-internals-2025-2-8-20-55-39.md b/.chronus/changes/expose-init-internals-2025-2-8-20-55-39.md new file mode 100644 index 00000000000..2b4998e4d46 --- /dev/null +++ b/.chronus/changes/expose-init-internals-2025-2-8-20-55-39.md @@ -0,0 +1,8 @@ +--- +# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking +changeKind: internal +packages: + - "@typespec/compiler" +--- + +Expose initScaffolding via an internals export diff --git a/packages/bundler/src/bundler.ts b/packages/bundler/src/bundler.ts index d06b3aa10fa..1af72475880 100644 --- a/packages/bundler/src/bundler.ts +++ b/packages/bundler/src/bundler.ts @@ -126,7 +126,9 @@ async function resolveTypeSpecBundleDefinition( const exports = pkg.exports ? Object.fromEntries( - Object.entries(pkg.exports).filter(([k, v]) => k !== "." && k !== "./testing"), + Object.entries(pkg.exports).filter( + ([k, v]) => k !== "." && k !== "./testing" && k !== "./internals", + ), ) : {}; diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 48fbd54a94c..fb6995a2834 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -49,6 +49,10 @@ "./experimental/typekit": { "types": "./dist/src/experimental/typekit/index.d.ts", "default": "./dist/src/experimental/typekit/index.js" + }, + "./internals": { + "types": "./dist/src/internals/index.d.ts", + "import": "./dist/src/internals/index.js" } }, "browser": { diff --git a/packages/compiler/src/config/config-loader.ts b/packages/compiler/src/config/config-loader.ts index 1d27899dda9..2dd1ace6366 100644 --- a/packages/compiler/src/config/config-loader.ts +++ b/packages/compiler/src/config/config-loader.ts @@ -3,7 +3,8 @@ import { getDirectoryPath, isPathAbsolute, joinPaths, resolvePath } from "../cor import { createJSONSchemaValidator } from "../core/schema-validator.js"; import { createSourceFile } from "../core/source-file.js"; import { CompilerHost, Diagnostic, NoTarget, SourceFile } from "../core/types.js"; -import { deepClone, deepFreeze, doIO, omitUndefined } from "../utils/misc.js"; +import { doIO } from "../utils/io.js"; +import { deepClone, deepFreeze, omitUndefined } from "../utils/misc.js"; import { getLocationInYamlScript } from "../yaml/index.js"; import { parseYaml } from "../yaml/parser.js"; import { YamlScript } from "../yaml/types.js"; diff --git a/packages/compiler/src/config/config-to-options.ts b/packages/compiler/src/config/config-to-options.ts index 6471259448c..322b6b372aa 100644 --- a/packages/compiler/src/config/config-to-options.ts +++ b/packages/compiler/src/config/config-to-options.ts @@ -3,7 +3,8 @@ import { createDiagnostic } from "../core/messages.js"; import { CompilerOptions } from "../core/options.js"; import { getDirectoryPath, normalizePath } from "../core/path-utils.js"; import { CompilerHost, Diagnostic, NoTarget } from "../core/types.js"; -import { deepClone, doIO, omitUndefined } from "../utils/misc.js"; +import { doIO } from "../utils/io.js"; +import { deepClone, omitUndefined } from "../utils/misc.js"; import { expandConfigVariables } from "./config-interpolation.js"; import { loadTypeSpecConfigForPath, validateConfigPathsAbsolute } from "./config-loader.js"; import { EmitterOptions, TypeSpecConfig } from "./types.js"; diff --git a/packages/compiler/src/core/cli/actions/compile/compile.ts b/packages/compiler/src/core/cli/actions/compile/compile.ts index fe2ce70aa6b..c827cc0931c 100644 --- a/packages/compiler/src/core/cli/actions/compile/compile.ts +++ b/packages/compiler/src/core/cli/actions/compile/compile.ts @@ -1,4 +1,4 @@ -import { typespecVersion } from "../../../../utils/misc.js"; +import { typespecVersion } from "../../../../manifest.js"; import { logDiagnostics } from "../../../diagnostics.js"; import { resolveTypeSpecEntrypoint } from "../../../entrypoint-resolution.js"; import { CompilerOptions } from "../../../options.js"; diff --git a/packages/compiler/src/core/cli/cli.ts b/packages/compiler/src/core/cli/cli.ts index c426b10467d..2150a6c2e6b 100644 --- a/packages/compiler/src/core/cli/cli.ts +++ b/packages/compiler/src/core/cli/cli.ts @@ -8,7 +8,7 @@ try { import yargs from "yargs"; import { installTypeSpecDependencies } from "../../install/install.js"; -import { typespecVersion } from "../../utils/misc.js"; +import { typespecVersion } from "../../manifest.js"; import { getTypeSpecEngine } from "../engine.js"; import { compileAction } from "./actions/compile/compile.js"; import { formatAction } from "./actions/format.js"; diff --git a/packages/compiler/src/core/entrypoint-resolution.ts b/packages/compiler/src/core/entrypoint-resolution.ts index dc290321e9a..b9d34c17869 100644 --- a/packages/compiler/src/core/entrypoint-resolution.ts +++ b/packages/compiler/src/core/entrypoint-resolution.ts @@ -1,4 +1,5 @@ -import { doIO, loadFile, resolveTspMain } from "../utils/misc.js"; +import { doIO, loadFile } from "../utils/io.js"; +import { resolveTspMain } from "../utils/misc.js"; import { DiagnosticHandler } from "./diagnostics.js"; import { resolvePath } from "./path-utils.js"; import { CompilerHost } from "./types.js"; diff --git a/packages/compiler/src/core/node-host.ts b/packages/compiler/src/core/node-host.ts index 34d8998c3bd..8ecb429265b 100644 --- a/packages/compiler/src/core/node-host.ts +++ b/packages/compiler/src/core/node-host.ts @@ -1,11 +1,11 @@ -import { realpath } from "fs"; -import { mkdir, readdir, readFile, rm, stat, writeFile } from "fs/promises"; +import { mkdir, stat } from "fs/promises"; import { fileURLToPath, pathToFileURL } from "url"; -import { findProjectRoot } from "../utils/misc.js"; +import { findProjectRoot } from "../utils/io.js"; import { createConsoleSink } from "./logger/index.js"; +import { NodeSystemHost } from "./node-system-host.js"; import { joinPaths } from "./path-utils.js"; -import { createSourceFile, getSourceFileKindFromExt } from "./source-file.js"; -import { CompilerHost, RmOptions } from "./types.js"; +import { getSourceFileKindFromExt } from "./source-file.js"; +import { CompilerHost } from "./types.js"; export const CompilerPackageRoot = (await findProjectRoot(stat, fileURLToPath(import.meta.url)))!; @@ -14,37 +14,13 @@ export const CompilerPackageRoot = (await findProjectRoot(stat, fileURLToPath(im * This is the the CompilerHost used by TypeSpec CLI. */ export const NodeHost: CompilerHost = { - readUrl: async (url: string) => { - const response = await fetch(url, { redirect: "follow" }); - const text = await response.text(); - return createSourceFile(text, response.url); - }, - readFile: async (path: string) => createSourceFile(await readUtf8File(path), path), - writeFile: (path: string, content: string) => writeFile(path, content, { encoding: "utf-8" }), - readDir: (path: string) => readdir(path), - rm: (path: string, options: RmOptions) => rm(path, options), + ...NodeSystemHost, getExecutionRoot: () => CompilerPackageRoot, getJsImport: (path: string) => import(pathToFileURL(path).href), getLibDirs() { const rootDir = this.getExecutionRoot(); return [joinPaths(rootDir, "lib/std")]; }, - stat(path: string) { - return stat(path); - }, - realpath(path) { - // BUG in the promise version of realpath https://github.com/microsoft/typespec/issues/2783 - // Fix was only made to node 21.6 at this time. https://github.com/nodejs/node/issues/51031 - return new Promise((resolve, reject) => { - realpath(path, (err, resolvedPath) => { - if (err) { - reject(err); - } else { - resolve(resolvedPath); - } - }); - }); - }, getSourceFileKind: getSourceFileKindFromExt, mkdirp: (path: string) => mkdir(path, { recursive: true }), logSink: createConsoleSink(), @@ -53,26 +29,3 @@ export const NodeHost: CompilerHost = { return pathToFileURL(path).href; }, }; - -async function readUtf8File(path: string) { - const buffer = await readFile(path); - const len = buffer.length; - if (len >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) { - throw new InvalidEncodingError("UTF-16 BE"); - } - if (len >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) { - throw new InvalidEncodingError("UTF-16 LE"); - } - if (len >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) { - // UTF-8 byte order mark detected - return buffer.toString("utf8", 3); - } - // Default is UTF-8 with no byte order mark - return buffer.toString("utf8"); -} - -export class InvalidEncodingError extends Error { - constructor(encoding: string) { - super(`Invalid encoding ${encoding}. TypeSpec only supports UTF-8 and UTF-8 with bom`); - } -} diff --git a/packages/compiler/src/core/node-system-host.ts b/packages/compiler/src/core/node-system-host.ts new file mode 100644 index 00000000000..0384d61fee6 --- /dev/null +++ b/packages/compiler/src/core/node-system-host.ts @@ -0,0 +1,59 @@ +import { realpath } from "fs"; +import { mkdir, readdir, readFile, rm, stat, writeFile } from "fs/promises"; +import { createSourceFile } from "./source-file.js"; +import { RmOptions, SystemHost } from "./types.js"; + +/** + * Implementation of the @see SystemHost using the real file system. + */ +export const NodeSystemHost: SystemHost = { + readUrl: async (url: string) => { + const response = await fetch(url, { redirect: "follow" }); + const text = await response.text(); + return createSourceFile(text, response.url); + }, + readFile: async (path: string) => createSourceFile(await readUtf8File(path), path), + writeFile: (path: string, content: string) => writeFile(path, content, { encoding: "utf-8" }), + readDir: (path: string) => readdir(path), + rm: (path: string, options: RmOptions) => rm(path, options), + stat(path: string) { + return stat(path); + }, + realpath(path) { + // BUG in the promise version of realpath https://github.com/microsoft/typespec/issues/2783 + // Fix was only made to node 21.6 at this time. https://github.com/nodejs/node/issues/51031 + return new Promise((resolve, reject) => { + realpath(path, (err, resolvedPath) => { + if (err) { + reject(err); + } else { + resolve(resolvedPath); + } + }); + }); + }, + mkdirp: (path: string) => mkdir(path, { recursive: true }), +}; + +async function readUtf8File(path: string) { + const buffer = await readFile(path); + const len = buffer.length; + if (len >= 2 && buffer[0] === 0xfe && buffer[1] === 0xff) { + throw new InvalidEncodingError("UTF-16 BE"); + } + if (len >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) { + throw new InvalidEncodingError("UTF-16 LE"); + } + if (len >= 3 && buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) { + // UTF-8 byte order mark detected + return buffer.toString("utf8", 3); + } + // Default is UTF-8 with no byte order mark + return buffer.toString("utf8"); +} + +export class InvalidEncodingError extends Error { + constructor(encoding: string) { + super(`Invalid encoding ${encoding}. TypeSpec only supports UTF-8 and UTF-8 with bom`); + } +} diff --git a/packages/compiler/src/core/program.ts b/packages/compiler/src/core/program.ts index f84902160c7..fa1fa0f7057 100644 --- a/packages/compiler/src/core/program.ts +++ b/packages/compiler/src/core/program.ts @@ -11,7 +11,8 @@ import { resolveModule, } from "../module-resolver/module-resolver.js"; import { PackageJson } from "../types/package-json.js"; -import { deepEquals, findProjectRoot, isDefined, mapEquals, mutate } from "../utils/misc.js"; +import { findProjectRoot } from "../utils/io.js"; +import { deepEquals, 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"; diff --git a/packages/compiler/src/core/source-loader.ts b/packages/compiler/src/core/source-loader.ts index 16fdcc60f13..f946f78c331 100644 --- a/packages/compiler/src/core/source-loader.ts +++ b/packages/compiler/src/core/source-loader.ts @@ -6,7 +6,8 @@ import { ResolveModuleHost, } from "../module-resolver/module-resolver.js"; import { PackageJson } from "../types/package-json.js"; -import { deepEquals, doIO, resolveTspMain } from "../utils/misc.js"; +import { doIO } from "../utils/io.js"; +import { deepEquals, resolveTspMain } from "../utils/misc.js"; import { compilerAssert, createDiagnosticCollector } from "./diagnostics.js"; import { resolveTypeSpecEntrypointForDir } from "./entrypoint-resolution.js"; import { createDiagnostic } from "./messages.js"; diff --git a/packages/compiler/src/core/types.ts b/packages/compiler/src/core/types.ts index 260cd5a2916..91018c26bad 100644 --- a/packages/compiler/src/core/types.ts +++ b/packages/compiler/src/core/types.ts @@ -2034,18 +2034,13 @@ export interface RmOptions { recursive?: boolean; } -export interface CompilerHost { +export interface SystemHost { /** read a file at the given url. */ readUrl(url: string): Promise; /** read a utf-8 or utf-8 with bom encoded file */ readFile(path: string): Promise; - /** - * Optional cache to reuse the results of parsing and binding across programs. - */ - parseCache?: WeakMap; - /** * Write the file. * @param path Path to the file. @@ -2072,6 +2067,19 @@ export interface CompilerHost { */ mkdirp(path: string): Promise; + // get info about a path + stat(path: string): Promise<{ isDirectory(): boolean; isFile(): boolean }>; + + // get the real path of a possibly symlinked path + realpath(path: string): Promise; +} + +export interface CompilerHost extends SystemHost { + /** + * Optional cache to reuse the results of parsing and binding across programs. + */ + parseCache?: WeakMap; + // get the directory TypeSpec is executing from getExecutionRoot(): string; @@ -2081,14 +2089,8 @@ export interface CompilerHost { // get a promise for the ESM module shape of a JS module getJsImport(path: string): Promise>; - // get info about a path - stat(path: string): Promise<{ isDirectory(): boolean; isFile(): boolean }>; - getSourceFileKind(path: string): SourceFileKind | undefined; - // get the real path of a possibly symlinked path - realpath(path: string): Promise; - // convert a file URL to a path in a file system fileURLToPath(url: string): string; diff --git a/packages/compiler/src/init/core-templates.ts b/packages/compiler/src/init/core-templates.ts index ad284f8a704..8e9f3ab45cb 100644 --- a/packages/compiler/src/init/core-templates.ts +++ b/packages/compiler/src/init/core-templates.ts @@ -1,6 +1,6 @@ import { CompilerPackageRoot } from "../core/node-host.js"; import { resolvePath } from "../core/path-utils.js"; -import { CompilerHost } from "../index.js"; +import type { SystemHost } from "../core/types.js"; export const templatesDir = resolvePath(CompilerPackageRoot, "templates"); export interface LoadedCoreTemplates { @@ -9,7 +9,7 @@ export interface LoadedCoreTemplates { } let typeSpecCoreTemplates: LoadedCoreTemplates | undefined; -export async function getTypeSpecCoreTemplates(host: CompilerHost): Promise { +export async function getTypeSpecCoreTemplates(host: SystemHost): Promise { if (typeSpecCoreTemplates === undefined) { const file = await host.readFile(resolvePath(templatesDir, "scaffolding.json")); const content = JSON.parse(file.text); diff --git a/packages/compiler/src/init/scaffold.ts b/packages/compiler/src/init/scaffold.ts index 51a1f9e4fe7..9a43d1bb0ac 100644 --- a/packages/compiler/src/init/scaffold.ts +++ b/packages/compiler/src/init/scaffold.ts @@ -1,19 +1,23 @@ import { stringify } from "yaml"; -import { TypeSpecConfigFilename } from "../config/config-loader.js"; -import { TypeSpecRawConfig } from "../config/types.js"; -import { formatTypeSpec } from "../core/formatter.js"; +import type { TypeSpecRawConfig } from "../config/types.js"; import { getDirectoryPath, joinPaths } from "../core/path-utils.js"; -import { CompilerHost } from "../core/types.js"; -import { PackageJson } from "../types/package-json.js"; +import type { SystemHost } from "../core/types.js"; +import type { PackageJson } from "../types/package-json.js"; import { readUrlOrPath, resolveRelativeUrlOrPath } from "../utils/misc.js"; -import { FileTemplatingContext, createFileTemplatingContext, render } from "./file-templating.js"; import { + createFileTemplatingContext, + type FileTemplatingContext, + render, +} from "./file-templating.js"; +import type { InitTemplate, InitTemplateFile, InitTemplateLibrary, InitTemplateLibrarySpec, } from "./init-template.js"; +export const TypeSpecConfigFilename = "tspconfig.yaml"; + export interface ScaffoldingConfig { /** Template used to resolve that config */ template: InitTemplate; @@ -83,7 +87,7 @@ export function makeScaffoldingConfig( * @param host * @param config */ -export async function scaffoldNewProject(host: CompilerHost, config: ScaffoldingConfig) { +export async function scaffoldNewProject(host: SystemHost, config: ScaffoldingConfig) { await host.mkdirp(config.directory); await writePackageJson(host, config); await writeConfig(host, config); @@ -101,7 +105,7 @@ export function isFileSkipGeneration(fileName: string, files: InitTemplateFile[] return false; } -async function writePackageJson(host: CompilerHost, config: ScaffoldingConfig) { +async function writePackageJson(host: SystemHost, config: ScaffoldingConfig) { if (isFileSkipGeneration("package.json", config.template.files ?? [])) { return; } @@ -156,7 +160,7 @@ const placeholderConfig = ` # warn-as-error: true # Treat warnings as errors # output-dir: "{project-root}/_generated" # Configure the base output directory for all emitters `.trim(); -async function writeConfig(host: CompilerHost, config: ScaffoldingConfig) { +async function writeConfig(host: SystemHost, config: ScaffoldingConfig) { if (isFileSkipGeneration(TypeSpecConfigFilename, config.template.files ?? [])) { return; } @@ -178,7 +182,7 @@ async function writeConfig(host: CompilerHost, config: ScaffoldingConfig) { return host.writeFile(joinPaths(config.directory, TypeSpecConfigFilename), content); } -async function writeMain(host: CompilerHost, config: ScaffoldingConfig) { +async function writeMain(host: SystemHost, config: ScaffoldingConfig) { if (isFileSkipGeneration("main.tsp", config.template.files ?? [])) { return; } @@ -191,7 +195,7 @@ async function writeMain(host: CompilerHost, config: ScaffoldingConfig) { const lines = [...config.libraries.map((x) => `import "${x.name}";`), ""]; const content = lines.join("\n"); - return host.writeFile(joinPaths(config.directory, "main.tsp"), await formatTypeSpec(content)); + return host.writeFile(joinPaths(config.directory, "main.tsp"), content); } const defaultGitignore = ` @@ -205,7 +209,7 @@ dist/ # Dependency directories node_modules/ `.trim(); -async function writeGitignore(host: CompilerHost, config: ScaffoldingConfig) { +async function writeGitignore(host: SystemHost, config: ScaffoldingConfig) { if (!config.includeGitignore || isFileSkipGeneration(".gitignore", config.template.files ?? [])) { return; } @@ -213,7 +217,7 @@ async function writeGitignore(host: CompilerHost, config: ScaffoldingConfig) { return host.writeFile(joinPaths(config.directory, ".gitignore"), defaultGitignore); } -async function writeFiles(host: CompilerHost, config: ScaffoldingConfig) { +async function writeFiles(host: SystemHost, config: ScaffoldingConfig) { const templateContext = createFileTemplatingContext(config); if (!config.template.files) { return; @@ -226,7 +230,7 @@ async function writeFiles(host: CompilerHost, config: ScaffoldingConfig) { } async function writeFile( - host: CompilerHost, + host: SystemHost, config: ScaffoldingConfig, context: FileTemplatingContext, file: InitTemplateFile, diff --git a/packages/compiler/src/internals/index.ts b/packages/compiler/src/internals/index.ts new file mode 100644 index 00000000000..c5a6c2dbb1a --- /dev/null +++ b/packages/compiler/src/internals/index.ts @@ -0,0 +1,12 @@ +/** + * This file is meant to export internal items from the TypeSpec compiler to some other tools that bundle them. + * DO NOT USE it, it might change at any time with no warning. + */ + +if (!(globalThis as any).enableCompilerInternalsExport) { + throw new Error("Importing @typespec/compiler/internals is reserved for internal use only."); +} + +export { NodeSystemHost } from "../core/node-system-host.js"; +export { InitTemplateSchema } from "../init/init-template.js"; +export { scaffoldNewProject } from "../init/scaffold.js"; diff --git a/packages/compiler/src/server/compile-service.ts b/packages/compiler/src/server/compile-service.ts index 8064adb3410..1bdf867e26c 100644 --- a/packages/compiler/src/server/compile-service.ts +++ b/packages/compiler/src/server/compile-service.ts @@ -20,7 +20,8 @@ import type { Diagnostic as TypeSpecDiagnostic, TypeSpecScriptNode, } from "../core/types.js"; -import { doIO, loadFile, resolveTspMain } from "../utils/misc.js"; +import { doIO, loadFile } from "../utils/io.js"; +import { resolveTspMain } from "../utils/misc.js"; import { serverOptions } from "./constants.js"; import { FileService } from "./file-service.js"; import { FileSystemCache } from "./file-system-cache.js"; diff --git a/packages/compiler/src/server/completion.ts b/packages/compiler/src/server/completion.ts index b47fa87a4f3..fb9fcb8f315 100644 --- a/packages/compiler/src/server/completion.ts +++ b/packages/compiler/src/server/completion.ts @@ -32,7 +32,8 @@ import { TypeSpecScriptNode, } from "../core/types.js"; import { PackageJson } from "../types/package-json.js"; -import { findProjectRoot, loadFile, resolveTspMain } from "../utils/misc.js"; +import { findProjectRoot, loadFile } from "../utils/io.js"; +import { resolveTspMain } from "../utils/misc.js"; import { getSymbolDetails } from "./type-details.js"; export type CompletionContext = { diff --git a/packages/compiler/src/server/server.ts b/packages/compiler/src/server/server.ts index b7ceb6e16fe..355fbd214eb 100644 --- a/packages/compiler/src/server/server.ts +++ b/packages/compiler/src/server/server.ts @@ -14,7 +14,7 @@ import { createConnection, } from "vscode-languageserver/node.js"; import { NodeHost } from "../core/node-host.js"; -import { typespecVersion } from "../utils/misc.js"; +import { typespecVersion } from "../manifest.js"; import { createServer } from "./serverlib.js"; import { CustomRequestName, Server, ServerHost, ServerLog } from "./types.js"; diff --git a/packages/compiler/src/server/serverlib.ts b/packages/compiler/src/server/serverlib.ts index 67fb4fec76b..07733dfd630 100644 --- a/packages/compiler/src/server/serverlib.ts +++ b/packages/compiler/src/server/serverlib.ts @@ -89,8 +89,9 @@ import { getTypeSpecCoreTemplates } from "../init/core-templates.js"; import { validateTemplateDefinitions } from "../init/init-template-validate.js"; import { InitTemplate } from "../init/init-template.js"; import { scaffoldNewProject } from "../init/scaffold.js"; +import { typespecVersion } from "../manifest.js"; import { resolveModule, ResolveModuleHost } from "../module-resolver/module-resolver.js"; -import { getNormalizedRealPath, resolveTspMain, typespecVersion } from "../utils/misc.js"; +import { getNormalizedRealPath, resolveTspMain } from "../utils/misc.js"; import { getSemanticTokens } from "./classify.js"; import { createCompileService } from "./compile-service.js"; import { resolveCompletion } from "./completion.js"; diff --git a/packages/compiler/src/testing/test-utils.ts b/packages/compiler/src/testing/test-utils.ts index 354b1b0fce1..6b34d559b97 100644 --- a/packages/compiler/src/testing/test-utils.ts +++ b/packages/compiler/src/testing/test-utils.ts @@ -5,7 +5,7 @@ import { NodeHost } from "../core/node-host.js"; import { CompilerOptions } from "../core/options.js"; import { resolvePath } from "../core/path-utils.js"; import type { Type } from "../core/types.js"; -import { findProjectRoot } from "../utils/misc.js"; +import { findProjectRoot } from "../utils/io.js"; import { BasicTestRunner, TestHost, diff --git a/packages/compiler/src/utils/io.ts b/packages/compiler/src/utils/io.ts new file mode 100644 index 00000000000..125bfae5b64 --- /dev/null +++ b/packages/compiler/src/utils/io.ts @@ -0,0 +1,107 @@ +import { DiagnosticHandler } from "../core/diagnostics.js"; +import { createDiagnostic } from "../core/messages.js"; +import { getDirectoryPath, joinPaths } from "../core/path-utils.js"; +import { createSourceFile } from "../core/source-file.js"; +import { CompilerHost, Diagnostic, DiagnosticTarget, NoTarget, SourceFile } from "../core/types.js"; + +export interface FileHandlingOptions { + allowFileNotFound?: boolean; + diagnosticTarget?: DiagnosticTarget | typeof NoTarget; + jsDiagnosticTarget?: DiagnosticTarget; +} + +export async function doIO( + action: (path: string) => Promise, + path: string, + reportDiagnostic: DiagnosticHandler, + options?: FileHandlingOptions, +): Promise { + let result; + try { + result = await action(path); + } catch (e: any) { + let diagnostic: Diagnostic; + let target = options?.diagnosticTarget ?? NoTarget; + + // blame the JS file, not the TypeSpec import statement for JS syntax errors. + if (e instanceof SyntaxError && options?.jsDiagnosticTarget) { + target = options.jsDiagnosticTarget; + } + + switch (e.code) { + case "ENOENT": + if (options?.allowFileNotFound) { + return undefined; + } + diagnostic = createDiagnostic({ code: "file-not-found", target, format: { path } }); + break; + default: + diagnostic = createDiagnostic({ + code: "file-load", + target, + format: { message: e.message }, + }); + break; + } + + reportDiagnostic(diagnostic); + return undefined; + } + + return result; +} + +export async function loadFile( + host: CompilerHost, + path: string, + load: (contents: string) => T, + reportDiagnostic: DiagnosticHandler, + options?: FileHandlingOptions, +): Promise<[T | undefined, SourceFile]> { + const file = await doIO(host.readFile, path, reportDiagnostic, options); + if (!file) { + return [undefined, createSourceFile("", path)]; + } + let data: T; + try { + data = load(file.text); + } catch (e: any) { + reportDiagnostic({ + code: "file-load", + message: e.message, + severity: "error", + target: { file, pos: 1, end: 1 }, + }); + return [undefined, file]; + } + + return [data, file]; +} + +/** + * Look for the project root by looking up until a `package.json` is found. + * @param path Path to start looking + * @param lookIn + */ +export async function findProjectRoot( + statFn: CompilerHost["stat"], + path: string, +): Promise { + let current = path; + while (true) { + const pkgPath = joinPaths(current, "package.json"); + const stat = await doIO( + () => statFn(pkgPath), + pkgPath, + () => {}, + ); + if (stat?.isFile()) { + return current; + } + const parent = getDirectoryPath(current); + if (parent === current) { + return undefined; + } + current = parent; + } +} diff --git a/packages/compiler/src/utils/misc.ts b/packages/compiler/src/utils/misc.ts index eeaff7232ee..df6d3aa7e60 100644 --- a/packages/compiler/src/utils/misc.ts +++ b/packages/compiler/src/utils/misc.ts @@ -1,27 +1,12 @@ -import { DiagnosticHandler } from "../core/diagnostics.js"; -import { createDiagnostic } from "../core/messages.js"; -import { - getDirectoryPath, - isPathAbsolute, - isUrl, - joinPaths, - normalizePath, - resolvePath, -} from "../core/path-utils.js"; -import { createSourceFile } from "../core/source-file.js"; -import { - CompilerHost, - Diagnostic, - DiagnosticTarget, +import { isPathAbsolute, isUrl, normalizePath, resolvePath } from "../core/path-utils.js"; +import type { MutableSymbolTable, - NoTarget, RekeyableMap, SourceFile, SymbolTable, + SystemHost, } from "../core/types.js"; -export { typespecVersion } from "../manifest.js"; - /** * Recursively calls Object.freeze such that all objects and arrays * referenced are frozen. @@ -135,7 +120,7 @@ export function mapEquals( return true; } -export async function getNormalizedRealPath(host: CompilerHost, path: string) { +export async function getNormalizedRealPath(host: SystemHost, path: string) { try { return normalizePath(await host.realpath(path)); } catch (error: any) { @@ -147,81 +132,7 @@ export async function getNormalizedRealPath(host: CompilerHost, path: string) { } } -export interface FileHandlingOptions { - allowFileNotFound?: boolean; - diagnosticTarget?: DiagnosticTarget | typeof NoTarget; - jsDiagnosticTarget?: DiagnosticTarget; -} - -export async function doIO( - action: (path: string) => Promise, - path: string, - reportDiagnostic: DiagnosticHandler, - options?: FileHandlingOptions, -): Promise { - let result; - try { - result = await action(path); - } catch (e: any) { - let diagnostic: Diagnostic; - let target = options?.diagnosticTarget ?? NoTarget; - - // blame the JS file, not the TypeSpec import statement for JS syntax errors. - if (e instanceof SyntaxError && options?.jsDiagnosticTarget) { - target = options.jsDiagnosticTarget; - } - - switch (e.code) { - case "ENOENT": - if (options?.allowFileNotFound) { - return undefined; - } - diagnostic = createDiagnostic({ code: "file-not-found", target, format: { path } }); - break; - default: - diagnostic = createDiagnostic({ - code: "file-load", - target, - format: { message: e.message }, - }); - break; - } - - reportDiagnostic(diagnostic); - return undefined; - } - - return result; -} - -export async function loadFile( - host: CompilerHost, - path: string, - load: (contents: string) => T, - reportDiagnostic: DiagnosticHandler, - options?: FileHandlingOptions, -): Promise<[T | undefined, SourceFile]> { - const file = await doIO(host.readFile, path, reportDiagnostic, options); - if (!file) { - return [undefined, createSourceFile("", path)]; - } - let data: T; - try { - data = load(file.text); - } catch (e: any) { - reportDiagnostic({ - code: "file-load", - message: e.message, - severity: "error", - target: { file, pos: 1, end: 1 }, - }); - return [undefined, file]; - } - - return [data, file]; -} - -export async function readUrlOrPath(host: CompilerHost, pathOrUrl: string): Promise { +export async function readUrlOrPath(host: SystemHost, pathOrUrl: string): Promise { if (isUrl(pathOrUrl)) { return host.readUrl(pathOrUrl); } @@ -299,34 +210,6 @@ export function omitUndefined>(data: T): T { return Object.fromEntries(Object.entries(data).filter(([k, v]) => v !== undefined)) as any; } -/** - * Look for the project root by looking up until a `package.json` is found. - * @param path Path to start looking - * @param lookIn - */ -export async function findProjectRoot( - statFn: CompilerHost["stat"], - path: string, -): Promise { - let current = path; - while (true) { - const pkgPath = joinPaths(current, "package.json"); - const stat = await doIO( - () => statFn(pkgPath), - pkgPath, - () => {}, - ); - if (stat?.isFile()) { - return current; - } - const parent = getDirectoryPath(current); - if (parent === current) { - return undefined; - } - current = parent; - } -} - /** * Extract package.json's tspMain entry point in a given path. * @param path Path that contains package.json diff --git a/packages/compiler/test/core/node-host/node-host.test.ts b/packages/compiler/test/core/node-host/node-host.test.ts index cd70d2dce0f..48c3d62f603 100644 --- a/packages/compiler/test/core/node-host/node-host.test.ts +++ b/packages/compiler/test/core/node-host/node-host.test.ts @@ -3,7 +3,8 @@ import { mkdir, readFile, rm, writeFile } from "fs/promises"; import { dirname } from "path"; import { fileURLToPath } from "url"; import { beforeAll, describe, it } from "vitest"; -import { InvalidEncodingError, NodeHost } from "../../../src/core/node-host.js"; +import { NodeHost } from "../../../src/core/node-host.js"; +import { InvalidEncodingError } from "../../../src/core/node-system-host.js"; import { getDirectoryPath, resolvePath } from "../../../src/index.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/compiler/test/internals.test.ts b/packages/compiler/test/internals.test.ts new file mode 100644 index 00000000000..05cf023eb90 --- /dev/null +++ b/packages/compiler/test/internals.test.ts @@ -0,0 +1,7 @@ +import { expect, it } from "vitest"; + +it("throw error if trying to import @typespec/compiler/internals", async () => { + await expect(() => import("@typespec/compiler/internals")).rejects.toThrowError( + "Importing @typespec/compiler/internals is reserved for internal use only.", + ); +});