diff --git a/packages/ember-auto-import/ts/auto-import.ts b/packages/ember-auto-import/ts/auto-import.ts index 927c6105..15377c98 100644 --- a/packages/ember-auto-import/ts/auto-import.ts +++ b/packages/ember-auto-import/ts/auto-import.ts @@ -161,14 +161,9 @@ export default class AutoImport implements AutoImportSharedAPI { splitter, environment: this.env, packages: this.packages, - appRoot: this.rootPackage.root, consoleWrite: this.consoleWrite, bundles: this.bundles, - babelConfig: this.rootPackage.cleanBabelConfig(), - browserslist: this.rootPackage.browserslist(), - publicAssetURL: this.rootPackage.publicAssetURL(), webpack, - hasFastboot: this.rootPackage.isFastBootEnabled, v2Addons: this.v2Addons, rootPackage: this.rootPackage, }); diff --git a/packages/ember-auto-import/ts/bundler.ts b/packages/ember-auto-import/ts/bundler.ts index 77a58fbd..50ecccf7 100644 --- a/packages/ember-auto-import/ts/bundler.ts +++ b/packages/ember-auto-import/ts/bundler.ts @@ -4,7 +4,6 @@ import type Package from './package'; import type BundleConfig from './bundle-config'; import type { BundleName } from './bundle-config'; import { buildDebugCallback } from 'broccoli-debug'; -import type { TransformOptions } from '@babel/core'; import type webpack from 'webpack'; const debugTree = buildDebugCallback('ember-auto-import'); @@ -14,13 +13,8 @@ export interface BundlerOptions { environment: 'development' | 'test' | 'production'; splitter: Splitter; packages: Set; - appRoot: string; bundles: BundleConfig; - babelConfig: TransformOptions; - publicAssetURL: string | undefined; - browserslist: string; webpack: typeof webpack; - hasFastboot: boolean; v2Addons: Map; rootPackage: Package; } diff --git a/packages/ember-auto-import/ts/package.ts b/packages/ember-auto-import/ts/package.ts index fffdc5dc..e0c2d1ec 100644 --- a/packages/ember-auto-import/ts/package.ts +++ b/packages/ember-auto-import/ts/package.ts @@ -576,6 +576,16 @@ export default class Package { ]); } + // this is to facilitate testing external dependencies against our cleanBabelConfig. + // We only want to do this in our own testing as it checks for the name of all string + // identifiers and is only ever going to be necessary in our tests. + // previously we tested that a `let` got transpiled to a var, but since the IE11 target + // was removed that test wasn't checking the right thing. This was the simplest way that + // we could think to test that would be future-proof + if (process.env.USE_EAI_BABEL_WATERMARK) { + plugins.push([require.resolve('./watermark-plugin')]); + } + return { // do not use the host project's own `babel.config.js` file. Only a strict // subset of features are allowed in the third-party code we're @@ -612,7 +622,7 @@ export default class Package { if (this.isAddon) { throw new Error(`Only the app can determine the browserslist`); } - // cast here is safe because we just checked isAddon is false + let parent = this._parent as Project; return (parent.targets as { browsers: string[] }).browsers.join(','); } diff --git a/packages/ember-auto-import/ts/watermark-plugin.ts b/packages/ember-auto-import/ts/watermark-plugin.ts new file mode 100644 index 00000000..f95b918f --- /dev/null +++ b/packages/ember-auto-import/ts/watermark-plugin.ts @@ -0,0 +1,17 @@ +import type { NodePath } from '@babel/traverse'; +import type * as t from '@babel/types'; +import type * as Babel from '@babel/core'; + +export default function watermark(babel: { types: typeof t }): Babel.PluginObj { + return { + visitor: { + Identifier(path: NodePath) { + if (path.node.name === '__EAI_WATERMARK__') { + path.replaceWith( + babel.types.stringLiteral('successfully watermarked') + ); + } + }, + }, + }; +} diff --git a/packages/ember-auto-import/ts/webpack.ts b/packages/ember-auto-import/ts/webpack.ts index 9adcff81..2f15037f 100644 --- a/packages/ember-auto-import/ts/webpack.ts +++ b/packages/ember-auto-import/ts/webpack.ts @@ -24,6 +24,7 @@ import makeDebug from 'debug'; import { ensureDirSync, symlinkSync, existsSync } from 'fs-extra'; import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import minimatch from 'minimatch'; +import { TransformOptions } from '@babel/core'; const EXTENSIONS = ['.js', '.ts', '.json']; @@ -157,10 +158,10 @@ export default class WebpackBundler extends Plugin implements Bundler { // this controls webpack's own runtime code generation. You still need // preset-env to preprocess the libraries themselves (which is already // part of this.opts.babelConfig) - target: `browserslist:${this.opts.browserslist}`, + target: `browserslist:${this.opts.rootPackage.browserslist()}`, output: { path: join(this.outputPath, 'assets'), - publicPath: this.opts.publicAssetURL, + publicPath: this.opts.rootPackage.publicAssetURL(), filename: `chunk.[id].[chunkhash].js`, chunkFilename: `chunk.[id].[chunkhash].js`, libraryTarget: 'var', @@ -198,7 +199,16 @@ export default class WebpackBundler extends Plugin implements Bundler { module: { noParse: (file: string) => file === join(stagingDir, 'l.cjs'), rules: [ - this.babelRule(stagingDir), + this.babelRule( + stagingDir, + (filename) => !this.fileIsInApp(filename), + this.opts.rootPackage.cleanBabelConfig() + ), + this.babelRule( + stagingDir, + (filename) => this.fileIsInApp(filename), + this.opts.rootPackage.babelOptions + ), { test: /\.css$/i, use: [ @@ -232,7 +242,10 @@ export default class WebpackBundler extends Plugin implements Bundler { loader: RuleSetUseItem; plugin: WebpackPluginInstance | undefined; } { - if (this.opts.environment === 'production' || this.opts.hasFastboot) { + if ( + this.opts.environment === 'production' || + this.opts.rootPackage.isFastBootEnabled + ) { return { loader: MiniCssExtractPlugin.loader, plugin: new MiniCssExtractPlugin({ @@ -265,22 +278,44 @@ export default class WebpackBundler extends Plugin implements Bundler { return output; } - private babelRule(stagingDir: string): RuleSetRule { - let shouldTranspile = babelFilter(this.skipBabel(), this.opts.appRoot); + private fileIsInApp(filename: string) { + let packageCache = PackageCache.shared( + 'ember-auto-import', + this.opts.rootPackage.root + ); + + const pkg = packageCache.ownerOfFile(filename); + + return pkg?.root === this.opts.rootPackage.root; + } + + private babelRule( + stagingDir: string, + filter: (filename: string) => boolean, + babelConfig: TransformOptions + ): RuleSetRule { + let shouldTranspile = babelFilter( + this.skipBabel(), + this.opts.rootPackage.root + ); return { - test(filename: string) { + test: (filename: string) => { // We don't apply babel to our own stagingDir (it contains only our own // entrypoints that we wrote, and it can use `import()`, which we want // to leave directly for webpack). // // And we otherwise defer to the `skipBabel` setting as implemented by // `@embroider/shared-internals`. - return dirname(filename) !== stagingDir && shouldTranspile(filename); + return ( + dirname(filename) !== stagingDir && + shouldTranspile(filename) && + filter(filename) + ); }, use: { loader: 'babel-loader-8', - options: this.opts.babelConfig, + options: babelConfig, }, }; } @@ -291,7 +326,7 @@ export default class WebpackBundler extends Plugin implements Bundler { private get externalsHandler(): Configuration['externals'] { let packageCache = PackageCache.shared( 'ember-auto-import', - this.opts.appRoot + this.opts.rootPackage.root ); return (params, callback) => { let { context, request, contextInfo } = params; @@ -407,7 +442,7 @@ export default class WebpackBundler extends Plugin implements Bundler { return false; } - if (pkg.root !== this.opts.appRoot) { + if (pkg.root !== this.opts.rootPackage.root) { return false; } @@ -558,7 +593,7 @@ export default class WebpackBundler extends Plugin implements Bundler { deps.dynamicTemplateImports.map(mapTemplateImports), staticTemplateImports: deps.staticTemplateImports.map(mapTemplateImports), - publicAssetURL: this.opts.publicAssetURL, + publicAssetURL: this.opts.rootPackage.publicAssetURL(), }) ); } diff --git a/test-scenarios/babel-test.ts b/test-scenarios/babel-test.ts new file mode 100644 index 00000000..6d8a183f --- /dev/null +++ b/test-scenarios/babel-test.ts @@ -0,0 +1,177 @@ +import merge from 'lodash/merge'; +import { appScenarios } from './scenarios'; +import { PreparedApp, Project } from 'scenario-tester'; +import QUnit from 'qunit'; +const { module: Qmodule, test } = QUnit; + +appScenarios + .map('babel', project => { + let aModuleDependency = new Project({ + files: { + 'package.json': '{ "name": "a-module-dependency", "version": "0.0.1" }', + 'index.js': ` + export default function aModuleDependency() { + if (typeof __EAI_WATERMARK__ === "string" && __EAI_WATERMARK__ === "successfully watermarked") { + return 'module transpiled with cleanBabelConfig'; + } else { + return 'module not transpiled with cleanBabelConfig'; + } + }`, + }, + }); + + let bModuleDependency = new Project({ + files: { + 'package.json': '{ "name": "b-module-dependency", "version": "0.1.1" }', + 'index.js': ` + export function externalLibUsingTranspileTargetFunction() { + try { + return TRANSPILE_TARGET(); + } catch (e) { + return "this function has not been transpiled with our custom babel config"; + } + } + + export function needsToBeTranspiled() { + if (typeof __EAI_WATERMARK__ === "string" && __EAI_WATERMARK__ === "successfully watermarked") { + return 'module transpiled with cleanBabelConfig'; + } else { + return 'module not transpiled with cleanBabelConfig'; + } + }`, + }, + }); + project.addDevDependency(aModuleDependency); + project.addDevDependency(bModuleDependency); + project.linkDevDependency('ember-auto-import', { baseDir: __dirname }); + project.linkDependency('webpack', { baseDir: __dirname }); + + merge(project.files, { + 'ember-cli-build.js': EMBER_CLI_BUILD_JS, + app: { + lib: { + 'example1.js': `export default function() { + return TRANSPILE_TARGET(); + } + + export function ensureNotWatermarked() { + return typeof __EAI_WATERMARK__ === "undefined" + } + `, + }, + controllers: { + 'application.js': APPLICATION_JS, + }, + templates: { + 'application.hbs': '
{{this.moduleResult}}
', + }, + }, + tests: { + acceptance: { + 'basic-test.js': BASIC_TEST_JS, + }, + unit: { + 'babel-transform-app-code-test.js': ` + import { module, test } from 'qunit'; + import example1, { ensureNotWatermarked } from '@ef4/app-template/lib/example1'; + + import { needsToBeTranspiled, externalLibUsingTranspileTargetFunction} from 'b-module-dependency'; + + module('Unit | babel-transform-app-code tests', function () { + test('it successfully transforms code imported using allowAppImports', function (assert) { + assert.equal(example1(), 'it woked'); + assert.ok(ensureNotWatermarked(), 'app code doesnt get 3rd party babel config'); + }); + + test('it transpiles external deps but doesnt use the apps config', function(assert) { + assert.equal(needsToBeTranspiled(), 'module transpiled with cleanBabelConfig'); + assert.equal(externalLibUsingTranspileTargetFunction(), 'this function has not been transpiled with our custom babel config'); + }) + }); + `, + }, + }, + }); + }) + .forEachScenario(scenario => { + Qmodule(scenario.name, function (hooks) { + let app: PreparedApp; + hooks.before(async () => { + app = await scenario.prepare(); + }); + test('yarn test', async function (assert) { + let result = await app.execute('volta run npm -- run test'); + assert.equal(result.exitCode, 0, result.output); + }); + }); + }); + +const EMBER_CLI_BUILD_JS = ` +process.env.USE_EAI_BABEL_WATERMARK = 'true'; +'use strict'; + +const EmberApp = require('ember-cli/lib/broccoli/ember-app'); + +module.exports = function(defaults) { + let app = new EmberApp(defaults, { + babel: { + plugins: [ + function testTransform(babel) { + return { + visitor: { + CallExpression(path) { + let callee = path.get('callee'); + if (!callee.isIdentifier()) { + return; + } + if (callee.node.name === 'TRANSPILE_TARGET') { + path.replaceWith(babel.types.stringLiteral("it woked")) + } + } + } + } + } + ] + }, + autoImport: { + skipBabel: [{ + package: 'a-module-dependency', + semverRange: '*' + }], + allowAppImports: [ + 'lib/**' + ] + } + }); + + return app.toTree(); +}; +`; + +const APPLICATION_JS = ` +import Controller from '@ember/controller'; +import { computed } from '@ember-decorators/object'; +import aModuleDependency from 'a-module-dependency'; + +export default class extends Controller { + @computed() + get moduleResult() { + return aModuleDependency(); + } +} +`; + +const BASIC_TEST_JS = ` +import { module, test } from 'qunit'; +import { visit } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; + +module('Acceptance | basic', function(hooks) { + setupApplicationTest(hooks); + + test('visiting /basic', async function(assert) { + await visit('/'); + assert.dom('[data-test-import-result]').hasText('module not transpiled with cleanBabelConfig'); + }); +}); + `; diff --git a/test-scenarios/skip-babel-test.ts b/test-scenarios/skip-babel-test.ts deleted file mode 100644 index bf2943aa..00000000 --- a/test-scenarios/skip-babel-test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import merge from 'lodash/merge'; -import { appScenarios } from './scenarios'; -import { PreparedApp, Project } from 'scenario-tester'; -import QUnit from 'qunit'; -const { module: Qmodule, test } = QUnit; - -appScenarios - .map('skip-babel', project => { - let aModuleDependency = new Project({ - files: { - 'package.json': '{ "name": "a-module-dependency", "version": "0.0.1" }', - 'index.js': ` - function returnUndefined() { - // this should throw an error unless it's been transpiled - return foo; - let foo = 123; - } - - export default function aModuleDependency() { - try { - if (returnUndefined() === undefined) { - return 'module transpiled'; - } - } catch (e) { - return 'module not transpiled'; - } - }`, - }, - }); - project.addDevDependency(aModuleDependency); - project.linkDevDependency('ember-auto-import', { baseDir: __dirname }); - project.linkDependency('webpack', { baseDir: __dirname }); - - merge(project.files, { - 'ember-cli-build.js': EMBER_CLI_BUILD_JS, - app: { - controllers: { - 'application.js': APPLICATION_JS, - }, - templates: { - 'application.hbs': '
{{this.moduleResult}}
', - }, - }, - tests: { - acceptance: { - 'basic-test.js': BASIC_TEST_JS, - }, - }, - }); - }) - .forEachScenario(scenario => { - Qmodule(scenario.name, function (hooks) { - let app: PreparedApp; - hooks.before(async () => { - app = await scenario.prepare(); - }); - test('yarn test', async function (assert) { - let result = await app.execute('volta run npm -- run test'); - assert.equal(result.exitCode, 0, result.output); - }); - }); - }); - -const EMBER_CLI_BUILD_JS = ` -'use strict'; - -const EmberApp = require('ember-cli/lib/broccoli/ember-app'); - -module.exports = function(defaults) { - let app = new EmberApp(defaults, { - autoImport: { - skipBabel: [{ - package: 'a-module-dependency', - semverRange: '*' - }] - } - }); - - return app.toTree(); -}; -`; - -const APPLICATION_JS = ` -import Controller from '@ember/controller'; -import { computed } from '@ember-decorators/object'; -import aModuleDependency from 'a-module-dependency'; - -export default class extends Controller { - @computed() - get moduleResult() { - return aModuleDependency(); - } -} -`; - -const BASIC_TEST_JS = ` -import { module, test } from 'qunit'; -import { visit } from '@ember/test-helpers'; -import { setupApplicationTest } from 'ember-qunit'; - -module('Acceptance | basic', function(hooks) { - setupApplicationTest(hooks); - - test('visiting /basic', async function(assert) { - await visit('/'); - assert.dom('[data-test-import-result]').hasText('module not transpiled'); - }); -}); - `;