Skip to content

Commit

Permalink
implementing engine-level fastboot overrides in resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
ef4 committed Jun 27, 2023
1 parent 69a9516 commit da744d9
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 22 deletions.
1 change: 1 addition & 0 deletions packages/compat/src/compat-app-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ export class CompatAppBuilder {
engines: engines.map((engine, index) => ({
packageName: engine.package.name,
root: index === 0 ? this.root : engine.package.root, // first engine is the app, which has been relocated to this.roto
fastbootFiles: {},
activeAddons: [...engine.addons]
.map(a => ({
name: a.name,
Expand Down
1 change: 1 addition & 0 deletions packages/compat/tests/audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('audit', function () {
engines: [
{
packageName: 'audit-this-app',
fastbootFiles: {},
activeAddons: [],
root: app.baseDir,
},
Expand Down
110 changes: 88 additions & 22 deletions packages/core/src/module-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
emberVirtualPeerDeps,
extensionsPattern,
packageName as getPackageName,
packageName,
} from '@embroider/shared-internals';
import { dirname, resolve } from 'path';
import { Package, V2Package, explicitRelative, RewrittenPackageCache } from '@embroider/shared-internals';
Expand Down Expand Up @@ -65,6 +66,7 @@ export interface Options {
interface EngineConfig {
packageName: string;
activeAddons: { name: string; root: string }[];
fastbootFiles: { [appName: string]: { localFilename: string; shadowedFilename: string | undefined } };
root: string;
}

Expand Down Expand Up @@ -156,6 +158,8 @@ export class Resolver {
request = this.handleFastbootCompat(request);
request = this.handleGlobalsCompat(request);
request = this.handleRenaming(request);
// we expect the specifier to be app relative at this point - must be after handleRenaming
request = this.handleAppFastboot(request);
request = this.preHandleExternal(request);

// this should probably stay the last step in beforeResolve, because it can
Expand Down Expand Up @@ -295,6 +299,46 @@ export class Resolver {
return owningPackage;
}

private handleAppFastboot<R extends ModuleRequest>(request: R): R {
let pkg = this.owningPackage(request.fromFile);

if (!pkg) {
return request;
}

if (packageName(request.specifier)) {
// not a relative request, and we're assuming all within-engine requests
// are relative by this point due to `v1 self-import` which happens
// earlier
return request;
}

let engineConfig = this.engineConfig(pkg.name);
if (engineConfig) {
for (let candidate of this.withResolvableExtensions(request.specifier)) {
let fastbootFile = engineConfig.fastbootFiles[candidate];
if (fastbootFile) {
if (fastbootFile.shadowedFilename) {
let { names } = describeExports(readFileSync(resolve(pkg.root, fastbootFile.shadowedFilename), 'utf8'), {});
return logTransition(
'shadowed app fastboot',
request,
request.virtualize(fastbootSwitch(candidate, resolve(pkg.root, 'package.json'), names))
);
} else {
return logTransition(
'unshadowed app fastboot',
request,
request.alias(fastbootFile.localFilename).rehome(resolve(pkg.root, 'package.json'))
);
}
}
}
}

return request;
}

private handleFastbootCompat<R extends ModuleRequest>(request: R): R {
let match = decodeFastbootSwitch(request.fromFile);
if (!match) {
Expand All @@ -315,7 +359,22 @@ export class Resolver {
let pkg = this.owningPackage(match.filename);
if (pkg) {
let rel = explicitRelative(pkg.root, match.filename);
let entry = this.getEntryFromMergeMap(rel, pkg.root);

let engineConfig = this.engineConfig(pkg.name);
if (engineConfig) {
let fastbootFile = engineConfig.fastbootFiles[rel];
if (fastbootFile && fastbootFile.shadowedFilename) {
let targetFile: string;
if (section === 'app-js') {
targetFile = fastbootFile.shadowedFilename;
} else {
targetFile = fastbootFile.localFilename;
}
return request.alias(targetFile).rehome(resolve(pkg.root, 'package.json'));
}
}

let entry = this.getEntryFromMergeMap(rel, pkg.root)?.entry;
if (entry?.type === 'both') {
return request.alias(entry[section].localPath).rehome(resolve(entry[section].packageRoot, 'package.json'));
}
Expand Down Expand Up @@ -951,52 +1010,59 @@ export class Resolver {
return logTransition('fallbackResolve final exit', request);
}

private getEntryFromMergeMap(inEngineSpecifier: string, root: string): MergeEntry | undefined {
private getEntryFromMergeMap(
inEngineSpecifier: string,
root: string
): { entry: MergeEntry; matched: string } | undefined {
let entry: MergeEntry | undefined;
for (let candidate of this.withResolvableExtensions(inEngineSpecifier)) {
entry = this.mergeMap.get(root)?.get(candidate);
if (entry) {
return { entry, matched: candidate };
}
}
}

if (inEngineSpecifier.match(/\.(hbs|js|hbs\.js)$/)) {
entry = this.mergeMap.get(root)?.get(inEngineSpecifier);
private *withResolvableExtensions(filename: string): Generator<string, void, void> {
if (filename.match(/\.(hbs|js|hbs\.js)$/)) {
yield filename;
} else {
// try looking up .hbs .js and .hbs.js in that order for specifiers without extenstions
['.hbs', '.js', '.hbs.js'].forEach(ext => {
if (entry) {
return;
}

entry = this.mergeMap.get(root)?.get(`${inEngineSpecifier}${ext}`);
});
for (let ext of ['.hbs', '.js', '.hbs.js']) {
yield `${filename}${ext}`;
}
}
return entry;
}

private searchAppTree<R extends ModuleRequest>(
request: R,
engine: EngineConfig,
inEngineSpecifier: string
): R | undefined {
let entry = this.getEntryFromMergeMap(inEngineSpecifier, engine.root);
let matched = this.getEntryFromMergeMap(inEngineSpecifier, engine.root);

switch (entry?.type) {
switch (matched?.entry.type) {
case undefined:
return undefined;
case 'app-only':
return request.alias(entry['app-js'].localPath).rehome(resolve(entry['app-js'].packageRoot, 'package.json'));
return request
.alias(matched.entry['app-js'].localPath)
.rehome(resolve(matched.entry['app-js'].packageRoot, 'package.json'));
case 'fastboot-only':
return request
.alias(entry['fastboot-js'].localPath)
.rehome(resolve(entry['fastboot-js'].packageRoot, 'package.json'));
.alias(matched.entry['fastboot-js'].localPath)
.rehome(resolve(matched.entry['fastboot-js'].packageRoot, 'package.json'));
case 'both':
let foundAppJS = this.nodeResolve(
entry['app-js'].localPath,
resolve(entry['app-js'].packageRoot, 'package.json')
matched.entry['app-js'].localPath,
resolve(matched.entry['app-js'].packageRoot, 'package.json')
);
if (foundAppJS.type !== 'real') {
throw new Error(
`${entry['app-js'].fromPackageName} declared ${inEngineSpecifier} in packageJSON.ember-addon.app-js, but that module does not exist`
`${matched.entry['app-js'].fromPackageName} declared ${inEngineSpecifier} in packageJSON.ember-addon.app-js, but that module does not exist`
);
}
let { names } = describeExports(readFileSync(foundAppJS.filename, 'utf8'), {});
return request.virtualize(fastbootSwitch(request.specifier, request.fromFile, names));
return request.virtualize(fastbootSwitch(matched.matched, resolve(engine.root, 'package.json'), names));
}
}

Expand Down
1 change: 1 addition & 0 deletions tests/scenarios/compat-resolver-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Scenarios.fromProject(() => new Project())
packageName: 'my-app',
root: app.dir,
activeAddons: [],
fastbootFiles: {},
},
],
modulePrefix: 'my-app',
Expand Down
67 changes: 67 additions & 0 deletions tests/scenarios/core-resolver-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Scenarios.fromProject(() => new Project())
podModulePrefix?: string;
renamePackages?: Record<string, string>;
addonMeta?: Partial<AddonMeta>;
fastbootFiles?: { [appName: string]: { localFilename: string; shadowedFilename: string | undefined } };
}

let configure: (opts?: ConfigureOpts) => Promise<void>;
Expand Down Expand Up @@ -92,6 +93,7 @@ Scenarios.fromProject(() => new Project())
{
packageName: 'my-app',
root: app.dir,
fastbootFiles: opts?.fastbootFiles ?? {},
activeAddons: [
{
name: 'my-addon',
Expand Down Expand Up @@ -569,6 +571,21 @@ Scenarios.fromProject(() => new Project())
.to('./node_modules/my-addon/_fastboot_/hello-world.js');
});

test(`resolves app fastboot-js`, async function () {
givenFiles({
'./fastboot/hello-world.js': ``,
'app.js': `import "my-app/hello-world"`,
});

await configure({
fastbootFiles: {
'./hello-world.js': { localFilename: './fastboot/hello-world.js', shadowedFilename: undefined },
},
});

expectAudit.module('./app.js').resolves('my-app/hello-world').to('./fastboot/hello-world.js');
});

test(`file exists in both app-js and fastboot-js`, async function () {
givenFiles({
'node_modules/my-addon/_fastboot_/hello-world.js': `
Expand Down Expand Up @@ -616,6 +633,56 @@ Scenarios.fromProject(() => new Project())
switcherModule.resolves('./fastboot').to('./node_modules/my-addon/_fastboot_/hello-world.js');
switcherModule.resolves('./browser').to('./node_modules/my-addon/_app_/hello-world.js');
});

test(`app and fastboot file exists`, async function () {
givenFiles({
'fastboot/hello-world.js': `
export function hello() { return 'fastboot'; }
export class Bonjour {}
export default function() {}
const value = 1;
export { value };
export const x = 2;
`,
'app/hello-world.js': `
export function hello() { return 'browser'; }
export class Bonjour {}
export default function() {}
const value = 1;
export { value };
export const x = 2;
`,
'app.js': `import "my-app/hello-world"`,
});

await configure({
fastbootFiles: {
'./hello-world.js': {
localFilename: './fastboot/hello-world.js',
shadowedFilename: './app/hello-world.js',
},
},
});

let switcherModule = expectAudit.module('./app.js').resolves('my-app/hello-world').toModule();
switcherModule.codeEquals(`
import { macroCondition, getGlobalConfig, importSync } from '@embroider/macros';
let mod;
if (macroCondition(getGlobalConfig().fastboot?.isRunning)) {
mod = importSync("./fastboot");
} else {
mod = importSync("./browser");
}
export default mod.default;
export const hello = mod.hello;
export const Bonjour = mod.Bonjour;
export const value = mod.value;
export const x = mod.x;
`);

switcherModule.resolves('./fastboot').to('./fastboot/hello-world.js');
switcherModule.resolves('./browser').to('./app/hello-world.js');
});
});

Qmodule('legacy-addons', function () {
Expand Down

0 comments on commit da744d9

Please sign in to comment.