Skip to content

Commit

Permalink
Merge pull request #1363 from embroider-build/simplified-template-res…
Browse files Browse the repository at this point in the history
…olution

Simplified template resolution
  • Loading branch information
ef4 authored Mar 7, 2023
2 parents 8b65699 + ee43b97 commit 97d8b0d
Show file tree
Hide file tree
Showing 47 changed files with 4,577 additions and 4,195 deletions.
3 changes: 2 additions & 1 deletion packages/compat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@types/babel__code-frame": "^7.0.2",
"@types/yargs": "^17.0.3",
"assert-never": "^1.1.0",
"babel-import-util": "^1.1.0",
"babel-plugin-ember-template-compilation": "^2.0.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babylon": "^6.18.0",
Expand Down Expand Up @@ -76,7 +77,7 @@
"@types/node": "^15.12.2",
"@types/resolve": "^1.20.0",
"@types/semver": "^7.3.6",
"code-equality-assertions": "^0.7.0",
"code-equality-assertions": "^0.9.0",
"ember-engines": "^0.8.19",
"scenario-tester": "^2.0.1",
"typescript": "*"
Expand Down
92 changes: 63 additions & 29 deletions packages/compat/src/audit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,22 @@ import {
} from './audit/babel-visitor';
import { AuditBuildOptions, AuditOptions } from './audit/options';
import { buildApp, BuildError, isBuildError } from './audit/build';
import { AuditMessage } from './resolver';

const { JSDOM } = jsdom;

export interface AuditMessage {
message: string;
detail: string;
loc: Loc;
source: string;
filename: string;
}

export interface Loc {
start: { line: number; column: number };
end: { line: number; column: number };
}

export { AuditOptions, AuditBuildOptions, BuildError, isBuildError };

