Skip to content

Commit

Permalink
Merge pull request #1801 from BlueCutOfficial/virtualize-vendor
Browse files Browse the repository at this point in the history
Module resolver: virtualize vendor.js
  • Loading branch information
mansona authored Apr 24, 2024
2 parents ea895c2 + ef491d7 commit ccbf41f
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 67 deletions.
84 changes: 18 additions & 66 deletions packages/compat/src/compat-app-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,7 @@ export class CompatAppBuilder {
return extensionsPattern(this.resolvableExtensions());
}

private impliedAssets(
type: keyof ImplicitAssetPaths,
engine: AppFiles,
emberENV?: EmberENV
): (OnDiskAsset | InMemoryAsset)[] {
private impliedAssets(type: keyof ImplicitAssetPaths, engine: AppFiles): (OnDiskAsset | InMemoryAsset)[] {
let result: (OnDiskAsset | InMemoryAsset)[] = this.impliedAddonAssets(type, engine).map(
(sourcePath: string): OnDiskAsset => {
let stats = statSync(sourcePath);
Expand All @@ -325,26 +321,6 @@ export class CompatAppBuilder {
}
);

if (type === 'implicit-scripts') {
result.unshift({
kind: 'in-memory',
relativePath: '_testing_prefix_.js',
source: `var runningTests=false;`,
});

result.unshift({
kind: 'in-memory',
relativePath: '_ember_env_.js',
source: `window.EmberENV={ ...(window.EmberENV || {}), ...${JSON.stringify(emberENV, null, 2)} };`,
});

result.push({
kind: 'in-memory',
relativePath: '_loader_.js',
source: `loader.makeDefaultExport=false;`,
});
}

return result;
}

Expand Down Expand Up @@ -451,12 +427,7 @@ export class CompatAppBuilder {
return portable;
}

private insertEmberApp(
asset: ParsedEmberAsset,
appFiles: AppFiles[],
prepared: Map<string, InternalAsset>,
emberENV: EmberENV
) {
private insertEmberApp(asset: ParsedEmberAsset, appFiles: AppFiles[], prepared: Map<string, InternalAsset>) {
let html = asset.html;

if (this.fastbootConfig) {
Expand Down Expand Up @@ -485,11 +456,8 @@ export class CompatAppBuilder {

html.insertStyleLink(html.styles, `assets/${this.origAppPackage.name}.css`);

const parentEngine = appFiles.find(e => e.engine.isApp)!;
let vendorJS = this.implicitScriptsAsset(prepared, parentEngine, emberENV);
if (vendorJS) {
html.insertScriptTag(html.implicitScripts, vendorJS.relativePath);
}
// virtual-vendor entrypoint
html.insertScriptTag(html.implicitScripts, '@embroider/core/vendor.js');

if (this.fastbootConfig) {
// any extra fastboot vendor files get inserted into our
Expand Down Expand Up @@ -518,22 +486,6 @@ export class CompatAppBuilder {
html.insertStyleLink(html.implicitTestStyles, '@embroider/core/test-support.css');
}

private implicitScriptsAsset(
prepared: Map<string, InternalAsset>,
application: AppFiles,
emberENV: EmberENV
): InternalAsset | undefined {
let asset = prepared.get('assets/vendor.js');
if (!asset) {
let implicitScripts = this.impliedAssets('implicit-scripts', application, emberENV);
if (implicitScripts.length > 0) {
asset = new ConcatenatedAsset('assets/vendor.js', implicitScripts, this.resolvableExtensionsPattern);
prepared.set(asset.relativePath, asset);
}
}
return asset;
}

// recurse to find all active addons that don't cross an engine boundary.
// Inner engines themselves will be returned, but not those engines' children.
// The output set's insertion order is the proper ember-cli compatible
Expand Down Expand Up @@ -671,7 +623,7 @@ export class CompatAppBuilder {
);
}

private prepareAsset(asset: Asset, appFiles: AppFiles[], prepared: Map<string, InternalAsset>, emberENV: EmberENV) {
private prepareAsset(asset: Asset, appFiles: AppFiles[], prepared: Map<string, InternalAsset>) {
if (asset.kind === 'ember') {
let prior = this.assets.get(asset.relativePath);
let parsed: ParsedEmberAsset;
Expand All @@ -682,21 +634,17 @@ export class CompatAppBuilder {
} else {
parsed = new ParsedEmberAsset(asset);
}
this.insertEmberApp(parsed, appFiles, prepared, emberENV);
this.insertEmberApp(parsed, appFiles, prepared);
prepared.set(asset.relativePath, new BuiltEmberAsset(parsed));
} else {
prepared.set(asset.relativePath, asset);
}
}

private prepareAssets(
requestedAssets: Asset[],
appFiles: AppFiles[],
emberENV: EmberENV
): Map<string, InternalAsset> {
private prepareAssets(requestedAssets: Asset[], appFiles: AppFiles[]): Map<string, InternalAsset> {
let prepared: Map<string, InternalAsset> = new Map();
for (let asset of requestedAssets) {
this.prepareAsset(asset, appFiles, prepared, emberENV);
this.prepareAsset(asset, appFiles, prepared);
}
return prepared;
}
Expand Down Expand Up @@ -770,8 +718,8 @@ export class CompatAppBuilder {
await concat.end();
}

private async updateAssets(requestedAssets: Asset[], appFiles: AppFiles[], emberENV: EmberENV) {
let assets = this.prepareAssets(requestedAssets, appFiles, emberENV);
private async updateAssets(requestedAssets: Asset[], appFiles: AppFiles[]) {
let assets = this.prepareAssets(requestedAssets, appFiles);
for (let asset of assets.values()) {
if (this.assetIsValid(asset, this.assets.get(asset.relativePath))) {
continue;
Expand Down Expand Up @@ -841,10 +789,9 @@ export class CompatAppBuilder {
}

let appFiles = this.updateAppJS(inputPaths.appJS);
let emberENV = this.configTree.readConfig().EmberENV;
let assets = this.gatherAssets(inputPaths);

let finalAssets = await this.updateAssets(assets, appFiles, emberENV);
let finalAssets = await this.updateAssets(assets, appFiles);

let assetPaths = assets.map(asset => asset.relativePath);

Expand Down Expand Up @@ -884,6 +831,7 @@ export class CompatAppBuilder {
let resolverConfig = this.resolverConfig(appFiles);
this.addResolverConfig(resolverConfig);
this.addContentForConfig(this.contentForTree.readContents());
this.addEmberEnvConfig(this.configTree.readConfig().EmberENV);
let babelConfig = await this.babelConfig(resolverConfig);
this.addBabelConfig(babelConfig);
writeFileSync(
Expand Down Expand Up @@ -987,6 +935,12 @@ export class CompatAppBuilder {
});
}

private addEmberEnvConfig(emberEnvConfig: any) {
outputJSONSync(join(locateEmbroiderWorkingDir(this.compatApp.root), 'ember-env.json'), emberEnvConfig, {
spaces: 2,
});
}

private shouldSplitRoute(routeName: string) {
return (
!this.options.splitAtRoutes ||
Expand Down Expand Up @@ -1477,8 +1431,6 @@ interface TreeNames {
configTree: BroccoliNode;
}

type EmberENV = unknown;

type InternalAsset = OnDiskAsset | InMemoryAsset | BuiltEmberAsset | ConcatenatedAsset;

class ParsedEmberAsset {
Expand Down
19 changes: 19 additions & 0 deletions packages/core/src/module-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ export class Resolver {
request = this.handleVendorStyles(request);
request = this.handleTestSupportStyles(request);
request = this.handleRenaming(request);
request = this.handleVendor(request);
// we expect the specifier to be app relative at this point - must be after handleRenaming
request = this.generateFastbootSwitch(request);
request = this.preHandleExternal(request);
Expand Down Expand Up @@ -945,6 +946,24 @@ export class Resolver {
return request;
}

private handleVendor<R extends ModuleRequest>(request: R): R {
//TODO move the extra forwardslash handling out into the vite plugin
const candidates = ['@embroider/core/vendor.js', '/@embroider/core/vendor.js', './@embroider/core/vendor.js'];

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.js. If you think something should be fixed in Embroider, please open an issue on https://github.com/embroider-build/embroider/issues.`
);
}

return logTransition('vendor', request, request.virtualize(resolve(pkg.root, '-embroider-vendor.js')));
}

private resolveWithinMovedPackage<R extends ModuleRequest>(request: R, pkg: Package): R {
let levels = ['..'];
if (pkg.name.startsWith('@')) {
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/virtual-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { explicitRelative, extensionsPattern } from '.';
import { compile } from './js-handlebars';
import { decodeImplicitTestScripts, renderImplicitTestScripts } from './virtual-test-support';
import { decodeTestSupportStyles, renderTestSupportStyles } from './virtual-test-support-styles';
import { decodeVirtualVendor, renderVendor } from './virtual-vendor';
import { decodeVirtualVendorStyles, renderVendorStyles } from './virtual-vendor-styles';

const externalESPrefix = '/@embroider/ext-es/';
Expand Down Expand Up @@ -43,6 +44,11 @@ export function virtualContent(filename: string, resolver: Resolver): VirtualCon
return renderImplicitModules(im, resolver);
}

let isVendor = decodeVirtualVendor(filename);
if (isVendor) {
return renderVendor(filename, resolver);
}

let isImplicitTestScripts = decodeImplicitTestScripts(filename);
if (isImplicitTestScripts) {
return renderImplicitTestScripts(filename, resolver);
Expand Down
77 changes: 77 additions & 0 deletions packages/core/src/virtual-vendor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { type Package, locateEmbroiderWorkingDir } from '@embroider/shared-internals';
import type { V2AddonPackage } from '@embroider/shared-internals/src/package';
import { lstatSync, readFileSync, readJSONSync } from 'fs-extra';
import { sortBy } from 'lodash';
import { join } from 'path';
import resolve from 'resolve';
import type { Resolver } from './module-resolver';
import type { VirtualContentResult } from './virtual-content';

export function decodeVirtualVendor(filename: string): boolean {
return filename.endsWith('-embroider-vendor.js');
}

export function renderVendor(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: getVendor(owner, resolver, filename), watches: [] };
}

function getVendor(owner: Package, resolver: Resolver, filename: string): string {
let engineConfig = resolver.owningEngine(owner);
let addons = new Map(
engineConfig.activeAddons.map(addon => [
resolver.packageCache.get(addon.root) as V2AddonPackage,
addon.canResolveFromFile,
])
);

let path = join(locateEmbroiderWorkingDir(resolver.options.appRoot), 'ember-env.json');
if (!lstatSync(path).isFile()) {
throw new Error(`Failed to read the ember-env.json when generating content for ${filename}`);
}
let emberENV = readJSONSync(path);

return generateVendor(addons, emberENV);
}

function generateVendor(addons: Map<V2AddonPackage, string>, emberENV?: unknown): string {
// Add addons implicit-scripts
let vendor: string[] = impliedAddonVendors(addons).map((sourcePath: string): string => {
let source = readFileSync(sourcePath);
return `${source}`;
});
// Add _testing_prefix_.js
vendor.unshift(`var runningTests=false;`);
// Add _ember_env_.js
vendor.unshift(`window.EmberENV={ ...(window.EmberENV || {}), ...${JSON.stringify(emberENV, null, 2)} };`);
// Add _loader_.js
vendor.push(`loader.makeDefaultExport=false;`);

return vendor.join('') as string;
}

function impliedAddonVendors(addons: Map<V2AddonPackage, string>): string[] {
let result: Array<string> = [];
for (let addon of sortBy(Array.from(addons.keys()), pkg => {
switch (pkg.name) {
case 'loader.js':
return 0;
case 'ember-source':
return 10;
default:
return 1000;
}
})) {
let implicitScripts = addon.meta['implicit-scripts'];
if (implicitScripts) {
let options = { basedir: addon.root };
for (let mod of implicitScripts) {
result.push(resolve.sync(mod, options));
}
}
}
return result;
}
8 changes: 8 additions & 0 deletions packages/vite/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ export function resolver(): Plugin {
}
},
buildEnd() {
this.emitFile({
type: 'asset',
fileName: '@embroider/core/vendor.js',
source: virtualContent(
resolve(resolverLoader.resolver.options.engines[0].root, '-embroider-vendor.js'),
resolverLoader.resolver
).src,
});
this.emitFile({
type: 'asset',
fileName: '@embroider/core/test-support.js',
Expand Down
1 change: 1 addition & 0 deletions test-packages/support/suite-setup-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ async function githubMatrix() {
...suites
.filter(s => s.name !== 'jest-suites') // TODO: jest tests do not work under windows yet
.filter(s => !s.name.includes('watch-mode')) // TODO: watch tests are far too slow on windows right now
.filter(s => !s.name.endsWith('compat-addon-classic-features-virtual-scripts')) // TODO: these tests are too slow on windows right now
.map(s => ({
name: `${s.name} windows`,
os: 'windows',
Expand Down
42 changes: 41 additions & 1 deletion tests/scenarios/compat-addon-classic-features-test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { throwOnWarnings } from '@embroider/core';
import { readFileSync } from 'fs';
import { lstatSync, readFileSync } from 'fs';
import { merge } from 'lodash';
import QUnit from 'qunit';
import type { PreparedApp } from 'scenario-tester';
Expand Down Expand Up @@ -131,3 +131,43 @@ appScenarios
});
});
});

appScenarios
.map('compat-addon-classic-features-virtual-scripts', () => {})
.forEachScenario(scenario => {
let app: PreparedApp;

Qmodule(`${scenario.name} - build mode`, function (hooks) {
hooks.before(async assert => {
app = await scenario.prepare();
let result = await app.execute('pnpm build');
assert.equal(result.exitCode, 0, result.output);
});

test('vendor.js script is emitted in the build', async function (assert) {
assert.true(lstatSync(`${app.dir}/dist/@embroider/core/vendor.js`).isFile());
});
});

Qmodule(`${scenario.name} - dev mode`, function (hooks) {
hooks.before(async () => {
app = await scenario.prepare();
});

test('vendor.js script is served', async function (assert) {
const server = CommandWatcher.launch('vite', ['--clearScreen', 'false'], { cwd: app.dir });
try {
const [, url] = await server.waitFor(/Local:\s+(https?:\/\/.*)\//g);
let response = await fetch(`${url}/@embroider/core/vendor.js`);
assert.strictEqual(response.status, 200);
// checking the response status 200 is not enough to assert vendor.js is served,
// because when the URL is not recognized, the response contains the index.html
// and has a 200 status (for index.html being returned correctly)
let text = await response.text();
assert.true(!text.includes('<!DOCTYPE html>'));
} finally {
await server.shutdown();
}
});
});
});

0 comments on commit ccbf41f

Please sign in to comment.