diff --git a/packages/core/src/app.ts b/packages/core/src/app.ts index de4f75779..ff5a0330c 100644 --- a/packages/core/src/app.ts +++ b/packages/core/src/app.ts @@ -147,7 +147,7 @@ export function excludeDotFiles(files: string[]) { } export const CACHE_BUSTING_PLUGIN = { - path: require.resolve('./babel-plugin-cache-busting'), + path: require.resolve('@embroider/shared-internals/src/babel-plugin-cache-busting.js'), version: readJSONSync(`${__dirname}/../package.json`).version, }; @@ -393,7 +393,7 @@ export class AppBuilder { ]); // this is @embroider/macros configured for full stage3 resolution - babel.plugins.push(this.macrosConfig.babelPluginConfig()); + babel.plugins.push(...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 bdd019146..d5e1b2a0f 100644 --- a/packages/macros/src/ember-addon-main.ts +++ b/packages/macros/src/ember-addon-main.ts @@ -69,7 +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)); + babelPlugins.unshift(...MacrosConfig.for(appInstance).babelPluginConfig(source)); } }, diff --git a/packages/macros/src/macros-config.ts b/packages/macros/src/macros-config.ts index b121390d2..d1ccd77f5 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'], { cwd: opts.appPackageRoot }); + + if (!lockFilePath) { + lockFilePath = findUp.sync('package.json', { cwd: opts.appPackageRoot }); + } + + 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/packages/shared-internals/package.json b/packages/shared-internals/package.json index 259211fea..d06b06880 100644 --- a/packages/shared-internals/package.json +++ b/packages/shared-internals/package.json @@ -12,7 +12,8 @@ ".": { "browser": "./src/browser-index.js", "default": "./src/index.js" - } + }, + "./src/babel-plugin-cache-busting.js": "./src/babel-plugin-cache-busting.js" }, "license": "MIT", "author": "Edward Faulkner", diff --git a/packages/core/src/babel-plugin-cache-busting.ts b/packages/shared-internals/src/babel-plugin-cache-busting.ts similarity index 100% rename from packages/core/src/babel-plugin-cache-busting.ts rename to packages/shared-internals/src/babel-plugin-cache-busting.ts diff --git a/tests/fixtures/macro-test/app/controllers/application.js b/tests/fixtures/macro-test/app/controllers/application.js index e75f85988..6c85764c3 100644 --- a/tests/fixtures/macro-test/app/controllers/application.js +++ b/tests/fixtures/macro-test/app/controllers/application.js @@ -1,5 +1,5 @@ import Controller from '@ember/controller'; -import { getOwnConfig, isTesting, isDevelopingApp } from '@embroider/macros'; +import { getOwnConfig, isTesting, isDevelopingApp, macroCondition, dependencySatisfies } from '@embroider/macros'; export default class Application extends Controller { constructor() { @@ -7,5 +7,11 @@ export default class Application extends Controller { this.mode = getOwnConfig()['mode']; this.isTesting = isTesting(); this.isDeveloping = isDevelopingApp(); + + if (macroCondition(dependencySatisfies('lodash', '^4'))) { + this.lodashVersion = 'four'; + } else { + this.lodashVersion = 'three'; + } } } diff --git a/tests/fixtures/macro-test/app/templates/application.hbs b/tests/fixtures/macro-test/app/templates/application.hbs index 1d0d1451e..dad21ec84 100644 --- a/tests/fixtures/macro-test/app/templates/application.hbs +++ b/tests/fixtures/macro-test/app/templates/application.hbs @@ -1,4 +1,5 @@
{{this.mode}}
{{macroGetOwnConfig "count"}}
isDeveloping: {{this.isDeveloping}}
-
isTesting: {{this.isTesting}}
\ No newline at end of file +
isTesting: {{this.isTesting}}
+
{{this.lodashVersion}}
diff --git a/tests/fixtures/macro-test/config/environment.js b/tests/fixtures/macro-test/config/environment.js new file mode 100644 index 000000000..43bb7ed81 --- /dev/null +++ b/tests/fixtures/macro-test/config/environment.js @@ -0,0 +1,52 @@ +'use strict'; + +module.exports = function (environment) { + let ENV = { + modulePrefix: 'app-template', + environment, + rootURL: '/', + locationType: 'auto', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true + }, + EXTEND_PROTOTYPES: { + // Prevent Ember Data from overriding Date.parse. + Date: false, + }, + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + }, + LODASH_VERSION: process.env.LODASH_VERSION || 'four', + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + ENV.APP.autoboot = false; + } + + if (environment === 'production') { + // here you can enable a production-specific feature + } + + return ENV; +}; diff --git a/tests/fixtures/macro-test/tests/acceptance/basic-test.js b/tests/fixtures/macro-test/tests/acceptance/basic-test.js index f3144f51b..6d866ddf4 100644 --- a/tests/fixtures/macro-test/tests/acceptance/basic-test.js +++ b/tests/fixtures/macro-test/tests/acceptance/basic-test.js @@ -1,36 +1,37 @@ import { module, test } from 'qunit'; import { visit, currentURL } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; +import ENV from 'app-template/config/environment'; -module('Acceptance | smoke tests', function(hooks) { +module('Acceptance | smoke tests', function (hooks) { setupApplicationTest(hooks); - test('ensure all scripts in index.html 200', async function(assert) { + test('ensure all scripts in index.html 200', async function (assert) { for (let { src } of document.scripts) { let { status } = await fetch(src); assert.equal(status, 200, `expected: '${src}' to be accessible`); } }); - test('JS getOwnConfig worked', async function(assert) { + test('JS getOwnConfig worked', async function (assert) { await visit('/'); assert.equal(currentURL(), '/'); assert.equal(this.element.querySelector('[data-test-mode]').textContent.trim(), 'amazing'); }); - test('HBS getOwnConfig worked', async function(assert) { + test('HBS getOwnConfig worked', async function (assert) { await visit('/'); assert.equal(currentURL(), '/'); assert.equal(this.element.querySelector('[data-test-count]').textContent.trim(), '42'); }); - test('JS isTesting worked', async function(assert) { + test('JS isTesting worked', async function (assert) { await visit('/'); assert.equal(currentURL(), '/'); assert.equal(this.element.querySelector('[data-test-testing]').textContent.trim(), 'true'); }); - test('/ordered.js is ordered correctly', function(assert) { + test('/ordered.js is ordered correctly', function (assert) { assert.deepEqual(self.ORDER, [ // these come via app.import(name, { prepend: true }); // which ultimately end up in vendor.js @@ -49,4 +50,12 @@ module('Acceptance | smoke tests', function(hooks) { 'ONE', ]); }); + + test('dependency satisfies works correctly', async function (assert) { + await visit('/'); + assert.equal(currentURL(), '/'); + + let expectedVersion = ENV.LODASH_VERSION; + assert.equal(this.element.querySelector('[data-test-version]').textContent.trim(), expectedVersion); + }); }); diff --git a/tests/scenarios/macro-test.ts b/tests/scenarios/macro-test.ts index 7ddcfe3c4..bfd398fe5 100644 --- a/tests/scenarios/macro-test.ts +++ b/tests/scenarios/macro-test.ts @@ -2,10 +2,22 @@ import { appScenarios } from './scenarios'; import { PreparedApp, Project } from 'scenario-tester'; import QUnit from 'qunit'; import merge from 'lodash/merge'; -import { dirname } from 'path'; +import { dirname, join } from 'path'; 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')), { @@ -27,6 +39,7 @@ appScenarios funkySampleAddon.linkDependency('@embroider/macros', { baseDir: __dirname }); macroSampleAddon.linkDependency('@embroider/macros', { baseDir: __dirname }); project.linkDevDependency('@embroider/macros', { baseDir: __dirname }); + project.linkDevDependency('lodash', { baseDir: __dirname }); project.addDevDependency(macroSampleAddon); project.addDevDependency(funkySampleAddon); @@ -34,8 +47,10 @@ appScenarios .forEachScenario(scenario => { Qmodule(scenario.name, function (hooks) { let app: PreparedApp; + hooks.before(async () => { app = await scenario.prepare(); + updateLodashVersion(app, '4.0.0'); }); test(`yarn test`, async function (assert) { @@ -53,5 +68,29 @@ appScenarios let result = await app.execute(`cross-env THROW_UNLESS_PARALLELIZABLE=1 CLASSIC=true yarn test`); assert.equal(result.exitCode, 0, result.output); }); + + test(`@embroider/macros babel caching plugin works`, async function (assert) { + let lodashFourRun = await app.execute(`yarn test`); + assert.equal(lodashFourRun.exitCode, 0, lodashFourRun.output); + + // simulate a different version being installed + updateLodashVersion(app, '3.0.0'); + + let lodashThreeRun = await app.execute(`cross-env LODASH_VERSION=three yarn test`); + assert.equal(lodashThreeRun.exitCode, 0, lodashThreeRun.output); + }); + + 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); + + // simulate a different version being installed + updateLodashVersion(app, '3.0.0'); + + let lodashThreeRun = await app.execute(`cross-env LODASH_VERSION=three CLASSIC=true yarn test`); + assert.equal(lodashThreeRun.exitCode, 0, lodashThreeRun.output); + }); }); }); diff --git a/tests/scenarios/package.json b/tests/scenarios/package.json index 8d863dc5a..02e231bf7 100644 --- a/tests/scenarios/package.json +++ b/tests/scenarios/package.json @@ -4,6 +4,7 @@ "dependencies": { "@types/qunit": "^2.11.1", "fastboot": "^3.1.0", + "fs-extra": "^10.0.0", "globby": "^11.0.3", "jsdom": "^16.2.2", "lodash": "^4.17.20", diff --git a/yarn.lock b/yarn.lock index 827180565..3fbecf00b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5673,15 +5673,15 @@ browserify-zlib@^0.2.0: pako "~1.0.5" browserslist@^3.2.6, browserslist@^4.0.0, browserslist@^4.14.0, browserslist@^4.14.5, browserslist@^4.16.6: - version "4.16.6" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2" - integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ== + version "4.16.7" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.7.tgz#108b0d1ef33c4af1b587c54f390e7041178e4335" + integrity sha512-7I4qVwqZltJ7j37wObBe3SoTz+nS8APaNcrBOlgoirb6/HbEU2XxW/LpUDTCngM6iauwFqmRTuOMfyKnFGY5JA== dependencies: - caniuse-lite "^1.0.30001219" + caniuse-lite "^1.0.30001248" colorette "^1.2.2" - electron-to-chromium "^1.3.723" + electron-to-chromium "^1.3.793" escalade "^3.1.1" - node-releases "^1.1.71" + node-releases "^1.1.73" bser@2.1.1: version "2.1.1" @@ -5917,11 +5917,16 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001219: +caniuse-lite@^1.0.0: version "1.0.30001236" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001236.tgz#0a80de4cdf62e1770bb46a30d884fc8d633e3958" integrity sha512-o0PRQSrSCGJKCPZcgMzl5fUaj5xHe8qA2m4QRvnyY4e1lITqoNkr7q/Oh1NcpGSy0Th97UZ35yoKcINPoq7YOQ== +caniuse-lite@^1.0.30001248: + version "1.0.30001248" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001248.tgz#26ab45e340f155ea5da2920dadb76a533cb8ebce" + integrity sha512-NwlQbJkxUFJ8nMErnGtT0QTM2TJ33xgz4KXJSMIrjXIbDVdaYueGyjOrLKRtJC+rTiWfi6j5cnZN1NBiSBJGNw== + capture-exit@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" @@ -7181,10 +7186,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.723: - version "1.3.752" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.752.tgz#0728587f1b9b970ec9ffad932496429aef750d09" - integrity sha512-2Tg+7jSl3oPxgsBsWKh5H83QazTkmWG/cnNwJplmyZc7KcN61+I10oUgaXSVk/NwfvN3BdkKDR4FYuRBQQ2v0A== +electron-to-chromium@^1.3.793: + version "1.3.796" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.796.tgz#bd74a4367902c9d432d129f265bf4542cddd9f54" + integrity sha512-agwJFgM0FUC1UPPbQ4aII3HamaaJ09fqWGAWYHmzxDWqdmTleCHyyA0kt3fJlTd5M440IaeuBfzXzXzCotnZcQ== elliptic@^6.5.3: version "6.5.4" @@ -10982,6 +10987,15 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-3.0.1.tgz#3794f378c58b342ea7dbbb23095109c4b3b62291" @@ -14563,7 +14577,7 @@ node-notifier@^9.0.1: uuid "^8.3.0" which "^2.0.2" -node-releases@^1.1.71: +node-releases@^1.1.73: version "1.1.73" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.73.tgz#dd4e81ddd5277ff846b80b52bb40c49edf7a7b20" integrity sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==