export interface Finding {
Expand All @@ -30,10 +42,12 @@ export interface Finding {
}

export interface Module {
appRelativePath: string;
consumedFrom: (string | RootMarker)[];
imports: Import[];
exports: string[];
resolutions: { [source: string]: string | null };
content: string;
}

interface ResolutionFailure {
Expand All @@ -57,6 +71,8 @@ interface InternalModule {

resolved?: Map<string, string | ResolutionFailure>;

content?: string | Buffer;

linked?: {
exports: Set<string>;
};
Expand Down Expand Up @@ -98,6 +114,7 @@ export class AuditResults {
let results = new this();
for (let [filename, module] of modules) {
let publicModule: Module = {
appRelativePath: explicitRelative(baseDir, filename),
consumedFrom: module.consumedFrom.map(entry => {
if (isRootMarker(entry)) {
return entry;
Expand All @@ -123,6 +140,7 @@ export class AuditResults {
}))
: [],
exports: module.linked?.exports ? [...module.linked.exports] : [],
content: module.content ? module.content.toString() : '',
};
results.modules[explicitRelative(baseDir, filename)] = publicModule;
}
Expand Down Expand Up @@ -187,16 +205,23 @@ export class AuditResults {

export class Audit {
private modules: Map<string, InternalModule> = new Map();
private virtualModules: Map<string, string> = new Map();
private moduleQueue = new Set<string>();
private findings = [] as Finding[];

private frames = new CodeFrameStorage();

static async run(options: AuditBuildOptions): Promise<AuditResults> {
if (!options['reuse-build']) {
await buildApp(options);
let dir: string;

if (options.outputDir) {
dir = options.outputDir;
} else {
if (!options['reuse-build']) {
await buildApp(options);
}
dir = await this.findStage2Output(options);
}
let dir = await this.findStage2Output(options);

let audit = new this(dir, options);
if (options['reuse-build']) {
Expand All @@ -213,6 +238,9 @@ export class Audit {

private static async findStage2Output(options: AuditBuildOptions): Promise<string> {
try {
if (!options.app) {
throw new Error(`AuditBuildOptions needs "app" directory`);
}
return readFileSync(join(options.app, 'dist/.stage2-output'), 'utf8');
} catch (err) {
if (err.code === 'ENOENT') {
Expand Down Expand Up @@ -284,7 +312,12 @@ export class Audit {
this.moduleQueue.delete(filename);
this.debug('visit', filename);
let visitor = this.visitorFor(filename);
let content = readFileSync(filename);
let content: string | Buffer;
if (this.virtualModules.has(filename)) {
content = this.virtualModules.get(filename)!;
} else {
content = readFileSync(filename);
}
// cast is safe because the only way to get into the queue is to go
// through scheduleVisit, and scheduleVisit creates the entry in
// this.modules.
Expand All @@ -298,17 +331,8 @@ export class Audit {
}
} else {
module.parsed = visitResult;
let resolved = new Map() as NonNullable<InternalModule['resolved']>;
for (let dep of visitResult.dependencies) {
let depFilename = await this.resolve(dep, filename);
if (depFilename) {
resolved.set(dep, depFilename);
if (!isResolutionFailure(depFilename)) {
this.scheduleVisit(depFilename, filename);
}
}
}
module.resolved = resolved;
module.resolved = await this.resolveDeps(visitResult.dependencies, filename);
module.content = content;
}
}
}
Expand Down Expand Up @@ -525,21 +549,31 @@ export class Audit {
return this.visitJS(filename, js);
}

private async resolve(specifier: string, fromFile: string) {
let resolution = await this.resolver.nodeResolve(specifier, fromFile);
if (resolution.type === 'virtual') {
// nothing to audit
return undefined;
}
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;
private async resolveDeps(deps: string[], fromFile: string): Promise<InternalModule['resolved']> {
let resolved = new Map() as NonNullable<InternalModule['resolved']>;
for (let dep of deps) {
let resolution = await this.resolver.nodeResolve(dep, fromFile);
switch (resolution.type) {
case 'virtual':
this.virtualModules.set(resolution.filename, resolution.content);
resolved.set(dep, resolution.filename);
this.scheduleVisit(resolution.filename, fromFile);
break;
case 'not_found':
if (['@embroider/macros', '@ember/template-factory'].includes(dep)) {
// the audit process deliberately removes the @embroider/macros babel
// plugins, so the imports are still present and should be left alone.
continue;
}
resolved.set(dep, { isResolutionFailure: true as true });
break;
case 'real':
resolved.set(dep, resolution.filename);
this.scheduleVisit(resolution.filename, fromFile);
break;
}
return { isResolutionFailure: true as true };
}
return resolution.filename;
return resolved;
}

private pushFinding(finding: Finding) {
Expand Down
5 changes: 3 additions & 2 deletions packages/compat/src/audit/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export interface AuditOptions {
}

export interface AuditBuildOptions extends AuditOptions {
'reuse-build': boolean;
app: string;
'reuse-build'?: boolean;
app?: string;
outputDir?: string;
}
181 changes: 181 additions & 0 deletions packages/compat/src/babel-plugin-adjust-imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import { join } from 'path';
import type { NodePath } from '@babel/traverse';
import type * as Babel from '@babel/core';
import type { types as t } from '@babel/core';
import { ImportUtil } from 'babel-import-util';
import { readJSONSync } from 'fs-extra';
import { CompatResolverOptions } from './resolver-transform';
import { Resolver } from '@embroider/core';
import { snippetToDasherizedName } from './dasherize-component-name';
import { ModuleRules, TemplateRules } from './dependency-rules';

export type Options = { appRoot: string };

interface State {
opts: Options;
}

type BabelTypes = typeof t;
type InternalConfig = {
resolverOptions: CompatResolverOptions;
resolver: Resolver;
extraImports: {
[absPath: string]: {
dependsOnComponents?: string[]; // these are already standardized in dasherized form
dependsOnModules?: string[];
};
};
};

export default function main(babel: typeof Babel) {
let t = babel.types;
let cached: InternalConfig | undefined;
function getConfig(appRoot: string) {
if (cached) {
return cached;
}
let resolverOptions: CompatResolverOptions = readJSONSync(join(appRoot, '.embroider', 'resolver.json'));
cached = {
resolverOptions,
resolver: new Resolver(resolverOptions),
extraImports: preprocessExtraImports(resolverOptions),
};
return cached;
}

return {
visitor: {
Program: {
enter(path: NodePath<t.Program>, state: State) {
addExtraImports(t, path, getConfig(state.opts.appRoot));
},
},
},
};
}

(main as any).baseDir = function () {
return join(__dirname, '..');
};

function addExtraImports(t: BabelTypes, path: NodePath<t.Program>, config: InternalConfig) {
let filename: string = path.hub.file.opts.filename;
let entry = config.extraImports[filename];
if (entry) {
let adder = new ImportUtil(t, path);
if (entry.dependsOnModules) {
for (let target of entry.dependsOnModules) {
path.node.body.unshift(amdDefine(t, adder, path, target, target));
}
}
if (entry.dependsOnComponents) {
for (let dasherizedName of entry.dependsOnComponents) {
let pkg = config.resolver.owningPackage(filename);
if (pkg) {
let owningEngine = config.resolver.owningEngine(pkg);
if (owningEngine) {
path.node.body.unshift(
amdDefine(
t,
adder,
path,
`#embroider_compat/components/${dasherizedName}`,
`${owningEngine.packageName}/components/${dasherizedName}`
)
);
}
}
}
}
}

//let componentName = config.resolver.reverseComponentLookup(filename);
}

function amdDefine(t: BabelTypes, adder: ImportUtil, path: NodePath<t.Program>, target: string, runtimeName: string) {
let value = t.callExpression(adder.import(path, '@embroider/macros', 'importSync'), [t.stringLiteral(target)]);
return t.expressionStatement(
t.callExpression(t.memberExpression(t.identifier('window'), t.identifier('define')), [
t.stringLiteral(runtimeName),
t.functionExpression(null, [], t.blockStatement([t.returnStatement(value)])),
])
);
}

function preprocessExtraImports(config: CompatResolverOptions): InternalConfig['extraImports'] {
let extraImports: InternalConfig['extraImports'] = {};
for (let rule of config.activePackageRules) {
if (rule.addonModules) {
for (let [filename, moduleRules] of Object.entries(rule.addonModules)) {
for (let root of rule.roots) {
expandDependsOnRules(root, filename, moduleRules, extraImports);
}
}
}
if (rule.appModules) {
for (let [filename, moduleRules] of Object.entries(rule.appModules)) {
expandDependsOnRules(config.appRoot, filename, moduleRules, extraImports);
}
}
if (rule.addonTemplates) {
for (let [filename, moduleRules] of Object.entries(rule.addonTemplates)) {
for (let root of rule.roots) {
expandInvokesRules(root, filename, moduleRules, extraImports);
}
}
}
if (rule.appTemplates) {
for (let [filename, moduleRules] of Object.entries(rule.appTemplates)) {
expandInvokesRules(config.appRoot, filename, moduleRules, extraImports);
}
}
}
return extraImports;
}

function expandDependsOnRules(
root: string,
filename: string,
rules: ModuleRules,
extraImports: InternalConfig['extraImports']
) {
if (rules.dependsOnModules || rules.dependsOnComponents) {
let entry: InternalConfig['extraImports'][string] = {};
if (rules.dependsOnModules) {
entry.dependsOnModules = rules.dependsOnModules;
}
if (rules.dependsOnComponents) {
entry.dependsOnComponents = rules.dependsOnComponents.map(c => {
let d = snippetToDasherizedName(c);
if (!d) {
throw new Error(`unable to parse component snippet "${c}" from rule ${JSON.stringify(rules, null, 2)}`);
}
return d;
});
}
extraImports[join(root, filename)] = entry;
}
}

function expandInvokesRules(
root: string,
filename: string,
rules: TemplateRules,
extraImports: InternalConfig['extraImports']
) {
if (rules.invokes) {
let dependsOnComponents: string[] = [];
for (let componentList of Object.values(rules.invokes)) {
for (let component of componentList) {
let d = snippetToDasherizedName(component);
if (!d) {
throw new Error(
`unable to parse component snippet "${component}" from rule ${JSON.stringify(rules, null, 2)}`
);
}
dependsOnComponents.push(d);
}
}
extraImports[join(root, filename)] = { dependsOnComponents };
}
}
Loading

0 comments on commit 97d8b0d

Please sign in to comment.