diff --git a/packages/core/package.json b/packages/core/package.json index e7e35af9a..d50dae34f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -57,7 +57,7 @@ "lodash": "^4.17.10", "pkg-up": "^3.1.0", "resolve": "^1.8.1", - "resolve-package-path": "^1.2.2", + "resolve-package-path": "^4.0.1", "strip-bom": "^3.0.0", "typescript-memoize": "^1.0.0", "walk-sync": "^1.1.3", diff --git a/packages/core/src/app.ts b/packages/core/src/app.ts index 49da8f39f..746dff0a1 100644 --- a/packages/core/src/app.ts +++ b/packages/core/src/app.ts @@ -35,7 +35,7 @@ import partition from 'lodash/partition'; import mergeWith from 'lodash/mergeWith'; import cloneDeep from 'lodash/cloneDeep'; import type { Params as InlineBabelParams } from './babel-plugin-inline-hbs-node'; -import { PortableHint } from './portable'; +import { PortableHint, maybeNodeModuleVersion } from './portable'; import escapeRegExp from 'escape-string-regexp'; import { getEmberExports } from './load-ember-template-compiler'; @@ -144,6 +144,38 @@ export function excludeDotFiles(files: string[]) { return files.filter(file => !file.startsWith('.') && !file.includes('/.')); } +export const CACHE_BUSTING_PLUGIN = { + path: require.resolve('./babel-plugin-cache-busting'), + version: readJSONSync(`${__dirname}/../package.json`).version, +}; + +export function addCachablePlugin(babelConfig: TransformOptions) { + if (Array.isArray(babelConfig.plugins) && babelConfig.plugins.length > 0) { + const plugins = Object.create(null); + plugins[CACHE_BUSTING_PLUGIN.path] = CACHE_BUSTING_PLUGIN.version; + + for (const plugin of babelConfig.plugins) { + let absolutePathToPlugin: string; + if (Array.isArray(plugin) && typeof plugin[0] === 'string') { + absolutePathToPlugin = plugin[0] as string; + } else if (typeof plugin === 'string') { + absolutePathToPlugin = plugin; + } else { + throw new Error(`[Embroider] a babel plugin without an absolute path was from: ${plugin}`); + } + + plugins[absolutePathToPlugin] = maybeNodeModuleVersion(absolutePathToPlugin); + } + + babelConfig.plugins.push([ + CACHE_BUSTING_PLUGIN.path, + { + plugins, + }, + ]); + } +} + class ParsedEmberAsset { kind: 'parsed-ember' = 'parsed-ember'; relativePath: string; @@ -383,7 +415,9 @@ export class AppBuilder { { absoluteRuntime: __dirname, useESModules: true, regenerator: false }, ]); - return makePortable(babel, { basedir: this.root }, this.portableHints); + const portable = makePortable(babel, { basedir: this.root }, this.portableHints); + addCachablePlugin(portable.config); + return portable; } private adjustImportsPlugin(engines: Engine[]): PluginItem { @@ -922,7 +956,12 @@ export class AppBuilder { } cursor = resolve.sync(target, { basedir: dirname(cursor) }); } - return { requireFile: cursor, useMethod: hint.useMethod }; + + return { + requireFile: cursor, + useMethod: hint.useMethod, + packageVersion: maybeNodeModuleVersion(cursor), + }; }); } diff --git a/packages/core/src/babel-plugin-cache-busting.ts b/packages/core/src/babel-plugin-cache-busting.ts new file mode 100644 index 000000000..16d25884e --- /dev/null +++ b/packages/core/src/babel-plugin-cache-busting.ts @@ -0,0 +1,11 @@ +export default function makePlugin(): any { + // Dear future @rwjblue, + // + // This plugin exists as a sentinel plugin which has no behavior, but + // provides a position in the babel configuration to include cache busting + // meta-data about other plugins. Specifically their versions. + // + // Yours sincerely, + // Contributor + return {}; +} diff --git a/packages/core/src/portable.ts b/packages/core/src/portable.ts index 611c2f151..c2f6d9933 100644 --- a/packages/core/src/portable.ts +++ b/packages/core/src/portable.ts @@ -1,6 +1,7 @@ import mapValues from 'lodash/mapValues'; import assertNever from 'assert-never'; import { Memoize } from 'typescript-memoize'; +import resolvePackagePath from 'resolve-package-path'; export const protocol = '__embroider_portable_values__'; const { globalValues, nonce } = setupGlobals(); @@ -13,9 +14,22 @@ export interface PortableResult { export interface PortableHint { requireFile: string; + packageVersion: string | undefined; useMethod?: string; } +const { findUpPackagePath } = resolvePackagePath; + +export function maybeNodeModuleVersion(path: string) { + const packagePath = findUpPackagePath(path); + + if (packagePath === null) { + throw new Error(`Could not find package.json for '${path}'`); + } else { + return require(packagePath).version; // eslint-disable-line @typescript-eslint/no-require-imports + } +} + export class Portable { constructor( private opts: { @@ -83,6 +97,7 @@ export class Portable { embroiderPlaceholder: true, type: 'broccoli-parallel', requireFile: found.requireFile, + packageVersion: maybeNodeModuleVersion(found.requireFile), useMethod: found.useMethod, }, isParallelSafe: true, @@ -154,6 +169,7 @@ interface BroccoliParallelPlaceholder { embroiderPlaceholder: true; type: 'broccoli-parallel'; requireFile: string; + packageVersion: string | undefined; useMethod: string | undefined; buildUsing: string | undefined; params: any; @@ -162,6 +178,7 @@ interface BroccoliParallelPlaceholder { interface HTMLBarsParallelPlaceholder { embroiderPlaceholder: true; type: 'htmlbars-parallel'; + packageVersion: string | undefined; requireFile: string; buildUsing: string; params: any; @@ -193,6 +210,7 @@ function maybeBroccoli(object: any): BroccoliParallelPlaceholder | undefined { embroiderPlaceholder: true, type: 'broccoli-parallel', requireFile: object._parallelBabel.requireFile, + packageVersion: maybeNodeModuleVersion(object._parallelBabel.requireFile), buildUsing: object._parallelBabel.buildUsing, useMethod: object._parallelBabel.useMethod, params: object._parallelBabel.params, @@ -238,6 +256,7 @@ function maybeHTMLBars(object: any): HTMLBarsParallelPlaceholder | undefined { embroiderPlaceholder: true, type: 'htmlbars-parallel', requireFile: object.parallelBabel.requireFile, + packageVersion: maybeNodeModuleVersion(object.parallelBabel.requireFile), buildUsing: String(object.parallelBabel.buildUsing), params: object.parallelBabel.params, }; diff --git a/packages/core/tests/app.test.ts b/packages/core/tests/app.test.ts index cd698dd62..9b0704233 100644 --- a/packages/core/tests/app.test.ts +++ b/packages/core/tests/app.test.ts @@ -1,4 +1,4 @@ -import { excludeDotFiles } from '../src/app'; +import { excludeDotFiles, addCachablePlugin, CACHE_BUSTING_PLUGIN } from '../src/app'; describe('dot files can be excluded', () => { test('excludeDotFiles works', () => { @@ -10,3 +10,46 @@ describe('dot files can be excluded', () => { expect(excludeDotFiles(['foo/bar/baz/.foo.js'])).toEqual([]); }); }); + +describe('cacheable-plugin', function () { + test('noop', function () { + const input = {}; + addCachablePlugin(input); + expect(input).toEqual({}); + }); + + test('no plugins', function () { + const input = { plugins: [] }; + addCachablePlugin(input); + expect(input).toEqual({ plugins: [] }); + }); + + test('some plugins', function () { + const input = { + plugins: [__dirname, [__dirname, []], [`${__dirname}/../`, []], __dirname, [__dirname, []]], + }; + + addCachablePlugin(input); + + expect(input).toEqual({ + plugins: [ + __dirname, + [__dirname, []], + [`${__dirname}/../`, []], + __dirname, + [__dirname, []], + + [ + CACHE_BUSTING_PLUGIN.path, + { + plugins: { + [CACHE_BUSTING_PLUGIN.path]: CACHE_BUSTING_PLUGIN.version, + [__dirname]: CACHE_BUSTING_PLUGIN.version, + [`${__dirname}/../`]: CACHE_BUSTING_PLUGIN.version, + }, + }, + ], + ], + }); + }); +}); diff --git a/packages/core/tests/portable.test.ts b/packages/core/tests/portable.test.ts new file mode 100644 index 000000000..355e08700 --- /dev/null +++ b/packages/core/tests/portable.test.ts @@ -0,0 +1,15 @@ +import { maybeNodeModuleVersion } from '../src/portable'; +import { readJSONSync } from 'fs-extra'; + +const EMBROIDER_CORE_VERSION = readJSONSync(`${__dirname}/../package.json`).version; + +describe('maybeNodeModuleVersion', () => { + test('it', () => { + expect(() => maybeNodeModuleVersion('/dev/null')).toThrow(/Could not find package.json for '\/dev\/null'/); + expect(() => maybeNodeModuleVersion('/does/not/exist')).toThrow( + /Could not find package.json for '\/does\/not\/exist'/ + ); + expect(maybeNodeModuleVersion(__dirname)).toEqual(EMBROIDER_CORE_VERSION); + expect(maybeNodeModuleVersion(__filename)).toEqual(EMBROIDER_CORE_VERSION); + }); +}); diff --git a/yarn.lock b/yarn.lock index ff79eacf6..297f35360 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16642,6 +16642,13 @@ resolve-package-path@^3.1.0: path-root "^0.1.1" resolve "^1.17.0" +resolve-package-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/resolve-package-path/-/resolve-package-path-4.0.1.tgz#0e77271e06c8cc41740d28ef974806a77fdc8880" + integrity sha512-2gb/yU2fSfX22pjDYyevzyOKK9q72XKUFqlAsrfPzZArM4JkIH/Qcme4n3EbaZttObWm/fIFLbPxrXIyiL8wdQ== + dependencies: + path-root "^0.1.1" + resolve-path@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7"