diff --git a/packages/compat/src/compat-app-builder.ts b/packages/compat/src/compat-app-builder.ts index 67b5a4118..5d3a45bcf 100644 --- a/packages/compat/src/compat-app-builder.ts +++ b/packages/compat/src/compat-app-builder.ts @@ -516,10 +516,8 @@ export class CompatAppBuilder { } } - let implicitStyles = this.implicitStylesAsset(prepared, parentEngine); - if (implicitStyles) { - html.insertStyleLink(html.implicitStyles, implicitStyles.relativePath); - } + // virtual vendor.css entrypoint + html.insertStyleLink(html.implicitStyles, '@embroider/core/vendor.css'); if (!asset.fileAsset.includeTests) { return; @@ -555,19 +553,6 @@ export class CompatAppBuilder { return asset; } - private implicitStylesAsset(prepared: Map, application: AppFiles): InternalAsset | undefined { - let asset = prepared.get('assets/vendor.css'); - if (!asset) { - let implicitStyles = this.impliedAssets('implicit-styles', application); - if (implicitStyles.length > 0) { - // we reverse because we want the synthetic vendor style at the top - asset = new ConcatenatedAsset('assets/vendor.css', implicitStyles.reverse(), this.resolvableExtensionsPattern); - prepared.set(asset.relativePath, asset); - } - } - return asset; - } - private implicitTestStylesAsset( prepared: Map, application: AppFiles diff --git a/packages/core/src/module-resolver.ts b/packages/core/src/module-resolver.ts index d10debe36..7a6823b46 100644 --- a/packages/core/src/module-resolver.ts +++ b/packages/core/src/module-resolver.ts @@ -194,6 +194,7 @@ export class Resolver { request = await this.handleGlobalsCompat(request); request = this.handleImplicitModules(request); request = this.handleImplicitTestScripts(request); + request = this.handleVendorStyles(request); request = this.handleRenaming(request); // we expect the specifier to be app relative at this point - must be after handleRenaming request = this.generateFastbootSwitch(request); @@ -473,6 +474,28 @@ export class Resolver { } } + private handleVendorStyles(request: R): R { + //TODO move the extra forwardslash handling out into the vite plugin + const candidates = ['@embroider/core/vendor.css', '/@embroider/core/vendor.css', './@embroider/core/vendor.css']; + + if (!candidates.includes(request.specifier)) { + return request; + } + + let pkg = this.packageCache.ownerOfFile(request.fromFile); + if (pkg?.root !== this.options.engines[0].root) { + throw new Error( + `bug: found an import of ${request.specifier} in ${request.fromFile}, but this is not the top-level Ember app. The top-level Ember app is the only one that has support for @embroider/core/vendor.css. If you think something should be fixed in Embroider, please open an issue on https://github.com/embroider-build/embroider/issues.` + ); + } + + return logTransition( + 'vendor-styles', + request, + request.virtualize(resolve(pkg.root, '-embroider-vendor-styles.css')) + ); + } + private resolveHelper(path: string, inEngine: EngineConfig, request: R): R { let target = this.parseGlobalPath(path, inEngine); return logTransition( diff --git a/packages/core/src/virtual-content.ts b/packages/core/src/virtual-content.ts index 9e51dc9cf..d82f20f40 100644 --- a/packages/core/src/virtual-content.ts +++ b/packages/core/src/virtual-content.ts @@ -4,6 +4,8 @@ import { explicitRelative, extensionsPattern } from '.'; import { compile } from './js-handlebars'; import { decodeImplicitTestScripts, renderImplicitTestScripts } from './virtual-test-support'; +import { decodeVirtualVendorStyles, renderVendorStyles } from './virtual-vendor-styles'; + const externalESPrefix = '/@embroider/ext-es/'; const externalCJSPrefix = '/@embroider/ext-cjs/'; @@ -46,6 +48,11 @@ export function virtualContent(filename: string, resolver: Resolver): VirtualCon return renderImplicitTestScripts(filename, resolver); } + let isVendorStyles = decodeVirtualVendorStyles(filename); + if (isVendorStyles) { + return renderVendorStyles(filename, resolver); + } + throw new Error(`not an @embroider/core virtual file: ${filename}`); } diff --git a/packages/core/src/virtual-vendor-styles.ts b/packages/core/src/virtual-vendor-styles.ts new file mode 100644 index 000000000..27f512eaf --- /dev/null +++ b/packages/core/src/virtual-vendor-styles.ts @@ -0,0 +1,77 @@ +import type { Package } from '@embroider/shared-internals'; +import type { V2AddonPackage } from '@embroider/shared-internals/src/package'; +import { readFileSync } from 'fs'; +import { sortBy } from 'lodash'; +import resolve from 'resolve'; +import type { Resolver } from './module-resolver'; +import type { VirtualContentResult } from './virtual-content'; +import type { Engine } from './app-files'; + +export function decodeVirtualVendorStyles(filename: string): boolean { + return filename.endsWith('-embroider-vendor-styles.css'); +} + +export function renderVendorStyles(filename: string, resolver: Resolver): VirtualContentResult { + const owner = resolver.packageCache.ownerOfFile(filename); + if (!owner) { + throw new Error(`Failed to find a valid owner for ${filename}`); + } + return { src: getVendorStyles(owner, resolver), watches: [] }; +} + +function getVendorStyles(owner: Package, resolver: Resolver): string { + let engineConfig = resolver.owningEngine(owner); + let engine: Engine = { + package: owner, + addons: new Map( + engineConfig.activeAddons.map(addon => [ + resolver.packageCache.get(addon.root) as V2AddonPackage, + addon.canResolveFromFile, + ]) + ), + isApp: true, + modulePrefix: resolver.options.modulePrefix, + appRelativePath: 'NOT_USED_DELETE_ME', + }; + + return generateVendorStyles(engine); +} + +function generateVendorStyles(engine: Engine): string { + let result: string[] = impliedAddonVendorStyles(engine).map((sourcePath: string): string => { + let source = readFileSync(sourcePath); + return `${source}`; + }); + + return result.join('') as string; +} + +function impliedAddonVendorStyles(engine: Engine): string[] { + let result: Array = []; + for (let addon of sortBy(Array.from(engine.addons.keys()), pkg => { + switch (pkg.name) { + case 'loader.js': + return 0; + case 'ember-source': + return 10; + default: + return 1000; + } + })) { + let implicitStyles = addon.meta['implicit-styles']; + if (implicitStyles) { + let styles = []; + let options = { basedir: addon.root }; + for (let mod of implicitStyles) { + // exclude engines because they will handle their own css importation + if (!addon.isLazyEngine()) { + styles.push(resolve.sync(mod, options)); + } + } + if (styles.length) { + result = [...styles, ...result]; + } + } + } + return result; +} diff --git a/packages/vite/src/esbuild-resolver.ts b/packages/vite/src/esbuild-resolver.ts index d22c0b83a..094a2f059 100644 --- a/packages/vite/src/esbuild-resolver.ts +++ b/packages/vite/src/esbuild-resolver.ts @@ -69,6 +69,10 @@ export function esBuildResolver(root = process.cwd()): EsBuildPlugin { }); build.onLoad({ namespace: 'embroider', filter: /./ }, ({ path }) => { + // We don't want esbuild to try loading virtual CSS files + if (path.endsWith('.css')) { + return { contents: '' }; + } let { src } = virtualContent(path, resolverLoader.resolver); if (!macrosConfig) { macrosConfig = readJSONSync( diff --git a/tests/scenarios/compat-app-html-attributes-test.ts b/tests/scenarios/compat-app-html-attributes-test.ts index 438c2f9c3..aecc19de8 100644 --- a/tests/scenarios/compat-app-html-attributes-test.ts +++ b/tests/scenarios/compat-app-html-attributes-test.ts @@ -59,9 +59,9 @@ appScenarios expectFile('./index.html').doesNotMatch('rel="stylesheet"', 'does not have rel=stylesheet'); expectFile('./index.html').matches('