diff --git a/packages/compat/src/compat-addons.ts b/packages/compat/src/compat-addons.ts index a5eeb9731..499441b58 100644 --- a/packages/compat/src/compat-addons.ts +++ b/packages/compat/src/compat-addons.ts @@ -29,7 +29,7 @@ export default class CompatAddons implements Stage { ensureDirSync(options.workspaceDir!); this.destDir = realpathSync(options.workspaceDir!); - this.packageCache = v1Cache.packageCache.moveAddons(v1Cache.app.root, this.destDir); + this.packageCache = v1Cache.app.packageCache.moveAddons(this.destDir); this.inputPath = v1Cache.app.root; this.treeSyncMap = new WeakMap(); this.v1Cache = v1Cache; @@ -57,16 +57,16 @@ export default class CompatAddons implements Stage { async ready(): Promise<{ outputPath: string; packageCache: PackageCache }> { await this.deferReady.promise; writeJSONSync(join(this.destDir, '.embroider-reuse.json'), { - appDestDir: relative(this.destDir, this.packageCache.appDestDir), + appDestDir: relative(this.destDir, this.packageCache.appRoot), }); return { - outputPath: this.packageCache.appDestDir, + outputPath: this.packageCache.appRoot, packageCache: this.packageCache, }; } private get appDestDir(): string { - return this.packageCache.appDestDir; + return this.packageCache.appRoot; } private get app(): Package { diff --git a/packages/compat/src/compat-app.ts b/packages/compat/src/compat-app.ts index 777da89a7..751502e5d 100644 --- a/packages/compat/src/compat-app.ts +++ b/packages/compat/src/compat-app.ts @@ -71,7 +71,7 @@ function setup(legacyEmberAppInstance: object, options: Required) { }; let instantiate = async (root: string, appSrcDir: string, packageCache: PackageCache) => { - let appPackage = packageCache.getApp(appSrcDir); + let appPackage = packageCache.get(appSrcDir); let adapter = new CompatAppAdapter( root, appPackage, @@ -82,7 +82,13 @@ function setup(legacyEmberAppInstance: object, options: Required) { packageCache.get(join(root, 'node_modules', '@embroider', 'synthesized-styles')) ); - return new AppBuilder(root, appPackage, adapter, options, MacrosConfig.for(legacyEmberAppInstance)); + return new AppBuilder( + root, + appPackage, + adapter, + options, + MacrosConfig.for(legacyEmberAppInstance, appSrcDir) + ); }; return { inTrees, instantiate }; @@ -387,6 +393,7 @@ class CompatAppAdapter implements AppAdapter { // persistent caching. externalsDir: join(tmpdir, 'embroider', 'externals'), emberNeedsModulesPolyfill, + appRoot: this.root, }; } diff --git a/packages/compat/src/moved-package-cache.ts b/packages/compat/src/moved-package-cache.ts index bbff25c7d..f5c7f7463 100644 --- a/packages/compat/src/moved-package-cache.ts +++ b/packages/compat/src/moved-package-cache.ts @@ -12,13 +12,13 @@ function assertNoTildeExpansion(source: string, target: string) { } } export class MovablePackageCache extends PackageCache { - constructor(private macrosConfig: MacrosConfig) { - super(); + constructor(private macrosConfig: MacrosConfig, appRoot: string) { + super(appRoot); } - moveAddons(appSrcDir: string, destDir: string): MovedPackageCache { + moveAddons(destDir: string): MovedPackageCache { // start with the plain old app package - let origApp = this.getApp(appSrcDir); + let origApp = this.get(this.appRoot); // discover the set of all packages that will need to be moved into the // workspace @@ -30,7 +30,6 @@ export class MovablePackageCache extends PackageCache { export class MovedPackageCache extends PackageCache { readonly app!: Package; - readonly appDestDir: string; private commonSegmentCount: number; readonly moved: Map = new Map(); readonly unmovedAddons: Set; @@ -43,14 +42,17 @@ export class MovedPackageCache extends PackageCache { private origApp: Package, private macrosConfig: MacrosConfig ) { - super(); + // this is the initial appRoot, which we can't know until just below here + super('not-the-real-root'); // that gives us our common segment count, which enables localPath mapping this.commonSegmentCount = movedSet.commonSegmentCount; - // so we can now determine where the app will go inside the workspace - this.appDestDir = this.localPath(origApp.root); - this.macrosConfig.packageMoved(origApp.root, this.appDestDir); + // so we can now determine where the app will go inside the workspace. THIS + // is where we fix 'not-the-real-root' from above. + this.appRoot = this.localPath(origApp.root); + + this.macrosConfig.packageMoved(origApp.root, this.appRoot); for (let originalPkg of movedSet.packages) { // Update our rootCache so we don't need to rediscover moved packages diff --git a/packages/compat/src/prebuilt-addons.ts b/packages/compat/src/prebuilt-addons.ts index 83aab498e..8437b5fbc 100644 --- a/packages/compat/src/prebuilt-addons.ts +++ b/packages/compat/src/prebuilt-addons.ts @@ -31,12 +31,12 @@ export default class PrebuiltAddons implements Stage { } class RehomedPackageCache extends PackageCache { - constructor(private appSrcDir: string, private appDestDir: string) { - super(); + constructor(private appSrcDir: string, appDestDir: string) { + super(appDestDir); } basedir(pkg: Package): string { if (pkg.root === this.appSrcDir) { - return this.appDestDir; + return this.appRoot; } return super.basedir(pkg); } diff --git a/packages/compat/src/resolver.ts b/packages/compat/src/resolver.ts index 06574aecb..fc3f5c6c4 100644 --- a/packages/compat/src/resolver.ts +++ b/packages/compat/src/resolver.ts @@ -416,7 +416,7 @@ export default class CompatResolver implements Resolver { } absPathToRuntimePath(absPath: string, owningPackage?: { root: string; name: string }) { - let pkg = owningPackage || PackageCache.shared('embroider-stage3').ownerOfFile(absPath); + let pkg = owningPackage || PackageCache.shared('embroider-stage3', this.params.root).ownerOfFile(absPath); if (pkg) { let packageRuntimeName = pkg.name; for (let [runtimeName, realName] of Object.entries(this.adjustImportsOptions.renamePackages)) { @@ -454,7 +454,7 @@ export default class CompatResolver implements Resolver { private tryHelper(path: string, from: string): Resolution | null { let parts = path.split('@'); if (parts.length > 1 && parts[0].length > 0) { - let cache = PackageCache.shared('embroider-stage3'); + let cache = PackageCache.shared('embroider-stage3', this.params.root); let packageName = parts[0]; let renamed = this.adjustImportsOptions.renamePackages[packageName]; if (renamed) { @@ -488,7 +488,7 @@ export default class CompatResolver implements Resolver { private tryModifier(path: string, from: string): Resolution | null { let parts = path.split('@'); if (parts.length > 1 && parts[0].length > 0) { - let cache = PackageCache.shared('embroider-stage3'); + let cache = PackageCache.shared('embroider-stage3', this.params.root); let packageName = parts[0]; let renamed = this.adjustImportsOptions.renamePackages[packageName]; if (renamed) { @@ -527,7 +527,7 @@ export default class CompatResolver implements Resolver { private tryComponent(path: string, from: string, withRuleLookup = true): Resolution | null { let parts = path.split('@'); if (parts.length > 1 && parts[0].length > 0) { - let cache = PackageCache.shared('embroider-stage3'); + let cache = PackageCache.shared('embroider-stage3', this.params.root); let packageName = parts[0]; let renamed = this.adjustImportsOptions.renamePackages[packageName]; if (renamed) { diff --git a/packages/compat/src/v1-app.ts b/packages/compat/src/v1-app.ts index 5e35a6c3c..5fe07ed87 100644 --- a/packages/compat/src/v1-app.ts +++ b/packages/compat/src/v1-app.ts @@ -9,7 +9,6 @@ import { Node } from 'broccoli-node-api'; import { V1Config, WriteV1Config } from './v1-config'; import { WriteV1AppBoot, ReadV1AppBoot } from './v1-appboot'; import { - PackageCache, TemplateCompiler, TemplateCompilerPlugins, AddonMeta, @@ -23,7 +22,7 @@ import { writeJSONSync, ensureDirSync, copySync, readdirSync, pathExistsSync, ex import AddToTree from './add-to-tree'; import DummyPackage, { OwningAddon } from './dummy-package'; import { TransformOptions } from '@babel/core'; -import { isEmbroiderMacrosPlugin } from '@embroider/macros/src/node'; +import { isEmbroiderMacrosPlugin, MacrosConfig } from '@embroider/macros/src/node'; import resolvePackagePath from 'resolve-package-path'; import Concat from 'broccoli-concat'; import mapKeys from 'lodash/mapKeys'; @@ -33,6 +32,7 @@ import prepHtmlbarsAstPluginsForUnwrap from './prepare-htmlbars-ast-plugins'; import { readFileSync } from 'fs'; import type { Options as HTMLBarsOptions } from 'ember-cli-htmlbars'; import semver from 'semver'; +import { MovablePackageCache } from './moved-package-cache'; // This controls and types the interface between our new world and the classic // v1 app instance. @@ -51,12 +51,12 @@ export default class V1App { // used to signal that this is a dummy app owned by a particular addon owningAddon: Package | undefined; - static create(app: EmberAppInstance, packageCache: PackageCache): V1App { + static create(app: EmberAppInstance): V1App { if (app.project.pkg.keywords?.includes('ember-addon')) { // we are a dummy app, which is unfortunately weird and special - return new V1DummyApp(app, packageCache); + return new V1DummyApp(app); } else { - return new V1App(app, packageCache); + return new V1App(app); } } @@ -64,7 +64,11 @@ export default class V1App { private _implicitScripts: string[] = []; private _implicitStyles: string[] = []; - protected constructor(protected app: EmberAppInstance, protected packageCache: PackageCache) {} + packageCache: MovablePackageCache; + + protected constructor(protected app: EmberAppInstance) { + this.packageCache = new MovablePackageCache(MacrosConfig.for(app, this.root), this.root); + } // always the name from package.json. Not the one that apps may have weirdly // customized. @@ -742,9 +746,9 @@ function throwIfMissing( } class V1DummyApp extends V1App { - constructor(app: EmberAppInstance, packageCache: PackageCache) { - super(app, packageCache); - this.owningAddon = new OwningAddon(this.app.project.root, packageCache); + constructor(app: EmberAppInstance) { + super(app); + this.owningAddon = new OwningAddon(this.app.project.root, this.packageCache); this.packageCache.seed(this.owningAddon); this.packageCache.seed(new DummyPackage(this.root, this.owningAddon, this.packageCache)); } diff --git a/packages/compat/src/v1-instance-cache.ts b/packages/compat/src/v1-instance-cache.ts index 9897a2681..d2d7392a7 100644 --- a/packages/compat/src/v1-instance-cache.ts +++ b/packages/compat/src/v1-instance-cache.ts @@ -6,10 +6,8 @@ import V1App from './v1-app'; import V1Addon, { V1AddonConstructor } from './v1-addon'; import { pathExistsSync } from 'fs-extra'; import { AddonInstance, getOrCreate } from '@embroider/core'; -import { MovablePackageCache } from './moved-package-cache'; import Options from './options'; import isEqual from 'lodash/isEqual'; -import { MacrosConfig } from '@embroider/macros/src/node'; export default class V1InstanceCache { static caches: WeakMap = new WeakMap(); @@ -28,12 +26,10 @@ export default class V1InstanceCache { private addons: Map = new Map(); app: V1App; - packageCache: MovablePackageCache; orderIdx: number; private constructor(oldApp: any, private options: Required) { - this.packageCache = new MovablePackageCache(MacrosConfig.for(oldApp)); - this.app = V1App.create(oldApp, this.packageCache); + this.app = V1App.create(oldApp); this.orderIdx = 0; // no reason to do this on demand because oldApp already eagerly loaded @@ -75,7 +71,7 @@ export default class V1InstanceCache { private addAddon(addonInstance: AddonInstance) { this.orderIdx += 1; let Klass = this.adapterClass(addonInstance); - let v1Addon = new Klass(addonInstance, this.options, this.app, this.packageCache, this.orderIdx); + let v1Addon = new Klass(addonInstance, this.options, this.app, this.app.packageCache, this.orderIdx); let pkgs = getOrCreate(this.addons, v1Addon.root, () => []); pkgs.push(v1Addon); addonInstance.addons.forEach(a => this.addAddon(a)); diff --git a/packages/compat/tests/audit.test.ts b/packages/compat/tests/audit.test.ts index fe174ed41..48c102508 100644 --- a/packages/compat/tests/audit.test.ts +++ b/packages/compat/tests/audit.test.ts @@ -46,6 +46,7 @@ describe('audit', function () { relocatedFiles: {}, resolvableExtensions, emberNeedsModulesPolyfill: true, + appRoot: app.baseDir, }, }), }, diff --git a/packages/compat/tests/resolver.test.ts b/packages/compat/tests/resolver.test.ts index 97feca39f..901acfa83 100644 --- a/packages/compat/tests/resolver.test.ts +++ b/packages/compat/tests/resolver.test.ts @@ -107,6 +107,7 @@ describe('compat-resolver', function () { relocatedFiles: {}, resolvableExtensions: ['.js', '.hbs'], emberNeedsModulesPolyfill: false, + appRoot: appDir, }, otherOptions.adjustImportsImports ), diff --git a/packages/core/src/app.ts b/packages/core/src/app.ts index 46c65cec9..8c613c109 100644 --- a/packages/core/src/app.ts +++ b/packages/core/src/app.ts @@ -418,6 +418,7 @@ export class AppBuilder { let colocationOptions: ColocationOptions = { packageGuard: true, + appRoot: this.root, }; babel.plugins.push([ require.resolve('@embroider/shared-internals/src/template-colocation-plugin'), @@ -872,7 +873,7 @@ export class AppBuilder { async build(inputPaths: OutputPaths) { if (this.adapter.env !== 'production') { - this.macrosConfig.enableAppDevelopment(this.root); + this.macrosConfig.enablePackageDevelopment(this.root); this.macrosConfig.enableRuntimeMode(); } for (let pkgRoot of this.adapter.developingAddons()) { @@ -1007,7 +1008,7 @@ export class AppBuilder { ); writeFileSync( join(this.root, '_babel_filter_.js'), - babelFilterTemplate({ skipBabel: this.options.skipBabel }), + babelFilterTemplate({ skipBabel: this.options.skipBabel, appRoot: this.root }), 'utf8' ); } @@ -1468,8 +1469,8 @@ function stringOrBufferEqual(a: string | Buffer, b: string | Buffer): boolean { const babelFilterTemplate = compile(` const { babelFilter } = require('@embroider/core'); -module.exports = babelFilter({{{json-stringify skipBabel}}}); -`) as (params: { skipBabel: Options['skipBabel'] }) => string; +module.exports = babelFilter({{{json-stringify skipBabel}}}, "{{{js-string-escape appRoot}}}"); +`) as (params: { skipBabel: Options['skipBabel']; appRoot: string }) => string; // meta['renamed-modules'] has mapping from classic filename to real filename. // This takes that and converts it to the inverst mapping from real import path diff --git a/packages/core/src/babel-plugin-adjust-imports.ts b/packages/core/src/babel-plugin-adjust-imports.ts index e2a72b684..e2c2f25ce 100644 --- a/packages/core/src/babel-plugin-adjust-imports.ts +++ b/packages/core/src/babel-plugin-adjust-imports.ts @@ -41,10 +41,9 @@ export interface Options { relocatedFiles: { [relativePath: string]: string }; resolvableExtensions: string[]; emberNeedsModulesPolyfill: boolean; + appRoot: string; } -const packageCache = PackageCache.shared('embroider-stage3'); - type DefineExpressionPath = NodePath & { node: t.CallExpression & { arguments: [t.StringLiteral, t.ArrayExpression, Function]; @@ -160,9 +159,9 @@ function isExplicitlyExternal(specifier: string, fromPkg: V2Package): boolean { return Boolean(fromPkg.isV2Addon() && fromPkg.meta['externals'] && fromPkg.meta['externals'].includes(specifier)); } -function isResolvable(packageName: string, fromPkg: Package): false | Package { +function isResolvable(packageName: string, fromPkg: Package, appRoot: string): false | Package { try { - let dep = packageCache.resolve(packageName, fromPkg); + let dep = PackageCache.shared('embroider-stage3', appRoot).resolve(packageName, fromPkg); if (!dep.isEmberPackage() && !fromPkg.hasDependency('ember-auto-import')) { return false; } @@ -262,7 +261,7 @@ function handleExternal(specifier: string, sourceFile: AdjustFile, opts: Options } // first try to resolve from the destination package - if (isResolvable(packageName, relocatedPkg)) { + if (isResolvable(packageName, relocatedPkg, opts.appRoot)) { if (!pkg.meta['auto-upgraded']) { throw new Error( `${pkg.name} is trying to import ${packageName} from within its app tree. This is unsafe, because ${pkg.name} can't control which dependencies are resolvable from the app` @@ -271,7 +270,7 @@ function handleExternal(specifier: string, sourceFile: AdjustFile, opts: Options return specifier; } else { // second try to resolve from the source package - let targetPkg = isResolvable(packageName, pkg); + let targetPkg = isResolvable(packageName, pkg, opts.appRoot); if (targetPkg) { if (!pkg.meta['auto-upgraded']) { throw new Error( @@ -284,7 +283,7 @@ function handleExternal(specifier: string, sourceFile: AdjustFile, opts: Options } } } else { - if (isResolvable(packageName, pkg)) { + if (isResolvable(packageName, pkg, opts.appRoot)) { if (!pkg.meta['auto-upgraded'] && !reliablyResolvable(pkg, packageName)) { throw new Error( `${pkg.name} is trying to import from ${packageName} but that is not one of its explicit dependencies` @@ -357,7 +356,7 @@ export default function main(babel: typeof Babel) { Program: { enter(path: NodePath, state: State) { let opts = ensureOpts(state); - state.adjustFile = new AdjustFile(path.hub.file.opts.filename, opts.relocatedFiles); + state.adjustFile = new AdjustFile(path.hub.file.opts.filename, opts.relocatedFiles, opts.appRoot); let adder = new ImportUtil(t, path); addExtraImports(adder, t, path, opts.extraImports); }, @@ -480,8 +479,10 @@ function amdDefine(t: BabelTypes, adder: ImportUtil, path: NodePath, class AdjustFile { readonly originalFile: string; + private packageCache: PackageCache; - constructor(public name: string, relocatedFiles: Options['relocatedFiles']) { + constructor(public name: string, relocatedFiles: Options['relocatedFiles'], appRoot: string) { + this.packageCache = PackageCache.shared('embroider-stage3', appRoot); if (!name) { throw new Error(`bug: adjust-imports plugin was run without a filename`); } @@ -494,13 +495,13 @@ class AdjustFile { @Memoize() owningPackage(): Package | undefined { - return packageCache.ownerOfFile(this.originalFile); + return this.packageCache.ownerOfFile(this.originalFile); } @Memoize() relocatedIntoPackage(): Package | undefined { if (this.isRelocated) { - return packageCache.ownerOfFile(this.name); + return this.packageCache.ownerOfFile(this.name); } } } diff --git a/packages/core/src/build-stage.ts b/packages/core/src/build-stage.ts index af5c131ff..35ac64f05 100644 --- a/packages/core/src/build-stage.ts +++ b/packages/core/src/build-stage.ts @@ -28,9 +28,6 @@ export default class BuildStage implements Stage { return new WaitForTrees(this.augment(this.inTrees), this.annotation, async treePaths => { if (!this.active) { let { outputPath, packageCache } = await this.prevStage.ready(); - if (!packageCache) { - packageCache = new PackageCache(); - } this.outputPath = outputPath; this.packageCache = packageCache; this.active = await this.instantiate(outputPath, this.prevStage.inputPath, packageCache); diff --git a/packages/core/src/stage.ts b/packages/core/src/stage.ts index 03adcae0b..8a9624ce6 100644 --- a/packages/core/src/stage.ts +++ b/packages/core/src/stage.ts @@ -28,9 +28,8 @@ export default interface Stage { // to not change once you get it. readonly outputPath: string; - // This optionally allows the Stage to share a PackageCache with the next - // Stage, as an optimization. If the Stage uses a PackageCache, it _should_ - // share it here. - readonly packageCache?: PackageCache; + // Stages must propagate their PackageCache forward to the next stage so we + // don't repeat a lot of resolving work. + readonly packageCache: PackageCache; }>; } diff --git a/packages/core/tests/babel-plugin-adjust-imports.test.ts b/packages/core/tests/babel-plugin-adjust-imports.test.ts index 65295f386..d7ae0648d 100644 --- a/packages/core/tests/babel-plugin-adjust-imports.test.ts +++ b/packages/core/tests/babel-plugin-adjust-imports.test.ts @@ -95,6 +95,7 @@ describe('babel-plugin-adjust-imports', function () { externalsDir: 'test', resolvableExtensions: ['.js', '.hbs'], emberNeedsModulesPolyfill: false, + appRoot: '/nonexistent', }; { diff --git a/packages/core/tests/inline-hbs.test.ts b/packages/core/tests/inline-hbs.test.ts index 59abae09c..5a865ab37 100644 --- a/packages/core/tests/inline-hbs.test.ts +++ b/packages/core/tests/inline-hbs.test.ts @@ -242,5 +242,6 @@ class StubResolver implements Resolver { relocatedFiles: {}, resolvableExtensions: ['.js', '.hbs'], emberNeedsModulesPolyfill: true, + appRoot: '/tmp/nonexistent', }; } diff --git a/packages/macros/src/babel/dependency-satisfies.ts b/packages/macros/src/babel/dependency-satisfies.ts index 77fe825cd..fc465686e 100644 --- a/packages/macros/src/babel/dependency-satisfies.ts +++ b/packages/macros/src/babel/dependency-satisfies.ts @@ -2,17 +2,14 @@ import type { NodePath } from '@babel/traverse'; import type { types as t } from '@babel/core'; import State, { sourceFile } from './state'; import { satisfies } from 'semver'; -import { PackageCache } from '@embroider/shared-internals'; import error from './error'; import { assertArray } from './evaluate-json'; -const packageCache = PackageCache.shared('embroider-stage3'); - export default function dependencySatisfies(path: NodePath, state: State): boolean { if (path.node.arguments.length !== 2) { throw error(path, `dependencySatisfies takes exactly two arguments, you passed ${path.node.arguments.length}`); } - let [packageName, range] = path.node.arguments; + const [packageName, range] = path.node.arguments; if (packageName.type !== 'StringLiteral') { throw error( assertArray(path.get('arguments'))[0], @@ -27,11 +24,12 @@ export default function dependencySatisfies(path: NodePath, st } let sourceFileName = sourceFile(path, state); try { - let us = packageCache.ownerOfFile(sourceFileName); - if (!us) { + let us = state.packageCache.ownerOfFile(sourceFileName); + if (!us || us.dependencies.every(dep => dep.name !== packageName.value)) { return false; } - let version = packageCache.resolve(packageName.value, us).version; + + let version = state.packageCache.resolve(packageName.value, us).version; return satisfies(version, range.value, { includePrerelease: true, }); diff --git a/packages/macros/src/babel/get-config.ts b/packages/macros/src/babel/get-config.ts index 1016fbdbe..45f3e3eae 100644 --- a/packages/macros/src/babel/get-config.ts +++ b/packages/macros/src/babel/get-config.ts @@ -7,7 +7,6 @@ import assertNever from 'assert-never'; import type * as Babel from '@babel/core'; import type { types as t } from '@babel/core'; -const packageCache = PackageCache.shared('embroider-stage3'); export type Mode = 'own' | 'getGlobalConfig' | 'package'; function getPackage(path: NodePath, state: State, mode: 'own' | 'package'): { root: string } | null { @@ -29,7 +28,7 @@ function getPackage(path: NodePath, state: State, mode: 'own' } else { assertNever(mode); } - return targetPackage(sourceFile(path, state), packageName, packageCache); + return targetPackage(sourceFile(path, state), packageName, state.packageCache); } // this evaluates to the actual value of the config. It can be used directly by the Evaluator. diff --git a/packages/macros/src/babel/macros-babel-plugin.ts b/packages/macros/src/babel/macros-babel-plugin.ts index 5f48abf56..abec0019c 100644 --- a/packages/macros/src/babel/macros-babel-plugin.ts +++ b/packages/macros/src/babel/macros-babel-plugin.ts @@ -11,8 +11,6 @@ import failBuild from './fail-build'; import { Evaluator, buildLiterals } from './evaluate-json'; import type * as Babel from '@babel/core'; -const packageCache = PackageCache.shared('embroider-stage3'); - export default function main(context: typeof Babel): unknown { let t = context.types; let visitor = { @@ -24,6 +22,7 @@ export default function main(context: typeof Babel): unknown { state.calledIdentifiers = new Set(); state.neededRuntimeImports = new Map(); state.neededEagerImports = new Map(); + state.packageCache = PackageCache.shared('embroider-stage3', state.opts.appPackageRoot); }, exit(path: NodePath, state: State) { pruneMacroImports(path); @@ -54,7 +53,7 @@ export default function main(context: typeof Babel): unknown { enter(path: NodePath, state: State) { let id = path.get('id'); if (id.isIdentifier() && id.node.name === 'initializeRuntimeMacrosConfig') { - let pkg = packageCache.ownerOfFile(sourceFile(path, state)); + let pkg = state.packageCache.ownerOfFile(sourceFile(path, state)); if (pkg && pkg.name === '@embroider/macros') { inlineRuntimeConfig(path, state, context); } @@ -268,7 +267,7 @@ function addEagerImports(path: NodePath, state: State, t: typeof Babe function ownedByEmberPackage(path: NodePath, state: State) { let filename = sourceFile(path, state); - let pkg = packageCache.ownerOfFile(filename); + let pkg = state.packageCache.ownerOfFile(filename); return pkg && pkg.isEmberPackage(); } diff --git a/packages/macros/src/babel/state.ts b/packages/macros/src/babel/state.ts index a036a4c09..41621f53a 100644 --- a/packages/macros/src/babel/state.ts +++ b/packages/macros/src/babel/state.ts @@ -19,6 +19,8 @@ export default interface State { // module value. neededEagerImports: Map; + packageCache: PackageCache; + opts: { userConfigs: { [pkgRoot: string]: unknown; @@ -37,9 +39,9 @@ export default interface State { // path to their package root directory isDevelopingPackageRoots: string[]; - // the package root directory of the app, if the app is under active - // development. Needed so that we can get consistent answers to - // `isDevelopingApp` and `isDeveopingThisPackage` + // the package root directory of the app. Needed so that we can get + // consistent answers to `isDevelopingApp` and `isDeveopingThisPackage`, as + // well as consistent handling of Package devDependencies vs dependencies. appPackageRoot: string; embroiderMacrosConfigMarker: true; @@ -74,11 +76,9 @@ export function sourceFile(path: NodePath, state: State): string { return state.opts.owningPackageRoot || path.hub.file.opts.filename; } -const packageCache = PackageCache.shared('embroider-stage3'); - export function owningPackage(path: NodePath, state: State): Package { let file = sourceFile(path, state); - let pkg = packageCache.ownerOfFile(file); + let pkg = state.packageCache.ownerOfFile(file); if (!pkg) { throw new Error(`unable to determine which npm package owns the file ${file}`); } diff --git a/packages/macros/src/ember-addon-main.ts b/packages/macros/src/ember-addon-main.ts index 70ebde031..766718214 100644 --- a/packages/macros/src/ember-addon-main.ts +++ b/packages/macros/src/ember-addon-main.ts @@ -1,4 +1,6 @@ +import { AppInstance } from '@embroider/shared-internals'; import { join } from 'path'; +import { BuildPluginParams } from './glimmer/ast-transform'; import { MacrosConfig, isEmbroiderMacrosPlugin } from './node'; export = { @@ -9,31 +11,32 @@ export = { let parentOptions = (parent.options = parent.options || {}); let ownOptions = (parentOptions['@embroider/macros'] = parentOptions['@embroider/macros'] || {}); - const appInstance = this._findHost(); - this.setMacrosConfig(MacrosConfig.for(appInstance)); + let appInstance: AppInstance = this._findHost(); + let macrosConfig = getMacrosConfig(appInstance); + this.setMacrosConfig(macrosConfig); + // if parent is an addon it has root. If it's an app it has project.root. let source = parent.root || parent.project.root; if (ownOptions.setOwnConfig) { - MacrosConfig.for(appInstance).setOwnConfig(source, ownOptions.setOwnConfig); + macrosConfig.setOwnConfig(source, ownOptions.setOwnConfig); } if (ownOptions.setConfig) { for (let [packageName, config] of Object.entries(ownOptions.setConfig)) { - MacrosConfig.for(appInstance).setConfig(source, packageName, config as object); + macrosConfig.setConfig(source, packageName, config as object); } } if (appInstance.env !== 'production') { - let macros = MacrosConfig.for(appInstance); - // tell the macros where our app is - macros.enableAppDevelopment(join(appInstance.project.configPath(), '..', '..')); + // tell the macros our app is under development + macrosConfig.enablePackageDevelopment(getAppRoot(appInstance)); // also tell them our root project is under development. This can be // different, in the case where this is an addon and the app is the dummy // app. - macros.enablePackageDevelopment(appInstance.project.root); + macrosConfig.enablePackageDevelopment(appInstance.project.root); // keep the macros in runtime mode for development & testing - macros.enableRuntimeMode(); + macrosConfig.enableRuntimeMode(); } // add our babel plugin to our parent's babel @@ -51,9 +54,9 @@ export = { // forbidden and consuming it becomes allowed. There's no existing hook with // that timing. const originalToTree = appInstance.toTree; - appInstance.toTree = function () { - MacrosConfig.for(appInstance).finalize(); - return originalToTree.apply(appInstance, arguments); + appInstance.toTree = function (...args) { + macrosConfig.finalize(); + return originalToTree.apply(appInstance, args); }; }, @@ -68,7 +71,7 @@ export = { let babelPlugins = (babelOptions.plugins = babelOptions.plugins || []); if (!babelPlugins.some(isEmbroiderMacrosPlugin)) { let appInstance = this._findHost(); - babelPlugins.unshift(...MacrosConfig.for(appInstance).babelPluginConfig(appOrAddonInstance)); + babelPlugins.unshift(...getMacrosConfig(appInstance).babelPluginConfig(appOrAddonInstance)); } }, @@ -81,12 +84,18 @@ export = { // MacrosConfig.astPlugins is static because in classic ember-cli, at this // point there's not yet an appInstance, so we defer getting it and // calling setConfig until our included hook. - let { plugins, setConfig, getConfigForPlugin } = MacrosConfig.astPlugins((this as any).parent.root); + let { plugins, setConfig, lazyParams } = MacrosConfig.astPlugins((this as any).parent.root); this.setMacrosConfig = setConfig; plugins.forEach((plugin, index) => { let name = `@embroider/macros/${index}`; let baseDir = join(__dirname, '..'); - let projectRoot = (this as any).parent.root; + + let params: BuildPluginParams = { + name, + firstTransformParams: lazyParams, + methodName: index === 0 ? 'makeSecondTransform' : 'makeFirstTransform', + baseDir, + }; registry.add('htmlbars-ast-plugin', { name, @@ -94,15 +103,7 @@ export = { parallelBabel: { requireFile: join(__dirname, 'glimmer', 'ast-transform.js'), buildUsing: 'buildPlugin', - params: { - name, - get configs() { - return getConfigForPlugin(); - }, - methodName: index === 0 ? 'makeSecondTransform' : 'makeFirstTransform', - projectRoot: projectRoot, - baseDir, - }, + params, }, baseDir: () => baseDir, }); @@ -112,3 +113,13 @@ export = { options: {}, }; + +// this can differ from appInstance.project.root because Dummy apps are terrible +function getAppRoot(appInstance: AppInstance): string { + return join(appInstance.project.configPath(), '..', '..'); +} + +function getMacrosConfig(appInstance: AppInstance): MacrosConfig { + let appRoot = join(appInstance.project.configPath(), '..', '..'); + return MacrosConfig.for(appInstance, appRoot); +} diff --git a/packages/macros/src/glimmer/ast-transform.ts b/packages/macros/src/glimmer/ast-transform.ts index 85cf86a5a..f777ef763 100644 --- a/packages/macros/src/glimmer/ast-transform.ts +++ b/packages/macros/src/glimmer/ast-transform.ts @@ -4,39 +4,62 @@ import dependencySatisfies from './dependency-satisfies'; import { maybeAttrs } from './macro-maybe-attrs'; import { macroIfBlock, macroIfExpression, macroIfMustache } from './macro-condition'; import { failBuild } from './fail-build'; +import { PackageCache } from '@embroider/shared-internals'; -export function buildPlugin(params: { +export interface BuildPluginParams { + // Glimmer requires this on ast transforms. name: string; + + // this is the location of @embroider/macros itself. Glimmer requires this on + // ast transforms. baseDir: string; - projectRoot: string; + methodName: string; - configs: any; -}) { + + firstTransformParams: FirstTransformParams; +} + +export interface FirstTransformParams { + // this is the location of the particular package (app or addon) that is + // depending on @embroider/macros *if* we're in a classic build. Under + // embroider the build is global and there's no single packageRoot. + packageRoot: string | undefined; + + // this is the path to the topmost package + appRoot: string; + + // this holds all the actual user configs that were sent into the macros + configs: { [packageRoot: string]: object }; +} + +export function buildPlugin(params: BuildPluginParams) { return { name: params.name, plugin: params.methodName === 'makeFirstTransform' - ? makeFirstTransform({ userConfigs: params.configs, baseDir: params.projectRoot }) + ? makeFirstTransform(params.firstTransformParams) : makeSecondTransform(), baseDir: () => params.baseDir, }; } -export function makeFirstTransform(opts: { userConfigs: { [packageRoot: string]: unknown }; baseDir?: string }) { +export function makeFirstTransform(opts: FirstTransformParams) { function embroiderFirstMacrosTransform(env: { syntax: { builders: any }; meta: { moduleName: string }; filename: string; }) { - if (!opts.baseDir && !env.filename) { - throw new Error(`bug in @embroider/macros. Running without baseDir but don't have filename.`); + if (!opts.packageRoot && !env.filename) { + throw new Error(`bug in @embroider/macros. Running without packageRoot but don't have filename.`); } + let packageCache = PackageCache.shared('embroider-stage3', opts.appRoot); + let scopeStack: string[][] = []; - // baseDir is set when we run inside classic ember-cli. Otherwise we're in + // packageRoot is set when we run inside classic ember-cli. Otherwise we're in // Embroider, where we can use absolute filenames. - const moduleName = opts.baseDir ? env.meta.moduleName : env.filename; + const moduleName = opts.packageRoot ? env.meta.moduleName : env.filename; return { name: '@embroider/macros/first', @@ -62,13 +85,19 @@ export function makeFirstTransform(opts: { userConfigs: { [packageRoot: string]: return; } if (node.path.original === 'macroGetOwnConfig') { - return literal(getConfig(node, opts.userConfigs, opts.baseDir, moduleName, true), env.syntax.builders); + return literal( + getConfig(node, opts.configs, opts.packageRoot, moduleName, true, packageCache), + env.syntax.builders + ); } if (node.path.original === 'macroGetConfig') { - return literal(getConfig(node, opts.userConfigs, opts.baseDir, moduleName, false), env.syntax.builders); + return literal( + getConfig(node, opts.configs, opts.packageRoot, moduleName, false, packageCache), + env.syntax.builders + ); } if (node.path.original === 'macroDependencySatisfies') { - return literal(dependencySatisfies(node, opts.baseDir, moduleName), env.syntax.builders); + return literal(dependencySatisfies(node, opts.packageRoot, moduleName, packageCache), env.syntax.builders); } }, MustacheStatement(node: any) { @@ -80,17 +109,23 @@ export function makeFirstTransform(opts: { userConfigs: { [packageRoot: string]: } if (node.path.original === 'macroGetOwnConfig') { return env.syntax.builders.mustache( - literal(getConfig(node, opts.userConfigs, opts.baseDir, moduleName, true), env.syntax.builders) + literal( + getConfig(node, opts.configs, opts.packageRoot, moduleName, true, packageCache), + env.syntax.builders + ) ); } if (node.path.original === 'macroGetConfig') { return env.syntax.builders.mustache( - literal(getConfig(node, opts.userConfigs, opts.baseDir, moduleName, false), env.syntax.builders) + literal( + getConfig(node, opts.configs, opts.packageRoot, moduleName, false, packageCache), + env.syntax.builders + ) ); } if (node.path.original === 'macroDependencySatisfies') { return env.syntax.builders.mustache( - literal(dependencySatisfies(node, opts.baseDir, moduleName), env.syntax.builders) + literal(dependencySatisfies(node, opts.packageRoot, moduleName, packageCache), env.syntax.builders) ); } }, @@ -101,11 +136,8 @@ export function makeFirstTransform(opts: { userConfigs: { [packageRoot: string]: (embroiderFirstMacrosTransform as any).parallelBabel = { requireFile: __filename, buildUsing: 'makeFirstTransform', - get params() { - return { - userConfigs: opts.userConfigs, - baseDir: opts.baseDir, - }; + get params(): FirstTransformParams { + return opts; }, }; return embroiderFirstMacrosTransform; diff --git a/packages/macros/src/glimmer/dependency-satisfies.ts b/packages/macros/src/glimmer/dependency-satisfies.ts index b2683e7a4..a01e301cb 100644 --- a/packages/macros/src/glimmer/dependency-satisfies.ts +++ b/packages/macros/src/glimmer/dependency-satisfies.ts @@ -1,7 +1,5 @@ import { satisfies } from 'semver'; -import { PackageCache } from '@embroider/shared-internals'; - -let packageCache = PackageCache.shared('embroider-stage3'); +import type { PackageCache } from '@embroider/shared-internals'; export default function dependencySatisfies( node: any, @@ -10,7 +8,8 @@ export default function dependencySatisfies( // embroider stage3 we process all packages simultaneously, so baseDir is left // unconfigured and moduleName will be the full path to the source file. baseDir: string | undefined, - moduleName: string + moduleName: string, + packageCache: PackageCache ) { if (node.params.length !== 2) { throw new Error(`macroDependencySatisfies requires two arguments, you passed ${node.params.length}`); @@ -24,7 +23,7 @@ export default function dependencySatisfies( let range = node.params[1].value; let us = packageCache.ownerOfFile(baseDir || moduleName); - if (!us) { + if (!us || us.dependencies.every(dep => dep.name !== packageName)) { return false; } diff --git a/packages/macros/src/glimmer/get-config.ts b/packages/macros/src/glimmer/get-config.ts index 9e8fc65fc..62e17209e 100644 --- a/packages/macros/src/glimmer/get-config.ts +++ b/packages/macros/src/glimmer/get-config.ts @@ -1,6 +1,4 @@ -import { PackageCache } from '@embroider/shared-internals'; - -let packageCache = PackageCache.shared('embroider-stage3'); +import type { PackageCache } from '@embroider/shared-internals'; export default function getConfig( node: any, @@ -11,7 +9,8 @@ export default function getConfig( // unconfigured and moduleName will be the full path to the source file. baseDir: string | undefined, moduleName: string, - own: boolean + own: boolean, + packageCache: PackageCache ) { let targetConfig; let params = node.params.slice(); diff --git a/packages/macros/src/macros-config.ts b/packages/macros/src/macros-config.ts index 6d88ca8d8..f153b5138 100644 --- a/packages/macros/src/macros-config.ts +++ b/packages/macros/src/macros-config.ts @@ -4,12 +4,10 @@ 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'; +import { FirstTransformParams, makeFirstTransform, makeSecondTransform } from './glimmer/ast-transform'; import State from './babel/state'; import partition from 'lodash/partition'; -const packageCache = new PackageCache(); - export type SourceOfConfig = (config: object) => { readonly name: string; readonly root: string; @@ -54,7 +52,7 @@ function gatherAddonCacheKey(item: any, memo = new Set()) { } export default class MacrosConfig { - static for(key: any): MacrosConfig { + static for(key: any, appRoot: string): MacrosConfig { let found = localSharedState.get(key); if (found) { return found; @@ -81,7 +79,7 @@ export default class MacrosConfig { g.__embroider_macros_global__.set(key, shared); } - let config = new MacrosConfig(); + let config = new MacrosConfig(appRoot); config.configs = shared.configs; config.configSources = shared.configSources; config.mergers = shared.mergers; @@ -93,7 +91,6 @@ export default class MacrosConfig { private globalConfig: { [key: string]: unknown } = {}; private isDevelopingPackageRoots: Set = new Set(); - private appPackageRoot: string | undefined; enableRuntimeMode() { if (this.mode !== 'run-time') { @@ -104,23 +101,6 @@ export default class MacrosConfig { } } - enableAppDevelopment(appPackageRoot: string) { - if (!appPackageRoot) { - throw new Error(`must provide appPackageRoot`); - } - if (this.appPackageRoot) { - if (this.appPackageRoot !== appPackageRoot && this.moves.get(this.appPackageRoot) !== appPackageRoot) { - throw new Error(`bug: conflicting appPackageRoots ${this.appPackageRoot} vs ${appPackageRoot}`); - } - } else { - if (!this._configWritable) { - throw new Error(`[Embroider:MacrosConfig] attempted to enableAppDevelopment after configs have been finalized`); - } - this.appPackageRoot = appPackageRoot; - this.isDevelopingPackageRoots.add(appPackageRoot); - } - } - enablePackageDevelopment(packageRoot: string) { if (!this.isDevelopingPackageRoots.has(packageRoot)) { if (!this._configWritable) { @@ -147,7 +127,9 @@ export default class MacrosConfig { this._importSyncImplementation = value; } - private constructor() { + private packageCache: PackageCache; + + private constructor(private appRoot: string) { // this uses globalConfig because these things truly are global. Even if a // package doesn't have a dep or peerDep on @embroider/macros, it's legit // for them to want to know the answer to these questions, and there is only @@ -165,6 +147,7 @@ export default class MacrosConfig { // true to distinguish the two. isTesting: false, }; + this.packageCache = PackageCache.shared('embroider-stage3', appRoot); } private _configWritable = true; @@ -243,7 +226,7 @@ export default class MacrosConfig { if (!this.cachedUserConfigs) { let userConfigs: { [packageRoot: string]: object } = {}; - let sourceOfConfig = makeConfigSourcer(this.configSources); + let sourceOfConfig = this.makeConfigSourcer(this.configSources); for (let [pkgRoot, configs] of this.configs) { let combined: object; if (configs.length > 1) { @@ -262,6 +245,35 @@ export default class MacrosConfig { return this.cachedUserConfigs; } + private makeConfigSourcer(configSources: WeakMap): SourceOfConfig { + return config => { + let fromPath = configSources.get(config); + if (!fromPath) { + throw new Error( + `unknown object passed to sourceOfConfig(). You can only pass back the configs you were given.` + ); + } + let maybePkg = this.packageCache.ownerOfFile(fromPath); + if (!maybePkg) { + throw new Error( + `bug: unexpected error, we always check that fromPath is owned during internalSetConfig so this should never happen` + ); + } + let pkg = maybePkg; + return { + get name() { + return pkg.name; + }, + get version() { + return pkg.version; + }, + get root() { + return pkg.root; + }, + }; + }; + } + // to be called from within your build system. Returns the thing you should // push into your babel plugins list. // @@ -285,7 +297,7 @@ export default class MacrosConfig { owningPackageRoot, isDevelopingPackageRoots: [...this.isDevelopingPackageRoots].map(root => this.moves.get(root) || root), - appPackageRoot: this.appPackageRoot ? this.moves.get(this.appPackageRoot) || this.appPackageRoot : '', + appPackageRoot: this.moves.get(this.appRoot) ?? this.appRoot, // This is used as a signature so we can detect ourself among the plugins // emitted from v1 addons. @@ -337,34 +349,33 @@ export default class MacrosConfig { static astPlugins(owningPackageRoot?: string): { plugins: Function[]; setConfig: (config: MacrosConfig) => void; - getConfigForPlugin(): any; + lazyParams: FirstTransformParams; } { let configs: MacrosConfig | undefined; - let plugins = [ - makeFirstTransform({ - // this is deliberately lazy because we want to allow everyone to finish - // setting config before we generate the userConfigs - get userConfigs() { - if (!configs) { - throw new Error(`Bug: @embroider/macros ast-transforms were not plugged into a MacrosConfig`); - } - return configs.userConfigs; - }, - baseDir: owningPackageRoot, - }), - makeSecondTransform(), - ].reverse(); + + let lazyParams = { + // this is deliberately lazy because we want to allow everyone to finish + // setting config before we generate the userConfigs + get configs() { + if (!configs) { + throw new Error(`Bug: @embroider/macros ast-transforms were not plugged into a MacrosConfig`); + } + return configs.userConfigs; + }, + packageRoot: owningPackageRoot, + get appRoot() { + if (!configs) { + throw new Error(`Bug: @embroider/macros ast-transforms were not plugged into a MacrosConfig`); + } + return configs.appRoot; + }, + }; + + let plugins = [makeFirstTransform(lazyParams), makeSecondTransform()].reverse(); function setConfig(c: MacrosConfig) { configs = c; } - function getConfigForPlugin() { - if (!configs) { - throw new Error(`Bug: @embroider/macros ast-transforms were not plugged into a MacrosConfig`); - } - - return configs.userConfigs; - } - return { plugins, setConfig, getConfigForPlugin }; + return { plugins, setConfig, lazyParams }; } private mergerFor(pkgRoot: string) { @@ -387,21 +398,13 @@ export default class MacrosConfig { private moves: Map = new Map(); - getConfig(fromPath: string, packageName: string) { - return this.userConfigs[this.resolvePackage(fromPath, packageName).root]; - } - - getOwnConfig(fromPath: string) { - return this.userConfigs[this.resolvePackage(fromPath, undefined).root]; - } - private resolvePackage(fromPath: string, packageName?: string | undefined) { - let us = packageCache.ownerOfFile(fromPath); + let us = this.packageCache.ownerOfFile(fromPath); if (!us) { throw new Error(`[Embroider:MacrosConfig] unable to determine which npm package owns the file ${fromPath}`); } if (packageName) { - return packageCache.resolve(packageName, us); + return this.packageCache.resolve(packageName, us); } else { return us; } @@ -418,30 +421,3 @@ function defaultMergerFor(pkgRoot: string) { return Object.assign({}, ...ownConfigs, ...otherConfigs); }; } - -function makeConfigSourcer(configSources: WeakMap): SourceOfConfig { - return config => { - let fromPath = configSources.get(config); - if (!fromPath) { - throw new Error(`unknown object passed to sourceOfConfig(). You can only pass back the configs you were given.`); - } - let maybePkg = packageCache.ownerOfFile(fromPath); - if (!maybePkg) { - throw new Error( - `bug: unexpected error, we always check that fromPath is owned during internalSetConfig so this should never happen` - ); - } - let pkg = maybePkg; - return { - get name() { - return pkg.name; - }, - get version() { - return pkg.version; - }, - get root() { - return pkg.root; - }, - }; - }; -} diff --git a/packages/macros/tests/babel/dependency-satisfies.test.ts b/packages/macros/tests/babel/dependency-satisfies.test.ts index 96fb00377..9cb02fb6d 100644 --- a/packages/macros/tests/babel/dependency-satisfies.test.ts +++ b/packages/macros/tests/babel/dependency-satisfies.test.ts @@ -1,127 +1,157 @@ -import * as path from 'path'; -import { allBabelVersions, Project, runDefault } from './helpers'; +import { allBabelVersions, Project, runDefault } from '@embroider/test-support'; +import { join } from 'path'; +import { MacrosConfig } from '../../src/node'; const ROOT = process.cwd(); describe(`dependencySatisfies`, function () { let project: Project; - afterEach(() => { - if (project) { - project.dispose(); - } + beforeEach(() => { + project = new Project('test-app'); + }); + afterEach(() => { + project?.dispose(); process.chdir(ROOT); }); - allBabelVersions(function (transform: (code: string, options?: object) => string) { - test('is satisfied', () => { - let code = transform(` + allBabelVersions({ + includePresetsTests: true, + babelConfig() { + project.writeSync(); + let config = MacrosConfig.for({}, project.baseDir); + config.finalize(); + return { + filename: join(project.baseDir, 'sample.js'), + plugins: config.babelPluginConfig(), + }; + }, + + createTests(transform) { + test('is satisfied', () => { + project.addDependency('example-package', '2.9.0'); + let code = transform(` import { dependencySatisfies } from '@embroider/macros'; export default function() { - return dependencySatisfies('qunit', '^2.8.0'); + return dependencySatisfies('example-package', '^2.8.0'); } `); - expect(runDefault(code)).toBe(true); - }); + expect(runDefault(code)).toBe(true); + }); - test('is not satisfied', () => { - let code = transform(` + test('is not satisfied', () => { + project.addDependency('example-package', '2.9.0'); + let code = transform(` import { dependencySatisfies } from '@embroider/macros'; export default function() { - return dependencySatisfies('qunit', '^10.0.0'); + return dependencySatisfies('example-package', '^10.0.0'); } `); - expect(runDefault(code)).toBe(false); - }); + expect(runDefault(code)).toBe(false); + }); - test('is not present', () => { - let code = transform(` + test('is not present', () => { + let code = transform(` import { dependencySatisfies } from '@embroider/macros'; export default function() { return dependencySatisfies('not-a-real-dep', '^10.0.0'); } `); - expect(runDefault(code)).toBe(false); - }); + expect(runDefault(code)).toBe(false); + }); - test('import gets removed', () => { - let code = transform(` + test('import gets removed', () => { + let code = transform(` import { dependencySatisfies } from '@embroider/macros'; export default function() { return dependencySatisfies('not-a-real-dep', '1'); } `); - expect(code).not.toMatch(/dependencySatisfies/); - }); + expect(code).not.toMatch(/dependencySatisfies/); + }); - test('entire import statement gets removed', () => { - let code = transform(` + test('entire import statement gets removed', () => { + let code = transform(` import { dependencySatisfies } from '@embroider/macros'; export default function() { return dependencySatisfies('not-a-real-dep', '*'); } `); - expect(code).not.toMatch(/dependencySatisfies/); - expect(code).not.toMatch(/@embroider\/macros/); - }); + expect(code).not.toMatch(/dependencySatisfies/); + expect(code).not.toMatch(/@embroider\/macros/); + }); - test('unused import gets removed', () => { - let code = transform(` + test('unused import gets removed', () => { + let code = transform(` import { dependencySatisfies } from '@embroider/macros'; export default function() { return 1; } `); - expect(code).not.toMatch(/dependencySatisfies/); - expect(code).not.toMatch(/@embroider\/macros/); - }); + expect(code).not.toMatch(/dependencySatisfies/); + expect(code).not.toMatch(/@embroider\/macros/); + }); - test('non call error', () => { - expect(() => { - transform(` + test('non call error', () => { + expect(() => { + transform(` import { dependencySatisfies } from '@embroider/macros'; let x = dependencySatisfies; `); - }).toThrow(/You can only use dependencySatisfies as a function call/); - }); + }).toThrow(/You can only use dependencySatisfies as a function call/); + }); - test('args length error', () => { - expect(() => { - transform(` + test('args length error', () => { + expect(() => { + transform(` import { dependencySatisfies } from '@embroider/macros'; dependencySatisfies('foo', 'bar', 'baz'); `); - }).toThrow(/dependencySatisfies takes exactly two arguments, you passed 3/); - }); + }).toThrow(/dependencySatisfies takes exactly two arguments, you passed 3/); + }); - test('non literal arg error', () => { - expect(() => { - transform(` + test('non literal arg error', () => { + expect(() => { + transform(` import { dependencySatisfies } from '@embroider/macros'; let name = 'qunit'; dependencySatisfies(name, '*'); `); - }).toThrow(/the first argument to dependencySatisfies must be a string literal/); - }); - - test('it considers prereleases (otherwise within the range) as allowed', () => { - project = new Project('test-app', '1.0.0'); - project.addDevDependency('foo', '1.1.0-beta.1'); - project.writeSync(); + }).toThrow(/the first argument to dependencySatisfies must be a string literal/); + }); - process.chdir(project.baseDir); - - let code = transform( - ` + test('it considers prereleases (otherwise within the range) as allowed', () => { + project.addDependency('foo', '1.1.0-beta.1'); + let code = transform( + ` import { dependencySatisfies } from '@embroider/macros'; export default function() { return dependencySatisfies('foo', '^1.0.0'); } - `, - { filename: path.join(project.baseDir, 'foo.js') } - ); - expect(runDefault(code)).toBe(true); - }); + ` + ); + expect(runDefault(code)).toBe(true); + }); + + test('monorepo resolutions resolve correctly', () => { + project.addDependency('@embroider/util', '1.2.3'); + let code = transform(` + import { dependencySatisfies } from '@embroider/macros'; + + export default function() { + return { + // specified in dependencies + util: dependencySatisfies('@embroider/util', '*'), + + // not specified as any kind of dep + webpack: dependencySatisfies('@embroider/webpack', '*'), + } + } + `); + + expect(runDefault(code)).toEqual({ util: true, webpack: false }); + }); + }, }); }); diff --git a/packages/macros/tests/babel/each.test.ts b/packages/macros/tests/babel/each.test.ts index 4369568e4..189d15159 100644 --- a/packages/macros/tests/babel/each.test.ts +++ b/packages/macros/tests/babel/each.test.ts @@ -14,7 +14,7 @@ describe('each', function () { let run = makeRunner(transform); beforeEach(function () { - macrosConfig = MacrosConfig.for({}); + macrosConfig = MacrosConfig.for({}, __dirname); macrosConfig.setOwnConfig(__filename, { plugins: ['alpha', 'beta'], flavor: 'chocolate' }); applyMode(macrosConfig); macrosConfig.finalize(); diff --git a/packages/macros/tests/babel/env-macros.test.ts b/packages/macros/tests/babel/env-macros.test.ts index a2d06dfd7..2f4acaffc 100644 --- a/packages/macros/tests/babel/env-macros.test.ts +++ b/packages/macros/tests/babel/env-macros.test.ts @@ -1,7 +1,7 @@ import { allBabelVersions } from '@embroider/test-support'; import { makeBabelConfig, allModes, makeRunner } from './helpers'; -import { PackageCache } from '@embroider/core'; import { MacrosConfig } from '../../src/node'; +import { resolve } from 'path'; describe(`env macros`, function () { let macrosConfig: MacrosConfig; @@ -16,9 +16,9 @@ describe(`env macros`, function () { describe(`true cases`, function () { beforeEach(function () { - macrosConfig = MacrosConfig.for({}); + macrosConfig = MacrosConfig.for({}, resolve(__dirname, '..', '..')); macrosConfig.setGlobalConfig(__filename, '@embroider/macros', { isTesting: true }); - macrosConfig.enableAppDevelopment(PackageCache.shared('embroider-stage3').ownerOfFile(__filename)!.root); + macrosConfig.enablePackageDevelopment(resolve(__dirname, '..', '..')); applyMode(macrosConfig); macrosConfig.finalize(); run = makeRunner(transform); @@ -121,7 +121,7 @@ describe(`env macros`, function () { describe(`false cases`, function () { beforeEach(function () { - macrosConfig = MacrosConfig.for({}); + macrosConfig = MacrosConfig.for({}, '/nonexistent'); macrosConfig.setGlobalConfig(__filename, '@embroider/macros', { isTesting: false }); applyMode(macrosConfig); macrosConfig.finalize(); diff --git a/packages/macros/tests/babel/get-config.test.ts b/packages/macros/tests/babel/get-config.test.ts index dd9a01b79..a4c63f053 100644 --- a/packages/macros/tests/babel/get-config.test.ts +++ b/packages/macros/tests/babel/get-config.test.ts @@ -24,7 +24,7 @@ describe(`getConfig`, function () { // we know it will be available. filename = `${dirname(require.resolve('@embroider/core/package.json'))}/sample.js`; - config = MacrosConfig.for({}); + config = MacrosConfig.for({}, dirname(require.resolve('@embroider/core/package.json'))); config.setOwnConfig(filename, { beverage: 'coffee', }); diff --git a/packages/macros/tests/babel/helpers.ts b/packages/macros/tests/babel/helpers.ts index 4f6bde184..cc79c4155 100644 --- a/packages/macros/tests/babel/helpers.ts +++ b/packages/macros/tests/babel/helpers.ts @@ -105,7 +105,7 @@ export function allBabelVersions(createTests: CreateTests | CreateTestsWithConfi }, createTests(transform) { - config = MacrosConfig.for({}); + config = MacrosConfig.for({}, __dirname); if (createTests.length === 1) { // The caller will not be using `config`, so we finalize it for them. config.finalize(); diff --git a/packages/macros/tests/babel/macro-condition.test.ts b/packages/macros/tests/babel/macro-condition.test.ts index 433c11da4..ecf383f9f 100644 --- a/packages/macros/tests/babel/macro-condition.test.ts +++ b/packages/macros/tests/babel/macro-condition.test.ts @@ -1,9 +1,23 @@ import { makeRunner, makeBabelConfig, allModes } from './helpers'; -import { allBabelVersions } from '@embroider/test-support'; +import { allBabelVersions, Project } from '@embroider/test-support'; import { MacrosConfig } from '../../src/node'; +import { join } from 'path'; describe('macroCondition', function () { let config: MacrosConfig; + let project: Project; + let filename: string; + + beforeAll(() => { + project = new Project('test-app'); + project.addDependency('qunit', '2.0.0'); + project.writeSync(); + filename = join(project.baseDir, 'sample.js'); + }); + + afterAll(() => { + project?.dispose(); + }); allBabelVersions({ babelConfig(version: number) { @@ -11,14 +25,17 @@ describe('macroCondition', function () { if (version === 7) { babelConfig.plugins.push('@babel/plugin-proposal-class-properties'); } + babelConfig.filename = filename; return babelConfig; }, includePresetsTests: true, createTests: allModes((transform, { applyMode, buildTimeTest, runTimeTest }) => { let run = makeRunner(transform); beforeEach(function () { - config = MacrosConfig.for({}); - config.setConfig(__filename, 'qunit', { items: [{ approved: true, other: null, size: 2.3 }] }); + config = MacrosConfig.for({}, project.baseDir); + config.setConfig(join(project.baseDir, 'sample.js'), 'qunit', { + items: [{ approved: true, other: null, size: 2.3 }], + }); applyMode(config); config.finalize(); }); @@ -34,7 +51,7 @@ describe('macroCondition', function () { } } `); - expect(run(code)).toBe('alpha'); + expect(run(code, { filename })).toBe('alpha'); expect(code).not.toMatch(/beta/); expect(code).not.toMatch(/macroCondition/); expect(code).not.toMatch(/if/); @@ -53,7 +70,7 @@ describe('macroCondition', function () { } } `); - expect(run(code)).toBe('alpha'); + expect(run(code, { filename })).toBe('alpha'); expect(code).toMatch(/beta/); expect(code).toMatch(/macroCondition/); expect(code).toMatch(/if/); @@ -69,7 +86,7 @@ describe('macroCondition', function () { return 'alpha'; } `); - expect(run(code)).toBe('alpha'); + expect(run(code, { filename })).toBe('alpha'); expect(code).not.toMatch(/beta/); expect(code).not.toMatch(/macroCondition/); expect(code).not.toMatch(/if/); @@ -88,7 +105,7 @@ describe('macroCondition', function () { } } `); - expect(run(code)).toBe('beta'); + expect(run(code, { filename })).toBe('beta'); expect(code).not.toMatch(/alpha/); expect(code).not.toMatch(/macroCondition/); expect(code).not.toMatch(/if/); @@ -103,7 +120,7 @@ describe('macroCondition', function () { return macroCondition(true) ? 'alpha' : 'beta'; } `); - expect(run(code)).toBe('alpha'); + expect(run(code, { filename })).toBe('alpha'); expect(code).not.toMatch(/beta/); expect(code).not.toMatch(/macroCondition/); expect(code).not.toMatch(/\?/); @@ -118,7 +135,7 @@ describe('macroCondition', function () { return macroCondition(true) ? 'alpha' : 'beta'; } `); - expect(run(code)).toBe('alpha'); + expect(run(code, { filename })).toBe('alpha'); expect(code).toMatch(/beta/); expect(code).toMatch(/macroCondition/); expect(code).toMatch(/\?/); @@ -133,7 +150,7 @@ describe('macroCondition', function () { return macroCondition(false) ? 'alpha' : 'beta'; } `); - expect(run(code)).toBe('beta'); + expect(run(code, { filename })).toBe('beta'); expect(code).not.toMatch(/alpha/); expect(code).not.toMatch(/macroCondition/); expect(code).not.toMatch(/\?/); @@ -150,7 +167,7 @@ describe('macroCondition', function () { } } `); - expect(run(code)).toBe('alpha'); + expect(run(code, { filename })).toBe('alpha'); expect(code).not.toMatch(/macroCondition/); expect(code).not.toMatch(/@embroider\/macros/); expect(code).not.toMatch(/\/runtime/); @@ -165,7 +182,7 @@ describe('macroCondition', function () { } } `); - expect(run(code)).toBe(undefined); + expect(run(code, { filename })).toBe(undefined); }); buildTimeTest('else if consequent', () => { @@ -181,7 +198,7 @@ describe('macroCondition', function () { } } `); - expect(run(code)).toBe('beta'); + expect(run(code, { filename })).toBe('beta'); expect(code).not.toMatch(/alpha/); expect(code).not.toMatch(/gamma/); }); @@ -199,7 +216,7 @@ describe('macroCondition', function () { } } `); - expect(run(code)).toBe('gamma'); + expect(run(code, { filename })).toBe('gamma'); expect(code).not.toMatch(/alpha/); expect(code).not.toMatch(/beta/); }); @@ -281,7 +298,7 @@ describe('macroCondition', function () { return macroCondition(dependencySatisfies('qunit', '*')) ? 'alpha' : 'beta'; } `); - expect(run(code)).toBe('alpha'); + expect(run(code, { filename })).toBe('alpha'); expect(code).not.toMatch(/beta/); }); @@ -292,7 +309,7 @@ describe('macroCondition', function () { return macroCondition(dependencySatisfies('qunit', '*')) ? 'alpha' : 'beta'; } `); - expect(run(code)).toBe('alpha'); + expect(run(code, { filename })).toBe('alpha'); expect(code).toMatch(/beta/); }); @@ -315,7 +332,7 @@ describe('macroCondition', function () { return { qunit, notARealPackage }; } `); - expect(run(code)).toEqual({ qunit: 'found', notARealPackage: 'not found' }); + expect(run(code, { filename })).toEqual({ qunit: 'found', notARealPackage: 'not found' }); expect(code).not.toMatch(/beta/); }); @@ -326,7 +343,7 @@ describe('macroCondition', function () { return macroCondition((2 > 1) && dependencySatisfies('qunit', '*')) ? 'alpha' : 'beta'; } `); - expect(run(code)).toBe('alpha'); + expect(run(code, { filename })).toBe('alpha'); expect(code).toMatch(/beta/); }); @@ -337,7 +354,7 @@ describe('macroCondition', function () { return macroCondition((2 > 1) && dependencySatisfies('qunit', '*')) ? 'alpha' : 'beta'; } `); - expect(run(code)).toBe('alpha'); + expect(run(code, { filename })).toBe('alpha'); expect(code).not.toMatch(/beta/); }); @@ -350,7 +367,7 @@ describe('macroCondition', function () { return macroCondition(getConfig('qunit').items[0]["other"]) ? 'alpha' : 'beta'; } `); - expect(run(code)).toBe('beta'); + expect(run(code, { filename })).toBe('beta'); expect(code).not.toMatch(/alpha/); }); @@ -363,7 +380,7 @@ describe('macroCondition', function () { return macroCondition(getConfig('qunit').items[0]["other"]) ? 'alpha' : 'beta'; } `); - expect(run(code)).toBe('beta'); + expect(run(code, { filename })).toBe('beta'); expect(code).toMatch(/alpha/); }); @@ -379,7 +396,7 @@ describe('macroCondition', function () { return test.version; } `); - expect(run(code)).toBe('beta'); + expect(run(code, { filename })).toBe('beta'); expect(code).not.toMatch(/alpha/); }); @@ -394,7 +411,7 @@ describe('macroCondition', function () { return test.version; } `); - expect(run(code)).toBe('beta'); + expect(run(code, { filename })).toBe('beta'); expect(code).toMatch(/alpha/); }); } diff --git a/packages/macros/tests/glimmer/dependency-satisfies.test.ts b/packages/macros/tests/glimmer/dependency-satisfies.test.ts index f20c3e604..daaa87bc8 100644 --- a/packages/macros/tests/glimmer/dependency-satisfies.test.ts +++ b/packages/macros/tests/glimmer/dependency-satisfies.test.ts @@ -1,61 +1,57 @@ -import { Project, templateTests } from './helpers'; -import * as path from 'path'; +import { Project, templateTests, TemplateTransformOptions } from './helpers'; +import { join } from 'path'; -const ROOT = process.cwd(); describe('dependency satisfies', () => { let project: Project; + let filename: string; + + beforeAll(() => { + project = new Project('app'); + project.addDependency('qunit', '2.9.1'); + project.addDependency('foo', '1.1.0-beta.1'); + project.writeSync(); + filename = join(project.baseDir, 'sample.js'); + }); - afterEach(() => { - if (project) { - project.dispose(); - } - - process.chdir(ROOT); + afterAll(() => { + project?.dispose(); }); - templateTests((transform: (code: string, options?: object) => string) => { + templateTests((transform: (code: string, options?: TemplateTransformOptions) => string) => { test('in content position', () => { - let result = transform(`{{macroDependencySatisfies 'qunit' '^2.8.0'}}`); + let result = transform(`{{macroDependencySatisfies 'qunit' '^2.8.0'}}`, { filename }); expect(result).toEqual('{{true}}'); }); test('in subexpression position', () => { - let result = transform(``); + let result = transform(``, { filename }); expect(result).toMatch(/@a=\{\{true\}\}/); }); test('emits false for out-of-range package', () => { - let result = transform(`{{macroDependencySatisfies 'qunit' '^10.0.0'}}`); + let result = transform(`{{macroDependencySatisfies 'qunit' '^10.0.0'}}`, { filename }); expect(result).toEqual('{{false}}'); }); test('emits false for missing package', () => { - let result = transform(`{{macroDependencySatisfies 'not-a-real-dep' '^10.0.0'}}`); + let result = transform(`{{macroDependencySatisfies 'not-a-real-dep' '^10.0.0'}}`, { filename }); expect(result).toEqual('{{false}}'); }); test('args length error', () => { expect(() => { - transform(`{{macroDependencySatisfies 'not-a-real-dep'}}`); + transform(`{{macroDependencySatisfies 'not-a-real-dep'}}`, { filename }); }).toThrow(/macroDependencySatisfies requires two arguments, you passed 1/); }); test('non literal arg error', () => { expect(() => { - transform(`{{macroDependencySatisfies someDep "*"}}`); + transform(`{{macroDependencySatisfies someDep "*"}}`, { filename }); }).toThrow(/all arguments to macroDependencySatisfies must be string literals/); }); test('it considers prereleases (otherwise within the range) as allowed', () => { - project = new Project('test-app', '1.0.0'); - project.addDevDependency('foo', '1.1.0-beta.1'); - project.writeSync(); - - process.chdir(project.baseDir); - - let result = transform(`{{macroDependencySatisfies 'foo' '^1.0.0'}}`, { - filename: path.join(project.baseDir, 'foo.js'), - }); + let result = transform(`{{macroDependencySatisfies 'foo' '^1.0.0'}}`, { filename }); expect(result).toEqual('{{true}}'); }); }); diff --git a/packages/macros/tests/glimmer/helpers.ts b/packages/macros/tests/glimmer/helpers.ts index 4ff94dc0a..e5dcd7048 100644 --- a/packages/macros/tests/glimmer/helpers.ts +++ b/packages/macros/tests/glimmer/helpers.ts @@ -12,13 +12,13 @@ export { Project }; type CreateTestsWithConfig = (transform: (templateContents: string) => string, config: MacrosConfig) => void; type CreateTests = (transform: (templateContents: string) => string) => void; -interface TemplateTransformOptions { +export interface TemplateTransformOptions { filename?: string; } export function templateTests(createTests: CreateTestsWithConfig | CreateTests) { let { plugins, setConfig } = MacrosConfig.astPlugins(); - let config = MacrosConfig.for({}); + let config = MacrosConfig.for({}, '/nonexistent'); setConfig(config); let compiler = new NodeTemplateCompiler({ compilerPath, diff --git a/packages/macros/tests/glimmer/macro-condition.test.ts b/packages/macros/tests/glimmer/macro-condition.test.ts index 25ee499c6..88082f853 100644 --- a/packages/macros/tests/glimmer/macro-condition.test.ts +++ b/packages/macros/tests/glimmer/macro-condition.test.ts @@ -1,7 +1,15 @@ -import { templateTests } from './helpers'; +import { Project } from '@embroider/test-support'; +import { join } from 'path'; +import { templateTests, TemplateTransformOptions } from './helpers'; describe(`macroCondition`, function () { - templateTests(function (transform: (code: string) => string) { + let project: Project; + + afterEach(() => { + project?.dispose(); + }); + + templateTests(function (transform: (code: string, opts?: TemplateTransformOptions) => string) { test('leaves regular if-block untouched', function () { let code = transform(`{{#if this.error}}red{{else}}blue{{/if}}`); expect(code).toEqual(`{{#if this.error}}red{{else}}blue{{/if}}`); @@ -55,15 +63,23 @@ describe(`macroCondition`, function () { }); test('macroCondition composes with other macros, true case', function () { + project = new Project('app'); + project.addDependency('ember-source', '3.1.2'); + project.writeSync(); let code = transform( - `{{my-assertion (if (macroCondition (macroDependencySatisfies 'ember-source' '3.x')) 'red' 'blue') }}` + `{{my-assertion (if (macroCondition (macroDependencySatisfies 'ember-source' '3.x')) 'red' 'blue') }}`, + { filename: join(project.baseDir, 'sample.js') } ); expect(code).toMatch(/\{\{my-assertion ["']red["']\}\}/); }); test('macroCondition composes with other macros, false case', function () { + project = new Project('app'); + project.addDependency('ember-source', '3.1.2'); + project.writeSync(); let code = transform( - `{{my-assertion (if (macroCondition (macroDependencySatisfies 'ember-source' '10.x')) 'red' 'blue') }}` + `{{my-assertion (if (macroCondition (macroDependencySatisfies 'ember-source' '10.x')) 'red' 'blue') }}`, + { filename: join(project.baseDir, 'sample.js') } ); expect(code).toMatch(/\{\{my-assertion ["']blue["']\}\}/); }); diff --git a/packages/shared-internals/src/babel-filter.ts b/packages/shared-internals/src/babel-filter.ts index 5cf4cb78b..0932aee27 100644 --- a/packages/shared-internals/src/babel-filter.ts +++ b/packages/shared-internals/src/babel-filter.ts @@ -1,14 +1,14 @@ import PackageCache from './package-cache'; import semver from 'semver'; -export default function babelFilter(skipBabel: { package: string; semverRange?: string }[]) { +export default function babelFilter(skipBabel: { package: string; semverRange?: string }[], appRoot: string) { return function shouldTranspileFile(filename: string) { if (!babelCanHandle(filename)) { // quick exit for non JS extensions return false; } - let owner = PackageCache.shared('embroider-stage3').ownerOfFile(filename); + let owner = PackageCache.shared('embroider-stage3', appRoot).ownerOfFile(filename); if (owner) { for (let { package: pkg, semverRange } of skipBabel) { if (owner.name === pkg && (semverRange == null || semver.satisfies(owner.version, semverRange))) { diff --git a/packages/shared-internals/src/ember-cli-models.ts b/packages/shared-internals/src/ember-cli-models.ts index 4047b30c7..55d8038e2 100644 --- a/packages/shared-internals/src/ember-cli-models.ts +++ b/packages/shared-internals/src/ember-cli-models.ts @@ -18,6 +18,8 @@ export interface AppInstance { project: Project; options: any; addonPostprocessTree: (which: string, tree: Node) => Node; + import(path: string, opts?: { type?: string }): void; + toTree(additionalTrees?: Node[]): Node; } export type FilePath = string; diff --git a/packages/shared-internals/src/package-cache.ts b/packages/shared-internals/src/package-cache.ts index 7bed30514..4282f1a1d 100644 --- a/packages/shared-internals/src/package-cache.ts +++ b/packages/shared-internals/src/package-cache.ts @@ -5,6 +5,8 @@ import resolvePackagePath from 'resolve-package-path'; import { dirname, sep } from 'path'; export default class PackageCache { + constructor(public appRoot: string) {} + resolve(packageName: string, fromPackage: Package): Package { let cache = getOrCreate(this.resolutionCache, fromPackage, () => new Map() as Map); let result = getOrCreate(cache, packageName, () => { @@ -25,14 +27,6 @@ export default class PackageCache { return result; } - getApp(packageRoot: string) { - let root = realpathSync(packageRoot); - let p = getOrCreate(this.rootCache, root, () => { - return new Package(root, this, true); - }); - return p; - } - seed(pkg: Package) { if (this.rootCache.has(pkg.root)) { throw new Error(`bug: tried to seed package ${pkg.name} but it's already in packageCache`); @@ -50,7 +44,7 @@ export default class PackageCache { get(packageRoot: string) { let root = realpathSync(packageRoot); let p = getOrCreate(this.rootCache, root, () => { - return new Package(root, this); + return new Package(root, this, root === this.appRoot); }); return p; } @@ -83,8 +77,8 @@ export default class PackageCache { shared.set(identifier, this); } - static shared(identifier: string) { - return getOrCreate(shared, identifier, () => new PackageCache()); + static shared(identifier: string, appRoot: string) { + return getOrCreate(shared, identifier, () => new PackageCache(appRoot)); } } diff --git a/packages/shared-internals/src/package.ts b/packages/shared-internals/src/package.ts index e9a413edd..691a0b257 100644 --- a/packages/shared-internals/src/package.ts +++ b/packages/shared-internals/src/package.ts @@ -8,14 +8,8 @@ import flatMap from 'lodash/flatMap'; export default class Package { private dependencyKeys: ('dependencies' | 'devDependencies' | 'peerDependencies')[]; - constructor(readonly root: string, protected packageCache: PackageCache, isApp?: boolean) { - // In stage1 and stage2, we're careful to make sure our PackageCache entry - // for the app itself gets created with an explicit `isApp` flag. In stage3 - // we don't have that much control, but we can rely on the v2-formatted app - // being easy to identify from its metadata. - let mayUseDevDeps = typeof isApp === 'boolean' ? isApp : this.isV2App(); - - this.dependencyKeys = mayUseDevDeps + constructor(readonly root: string, protected packageCache: PackageCache, isApp: boolean) { + this.dependencyKeys = isApp ? ['dependencies', 'devDependencies', 'peerDependencies'] : ['dependencies', 'peerDependencies']; } @@ -137,15 +131,15 @@ export default class Package { // which is why this logic is here in nonResolvableDeps. If you try // to ship broken stuff in regular dependencies, NPM is going to // stop you. - let pkg; + let pkg, main; try { pkg = this.packageCache.get(join(this.packageCache.basedir(this), path)); + main = pkg.packageJSON['ember-addon']?.main || pkg.packageJSON['main']; } catch (err) { // package was missing or had invalid package.json return false; } - let main = - (pkg.packageJSON['ember-addon'] && pkg.packageJSON['ember-addon'].main) || pkg.packageJSON['main']; + if (!main || main === '.' || main === './') { main = 'index.js'; } else if (!extname(main)) { diff --git a/packages/shared-internals/src/template-colocation-plugin.ts b/packages/shared-internals/src/template-colocation-plugin.ts index 261f83356..144cb690b 100644 --- a/packages/shared-internals/src/template-colocation-plugin.ts +++ b/packages/shared-internals/src/template-colocation-plugin.ts @@ -20,6 +20,8 @@ export interface Options { // This option is used by Embroider itself to help with compatibility, other // users should probably not use it. packageGuard?: boolean; + + appRoot: string; } interface State { @@ -43,7 +45,7 @@ export default function main(babel: typeof Babel) { let filename = path.hub.file.opts.filename; if (state.opts.packageGuard) { - let owningPackage = PackageCache.shared('embroider-stage3').ownerOfFile(filename); + let owningPackage = PackageCache.shared('embroider-stage3', state.opts.appRoot).ownerOfFile(filename); if (!owningPackage || !owningPackage.isV2Ember() || !owningPackage.meta['auto-upgraded']) { return; } diff --git a/packages/shared-internals/tests/package-cache.test.ts b/packages/shared-internals/tests/package-cache.test.ts index c18f540af..46d6ad7a9 100644 --- a/packages/shared-internals/tests/package-cache.test.ts +++ b/packages/shared-internals/tests/package-cache.test.ts @@ -24,7 +24,7 @@ describe('package-cache', () => { }, }; fixturify.writeSync(tmpLocation, projectJSON); - let packageCache = new PackageCache(); + let packageCache = new PackageCache(tmpLocation); expect(packageCache.ownerOfFile(join(tmpLocation, 'inner', 'index.js'))!.root).toBe(join(tmpLocation, 'inner')); }); @@ -45,7 +45,7 @@ describe('package-cache', () => { }, }; fixturify.writeSync(tmpLocation, projectJSON); - let packageCache = new PackageCache(); + let packageCache = new PackageCache(tmpLocation); packageCache.ownerOfFile(join(tmpLocation, 'index.js')); expect(packageCache.ownerOfFile(join(tmpLocation, 'inner', 'index.js'))!.root).toBe(join(tmpLocation, 'inner')); }); @@ -66,7 +66,7 @@ describe('package-cache', () => { }, }; fixturify.writeSync(tmpLocation, projectJSON); - let packageCache = new PackageCache(); + let packageCache = new PackageCache(tmpLocation); expect(packageCache.ownerOfFile(join(tmpLocation, 'inner', 'index.js'))!.root).toBe(join(tmpLocation, 'inner')); }); @@ -87,7 +87,7 @@ describe('package-cache', () => { }, }; fixturify.writeSync(tmpLocation, projectJSON); - let packageCache = new PackageCache(); + let packageCache = new PackageCache(tmpLocation); expect(packageCache.ownerOfFile(join(tmpLocation, 'inner'))!.root).toBe(join(tmpLocation, 'inner')); }); }); diff --git a/packages/shared-internals/tests/package.test.ts b/packages/shared-internals/tests/package.test.ts index a3a0c04e5..8ba8b5514 100644 --- a/packages/shared-internals/tests/package.test.ts +++ b/packages/shared-internals/tests/package.test.ts @@ -16,8 +16,8 @@ describe('package', () => { fixturify.writeSync(tmpLocation, projectJSON); - let packageCache = new PackageCache(); - let packageInstance = new Package(tmpLocation, packageCache); + let packageCache = new PackageCache(tmpLocation); + let packageInstance = new Package(tmpLocation, packageCache, true); let originalProcessValue = process.env['BROCCOLI_ENABLED_MEMOIZE']; process.env['BROCCOLI_ENABLED_MEMOIZE'] = 'true'; @@ -59,8 +59,8 @@ describe('package', () => { fixturify.writeSync(tmpLocation, projectJSON); - let packageCache = new PackageCache(); - let packageInstance = new Package(tmpLocation, packageCache); + let packageCache = new PackageCache(tmpLocation); + let packageInstance = new Package(tmpLocation, packageCache, true); let nonResolvableDeps = packageInstance.nonResolvableDeps; if (!nonResolvableDeps) { @@ -101,8 +101,8 @@ describe('package', () => { fixturify.writeSync(tmpLocation, projectJSON); - let packageCache = new PackageCache(); - let packageInstance = new Package(tmpLocation, packageCache); + let packageCache = new PackageCache(tmpLocation); + let packageInstance = new Package(tmpLocation, packageCache, true); let dependencies = packageInstance.dependencies; expect(dependencies.length).toBe(2); diff --git a/tests/fixtures/macro-test/app/controllers/application.js b/tests/fixtures/macro-test/app/controllers/application.js index 6c85764c3..18ec0e752 100644 --- a/tests/fixtures/macro-test/app/controllers/application.js +++ b/tests/fixtures/macro-test/app/controllers/application.js @@ -8,10 +8,10 @@ export default class Application extends Controller { this.isTesting = isTesting(); this.isDeveloping = isDevelopingApp(); - if (macroCondition(dependencySatisfies('lodash', '^4'))) { - this.lodashVersion = 'four'; + if (macroCondition(dependencySatisfies('version-changer', '^4'))) { + this.versionChangerVersion = 'four'; } else { - this.lodashVersion = 'three'; + this.versionChangerVersion = 'three'; } } } diff --git a/tests/fixtures/macro-test/app/templates/application.hbs b/tests/fixtures/macro-test/app/templates/application.hbs index dad21ec84..88febbb99 100644 --- a/tests/fixtures/macro-test/app/templates/application.hbs +++ b/tests/fixtures/macro-test/app/templates/application.hbs @@ -1,5 +1,5 @@
{{this.mode}}
-
{{macroGetOwnConfig "count"}}
+
{{macroGetOwnConfig 'count'}}
isDeveloping: {{this.isDeveloping}}
isTesting: {{this.isTesting}}
-
{{this.lodashVersion}}
+
{{this.versionChangerVersion}}
\ No newline at end of file diff --git a/tests/fixtures/macro-test/config/environment.js b/tests/fixtures/macro-test/config/environment.js index 43bb7ed81..6a4f4b33a 100644 --- a/tests/fixtures/macro-test/config/environment.js +++ b/tests/fixtures/macro-test/config/environment.js @@ -21,7 +21,7 @@ module.exports = function (environment) { // Here you can pass flags/options to your application instance // when it is created }, - LODASH_VERSION: process.env.LODASH_VERSION || 'four', + EXPECTED_VERSION: process.env.EXPECTED_VERSION || 'four', }; if (environment === 'development') { diff --git a/tests/fixtures/macro-test/tests/acceptance/basic-test.js b/tests/fixtures/macro-test/tests/acceptance/basic-test.js index 6d866ddf4..8e6daad09 100644 --- a/tests/fixtures/macro-test/tests/acceptance/basic-test.js +++ b/tests/fixtures/macro-test/tests/acceptance/basic-test.js @@ -54,8 +54,6 @@ module('Acceptance | smoke tests', function (hooks) { 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); + assert.equal(this.element.querySelector('[data-test-version]').textContent.trim(), ENV.EXPECTED_VERSION); }); }); diff --git a/tests/scenarios/macro-test.ts b/tests/scenarios/macro-test.ts index 5c8b0071e..a97b114e0 100644 --- a/tests/scenarios/macro-test.ts +++ b/tests/scenarios/macro-test.ts @@ -7,15 +7,15 @@ 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')); +function updateVersionChanger(app: PreparedApp, version: string) { + let pkgJsonApp = fs.readJsonSync(join(app.dir, 'package.json')); + let pkgJsonLib = fs.readJsonSync(join(app.dir, 'node_modules', 'version-changer', 'package.json')); - pkgJson.devDependencies.lodash = version; - pkgJsonLodash.version = version; + pkgJsonApp.devDependencies['version-changer'] = version; + pkgJsonLib.version = version; - fs.writeJsonSync(join(app.dir, 'package.json'), pkgJson); - fs.writeJsonSync(join(app.dir, 'node_modules', 'lodash', 'package.json'), pkgJsonLodash); + fs.writeJsonSync(join(app.dir, 'package.json'), pkgJsonApp); + fs.writeJsonSync(join(app.dir, 'node_modules', 'version-changer', 'package.json'), pkgJsonLib); } function scenarioSetup(project: Project) { @@ -34,7 +34,7 @@ function scenarioSetup(project: Project) { 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('version-changer', '4.0.0'); project.addDevDependency(macroSampleAddon); project.addDevDependency(funkySampleAddon); @@ -80,30 +80,29 @@ appReleaseScenario hooks.before(async () => { app = await scenario.prepare(); - updateLodashVersion(app, '4.0.0'); }); test(`@embroider/macros babel caching plugin works`, async function (assert) { - let lodashFourRun = await app.execute(`yarn test`); - assert.equal(lodashFourRun.exitCode, 0, lodashFourRun.output); + let fourRun = await app.execute(`yarn test`); + assert.equal(fourRun.exitCode, 0, fourRun.output); // simulate a different version being installed - updateLodashVersion(app, '3.0.0'); + updateVersionChanger(app, '3.0.0'); - let lodashThreeRun = await app.execute(`cross-env LODASH_VERSION=three yarn test`); + let lodashThreeRun = await app.execute(`cross-env EXPECTED_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'); + updateVersionChanger(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'); + updateVersionChanger(app, '3.0.0'); - let lodashThreeRun = await app.execute(`cross-env LODASH_VERSION=three CLASSIC=true yarn test`); + let lodashThreeRun = await app.execute(`cross-env EXPECTED_VERSION=three CLASSIC=true yarn test`); assert.equal(lodashThreeRun.exitCode, 0, lodashThreeRun.output); }); });