Skip to content

Commit

Permalink
Merge pull request #1339 from embroider-build/template-resolver-layering
Browse files Browse the repository at this point in the history
Layer template resolver on top of module resolver
  • Loading branch information
ef4 authored Feb 8, 2023
2 parents de6938a + 1948bca commit b869b2c
Show file tree
Hide file tree
Showing 15 changed files with 610 additions and 708 deletions.
81 changes: 14 additions & 67 deletions packages/compat/src/audit.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { readFileSync, readJSONSync } from 'fs-extra';
import { dirname, join, resolve as resolvePath } from 'path';
import resolveModule from 'resolve';
import { AppMeta, explicitRelative, hbsToJS, Resolution, Resolver, ResolverOptions } from '@embroider/core';
import { AppMeta, explicitRelative, hbsToJS, Resolver, ResolverOptions } from '@embroider/core';
import { Memoize } from 'typescript-memoize';
import chalk from 'chalk';
import jsdom from 'jsdom';
Expand All @@ -18,7 +17,6 @@ import {
import { AuditBuildOptions, AuditOptions } from './audit/options';
import { buildApp, BuildError, isBuildError } from './audit/build';
import { AuditMessage } from './resolver';
import assertNever from 'assert-never';

const { JSDOM } = jsdom;

Expand Down Expand Up @@ -84,11 +82,6 @@ function isLinked(module: InternalModule | undefined): module is LinkedInternalM
return Boolean(module?.parsed && module.resolved && module.linked);
}

interface Request {
specifier: string;
fromFile: string;
}

