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

optional ES-module compatibility setting #1548

Merged
merged 1 commit into from
Jul 19, 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
12 changes: 7 additions & 5 deletions packages/compat/src/compat-app-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ export class CompatAppBuilder {
// search, so first one wins.
.reverse(),
})),
amdCompatibility: this.options.amdCompatibility,

// this is the additional stufff that @embroider/compat adds on top to do
// global template resolving
Expand Down Expand Up @@ -1272,7 +1273,7 @@ export class CompatAppBuilder {

// this is a backward-compatibility feature: addons can force inclusion of
// modules.
eagerModules.push('./#embroider-implicit-modules');
eagerModules.push('./-embroider-implicit-modules.js');

let params = { amdModules, fastbootOnlyAmdModules, lazyRoutes, lazyEngines, eagerModules, styles };
if (entryParams) {
Expand Down Expand Up @@ -1337,7 +1338,7 @@ export class CompatAppBuilder {
let amdModules: { runtime: string; buildtime: string }[] = [];
// this is a backward-compatibility feature: addons can force inclusion of
// test support modules.
eagerModules.push('./#embroider-implicit-test-modules');
eagerModules.push('./-embroider-implicit-test-modules.js');

for (let relativePath of engine.tests) {
amdModules.push(this.importPaths(engine, relativePath));
Expand Down Expand Up @@ -1396,6 +1397,10 @@ let d = w.define;
}
{{/if}}

{{#each eagerModules as |eagerModule| ~}}
i("{{js-string-escape eagerModule}}");
{{/each}}

{{#each amdModules as |amdModule| ~}}
d("{{js-string-escape amdModule.runtime}}", function(){ return i("{{js-string-escape amdModule.buildtime}}");});
{{/each}}
Expand All @@ -1408,9 +1413,6 @@ let d = w.define;
}
{{/if}}

{{#each eagerModules as |eagerModule| ~}}
i("{{js-string-escape eagerModule}}");
{{/each}}

{{#if lazyRoutes}}
w._embroiderRouteBundles_ = [
Expand Down
3 changes: 2 additions & 1 deletion packages/compat/tests/audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('audit', function () {
const resolvableExtensions = ['.js', '.hbs'];

let resolverConfig: CompatResolverOptions = {
amdCompatibility: 'cjs',
appRoot: app.baseDir,
modulePrefix: 'audit-this-app',
options: {
Expand Down Expand Up @@ -113,7 +114,7 @@ describe('audit', function () {
'./index.html',
'./app.js',
'./hello.hbs',
'/@embroider/external/@ember/template-factory',
'/@embroider/ext-cjs/@ember/template-factory',
]);
});

Expand Down
66 changes: 52 additions & 14 deletions packages/core/src/module-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import makeDebug from 'debug';
import assertNever from 'assert-never';
import resolveModule from 'resolve';
import {
virtualExternalModule,
virtualExternalESModule,
virtualExternalCJSModule,
virtualPairComponent,
virtualContent,
fastbootSwitch,
Expand All @@ -21,6 +22,7 @@ import {
import { Memoize } from 'typescript-memoize';
import { describeExports } from './describe-exports';
import { readFileSync } from 'fs';
import UserOptions from './options';

const debug = makeDebug('embroider:resolver');
function logTransition<R extends ModuleRequest>(reason: string, before: R, after: R = before): R {
Expand Down Expand Up @@ -62,6 +64,7 @@ export interface Options {
engines: EngineConfig[];
modulePrefix: string;
podModulePrefix?: string;
amdCompatibility: Required<UserOptions['amdCompatibility']>;
}

interface EngineConfig {
Expand Down Expand Up @@ -422,13 +425,13 @@ export class Resolver {
return logTransition(
`dep's implicit modules`,
request,
request.virtualize(resolve(dep.root, `#embroider-${im.type}`))
request.virtualize(resolve(dep.root, `-embroider-${im.type}.js`))
);
} else {
return logTransition(
`own implicit modules`,
request,
request.virtualize(resolve(pkg.root, `#embroider-${im.type}`))
request.virtualize(resolve(pkg.root, `-embroider-${im.type}.js`))
);
}
}
Expand Down Expand Up @@ -713,6 +716,9 @@ export class Resolver {
}

private handleRewrittenPackages<R extends ModuleRequest>(request: R): R {
if (request.isVirtual) {
return request;
}
let requestingPkg = this.packageCache.ownerOfFile(request.fromFile);
if (!requestingPkg) {
return request;
Expand Down Expand Up @@ -856,7 +862,7 @@ export class Resolver {
let packageRelativeSpecifier = explicitRelative(pkg.root, absoluteSpecifier);
if (isExplicitlyExternal(packageRelativeSpecifier, pkg)) {
let publicSpecifier = absoluteSpecifier.replace(pkg.root, pkg.name);
return external('beforeResolve', request, publicSpecifier);
return this.external('beforeResolve', request, publicSpecifier);
}

// if the requesting file is in an addon's app-js, the relative request
Expand All @@ -877,11 +883,11 @@ export class Resolver {
// absolute package imports can also be explicitly external based on their
// full specifier name
if (isExplicitlyExternal(specifier, pkg)) {
return external('beforeResolve', request, specifier);
return this.external('beforeResolve', request, specifier);
}

if (emberVirtualPackages.has(packageName) && !pkg.hasDependency(packageName)) {
return external('beforeResolve emberVirtualPackages', request, specifier);
return this.external('beforeResolve emberVirtualPackages', request, specifier);
}

if (emberVirtualPeerDeps.has(packageName) && !pkg.hasDependency(packageName)) {
Expand All @@ -908,7 +914,7 @@ export class Resolver {
if (!dep.isEmberPackage()) {
// classic ember addons can only import non-ember dependencies if they
// have ember-auto-import.
return external('v1 package without auto-import', request, specifier);
return this.external('v1 package without auto-import', request, specifier);
}
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') {
Expand All @@ -932,6 +938,43 @@ export class Resolver {
return request;
}

private external<R extends ModuleRequest>(label: string, request: R, specifier: string): R {
if (this.options.amdCompatibility === 'cjs') {
let filename = virtualExternalCJSModule(specifier);
return logTransition(label, request, request.virtualize(filename));
} else if (this.options.amdCompatibility) {
let entry = this.options.amdCompatibility.es.find(
entry => entry[0] === specifier || entry[0] + '/index' === specifier
);
if (!entry && request.specifier === 'require') {
entry = ['require', ['default', 'has']];
}
if (!entry) {
throw new Error(
`A module tried to resolve "${request.specifier}" and didn't find it (${label}).

- Maybe a dependency declaration is missing?
- Remember that v1 addons can only import non-Ember-addon NPM dependencies if they include ember-auto-import in their dependencies.
- If this dependency is available in the AMD loader (because someone manually called "define()" for it), you can configure a shim like:

amdCompatibility: {
es: [
["${request.specifier}", ["default", "yourNamedExportsGoHere"]],
]
}

`
);
}
let filename = virtualExternalESModule(specifier, entry[1]);
return logTransition(label, request, request.virtualize(filename));
} else {
throw new Error(
`Embroider's amdCompatibility option is disabled, but something tried to use it to access "${request.specifier}"`
);
}
}

fallbackResolve<R extends ModuleRequest>(request: R): R {
let { specifier, fromFile } = request;

Expand Down Expand Up @@ -1036,13 +1079,13 @@ export class Resolver {
// runtime. Native v2 packages can only get this behavior in the
// isExplicitlyExternal case above because they need to explicitly ask for
// externals.
return external('v1 catch-all fallback', request, specifier);
return this.external('v1 catch-all fallback', request, specifier);
} else {
// native v2 packages don't automatically externalize *everything* the way
// auto-upgraded packages do, but they still externalize known and approved
// ember virtual packages (like @ember/component)
if (emberVirtualPackages.has(packageName)) {
return external('emberVirtualPackages', request, specifier);
return this.external('emberVirtualPackages', request, specifier);
}
}

Expand Down Expand Up @@ -1203,8 +1246,3 @@ function reliablyResolvable(pkg: V2Package, packageName: string) {
function appImportInAppTree(inPackage: Package, inLogicalPackage: Package, importedPackageName: string): boolean {
return inPackage !== inLogicalPackage && importedPackageName === inLogicalPackage.name;
}

function external<R extends ModuleRequest>(label: string, request: R, specifier: string): R {
let filename = virtualExternalModule(specifier);
return logTransition(label, request, request.virtualize(filename));
}
45 changes: 44 additions & 1 deletion packages/core/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,49 @@ export default interface Options {
// useMethod optionally lets you pick which property within the module to use.
// If not provided, we use the module.exports itself.
pluginHints?: { resolve: string[]; useMethod?: string }[];

// Ember classically used a runtime AMD module loader.
//
// Embroider *can* locate the vast majority of modules statically, but when an
// addon is doing something highly dynamic (like injecting AMD `define()`
// statements directly into a <script>), we still may not be able to locate
// them. So Embroider can emit a placeholder shim for the missing module that
// attempts to locate it at runtime in the classic AMD loader.
//
// This shim can be generated as commonJS (cjs) or an ES module (es). The
// default is cjs.
//
// CJS is useful when you're building in an environment that is tolerant of
// mixed CJS and ES modules (like Webpack), because the set of exported names
// from the module doesn't need to be known in advance. For this reason, CJS
// shims are generated on-demand and are fully-automatic. This is the default
// for maximum backward-compatibility.
//
// ES is useful when you're building in a strict ES module environment (like
// Vite). It's fully spec-defined and doesn't suffer interoperability
// complexities. The downside is, we can only emit a correct shim for a module
// if you tell embroider what set of names it exports. Example:

// emberExternals: {
// es: [
// // import { first, second } from "my-library";
// ['my-library', ['first', 'second']],
// // import Example from "my-library/components/example";
// ['my-library/components/example', ['default']]
// ];
// }

// It is not recommended to use `es` mode without also using
// staticEmberSource, because without staticEmberSource ember itself needs
// many external shims.
//
// false means we don't do any external shimming.
amdCompatibility?:
| false
| 'cjs'
| {
es: [string, string[]][];
};
}

export function optionsWithDefaults(options?: Options): Required<Options> {
Expand All @@ -90,7 +133,7 @@ export function optionsWithDefaults(options?: Options): Required<Options> {
staticAppPaths: [],
skipBabel: [],
pluginHints: [],
implicitModulesStrategy: 'relativePaths' as 'relativePaths',
amdCompatibility: 'cjs' as const,
};
if (options) {
return Object.assign(defaults, options);
Expand Down
Loading