diff --git a/packages/compat/package.json b/packages/compat/package.json index 971ea0da8d..33e141aa79 100644 --- a/packages/compat/package.json +++ b/packages/compat/package.json @@ -57,6 +57,7 @@ "lodash": "^4.17.21", "pkg-up": "^3.1.0", "resolve": "^1.20.0", + "resolve.exports": "^2.0.2", "resolve-package-path": "^4.0.1", "semver": "^7.3.5", "symlink-or-copy": "^1.3.1", diff --git a/packages/compat/src/compat-app-builder.ts b/packages/compat/src/compat-app-builder.ts index 97b2fc975f..290d701e7d 100644 --- a/packages/compat/src/compat-app-builder.ts +++ b/packages/compat/src/compat-app-builder.ts @@ -127,13 +127,10 @@ export class CompatAppBuilder { return ['.wasm', '.mjs', '.js', '.json', '.ts', '.hbs', '.hbs.js', '.gjs', '.gts']; } - private addEmberEntrypoints(htmlTreePath: string): string[] { - let classicEntrypoints = ['index.html', 'tests/index.html']; - if (!this.compatApp.shouldBuildTests) { - classicEntrypoints.pop(); - } + private addEmberEntrypoints(): string[] { + let classicEntrypoints = ['index.html']; for (let entrypoint of classicEntrypoints) { - let sourcePath = join(htmlTreePath, entrypoint); + let sourcePath = join(this.compatApp.root, entrypoint); let rewrittenAppPath = join(this.root, entrypoint); writeFileSync(rewrittenAppPath, readFileSync(sourcePath)); } @@ -463,7 +460,7 @@ export class CompatAppBuilder { } let appFiles = this.updateAppJS(inputPaths.appJS); - let assetPaths = this.addEmberEntrypoints(inputPaths.htmlTree); + let assetPaths = this.addEmberEntrypoints(); if (this.activeFastboot) { // when using fastboot, our own package.json needs to be in the output so fastboot can read it. @@ -509,6 +506,13 @@ export class CompatAppBuilder { if ((this.origAppPackage.packageJSON['ember-addon']?.version ?? 0) < 2) { meta['auto-upgraded'] = true; + // our rewriting keeps app in app directory, etc. + pkgLayers.push({ + exports: { + './*': './app/*', + './tests/*': './tests/*', + }, + }); } pkgLayers.push({ 'ember-addon': meta }); @@ -734,7 +738,6 @@ function addCachablePlugin(babelConfig: TransformOptions) { interface TreeNames { appJS: BroccoliNode; - htmlTree: BroccoliNode; publicTree: BroccoliNode | undefined; configTree: BroccoliNode; } diff --git a/packages/compat/src/compat-app.ts b/packages/compat/src/compat-app.ts index 963df0d978..32459e261f 100644 --- a/packages/compat/src/compat-app.ts +++ b/packages/compat/src/compat-app.ts @@ -96,10 +96,6 @@ export default class CompatApp { return require(resolve.sync(specifier, { basedir: this.emberCLILocation })); } - private get configReplace() { - return this.requireFromEmberCLI('broccoli-config-replace'); - } - private get configLoader() { return this.requireFromEmberCLI('broccoli-config-loader'); } @@ -184,45 +180,6 @@ export default class CompatApp { }; } - private get htmlTree() { - if (this.legacyEmberAppInstance.tests) { - return mergeTrees([this.indexTree, this.testIndexTree]); - } else { - return this.indexTree; - } - } - - private get indexTree() { - let indexFilePath = this.legacyEmberAppInstance.options.outputPaths.app.html; - let index = buildFunnel(this.legacyEmberAppInstance.trees.app, { - allowEmpty: true, - include: [`index.html`], - getDestinationPath: () => indexFilePath, - annotation: 'app/index.html', - }); - return new this.configReplace(index, this.configTree, { - configPath: join('environments', `${this.legacyEmberAppInstance.env}.json`), - files: [indexFilePath], - patterns: this.filteredPatternsByContentFor.others, - annotation: 'ConfigReplace/indexTree', - }); - } - - private get testIndexTree() { - let index = buildFunnel(this.legacyEmberAppInstance.trees.tests, { - allowEmpty: true, - include: [`index.html`], - destDir: 'tests', - annotation: 'tests/index.html', - }); - return new this.configReplace(index, this.configTree, { - configPath: join('environments', `test.json`), - files: ['tests/index.html'], - patterns: this.filteredPatternsByContentFor.others, - annotation: 'ConfigReplace/testIndexTree', - }); - } - @Memoize() babelConfig(): TransformOptions { // this finds all the built-in babel configuration that comes with ember-cli-babel @@ -618,6 +575,7 @@ export default class CompatApp { return this.preprocessJS( buildFunnel(this.legacyEmberAppInstance.trees.app, { exclude: ['styles/**', '*.html'], + destDir: 'app', }) ); } @@ -726,7 +684,6 @@ export default class CompatApp { return { appJS: this.processAppJS().appJS, - htmlTree: this.htmlTree, publicTree, configTree, contentForTree, diff --git a/packages/compat/src/dependency-rules.ts b/packages/compat/src/dependency-rules.ts index 502a76d589..15f8d326a4 100644 --- a/packages/compat/src/dependency-rules.ts +++ b/packages/compat/src/dependency-rules.ts @@ -1,7 +1,8 @@ import type { Resolver } from '@embroider/core'; import { getOrCreate } from '@embroider/core'; -import { resolve } from 'path'; +import { resolve as pathResolve, dirname } from 'path'; import { satisfies } from 'semver'; +import { resolve as resolveExports } from 'resolve.exports'; export interface PackageRules { // This whole set of rules will only apply when the given addon package @@ -232,14 +233,20 @@ export function activePackageRules( export function appTreeRulesDir(root: string, resolver: Resolver) { let pkg = resolver.packageCache.ownerOfFile(root); - if (pkg?.isV2Addon()) { - // in general v2 addons can keep their app tree stuff in other places than - // "_app_" and we would need to check their package.json to see. But this code - // is only for applying packageRules to auto-upgraded v1 addons and apps, and - // those we always organize predictably. - return resolve(root, '_app_'); - } else { - // auto-upgraded apps don't get an exist _app_ dir. - return root; + if (pkg) { + if (pkg.isV2Addon()) { + // in general v2 addons can keep their app tree stuff in other places than + // "_app_" and we would need to check their package.json to see. But this code + // is only for applying packageRules to auto-upgraded v1 addons and apps, and + // those we always organize predictably. + return pathResolve(root, '_app_'); + } else { + // this is an app + let matched = resolveExports(pkg.packageJSON, './index.js'); + if (matched) { + return dirname(pathResolve(root, matched[0])); + } + } } + return root; } diff --git a/packages/compat/src/resolver-transform.ts b/packages/compat/src/resolver-transform.ts index 395c804a30..967f5bcaa4 100644 --- a/packages/compat/src/resolver-transform.ts +++ b/packages/compat/src/resolver-transform.ts @@ -547,17 +547,17 @@ class TemplateResolver implements ASTPlugin { 2. Have a mustache statement like: `{{something}}`, where `something` is: - a. Not a variable in scope (for example, there's no preceeding line + a. Not a variable in scope (for example, there's no preceeding line like ``) b. Does not start with `@` because that must be an argument from outside this template. - c. Does not contain a dot, like `some.thing` (because that case is classically + c. Does not contain a dot, like `some.thing` (because that case is classically never a global component resolution that we would need to handle) - d. Does not start with `this` (this rule is mostly redundant with the previous rule, + d. Does not start with `this` (this rule is mostly redundant with the previous rule, but even a standalone `this` is never a component invocation). - e. Does not have any arguments. If there are argument like `{{something a=b}}`, - there is still ambiguity between helper vs component, but there is no longer + e. Does not have any arguments. If there are argument like `{{something a=b}}`, + there is still ambiguity between helper vs component, but there is no longer the possibility that this was just rendering some data. - f. Does not take a block, like `{{#something}}{{/something}}` (because that is + f. Does not take a block, like `{{#something}}{{/something}}` (because that is always a component, no ambiguity.) We can't tell if this problematic case is really: @@ -571,7 +571,7 @@ class TemplateResolver implements ASTPlugin { 2. A component invocation, which you could have written `` instead. Angle-bracket invocation has been available and easy-to-adopt - for a very long time. + for a very long time. 3. Property-this-fallback for `{{this.something}}`. Property-this-fallback is eliminated at Ember 4.0, so people have been heavily pushed to get diff --git a/packages/compat/tests/audit.test.ts b/packages/compat/tests/audit.test.ts index d6b40712e3..4634d5cade 100644 --- a/packages/compat/tests/audit.test.ts +++ b/packages/compat/tests/audit.test.ts @@ -104,6 +104,10 @@ describe('audit', function () { merge(app.pkg, { 'ember-addon': appMeta, keywords: ['ember-addon'], + exports: { + './*': './*', + './tests/*': './tests/*', + }, }); }); diff --git a/packages/core/src/app-files.ts b/packages/core/src/app-files.ts index 5441164e7a..27316b2655 100644 --- a/packages/core/src/app-files.ts +++ b/packages/core/src/app-files.ts @@ -9,7 +9,6 @@ export interface RouteFiles { } export class AppFiles { - readonly tests: ReadonlyArray; readonly components: ReadonlyArray; readonly helpers: ReadonlyArray; readonly modifiers: ReadonlyArray; @@ -26,7 +25,6 @@ export class AppFiles { staticAppPathsPattern: RegExp | undefined, podModulePrefix?: string ) { - let tests: string[] = []; let components: string[] = []; let helpers: string[] = []; let modifiers: string[] = []; @@ -78,7 +76,7 @@ export class AppFiles { } if (relativePath.startsWith('tests/')) { - tests.push(relativePath); + // skip tests because they are dealt with separately continue; } @@ -134,7 +132,6 @@ export class AppFiles { otherAppFiles.push(relativePath); } } - this.tests = tests; this.components = components; this.helpers = helpers; this.modifiers = modifiers; diff --git a/packages/core/src/module-resolver.ts b/packages/core/src/module-resolver.ts index 9c4a9846a8..bad3a99944 100644 --- a/packages/core/src/module-resolver.ts +++ b/packages/core/src/module-resolver.ts @@ -201,7 +201,6 @@ export class Resolver { request = this.handleVendorStyles(request); request = this.handleTestSupportStyles(request); request = this.handleEntrypoint(request); - request = this.handleTestEntrypoint(request); request = this.handleRouteEntrypoint(request); request = this.handleRenaming(request); request = this.handleVendor(request); @@ -466,38 +465,14 @@ export class Resolver { } else { pkg = requestingPkg; } - - return logTransition('entrypoint', request, request.virtualize(resolve(pkg.root, '-embroider-entrypoint.js'))); - } - - private handleTestEntrypoint(request: R): R { - if (isTerminal(request)) { - return request; - } - - //TODO move the extra forwardslash handling out into the vite plugin - const candidates = [ - '@embroider/core/test-entrypoint', - '/@embroider/core/test-entrypoint', - './@embroider/core/test-entrypoint', - ]; - - if (!candidates.some(c => request.specifier === c)) { - return request; - } - - const pkg = this.packageCache.ownerOfFile(request.fromFile); - - if (!pkg?.isV2Ember() || !pkg.isV2App()) { - throw new Error( - `bug: found test entrypoint import from somewhere other than the top-level app engine: ${request.fromFile}` - ); - } - + let matched = resolveExports(pkg.packageJSON, '-embroider-entrypoint.js', { + browser: true, + conditions: ['default', 'imports'], + }); return logTransition( - 'test-entrypoint', + 'entrypoint', request, - request.virtualize(resolve(pkg.root, '-embroider-test-entrypoint.js')) + request.virtualize(resolve(pkg.root, matched?.[0] ?? '-embroider-entrypoint.js')) ); } @@ -518,7 +493,16 @@ export class Resolver { throw new Error(`bug: found entrypoint import in non-ember package at ${request.fromFile}`); } - return logTransition('route entrypoint', request, request.virtualize(encodeRouteEntrypoint(pkg.root, routeName))); + let matched = resolveExports(pkg.packageJSON, '-embroider-route-entrypoint.js', { + browser: true, + conditions: ['default', 'imports'], + }); + + return logTransition( + 'route entrypoint', + request, + request.virtualize(encodeRouteEntrypoint(pkg.root, matched?.[0], routeName)) + ); } private handleImplicitTestScripts(request: R): R { @@ -995,6 +979,11 @@ export class Resolver { // ember-source might provide backburner via module renaming, but if you // have an explicit dependency on backburner you should still get that real // copy. + + // if (pkg.root === this.options.engines[0].root && request.specifier === `${pkg.name}/environment/config`) { + // return logTransition('legacy config location', request, request.alias(`${pkg.name}/app/environment/config`)); + // } + if (!pkg.hasDependency(packageName)) { for (let [candidate, replacement] of Object.entries(this.options.renameModules)) { if (candidate === request.specifier) { @@ -1032,35 +1021,33 @@ export class Resolver { let owningEngine = this.owningEngine(pkg); let addonConfig = owningEngine.activeAddons.find(a => a.root === pkg.root); if (addonConfig) { + // auto-upgraded addons get special support for self-resolving here. return logTransition(`v1 addon self-import`, request, request.rehome(addonConfig.canResolveFromFile)); } else { - let selfImportPath = request.specifier === pkg.name ? './' : request.specifier.replace(pkg.name, '.'); + // auto-upgraded apps will necessarily have packageJSON.exports + // because we insert them, so for that support we can fall through to + // that support below. + } + } + + // v2 packages are supposed to use package.json `exports` to enable + // self-imports, but not all build tools actually follow the spec. This + // is a workaround for badly behaved packagers. + // + // Known upstream bugs this works around: + // - https://github.com/vitejs/vite/issues/9731 + if (pkg.packageJSON.exports) { + let found = resolveExports(pkg.packageJSON, request.specifier, { + browser: true, + conditions: ['default', 'imports'], + }); + if (found?.[0]) { return logTransition( - `v1 app self-import`, + `v2 self-import with package.json exports`, request, - request.alias(selfImportPath).rehome(resolve(pkg.root, 'package.json')) + request.alias(found?.[0]).rehome(resolve(pkg.root, 'package.json')) ); } - } else { - // v2 packages are supposed to use package.json `exports` to enable - // self-imports, but not all build tools actually follow the spec. This - // is a workaround for badly behaved packagers. - // - // Known upstream bugs this works around: - // - https://github.com/vitejs/vite/issues/9731 - if (pkg.packageJSON.exports) { - let found = resolveExports(pkg.packageJSON, request.specifier, { - browser: true, - conditions: ['default', 'imports'], - }); - if (found?.[0]) { - return logTransition( - `v2 self-import with package.json exports`, - request, - request.alias(found?.[0]).rehome(resolve(pkg.root, 'package.json')) - ); - } - } } } @@ -1137,21 +1124,15 @@ export class Resolver { // if the requesting file is in an addon's app-js, the relative request // should really be understood as a request for a module in the containing - // engine + // engine. let logicalLocation = this.reverseSearchAppTree(pkg, request.fromFile); if (logicalLocation) { return logTransition( 'beforeResolve: relative import in app-js', request, - request - .alias('./' + posix.join(dirname(logicalLocation.inAppName), request.specifier)) - // it's important that we're rehoming this to the root of the engine - // (which we know really exists), and not to a subdir like - // logicalLocation.inAppName (which might not physically exist), - // because some environments (including node's require.resolve) will - // refuse to do resolution from a notional path that doesn't - // physically exist. - .rehome(resolve(logicalLocation.owningEngine.root, 'package.json')) + request.alias( + posix.join(logicalLocation.owningEngine.packageName, dirname(logicalLocation.inAppName), request.specifier) + ) ); } @@ -1333,11 +1314,15 @@ export class Resolver { if (withinEngine) { // it's a relative import inside an engine (which also means app), which // means we may need to satisfy the request via app tree merging. - let appJSMatch = await this.searchAppTree( - request, - withinEngine, - explicitRelative(pkg.root, resolve(dirname(request.fromFile), request.specifier)) - ); + + let logicalName = engineRelativeName(pkg, resolve(dirname(request.fromFile), request.specifier)); + if (!logicalName) { + return logTransition( + 'fallbackResolve: relative failure because this file is not externally accessible', + request + ); + } + let appJSMatch = await this.searchAppTree(request, withinEngine, logicalName); if (appJSMatch) { return logTransition('fallbackResolve: relative appJsMatch', request, appJSMatch); } else { @@ -1494,16 +1479,10 @@ export class Resolver { if (engineConfig) { // we're directly inside an engine, so we're potentially resolvable as a // global component - - // this kind of mapping is not true in general for all packages, but it - // *is* true for all classical engines (which includes apps) since they - // don't support package.json `exports`. As for a future v2 engine or app: - // this whole method is only relevant for implementing packageRules, which - // should only be for classic stuff. v2 packages should do the right - // things from the beginning and not need packageRules about themselves. - let inAppName = explicitRelative(engineConfig.root, filename); - - return this.tryReverseComponent(engineConfig.packageName, inAppName); + let inAppName = engineRelativeName(owningPackage, filename); + if (inAppName) { + return this.tryReverseComponent(engineConfig.packageName, inAppName); + } } let engineInfo = this.reverseSearchAppTree(owningPackage, filename); @@ -1555,3 +1534,10 @@ function reliablyResolvable(pkg: V2Package, packageName: string) { function appImportInAppTree(inPackage: Package, inLogicalPackage: Package, importedPackageName: string): boolean { return inPackage !== inLogicalPackage && importedPackageName === inLogicalPackage.name; } + +function engineRelativeName(pkg: Package, filename: string): string | undefined { + let outsideName = externalName(pkg.packageJSON, explicitRelative(pkg.root, filename)); + if (outsideName) { + return '.' + outsideName.slice(pkg.name.length); + } +} diff --git a/packages/core/src/virtual-content.ts b/packages/core/src/virtual-content.ts index 7e405462e9..b46f64ab37 100644 --- a/packages/core/src/virtual-content.ts +++ b/packages/core/src/virtual-content.ts @@ -8,7 +8,6 @@ import { decodeVirtualVendor, renderVendor } from './virtual-vendor'; import { decodeVirtualVendorStyles, renderVendorStyles } from './virtual-vendor-styles'; import { decodeEntrypoint, renderEntrypoint } from './virtual-entrypoint'; -import { decodeTestEntrypoint, renderTestEntrypoint } from './virtual-test-entrypoint'; import { decodeRouteEntrypoint, renderRouteEntrypoint } from './virtual-route-entrypoint'; const externalESPrefix = '/@embroider/ext-es/'; @@ -34,11 +33,6 @@ export function virtualContent(filename: string, resolver: Resolver): VirtualCon return renderEntrypoint(resolver, entrypoint); } - let testEntrypoint = decodeTestEntrypoint(filename); - if (testEntrypoint) { - return renderTestEntrypoint(resolver, testEntrypoint); - } - let routeEntrypoint = decodeRouteEntrypoint(filename); if (routeEntrypoint) { return renderRouteEntrypoint(resolver, routeEntrypoint); diff --git a/packages/core/src/virtual-entrypoint.ts b/packages/core/src/virtual-entrypoint.ts index 257e6b7a13..a74c5648ae 100644 --- a/packages/core/src/virtual-entrypoint.ts +++ b/packages/core/src/virtual-entrypoint.ts @@ -12,7 +12,7 @@ import escapeRegExp from 'escape-string-regexp'; const entrypointPattern = /(?.*)[\\/]-embroider-entrypoint.js/; -export function decodeEntrypoint(filename: string): { fromFile: string } | undefined { +export function decodeEntrypoint(filename: string): { fromDir: string } | undefined { // Performance: avoid paying regex exec cost unless needed if (!filename.includes('-embroider-entrypoint')) { return; @@ -20,7 +20,7 @@ export function decodeEntrypoint(filename: string): { fromFile: string } | undef let m = entrypointPattern.exec(filename); if (m) { return { - fromFile: m.groups!.filename, + fromDir: m.groups!.filename, }; } } @@ -33,10 +33,10 @@ export function staticAppPathsPattern(staticAppPaths: string[] | undefined): Reg export function renderEntrypoint( resolver: Resolver, - { fromFile }: { fromFile: string } + { fromDir }: { fromDir: string } ): { src: string; watches: string[] } { // this is new - const owner = resolver.packageCache.ownerOfFile(fromFile); + const owner = resolver.packageCache.ownerOfFile(fromDir); let eagerModules: string[] = []; @@ -61,7 +61,7 @@ export function renderEntrypoint( modulePrefix: isApp ? resolver.options.modulePrefix : engine.packageName, appRelativePath: 'NOT_USED_DELETE_ME', }, - getAppFiles(owner.root), + getAppFiles(fromDir), hasFastboot ? getFastbootFiles(owner.root) : new Set(), extensionsPattern(resolver.options.resolvableExtensions), staticAppPathsPattern(resolver.options.staticAppPaths), @@ -154,8 +154,6 @@ export function renderEntrypoint( const entryTemplate = compile(` import { macroCondition, getGlobalConfig } from '@embroider/macros'; -import environment from './config/environment'; - {{#if styles}} if (macroCondition(!getGlobalConfig().fastboot?.isRunning)) { {{#each styles as |stylePath| ~}} diff --git a/packages/core/src/virtual-route-entrypoint.ts b/packages/core/src/virtual-route-entrypoint.ts index 82f589fe62..32a7af5a64 100644 --- a/packages/core/src/virtual-route-entrypoint.ts +++ b/packages/core/src/virtual-route-entrypoint.ts @@ -9,11 +9,11 @@ import { getAppFiles, getFastbootFiles, importPaths, splitRoute, staticAppPathsP const entrypointPattern = /(?.*)[\\/]-embroider-route-entrypoint.js:route=(?.*)/; -export function encodeRouteEntrypoint(packagePath: string, routeName: string): string { - return resolve(packagePath, `-embroider-route-entrypoint.js:route=${routeName}`); +export function encodeRouteEntrypoint(packagePath: string, matched: string | undefined, routeName: string): string { + return resolve(packagePath, `${matched}:route=${routeName}` ?? `-embroider-route-entrypoint.js:route=${routeName}`); } -export function decodeRouteEntrypoint(filename: string): { fromFile: string; route: string } | undefined { +export function decodeRouteEntrypoint(filename: string): { fromDir: string; route: string } | undefined { // Performance: avoid paying regex exec cost unless needed if (!filename.includes('-embroider-route-entrypoint')) { return; @@ -21,7 +21,7 @@ export function decodeRouteEntrypoint(filename: string): { fromFile: string; rou let m = entrypointPattern.exec(filename); if (m) { return { - fromFile: m.groups!.filename, + fromDir: m.groups!.filename, route: m.groups!.route, }; } @@ -42,9 +42,9 @@ export function decodePublicRouteEntrypoint(specifier: string): string | null { export function renderRouteEntrypoint( resolver: Resolver, - { fromFile, route }: { fromFile: string; route: string } + { fromDir, route }: { fromDir: string; route: string } ): { src: string; watches: string[] } { - const owner = resolver.packageCache.ownerOfFile(fromFile); + const owner = resolver.packageCache.ownerOfFile(fromDir); if (!owner) { throw new Error('Owner expected'); // ToDo: Really bad error, update message @@ -67,7 +67,7 @@ export function renderRouteEntrypoint( modulePrefix: isApp ? resolver.options.modulePrefix : engine.packageName, appRelativePath: 'NOT_USED_DELETE_ME', }, - getAppFiles(owner.root), + getAppFiles(fromDir), hasFastboot ? getFastbootFiles(owner.root) : new Set(), extensionsPattern(resolver.options.resolvableExtensions), staticAppPathsPattern(resolver.options.staticAppPaths), diff --git a/packages/core/src/virtual-test-entrypoint.ts b/packages/core/src/virtual-test-entrypoint.ts deleted file mode 100644 index cb9168f816..0000000000 --- a/packages/core/src/virtual-test-entrypoint.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { AppFiles } from './app-files'; -import { compile } from './js-handlebars'; -import type { Resolver } from './module-resolver'; -import { extensionsPattern } from '@embroider/shared-internals'; -import type { V2AddonPackage } from '@embroider/shared-internals/src/package'; -import { getAppFiles, importPaths, staticAppPathsPattern } from './virtual-entrypoint'; - -const entrypointPattern = /(?.*)[\\/]-embroider-test-entrypoint.js/; - -export function decodeTestEntrypoint(filename: string): { fromFile: string } | undefined { - // Performance: avoid paying regex exec cost unless needed - if (!filename.includes('-embroider-test-entrypoint.js')) { - return; - } - let m = entrypointPattern.exec(filename); - if (m) { - return { - fromFile: m.groups!.filename, - }; - } -} - -export function renderTestEntrypoint( - resolver: Resolver, - { fromFile }: { fromFile: string } -): { src: string; watches: string[] } { - const owner = resolver.packageCache.ownerOfFile(fromFile); - - if (!owner) { - throw new Error(`Owner expected while loading test entrypoint from file: ${fromFile}`); - } - - let engine = resolver.owningEngine(owner); - - let appFiles = new AppFiles( - { - package: owner, - addons: new Map( - engine.activeAddons.map(addon => [ - resolver.packageCache.get(addon.root) as V2AddonPackage, - addon.canResolveFromFile, - ]) - ), - isApp: true, - modulePrefix: resolver.options.modulePrefix, - appRelativePath: 'NOT_USED_DELETE_ME', - }, - getAppFiles(owner.root), - new Set(), // no fastboot files - extensionsPattern(resolver.options.resolvableExtensions), - staticAppPathsPattern(resolver.options.staticAppPaths), - resolver.options.podModulePrefix - ); - - let amdModules: { runtime: string; buildtime: string }[] = []; - - for (let relativePath of appFiles.tests) { - amdModules.push(importPaths(resolver, appFiles, relativePath)); - } - - let src = entryTemplate({ - amdModules, - }); - - return { - src, - watches: [], - }; -} - -const entryTemplate = compile(` -import { importSync as i } from '@embroider/macros'; -let w = window; -let d = w.define; - -import "ember-testing"; -import "@embroider/core/entrypoint"; - -{{#each amdModules as |amdModule| ~}} - d("{{js-string-escape amdModule.runtime}}", function(){ return i("{{js-string-escape amdModule.buildtime}}");}); -{{/each}} - -import('./tests/test-helper'); -EmberENV.TESTS_FILE_LOADED = true; -`) as (params: { amdModules: { runtime: string; buildtime: string }[] }) => string; diff --git a/packages/shared-internals/package.json b/packages/shared-internals/package.json index c6aa44a287..4da247271f 100644 --- a/packages/shared-internals/package.json +++ b/packages/shared-internals/package.json @@ -37,6 +37,7 @@ "fs-extra": "^9.1.0", "lodash": "^4.17.21", "minimatch": "^3.0.4", + "resolve.exports": "^2.0.2", "semver": "^7.3.5" }, "devDependencies": { diff --git a/packages/shared-internals/src/colocation.ts b/packages/shared-internals/src/colocation.ts index 7ab2618271..33b4a974cd 100644 --- a/packages/shared-internals/src/colocation.ts +++ b/packages/shared-internals/src/colocation.ts @@ -2,6 +2,7 @@ import { existsSync } from 'fs-extra'; import { cleanUrl } from './paths'; import type PackageCache from './package-cache'; import { sep } from 'path'; +import { resolve as resolveExports } from 'resolve.exports'; export function syntheticJStoHBS(source: string): string | null { // explicit js is the only case we care about here. Synthetic template JS is @@ -37,8 +38,18 @@ function correspondingJSExists(id: string): boolean { export function isInComponents(url: string, packageCache: Pick) { const id = cleanUrl(url); + const pkg = packageCache.ownerOfFile(id); - return pkg?.isV2App() && id.slice(pkg?.root.length).split(sep).join('/').startsWith('/components'); + if (!pkg?.isV2App()) { + return false; + } + + let tryResolve = resolveExports(pkg.packageJSON, './components', { + browser: true, + conditions: ['default', 'imports'], + }); + let componentsDir = tryResolve?.[0] ?? './components'; + return ('.' + id.slice(pkg?.root.length).split(sep).join('/')).startsWith(componentsDir); } export function templateOnlyComponentSource() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a723932b78..cf7769e4d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -274,6 +274,9 @@ importers: resolve-package-path: specifier: ^4.0.1 version: 4.0.3 + resolve.exports: + specifier: ^2.0.2 + version: 2.0.2 semver: specifier: ^7.3.5 version: 7.6.3 @@ -707,6 +710,9 @@ importers: resolve-package-path: specifier: ^4.0.1 version: 4.0.3 + resolve.exports: + specifier: ^2.0.2 + version: 2.0.2 semver: specifier: ^7.3.5 version: 7.6.3 diff --git a/test-packages/sample-transforms/tests/index.html b/test-packages/sample-transforms/tests/index.html index 422eec652f..1f9b19483c 100644 --- a/test-packages/sample-transforms/tests/index.html +++ b/test-packages/sample-transforms/tests/index.html @@ -32,9 +32,14 @@ - + + + {{content-for "body-footer"}} - {{content-for "test-body-footer"}} diff --git a/test-packages/sample-transforms/tests/test-helper.js b/test-packages/sample-transforms/tests/test-helper.js index 4efd6e58ae..737ad5d020 100644 --- a/test-packages/sample-transforms/tests/test-helper.js +++ b/test-packages/sample-transforms/tests/test-helper.js @@ -3,10 +3,12 @@ import config from 'dummy/config/environment'; import * as QUnit from 'qunit'; import { setApplication } from '@ember/test-helpers'; import { setup } from 'qunit-dom'; -import { start } from 'ember-qunit'; +import { start as qunitStart } from 'ember-qunit'; -setApplication(Application.create(config.APP)); +export function start() { + setApplication(Application.create(config.APP)); -setup(QUnit.assert); + setup(QUnit.assert); -start(); + qunitStart(); +} diff --git a/tests/addon-template/tests/dummy/app/index.html b/tests/addon-template/tests/dummy/index.html similarity index 86% rename from tests/addon-template/tests/dummy/app/index.html rename to tests/addon-template/tests/dummy/index.html index c55e417ab7..44af3dc9b1 100644 --- a/tests/addon-template/tests/dummy/app/index.html +++ b/tests/addon-template/tests/dummy/index.html @@ -18,8 +18,8 @@ diff --git a/tests/addon-template/tests/index.html b/tests/addon-template/tests/index.html index 242088d317..3922c38923 100644 --- a/tests/addon-template/tests/index.html +++ b/tests/addon-template/tests/index.html @@ -30,9 +30,14 @@ - + + + {{content-for "body-footer"}} - {{content-for "test-body-footer"}} diff --git a/tests/addon-template/tests/test-helper.js b/tests/addon-template/tests/test-helper.js index 4efd6e58ae..737ad5d020 100644 --- a/tests/addon-template/tests/test-helper.js +++ b/tests/addon-template/tests/test-helper.js @@ -3,10 +3,12 @@ import config from 'dummy/config/environment'; import * as QUnit from 'qunit'; import { setApplication } from '@ember/test-helpers'; import { setup } from 'qunit-dom'; -import { start } from 'ember-qunit'; +import { start as qunitStart } from 'ember-qunit'; -setApplication(Application.create(config.APP)); +export function start() { + setApplication(Application.create(config.APP)); -setup(QUnit.assert); + setup(QUnit.assert); -start(); + qunitStart(); +} diff --git a/tests/app-template/app/index.html b/tests/app-template/index.html similarity index 86% rename from tests/app-template/app/index.html rename to tests/app-template/index.html index c53dfc7da6..60a4b93e50 100644 --- a/tests/app-template/app/index.html +++ b/tests/app-template/index.html @@ -18,8 +18,8 @@ diff --git a/tests/app-template/package.json b/tests/app-template/package.json index 978fc99dcd..0d674cf66b 100644 --- a/tests/app-template/package.json +++ b/tests/app-template/package.json @@ -11,7 +11,8 @@ "test": "tests" }, "exports": { - "./*": "./*" + "./tests/*": "./tests/*", + "./*": "./app/*" }, "scripts": { "build": "vite build", diff --git a/tests/app-template/tests/index.html b/tests/app-template/tests/index.html index c68003bd76..a9f23697fc 100644 --- a/tests/app-template/tests/index.html +++ b/tests/app-template/tests/index.html @@ -27,8 +27,14 @@ - + - {{content-for "body-footer"}} {{content-for "test-body-footer"}} + + + {{content-for "body-footer"}} diff --git a/tests/app-template/tests/test-helper.js b/tests/app-template/tests/test-helper.js index 06c62fcbbe..2ee2ff8d1f 100644 --- a/tests/app-template/tests/test-helper.js +++ b/tests/app-template/tests/test-helper.js @@ -3,10 +3,12 @@ import config from 'app-template/config/environment'; import * as QUnit from 'qunit'; import { setApplication } from '@ember/test-helpers'; import { setup } from 'qunit-dom'; -import { start } from 'ember-qunit'; +import { start as qunitStart } from 'ember-qunit'; -setApplication(Application.create(config.APP)); +export function start() { + setApplication(Application.create(config.APP)); -setup(QUnit.assert); + setup(QUnit.assert); -start(); + qunitStart(); +} diff --git a/tests/fixtures/macro-sample-addon-classic/tests/test-helper.js b/tests/fixtures/macro-sample-addon-classic/tests/test-helper.js new file mode 100644 index 0000000000..4efd6e58ae --- /dev/null +++ b/tests/fixtures/macro-sample-addon-classic/tests/test-helper.js @@ -0,0 +1,12 @@ +import Application from 'dummy/app'; +import config from 'dummy/config/environment'; +import * as QUnit from 'qunit'; +import { setApplication } from '@ember/test-helpers'; +import { setup } from 'qunit-dom'; +import { start } from 'ember-qunit'; + +setApplication(Application.create(config.APP)); + +setup(QUnit.assert); + +start(); diff --git a/tests/fixtures/macro-sample-addon/tests/dummy/app/index.html b/tests/fixtures/macro-sample-addon/tests/dummy/app/index.html index 24d04a975f..8dfc7c9bd7 100644 --- a/tests/fixtures/macro-sample-addon/tests/dummy/app/index.html +++ b/tests/fixtures/macro-sample-addon/tests/dummy/app/index.html @@ -18,8 +18,8 @@ - + + + {{content-for "body-footer"}} - {{content-for "test-body-footer"}} diff --git a/tests/fixtures/macro-sample-addon/tests/test-helper.js b/tests/fixtures/macro-sample-addon/tests/test-helper.js index 8b86cbbe86..a656e27c7f 100644 --- a/tests/fixtures/macro-sample-addon/tests/test-helper.js +++ b/tests/fixtures/macro-sample-addon/tests/test-helper.js @@ -3,11 +3,13 @@ import config from 'dummy/config/environment'; import * as QUnit from 'qunit'; import { setApplication } from '@ember/test-helpers'; import { setup } from 'qunit-dom'; -import { start } from 'ember-qunit'; +import { start as qunitStart } from 'ember-qunit'; -window.LoadedFromCustomAppBoot = true; -setApplication(Application.create(config.APP)); +export function start() { + window.LoadedFromCustomAppBoot = true; + setApplication(Application.create(config.APP)); -setup(QUnit.assert); + setup(QUnit.assert); -start(); + qunitStart(); +} diff --git a/tests/fixtures/macro-test-classic/tests/test-helper.js b/tests/fixtures/macro-test-classic/tests/test-helper.js new file mode 100644 index 0000000000..06c62fcbbe --- /dev/null +++ b/tests/fixtures/macro-test-classic/tests/test-helper.js @@ -0,0 +1,12 @@ +import Application from 'app-template/app'; +import config from 'app-template/config/environment'; +import * as QUnit from 'qunit'; +import { setApplication } from '@ember/test-helpers'; +import { setup } from 'qunit-dom'; +import { start } from 'ember-qunit'; + +setApplication(Application.create(config.APP)); + +setup(QUnit.assert); + +start(); diff --git a/tests/fixtures/macro-test/tests/index.html b/tests/fixtures/macro-test/tests/index.html index 12e974b67f..c231c4e5e6 100644 --- a/tests/fixtures/macro-test/tests/index.html +++ b/tests/fixtures/macro-test/tests/index.html @@ -36,9 +36,14 @@ - + + + {{content-for "body-footer"}} - {{content-for "test-body-footer"}} - \ No newline at end of file + diff --git a/tests/scenarios/compat-addon-classic-features-test.ts b/tests/scenarios/compat-addon-classic-features-test.ts index 3f5ebbd58c..a6ef2a8e00 100644 --- a/tests/scenarios/compat-addon-classic-features-test.ts +++ b/tests/scenarios/compat-addon-classic-features-test.ts @@ -42,14 +42,14 @@ appScenarios const EmberApp = require('ember-cli/lib/broccoli/ember-app'); const { maybeEmbroider } = require('@embroider/test-setup'); - + module.exports = function (defaults) { let app = new EmberApp(defaults, { ...(process.env.FORCE_BUILD_TESTS ? { tests: true, } : undefined), }); - + return maybeEmbroider(app, { availableContentForTypes: ['custom'], skipBabel: [ @@ -60,8 +60,7 @@ appScenarios }); }; `, - app: { - 'index.html': ` + 'index.html': ` @@ -69,31 +68,30 @@ appScenarios AppTemplate - + {{content-for "head"}} - + - + {{content-for "head-footer"}} {{content-for "body"}} {{content-for "custom"}} - + - + {{content-for "body-footer"}} `, - }, }); }) .forEachScenario(scenario => { diff --git a/tests/scenarios/compat-app-html-attributes-test.ts b/tests/scenarios/compat-app-html-attributes-test.ts index e4875e1324..ec04b0a283 100644 --- a/tests/scenarios/compat-app-html-attributes-test.ts +++ b/tests/scenarios/compat-app-html-attributes-test.ts @@ -8,13 +8,7 @@ const { module: Qmodule, test } = QUnit; appScenarios .map('compat-app-script-attributes', app => { - let appFolder = app.files.app; - - if (appFolder === null || typeof appFolder !== 'object') { - throw new Error('app folder unexpectedly missing'); - } - - let indexHtml = appFolder['index.html']; + let indexHtml = app.files['index.html']; if (typeof indexHtml !== 'string') { throw new Error('index.html unexpectedly missing'); @@ -38,9 +32,7 @@ appScenarios indexHtml = indexHtml.replace(/', diff --git a/tests/scenarios/helpers/command-watcher.ts b/tests/scenarios/helpers/command-watcher.ts index ca9d145ebf..817ec1cf11 100644 --- a/tests/scenarios/helpers/command-watcher.ts +++ b/tests/scenarios/helpers/command-watcher.ts @@ -105,6 +105,7 @@ export default class CommandWatcher { async shutdown(): Promise { if (this.exitCode != null) { + this.maybeEmitLogs(); return; } @@ -119,6 +120,14 @@ export default class CommandWatcher { }); await this.waitForExit(); + this.maybeEmitLogs(); + } + + private maybeEmitLogs() { + if (this.exitCode !== 0) { + console.error(`CommandWatcher saw non-zero exit, dumping logs:`); + console.error(this.lines.join('\n')); + } } async waitForExit(): Promise { diff --git a/tests/scenarios/macro-test.ts b/tests/scenarios/macro-test.ts index 297f0e2a01..f592432595 100644 --- a/tests/scenarios/macro-test.ts +++ b/tests/scenarios/macro-test.ts @@ -108,7 +108,7 @@ appScenarios }); appScenarios - .map('macro-tests-classic', project => { + .map('classic-macro-tests', project => { scenarioSetup(project); merge(project.files, loadFromFixtureData('macro-test-classic')); }) @@ -120,7 +120,7 @@ appScenarios app = await scenario.prepare(); }); - test(`EMBROIDER_TEST_SETUP_FORCE=classic pnpm test`, async function (assert) { + test(`EMBROIDER_TEST_SETUP_FORCE=classic pnpm ember test`, async function (assert) { // throw_unless_parallelizable is enabled to ensure that @embroider/macros is parallelizable let result = await app.execute(`pnpm ember test`, { env: { diff --git a/tests/ts-app-template/app/index.html b/tests/ts-app-template/index.html similarity index 86% rename from tests/ts-app-template/app/index.html rename to tests/ts-app-template/index.html index 8fcb46225b..3c632d832b 100644 --- a/tests/ts-app-template/app/index.html +++ b/tests/ts-app-template/index.html @@ -18,8 +18,8 @@ diff --git a/tests/ts-app-template/package.json b/tests/ts-app-template/package.json index 090209d6c8..2b35c57255 100644 --- a/tests/ts-app-template/package.json +++ b/tests/ts-app-template/package.json @@ -11,7 +11,8 @@ "test": "tests" }, "exports": { - "./*": "./*" + "./tests/*": "./tests/*", + "./*": "./app/*" }, "scripts": { "build": "ember build --environment=production", diff --git a/tests/ts-app-template/tests/index.html b/tests/ts-app-template/tests/index.html index 775da4da1e..337ce67a50 100644 --- a/tests/ts-app-template/tests/index.html +++ b/tests/ts-app-template/tests/index.html @@ -30,9 +30,14 @@ - + + + {{content-for "body-footer"}} - {{content-for "test-body-footer"}} diff --git a/tests/ts-app-template/tests/test-helper.ts b/tests/ts-app-template/tests/test-helper.ts index 3570b8c6b7..d7f83ececa 100644 --- a/tests/ts-app-template/tests/test-helper.ts +++ b/tests/ts-app-template/tests/test-helper.ts @@ -3,10 +3,12 @@ import config from 'ts-app-template/config/environment'; import * as QUnit from 'qunit'; import { setApplication } from '@ember/test-helpers'; import { setup } from 'qunit-dom'; -import { start } from 'ember-qunit'; +import { start as qunitStart } from 'ember-qunit'; -setApplication(Application.create(config.APP)); +export function start() { + setApplication(Application.create(config.APP)); -setup(QUnit.assert); + setup(QUnit.assert); -start(); + qunitStart(); +}