export interface Import {
source: string;
specifiers: {
Expand Down Expand Up @@ -255,15 +248,12 @@ export class Audit {
return config;
}

@Memoize()
private get resolverParams(): ResolverOptions {
let config = {
...readJSONSync(join(this.appDir, '_adjust_imports.json')),
...readJSONSync(join(this.appDir, '_relocated_files.json')),
};
return config;
return readJSONSync(join(this.appDir, '.embroider', 'resolver.json'));
}

private resolver = new Resolver(this.resolverParams);

private debug(message: string, ...args: any[]) {
if (this.options.debug) {
console.log(message, ...args);
Expand Down Expand Up @@ -309,9 +299,8 @@ export class Audit {
} else {
module.parsed = visitResult;
let resolved = new Map() as NonNullable<InternalModule['resolved']>;
let resolver = new Resolver(this.resolverParams);
for (let dep of visitResult.dependencies) {
let depFilename = await this.resolve({ specifier: dep, fromFile: filename }, resolver);
let depFilename = await this.resolve(dep, filename);
if (depFilename) {
resolved.set(dep, depFilename);
if (!isResolutionFailure(depFilename)) {
Expand Down Expand Up @@ -536,63 +525,21 @@ export class Audit {
return this.visitJS(filename, js);
}

private nextRequest(
prevRequest: { specifier: string; fromFile: string },
resolution: Resolution
): { specifier: string; fromFile: string } | undefined {
switch (resolution.result) {
case 'virtual':
// nothing to audit
return undefined;
case 'alias':
// follow the redirect
let specifier = resolution.specifier;
let fromFile = resolution.fromFile ?? prevRequest.fromFile;
return { specifier, fromFile };
case 'rehome':
return { specifier: prevRequest.specifier, fromFile: resolution.fromFile };
case 'continue':
return prevRequest;
default:
throw assertNever(resolution);
private async resolve(specifier: string, fromFile: string) {
let resolution = await this.resolver.nodeResolve(specifier, fromFile);
if (resolution.type === 'virtual') {
// nothing to audit
return undefined;
}
}

private async resolve(request: Request, resolver: Resolver): Promise<string | ResolutionFailure | undefined> {
let current: Request | undefined = request;

while (true) {
current = this.nextRequest(current, resolver.beforeResolve(current.specifier, current.fromFile));
if (!current) {
return;
}

if (['@embroider/macros', '@ember/template-factory'].includes(current.specifier)) {
if (resolution.type === 'not_found') {
if (['@embroider/macros', '@ember/template-factory'].includes(specifier)) {
// the audit process deliberately removes the @embroider/macros babel
// plugins, so the imports are still present and should be left alone.
return;
}
try {
return resolveModule.sync(current.specifier, {
basedir: dirname(current.fromFile),
extensions: this.meta['resolvable-extensions'],
});
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') {
throw err;
}
let retry = this.nextRequest(current, resolver.fallbackResolve(current.specifier, current.fromFile));
if (!retry) {
// the request got virtualized
return;
}
if (retry === current) {
// the fallback has nothing new to offer, so this is a real resolution failure
return { isResolutionFailure: true };
}
current = retry;
}
return { isResolutionFailure: true as true };
}
return resolution.filename;
}

private pushFinding(finding: Finding) {
Expand Down
101 changes: 45 additions & 56 deletions packages/compat/src/compat-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ import {
EmberENV,
Package,
AddonPackage,
Engine,
} from '@embroider/core';
import V1InstanceCache from './v1-instance-cache';
import V1App from './v1-app';
import walkSync from 'walk-sync';
import { join } from 'path';
import { JSDOM } from 'jsdom';
import { V1Config } from './v1-config';
import { statSync, readdirSync, writeFileSync } from 'fs';
import { statSync, readdirSync } from 'fs';
import Options, { optionsWithDefaults } from './options';
import CompatResolver from './resolver';
import CompatResolver, { CompatResolverOptions } from './resolver';
import { activePackageRules, PackageRules, expandModuleRules } from './dependency-rules';
import flatMap from 'lodash/flatMap';
import { Memoize } from 'typescript-memoize';
Expand All @@ -30,8 +31,8 @@ import { sync as resolveSync } from 'resolve';
import { MacrosConfig } from '@embroider/macros/src/node';
import bind from 'bind-decorator';
import { pathExistsSync } from 'fs-extra';
import { tmpdir, ResolverOptions } from '@embroider/core';
import type { Transform } from 'babel-plugin-ember-template-compilation';
import type { Options as ResolverTransformOptions } from './resolver-transform';

interface TreeNames {
appJS: BroccoliNode;
Expand Down Expand Up @@ -88,7 +89,7 @@ function setup(legacyEmberAppInstance: object, options: Required<Options>) {
return { inTrees, instantiate };
}

class CompatAppAdapter implements AppAdapter<TreeNames> {
class CompatAppAdapter implements AppAdapter<TreeNames, CompatResolverOptions> {
constructor(
private root: string,
private appPackage: Package,
Expand Down Expand Up @@ -223,7 +224,7 @@ class CompatAppAdapter implements AppAdapter<TreeNames> {
}

@Memoize()
private resolvableExtensions(): string[] {
resolvableExtensions(): string[] {
// webpack's default is ['.wasm', '.mjs', '.js', '.json']. Keeping that
// subset in that order is sensible, since many third-party libraries will
// expect it to work that way.
Expand Down Expand Up @@ -321,94 +322,82 @@ class CompatAppAdapter implements AppAdapter<TreeNames> {
}

@Memoize()
resolverTransform(): Transform | undefined {
return new CompatResolver({
emberVersion: this.activeAddonChildren().find(a => a.name === 'ember-source')!.packageJSON.version,
root: this.root,
modulePrefix: this.modulePrefix(),
podModulePrefix: this.podModulePrefix(),
options: this.options,
activePackageRules: this.activeRules(),
adjustImportsOptionsPath: this.adjustImportsOptionsPath(),
}).astTransformer();
}

@Memoize()
adjustImportsOptionsPath(): string {
let file = join(this.root, '_adjust_imports.json');
writeFileSync(file, JSON.stringify(this.resolverConfig()));
return file;
}

@Memoize()
resolverConfig(): ResolverOptions {
return this.makeAdjustImportOptions(true);
resolverTransform(resolverConfig: CompatResolverOptions): Transform | undefined {
if (
this.options.staticComponents ||
this.options.staticHelpers ||
this.options.staticModifiers ||
(globalThis as any).embroider_audit
) {
let opts: ResolverTransformOptions = {
appRoot: resolverConfig.appRoot,
emberVersion: resolverConfig.emberVersion,
};
return [require.resolve('./resolver-transform'), opts];
}
}

// this gets serialized out by babel plugin and ast plugin
private makeAdjustImportOptions(outer: boolean): ResolverOptions {
resolverConfig(engines: Engine[]): CompatResolverOptions {
let renamePackages = Object.assign({}, ...this.allActiveAddons.map(dep => dep.meta['renamed-packages']));
let renameModules = Object.assign({}, ...this.allActiveAddons.map(dep => dep.meta['renamed-modules']));

let activeAddons: ResolverOptions['activeAddons'] = {};
let activeAddons: CompatResolverOptions['activeAddons'] = {};
for (let addon of this.allActiveAddons) {
activeAddons[addon.name] = addon.root;
}

return {
let relocatedFiles: CompatResolverOptions['relocatedFiles'] = {};
for (let { destPath, appFiles } of engines) {
for (let [relativePath, originalPath] of appFiles.relocatedFiles) {
relocatedFiles[join(destPath, relativePath)] = originalPath;
}
}

let config: CompatResolverOptions = {
// this part is the base ModuleResolverOptions as required by @embroider/core
activeAddons,
renameModules,
renamePackages,
// "outer" here prevents uncontrolled recursion. We can't know our
// extraImports until after we have the internalTemplateResolver which in
// turn needs some adjustImportsOptions
extraImports: outer ? this.extraImports() : [],
relocatedFiles: {}, // this is the only part we can't completely fill out here. It needs to wait for the AppBuilder to finish smooshing together all appTrees
extraImports: [], // extraImports gets filled in below
relocatedFiles,
resolvableExtensions: this.resolvableExtensions(),

// it's important that this is a persistent location, because we fill it
// up as a side-effect of babel transpilation, and babel is subject to
// persistent caching.
externalsDir: join(tmpdir, 'embroider', 'externals'),
appRoot: this.root,
};
}

// unlike `templateResolver`, this one brings its own simple TemplateCompiler
// along so it's capable of parsing component snippets in people's module
// rules.
@Memoize()
private internalTemplateResolver(): CompatResolver {
return new CompatResolver({
// this is the additional stufff that @embroider/compat adds on top to do
// global template resolving
emberVersion: this.activeAddonChildren().find(a => a.name === 'ember-source')!.packageJSON.version,
root: this.root,
modulePrefix: this.modulePrefix(),
podModulePrefix: this.podModulePrefix(),
options: this.options,
activePackageRules: this.activeRules(),
adjustImportsOptions: this.makeAdjustImportOptions(false),
});
};

this.addExtraImports(config);
return config;
}

private extraImports() {
private addExtraImports(config: CompatResolverOptions) {
let internalResolver = new CompatResolver(config);

let output: { absPath: string; target: string; runtimeName?: string }[][] = [];

for (let rule of this.activeRules()) {
if (rule.addonModules) {
for (let [filename, moduleRules] of Object.entries(rule.addonModules)) {
for (let root of rule.roots) {
let absPath = join(root, filename);
output.push(expandModuleRules(absPath, moduleRules, this.internalTemplateResolver()));
output.push(expandModuleRules(absPath, moduleRules, internalResolver));
}
}
}
if (rule.appModules) {
for (let [filename, moduleRules] of Object.entries(rule.appModules)) {
let absPath = join(this.root, filename);
output.push(expandModuleRules(absPath, moduleRules, this.internalTemplateResolver()));
output.push(expandModuleRules(absPath, moduleRules, internalResolver));
}
}
}
return flatten(output);
config.extraImports = flatten(output);
}

htmlbarsPlugins(): Transform[] {
Expand Down
Loading

0 comments on commit b869b2c

Please sign in to comment.