@glint/core
Version:
A CLI for performing typechecking on Glimmer templates
192 lines • 7.96 kB
JavaScript
import * as path from 'node:path';
import { createRequire } from 'node:module';
import escapeStringRegexp from 'escape-string-regexp';
import SilentError from 'silent-error';
const require = createRequire(import.meta.url);
export const DEFAULT_EXTENSIONS = {
'.hbs': { kind: 'template' },
'.js': { kind: 'untyped-script' },
'.ts': { kind: 'typed-script' },
};
export class GlintEnvironment {
constructor(names, config) {
this.names = names;
this.tagConfig = config.tags ?? {};
this.extensionsConfig = config.extensions ?? {};
this.standaloneTemplateConfig = config.template;
this.tagImportRegexp = this.buildTagImportRegexp();
this.typedScriptExtensions = this.extensionsOfType('typed-script');
this.untypedScriptExtensions = this.extensionsOfType('untyped-script');
this.templateExtensions = this.extensionsOfType('template');
}
static load(specifier, { rootDir = process.cwd() } = {}) {
let envs = normalizeEnvironmentSpecifier(specifier);
let config = loadMergedEnvironmentConfig(envs, rootDir);
return new GlintEnvironment(Object.keys(envs), config);
}
getSourceKind(fileName) {
let extension = path.extname(fileName);
return this.extensionsConfig[extension]?.kind ?? 'unknown';
}
isTypedScript(path) {
return this.getSourceKind(path) === 'typed-script';
}
isUntypedScript(path) {
return this.getSourceKind(path) === 'untyped-script';
}
isScript(path) {
let kind = this.getSourceKind(path);
return kind === 'typed-script' || kind === 'untyped-script';
}
isTemplate(path) {
return this.getSourceKind(path) === 'template';
}
/**
* Returns an array of custom file extensions that the active environment
* is able to handle.
*/
getConfiguredFileExtensions() {
return Object.keys(this.extensionsConfig);
}
/**
* Returns any custom configuration for the given file extension.
*/
getConfigForExtension(extension) {
return this.extensionsConfig[extension];
}
/**
* Returns configuration information for standalone templates in this environment,
* including the location of their backing types module and any special forms
* they support.
*/
getStandaloneTemplateConfig() {
if (this.standaloneTemplateConfig) {
let { typesModule, specialForms } = this.standaloneTemplateConfig;
return { typesModule, specialForms };
}
}
/**
* Given the path of a script, returns an array of candidate paths where
* a template corresponding to that script might be located.
*/
getPossibleTemplatePaths(scriptPath) {
return normalizePathCandidates(this.standaloneTemplateConfig?.getPossibleTemplatePaths(scriptPath) ?? []);
}
/**
* Given the path of a template, returns an array of candidate paths where
* a script corresponding to that script might be located.
*/
getPossibleScriptPaths(templatePath) {
return normalizePathCandidates(this.standaloneTemplateConfig?.getPossibleScriptPaths(templatePath) ?? []);
}
/**
* Indicates whether the given module _may_ have embedded templates in it.
*
* Note that this method is intended to be a cheaper initial pass to avoid needlessly
* parsing modules that definitely don't require rewriting. It therefore may produce
* false positives, but should never give a false negative.
*/
moduleMayHaveEmbeddedTemplates(modulePath, moduleContents) {
let config = this.getConfigForExtension(path.extname(modulePath));
return Boolean(config?.preprocess || config?.transform || this.tagImportRegexp.test(moduleContents));
}
/**
* Returns an array of template tags that should be rewritten according to this
* config object, along with an import specifier indicating where the template types
* for each tag can be found.
*/
getConfiguredTemplateTags() {
return this.tagConfig;
}
buildTagImportRegexp() {
let importSources = Object.keys(this.tagConfig);
let regexpSource = importSources.map(escapeStringRegexp).join('|');
return new RegExp(regexpSource);
}
extensionsOfType(kind) {
return Object.keys(this.extensionsConfig).filter((key) => this.extensionsConfig[key].kind === kind);
}
}
function normalizeEnvironmentSpecifier(specifier) {
if (typeof specifier === 'string') {
return { [specifier]: null };
}
else if (Array.isArray(specifier)) {
return specifier.reduce((obj, name) => ({ ...obj, [name]: null }), {});
}
return specifier;
}
function loadMergedEnvironmentConfig(envs, rootDir) {
let tags = {};
let extensions = { ...DEFAULT_EXTENSIONS };
let template;
for (let [envName, envUserConfig] of Object.entries(envs)) {
let envPath = locateEnvironment(envName, rootDir);
let envModule = require(envPath);
let envFunction = envModule?.default ?? envModule;
if (typeof envFunction !== 'function') {
throw new SilentError(`The specified environment '${envName}', which was loaded from ${envPath}, ` +
`does not appear to be a Glint environment package.`);
}
let config = envFunction(envUserConfig ?? {});
if (config.template) {
if (template) {
throw new SilentError('Multiple configured Glint environments attempted to define behavior for standalone template files.');
}
template = config.template;
}
if (config.tags) {
for (let [importSource, specifiers] of Object.entries(config.tags)) {
tags[importSource] ?? (tags[importSource] = {});
for (let [importSpecifier, tagConfig] of Object.entries(specifiers)) {
if (importSpecifier in tags[importSource]) {
throw new SilentError('Multiple configured Glint environments attempted to define behavior for the tag `' +
importSpecifier +
"` in module '" +
importSource +
"'.");
}
tags[importSource][importSpecifier] = tagConfig;
}
}
}
if (config.extensions) {
for (let [extension, extensionConfig] of Object.entries(config.extensions)) {
if (extension in extensions) {
throw new SilentError('Multiple configured Glint environments attempted to define handling for the ' +
extension +
' file extension.');
}
extensions[extension] = extensionConfig;
}
}
}
return { tags, extensions, template };
}
function locateEnvironment(name, basedir) {
let require = createRequire(path.resolve(basedir, 'package.json'));
for (let candidate of [
// 1st-party package name shorthand
`@glint/environment-${name}/glint-environment-definition`,
// 3rd-party package name shorthand
`glint-environment-${name}/glint-environment-definition`,
// Full package name
`${name}/glint-environment-definition`,
// Literal file path
name,
]) {
try {
return require.resolve(candidate);
}
catch (error) {
if (error?.code !== 'MODULE_NOT_FOUND') {
throw error;
}
}
}
throw new SilentError(`Unable to resolve environment '${name}' from ${basedir}`);
}
function normalizePathCandidates(candidates) {
return candidates.map((candidate) => typeof candidate === 'string' ? { path: candidate, deferTo: [] } : candidate);
}
//# sourceMappingURL=environment.js.map