diff --git a/packages/core/src/cli/diagnostics.ts b/packages/core/src/cli/diagnostics.ts deleted file mode 100644 index f216f9007..000000000 --- a/packages/core/src/cli/diagnostics.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type TS from 'typescript'; - -type TypeScript = typeof TS; - -export function buildDiagnosticFormatter(ts: TypeScript): (diagnostic: TS.Diagnostic) => string { - const formatDiagnosticHost: TS.FormatDiagnosticsHost = { - getCanonicalFileName: (name) => name, - getCurrentDirectory: ts.sys.getCurrentDirectory, - getNewLine: () => ts.sys.newLine, - }; - - return (diagnostic) => - ts.formatDiagnosticsWithColorAndContext([diagnostic], formatDiagnosticHost); -} diff --git a/packages/core/src/cli/index.ts b/packages/core/src/cli/index.ts deleted file mode 100644 index c3f26f51d..000000000 --- a/packages/core/src/cli/index.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { createRequire } from 'node:module'; -import yargs from 'yargs'; -import { findTypeScript, loadConfig } from '../config/index.js'; -// import { performWatch } from './perform-watch.js'; -import { performCheck } from './perform-check.js'; -import { determineOptionsToExtend } from './options.js'; -// import { performBuild } from './perform-build.js'; -import type TS from 'typescript'; -// import { performBuildWatch } from './perform-build-watch.js'; -import { validateTSOrExit } from '../common/typescript-compatibility.js'; - -const require = createRequire(import.meta.url); -const pkg = require('../../package.json'); - -const argv = yargs(process.argv.slice(2)) - .scriptName('glint') - .usage('$0 [options]') - .option('project', { - alias: 'p', - string: true, - description: 'The path to the tsconfig file to use', - }) - .option('watch', { - alias: 'w', - boolean: true, - description: 'Whether to perform an ongoing watched build', - }) - .option('preserveWatchOutput', { - implies: 'watch', - boolean: true, - description: - 'Whether to keep outdated console output in watch mode instead of clearing the screen every time a change happened.', - }) - .option('declaration', { - alias: 'd', - boolean: true, - description: 'Whether to emit declaration files', - }) - .option('build', { - alias: 'b', - boolean: true, - description: - 'Build one or more projects and their dependencies, if out of date. Same as the TS `--build` flag.', - // As with TS itself, we *must* emit declarations when in build mode - conflicts: 'declaration', - }) - .option('clean', { - implies: 'build', - boolean: true, - description: 'Delete the outputs of all projects.', - conflicts: 'watch', - }) - .option('force', { - implies: 'build', - description: 'Act as if all projects are out of date. Same as the TS `--force` flag.', - type: 'boolean', - }) - .option('dry', { - implies: 'build', - description: `Show what would be built (or deleted, if specified with '--clean'). Same as the TS \`--dry\` flag.`, - type: 'boolean', - }) - .option('incremental', { - description: - 'Save .tsbuildinfo files to allow for incremental compilation of projects. Same as the TS `--incremental` flag.', - type: 'boolean', - }) - .version(pkg.version) - .wrap(100) - .strict() - .parseSync(); - -let cwd = process.cwd(); - -if (argv.build) { - // Type signature here so we get a useful error as close to the source of the - // error as possible, rather than at the *use* sites below. - let buildOptions: TS.BuildOptions = { - clean: argv.clean, - force: argv.force, - dry: argv.dry, - }; - - if ('incremental' in argv) { - buildOptions.incremental = argv.incremental; - } - - if ('watch' in argv) { - buildOptions['preserveWatchOutput'] = argv.preserveWatchOutput; - } - - // Get the closest TS to us, since we have to assume that we may be in the - // root of a project which has no `Glint` config *at all* in its root, but - // which must have *some* TS to be useful. - let ts = findTypeScript(cwd); - - validateTSOrExit(ts); - - // This continues using the hack of a 'default command' to get the projects - // specified (if any). - let projects = [cwd]; - - if (argv.watch) { - // build - - // performBuildWatch(ts, projects, buildOptions); - throw new Error('TODO performBuildWatch'); - } else { - // performBuild(ts, projects, buildOptions); - throw new Error('TODO performBuild'); - } -} else { - // why does typechecking require glint config but not performBuild watch? - // not sure... - - const glintConfig = loadConfig(argv.project ?? cwd); - const optionsToExtend = determineOptionsToExtend(argv); - - validateTSOrExit(glintConfig.ts); - - if (argv.watch) { - throw new Error('TODO performWatch'); - - // performWatch(glintConfig, optionsToExtend); - } else { - throw new Error('TODO performCheck'); - - // performCheck(glintConfig, optionsToExtend); - } -} diff --git a/packages/core/src/cli/options.ts b/packages/core/src/cli/options.ts deleted file mode 100644 index e594f671b..000000000 --- a/packages/core/src/cli/options.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { type CompilerOptions } from 'typescript'; - -export function determineOptionsToExtend(argv: { - declaration?: boolean | undefined; - incremental?: boolean | undefined; - preserveWatchOutput?: boolean | undefined; -}): CompilerOptions { - let options: CompilerOptions = {}; - - if ('incremental' in argv) { - options.incremental = argv.incremental; - } - - if ('watch' in argv) { - options['preserveWatchOutput'] = argv['preserveWatchOutput'] as boolean; - } - - if ('declaration' in argv) { - options.noEmit = !argv.declaration; - options.declaration = Boolean(argv.declaration); - options.emitDeclarationOnly = Boolean(argv.declaration); - } else if ('build' in argv) { - options.noEmit = false; - options.declaration = true; - options.emitDeclarationOnly = true; - } else { - options.noEmit = true; - options.declaration = false; - options.declarationMap = false; - } - - return options; -} diff --git a/packages/core/src/cli/perform-build-watch.ts b/packages/core/src/cli/perform-build-watch.ts deleted file mode 100644 index d648c9690..000000000 --- a/packages/core/src/cli/perform-build-watch.ts +++ /dev/null @@ -1,28 +0,0 @@ -// import type TS from 'typescript'; - -// import { buildDiagnosticFormatter } from './diagnostics.js'; -// import { sysForCompilerHost } from './utils/sys-for-compiler-host.js'; -// import { patchProgramBuilder } from './utils/patch-program.js'; -// import TransformManagerPool from './utils/transform-manager-pool.js'; - -// export function performBuildWatch( -// ts: typeof TS, -// projects: string[], -// buildOptions: TS.BuildOptions -// ): void { -// let transformManagerPool = new TransformManagerPool(ts); -// let formatDiagnostic = buildDiagnosticFormatter(ts); -// let buildProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram; - -// let host = ts.createSolutionBuilderWithWatchHost( -// sysForCompilerHost(ts, transformManagerPool), -// patchProgramBuilder(ts, transformManagerPool, buildProgram), -// (diagnostic) => console.error(formatDiagnostic(diagnostic)) -// ); - -// // @ts-ignore: This hook was added in TS5, and is safely irrelevant in earlier versions. Once we drop support for 4.x, we can also remove this @ts-ignore comment. -// host.resolveModuleNameLiterals = transformManagerPool.resolveModuleNameLiterals; - -// let builder = ts.createSolutionBuilderWithWatch(host, projects, buildOptions); -// builder.build(); -// } diff --git a/packages/core/src/cli/perform-build.ts b/packages/core/src/cli/perform-build.ts deleted file mode 100644 index 26598d3ac..000000000 --- a/packages/core/src/cli/perform-build.ts +++ /dev/null @@ -1,32 +0,0 @@ -// import type TS from 'typescript'; - -// import { buildDiagnosticFormatter } from './diagnostics.js'; -// import { sysForCompilerHost } from './utils/sys-for-compiler-host.js'; -// import { patchProgramBuilder } from './utils/patch-program.js'; -// import TransformManagerPool from './utils/transform-manager-pool.js'; - -// type TypeScript = typeof TS; - -// // Because `--clean` is public API for the CLI but *not* public in the type?!? -// interface BuildOptions extends TS.BuildOptions { -// clean?: boolean | undefined; -// } - -// export function performBuild(ts: TypeScript, projects: string[], buildOptions: BuildOptions): void { -// let transformManagerPool = new TransformManagerPool(ts); -// let formatDiagnostic = buildDiagnosticFormatter(ts); -// let buildProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram; - -// let host = ts.createSolutionBuilderHost( -// sysForCompilerHost(ts, transformManagerPool), -// patchProgramBuilder(ts, transformManagerPool, buildProgram), -// (diagnostic) => console.error(formatDiagnostic(diagnostic)) -// ); - -// // @ts-ignore: This hook was added in TS5, and is safely irrelevant in earlier versions. Once we drop support for 4.x, we can also remove this @ts-ignore comment. -// host.resolveModuleNameLiterals = transformManagerPool.resolveModuleNameLiterals; - -// let builder = ts.createSolutionBuilder(host, projects, buildOptions); -// let exitStatus = buildOptions.clean ? builder.clean() : builder.build(); -// process.exit(exitStatus); -// } diff --git a/packages/core/src/cli/perform-check.ts b/packages/core/src/cli/perform-check.ts deleted file mode 100644 index 52508204c..000000000 --- a/packages/core/src/cli/perform-check.ts +++ /dev/null @@ -1,109 +0,0 @@ -import type TS from 'typescript'; -import TransformManager from '../common/transform-manager.js'; -import { GlintConfig } from '../config/index.js'; -import { buildDiagnosticFormatter } from './diagnostics.js'; -import { sysForCompilerHost } from './utils/sys-for-compiler-host.js'; - -type TypeScript = typeof TS; - -// TODO: convert this to volar runTsc -export function performCheck(glintConfig: GlintConfig, optionsToExtend: TS.CompilerOptions): void { - let { ts } = glintConfig; - let transformManager = new TransformManager(glintConfig); - let parsedConfig = loadTsconfig(ts, transformManager, glintConfig.configPath, optionsToExtend); - let compilerHost = createCompilerHost(ts, parsedConfig.options, transformManager); - let formatDiagnostic = buildDiagnosticFormatter(ts); - ``; - // let createProgram = parsedConfig.options.incremental - // ? ts.createIncrementalProgram - // : ts.createProgram; - - // let program = createProgram({ - // rootNames: parsedConfig.fileNames, - // options: parsedConfig.options, - // host: compilerHost, - // }); - - // // We run *before* doing emit, so that if we are in an `--incremental` program - // // TS caches the diagnostics in the `tsbuildinfo` file it generates. This is - // // quirky, but it's how TS itself works internally, and it's also *sort of* - // // documented [here][wiki-pr]. - // // - // // [wiki-pr]: https://github.com/microsoft/TypeScript-wiki/blob/ad7afb1b7049be5ac59ba55dce9a647390ee8481/Using-the-Compiler-API.md#a-minimal-incremental-compiler - // let baselineDiagnostics = collectDiagnostics(program, transformManager, parsedConfig.options); - // let emitResult = program.emit(); - // let diagnosticsWithEmit = baselineDiagnostics.concat(emitResult.diagnostics); - - // let fullDiagnostics = transformManager.rewriteDiagnostics(diagnosticsWithEmit); - // for (let diagnostic of fullDiagnostics) { - // console.error(formatDiagnostic(diagnostic)); - // } - - process.exit(0); -} - -function collectDiagnostics( - program: TS.Program | TS.EmitAndSemanticDiagnosticsBuilderProgram, - transformManager: TransformManager, - options: TS.CompilerOptions, -): Array { - // return [ - // ...program.getSyntacticDiagnostics(), - // ...transformManager.getTransformDiagnostics(), - // ...program.getSemanticDiagnostics(), - // ...(options.declaration ? program.getDeclarationDiagnostics() : []), - // ]; - return []; -} - -function createCompilerHost( - ts: TypeScript, - options: TS.CompilerOptions, - transformManager: TransformManager, -): TS.CompilerHost { - let host = options.incremental - ? ts.createIncrementalCompilerHost(options, sysForCompilerHost(ts, transformManager)) - : ts.createCompilerHost(options); - - // @ts-ignore: This hook was added in TS5, and is safely irrelevant in earlier versions. Once we drop support for 4.x, we can also remove this @ts-ignore comment. - host.resolveModuleNameLiterals = transformManager.resolveModuleNameLiterals; - host.fileExists = transformManager.fileExists; - host.readFile = transformManager.readTransformedFile; - host.readDirectory = transformManager.readDirectory; - - return host; -} - -function loadTsconfig( - ts: TypeScript, - transformManager: TransformManager, - configPath: string | undefined, - optionsToExtend: TS.CompilerOptions, -): TS.ParsedCommandLine { - if (!configPath) { - return { - fileNames: [], - options: optionsToExtend, - errors: [], - }; - } - - let config = ts.getParsedCommandLineOfConfigFile(configPath, optionsToExtend, { - ...ts.sys, - readDirectory: transformManager.readDirectory, - onUnRecoverableConfigFileDiagnostic(diagnostic) { - let { messageText } = diagnostic; - if (typeof messageText !== 'string') { - messageText = messageText.messageText; - } - - throw new Error(messageText); - }, - }); - - if (!config) { - throw new Error('Unknown error loading config'); - } - - return config; -} diff --git a/packages/core/src/cli/perform-watch.ts b/packages/core/src/cli/perform-watch.ts deleted file mode 100644 index 8fb41315c..000000000 --- a/packages/core/src/cli/perform-watch.ts +++ /dev/null @@ -1,26 +0,0 @@ -import TransformManager from '../common/transform-manager.js'; -import { GlintConfig } from '../config/index.js'; -import { buildDiagnosticFormatter } from './diagnostics.js'; -import type ts from 'typescript'; -import { sysForCompilerHost } from './utils/sys-for-compiler-host.js'; -// import { patchProgramBuilder } from './utils/patch-program.js'; - -export type TypeScript = typeof ts; - -// export function performWatch(glintConfig: GlintConfig, optionsToExtend: ts.CompilerOptions): void { -// let { ts } = glintConfig; -// let transformManager = new TransformManager(glintConfig); -// let formatDiagnostic = buildDiagnosticFormatter(ts); -// let host = ts.createWatchCompilerHost( -// glintConfig.configPath, -// optionsToExtend, -// sysForCompilerHost(ts, transformManager), -// patchProgramBuilder(ts, transformManager, ts.createSemanticDiagnosticsBuilderProgram), -// (diagnostic) => console.error(formatDiagnostic(diagnostic)) -// ); - -// // @ts-ignore: This hook was added in TS5, and is safely irrelevant in earlier versions. Once we drop support for 4.x, we can also remove this @ts-ignore comment. -// host.resolveModuleNameLiterals = transformManager.resolveModuleNameLiterals; - -// ts.createWatchProgram(host); -// } diff --git a/packages/core/src/cli/utils/assert.ts b/packages/core/src/cli/utils/assert.ts deleted file mode 100644 index a9d18e137..000000000 --- a/packages/core/src/cli/utils/assert.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function assert(test: unknown, message = 'Internal error'): asserts test { - if (test == null || test === false) { - throw new Error(message); - } -} diff --git a/packages/core/src/cli/utils/sys-for-compiler-host.ts b/packages/core/src/cli/utils/sys-for-compiler-host.ts deleted file mode 100644 index 60cf640a8..000000000 --- a/packages/core/src/cli/utils/sys-for-compiler-host.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type TS from 'typescript'; -import TransformManager from '../../common/transform-manager.js'; -import TransformManagerPool from './transform-manager-pool.js'; - -export function sysForCompilerHost( - ts: typeof TS, - transformManagerOrPool: TransformManager | TransformManagerPool, -): TS.System { - return { - ...ts.sys, - readDirectory: transformManagerOrPool.readDirectory, - watchDirectory: transformManagerOrPool.watchDirectory, - fileExists: transformManagerOrPool.fileExists, - watchFile: transformManagerOrPool.watchTransformedFile, - readFile: transformManagerOrPool.readTransformedFile, - getModifiedTime: transformManagerOrPool.getModifiedTime, - }; -} diff --git a/packages/core/src/cli/utils/transform-manager-pool.ts b/packages/core/src/cli/utils/transform-manager-pool.ts deleted file mode 100644 index bfad849c5..000000000 --- a/packages/core/src/cli/utils/transform-manager-pool.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { dirname } from 'node:path'; -import TS from 'typescript'; -import { ConfigLoader, GlintConfig } from '../../config/index.js'; -import TransformManager from '../../common/transform-manager.js'; -import { assert } from './assert.js'; - -/** - * NOTE: this class ONLY used for CLI commands like in `perform-build-watch` and `perform-build`. - * - * A lazy cache/lookup map for the parts of `TS.System` which `TransformManager` - * cares about, such that any given file will be resolved against its closest - * `GlintConfig`. This provides us three things: - * - * - The ability to apply the *correct* transforms to any given file, based on - * its closest Glint config. - * - Lazy instantation for each manager: we only get a `TransformManager` when - * we actually *require* it for transforming some file - * - A cache for the managers: we only instantiate them *once* for a given - * config. - */ -export default class TransformManagerPool { - #rootTS: typeof TS; - #rootSys: TS.System; - #managers = new Map(); - #loader = new ConfigLoader(); - - get isPool(): true { - return true; - } - - constructor(ts: typeof TS) { - this.#rootTS = ts; - this.#rootSys = ts.sys; - } - - public managerForFile(path: string): TransformManager | null { - return this.managerForDirectory(dirname(path)); - } - - public managerForDirectory(path: string): TransformManager | null { - let config = this.#loader.configForDirectory(path); - if (!config) return null; - - const existing = this.#managers.get(config); - if (existing) return existing; - - const manager = new TransformManager(config); - this.#managers.set(config, manager); - return manager; - } - - public resolveModuleNameLiterals = ( - moduleLiterals: readonly TS.StringLiteralLike[], - containingFile: string, - redirectedReference: TS.ResolvedProjectReference | undefined, - options: TS.CompilerOptions, - ): readonly TS.ResolvedModuleWithFailedLookupLocations[] => { - let resolveModuleNameLiterals = this.managerForFile(containingFile)?.resolveModuleNameLiterals; - if (resolveModuleNameLiterals) { - return resolveModuleNameLiterals( - moduleLiterals, - containingFile, - redirectedReference, - options, - ); - } else { - return moduleLiterals.map((literal) => - this.#rootTS.resolveModuleName( - literal.text, - containingFile, - options, - this.#rootSys, - undefined, - redirectedReference, - ), - ); - } - }; - - public readDirectory = ( - rootDir: string, - extensions: ReadonlyArray, - excludes: ReadonlyArray | undefined, - includes: ReadonlyArray, - depth?: number | undefined, - ): Array => { - let readDirectory = - this.managerForDirectory(rootDir)?.readDirectory ?? this.#rootSys.readDirectory; - return readDirectory(rootDir, extensions, excludes, includes, depth); - }; - - public watchDirectory = ( - path: string, - originalCallback: TS.DirectoryWatcherCallback, - recursive?: boolean, - options?: TS.WatchOptions, - ): TS.FileWatcher => { - assert(this.#rootSys.watchDirectory); - let watchDirectory = - this.managerForDirectory(path)?.watchDirectory ?? this.#rootSys.watchDirectory; - return watchDirectory(path, originalCallback, recursive, options); - }; - - public fileExists = (filename: string): boolean => { - let fileExists = this.managerForFile(filename)?.fileExists ?? this.#rootSys.fileExists; - return fileExists(filename); - }; - - public watchTransformedFile = ( - path: string, - originalCallback: TS.FileWatcherCallback, - pollingInterval?: number, - options?: TS.WatchOptions, - ): TS.FileWatcher => { - assert(this.#rootSys.watchFile); - let watchTransformedFile = - this.managerForFile(path)?.watchTransformedFile ?? this.#rootSys.watchFile; - return watchTransformedFile(path, originalCallback, pollingInterval, options); - }; - - public readTransformedFile = (filename: string, encoding?: string): string | undefined => { - let readTransformedFile = - this.managerForFile(filename)?.readTransformedFile ?? this.#rootSys.readFile; - return readTransformedFile(filename, encoding); - }; - - public getModifiedTime = (filename: string): Date | undefined => { - assert(this.#rootSys.getModifiedTime); - let getModifiedTime = - this.managerForFile(filename)?.getModifiedTime ?? this.#rootSys.getModifiedTime; - return getModifiedTime(filename); - }; -} diff --git a/packages/core/src/common/document-cache.ts b/packages/core/src/common/document-cache.ts deleted file mode 100644 index e101d624c..000000000 --- a/packages/core/src/common/document-cache.ts +++ /dev/null @@ -1,220 +0,0 @@ -import * as path from 'node:path'; -import { GlintConfig } from '../config/index.js'; -import { v4 as uuid } from 'uuid'; - -// import { DocumentsAndSourceMaps } from './documents'; -// import type { DocumentsAndSourceMaps } from '@volar/language-server/lib/common/documents.js'; - -// import type DocumentAndSou - -// import { Document } from '@volar/language-server/lib/common/documents.js'; - -// having trouble how to figure out DocumentsAndSourceMaps type. - -export type Document = { - /** A unique identifier shared by all possible paths that may point to a document. */ - id: string; - /** The "true" path where this document's source of truth can be found. */ - canonicalPath: string; - /** Incremented each time the contents of a document changes (used by TS itself). */ - version: number; - /** The current string contents of this document. */ - contents: string; - /** - * Whether this document is a placeholder for something that might exist, or has actually - * been read from disk or opened in an editor. - */ - speculative: boolean; - /** Whether this document has changed on disk since the last time we read it. */ - stale: boolean; -}; - -/** - * A read-through cache for workspace document contents. - * - * Via the Glint configuration it's instantiated with, this cache is aware - * of two things: - * - the relationship between companion script and template files, treating - * a change to one member of such pairs as affecting the version of both - * - the existence of custom extensions that would result in multiple - * potential on-disk paths corresponding to a single logical TS module, - * where one path must win out. - * TODO: what does this mean? custom extensions like .gts. Potentialy on - * disk paths that refer to single module? .gts is a file with many templates. - * - * OK so we probably need to use this; how does this compare to Volar's document cache? - * Check the stack frame to see where it's used. - */ -export default class DocumentCache { - private readonly documents = new Map(); - private readonly ts: typeof import('typescript'); - - // documents: DocumentsAndSourceMaps; - - public constructor(private glintConfig: GlintConfig) { - // where is GlintConfig created? - this.ts = glintConfig.ts; - } - - public getDocumentID(path: string): string { - return this.getDocument(path).id; - } - - public getCanonicalDocumentPath(path: string): string { - return this.getDocument(path).canonicalPath; - } - - public documentExists(path: string): boolean { - // If we have a document that's actually been read from disk, it definitely exists. - if (this.documents.get(path)?.speculative === false) { - return true; - } - - return this.getCandidateDocumentPaths(path).some((candidate) => - this.ts.sys.fileExists(candidate), - ); - } - - public getCandidateDocumentPaths(filename: string): Array { - let extension = path.extname(filename); - let filenameWithoutExtension = filename.slice(0, filename.lastIndexOf(extension)); - - return this.getCandidateExtensions(filename).map((ext) => `${filenameWithoutExtension}${ext}`); - } - - public getCompanionDocumentPath(path: string): string | undefined { - let { environment } = this.glintConfig; - let candidates = environment.isTemplate(path) - ? environment.getPossibleScriptPaths(path) - : environment.getPossibleTemplatePaths(path); - - for (let { path, deferTo } of candidates) { - // If a candidate companion exist and no other module that would claim that - // companion with a higher priority exists, we've found our winner. - if (this.documentExists(path) && !deferTo.some((path) => this.documentExists(path))) { - return path; - } - } - - if (environment.isTemplate(path)) { - return this.glintConfig.getSynthesizedScriptPathForTS(path); - } - } - - public getDocumentContents(path: string, encoding?: string): string { - if (!this.documentExists(path)) return ''; - - let document = this.getDocument(path); - if (document.stale) { - let onDiskPath = this.getCandidateDocumentPaths(path).find((path) => - this.ts.sys.fileExists(path), - ); - - document.stale = false; - - if (onDiskPath) { - document.contents = this.ts.sys.readFile(onDiskPath, encoding) ?? ''; - document.canonicalPath = onDiskPath; - document.speculative = false; - } else { - document.speculative = true; - } - } - - return document.contents; - } - - public getCompoundDocumentVersion(path: string): string { - let env = this.glintConfig.environment; - let template = env.isTemplate(path) ? this.getDocument(path) : this.findCompanionDocument(path); - let script = env.isTemplate(path) ? this.findCompanionDocument(path) : this.getDocument(path); - - return `${script?.version}:${template?.version}`; - } - - public getDocumentVersion(path: string): string { - return this.getDocument(path).version.toString(); - } - - public updateDocument(path: string, contents: string): void { - let document = this.getDocument(path); - - document.stale = false; - document.speculative = false; - document.contents = contents; - document.canonicalPath = path; - document.version++; - - this.incrementCompanionVersion(path); - } - - public markDocumentStale(path: string): void { - let document = this.getDocument(path); - - document.stale = true; - document.speculative = true; - document.version++; - - this.incrementCompanionVersion(path); - } - - // called by TransformManager, which has a watcher thing. - // called by GlintLanguageServer, which we no longer use. - // Can probably remove this? - public removeDocument(path: string): void { - for (let candidate of this.getCandidateDocumentPaths(path)) { - this.documents.delete(candidate); - } - } - - private incrementCompanionVersion(path: string): void { - let companion = this.findCompanionDocument(path); - if (companion) { - companion.version++; - } - } - - private findCompanionDocument(path: string): Document | undefined { - let companionPath = this.getCompanionDocumentPath(path); - return companionPath ? this.getDocument(companionPath) : undefined; - } - - private getDocument(path: string): Document { - let document = this.documents.get(path); - if (!document) { - document = { - id: uuid(), - canonicalPath: path, - version: 0, - contents: '', - stale: true, - speculative: true, - }; - - for (let candidate of this.getCandidateDocumentPaths(path)) { - this.documents.set(candidate, document); - } - } - return document; - } - - private getCandidateExtensions(filename: string): ReadonlyArray { - let { environment } = this.glintConfig; - switch (environment.getSourceKind(filename)) { - case 'template': - return environment.templateExtensions; - case 'typed-script': - return environment.typedScriptExtensions; - case 'untyped-script': - return environment.untypedScriptExtensions; - default: - return [path.extname(filename)]; - } - } -} - -const SCRIPT_EXTENSION_REGEX = /\.(ts|js)$/; - -export function templatePathForSynthesizedModule(path: string): string { - return path.replace(SCRIPT_EXTENSION_REGEX, '.hbs'); -} diff --git a/packages/core/src/common/scheduling.ts b/packages/core/src/common/scheduling.ts deleted file mode 100644 index 2ac9cbca5..000000000 --- a/packages/core/src/common/scheduling.ts +++ /dev/null @@ -1,10 +0,0 @@ -export function debounce(threshold: number, f: () => void): () => void { - let pending: NodeJS.Timeout | undefined; - return () => { - if (pending) { - clearTimeout(pending); - } - - pending = setTimeout(f, threshold); - }; -} diff --git a/packages/core/src/common/transform-manager.ts b/packages/core/src/common/transform-manager.ts deleted file mode 100644 index d7022a723..000000000 --- a/packages/core/src/common/transform-manager.ts +++ /dev/null @@ -1,436 +0,0 @@ -import { statSync as fsStatSync, Stats, existsSync } from 'fs'; -import { - TransformedModule, - rewriteModule, - // rewriteDiagnostic, - Directive, - Diagnostic, - // createTransformDiagnostic, -} from '../transform/index.js'; -import type ts from 'typescript'; -import { GlintConfig } from '../config/index.js'; -import DocumentCache, { templatePathForSynthesizedModule } from './document-cache.js'; - -type MappingTree = NonNullable['mapping']>; - -type TransformInfo = { - version: string; - transformedFileName: string; - transformedModule: TransformedModule | null; -}; - -export default class TransformManager { - private transformCache = new Map(); - private moduleResolutionHost: ts.ModuleResolutionHost; - private readonly ts: typeof import('typescript'); - - public readonly moduleResolutionCache: ts.ModuleResolutionCache; - - constructor( - private glintConfig: GlintConfig, - private documents: DocumentCache = new DocumentCache(glintConfig), - ) { - this.ts = glintConfig.ts; - this.moduleResolutionCache = this.ts.createModuleResolutionCache( - this.ts.sys.getCurrentDirectory(), - (name) => name, - ); - this.moduleResolutionHost = { - ...this.ts.sys, - readFile: this.readTransformedFile, - fileExists: this.fileExists, - }; - } - - public getTransformDiagnostics(fileName?: string): Array { - if (fileName) { - let transformedModule = this.getTransformInfo(fileName)?.transformedModule; - return transformedModule ? this.buildTransformDiagnostics(transformedModule) : []; - } - - return [...this.transformCache.values()].flatMap((transformInfo) => { - if (transformInfo.transformedModule) { - return this.buildTransformDiagnostics(transformInfo.transformedModule); - } - - return []; - }); - } - - public getTransformedRange( - originalFileName: string, - originalStart: number, - originalEnd: number, - ): { - transformedFileName: string; - transformedStart: number; - transformedEnd: number; - mapping?: MappingTree | undefined; - } { - let transformInfo = this.findTransformInfoForOriginalFile(originalFileName); - if (!transformInfo?.transformedModule) { - return { - transformedFileName: originalFileName, - transformedStart: originalStart, - transformedEnd: originalEnd, - }; - } - - let { transformedFileName, transformedModule } = transformInfo; - let transformedRange = transformedModule.getTransformedRange( - originalFileName, - originalStart, - originalEnd, - ); - - return { - transformedFileName, - transformedStart: transformedRange.start, - transformedEnd: transformedRange.end, - mapping: transformedRange.mapping, - }; - } - - public getOriginalRange( - transformedFileName: string, - transformedStart: number, - transformedEnd: number, - ): { - originalFileName: string; - originalStart: number; - originalEnd: number; - mapping?: MappingTree; - } { - let transformInfo = this.getTransformInfo(transformedFileName); - let { documents } = this; - if (!transformInfo?.transformedModule) { - return { - originalFileName: documents.getCanonicalDocumentPath(transformedFileName), - originalStart: transformedStart, - originalEnd: transformedEnd, - }; - } - - let original = transformInfo.transformedModule.getOriginalRange( - transformedStart, - transformedEnd, - ); - - return { - mapping: original.mapping, - originalFileName: documents.getCanonicalDocumentPath(original.source.filename), - originalStart: original.start, - originalEnd: original.end, - }; - } - - public getTransformedOffset( - originalFileName: string, - originalOffset: number, - ): { transformedFileName: string; transformedOffset: number } { - let transformInfo = this.findTransformInfoForOriginalFile(originalFileName); - if (!transformInfo?.transformedModule) { - return { transformedFileName: originalFileName, transformedOffset: originalOffset }; - } - - let { transformedFileName, transformedModule } = transformInfo; - let transformedOffset = transformedModule.getTransformedOffset( - originalFileName, - originalOffset, - ); - - return { transformedFileName, transformedOffset }; - } - - public resolveModuleNameLiterals = ( - moduleLiterals: readonly ts.StringLiteralLike[], - containingFile: string, - redirectedReference: ts.ResolvedProjectReference | undefined, - options: ts.CompilerOptions, - ): readonly ts.ResolvedModuleWithFailedLookupLocations[] => { - return moduleLiterals.map((literal) => { - // If import paths are allowed to include TS extensions (`.ts`, `.tsx`, etc), then we want to - // ensure we normalize things like `.gts` to the standard script path we present elsewhere so - // that TS understands the intent. - // @ts-ignore: this flag isn't available in the oldest versions of TS we support - let scriptPath = options.allowImportingTsExtensions - ? this.getScriptPathForTS(literal.text) - : literal.text; - - return this.ts.resolveModuleName( - scriptPath, - containingFile, - options, - this.moduleResolutionHost, - this.moduleResolutionCache, - redirectedReference, - ); - }); - }; - - // This is only called when using TransformManagerPool, which is only - // used for CLI commands like in `perform-build-watch` and `perform-build`. - public watchTransformedFile = ( - path: string, - originalCallback: ts.FileWatcherCallback, - pollingInterval?: number, - options?: ts.WatchOptions, - ): ts.FileWatcher => { - const { watchFile } = this.ts.sys; - if (!watchFile) { - throw new Error('Internal error: TS `watchFile` unavailable'); - } - - let { glintConfig, documents } = this; - let callback: ts.FileWatcherCallback = (watchedPath, eventKind) => { - if (eventKind === this.ts.FileWatcherEventKind.Deleted) { - // Adding or removing a file invalidates most of what we think we know about module resolution - this.moduleResolutionCache.clear(); - this.documents.removeDocument(watchedPath); - } else { - this.documents.markDocumentStale(watchedPath); - } - - return originalCallback(path, eventKind); - }; - - if (!glintConfig.includesFile(path)) { - return watchFile(path, callback, pollingInterval, options); - } - - let allPaths = [ - ...glintConfig.environment.getPossibleTemplatePaths(path).map((candidate) => candidate.path), - ...documents.getCandidateDocumentPaths(path), - ]; - - let allWatchers = allPaths.map((candidate) => - watchFile(candidate, callback, pollingInterval, options), - ); - - return { - close() { - allWatchers.forEach((watcher) => watcher.close()); - }, - }; - }; - - public watchDirectory = ( - path: string, - originalCallback: ts.DirectoryWatcherCallback, - recursive?: boolean, - options?: ts.WatchOptions, - ): ts.FileWatcher => { - if (!this.ts.sys.watchDirectory) { - throw new Error('Internal error: TS `watchDirectory` unavailable'); - } - - let callback: ts.DirectoryWatcherCallback = (filename) => { - // Adding or removing a file invalidates most of what we think we know about module resolution - this.moduleResolutionCache.clear(); - originalCallback(this.getScriptPathForTS(filename)); - }; - - return this.ts.sys.watchDirectory(path, callback, recursive, options); - }; - - public readDirectory = ( - rootDir: string, - extensions: ReadonlyArray, - excludes: ReadonlyArray | undefined, - includes: ReadonlyArray, - depth?: number | undefined, - ): Array => { - let env = this.glintConfig.environment; - let allExtensions = [...new Set([...extensions, ...env.getConfiguredFileExtensions()])]; - return this.ts.sys - .readDirectory(rootDir, allExtensions, excludes, includes, depth) - .map((filename) => this.getScriptPathForTS(filename)); - }; - - public fileExists = (filename: string): boolean => { - return this.documents.documentExists(filename); - }; - - public readTransformedFile = (filename: string, encoding?: string): string | undefined => { - let transformInfo = this.getTransformInfo(filename, encoding); - if (transformInfo?.transformedModule) { - return transformInfo.transformedModule.transformedContents; - } else { - return this.documents.getDocumentContents(filename, encoding); - } - }; - - public getModifiedTime = (filename: string): Date | undefined => { - // In most circumstances we can just ask the DocumentCache what the canonical path - // for a given document is, but since `getModifiedTime` is invoked as part of - // rehydrating a `.tsbuildinfo` file, typically won't actually know the answer to - // that question yet. - let canonicalFilename = this.documents - .getCandidateDocumentPaths(filename) - .find((path) => existsSync(path)); - if (!canonicalFilename) return undefined; - - let fileStat = statSync(canonicalFilename); - if (!fileStat) return undefined; - - let companionPath = this.documents.getCompanionDocumentPath(canonicalFilename); - if (!companionPath) return fileStat.mtime; - - let companionStat = statSync(companionPath); - if (!companionStat) return fileStat.mtime; - - return fileStat.mtime > companionStat.mtime ? fileStat.mtime : companionStat.mtime; - }; - - /** - * Given the path of a file on disk, returns the path under which we present TypeScript with - * its contents. This will include normalizations like `.gts` -> `.ts`, as well as relating - * a given `.hbs` file back to its backing module, if one exists. - */ - public getScriptPathForTS(filename: string): string { - // If the file is a template and already has a companion, return that path - if (this.glintConfig.environment.isTemplate(filename)) { - let companionPath = this.documents.getCompanionDocumentPath(filename); - if (companionPath) { - return companionPath; - } - } - - // Otherwise, follow the environment's standard rules for determining the path we present to TS - return this.glintConfig.getSynthesizedScriptPathForTS(filename); - } - - /** @internal `TransformInfo` is an unstable internal type */ - public findTransformInfoForOriginalFile(originalFileName: string): TransformInfo | null { - // when we're fetching completions for a template, we need to try and find the companion object, i.e. backing TS file. - let transformedFileName = this.glintConfig.environment.isTemplate(originalFileName) - ? this.documents.getCompanionDocumentPath(originalFileName) - : originalFileName; - - return transformedFileName ? this.getTransformInfo(transformedFileName) : null; - } - - // private rewriteDiagnostic(diagnostic: Diagnostic): { - // rewrittenDiagnostic?: ts.Diagnostic; - // appliedDirective?: Directive; - // } { - // if (!diagnostic.file) return {}; - - // // Transform diagnostics are already targeted at the original source and so - // // don't need to be rewritten. - // if ('isGlintTransformDiagnostic' in diagnostic && diagnostic.isGlintTransformDiagnostic) { - // return { rewrittenDiagnostic: diagnostic }; - // } - - // // fetch the transformInfo for a particular file....... can we just pass this in? - // let transformInfo = this.getTransformInfo(diagnostic.file?.fileName); - // let rewrittenDiagnostic = rewriteDiagnostic( - // this.ts, - // diagnostic, - // (fileName) => this.getTransformInfo(fileName)?.transformedModule - // ); - - // if (rewrittenDiagnostic.file) { - // rewrittenDiagnostic.file.fileName = this.documents.getCanonicalDocumentPath( - // rewrittenDiagnostic.file.fileName - // ); - // } - - // let appliedDirective = transformInfo.transformedModule?.directives.find( - // (directive) => - // directive.source.filename === rewrittenDiagnostic.file?.fileName && - // directive.areaOfEffect.start <= rewrittenDiagnostic.start! && - // directive.areaOfEffect.end > rewrittenDiagnostic.start! - // ); - - // // All current directives have the effect of squashing any diagnostics they apply - // // to, so if we have an applicable directive, we don't return the diagnostic. - // if (appliedDirective) { - // return { appliedDirective }; - // } else { - // return { rewrittenDiagnostic }; - // } - // } - - private getTransformInfo(filename: string, encoding?: string): TransformInfo { - let { documents, glintConfig } = this; - let { environment } = glintConfig; - let documentID = documents.getDocumentID(filename); - let existing = this.transformCache.get(documentID); - let version = documents.getCompoundDocumentVersion(filename); - if (existing?.version === version) { - return existing; - } - - let transformedModule: TransformedModule | null = null; - if (environment.isScript(filename) && glintConfig.includesFile(filename)) { - // if file (e.g. foo.ts) is script and glintConfig has registered extensions matching file - if (documents.documentExists(filename)) { - let contents = documents.getDocumentContents(filename, encoding); // filename is ember-component.ts - let templatePath = documents.getCompanionDocumentPath(filename); // templatePath is ember-component.hbs - let canonicalPath = documents.getCanonicalDocumentPath(filename); // same as filename (ember-component.ts) - let mayHaveEmbeds = environment.moduleMayHaveEmbeddedTemplates(canonicalPath, contents); - - if (mayHaveEmbeds || templatePath) { - let script = { filename: canonicalPath, contents }; - let template = templatePath - ? { - filename: templatePath, - contents: documents.getDocumentContents(templatePath, encoding), - } - : undefined; - - transformedModule = rewriteModule(this.ts, { script, template }, environment); // rewrite .ts to have embedded .hbs file - } - } else { - // i don't know... this isn't a real file? - let templatePath = templatePathForSynthesizedModule(filename); - if ( - documents.documentExists(templatePath) && - documents.getCompanionDocumentPath(templatePath) === filename - ) { - // The script we were asked for doesn't exist, but a corresponding template does, and - // it doesn't have a companion script elsewhere. - // We default to just `export {}` to reassure TypeScript that this is definitely a module - let script = { filename, contents: 'export {}' }; - let template = { - filename: templatePath, - contents: documents.getDocumentContents(templatePath, encoding), - }; - - transformedModule = rewriteModule(this.ts, { script, template }, glintConfig.environment); - } // ELSE set breakpoint? when does this happen? - } - } - - let transformedFileName = glintConfig.getSynthesizedScriptPathForTS(filename); - let cacheEntry = { version, transformedFileName, transformedModule }; - this.transformCache.set(documentID, cacheEntry); - return cacheEntry; - } - - private buildTransformDiagnostics(transformedModule: TransformedModule): Array { - return []; - // return transformedModule.errors.map((error) => - // createTransformDiagnostic( - // this.ts, - // error.source, - // error.message, - // error.location, - // error.isContentTagError - // ) - // ); - } -} - -function statSync(path: string): Stats | undefined { - try { - return fsStatSync(path); - } catch (e) { - if (e instanceof Error && (e as NodeJS.ErrnoException).code === 'ENOENT') { - return undefined; - } - - throw e; - } -} diff --git a/packages/core/src/common/typescript-compatibility.ts b/packages/core/src/common/typescript-compatibility.ts deleted file mode 100644 index 4a42b1e11..000000000 --- a/packages/core/src/common/typescript-compatibility.ts +++ /dev/null @@ -1,38 +0,0 @@ -import semver from 'semver'; -import { TSLib } from '../transform/util.js'; - -export const MINIMUM_VERSION = '4.8.0'; - -export type ValidationResult = { valid: true; ts: TSLib } | { valid: false; reason: string }; - -/** - * Ensures that the given copy of TypeScript is a) present, and - * b) a supported version. - */ -export function validateTS(ts: TSLib | null): ValidationResult { - if (!ts) { - return { valid: false, reason: 'Unable to locate `typescript` library' }; - } - - if (!semver.gte(ts.version, MINIMUM_VERSION)) { - return { - valid: false, - reason: `Expected TypeScript >= ${MINIMUM_VERSION}, but found ${ts.version}`, - }; - } - - return { valid: true, ts }; -} - -/** - * Validates the given copy of TypeScript as with `validateTS`, - * logging an error message and exiting the process if validation - * fails. - */ -export function validateTSOrExit(ts: TSLib | null): asserts ts { - let result = validateTS(ts); - if (!result.valid) { - console.error(result.reason); - process.exit(1); - } -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 06e6e1d0a..3f339a653 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,58 +1,9 @@ import { GlintConfig, loadConfig } from './config/index.js'; -import DocumentCache from './common/document-cache.js'; -import TransformManager from './common/transform-manager.js'; -// import GlintLanguageServer from './language-server/glint-language-server.js'; -// import GlintLanguageServer from './volar/language-server.js'; import * as utils from './language-server/util/index.js'; -// /Users/machty/code/glint/packages/core/src/volar/language-server.ts - -/** @internal */ -export interface ProjectAnalysis { - glintConfig: GlintConfig; - transformManager: TransformManager; - // languageServer: GlintLanguageServer; - shutdown: () => void; -} /** @internal */ export const pathUtils = utils; -/** - * This function is available to consumers as an unstable API. We will not go - * out of our way to change or break it, but there may be breaking changes - * to its behavior or type signature outside of major version bumps. - * - * See the `auto-glint-nocheck` implementation in `@glint/scripts` for a - * sample use of this API. - * - * So this "analyzes" your project so that we might put nochecks on there... - * is this the same as using glint cli for typechecking? - * - * Consumers: - * - * - * - * - * - * @internal - */ -export function analyzeProject(projectDirectory: string = process.cwd()): ProjectAnalysis { - let glintConfig = loadConfig(projectDirectory); - let documents = new DocumentCache(glintConfig); - let transformManager = new TransformManager(glintConfig, documents); - // let languageServer = new GlintLanguageServer(glintConfig, documents, transformManager); - // let shutdown = (): void => languageServer.dispose(); - let shutdown = (): void => {}; - - return { - glintConfig, - transformManager, - // languageServer, - shutdown, - }; -} - export { loadConfig }; -// export type { TransformManager, GlintConfig, GlintLanguageServer }; -export type { TransformManager, GlintConfig }; +export type { GlintConfig }; diff --git a/packages/scripts/src/auto-nocheck.ts b/packages/scripts/src/auto-nocheck.ts index 48ce94875..49ea4b638 100644 --- a/packages/scripts/src/auto-nocheck.ts +++ b/packages/scripts/src/auto-nocheck.ts @@ -1,9 +1,10 @@ #!/usr/bin/env node -import { autoNocheck } from './lib/_auto-nocheck.js'; +// import { autoNocheck } from './lib/_auto-nocheck.js'; try { - await autoNocheck(process.argv.slice(2)); + throw new Error('Not yet implemented since Volar'); + // await autoNocheck(process.argv.slice(2)); process.exit(0); } catch (error: any) { console.error(error?.message ?? error); diff --git a/packages/scripts/src/lib/_auto-nocheck.ts b/packages/scripts/src/lib/_auto-nocheck.ts index edebb666e..36c69620f 100644 --- a/packages/scripts/src/lib/_auto-nocheck.ts +++ b/packages/scripts/src/lib/_auto-nocheck.ts @@ -5,163 +5,163 @@ import { fileURLToPath, pathToFileURL } from 'node:url'; import globPkg from 'glob'; import yargs from 'yargs'; import ora from 'ora'; -import { type ProjectAnalysis } from '@glint/core'; - -const globSync = globPkg.sync; - -export async function autoNocheck( - args: Array, - { cwd = process.cwd(), spinner = ora() } = {}, -): Promise { - let glint = await loadGlintCore(cwd); - let { globs, explanation } = parseArgs(args); - - spinner.start('Starting Glint language service...'); - - let project = glint.analyzeProject(cwd); - let fileUpdates = new Map(); - - for (let filePath of collectFilePaths(globs, cwd)) { - spinner.text = `Checking ${relative(cwd, filePath)}...`; - - let fileContents = await readFile(filePath, 'utf-8'); - let templatesWithErrors = findTemplatesWithErrors(glint, filePath, fileContents, project); - if (templatesWithErrors.size) { - fileUpdates.set( - filePath, - insertNocheckComments(filePath, fileContents, explanation, templatesWithErrors), - ); - } - } - - project.shutdown(); - - spinner.text = 'Writing `@glint-nocheck` comments...'; - - await writeFileContents(fileUpdates); - - spinner.succeed(`Done! ${fileUpdates.size} files updated.`); -} - -function parseArgs(args: Array): { globs: Array; explanation: string } { - return yargs(args) - .scriptName('auto-nocheck') - .command('$0 ', 'Write a description here', (command) => - command - .positional('globs', { - type: 'string', - array: true, - describe: 'One or more paths or globs specifying the files to act on.', - demandOption: true, - }) - .option('explanation', { - type: 'string', - default: 'not typesafe yet', - describe: 'The explanation to be included in @glint-nocheck comments', - }), - ) - .wrap(100) - .strict() - .parseSync(); -} - -type GlintCore = typeof import('@glint/core'); - -function findImport(path: string, basedir: string): string { - let resolvedPath = createRequire(`${basedir}/package.json`).resolve(path); - let directory = dirname(fileURLToPath(import.meta.url)); - return relative(directory, resolvedPath); -} - -// We want to use the project-local version of @glint/core for maximum compatibility, -// but we need to guard against older versions that don't expose a programmatic API. -async function loadGlintCore(cwd: string): Promise { - let glint: GlintCore | undefined; - try { - glint = await import(findImport('@glint/core', cwd)); - } catch (error) { - console.log(error); - // Fall through - } - - if (!glint?.pathUtils) { - throw new Error( - 'This script requires a recent version of @glint/core to run. ' + - 'Consider upgrading to the latest version of Glint or using ' + - 'an older version of @glint/scripts.', - ); - } - - return glint; -} - -function collectFilePaths(globs: Array, cwd: string): Array { - return globs.flatMap((glob) => globSync(glob, { cwd, absolute: true })); -} - -// For the given file path, returns all templates in that file that have -// type errors, mapped from their offset to their contents. (This shape -// is a little weird, but it conveniently deduplicates and gives us exactly -// the information we need.) -function findTemplatesWithErrors( - glint: GlintCore, - filePath: string, - fileContents: string, - project: ProjectAnalysis, -): Map { - let templatesWithErrors = new Map(); - let info = project.transformManager.findTransformInfoForOriginalFile(filePath); - if (!info?.transformedModule) return templatesWithErrors; - - // disabled for now to skip analyzeProject - - // let { DiagnosticCategory } = project.glintConfig.ts; - // let diagnostics = project.languageServer.getDiagnostics(pathToFileURL(filePath).toString()); - // let errors = diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticCategory.Error); - - // for (let error of errors) { - // let originalStart = glint.pathUtils.positionToOffset(fileContents, error.range.start); - // let template = info.transformedModule.findTemplateAtOriginalOffset(filePath, originalStart); - // if (template) { - // templatesWithErrors.set(template.originalContentStart, template.originalContent); - // } - // } - - return templatesWithErrors; -} - -// Given information about a file and the location of templates within it -// that have type errors, returns a version of that file's contents with -// appropriate `{{! @glint-nocheck }}` comments added. -function insertNocheckComments( - filePath: string, - fileContents: string, - explanation: string, - templatesWithErrors: Map, -): string { - let chunks = []; - let progress = 0; - for (let [offset, template] of templatesWithErrors.entries()) { - let isMultiline = /\n/.test(template) || filePath.endsWith('.hbs'); - let [leadingWhitespace, indentation] = /^\s*?\n(\s*)/.exec(template) ?? ['', '']; - let insertAt = offset + leadingWhitespace.length; - let toInsert = isMultiline - ? `{{! @glint-nocheck: ${explanation} }}\n${indentation}` - : '{{! @glint-nocheck }}'; - - chunks.push(fileContents.slice(progress, insertAt)); - chunks.push(toInsert); - - progress = insertAt; - } - - chunks.push(fileContents.slice(progress)); - - return chunks.join(''); -} - -async function writeFileContents(fileUpdates: Map): Promise { - for (let [path, contents] of fileUpdates.entries()) { - await writeFile(path, contents); - } -} +// import { type ProjectAnalysis } from '@glint/core'; + +// const globSync = globPkg.sync; + +// export async function autoNocheck( +// args: Array, +// { cwd = process.cwd(), spinner = ora() } = {}, +// ): Promise { +// let glint = await loadGlintCore(cwd); +// let { globs, explanation } = parseArgs(args); + +// spinner.start('Starting Glint language service...'); + +// let project = glint.analyzeProject(cwd); +// let fileUpdates = new Map(); + +// for (let filePath of collectFilePaths(globs, cwd)) { +// spinner.text = `Checking ${relative(cwd, filePath)}...`; + +// let fileContents = await readFile(filePath, 'utf-8'); +// let templatesWithErrors = findTemplatesWithErrors(glint, filePath, fileContents, project); +// if (templatesWithErrors.size) { +// fileUpdates.set( +// filePath, +// insertNocheckComments(filePath, fileContents, explanation, templatesWithErrors), +// ); +// } +// } + +// project.shutdown(); + +// spinner.text = 'Writing `@glint-nocheck` comments...'; + +// await writeFileContents(fileUpdates); + +// spinner.succeed(`Done! ${fileUpdates.size} files updated.`); +// } + +// function parseArgs(args: Array): { globs: Array; explanation: string } { +// return yargs(args) +// .scriptName('auto-nocheck') +// .command('$0 ', 'Write a description here', (command) => +// command +// .positional('globs', { +// type: 'string', +// array: true, +// describe: 'One or more paths or globs specifying the files to act on.', +// demandOption: true, +// }) +// .option('explanation', { +// type: 'string', +// default: 'not typesafe yet', +// describe: 'The explanation to be included in @glint-nocheck comments', +// }), +// ) +// .wrap(100) +// .strict() +// .parseSync(); +// } + +// type GlintCore = typeof import('@glint/core'); + +// function findImport(path: string, basedir: string): string { +// let resolvedPath = createRequire(`${basedir}/package.json`).resolve(path); +// let directory = dirname(fileURLToPath(import.meta.url)); +// return relative(directory, resolvedPath); +// } + +// // We want to use the project-local version of @glint/core for maximum compatibility, +// // but we need to guard against older versions that don't expose a programmatic API. +// async function loadGlintCore(cwd: string): Promise { +// let glint: GlintCore | undefined; +// try { +// glint = await import(findImport('@glint/core', cwd)); +// } catch (error) { +// console.log(error); +// // Fall through +// } + +// if (!glint?.pathUtils) { +// throw new Error( +// 'This script requires a recent version of @glint/core to run. ' + +// 'Consider upgrading to the latest version of Glint or using ' + +// 'an older version of @glint/scripts.', +// ); +// } + +// return glint; +// } + +// function collectFilePaths(globs: Array, cwd: string): Array { +// return globs.flatMap((glob) => globSync(glob, { cwd, absolute: true })); +// } + +// // For the given file path, returns all templates in that file that have +// // type errors, mapped from their offset to their contents. (This shape +// // is a little weird, but it conveniently deduplicates and gives us exactly +// // the information we need.) +// function findTemplatesWithErrors( +// glint: GlintCore, +// filePath: string, +// fileContents: string, +// project: ProjectAnalysis, +// ): Map { +// let templatesWithErrors = new Map(); +// let info = project.transformManager.findTransformInfoForOriginalFile(filePath); +// if (!info?.transformedModule) return templatesWithErrors; + +// // disabled for now to skip analyzeProject + +// // let { DiagnosticCategory } = project.glintConfig.ts; +// // let diagnostics = project.languageServer.getDiagnostics(pathToFileURL(filePath).toString()); +// // let errors = diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticCategory.Error); + +// // for (let error of errors) { +// // let originalStart = glint.pathUtils.positionToOffset(fileContents, error.range.start); +// // let template = info.transformedModule.findTemplateAtOriginalOffset(filePath, originalStart); +// // if (template) { +// // templatesWithErrors.set(template.originalContentStart, template.originalContent); +// // } +// // } + +// return templatesWithErrors; +// } + +// // Given information about a file and the location of templates within it +// // that have type errors, returns a version of that file's contents with +// // appropriate `{{! @glint-nocheck }}` comments added. +// function insertNocheckComments( +// filePath: string, +// fileContents: string, +// explanation: string, +// templatesWithErrors: Map, +// ): string { +// let chunks = []; +// let progress = 0; +// for (let [offset, template] of templatesWithErrors.entries()) { +// let isMultiline = /\n/.test(template) || filePath.endsWith('.hbs'); +// let [leadingWhitespace, indentation] = /^\s*?\n(\s*)/.exec(template) ?? ['', '']; +// let insertAt = offset + leadingWhitespace.length; +// let toInsert = isMultiline +// ? `{{! @glint-nocheck: ${explanation} }}\n${indentation}` +// : '{{! @glint-nocheck }}'; + +// chunks.push(fileContents.slice(progress, insertAt)); +// chunks.push(toInsert); + +// progress = insertAt; +// } + +// chunks.push(fileContents.slice(progress)); + +// return chunks.join(''); +// } + +// async function writeFileContents(fileUpdates: Map): Promise { +// for (let [path, contents] of fileUpdates.entries()) { +// await writeFile(path, contents); +// } +// } diff --git a/packages/scripts/src/lib/_migrate-glintrc.ts b/packages/scripts/src/lib/_migrate-glintrc.ts deleted file mode 100644 index 951d65cf3..000000000 --- a/packages/scripts/src/lib/_migrate-glintrc.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { readFile, writeFile } from 'fs/promises'; -import * as path from 'path'; - -import { evaluate, patch } from 'golden-fleece'; -import yaml from 'js-yaml'; -import Result, { err, isInstance, ok, tryOrElse } from 'true-myth/result'; -import Unit from 'true-myth/unit'; -import yargs from 'yargs'; -import { z } from 'zod'; - -const EnvironmentList = z.array(z.string()); - -const EnvironmentMap = z.record( - z.object({ - additionalGlobals: z.optional(z.array(z.string())), - }), -); -type EnvironmentMap = z.infer; - -const Environment = z.optional(z.union([z.string(), EnvironmentList, EnvironmentMap])); -const Files = z.optional(z.array(z.string())); - -const GlintRc = z.object({ - environment: Environment, - checkStandaloneTemplates: z.optional(z.boolean()), - include: Files, - exclude: Files, -}); - -type GlintRc = z.infer; - -type GlintTsconfig = { - environment?: - | string - | string[] - | Record< - string, - { - additionalGlobals?: string[] | undefined; - } - >; - checkStandaloneTemplates?: boolean; - transform?: { - include?: string[]; - exclude?: string[]; - }; -}; - -function loadFile(path: string): Promise> { - return readFile(path, { encoding: 'utf-8' }).then( - (v) => ok(v), - (e) => err(`Could not load file at ${path}: ${JSON.stringify(e)}`), - ); -} - -function loadOrCreateTsconfig(configPath: string): Promise { - return readFile(configPath, { encoding: 'utf-8' }).catch((e) => { - console.info( - `Could not load tsconfig.json at ${configPath}: ${e}; attempting to create a new one`, - ); - return '{}'; - }); -} - -function saveFile(path: string, data: string): Promise> { - return writeFile(path, data).then( - () => ok(), - (e) => err(`Could not write file at ${path}: ${JSON.stringify(e)}`), - ); -} - -function assert(predicate: unknown, reason: string): asserts predicate { - if (!predicate) { - throw new Error(`panic: ${reason}`); - } -} - -function parseGlintRcFile(contents: string): Result { - let errors: string[] = []; - let yamlData = yaml.load(contents, { - onWarning: (yamlException) => errors.push(yamlException.toString()), - }); - if (errors.length) { - return err(`Could not parse YAML:\n\t${errors.join('\n\t')}`); - } - - return tryOrElse( - (e) => { - assert(e instanceof z.ZodError, 'error somehow not a zod error'); - return `Could not parse data as a Glint config:\n\t${e.format()._errors.join('\n\t')}`; - }, - () => { - return GlintRc.parse(yamlData); - }, - ); -} - -// Represents the part of a tsconfig.json file we care about. That is: not much! -type ReadyToTransform = { - glint?: GlintTsconfig; -}; - -function prepForPatching(contents: unknown): Result { - if (typeof contents !== 'string') { - return err(`Could not patch ${JSON.stringify(contents)}: not a string`); - } - - return tryOrElse( - (e) => `Could not patch data:\n\t${e instanceof Error ? e.message : JSON.stringify(e)}`, - () => evaluate(contents), - ); -} - -function parseTsConfig(config: unknown): Result { - if (typeof config !== 'object' || config == null || Array.isArray(config)) { - return err(`invalid contents of tsconfig.json file:\n${JSON.stringify(config)}`); - } - - let readyToTransform = config as ReadyToTransform; - return readyToTransform.glint - ? err('There is already a glint config present, not overriding it') - : ok(readyToTransform); -} - -// NOTE: mutates `config`! Because that's what `patch` expects. -function toMergedWith(rc: GlintRc): (config: ReadyToTransform) => ReadyToTransform { - return (config) => { - // Build up the modified config incrementally, *not* adding - // `undefined` to it at any point, because we cannot patch in - // `undefined`! - config.glint = {}; - - if (rc.environment !== undefined) { - config.glint.environment = rc.environment; - } - - if (rc.checkStandaloneTemplates !== undefined) { - config.glint.checkStandaloneTemplates = rc.checkStandaloneTemplates; - } - - if (rc.include || rc.exclude) { - config.glint.transform = {}; - - if (rc.include !== undefined) { - config.glint.transform.include = rc.include; - } - - if (rc.exclude !== undefined) { - config.glint.transform.exclude = rc.exclude; - } - } - - return config; - }; -} - -function patchTsconfig(contents: unknown, rc: GlintRc): Result { - if (typeof contents !== 'string') { - return err(`Could not patch ${JSON.stringify(contents)}: not a string`); - } - - return prepForPatching(contents) - .andThen(parseTsConfig) - .map(toMergedWith(rc)) - .andThen((transformed) => - tryOrElse( - (e) => `Could not patch data:\n\t${JSON.stringify(e)}`, - () => patch(contents, transformed), - ), - ); -} - -function settledToResult( - settledResults: Array>>, -): Array>; -function settledToResult( - settledResults: Array>, -): Array>; -function settledToResult( - settledResults: Array>>, -): Array> { - return settledResults.map((result) => { - if (result.status === 'fulfilled') { - return isInstance(result.value) - ? result.value.mapErr((e) => JSON.stringify(e)) - : ok(result.value); - } else { - return err(JSON.stringify(result.reason)); - } - }); -} - -function neighborTsconfigPath(rcPath: string): string { - return path.join(rcPath, '..', 'tsconfig.json'); -} - -type SplitResults = { - successes: T[]; - failures: E[]; -}; - -function toSplitResults( - settledResults: Array>>, -): SplitResults { - return splitResults(settledToResult(settledResults)); - - function splitResults(rs: Array>): SplitResults { - return rs.reduce( - ({ successes, failures }, r) => - r.match({ - Ok: (value) => ({ successes: successes.concat(value), failures }), - Err: (error) => ({ successes, failures: failures.concat(error) }), - }), - { - successes: [] as T[], - failures: [] as E[], - }, - ); - } -} - -type ArgParseResult = Result; - -function parseArgs(pathArgs: string[]): Promise { - return new Promise((resolve) => { - yargs(pathArgs) - .scriptName('migrate-glintrc') - .usage('$0 ') - .positional('paths', { - description: 'path to the `.glintrc.yml` file(s) to migrate', - array: true, - }) - .demandCommand(1, 'you must supply at least one path') - .wrap(100) - .strict() - .exitProcess(false) - .parseSync(pathArgs, {}, (error, { _: paths }, output) => { - if (error) { - resolve(err(output)); - return; - } - - if (output) { - resolve(ok(output)); - return; - } - - if (!Array.isArray(paths)) { - resolve(err('you must supply at least one path')); - return; - } - - let invalidPaths = paths.filter((p) => typeof p === 'number'); - if (invalidPaths.length > 0) { - let list = invalidPaths.join(', '); - resolve(err(`invalid paths supplied: ${list}`)); - return; - } - - // SAFETY: if there were any non-string paths, we resolved with an error - // above, so we know this is safe. - resolve(ok(paths as string[])); - }); - }); -} - -export function normalizePathString(p: string): string { - return path.resolve(p).split(path.sep).join('/'); -} - -export async function migrate(pathArgs: string[]): Promise> { - let parseResult = await parseArgs(pathArgs); - if (parseResult.isErr) { - return { - successes: [], - failures: [parseResult.error], - }; - } - - if (parseResult.isOk && typeof parseResult.value === 'string') { - return { - successes: [parseResult.value], - failures: [], - }; - } - - // SAFETY: if it was a `string`, we returned above. - let paths = parseResult.value as string[]; - - let parsed = await Promise.allSettled( - paths.map(async (p) => { - const contents = await loadFile(p); - - return contents - .andThen(parseGlintRcFile) - .map((config) => ({ path: p, config })) - .mapErr((err) => `${normalizePathString(p)}: ${err}`); - }), - ).then(toSplitResults); - - let patched = await Promise.allSettled( - parsed.successes.map(async ({ path: rcPath, config: rc }) => { - let tsconfigPath = neighborTsconfigPath(rcPath); - const contents = await loadOrCreateTsconfig(tsconfigPath); - - return patchTsconfig(contents, rc) - .map((patched) => ({ rcPath, tsconfigPath, patched })) - .mapErr((err) => `${normalizePathString(rcPath)}: ${err}`); - }), - ).then(toSplitResults); - - let write = await Promise.allSettled( - patched.successes.map(async ({ rcPath, tsconfigPath, patched }) => { - let writeResult = await saveFile(tsconfigPath, patched); - let normalizedTsconfig = normalizePathString(tsconfigPath); - let normalizedRc = normalizePathString(rcPath); - return writeResult.map( - () => `Updated ${normalizedTsconfig} with contents of ${normalizedRc}`, - ); - }), - ).then(toSplitResults); - - return { - successes: write.successes, - failures: [...parsed.failures, ...patched.failures, ...write.failures], - }; -} diff --git a/packages/scripts/src/migrate-glintrc.ts b/packages/scripts/src/migrate-glintrc.ts deleted file mode 100644 index e98f220ef..000000000 --- a/packages/scripts/src/migrate-glintrc.ts +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env node - -import { migrate } from './lib/_migrate-glintrc.js'; - -let { successes, failures } = await migrate(process.argv.slice(2)); -for (let success of successes) { - console.log(success); -} - -for (let failure of failures) { - console.error(failure); -} - -let exitCode = successes.length > 0 && failures.length === 0 ? 0 : 1; -process.exit(exitCode); diff --git a/test-packages/test-utils/src/project.ts b/test-packages/test-utils/src/project.ts index 7b0ac538e..7ab957385 100644 --- a/test-packages/test-utils/src/project.ts +++ b/test-packages/test-utils/src/project.ts @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; import { createRequire } from 'node:module'; import { execaNode, ExecaChildProcess, Options } from 'execa'; import { type GlintConfigInput } from '@glint/core/config-types'; -import { pathUtils, analyzeProject, ProjectAnalysis } from '@glint/core'; +import { pathUtils } from '@glint/core'; import { startLanguageServer, LanguageServerHandle } from '@volar/test-utils'; import { FullDocumentDiagnosticReport } from '@volar/language-service'; import { URI } from 'vscode-uri';