diff --git a/packages/core/src/app.ts b/packages/core/src/app.ts index 747b0f2e0..acbc572a4 100644 --- a/packages/core/src/app.ts +++ b/packages/core/src/app.ts @@ -393,7 +393,7 @@ export class AppBuilder { ]); // this is @embroider/macros configured for full stage3 resolution - babel.plugins.push(this.macrosConfig.babelPluginConfig()); + babel.plugins = babel.plugins.concat(this.macrosConfig.babelPluginConfig()); babel.plugins.push([require.resolve('./template-colocation-plugin')]); diff --git a/packages/macros/package.json b/packages/macros/package.json index e34881645..9493775b6 100644 --- a/packages/macros/package.json +++ b/packages/macros/package.json @@ -27,6 +27,7 @@ "@embroider/shared-internals": "0.43.4", "assert-never": "^1.2.1", "ember-cli-babel": "^7.26.6", + "find-up": "^5.0.0", "lodash": "^4.17.21", "resolve": "^1.20.0", "semver": "^7.3.2" diff --git a/packages/macros/src/ember-addon-main.ts b/packages/macros/src/ember-addon-main.ts index 45ccfcb31..cfa908521 100644 --- a/packages/macros/src/ember-addon-main.ts +++ b/packages/macros/src/ember-addon-main.ts @@ -1,6 +1,4 @@ -import fs from 'fs'; import { join } from 'path'; -import crypto from 'crypto'; import { MacrosConfig, isEmbroiderMacrosPlugin } from './node'; export = { @@ -71,38 +69,7 @@ export = { if (!babelPlugins.some(isEmbroiderMacrosPlugin)) { let appInstance = this._findHost(); let source = appOrAddonInstance.root || appOrAddonInstance.project.root; - babelPlugins.unshift(MacrosConfig.for(appInstance).babelPluginConfig(source)); - - let yarnLockPath = join(appInstance.project.root, 'yarn.lock'); - let npmLockPath = join(appInstance.project.root, 'package-lock.json'); - let pnpmLockPath = join(appInstance.project.root, 'pnpm-lock.yaml'); - let packagePath = join(appInstance.project.root, 'package.json'); - let lockFileBuffer; - - if (fs.existsSync(yarnLockPath)) { - lockFileBuffer = fs.readFileSync(yarnLockPath); - } else if (fs.existsSync(npmLockPath)) { - lockFileBuffer = fs.readFileSync(npmLockPath); - } else if (fs.existsSync(pnpmLockPath)) { - lockFileBuffer = fs.readFileSync(pnpmLockPath); - } else { - // no lock file found, using package.json as a fall back - lockFileBuffer = fs.readFileSync(packagePath); - } - - // @embroider/macros provides a macro called dependencySatisfies which checks if a given - // package name satisfies a given semver version range. Due to the way babel caches this can - // cause a problem where the macro plugin does not run (because it has been cached) but the version - // of the dependency being checked for changes (due to installing a different version). This will lead to - // the old evaluated state being used which might be invalid. This cache busting plugin keeps track of a - // hash representing the lock file of the app and if it ever changes forces babel to rerun its plugins. - // more information in issue #906 - let cacheKey = crypto.createHash('sha256').update(lockFileBuffer).digest('hex'); - babelPlugins.push([ - require.resolve('@embroider/shared-internals/src/babel-plugin-cache-busting.js'), - { version: cacheKey }, - '@embroider/macros cache buster', - ]); + babelOptions.plugins = babelPlugins.concat(MacrosConfig.for(appInstance).babelPluginConfig(source)); } }, diff --git a/packages/macros/src/macros-config.ts b/packages/macros/src/macros-config.ts index b121390d2..41fe8a310 100644 --- a/packages/macros/src/macros-config.ts +++ b/packages/macros/src/macros-config.ts @@ -1,4 +1,7 @@ +import fs from 'fs'; import { join } from 'path'; +import crypto from 'crypto'; +import findUp from 'find-up'; import type { PluginItem } from '@babel/core'; import { PackageCache, getOrCreate } from '@embroider/shared-internals'; import { makeFirstTransform, makeSecondTransform } from './glimmer/ast-transform'; @@ -255,7 +258,7 @@ export default class MacrosConfig { // normal node_modules resolution can find their dependencies. In other words, // owningPackageRoot is needed when you use this inside classic ember-cli, and // it's not appropriate inside embroider. - babelPluginConfig(owningPackageRoot?: string): PluginItem { + babelPluginConfig(owningPackageRoot?: string): PluginItem[] { let self = this; let opts: State['opts'] = { // this is deliberately lazy because we want to allow everyone to finish @@ -281,7 +284,31 @@ export default class MacrosConfig { importSyncImplementation: this.importSyncImplementation, }; - return [join(__dirname, 'babel', 'macros-babel-plugin.js'), opts]; + + let lockFilePath = findUp.sync(['yarn.lock', 'package-lock.json', 'pnpm-lock.yaml']); + + if (!lockFilePath) { + lockFilePath = findUp.sync('package.json'); + } + + let lockFileBuffer = lockFilePath ? fs.readFileSync(lockFilePath) : 'no-cache-key'; + + // @embroider/macros provides a macro called dependencySatisfies which checks if a given + // package name satisfies a given semver version range. Due to the way babel caches this can + // cause a problem where the macro plugin does not run (because it has been cached) but the version + // of the dependency being checked for changes (due to installing a different version). This will lead to + // the old evaluated state being used which might be invalid. This cache busting plugin keeps track of a + // hash representing the lock file of the app and if it ever changes forces babel to rerun its plugins. + // more information in issue #906 + let cacheKey = crypto.createHash('sha256').update(lockFileBuffer).digest('hex'); + return [ + [join(__dirname, 'babel', 'macros-babel-plugin.js'), opts], + [ + require.resolve('@embroider/shared-internals/src/babel-plugin-cache-busting.js'), + { version: cacheKey }, + `@embroider/macros cache buster: ${owningPackageRoot}`, + ], + ]; } static astPlugins(owningPackageRoot?: string): { diff --git a/packages/macros/tests/babel/helpers.ts b/packages/macros/tests/babel/helpers.ts index ddd04fbf9..4f6bde184 100644 --- a/packages/macros/tests/babel/helpers.ts +++ b/packages/macros/tests/babel/helpers.ts @@ -54,7 +54,7 @@ export function makeBabelConfig(_babelVersion: number, macroConfig: MacrosConfig return { filename: join(__dirname, 'sample.js'), presets: [], - plugins: [macroConfig.babelPluginConfig()], + plugins: macroConfig.babelPluginConfig(), }; } @@ -100,7 +100,7 @@ export function allBabelVersions(createTests: CreateTests | CreateTestsWithConfi return { filename: join(__dirname, 'sample.js'), presets: [], - plugins: [config.babelPluginConfig()], + plugins: config.babelPluginConfig(), }; }, diff --git a/tests/scenarios/macro-test.ts b/tests/scenarios/macro-test.ts index 4f866e041..58b4a059b 100644 --- a/tests/scenarios/macro-test.ts +++ b/tests/scenarios/macro-test.ts @@ -7,6 +7,17 @@ import { loadFromFixtureData } from './helpers'; import fs from 'fs-extra'; const { module: Qmodule, test } = QUnit; +function updateLodashVersion(app: PreparedApp, version: string) { + let pkgJson = fs.readJsonSync(join(app.dir, 'package.json')); + let pkgJsonLodash = fs.readJsonSync(join(app.dir, 'node_modules', 'lodash', 'package.json')); + + pkgJson.devDependencies.lodash = version; + pkgJsonLodash.version = version; + + fs.writeJsonSync(join(app.dir, 'package.json'), pkgJson); + fs.writeJsonSync(join(app.dir, 'node_modules', 'lodash', 'package.json'), pkgJsonLodash); +} + appScenarios .map('macro-tests', project => { let macroSampleAddon = Project.fromDir(dirname(require.resolve('../addon-template/package.json')), { @@ -36,8 +47,12 @@ appScenarios .forEachScenario(scenario => { Qmodule(scenario.name, function (hooks) { let app: PreparedApp; + let pkgJson: any; + let pkgJsonLodash: any; + hooks.before(async () => { app = await scenario.prepare(); + updateLodashVersion(app, '4.0.0'); }); test(`yarn test`, async function (assert) { @@ -56,27 +71,30 @@ appScenarios assert.equal(result.exitCode, 0, result.output); }); - test(`multiple dependency check`, async function (assert) { - let pkgJson = fs.readJsonSync(join(app.dir, 'package.json')); - let pkgJsonLodash = fs.readJsonSync(join(app.dir, 'node_modules', 'lodash', 'package.json')); + test(`@embroider/macros babel caching plugin works`, async function (assert) { + let lodashFourRun = await app.execute(`yarn test`); + assert.equal(lodashFourRun.exitCode, 0, lodashFourRun.output); - pkgJson.devDependencies.lodash = '4.0.0'; - pkgJsonLodash.version = '4.0.0'; + // simulate a different version being installed + updateLodashVersion(app, '3.0.0'); - fs.writeJsonSync(join(app.dir, 'package.json'), pkgJson); - fs.writeJsonSync(join(app.dir, 'node_modules', 'lodash', 'package.json'), pkgJsonLodash); + let lodashThreeRun = await app.execute(`cross-env LODASH_VERSION=three yarn test`); + assert.equal(lodashThreeRun.exitCode, 0, lodashThreeRun.output); + }); - let lodashFourRun = await app.execute(`yarn test`); + test(`CLASSIC=true @embroider/macros babel caching plugin works`, async function (assert) { + updateLodashVersion(app, '4.0.1'); + + let lodashFourRun = await app.execute(`cross-env CLASSIC=true yarn test`); assert.equal(lodashFourRun.exitCode, 0, lodashFourRun.output); - // downgrade lodash to 3.0.0 - pkgJson.devDependencies.lodash = '3.0.0'; - pkgJsonLodash.version = '3.0.0'; + // simulate a different version being installed + updateLodashVersion(app, '3.0.0'); fs.writeJsonSync(join(app.dir, 'package.json'), pkgJson); fs.writeJsonSync(join(app.dir, 'node_modules', 'lodash', 'package.json'), pkgJsonLodash); - let lodashThreeRun = await app.execute(`cross-env LODASH_VERSION=three yarn test`); + let lodashThreeRun = await app.execute(`cross-env LODASH_VERSION=three CLASSIC=true yarn test`); assert.equal(lodashThreeRun.exitCode, 0, lodashThreeRun.output); }); });