Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Layer template resolver on top of module resolver #1339

Merged
merged 21 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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