Skip to content

Commit

Permalink
Move template-only component synthesis into core resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
ef4 committed Dec 20, 2024
1 parent b470496 commit 91b124a
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 118 deletions.
10 changes: 10 additions & 0 deletions packages/core/src/module-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,13 @@ export class ModuleRequest<Res extends Resolution = Resolution> implements Modul
return new ModuleRequest(this.#adapter, this) as this;
}
}

export async function extractResolution<Res extends Resolution = Resolution>(
res: Res | (() => Promise<Res>)
): Promise<Res> {
if (typeof res === 'function') {
return await res();
} else {
return res;
}
}
58 changes: 47 additions & 11 deletions packages/core/src/module-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import {
emberVirtualPeerDeps,
extensionsPattern,
packageName as getPackageName,
isInComponents,
needsSyntheticComponentJS,
packageName,
syntheticJStoHBS,
} from '@embroider/shared-internals';
import { dirname, resolve, posix, basename } from 'path';
import type { Package, V2Package } from '@embroider/shared-internals';
Expand All @@ -18,7 +21,7 @@ import { readFileSync } from 'fs';
import { nodeResolve } from './node-resolve';
import type { Options, EngineConfig } from './module-resolver-options';
import { satisfies } from 'semver';
import type { ModuleRequest, Resolution } from './module-request';
import { extractResolution, type ModuleRequest, type Resolution } from './module-request';

const debug = makeDebug('embroider:resolver');

