diff --git a/package.json b/package.json index d93c14ba5..5cb29950e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,9 @@ "graceful-fs": "^4.0.0", "@types/eslint": "^8.37.0", "babel-plugin-module-resolver@5.0.1": "5.0.0" + }, + "patchedDependencies": { + "ember-source@5.8.0": "patches/ember-source@5.8.0.patch" } }, "devDependencies": { diff --git a/packages/compat/src/compat-app-builder.ts b/packages/compat/src/compat-app-builder.ts index 86d0357d2..32de08e87 100644 --- a/packages/compat/src/compat-app-builder.ts +++ b/packages/compat/src/compat-app-builder.ts @@ -27,8 +27,6 @@ import type { CompatResolverOptions } from './resolver-transform'; import type { PackageRules } from './dependency-rules'; import { activePackageRules } from './dependency-rules'; import flatMap from 'lodash/flatMap'; -import flatten from 'lodash/flatten'; -import partition from 'lodash/partition'; import mergeWith from 'lodash/mergeWith'; import cloneDeep from 'lodash/cloneDeep'; import { sync as resolveSync } from 'resolve'; @@ -40,7 +38,6 @@ import type { Options as AdjustImportsOptions } from './babel-plugin-adjust-impo import { PreparedEmberHTML } from '@embroider/core/src/ember-html'; import type { InMemoryAsset, OnDiskAsset } from '@embroider/core/src/asset'; import { makePortable } from '@embroider/core/src/portable-babel-config'; -import type { RouteFiles } from '@embroider/core/src/app-files'; import { AppFiles } from '@embroider/core/src/app-files'; import type { PortableHint } from '@embroider/core/src/portable'; import { maybeNodeModuleVersion } from '@embroider/core/src/portable'; @@ -50,11 +47,10 @@ import { join, dirname } from 'path'; import resolve from 'resolve'; import type ContentForConfig from './content-for-config'; import type { V1Config } from './v1-config'; -import type { AddonMeta, Package, PackageInfo } from '@embroider/core'; +import type { Package, PackageInfo } from '@embroider/core'; import { ensureDirSync, copySync, readdirSync, pathExistsSync } from 'fs-extra'; import type { TransformOptions } from '@babel/core'; import { MacrosConfig } from '@embroider/macros/src/node'; -import SourceMapConcat from 'fast-sourcemap-concat'; import escapeRegExp from 'escape-string-regexp'; import type CompatApp from './compat-app'; @@ -279,9 +275,12 @@ export class CompatAppBuilder { // this is the additional stufff that @embroider/compat adds on top to do // global template resolving modulePrefix: this.modulePrefix(), + splitAtRoutes: this.options.splitAtRoutes, podModulePrefix: this.podModulePrefix(), activePackageRules: this.activeRules(), options, + autoRun: this.compatApp.autoRun, + staticAppPaths: this.options.staticAppPaths, }; return config; @@ -385,8 +384,7 @@ export class CompatAppBuilder { // our tests entrypoint already includes a correct module dependency on the // app, so we only insert the app when we're not inserting tests if (!asset.fileAsset.includeTests) { - let appJS = this.topAppJSAsset(appFiles, prepared); - html.insertScriptTag(html.javascript, appJS.relativePath, { type: 'module' }); + html.insertScriptTag(html.javascript, '@embroider/core/entrypoint', { type: 'module' }); } if (this.fastbootConfig) { @@ -550,11 +548,21 @@ export class CompatAppBuilder { appSync.files, fastbootSync?.files ?? new Set(), this.resolvableExtensionsPattern, + this.staticAppPathsPattern, this.podModulePrefix() ) ); } + @Memoize() + private get staticAppPathsPattern(): RegExp | undefined { + if (this.options.staticAppPaths.length > 0) { + return new RegExp( + '^(?:' + this.options.staticAppPaths.map(staticAppPath => escapeRegExp(staticAppPath)).join('|') + ')(?:$|/)' + ); + } + } + private prepareAsset(asset: Asset, appFiles: AppFiles[], prepared: Map) { if (asset.kind === 'ember') { let prior = this.assets.get(asset.relativePath); @@ -592,15 +600,6 @@ export class CompatAppBuilder { return prior.kind === 'in-memory' && stringOrBufferEqual(prior.source, asset.source); case 'built-ember': return prior.kind === 'built-ember' && prior.source === asset.source; - case 'concatenated-asset': - return ( - prior.kind === 'concatenated-asset' && - prior.sources.length === asset.sources.length && - prior.sources.every((priorFile, index) => { - let newFile = asset.sources[index]; - return this.assetIsValid(newFile, priorFile); - }) - ); } } @@ -622,35 +621,7 @@ export class CompatAppBuilder { writeFileSync(destination, asset.source, 'utf8'); } - private async updateConcatenatedAsset(asset: ConcatenatedAsset) { - let concat = new SourceMapConcat({ - outputFile: join(this.root, asset.relativePath), - mapCommentType: asset.relativePath.endsWith('.js') ? 'line' : 'block', - baseDir: this.root, - }); - if (process.env.EMBROIDER_CONCAT_STATS) { - let MeasureConcat = (await import('@embroider/core/src/measure-concat')).default; - concat = new MeasureConcat(asset.relativePath, concat, this.root); - } - for (let source of asset.sources) { - switch (source.kind) { - case 'on-disk': - concat.addFile(explicitRelative(this.root, source.sourcePath)); - break; - case 'in-memory': - if (typeof source.source !== 'string') { - throw new Error(`attempted to concatenated a Buffer-backed in-memory asset`); - } - concat.addSpace(source.source); - break; - default: - assertNever(source); - } - } - await concat.end(); - } - - private async updateAssets(requestedAssets: Asset[], appFiles: AppFiles[]) { + private async updateAssets(requestedAssets: Asset[], appFiles: AppFiles[]): Promise { let assets = this.prepareAssets(requestedAssets, appFiles); for (let asset of assets.values()) { if (this.assetIsValid(asset, this.assets.get(asset.relativePath))) { @@ -667,9 +638,6 @@ export class CompatAppBuilder { case 'built-ember': this.updateBuiltEmberAsset(asset); break; - case 'concatenated-asset': - await this.updateConcatenatedAsset(asset); - break; default: assertNever(asset); } @@ -680,7 +648,6 @@ export class CompatAppBuilder { } } this.assets = assets; - return [...assets.values()]; } private gatherAssets(inputPaths: OutputPaths): Asset[] { @@ -723,7 +690,7 @@ export class CompatAppBuilder { let appFiles = this.updateAppJS(inputPaths.appJS); let assets = this.gatherAssets(inputPaths); - let finalAssets = await this.updateAssets(assets, appFiles); + await this.updateAssets(assets, appFiles); let assetPaths = assets.map(asset => asset.relativePath); @@ -732,15 +699,6 @@ export class CompatAppBuilder { assetPaths.push('package.json'); } - for (let asset of finalAssets) { - // our concatenated assets all have map files that ride along. Here we're - // telling the final stage packager to be sure and serve the map files - // too. - if (asset.kind === 'concatenated-asset') { - assetPaths.push(asset.sourcemapPath); - } - } - let meta: AppMeta = { type: 'app', version: 2, @@ -764,6 +722,7 @@ export class CompatAppBuilder { this.addResolverConfig(resolverConfig); this.addContentForConfig(this.contentForTree.readContents()); this.addEmberEnvConfig(this.configTree.readConfig().EmberENV); + this.addAppBoot(this.compatApp.appBoot.readAppBoot()); let babelConfig = await this.babelConfig(resolverConfig); this.addBabelConfig(babelConfig); writeFileSync( @@ -873,222 +832,8 @@ export class CompatAppBuilder { }); } - private shouldSplitRoute(routeName: string) { - return ( - !this.options.splitAtRoutes || - this.options.splitAtRoutes.find(pattern => { - if (typeof pattern === 'string') { - return pattern === routeName; - } else { - return pattern.test(routeName); - } - }) - ); - } - - private splitRoute( - routeName: string, - files: RouteFiles, - addToParent: (routeName: string, filename: string) => void, - addLazyBundle: (routeNames: string[], files: string[]) => void - ) { - let shouldSplit = routeName && this.shouldSplitRoute(routeName); - let ownFiles = []; - let ownNames = new Set() as Set; - - if (files.template) { - if (shouldSplit) { - ownFiles.push(files.template); - ownNames.add(routeName); - } else { - addToParent(routeName, files.template); - } - } - - if (files.controller) { - if (shouldSplit) { - ownFiles.push(files.controller); - ownNames.add(routeName); - } else { - addToParent(routeName, files.controller); - } - } - - if (files.route) { - if (shouldSplit) { - ownFiles.push(files.route); - ownNames.add(routeName); - } else { - addToParent(routeName, files.route); - } - } - - for (let [childName, childFiles] of files.children) { - this.splitRoute( - `${routeName}.${childName}`, - childFiles, - - (childRouteName: string, childFile: string) => { - // this is our child calling "addToParent" - if (shouldSplit) { - ownFiles.push(childFile); - ownNames.add(childRouteName); - } else { - addToParent(childRouteName, childFile); - } - }, - (routeNames: string[], files: string[]) => { - addLazyBundle(routeNames, files); - } - ); - } - - if (ownFiles.length > 0) { - addLazyBundle([...ownNames], ownFiles); - } - } - - private topAppJSAsset(engines: AppFiles[], prepared: Map): InternalAsset { - let [app, ...childEngines] = engines; - let relativePath = `assets/${this.origAppPackage.name}.js`; - return this.appJSAsset(relativePath, app, childEngines, prepared, { - autoRun: this.compatApp.autoRun, - appBoot: !this.compatApp.autoRun ? this.compatApp.appBoot.readAppBoot() : '', - mainModule: explicitRelative(dirname(relativePath), 'app'), - appConfig: this.configTree.readConfig().APP, - }); - } - - @Memoize() - private get staticAppPathsPattern(): RegExp | undefined { - if (this.options.staticAppPaths.length > 0) { - return new RegExp( - '^(?:' + this.options.staticAppPaths.map(staticAppPath => escapeRegExp(staticAppPath)).join('|') + ')(?:$|/)' - ); - } - } - - private requiredOtherFiles(appFiles: AppFiles): readonly string[] { - let pattern = this.staticAppPathsPattern; - if (pattern) { - return appFiles.otherAppFiles.filter(f => { - return !pattern!.test(f); - }); - } else { - return appFiles.otherAppFiles; - } - } - - private appJSAsset( - relativePath: string, - appFiles: AppFiles, - childEngines: AppFiles[], - prepared: Map, - entryParams?: Partial[0]> - ): InternalAsset { - let cached = prepared.get(relativePath); - if (cached) { - return cached; - } - - let eagerModules: string[] = []; - - let requiredAppFiles = [this.requiredOtherFiles(appFiles)]; - if (!this.options.staticComponents) { - requiredAppFiles.push(appFiles.components); - } - if (!this.options.staticHelpers) { - requiredAppFiles.push(appFiles.helpers); - } - if (!this.options.staticModifiers) { - requiredAppFiles.push(appFiles.modifiers); - } - - let styles = []; - // only import styles from engines with a parent (this excludeds the parent application) as their styles - // will be inserted via a direct tag. - if (!appFiles.engine.isApp && appFiles.engine.package.isLazyEngine()) { - styles.push({ - path: '@embroider/core/vendor.css', - }); - - let engineMeta = appFiles.engine.package.meta as AddonMeta; - if (engineMeta && engineMeta['implicit-styles']) { - for (let style of engineMeta['implicit-styles']) { - styles.push({ - path: explicitRelative(dirname(relativePath), join(appFiles.engine.appRelativePath, style)), - }); - } - } - } - - let lazyEngines: { names: string[]; path: string }[] = []; - for (let childEngine of childEngines) { - let asset = this.appJSAsset( - `assets/_engine_/${encodeURIComponent(childEngine.engine.package.name)}.js`, - childEngine, - [], - prepared - ); - if (childEngine.engine.package.isLazyEngine()) { - lazyEngines.push({ - names: [childEngine.engine.package.name], - path: explicitRelative(dirname(relativePath), asset.relativePath), - }); - } else { - eagerModules.push(explicitRelative(dirname(relativePath), asset.relativePath)); - } - } - let lazyRoutes: { names: string[]; path: string }[] = []; - for (let [routeName, routeFiles] of appFiles.routeFiles.children) { - this.splitRoute( - routeName, - routeFiles, - (_: string, filename: string) => { - requiredAppFiles.push([filename]); - }, - (routeNames: string[], files: string[]) => { - let routeEntrypoint = `assets/_route_/${encodeURIComponent(routeNames[0])}.js`; - if (!prepared.has(routeEntrypoint)) { - prepared.set(routeEntrypoint, this.routeEntrypoint(appFiles, routeEntrypoint, files)); - } - lazyRoutes.push({ - names: routeNames, - path: this.importPaths(appFiles, routeEntrypoint).buildtime, - }); - } - ); - } - - let [fastboot, nonFastboot] = partition(excludeDotFiles(flatten(requiredAppFiles)), file => - appFiles.isFastbootOnly.get(file) - ); - let amdModules = nonFastboot.map(file => this.importPaths(appFiles, file)); - let fastbootOnlyAmdModules = fastboot.map(file => this.importPaths(appFiles, file)); - - let params = { - amdModules, - fastbootOnlyAmdModules, - lazyRoutes, - lazyEngines, - eagerModules, - styles, - // this is a backward-compatibility feature: addons can force inclusion of modules. - defineModulesFrom: './-embroider-implicit-modules.js', - }; - if (entryParams) { - Object.assign(params, entryParams); - } - - let source = entryTemplate(params); - - let asset: InternalAsset = { - kind: 'in-memory', - source, - relativePath, - }; - prepared.set(relativePath, asset); - return asset; + private addAppBoot(appBoot?: string) { + writeFileSync(join(locateEmbroiderWorkingDir(this.compatApp.root), 'ember-app-boot.js'), appBoot ?? ''); } private importPaths({ engine }: AppFiles, engineRelativePath: string) { @@ -1099,20 +844,6 @@ export class CompatAppBuilder { }; } - private routeEntrypoint(appFiles: AppFiles, relativePath: string, files: string[]) { - let [fastboot, nonFastboot] = partition(files, file => appFiles.isFastbootOnly.get(file)); - - let asset: InternalAsset = { - kind: 'in-memory', - source: routeEntryTemplate({ - files: nonFastboot.map(f => this.importPaths(appFiles, f)), - fastbootOnlyFiles: fastboot.map(f => this.importPaths(appFiles, f)), - }), - relativePath, - }; - return asset; - } - private testJSEntrypoint(appFiles: AppFiles[], prepared: Map): InternalAsset { let asset = prepared.get(`assets/test.js`); if (asset) { @@ -1126,16 +857,6 @@ export class CompatAppBuilder { const myName = 'assets/test.js'; - // tests necessarily also include the app. This is where we account for - // that. The classic solution was to always include the app's separate - // script tag in the tests HTML, but that isn't as easy for final stage - // packagers to understand. It's better to express it here as a direct - // module dependency. - let eagerModules: string[] = [ - 'ember-testing', - explicitRelative(dirname(myName), this.topAppJSAsset(appFiles, prepared).relativePath), - ]; - let amdModules: { runtime: string; buildtime: string }[] = []; for (let relativePath of engine.tests) { @@ -1144,7 +865,6 @@ export class CompatAppBuilder { let source = entryTemplate({ amdModules, - eagerModules, testSuffix: true, // this is a backward-compatibility feature: addons can force inclusion of test support modules. defineModulesFrom: './-embroider-implicit-test-modules.js', @@ -1194,9 +914,8 @@ let d = w.define; {{/if}} -{{#each eagerModules as |eagerModule| ~}} - i("{{js-string-escape eagerModule}}"); -{{/each}} +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}}");}); @@ -1277,25 +996,6 @@ if (!runningTests) { styles?: { path: string }[]; }) => string; -const routeEntryTemplate = jsHandlebarsCompile(` -import { importSync as i } from '@embroider/macros'; -let d = window.define; -{{#each files as |amdModule| ~}} -d("{{js-string-escape amdModule.runtime}}", function(){ return i("{{js-string-escape amdModule.buildtime}}");}); -{{/each}} -{{#if fastbootOnlyFiles}} - import { macroCondition, getGlobalConfig } from '@embroider/macros'; - if (macroCondition(getGlobalConfig().fastboot?.isRunning)) { - {{#each fastbootOnlyFiles as |amdModule| ~}} - d("{{js-string-escape amdModule.runtime}}", function(){ return i("{{js-string-escape amdModule.buildtime}}");}); - {{/each}} - } -{{/if}} -`) as (params: { - files: { runtime: string; buildtime: string }[]; - fastbootOnlyFiles: { runtime: string; buildtime: string }[]; -}) => string; - function stringOrBufferEqual(a: string | Buffer, b: string | Buffer): boolean { if (typeof a === 'string' && typeof b === 'string') { return a === b; @@ -1349,10 +1049,6 @@ function addCachablePlugin(babelConfig: TransformOptions) { } } -function excludeDotFiles(files: string[]) { - return files.filter(file => !file.startsWith('.') && !file.includes('/.')); -} - interface TreeNames { appJS: BroccoliNode; htmlTree: BroccoliNode; @@ -1360,7 +1056,7 @@ interface TreeNames { configTree: BroccoliNode; } -type InternalAsset = OnDiskAsset | InMemoryAsset | BuiltEmberAsset | ConcatenatedAsset; +type InternalAsset = OnDiskAsset | InMemoryAsset | BuiltEmberAsset; class ParsedEmberAsset { kind: 'parsed-ember' = 'parsed-ember'; @@ -1391,15 +1087,3 @@ class BuiltEmberAsset { this.relativePath = asset.relativePath; } } - -class ConcatenatedAsset { - kind: 'concatenated-asset' = 'concatenated-asset'; - constructor( - public relativePath: string, - public sources: (OnDiskAsset | InMemoryAsset)[], - private resolvableExtensions: RegExp - ) {} - get sourcemapPath() { - return this.relativePath.replace(this.resolvableExtensions, '') + '.map'; - } -} diff --git a/packages/compat/tests/audit.test.ts b/packages/compat/tests/audit.test.ts index e96d1ebb4..71fa88239 100644 --- a/packages/compat/tests/audit.test.ts +++ b/packages/compat/tests/audit.test.ts @@ -51,6 +51,8 @@ describe('audit', function () { }, ], resolvableExtensions, + autoRun: true, + staticAppPaths: [], }; let babel: TransformOptions = { diff --git a/packages/core/package.json b/packages/core/package.json index b7d23b375..3cef5649c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -24,6 +24,7 @@ "@babel/parser": "^7.14.5", "@babel/traverse": "^7.14.5", "@embroider/macros": "workspace:*", + "@embroider/reverse-exports": "workspace:*", "@embroider/shared-internals": "workspace:*", "assert-never": "^1.2.1", "babel-plugin-ember-template-compilation": "^2.1.1", @@ -32,6 +33,7 @@ "broccoli-plugin": "^4.0.7", "broccoli-source": "^3.0.1", "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", "fast-sourcemap-concat": "^1.4.0", "filesize": "^10.0.7", "fs-extra": "^9.1.0", @@ -43,7 +45,6 @@ "resolve": "^1.20.0", "resolve-package-path": "^4.0.1", "resolve.exports": "^2.0.2", - "@embroider/reverse-exports": "workspace:*", "typescript-memoize": "^1.0.1", "walk-sync": "^3.0.0" }, @@ -56,8 +57,8 @@ "@types/babel__traverse": "^7.18.5", "@types/debug": "^4.1.5", "@types/fs-extra": "^9.0.12", - "@types/jsdom": "^16.2.11", "@types/js-string-escape": "^1.0.0", + "@types/jsdom": "^16.2.11", "@types/lodash": "^4.14.170", "@types/node": "^15.12.2", "@types/resolve": "^1.20.0", diff --git a/packages/core/src/app-files.ts b/packages/core/src/app-files.ts index d3ec4fa6b..26322af6d 100644 --- a/packages/core/src/app-files.ts +++ b/packages/core/src/app-files.ts @@ -23,6 +23,7 @@ export class AppFiles { appFiles: Set, fastbootFiles: Set, resolvableExtensions: RegExp, + staticAppPathsPattern: RegExp | undefined, podModulePrefix?: string ) { let tests: string[] = []; @@ -114,7 +115,13 @@ export class AppFiles { continue; } - otherAppFiles.push(relativePath); + if (staticAppPathsPattern) { + if (!staticAppPathsPattern.test(relativePath)) { + otherAppFiles.push(relativePath); + } + } else { + otherAppFiles.push(relativePath); + } } this.tests = tests; this.components = components; diff --git a/packages/core/src/module-resolver.ts b/packages/core/src/module-resolver.ts index 86302b22b..ab34268c2 100644 --- a/packages/core/src/module-resolver.ts +++ b/packages/core/src/module-resolver.ts @@ -26,6 +26,7 @@ import { describeExports } from './describe-exports'; import { readFileSync } from 'fs'; import type UserOptions from './options'; import { nodeResolve } from './node-resolve'; +import { decodePublicRouteEntrypoint, encodeRouteEntrypoint } from './virtual-route-entrypoint'; const debug = makeDebug('embroider:resolver'); @@ -91,8 +92,11 @@ export interface Options { appRoot: string; engines: EngineConfig[]; modulePrefix: string; + splitAtRoutes?: (RegExp | string)[]; podModulePrefix?: string; amdCompatibility: Required; + autoRun: boolean; + staticAppPaths: string[]; } // TODO: once we can remove the stage2 entrypoint this type can get streamlined @@ -196,6 +200,8 @@ export class Resolver { request = this.handleImplicitTestScripts(request); request = this.handleVendorStyles(request); request = this.handleTestSupportStyles(request); + request = this.handleEntrypoint(request); + request = this.handleRouteEntrypoint(request); request = this.handleRenaming(request); request = this.handleVendor(request); // we expect the specifier to be app relative at this point - must be after handleRenaming @@ -425,6 +431,67 @@ export class Resolver { } } + private handleEntrypoint(request: R): R { + if (isTerminal(request)) { + return request; + } + // TODO: also handle targeting from the outside (for engines) like: + // request.specifier === 'my-package-name/-embroider-entrypoint.js' + // just like implicit-modules does. + + //TODO move the extra forwardslash handling out into the vite plugin + const candidates = ['@embroider/core/entrypoint', '/@embroider/core/entrypoint', './@embroider/core/entrypoint']; + + if (!candidates.some(c => request.specifier.startsWith(c + '/') || request.specifier === c)) { + return request; + } + + const result = /\.?\/?@embroider\/core\/entrypoint(?:\/(?.*))?/.exec(request.specifier); + + if (!result) { + // TODO make a better error + throw new Error('entrypoint does not match pattern' + request.specifier); + } + + const { packageName } = result.groups!; + + const requestingPkg = this.packageCache.ownerOfFile(request.fromFile); + + if (!requestingPkg?.isV2Ember()) { + throw new Error(`bug: found entrypoint import in non-ember package at ${request.fromFile}`); + } + + let pkg; + + if (packageName) { + pkg = this.packageCache.resolve(packageName, requestingPkg); + } else { + pkg = requestingPkg; + } + + return logTransition('entrypoint', request, request.virtualize(resolve(pkg.root, '-embroider-entrypoint.js'))); + } + + private handleRouteEntrypoint(request: R): R { + if (isTerminal(request)) { + return request; + } + + let routeName = decodePublicRouteEntrypoint(request.specifier); + + if (!routeName) { + return request; + } + + let pkg = this.packageCache.ownerOfFile(request.fromFile); + + if (!pkg?.isV2Ember()) { + 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))); + } + private handleImplicitTestScripts(request: R): R { //TODO move the extra forwardslash handling out into the vite plugin const candidates = [ diff --git a/packages/core/src/virtual-content.ts b/packages/core/src/virtual-content.ts index 08e910494..b46f64ab3 100644 --- a/packages/core/src/virtual-content.ts +++ b/packages/core/src/virtual-content.ts @@ -7,6 +7,9 @@ import { decodeTestSupportStyles, renderTestSupportStyles } from './virtual-test import { decodeVirtualVendor, renderVendor } from './virtual-vendor'; import { decodeVirtualVendorStyles, renderVendorStyles } from './virtual-vendor-styles'; +import { decodeEntrypoint, renderEntrypoint } from './virtual-entrypoint'; +import { decodeRouteEntrypoint, renderRouteEntrypoint } from './virtual-route-entrypoint'; + const externalESPrefix = '/@embroider/ext-es/'; const externalCJSPrefix = '/@embroider/ext-cjs/'; @@ -25,6 +28,16 @@ export function virtualContent(filename: string, resolver: Resolver): VirtualCon return renderCJSExternalShim(cjsExtern); } + let entrypoint = decodeEntrypoint(filename); + if (entrypoint) { + return renderEntrypoint(resolver, entrypoint); + } + + let routeEntrypoint = decodeRouteEntrypoint(filename); + if (routeEntrypoint) { + return renderRouteEntrypoint(resolver, routeEntrypoint); + } + let extern = decodeVirtualExternalESModule(filename); if (extern) { return renderESExternalShim(extern); diff --git a/packages/core/src/virtual-entrypoint.ts b/packages/core/src/virtual-entrypoint.ts new file mode 100644 index 000000000..7f1b0bed4 --- /dev/null +++ b/packages/core/src/virtual-entrypoint.ts @@ -0,0 +1,376 @@ +import { AppFiles, type RouteFiles } from './app-files'; +import { compile } from './js-handlebars'; +import type { Resolver } from './module-resolver'; +import type { CompatResolverOptions } from '../../compat/src/resolver-transform'; +import { flatten, partition } from 'lodash'; +import { join } from 'path'; +import { extensionsPattern, locateEmbroiderWorkingDir } from '@embroider/shared-internals'; +import walkSync from 'walk-sync'; +import type { V2AddonPackage } from '@embroider/shared-internals/src/package'; +import { encodePublicRouteEntrypoint } from './virtual-route-entrypoint'; +import { readFileSync } from 'fs-extra'; +import escapeRegExp from 'escape-string-regexp'; + +const entrypointPattern = /(?.*)[\\/]-embroider-entrypoint.js/; + +export function decodeEntrypoint(filename: string): { fromFile: string } | undefined { + // Performance: avoid paying regex exec cost unless needed + if (!filename.includes('-embroider-entrypoint')) { + return; + } + let m = entrypointPattern.exec(filename); + if (m) { + return { + fromFile: m.groups!.filename, + }; + } +} + +export function staticAppPathsPattern(staticAppPaths: string[] | undefined): RegExp | undefined { + if (staticAppPaths && staticAppPaths.length > 0) { + return new RegExp('^(?:' + staticAppPaths.map(staticAppPath => escapeRegExp(staticAppPath)).join('|') + ')(?:$|/)'); + } +} + +export function renderEntrypoint( + resolver: Resolver, + { fromFile }: { fromFile: string } +): { src: string; watches: string[] } { + // this is new + const owner = resolver.packageCache.ownerOfFile(fromFile); + + let eagerModules: string[] = []; + + if (!owner) { + throw new Error('Owner expected'); // ToDo: Really bad error, update message + } + + let engine = resolver.owningEngine(owner); + let isApp = owner?.root === resolver.options.engines[0]!.root; + let hasFastboot = Boolean(resolver.options.engines[0]!.activeAddons.find(a => a.name === 'ember-cli-fastboot')); + + let appFiles = new AppFiles( + { + package: owner, + addons: new Map( + engine.activeAddons.map(addon => [ + resolver.packageCache.get(addon.root) as V2AddonPackage, + addon.canResolveFromFile, + ]) + ), + isApp, + modulePrefix: isApp ? resolver.options.modulePrefix : engine.packageName, + appRelativePath: 'NOT_USED_DELETE_ME', + }, + getAppFiles(owner.root), + hasFastboot ? getFastbootFiles(owner.root) : new Set(), + extensionsPattern(resolver.options.resolvableExtensions), + staticAppPathsPattern(resolver.options.staticAppPaths), + resolver.options.podModulePrefix + ); + + let options = (resolver.options as CompatResolverOptions).options; + + let requiredAppFiles = [appFiles.otherAppFiles]; + if (!options.staticComponents) { + requiredAppFiles.push(appFiles.components); + } + if (!options.staticHelpers) { + requiredAppFiles.push(appFiles.helpers); + } + if (!options.staticModifiers) { + requiredAppFiles.push(appFiles.modifiers); + } + + let styles = []; + // only import styles from engines with a parent (this excludeds the parent application) as their styles + // will be inserted via a direct tag. + if (!appFiles.engine.isApp && appFiles.engine.package.isLazyEngine()) { + styles.push({ + path: '@embroider/core/vendor.css', + }); + } + + let lazyEngines: { names: string[]; path: string }[] = []; + + if (isApp) { + // deliberately ignoring the app (which is the first entry in the engines array) + let [, ...childEngines] = resolver.options.engines; + for (let childEngine of childEngines) { + let target = `@embroider/core/entrypoint/${childEngine.packageName}`; + + if (childEngine.isLazy) { + lazyEngines.push({ + names: [childEngine.packageName], + path: target, + }); + } else { + eagerModules.push(target); + } + } + } + + let lazyRoutes: { names: string[]; path: string }[] = []; + for (let [routeName, routeFiles] of appFiles.routeFiles.children) { + splitRoute( + routeName, + routeFiles, + resolver.options.splitAtRoutes, + (_: string, filename: string) => { + requiredAppFiles.push([filename]); + }, + (routeNames: string[], _files: string[]) => { + lazyRoutes.push({ + names: routeNames, + path: encodePublicRouteEntrypoint(routeNames, _files), + }); + } + ); + } + + let [fastboot, nonFastboot] = partition(excludeDotFiles(flatten(requiredAppFiles)), file => + appFiles.isFastbootOnly.get(file) + ); + + let amdModules = nonFastboot.map(file => importPaths(resolver, appFiles, file)); + let fastbootOnlyAmdModules = fastboot.map(file => importPaths(resolver, appFiles, file)); + + let params = { + amdModules, + fastbootOnlyAmdModules, + lazyRoutes, + lazyEngines, + eagerModules, + styles, + // this is a backward-compatibility feature: addons can force inclusion of modules. + defineModulesFrom: './-embroider-implicit-modules.js', + }; + + // for the top-level entry template we need to pass extra params to the template + // this is new, it used to be passed into the appJS function instead + if (isApp) { + const appBoot = readFileSync(join(locateEmbroiderWorkingDir(resolver.options.appRoot), 'ember-app-boot.js'), { + encoding: 'utf-8', + }); + + Object.assign(params, { + autoRun: resolver.options.autoRun, + appBoot, + mainModule: './app', + }); + } + + return { + src: entryTemplate(params), + watches: [], + }; +} + +const entryTemplate = compile(` +import { importSync as i, macroCondition, getGlobalConfig } from '@embroider/macros'; +let w = window; +let d = w.define; + +import environment from './config/environment'; + +{{#if styles}} + if (macroCondition(!getGlobalConfig().fastboot?.isRunning)) { + {{#each styles as |stylePath| ~}} + await import("{{js-string-escape stylePath.path}}"); + {{/each}} + } +{{/if}} + +{{#if defineModulesFrom ~}} + import implicitModules from "{{js-string-escape defineModulesFrom}}"; + + for(const [name, module] of Object.entries(implicitModules)) { + d(name, function() { return module }); + } +{{/if}} + + +{{#each eagerModules as |eagerModule| ~}} + import "{{js-string-escape eagerModule}}"; +{{/each}} + +{{#each amdModules as |amdModule index| ~}} + import * as amdModule{{index}} from "{{js-string-escape amdModule.buildtime}}" + d("{{js-string-escape amdModule.runtime}}", function(){ return amdModule{{index}}; }); +{{/each}} + +{{#if fastbootOnlyAmdModules}} + if (macroCondition(getGlobalConfig().fastboot?.isRunning)) { + let fastbootModules = {}; + + {{#each fastbootOnlyAmdModules as |amdModule| ~}} + fastbootModules["{{js-string-escape amdModule.runtime}}"] = import("{{js-string-escape amdModule.buildtime}}"); + {{/each}} + + const resolvedValues = await Promise.all(Object.values(fastbootModules)); + + Object.keys(fastbootModules).forEach((k, i) => { + d(k, function(){ return resolvedValues[i];}); + }) + } +{{/if}} + + +{{#if lazyRoutes}} +w._embroiderRouteBundles_ = [ + {{#each lazyRoutes as |route|}} + { + names: {{json-stringify route.names}}, + load: function() { + return import("{{js-string-escape route.path}}"); + } + }, + {{/each}} +] +{{/if}} + +{{#if lazyEngines}} +w._embroiderEngineBundles_ = [ + {{#each lazyEngines as |engine|}} + { + names: {{json-stringify engine.names}}, + load: function() { + return import("{{js-string-escape engine.path}}"); + } + }, + {{/each}} +] +{{/if}} + +{{#if autoRun ~}} +if (!runningTests) { + i("{{js-string-escape mainModule}}").default.create(environment.APP); +} +{{else if appBoot ~}} + {{ appBoot }} +{{/if}} + +{{#if testSuffix ~}} + {{!- TODO: both of these suffixes should get dynamically generated so they incorporate + any content-for added by addons. -}} + + + {{!- this is the traditional tests-suffix.js -}} + i('../tests/test-helper'); + EmberENV.TESTS_FILE_LOADED = true; +{{/if}} +`) as (params: { + amdModules: { runtime: string; buildtime: string }[]; + fastbootOnlyAmdModules?: { runtime: string; buildtime: string }[]; + defineModulesFrom?: string; + eagerModules?: string[]; + autoRun?: boolean; + appBoot?: string; + mainModule?: string; + testSuffix?: boolean; + lazyRoutes?: { names: string[]; path: string }[]; + lazyEngines?: { names: string[]; path: string }[]; + styles?: { path: string }[]; +}) => string; + +function excludeDotFiles(files: string[]) { + return files.filter(file => !file.startsWith('.') && !file.includes('/.')); +} + +export function importPaths(resolver: Resolver, { engine }: AppFiles, engineRelativePath: string) { + let resolvableExtensionsPattern = extensionsPattern(resolver.options.resolvableExtensions); + let noHBS = engineRelativePath.replace(resolvableExtensionsPattern, '').replace(/\.hbs$/, ''); + return { + runtime: `${engine.modulePrefix}/${noHBS}`, + buildtime: `./${engineRelativePath}`, + }; +} + +export function splitRoute( + routeName: string, + files: RouteFiles, + splitAtRoutes: (RegExp | string)[] | undefined, + addToParent: (routeName: string, filename: string) => void, + addLazyBundle: (routeNames: string[], files: string[]) => void +) { + let shouldSplit = routeName && shouldSplitRoute(routeName, splitAtRoutes); + let ownFiles = []; + let ownNames = new Set() as Set; + + if (files.template) { + if (shouldSplit) { + ownFiles.push(files.template); + ownNames.add(routeName); + } else { + addToParent(routeName, files.template); + } + } + + if (files.controller) { + if (shouldSplit) { + ownFiles.push(files.controller); + ownNames.add(routeName); + } else { + addToParent(routeName, files.controller); + } + } + + if (files.route) { + if (shouldSplit) { + ownFiles.push(files.route); + ownNames.add(routeName); + } else { + addToParent(routeName, files.route); + } + } + + for (let [childName, childFiles] of files.children) { + splitRoute( + `${routeName}.${childName}`, + childFiles, + splitAtRoutes, + (childRouteName: string, childFile: string) => { + // this is our child calling "addToParent" + if (shouldSplit) { + ownFiles.push(childFile); + ownNames.add(childRouteName); + } else { + addToParent(childRouteName, childFile); + } + }, + (routeNames: string[], files: string[]) => { + addLazyBundle(routeNames, files); + } + ); + } + + if (ownFiles.length > 0) { + addLazyBundle([...ownNames], ownFiles); + } +} + +function shouldSplitRoute(routeName: string, splitAtRoutes: (RegExp | string)[] | undefined) { + if (!splitAtRoutes) { + return false; + } + return splitAtRoutes.find(pattern => { + if (typeof pattern === 'string') { + return pattern === routeName; + } else { + return pattern.test(routeName); + } + }); +} + +export function getAppFiles(appRoot: string): Set { + const files: string[] = walkSync(appRoot, { + ignore: ['_babel_config_.js', '_babel_filter_.js', 'app.js', 'assets', 'testem.js', 'node_modules'], + }); + return new Set(files); +} + +export function getFastbootFiles(appRoot: string): Set { + const appDirPath = join(appRoot, '_fastboot_'); + const files: string[] = walkSync(appDirPath); + return new Set(files); +} diff --git a/packages/core/src/virtual-route-entrypoint.ts b/packages/core/src/virtual-route-entrypoint.ts new file mode 100644 index 000000000..82f589fe6 --- /dev/null +++ b/packages/core/src/virtual-route-entrypoint.ts @@ -0,0 +1,132 @@ +import type { V2AddonPackage } from '@embroider/shared-internals/src/package'; +import { AppFiles } from './app-files'; +import type { Resolver } from './module-resolver'; +import { resolve } from 'path'; +import { compile } from './js-handlebars'; +import { extensionsPattern } from '@embroider/shared-internals'; +import { partition } from 'lodash'; +import { getAppFiles, getFastbootFiles, importPaths, splitRoute, staticAppPathsPattern } from './virtual-entrypoint'; + +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 decodeRouteEntrypoint(filename: string): { fromFile: string; route: string } | undefined { + // Performance: avoid paying regex exec cost unless needed + if (!filename.includes('-embroider-route-entrypoint')) { + return; + } + let m = entrypointPattern.exec(filename); + if (m) { + return { + fromFile: m.groups!.filename, + route: m.groups!.route, + }; + } +} + +export function encodePublicRouteEntrypoint(routeNames: string[], _files: string[]) { + return `@embroider/core/route/${encodeURIComponent(routeNames[0])}`; +} + +export function decodePublicRouteEntrypoint(specifier: string): string | null { + const publicPrefix = '@embroider/core/route/'; + if (!specifier.startsWith(publicPrefix)) { + return null; + } + + return specifier.slice(publicPrefix.length); +} + +export function renderRouteEntrypoint( + resolver: Resolver, + { fromFile, route }: { fromFile: string; route: string } +): { src: string; watches: string[] } { + const owner = resolver.packageCache.ownerOfFile(fromFile); + + if (!owner) { + throw new Error('Owner expected'); // ToDo: Really bad error, update message + } + + let engine = resolver.owningEngine(owner); + let isApp = owner?.root === resolver.options.engines[0]!.root; + let hasFastboot = Boolean(resolver.options.engines[0]!.activeAddons.find(a => a.name === 'ember-cli-fastboot')); + + let appFiles = new AppFiles( + { + package: owner, + addons: new Map( + engine.activeAddons.map(addon => [ + resolver.packageCache.get(addon.root) as V2AddonPackage, + addon.canResolveFromFile, + ]) + ), + isApp, + modulePrefix: isApp ? resolver.options.modulePrefix : engine.packageName, + appRelativePath: 'NOT_USED_DELETE_ME', + }, + getAppFiles(owner.root), + hasFastboot ? getFastbootFiles(owner.root) : new Set(), + extensionsPattern(resolver.options.resolvableExtensions), + staticAppPathsPattern(resolver.options.staticAppPaths), + resolver.options.podModulePrefix + ); + + let src = ''; + + for (let [routeName, routeFiles] of appFiles.routeFiles.children) { + splitRoute( + routeName, + routeFiles, + resolver.options.splitAtRoutes, + (_: string, _filename: string) => { + // noop + }, + (routeNames: string[], routeFiles: string[]) => { + if (routeNames[0] === route) { + let [fastboot, nonFastboot] = partition(routeFiles, file => appFiles.isFastbootOnly.get(file)); + + const amdModules = nonFastboot.map(f => importPaths(resolver, appFiles, f)); + const fastbootOnlyAmdModules = fastboot.map(f => importPaths(resolver, appFiles, f)); + + src = routeEntryTemplate({ + amdModules, + fastbootOnlyAmdModules, + }); + } + } + ); + } + + return { src, watches: [] }; +} + +const routeEntryTemplate = compile(` +let d = window.define; + +{{#each amdModules as |amdModule index| ~}} + import * as amdModule{{index}} from "{{js-string-escape amdModule.buildtime}}" + d("{{js-string-escape amdModule.runtime}}", function(){ return amdModule{{index}}; }); +{{/each}} + +{{#if fastbootOnlyAmdModules}} + if (macroCondition(getGlobalConfig().fastboot?.isRunning)) { + let fastbootModules = {}; + + {{#each fastbootOnlyAmdModules as |amdModule| ~}} + fastbootModules["{{js-string-escape amdModule.runtime}}"] = import("{{js-string-escape amdModule.buildtime}}"); + {{/each}} + + const resolvedValues = await Promise.all(Object.values(fastbootModules)); + + Object.keys(fastbootModules).forEach((k, i) => { + d(k, function(){ return resolvedValues[i];}); + }) + } +{{/if}} +`) as (params: { + amdModules: { runtime: string; buildtime: string }[]; + fastbootOnlyAmdModules: { runtime: string; buildtime: string }[]; +}) => string; diff --git a/packages/shared-internals/src/package-cache.ts b/packages/shared-internals/src/package-cache.ts index 7337bfe8d..9ec2edff8 100644 --- a/packages/shared-internals/src/package-cache.ts +++ b/packages/shared-internals/src/package-cache.ts @@ -66,7 +66,14 @@ export default class PackageCache { return p; } - ownerOfFile(filename: string): Package | undefined { + ownerOfFile(filenameInput: string): Package | undefined { + let filename = filenameInput; + const virtualPrefix = 'embroider_virtual:'; + + if (filename.includes(virtualPrefix)) { + filename = filename.replace(/^.*embroider_virtual:/, ''); + } + let segments = filename.replace(/\\/g, '/').split(posix.sep); // first we look through our cached packages for any that are rooted right diff --git a/patches/ember-source@5.8.0.patch b/patches/ember-source@5.8.0.patch new file mode 100644 index 000000000..6beb6acc3 --- /dev/null +++ b/patches/ember-source@5.8.0.patch @@ -0,0 +1,55 @@ +diff --git a/dist/packages/@ember/debug/index.js b/dist/packages/@ember/debug/index.js +index 27eb2872d475cbff7f80d24c7404627d072bada9..123147f265134c50e32e381b0f976a7f93697485 100644 +--- a/dist/packages/@ember/debug/index.js ++++ b/dist/packages/@ember/debug/index.js +@@ -1,6 +1,6 @@ + import { isChrome, isFirefox } from '@ember/-internals/browser-environment'; + import { DEBUG } from '@glimmer/env'; +-import _deprecate from './lib/deprecate'; ++import defaultDeprecate from './lib/deprecate'; + import { isTesting } from './lib/testing'; + import _warn from './lib/warn'; + export { registerHandler as registerWarnHandler } from './lib/warn'; +@@ -12,11 +12,11 @@ export { default as captureRenderTree } from './lib/capture-render-tree'; + const noop = () => {}; + // SAFETY: these casts are just straight-up lies, but the point is that they do + // not do anything in production builds. + let assert = noop; + let info = noop; + let warn = noop; + let debug = noop; +-let deprecate = noop; ++let currentDeprecate; + let debugSeal = noop; + let debugFreeze = noop; + let runInDebug = noop; +@@ -25,6 +25,12 @@ let getDebugFunction = noop; + let deprecateFunc = function () { + return arguments[arguments.length - 1]; + }; ++ ++function deprecate(...args) { ++ return (currentDeprecate ?? defaultDeprecate)(...args) ++} ++ ++ + if (DEBUG) { + setDebugFunction = function (type, callback) { + switch (type) { +@@ -37,7 +43,7 @@ if (DEBUG) { + case 'debug': + return debug = callback; + case 'deprecate': +- return deprecate = callback; ++ return currentDeprecate = callback; + case 'debugSeal': + return debugSeal = callback; + case 'debugFreeze': +@@ -190,7 +189,6 @@ if (DEBUG) { + Object.freeze(obj); + } + }); +- setDebugFunction('deprecate', _deprecate); + setDebugFunction('warn', _warn); + } + let _warnIfUsingStrippedFeatureFlags; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a06dc8a82..086fe82a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,6 +10,11 @@ overrides: '@types/eslint': ^8.37.0 babel-plugin-module-resolver@5.0.1: 5.0.0 +patchedDependencies: + ember-source@5.8.0: + hash: qsivfx5huurlb5tuvochap65l4 + path: patches/ember-source@5.8.0.patch + importers: .: @@ -402,6 +407,9 @@ importers: debug: specifier: ^4.3.2 version: 4.3.4(supports-color@9.4.0) + escape-string-regexp: + specifier: ^4.0.0 + version: 4.0.0 fast-sourcemap-concat: specifier: ^1.4.0 version: 1.4.0 @@ -638,7 +646,7 @@ importers: version: 7.6.0 ember-source: specifier: ^5.8.0 - version: 5.8.0(@babel/core@7.24.5) + version: 5.8.0(patch_hash=qsivfx5huurlb5tuvochap65l4)(@babel/core@7.24.5) ember-template-lint: specifier: ^4.0.0 version: 4.18.2 @@ -1782,6 +1790,9 @@ importers: ember-source-4.4: specifier: npm:ember-source@~4.4.0 version: /ember-source@4.4.5(@babel/core@7.24.5)(webpack@5.91.0) + ember-source-5.8: + specifier: npm:ember-source@~5.8.0 + version: /ember-source@5.8.0(patch_hash=qsivfx5huurlb5tuvochap65l4)(@babel/core@7.24.5)(webpack@5.91.0) ember-source-beta: specifier: npm:ember-source@beta version: /ember-source@5.9.0-beta.2(@babel/core@7.24.5)(webpack@5.91.0) @@ -1790,7 +1801,7 @@ importers: version: '@s3.amazonaws.com/builds.emberjs.com/canary/shas/370cf34f9e86df17b880f11fef35a5a0f24ff38a.tgz(@babel/core@7.24.5)(webpack@5.91.0)' ember-source-latest: specifier: npm:ember-source@latest - version: /ember-source@5.8.0(@babel/core@7.24.5)(webpack@5.91.0) + version: /ember-source@5.8.0(patch_hash=qsivfx5huurlb5tuvochap65l4)(@babel/core@7.24.5)(webpack@5.91.0) ember-truth-helpers: specifier: ^3.0.0 version: 3.1.1 @@ -8330,9 +8341,6 @@ packages: /ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependenciesMeta: - ajv: - optional: true dependencies: ajv: 8.13.0 @@ -14601,7 +14609,7 @@ packages: - webpack dev: true - /ember-source@5.8.0(@babel/core@7.24.5): + /ember-source@5.8.0(patch_hash=qsivfx5huurlb5tuvochap65l4)(@babel/core@7.24.5): resolution: {integrity: sha512-jRmT5egy7XG2G9pKNdNNwNBZqFxrl7xJwdYrJ3ugreR7zK1FR28lHSR5CMSKtYLmJZxu340cf2EbRohWEtO2Zw==} engines: {node: '>= 16.*'} dependencies: @@ -14659,8 +14667,9 @@ packages: - supports-color - webpack dev: true + patched: true - /ember-source@5.8.0(@babel/core@7.24.5)(webpack@5.91.0): + /ember-source@5.8.0(patch_hash=qsivfx5huurlb5tuvochap65l4)(@babel/core@7.24.5)(webpack@5.91.0): resolution: {integrity: sha512-jRmT5egy7XG2G9pKNdNNwNBZqFxrl7xJwdYrJ3ugreR7zK1FR28lHSR5CMSKtYLmJZxu340cf2EbRohWEtO2Zw==} engines: {node: '>= 16.*'} dependencies: @@ -14718,6 +14727,7 @@ packages: - supports-color - webpack dev: true + patched: true /ember-source@5.9.0-beta.2(@babel/core@7.24.5)(webpack@5.91.0): resolution: {integrity: sha512-3476KQBR7zlq9ITbgeG+P7oYpmtpMF6+/aKH+Sx7iLKjAlw3QRR2UEOboH0joAlbU+1/cLtDdK0txnefK43ViQ==} diff --git a/tests/fixtures/engines-host-app/tests/acceptance/basics-test.js b/tests/fixtures/engines-host-app/tests/acceptance/basics-test.js index 2c6177cbf..13f835ea1 100644 --- a/tests/fixtures/engines-host-app/tests/acceptance/basics-test.js +++ b/tests/fixtures/engines-host-app/tests/acceptance/basics-test.js @@ -56,15 +56,14 @@ function createLazyEngineTest(type) { ); } - // TODO: uncomment once we fix this appearing too eagerly - //assert.notOk(!!window.require.entries['lazy-engine/helpers/duplicated-helper']); + assert.notOk(!!window.require.entries['lazy-engine/helpers/duplicated-helper']); await visit('/use-lazy-engine'); let entriesAfter = Object.entries(window.require.entries).length; if (type === 'safe') { - assert.ok(!!window.require.entries['lazy-engine/helpers/duplicated-helper']); + assert.ok(!!window.require.entries['lazy-engine/helpers/duplicated-helper'], 'in safe mode we expect to see lazy-engine/helpers/duplicated-helper but its not there'); } else { - assert.notOk(!!window.require.entries['lazy-engine/helpers/duplicated-helper']); + assert.notOk(!!window.require.entries['lazy-engine/helpers/duplicated-helper'], 'in optimized mode we expect to *not* see lazy-engine/helpers/duplicated-helper but it is there'); } assert.ok(entriesAfter > entriesBefore); assert.equal(currentURL(), '/use-lazy-engine'); diff --git a/tests/scenarios/compat-exclude-dot-files-test.ts b/tests/scenarios/compat-exclude-dot-files-test.ts index db48e4121..5b0b2ec40 100644 --- a/tests/scenarios/compat-exclude-dot-files-test.ts +++ b/tests/scenarios/compat-exclude-dot-files-test.ts @@ -75,7 +75,7 @@ appScenarios // but not be picked up in the entrypoint expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/app-template.js') + .resolves('/@embroider/core/entrypoint') .toModule() .withContents(content => { assert.notOk(/app-template\/\.foobar/.test(content), '.foobar is not in the entrypoint'); diff --git a/tests/scenarios/compat-renaming-test.ts b/tests/scenarios/compat-renaming-test.ts index 41945d3f9..dd71ba944 100644 --- a/tests/scenarios/compat-renaming-test.ts +++ b/tests/scenarios/compat-renaming-test.ts @@ -256,7 +256,7 @@ appScenarios test('renamed modules keep their classic runtime name when used as implicit-modules', function () { expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/app-template.js') + .resolves('/@embroider/core/entrypoint') .toModule() .resolves('./-embroider-implicit-modules.js') .toModule() diff --git a/tests/scenarios/compat-resolver-test.ts b/tests/scenarios/compat-resolver-test.ts index 1141e72b7..724a33193 100644 --- a/tests/scenarios/compat-resolver-test.ts +++ b/tests/scenarios/compat-resolver-test.ts @@ -100,6 +100,8 @@ Scenarios.fromProject(() => new Project()) ...extraOpts?.appPackageRules, }, ], + autoRun: true, + staticAppPaths: [], }; givenFiles({ diff --git a/tests/scenarios/compat-route-split-test.ts b/tests/scenarios/compat-route-split-test.ts index 8932f335f..c5e590222 100644 --- a/tests/scenarios/compat-route-split-test.ts +++ b/tests/scenarios/compat-route-split-test.ts @@ -1,9 +1,5 @@ import type { PreparedApp } from 'scenario-tester'; import { appScenarios, renameApp } from './scenarios'; -import { readFileSync } from 'fs'; -import { join } from 'path'; -import type { ExpectFile } from '@embroider/test-support/file-assertions/qunit'; -import { expectFilesAt } from '@embroider/test-support/file-assertions/qunit'; import { throwOnWarnings } from '@embroider/core'; import merge from 'lodash/merge'; import { setupAuditTest } from '@embroider/test-support/audit-assertions'; @@ -44,11 +40,87 @@ let splitScenarios = appScenarios.map('compat-splitAtRoutes', app => { modifiers: { 'auto-focus.js': 'export default function(){}', }, + 'router.js': `import EmberRouter from '@embroider/router'; + import config from 'my-app/config/environment'; + + export default class Router extends EmberRouter { + location = config.locationType; + rootURL = config.rootURL; + } + + Router.map(function () { + this.route('people'); + }); + `, }, }); app.linkDependency('@ember/string', { baseDir: __dirname }); }); +function checkContents( + expectAudit: ReturnType, + fn: (contents: string) => void, + entrypointFile?: string +) { + let resolved = expectAudit + .module('./node_modules/.embroider/rewritten-app/index.html') + .resolves('/@embroider/core/entrypoint'); + + if (entrypointFile) { + resolved = resolved.toModule().resolves(entrypointFile); + } + resolved.toModule().withContents(contents => { + fn(contents); + return true; + }); +} + +function notInEntrypointFunction(expectAudit: ReturnType) { + return function (text: string[] | string, entrypointFile?: string) { + checkContents( + expectAudit, + contents => { + if (Array.isArray(text)) { + text.forEach(t => { + if (contents.includes(t)) { + throw new Error(`${t} should not be found in entrypoint`); + } + }); + } else { + if (contents.includes(text)) { + throw new Error(`${text} should not be found in entrypoint`); + } + } + return true; + }, + entrypointFile + ); + }; +} + +function inEntrypointFunction(expectAudit: ReturnType) { + return function (text: string[] | string, entrypointFile?: string) { + checkContents( + expectAudit, + contents => { + if (Array.isArray(text)) { + text.forEach(t => { + if (!contents.includes(t)) { + throw new Error(`${t} should be found in entrypoint`); + } + }); + } else { + if (!contents.includes(text)) { + console.log(contents); + throw new Error(`${text} should be found in entrypoint`); + } + } + }, + entrypointFile + ); + }; +} + splitScenarios .map('basic', app => { merge(app.files, { @@ -63,17 +135,17 @@ splitScenarios }, }, controllers: { - 'index.js': '', - 'people.js': '', + 'index.js': `import Controller from '@ember/controller'; export default class Thingy extends Controller {}`, + 'people.js': `import Controller from '@ember/controller'; export default class Thingy extends Controller {}`, people: { - 'show.js': '', + 'show.js': `import Controller from '@ember/controller'; export default class Thingy extends Controller {}`, }, }, routes: { - 'index.js': '', - 'people.js': '', + 'index.js': `import Route from '@ember/routing/route';export default class ThingyRoute extends Route {}`, + 'people.js': `import Route from '@ember/routing/route';export default class ThingyRoute extends Route {}`, people: { - 'show.js': '', + 'show.js': `import Route from '@ember/routing/route';export default class ThingyRoute extends Route {}`, }, }, }, @@ -84,7 +156,6 @@ splitScenarios throwOnWarnings(hooks); let app: PreparedApp; - let expectFile: ExpectFile; hooks.before(async assert => { app = await scenario.prepare(); @@ -92,84 +163,75 @@ splitScenarios assert.equal(result.exitCode, 0, result.output); }); - hooks.beforeEach(assert => { - expectFile = expectFilesAt(readFileSync(join(app.dir, 'dist/.stage2-output'), 'utf8'), { qunit: assert }); - }); + let expectAudit = setupAuditTest(hooks, () => ({ app: app.dir })); + let notInEntrypoint = notInEntrypointFunction(expectAudit); + let inEntrypoint = inEntrypointFunction(expectAudit); test('has no components in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('all-people'); - expectFile('./assets/my-app.js').doesNotMatch('welcome'); - expectFile('./assets/my-app.js').doesNotMatch('unused'); + notInEntrypoint(['all-people', 'welcome', 'unused']); }); test('has no helpers in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('capitalize'); + notInEntrypoint('capitalize'); }); test('has no modifiers in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('auto-focus'); + notInEntrypoint('auto-focus'); }); test('has non-split controllers in main entrypoint', function () { - expectFile('./assets/my-app.js').matches('controllers/index'); + inEntrypoint('controllers/index'); }); test('has non-split route templates in main entrypoint', function () { - expectFile('./assets/my-app.js').matches('templates/index'); + inEntrypoint('templates/index'); }); test('has non-split routes in main entrypoint', function () { - expectFile('./assets/my-app.js').matches('routes/index'); + inEntrypoint('routes/index'); }); test('does not have split controllers in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('controllers/people'); - expectFile('./assets/my-app.js').doesNotMatch('controllers/people/show'); + notInEntrypoint(['controllers/people', 'controllers/people/show']); }); test('does not have split route templates in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('templates/people'); - expectFile('./assets/my-app.js').doesNotMatch('templates/people/index'); - expectFile('./assets/my-app.js').doesNotMatch('templates/people/show'); + notInEntrypoint(['templates/people', 'templates/people/index', 'templates/people/show']); }); test('does not have split routes in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('routes/people'); - expectFile('./assets/my-app.js').doesNotMatch('routes/people/show'); + notInEntrypoint(['routes/people', 'routes/people/show']); }); test('dynamically imports the route entrypoint from the main entrypoint', function () { - expectFile('./assets/my-app.js').matches('import("my-app/assets/_route_/people.js")'); + inEntrypoint('import("@embroider/core/route/people");'); }); test('has split controllers in route entrypoint', function () { - expectFile('./assets/_route_/people.js').matches('controllers/people'); - expectFile('./assets/_route_/people.js').matches('controllers/people/show'); + inEntrypoint(['controllers/people', 'controllers/people/show'], '@embroider/core/route/people'); }); test('has split route templates in route entrypoint', function () { - expectFile('./assets/_route_/people.js').matches('templates/people'); - expectFile('./assets/_route_/people.js').matches('templates/people/index'); - expectFile('./assets/_route_/people.js').matches('templates/people/show'); + inEntrypoint( + ['templates/people', 'templates/people/index', 'templates/people/show'], + '@embroider/core/route/people' + ); }); test('has split routes in route entrypoint', function () { - expectFile('./assets/_route_/people.js').matches('routes/people'); - expectFile('./assets/_route_/people.js').matches('routes/people/show'); + inEntrypoint(['routes/people', 'routes/people/show'], '@embroider/core/route/people'); }); test('has no components in route entrypoint', function () { - expectFile('./assets/_route_/people.js').doesNotMatch('all-people'); - expectFile('./assets/_route_/people.js').doesNotMatch('welcome'); - expectFile('./assets/_route_/people.js').doesNotMatch('unused'); + notInEntrypoint(['all-people', 'welcome', 'unused'], '@embroider/core/route/people'); }); test('has no helpers in route entrypoint', function () { - expectFile('./assets/_route_/people.js').doesNotMatch('capitalize'); + notInEntrypoint('capitalize', '@embroider/core/route/people'); }); test('has no helpers in route entrypoint', function () { - expectFile('./assets/_route_/people.js').doesNotMatch('auto-focus'); + notInEntrypoint('auto-focus', '@embroider/core/route/people'); }); Qmodule('audit', function (hooks) { @@ -256,7 +318,6 @@ splitScenarios throwOnWarnings(hooks); let app: PreparedApp; - let expectFile: ExpectFile; hooks.before(async assert => { app = await scenario.prepare(); @@ -264,84 +325,75 @@ splitScenarios assert.equal(result.exitCode, 0, result.output); }); - hooks.beforeEach(assert => { - expectFile = expectFilesAt(readFileSync(join(app.dir, 'dist/.stage2-output'), 'utf8'), { qunit: assert }); - }); + let expectAudit = setupAuditTest(hooks, () => ({ app: app.dir })); + let notInEntrypoint = notInEntrypointFunction(expectAudit); + let inEntrypoint = inEntrypointFunction(expectAudit); test('has no components in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('all-people'); - expectFile('./assets/my-app.js').doesNotMatch('welcome'); - expectFile('./assets/my-app.js').doesNotMatch('unused'); + notInEntrypoint(['all-people', 'welcome', 'unused']); }); test('has no helpers in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('capitalize'); + notInEntrypoint('capitalize'); }); test('has no modifiers in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('auto-focus'); + notInEntrypoint('auto-focus'); }); test('has non-split controllers in main entrypoint', function () { - expectFile('./assets/my-app.js').matches('pods/index/controller'); + inEntrypoint('pods/index/controller'); }); test('has non-split route templates in main entrypoint', function () { - expectFile('./assets/my-app.js').matches('pods/index/template'); + inEntrypoint('pods/index/template'); }); test('has non-split routes in main entrypoint', function () { - expectFile('./assets/my-app.js').matches('pods/index/route'); + inEntrypoint('pods/index/route'); }); test('does not have split controllers in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('pods/people/controller'); - expectFile('./assets/my-app.js').doesNotMatch('pods/people/show/controller'); + notInEntrypoint(['pods/people/controller', 'pods/people/show/controller']); }); test('does not have split route templates in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('pods/people/template'); - expectFile('./assets/my-app.js').doesNotMatch('pods/people/index/template'); - expectFile('./assets/my-app.js').doesNotMatch('pods/people/show/template'); + notInEntrypoint(['pods/people/template', 'pods/people/index/template', 'pods/people/show/template']); }); test('does not have split routes in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('pods/people/route'); - expectFile('./assets/my-app.js').doesNotMatch('pods/people/show/route'); + notInEntrypoint(['pods/people/route', 'pods/people/show/route']); }); test('dynamically imports the route entrypoint from the main entrypoint', function () { - expectFile('./assets/my-app.js').matches('import("my-app/assets/_route_/people.js")'); + inEntrypoint('import("@embroider/core/route/people")'); }); test('has split controllers in route entrypoint', function () { - expectFile('./assets/_route_/people.js').matches('pods/people/controller'); - expectFile('./assets/_route_/people.js').matches('pods/people/show/controller'); + inEntrypoint(['pods/people/controller', 'pods/people/show/controller'], '@embroider/core/route/people'); }); test('has split route templates in route entrypoint', function () { - expectFile('./assets/_route_/people.js').matches('pods/people/template'); - expectFile('./assets/_route_/people.js').matches('pods/people/index/template'); - expectFile('./assets/_route_/people.js').matches('pods/people/show/template'); + inEntrypoint( + ['pods/people/template', 'pods/people/index/template', 'pods/people/show/template'], + '@embroider/core/route/people' + ); }); test('has split routes in route entrypoint', function () { - expectFile('./assets/_route_/people.js').matches('pods/people/route'); - expectFile('./assets/_route_/people.js').matches('pods/people/show/route'); + inEntrypoint(['pods/people/route', 'pods/people/show/route'], '@embroider/core/route/people'); }); test('has no components in route entrypoint', function () { - expectFile('./assets/_route_/people.js').doesNotMatch('all-people'); - expectFile('./assets/_route_/people.js').doesNotMatch('welcome'); - expectFile('./assets/_route_/people.js').doesNotMatch('unused'); + notInEntrypoint(['all-people', 'welcome', 'unused'], '@embroider/core/route/people'); }); test('has no helpers in route entrypoint', function () { - expectFile('./assets/_route_/people.js').doesNotMatch('capitalize'); + notInEntrypoint('capitalize', '@embroider/core/route/people'); }); test('has no modifiers in route entrypoint', function () { - expectFile('./assets/_route_/people.js').doesNotMatch('auto-focus'); + notInEntrypoint('auto-focus', '@embroider/core/route/people'); }); Qmodule('audit', function (hooks) { @@ -428,7 +480,6 @@ splitScenarios throwOnWarnings(hooks); let app: PreparedApp; - let expectFile: ExpectFile; hooks.before(async assert => { app = await scenario.prepare(); @@ -436,84 +487,75 @@ splitScenarios assert.equal(result.exitCode, 0, result.output); }); - hooks.beforeEach(assert => { - expectFile = expectFilesAt(readFileSync(join(app.dir, 'dist/.stage2-output'), 'utf8'), { qunit: assert }); - }); + let expectAudit = setupAuditTest(hooks, () => ({ app: app.dir })); + let notInEntrypoint = notInEntrypointFunction(expectAudit); + let inEntrypoint = inEntrypointFunction(expectAudit); test('has no components in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('all-people'); - expectFile('./assets/my-app.js').doesNotMatch('welcome'); - expectFile('./assets/my-app.js').doesNotMatch('unused'); + notInEntrypoint(['all-people', 'welcome', 'unused']); }); test('has no helpers in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('capitalize'); + notInEntrypoint('capitalize'); }); test('has no modifiers in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('auto-focus'); + notInEntrypoint('auto-focus'); }); test('has non-split controllers in main entrypoint', function () { - expectFile('./assets/my-app.js').matches('routes/index/controller'); + inEntrypoint('routes/index/controller'); }); test('has non-split route templates in main entrypoint', function () { - expectFile('./assets/my-app.js').matches('routes/index/template'); + inEntrypoint('routes/index/template'); }); test('has non-split routes in main entrypoint', function () { - expectFile('./assets/my-app.js').matches('routes/index/route'); + inEntrypoint('routes/index/route'); }); test('does not have split controllers in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('routes/people/controller'); - expectFile('./assets/my-app.js').doesNotMatch('routes/people/show/controller'); + notInEntrypoint(['routes/people/controller', 'routes/people/show/controller']); }); test('does not have split route templates in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('routes/people/template'); - expectFile('./assets/my-app.js').doesNotMatch('routes/people/index/template'); - expectFile('./assets/my-app.js').doesNotMatch('routes/people/show/template'); + notInEntrypoint(['routes/people/template', 'routes/people/index/template', 'routes/people/show/template']); }); test('does not have split routes in main entrypoint', function () { - expectFile('./assets/my-app.js').doesNotMatch('routes/people/route'); - expectFile('./assets/my-app.js').doesNotMatch('routes/people/show/route'); + notInEntrypoint(['routes/people/route', 'routes/people/show/route']); }); test('dynamically imports the route entrypoint from the main entrypoint', function () { - expectFile('./assets/my-app.js').matches('import("my-app/assets/_route_/people.js")'); + inEntrypoint('import("@embroider/core/route/people")'); }); test('has split controllers in route entrypoint', function () { - expectFile('./assets/_route_/people.js').matches('routes/people/controller'); - expectFile('./assets/_route_/people.js').matches('routes/people/show/controller'); + inEntrypoint(['routes/people/controller', 'routes/people/show/controller'], '@embroider/core/route/people'); }); test('has split route templates in route entrypoint', function () { - expectFile('./assets/_route_/people.js').matches('routes/people/template'); - expectFile('./assets/_route_/people.js').matches('routes/people/index/template'); - expectFile('./assets/_route_/people.js').matches('routes/people/show/template'); + inEntrypoint( + ['routes/people/template', 'routes/people/index/template', 'routes/people/show/template'], + '@embroider/core/route/people' + ); }); test('has split routes in route entrypoint', function () { - expectFile('./assets/_route_/people.js').matches('routes/people/route'); - expectFile('./assets/_route_/people.js').matches('routes/people/show/route'); + inEntrypoint(['routes/people/route', 'routes/people/show/route'], '@embroider/core/route/people'); }); test('has no components in route entrypoint', function () { - expectFile('./assets/_route_/people.js').doesNotMatch('all-people'); - expectFile('./assets/_route_/people.js').doesNotMatch('welcome'); - expectFile('./assets/_route_/people.js').doesNotMatch('unused'); + notInEntrypoint(['all-people', 'welcome', 'unused'], '@embroider/core/route/people'); }); test('has no helpers in route entrypoint', function () { - expectFile('./assets/_route_/people.js').doesNotMatch('capitalize'); + notInEntrypoint('capitalize', '@embroider/core/route/people'); }); test('has no modifiers in route entrypoint', function () { - expectFile('./assets/_route_/people.js').doesNotMatch('auto-focus'); + notInEntrypoint('auto-focus', '@embroider/core/route/people'); }); Qmodule('audit', function (hooks) { diff --git a/tests/scenarios/compat-stage2-test.ts b/tests/scenarios/compat-stage2-test.ts index 0124010aa..8100bbeec 100644 --- a/tests/scenarios/compat-stage2-test.ts +++ b/tests/scenarios/compat-stage2-test.ts @@ -131,10 +131,9 @@ stage2Scenarios // check that the app trees with in repo addon are combined correctly expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/my-app.js') + .resolves('/@embroider/core/entrypoint') .toModule() - //TODO investigate removing this @embroider-dep - .resolves('my-app/service/in-repo.js') + .resolves('./service/in-repo.js') .to('./node_modules/dep-b/lib/in-repo-c/_app_/service/in-repo.js'); }); @@ -142,10 +141,9 @@ stage2Scenarios // secondary in-repo-addon was correctly detected and activated expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/my-app.js') + .resolves('/@embroider/core/entrypoint') .toModule() - //TODO investigate removing this @embroider-dep - .resolves('my-app/services/secondary.js') + .resolves('./services/secondary.js') .to('./lib/secondary-in-repo-addon/_app_/services/secondary.js'); // secondary is resolvable from primary @@ -209,46 +207,46 @@ stage2Scenarios test('verifies that the correct lexigraphically sorted addons win', function () { let expectModule = expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/my-app.js') + .resolves('/@embroider/core/entrypoint') .toModule(); - expectModule.resolves('my-app/service/in-repo.js').to('./lib/in-repo-b/_app_/service/in-repo.js'); - expectModule.resolves('my-app/service/addon.js').to('./node_modules/dep-b/_app_/service/addon.js'); - expectModule.resolves('my-app/service/dev-addon.js').to('./node_modules/dev-c/_app_/service/dev-addon.js'); + expectModule.resolves('./service/in-repo.js').to('./lib/in-repo-b/_app_/service/in-repo.js'); + expectModule.resolves('./service/addon.js').to('./node_modules/dep-b/_app_/service/addon.js'); + expectModule.resolves('./service/dev-addon.js').to('./node_modules/dev-c/_app_/service/dev-addon.js'); }); test('addons declared as dependencies should win over devDependencies', function () { expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/my-app.js') + .resolves('/@embroider/core/entrypoint') .toModule() - .resolves('my-app/service/dep-wins-over-dev.js') + .resolves('./service/dep-wins-over-dev.js') .to('./node_modules/dep-b/_app_/service/dep-wins-over-dev.js'); }); test('in repo addons declared win over dependencies', function () { expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/my-app.js') + .resolves('/@embroider/core/entrypoint') .toModule() - .resolves('my-app/service/in-repo-over-deps.js') + .resolves('./service/in-repo-over-deps.js') .to('./lib/in-repo-a/_app_/service/in-repo-over-deps.js'); }); test('ordering with before specified', function () { expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/my-app.js') + .resolves('/@embroider/core/entrypoint') .toModule() - .resolves('my-app/service/test-before.js') + .resolves('./service/test-before.js') .to('./node_modules/dev-d/_app_/service/test-before.js'); }); test('ordering with after specified', function () { expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/my-app.js') + .resolves('/@embroider/core/entrypoint') .toModule() - .resolves('my-app/service/test-after.js') + .resolves('./service/test-after.js') .to('./node_modules/dev-b/_app_/service/test-after.js'); }); }); @@ -672,42 +670,57 @@ stage2Scenarios ); }); - test('non-static other paths are included in the entrypoint', function () { + test('non-static other paths are included in the entrypoint', function (assert) { expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/my-app.js') - .toModule().codeContains(`d("my-app/non-static-dir/another-library", function () { - return i("my-app/non-static-dir/another-library.js"); - });`); + .resolves('/@embroider/core/entrypoint') + .toModule() + .withContents(contents => { + const result = /import \* as (\w+) from "\.\/non-static-dir\/another-library.js";/.exec(contents); + + if (!result) { + throw new Error('Could not find import for non-static-dir/another-library'); + } + + const [, amdModule] = result; + + assert.codeContains( + contents, + `d("my-app/non-static-dir/another-library", function () { + return ${amdModule}; + });` + ); + return true; + }); }); test('static other paths are not included in the entrypoint', function () { expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/my-app.js') + .resolves('/@embroider/core/entrypoint') .toModule() .withContents(content => { - return !/my-app\/static-dir\/my-library\.js"/.test(content); + return !/\.\/static-dir\/my-library\.js"/.test(content); }); }); test('top-level static other paths are not included in the entrypoint', function () { expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/my-app.js') + .resolves('/@embroider/core/entrypoint') .toModule() .withContents(content => { - return !content.includes('my-app/top-level-static.js'); + return !content.includes('./top-level-static.js'); }); }); test('staticAppPaths do not match partial path segments', function () { expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/my-app.js') + .resolves('/@embroider/core/entrypoint') .toModule() .withContents(content => { - return content.includes('my-app/static-dir-not-really/something.js'); + return content.includes('./static-dir-not-really/something.js'); }); }); diff --git a/tests/scenarios/compat-template-colocation-test.ts b/tests/scenarios/compat-template-colocation-test.ts index 5efba6c18..bafc4aed0 100644 --- a/tests/scenarios/compat-template-colocation-test.ts +++ b/tests/scenarios/compat-template-colocation-test.ts @@ -1,10 +1,8 @@ import type { PreparedApp } from 'scenario-tester'; import { appScenarios, baseAddon, renameApp } from './scenarios'; -import { readFileSync } from 'fs'; -import { join } from 'path'; import { Transpiler } from '@embroider/test-support'; import type { ExpectFile } from '@embroider/test-support/file-assertions/qunit'; -import { expectFilesAt, expectRewrittenFilesAt } from '@embroider/test-support/file-assertions/qunit'; +import { expectRewrittenFilesAt } from '@embroider/test-support/file-assertions/qunit'; import { setupAuditTest } from '@embroider/test-support/audit-assertions'; import { throwOnWarnings } from '@embroider/core'; import merge from 'lodash/merge'; @@ -21,7 +19,8 @@ let scenarios = appScenarios.map('compat-template-colocation', app => { templates: { 'index.hbs': ` - + {{!-- TODO why is there a TS component here using an appScenario?? --}} + {{!-- --}} `, }, @@ -130,13 +129,28 @@ scenarios assertFile.doesNotExist('component stub was not created'); }); - test(`app's colocated components are implicitly included correctly`, function () { + test(`app's colocated components are implicitly included correctly`, function (assert) { expectAudit .module('./node_modules/.embroider/rewritten-app/index.html') - .resolves('/assets/my-app.js') - .toModule().codeContains(`d("my-app/components/has-colocated-template", function () { - return i("my-app/components/has-colocated-template.js"); - });`); + .resolves('/@embroider/core/entrypoint') + .toModule() + .withContents(contents => { + const result = /import \* as (\w+) from "\.\/components\/has-colocated-template.js";/.exec(contents); + + if (!result) { + throw new Error('Missing import of has-colocated-template'); + } + + const [, amdModule] = result; + + assert.codeContains( + contents, + `d("my-app/components/has-colocated-template", function () { + return ${amdModule}; + });` + ); + return true; + }); }); test(`addon's colocated template is associated with JS`, function () { @@ -189,14 +203,20 @@ scenarios expectFile = expectRewrittenFilesAt(app.dir, { qunit: assert }); }); - test(`app's colocated components are not implicitly included`, function () { - let assertFile = expectFile('assets/my-app.js'); - assertFile.doesNotMatch( - /d\(["']my-app\/components\/has-colocated-template["'], function\(\)\s*\{\s*return i\(["']my-app\/components\/has-colocated-template['"]\);\s*\}/ - ); - assertFile.doesNotMatch( - /d\(["']my-app\/components\/template-only-component["'], function\(\)\s*\{\s*return i\(["']my-app\/components\/template-only-component['"]\);\s*\}/ - ); + let expectAudit = setupAuditTest(hooks, () => ({ app: app.dir })); + + test(`app's colocated components are not implicitly included`, function (assert) { + expectAudit + .module('./node_modules/.embroider/rewritten-app/index.html') + .resolves('/@embroider/core/entrypoint') + .toModule() + .withContents(content => { + assert.notOk(/import \* as (\w+) from "\.\/components\/has-colocated-component.js"/.test(content)); + + assert.notOk(/import \* as (\w+) from "\.\/components\/template-only-component.js"/.test(content)); + + return true; + }); }); test(`addon's colocated components are not in implicit-modules`, function () { @@ -269,7 +289,6 @@ appScenarios throwOnWarnings(hooks); let app: PreparedApp; - let expectFile: ExpectFile; hooks.before(async assert => { app = await scenario.prepare(); @@ -277,21 +296,61 @@ appScenarios assert.equal(result.exitCode, 0, result.output); }); - hooks.beforeEach(assert => { - expectFile = expectFilesAt(readFileSync(join(app.dir, 'dist/.stage2-output'), 'utf8'), { qunit: assert }); - }); + let expectAudit = setupAuditTest(hooks, () => ({ app: app.dir })); - test(`app's pod components and templates are implicitly included correctly`, function () { - let assertFile = expectFile('assets/my-app.js'); - assertFile.matches( - /d\(["']my-app\/components\/pod-component\/component["'], function\(\)\s*\{\s*return i\(["']my-app\/components\/pod-component\/component\.js['"]\);\}\)/ - ); - assertFile.matches( - /d\(["']my-app\/components\/pod-component\/template["'], function\(\)\s*\{\s*return i\(["']my-app\/components\/pod-component\/template\.hbs['"]\);\}\)/ - ); - assertFile.matches( - /d\(["']my-app\/components\/template-only\/template["'], function\(\)\s*\{\s*return i\(["']my-app\/components\/template-only\/template\.hbs['"]\);\s*\}/ - ); + test(`app's pod components and templates are implicitly included correctly`, function (assert) { + expectAudit + .module('./node_modules/.embroider/rewritten-app/index.html') + .resolves('/@embroider/core/entrypoint') + .toModule() + .withContents(content => { + let result = /import \* as (\w+) from "\.\/components\/pod-component\/component.js"/.exec(content); + + if (!result) { + throw new Error('Could not find pod component'); + } + + const [, podComponentAmd] = result; + + assert.codeContains( + content, + `d("my-app/components/pod-component/component", function () { + return ${podComponentAmd}; + });` + ); + + result = /import \* as (\w+) from "\.\/components\/pod-component\/template.hbs"/.exec(content); + + if (!result) { + throw new Error('Could not find pod component template'); + } + + const [, podComponentTemplateAmd] = result; + + assert.codeContains( + content, + `d("my-app/components/pod-component/template", function () { + return ${podComponentTemplateAmd}; + });` + ); + + result = /import \* as (\w+) from "\.\/components\/template-only\/template.hbs"/.exec(content); + + if (!result) { + throw new Error('Could not find template only component'); + } + + const [, templateOnlyAmd] = result; + + assert.codeContains( + content, + `d("my-app/components/template-only/template", function () { + return ${templateOnlyAmd}; + });` + ); + + return true; + }); }); }); }); diff --git a/tests/scenarios/core-resolver-test.ts b/tests/scenarios/core-resolver-test.ts index 96b25a5aa..a4c4e8073 100644 --- a/tests/scenarios/core-resolver-test.ts +++ b/tests/scenarios/core-resolver-test.ts @@ -132,6 +132,8 @@ Scenarios.fromProject(() => new Project()) roots: [app.dir], }, ], + autoRun: true, + staticAppPaths: [], }; givenFiles({ diff --git a/tests/scenarios/engines-test.ts b/tests/scenarios/engines-test.ts index 95f01612d..7d94e373c 100644 --- a/tests/scenarios/engines-test.ts +++ b/tests/scenarios/engines-test.ts @@ -84,6 +84,7 @@ engineScenarios .skip('lts_3_28-engines') // this skip should be removed before https://github.com/embroider-build/embroider/pull/1435 is merged .skip('lts_4_4-engines') // this skip should be removed before https://github.com/embroider-build/embroider/pull/1435 is merged .skip('release-engines') // this skip should be removed before https://github.com/embroider-build/embroider/pull/1435 is merged + .skip('lts_5_8-engines') // this shouldn't be run .skip('canary-engines') // this shouldn't be run .map('without-fastboot', () => {}) .forEachScenario(scenario => { @@ -102,7 +103,7 @@ engineScenarios }); test(`pnpm test safe`, async function (assert) { - let result = await app.execute('pnpm test --filter=!@optimized', { + let result = await app.execute("pnpm run test --filter='!@optimized'", { env: { EMBROIDER_TEST_SETUP_OPTIONS: 'safe', EMBROIDER_TEST_SETUP_FORCE: 'embroider', @@ -141,6 +142,7 @@ engineScenarios .skip('lts_3_28-engines') // fails due to https://github.com/emberjs/ember.js/pull/20461 .skip('lts_4_4-engines') // fails due to https://github.com/emberjs/ember.js/pull/20461 .skip('release-engines') // fails due to https://github.com/emberjs/ember.js/pull/20461 + .skip('lts_5_8-engines') .skip('canary-engines') // this shouldn't be run .map('with-fastboot', app => { app.linkDependency('ember-cli-fastboot', { baseDir: __dirname }); diff --git a/tests/scenarios/fastboot-app-test.ts b/tests/scenarios/fastboot-app-test.ts index 28d168c20..0cd0c444f 100644 --- a/tests/scenarios/fastboot-app-test.ts +++ b/tests/scenarios/fastboot-app-test.ts @@ -83,6 +83,7 @@ appScenarios merge(project.files, loadFromFixtureData('fastboot-app')); }) // TODO remove once https://github.com/ember-fastboot/ember-cli-fastboot/issues/925 is fixed + .skip('lts_5_8-fastboot-app-test') .skip('canary-fastboot-app-test') .forEachScenario(scenario => { Qmodule(scenario.name, function (hooks) { diff --git a/tests/scenarios/package.json b/tests/scenarios/package.json index 7dfe99322..1f3864a9c 100644 --- a/tests/scenarios/package.json +++ b/tests/scenarios/package.json @@ -79,6 +79,7 @@ "ember-source-4.4": "npm:ember-source@~4.4.0", "ember-source-beta": "npm:ember-source@beta", "ember-source-canary": "https://s3.amazonaws.com/builds.emberjs.com/canary/shas/370cf34f9e86df17b880f11fef35a5a0f24ff38a.tgz", + "ember-source-5.8": "npm:ember-source@~5.8.0", "ember-source-latest": "npm:ember-source@latest", "ember-truth-helpers": "^3.0.0", "execa": "^5.1.1", diff --git a/tests/scenarios/scenarios.ts b/tests/scenarios/scenarios.ts index c21ae6b7a..004b4e5c8 100644 --- a/tests/scenarios/scenarios.ts +++ b/tests/scenarios/scenarios.ts @@ -21,6 +21,14 @@ async function release(project: Project) { project.linkDevDependency('ember-qunit', { baseDir: __dirname, resolveName: 'ember-qunit-7' }); } +async function lts_5_8(project: Project) { + project.linkDevDependency('ember-source', { baseDir: __dirname, resolveName: 'ember-source-5.8' }); + project.linkDevDependency('ember-cli', { baseDir: __dirname, resolveName: 'ember-cli-latest' }); + project.linkDevDependency('ember-data', { baseDir: __dirname, resolveName: 'ember-data-latest' }); + project.linkDevDependency('@ember/test-helpers', { baseDir: __dirname, resolveName: '@ember/test-helpers-3' }); + project.linkDevDependency('ember-qunit', { baseDir: __dirname, resolveName: 'ember-qunit-7' }); +} + async function canary(project: Project) { project.linkDevDependency('ember-source', { baseDir: __dirname, resolveName: 'ember-source-canary' }); project.linkDevDependency('ember-cli', { baseDir: __dirname, resolveName: 'ember-cli-beta' }); @@ -35,6 +43,7 @@ export function supportMatrix(scenarios: Scenarios) { .expand({ lts_3_28, lts_4_4, + lts_5_8, release, canary, }) diff --git a/tests/scenarios/watch-mode-test.ts b/tests/scenarios/watch-mode-test.ts index 4386226e5..e2b62922b 100644 --- a/tests/scenarios/watch-mode-test.ts +++ b/tests/scenarios/watch-mode-test.ts @@ -9,12 +9,15 @@ import CommandWatcher, { DEFAULT_TIMEOUT } from './helpers/command-watcher'; const { module: Qmodule, test } = QUnit; -let app = appScenarios.skip('canary').map('watch-mode', () => { - /** - * We will create files as a part of the watch-mode tests, - * because creating files should cause appropriate watch/update behavior - */ -}); +let app = appScenarios + .skip('canary') + .skip('lts_5_8') + .map('watch-mode', () => { + /** + * We will create files as a part of the watch-mode tests, + * because creating files should cause appropriate watch/update behavior + */ + }); class File { constructor(readonly label: string, readonly fullPath: string) {}