diff --git a/packages/ember-auto-import/ts/webpack.ts b/packages/ember-auto-import/ts/webpack.ts index f21a6330..d523ca49 100644 --- a/packages/ember-auto-import/ts/webpack.ts +++ b/packages/ember-auto-import/ts/webpack.ts @@ -219,6 +219,25 @@ export default class WebpackBundler extends Plugin implements Bundler { module: { noParse: (file: string) => file === join(stagingDir, 'l.cjs'), rules: [ + { + // Ember addons need their .js files to be interpreted the same + // way whether or not the addon's package.json says `"type": + // "module"`. Without this rule, webpack would give the strict ESM + // treatment to .js files in v2 addons that say `"type": "module"`: + // import specifiers would need to be fully-specified, and + // default-importing one of the CommonJS/AMD modules that we + // externalize (like `@ember/component/template-only` or a v1 + // addon) would yield the module's exports object rather than its + // default export. Opting these files back into "javascript/auto" + // keeps type=module v2 addons working the same as every other v2 + // addon. + test: (filename: string) => + filename.endsWith('.js') && this.fileIsInV2Addon(filename), + type: 'javascript/auto', + resolve: { + fullySpecified: false, + }, + }, this.babelRule( stagingDir, (filename) => !this.fileIsInApp(filename), @@ -298,6 +317,17 @@ export default class WebpackBundler extends Plugin implements Bundler { return output; } + private fileIsInV2Addon(filename: string): boolean { + let packageCache = PackageCache.shared( + 'ember-auto-import', + this.opts.rootPackage.root + ); + + const pkg = packageCache.ownerOfFile(filename); + + return Boolean(pkg?.isV2Addon()); + } + private fileIsInApp(filename: string) { let packageCache = PackageCache.shared( 'ember-auto-import', diff --git a/test-scenarios/v2-addon-type-module-test.ts b/test-scenarios/v2-addon-type-module-test.ts new file mode 100644 index 00000000..4b6c3475 --- /dev/null +++ b/test-scenarios/v2-addon-type-module-test.ts @@ -0,0 +1,217 @@ +import merge from 'lodash/merge'; +import { appScenarios, baseAddonInProject, baseV2Addon } from './scenarios'; +import { PreparedApp, Project } from 'scenario-tester'; +import QUnit from 'qunit'; +const { module: Qmodule, test } = QUnit; + +// v1 ember addons are not statically resolvable, so when a v2 addon imports +// one we externalize it to the runtime AMD loader. Default-importing an AMD +// module from a strict-ESM file is one of the things that breaks without our +// javascript/auto rule, so this addon exists to cover the "type=module v2 +// addon depends on a v1 addon" case explicitly. +function buildInnerV1Addon(project: Project) { + let addon = baseAddonInProject(project); + addon.name = 'inner-v1-addon'; + merge(addon.files, { + addon: { + 'index.js': ` + export default function innerV1Default() { + return 'inner-v1-default-worked'; + } + export function innerV1Named() { + return 'inner-v1-named-worked'; + } + `, + }, + }); + return addon; +} + +// A v2 addon that sets `"type": "module"` in its package.json. All of its +// .js files get webpack's strict ESM treatment unless we intervene, which +// breaks (1) default-imports of the CommonJS/AMD modules we externalize, +// like `@ember/component/template-only`, `@glimmer/component`, and any v1 +// addon, and (2) imports that aren't fully-specified, like directory imports +// and the relative `es-compat2` import that `@embroider/macros` emits for +// `importSync()`. +function buildTypeModuleV2Addon(project: Project) { + let addon = baseV2Addon(); + addon.pkg.name = 'esm-v2-addon'; + addon.pkg.type = 'module'; + addon.pkg.exports = { + '.': './index.js', + './*': './*.js', + './addon-main.cjs': './addon-main.cjs', + }; + + merge(addon.files, { + 'index.js': ` + import { two } from './lib'; + export function useDirectoryImport() { + return two(); + } + `, + lib: { + 'index.js': ` + export function two() { + return 'esm-directory-import-worked'; + } + `, + }, + 'side-effect.js': ` + window.__esm_v2_addon_side_effect = 'esm-side-effect-worked'; + `, + 'uses-import-sync.js': ` + import { importSync } from '@embroider/macros'; + importSync('./side-effect.js'); + `, + 'uses-v1-addon.js': ` + import innerV1Default, { innerV1Named } from 'inner-v1-addon'; + export function useV1AddonDefault() { + return innerV1Default(); + } + export function useV1AddonNamed() { + return innerV1Named(); + } + `, + app: { + components: { + 'esm-hello.js': ` + export { default } from 'esm-v2-addon/components/hello'; + `, + 'esm-counter.js': ` + export { default } from 'esm-v2-addon/components/counter'; + `, + }, + }, + components: { + 'hello.js': ` + import { setComponentTemplate } from "@ember/component"; + import { precompileTemplate } from "@ember/template-compilation"; + import templateOnlyComponent from "@ember/component/template-only"; + export default setComponentTemplate( + precompileTemplate( + "