@glint/core
Version:
A CLI for performing typechecking on Glimmer templates
313 lines • 16.1 kB
JavaScript
import { statSync as fsStatSync, existsSync } from 'fs';
import { rewriteModule, rewriteDiagnostic, createTransformDiagnostic, } from '../transform/index.js';
import DocumentCache, { templatePathForSynthesizedModule } from './document-cache.js';
export default class TransformManager {
constructor(glintConfig, documents = new DocumentCache(glintConfig)) {
this.glintConfig = glintConfig;
this.documents = documents;
this.transformCache = new Map();
this.resolveModuleNameLiterals = (moduleLiterals, containingFile, redirectedReference, options) => {
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.watchTransformedFile = (path, originalCallback, pollingInterval, options) => {
const { watchFile } = this.ts.sys;
if (!watchFile) {
throw new Error('Internal error: TS `watchFile` unavailable');
}
let { glintConfig, documents } = this;
let callback = (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());
},
};
};
this.watchDirectory = (path, originalCallback, recursive, options) => {
if (!this.ts.sys.watchDirectory) {
throw new Error('Internal error: TS `watchDirectory` unavailable');
}
let callback = (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);
};
this.readDirectory = (rootDir, extensions, excludes, includes, depth) => {
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));
};
this.fileExists = (filename) => {
return this.documents.documentExists(filename);
};
this.readTransformedFile = (filename, encoding) => {
let transformInfo = this.getTransformInfo(filename, encoding);
if (transformInfo?.transformedModule) {
return transformInfo.transformedModule.transformedContents;
}
else {
return this.documents.getDocumentContents(filename, encoding);
}
};
this.getModifiedTime = (filename) => {
// 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;
};
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,
};
}
getTransformDiagnostics(fileName) {
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 [];
});
}
rewriteDiagnostics(diagnostics, fileName) {
let unusedExpectErrors = new Set(this.getExpectErrorDirectives(fileName));
let allDiagnostics = [];
for (let diagnostic of diagnostics) {
let { rewrittenDiagnostic, appliedDirective } = this.rewriteDiagnostic(diagnostic);
if (rewrittenDiagnostic) {
allDiagnostics.push(rewrittenDiagnostic);
}
if (appliedDirective?.kind === 'expect-error') {
unusedExpectErrors.delete(appliedDirective);
}
}
for (let directive of unusedExpectErrors) {
allDiagnostics.push(createTransformDiagnostic(this.ts, directive.source, `Unused '@glint-expect-error' directive.`, directive.location));
}
// When we have syntax errors we get _too many errors_
// if we have an issue with <template> tranformation, we should
// make the user fix their syntax before revealing all the other errors.
let contentTagErrors = allDiagnostics.filter((diagnostic) => diagnostic.isContentTagError);
if (contentTagErrors.length) {
return this.ts.sortAndDeduplicateDiagnostics(contentTagErrors);
}
return this.ts.sortAndDeduplicateDiagnostics(allDiagnostics);
}
getTransformedRange(originalFileName, originalStart, originalEnd) {
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,
};
}
getOriginalRange(transformedFileName, transformedStart, transformedEnd) {
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,
};
}
getTransformedOffset(originalFileName, originalOffset) {
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 };
}
/**
* 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.
*/
getScriptPathForTS(filename) {
// 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 */
findTransformInfoForOriginalFile(originalFileName) {
let transformedFileName = this.glintConfig.environment.isTemplate(originalFileName)
? this.documents.getCompanionDocumentPath(originalFileName)
: originalFileName;
return transformedFileName ? this.getTransformInfo(transformedFileName) : null;
}
getExpectErrorDirectives(filename) {
let transformInfos = filename
? [this.getTransformInfo(filename)]
: [...this.transformCache.values()];
return transformInfos.flatMap((transformInfo) => {
if (!transformInfo.transformedModule)
return [];
return transformInfo.transformedModule.directives.filter((directive) => directive.kind === 'expect-error');
});
}
rewriteDiagnostic(diagnostic) {
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 };
}
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 };
}
}
getTransformInfo(filename, encoding) {
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 = null;
if (environment.isScript(filename) && glintConfig.includesFile(filename)) {
if (documents.documentExists(filename)) {
let contents = documents.getDocumentContents(filename, encoding);
let templatePath = documents.getCompanionDocumentPath(filename);
let canonicalPath = documents.getCanonicalDocumentPath(filename);
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);
}
}
else {
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);
}
}
}
let transformedFileName = glintConfig.getSynthesizedScriptPathForTS(filename);
let cacheEntry = { version, transformedFileName, transformedModule };
this.transformCache.set(documentID, cacheEntry);
return cacheEntry;
}
buildTransformDiagnostics(transformedModule) {
return transformedModule.errors.map((error) => createTransformDiagnostic(this.ts, error.source, error.message, error.location, error.isContentTagError));
}
}
function statSync(path) {
try {
return fsStatSync(path);
}
catch (e) {
if (e instanceof Error && e.code === 'ENOENT') {
return undefined;
}
throw e;
}
}
//# sourceMappingURL=transform-manager.js.map