Expand Down Expand Up @@ -145,15 +148,13 @@ export class Resolver {
let resolution: ResolveResolution;

if (request.resolvedTo) {
if (typeof request.resolvedTo === 'function') {
resolution = await request.resolvedTo();
} else {
resolution = request.resolvedTo;
}
resolution = await extractResolution(request.resolvedTo);
} else {
resolution = await request.defaultResolve();
}

resolution = await this.afterResolve(request, resolution);

switch (resolution.type) {
case 'found':
return resolution;
Expand All @@ -172,11 +173,7 @@ export class Resolver {
}

if (nextRequest.resolvedTo) {
if (typeof nextRequest.resolvedTo === 'function') {
return await nextRequest.resolvedTo();
} else {
return nextRequest.resolvedTo;
}
return await extractResolution(nextRequest.resolvedTo);
}

if (nextRequest.fromFile === request.fromFile && nextRequest.specifier === request.specifier) {
Expand Down Expand Up @@ -206,6 +203,45 @@ export class Resolver {
return RewrittenPackageCache.shared('embroider', this.options.appRoot);
}

private async afterResolve<Res extends Resolution>(request: ModuleRequest<Res>, resolution: Res): Promise<Res> {
resolution = await this.handleSyntheticComponents(request, resolution);
return resolution;
}

private async handleSyntheticComponents<Res extends Resolution>(
request: ModuleRequest<Res>,
resolution: Res
): Promise<Res> {
// Key assumption: the system's defaultResolve performs extension search for
// extensionless requests, with JS at a higher priority than HBS.

// When the request had an explicit ".js" extension, the system default
// extension search doesn't help us locate an HBS, so we need to check for
// it ourselves here.
if (resolution.type === 'not_found') {
let hbsSpecifier = syntheticJStoHBS(request.specifier);
if (hbsSpecifier) {
resolution = await this.resolve(request.alias(hbsSpecifier));
}
}

// At this point, we might have resolved an HBS file (either because the
// request was extensionless and the default search found it, or because of
// our own code above) when the request was for a JS file.
if (resolution.type === 'found') {
let syntheticId = needsSyntheticComponentJS(request.specifier, resolution.filename);
if (syntheticId && isInComponents(resolution.filename, this.packageCache)) {
let newRequest = logTransition(
`synthetic component JS`,
request,
request.virtualize({ type: 'template-only-component-js', specifier: syntheticId })
);
return await extractResolution(newRequest.resolvedTo!);
}
}
return resolution;
}

private logicalPackage(owningPackage: V2Package, file: string): V2Package {
let logicalLocation = this.reverseSearchAppTree(owningPackage, file);
if (logicalLocation) {
Expand Down
19 changes: 18 additions & 1 deletion packages/core/src/virtual-content.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { posix, sep, join } from 'path';
import type { Resolver, AddonPackage, Package } from '.';
import { extensionsPattern } from '.';
import { extensionsPattern, syntheticJStoHBS, templateOnlyComponentSource } from '.';
import { compile } from './js-handlebars';
import { renderImplicitTestScripts, type TestSupportResponse } from './virtual-test-support';
import { renderTestSupportStyles, type TestSupportStylesResponse } from './virtual-test-support-styles';
Expand All @@ -21,6 +21,7 @@ export type VirtualResponse = { specifier: string } & (
| VirtualVendorResponse
| VirtualVendorStylesResponse
| VirtualPairResponse
| TemplateOnlyComponentResponse
);

export interface VirtualContentResult {
Expand Down Expand Up @@ -53,6 +54,8 @@ export function virtualContent(response: VirtualResponse, resolver: Resolver): V
return renderRouteEntrypoint(response, resolver);
case 'fastboot-switch':
return renderFastbootSwitchTemplate(response);
case 'template-only-component-js':
return renderTemplateOnlyComponent(response);
default:
throw assertNever(response);
}
Expand Down Expand Up @@ -256,3 +259,17 @@ function orderAddons(depA: Package, depB: Package): number {

return depAIdx - depBIdx;
}

export interface TemplateOnlyComponentResponse {
type: 'template-only-component-js';
specifier: string;
}

function renderTemplateOnlyComponent({ specifier }: TemplateOnlyComponentResponse): VirtualContentResult {
let watches = [specifier];
let hbs = syntheticJStoHBS(specifier);
if (hbs) {
watches.push(hbs);
}
return { src: templateOnlyComponentSource(), watches };
}
43 changes: 5 additions & 38 deletions packages/vite/src/esbuild-resolver.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Plugin as EsBuildPlugin, OnLoadResult, PluginBuild, ResolveResult } from 'esbuild';
import { transformAsync } from '@babel/core';
import core, { ModuleRequest, type VirtualResponse } from '@embroider/core';
const { ResolverLoader, virtualContent, needsSyntheticComponentJS, isInComponents } = core;
const { ResolverLoader, virtualContent } = core;
import fs from 'fs-extra';
const { readFileSync } = fs;
import { EsBuildRequestAdapter } from './esbuild-request.js';
Expand All @@ -10,9 +10,6 @@ import { hbsToJS } from '@embroider/core';
import { Preprocessor } from 'content-tag';
import { extname } from 'path';

const templateOnlyComponent =
`import templateOnly from '@ember/component/template-only';\n` + `export default templateOnly();\n`;

export function esBuildResolver(): EsBuildPlugin {
let resolverLoader = new ResolverLoader(process.cwd());
let preprocessor = new Preprocessor();
Expand All @@ -35,9 +32,7 @@ export function esBuildResolver(): EsBuildPlugin {
pluginData?: { virtual: VirtualResponse };
}): Promise<OnLoadResult> {
let src: string;
if (namespace === 'embroider-template-only-component') {
src = templateOnlyComponent;
} else if (namespace === 'embroider-virtual') {
if (namespace === 'embroider-virtual') {
// castin because response in our namespace are supposed to always have
// this pluginData.
src = virtualContent(pluginData!.virtual, resolverLoader.resolver).src;
Expand Down Expand Up @@ -85,38 +80,11 @@ export function esBuildResolver(): EsBuildPlugin {
}
});

// template-only-component synthesis
build.onResolve({ filter: /./ }, async ({ path, importer, namespace, resolveDir, pluginData, kind }) => {
if (pluginData?.embroiderHBSResolving) {
// reentrance
return null;
}

let result = await build.resolve(path, {
namespace,
resolveDir,
importer,
kind,
// avoid reentrance
pluginData: { ...pluginData, embroiderHBSResolving: true },
});

if (result.errors.length === 0 && !result.external) {
let syntheticPath = needsSyntheticComponentJS(path, result.path);
if (syntheticPath && isInComponents(result.path, resolverLoader.resolver.packageCache)) {
return { path: syntheticPath, namespace: 'embroider-template-only-component' };
}
}

return result;
});

if (phase === 'bundling') {
// during bundling phase, we need to provide our own extension
// searching. We do it here in its own resolve plugin so that it's
// sitting beneath both embroider resolver and template-only-component
// synthesizer, since both expect the ambient system to have extension
// search.
// sitting beneath the embroider resolver since it expects the ambient
// system to have extension search.
build.onResolve({ filter: /./ }, async ({ path, importer, namespace, resolveDir, pluginData, kind }) => {
if (pluginData?.embroiderExtensionResolving) {
// reentrance
Expand Down Expand Up @@ -148,8 +116,7 @@ export function esBuildResolver(): EsBuildPlugin {
});
}

// we need to handle everything from one of our three special namespaces:
build.onLoad({ namespace: 'embroider-template-only-component', filter: /./ }, onLoad);
// we need to handle everything from our special namespaces
build.onLoad({ namespace: 'embroider-virtual', filter: /./ }, onLoad);
build.onLoad({ namespace: 'embroider-template-tag', filter: /./ }, onLoad);

Expand Down
69 changes: 1 addition & 68 deletions packages/vite/src/hbs.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,14 @@
import { createFilter } from '@rollup/pluginutils';
import type { PluginContext } from 'rollup';
import type { Plugin } from 'vite';
import {
hbsToJS,
ResolverLoader,
needsSyntheticComponentJS,
isInComponents,
templateOnlyComponentSource,
syntheticJStoHBS,
} from '@embroider/core';
import { hbsToJS, templateOnlyComponentSource } from '@embroider/core';

const resolverLoader = new ResolverLoader(process.cwd());
const hbsFilter = createFilter('**/*.hbs?([?]*)');

export function hbs(): Plugin {
return {
name: 'rollup-hbs-plugin',
enforce: 'pre',
async resolveId(source: string, importer: string | undefined, options) {
if (options.custom?.depScan) {
// during depscan we have a corresponding esbuild plugin that is
// responsible for this stuff instead. We don't want to fight with it.
return null;
}

if (options.custom?.embroider?.isExtensionSearch) {
return null;
}

let resolution = await this.resolve(source, importer, {
skipSelf: true,
});

if (!resolution) {
// vite already has extension search fallback for extensionless imports.
// This is different, it covers an explicit .js import fallback to the
// corresponding hbs.
let hbsSource = syntheticJStoHBS(source);
if (hbsSource) {
resolution = await this.resolve(hbsSource, importer, {
skipSelf: true,
custom: {
embroider: {
// we don't want to recurse into the whole embroider compatbility
// resolver here. It has presumably already steered our request to the
// correct place. All we want to do is slightly modify the request we
// were given (changing the extension) and check if that would resolve
// instead.
//
// Currently this guard is only actually exercised in rollup, not in
// vite, due to https://github.com/vitejs/vite/issues/13852
enableCustomResolver: false,
isExtensionSearch: true,
},
},
});
}

if (!resolution) {
return null;
}
}

let syntheticId = needsSyntheticComponentJS(source, resolution.id);
if (syntheticId && isInComponents(resolution.id, resolverLoader.resolver.packageCache)) {
return {
id: syntheticId,
meta: {
'rollup-hbs-plugin': {
type: 'template-only-component-js',
},
},
};
}

return resolution;
},

load(id: string) {
if (getMeta(this, id)?.type === 'template-only-component-js') {
Expand Down

0 comments on commit 91b124a

Please sign in to comment.