diff --git a/packages/core/src/node-resolve.ts b/packages/core/src/node-resolve.ts index 17e2919a2..86cb928ad 100644 --- a/packages/core/src/node-resolve.ts +++ b/packages/core/src/node-resolve.ts @@ -56,7 +56,7 @@ export function nodeResolve( type: 'found', result: { type: 'virtual' as 'virtual', - content: virtualContent(request.specifier, resolver), + content: virtualContent(request.specifier, resolver).src, filename: request.specifier, }, }; diff --git a/packages/core/src/virtual-content.ts b/packages/core/src/virtual-content.ts index f11123d81..55ac3a903 100644 --- a/packages/core/src/virtual-content.ts +++ b/packages/core/src/virtual-content.ts @@ -6,11 +6,16 @@ import { compile } from './js-handlebars'; const externalESPrefix = '/@embroider/ext-es/'; const externalCJSPrefix = '/@embroider/ext-cjs/'; +export interface VirtualContentResult { + src: string; + watches: string[]; +} + // Given a filename that was passed to your ModuleRequest's `virtualize()`, // this produces the corresponding contents. It's a static, stateless function // because we recognize that that process that did resolution might not be the // same one that loads the content. -export function virtualContent(filename: string, resolver: Resolver): string { +export function virtualContent(filename: string, resolver: Resolver): VirtualContentResult { let cjsExtern = decodeVirtualExternalCJSModule(filename); if (cjsExtern) { return renderCJSExternalShim(cjsExtern); @@ -27,7 +32,7 @@ export function virtualContent(filename: string, resolver: Resolver): string { let fb = decodeFastbootSwitch(filename); if (fb) { - return fastbootSwitchTemplate(fb); + return renderFastbootSwitchTemplate(fb); } let im = decodeImplicitModules(filename); @@ -56,15 +61,37 @@ export { {{#each names as |name|}}{{name}}, {{/each}} } {{/if}} `) as (params: { moduleName: string; default: boolean; names: string[] }) => string; -function renderESExternalShim({ moduleName, exports }: { moduleName: string; exports: string[] }): string { - return externalESShim({ - moduleName, - default: exports.includes('default'), - names: exports.filter(n => n !== 'default'), - }); +function renderESExternalShim({ + moduleName, + exports, +}: { + moduleName: string; + exports: string[]; +}): VirtualContentResult { + return { + src: externalESShim({ + moduleName, + default: exports.includes('default'), + names: exports.filter(n => n !== 'default'), + }), + watches: [], + }; +} + +interface PairedComponentShimParams { + relativeHBSModule: string; + relativeJSModule: string | null; + debugName: string; +} + +function pairedComponentShim(params: PairedComponentShimParams): VirtualContentResult { + return { + src: pairedComponentShimTemplate(params), + watches: [], + }; } -const pairedComponentShim = compile(` +const pairedComponentShimTemplate = compile(` import { setComponentTemplate } from "@ember/component"; import template from "{{{js-string-escape relativeHBSModule}}}"; {{#if relativeJSModule}} @@ -74,7 +101,7 @@ export default setComponentTemplate(template, component); import templateOnlyComponent from "@ember/component/template-only"; export default setComponentTemplate(template, templateOnlyComponent(undefined, "{{{js-string-escape debugName}}}")); {{/if}} -`) as (params: { relativeHBSModule: string; relativeJSModule: string | null; debugName: string }) => string; +`) as (params: PairedComponentShimParams) => string; export function virtualExternalESModule(specifier: string, exports: string[] | undefined): string { if (exports) { @@ -166,6 +193,18 @@ export function decodeFastbootSwitch(filename: string) { } } +interface FastbootSwitchParams { + names: string[]; + hasDefaultExport: boolean; +} + +function renderFastbootSwitchTemplate(params: FastbootSwitchParams): VirtualContentResult { + return { + src: fastbootSwitchTemplate(params), + watches: [], + }; +} + const fastbootSwitchTemplate = compile(` import { macroCondition, getGlobalConfig, importSync } from '@embroider/macros'; let mod; @@ -180,7 +219,7 @@ export default mod.default; {{#each names as |name|}} export const {{name}} = mod.{{name}}; {{/each}} -`) as (params: { names: string[]; hasDefaultExport: boolean }) => string; +`) as (params: FastbootSwitchParams) => string; const implicitModulesPattern = /(?.*)[\\/]-embroider-implicit-(?test-)?modules\.js$/; @@ -209,7 +248,7 @@ function renderImplicitModules( fromFile: string; }, resolver: Resolver -): string { +): VirtualContentResult { let resolvableExtensionsPattern = extensionsPattern(resolver.options.resolvableExtensions); const pkg = resolver.packageCache.ownerOfFile(fromFile); @@ -269,7 +308,7 @@ function renderImplicitModules( dependencyModules.push(posix.join(dep.name, `-embroider-${type}.js`)); } } - return implicitModulesTemplate({ ownModules, dependencyModules }); + return { src: implicitModulesTemplate({ ownModules, dependencyModules }), watches: [] }; } const implicitModulesTemplate = compile(` @@ -323,7 +362,14 @@ function orderAddons(depA: Package, depB: Package): number { return depAIdx - depBIdx; } -const renderCJSExternalShim = compile(` +function renderCJSExternalShim(params: { moduleName: string }): VirtualContentResult { + return { + src: renderCJSExternalShimTemplate(params), + watches: [], + }; +} + +const renderCJSExternalShimTemplate = compile(` module.exports = new Proxy({}, { get(target, prop) { diff --git a/packages/hbs-loader/src/index.ts b/packages/hbs-loader/src/index.ts index 2f10392f3..d12a5e942 100644 --- a/packages/hbs-loader/src/index.ts +++ b/packages/hbs-loader/src/index.ts @@ -8,7 +8,7 @@ export interface Options { }; } -export default function hbsLoader(this: LoaderContext, templateContent: string) { +export default function hbsLoader(this: LoaderContext, templateContent: string): string | undefined { let { compatModuleNaming } = this.getOptions(); try { return hbsToJS(templateContent, { filename: this.resourcePath, compatModuleNaming }); diff --git a/packages/vite/src/esbuild-resolver.ts b/packages/vite/src/esbuild-resolver.ts index 41018b231..beef04b4c 100644 --- a/packages/vite/src/esbuild-resolver.ts +++ b/packages/vite/src/esbuild-resolver.ts @@ -39,7 +39,7 @@ export function esBuildResolver(root = process.cwd()): EsBuildPlugin { }); build.onLoad({ namespace: 'embroider', filter: /./ }, ({ path }) => { - let src = virtualContent(path, resolverLoader.resolver); + let { src } = virtualContent(path, resolverLoader.resolver); if (!macrosConfig) { macrosConfig = readJSONSync( resolve(locateEmbroiderWorkingDir(root), 'rewritten-app', 'macros-config.json') @@ -82,7 +82,7 @@ export function esBuildResolver(root = process.cwd()): EsBuildPlugin { build.onLoad({ filter: /\.[jt]s$/ }, ({ path, namespace }) => { let src: string; if (namespace === 'embroider') { - src = virtualContent(path, resolverLoader.resolver); + src = virtualContent(path, resolverLoader.resolver).src; } else { src = readFileSync(path, 'utf8'); } diff --git a/packages/vite/src/resolver.ts b/packages/vite/src/resolver.ts index 89ff3f56e..ccb796859 100644 --- a/packages/vite/src/resolver.ts +++ b/packages/vite/src/resolver.ts @@ -1,16 +1,40 @@ import type { PluginContext, ResolveIdResult } from 'rollup'; -import type { Plugin } from 'vite'; +import type { Plugin, ViteDevServer } from 'vite'; import type { Resolution, ResolverFunction } from '@embroider/core'; import { virtualContent, ResolverLoader } from '@embroider/core'; import { RollupModuleRequest, virtualPrefix } from './request'; import assertNever from 'assert-never'; +import makeDebug from 'debug'; + +const debug = makeDebug('embroider:vite'); export function resolver(): Plugin { let resolverLoader = new ResolverLoader(process.cwd()); + let server: ViteDevServer; + let virtualDeps: Map = new Map(); return { name: 'embroider-resolver', enforce: 'pre', + + configureServer(s) { + server = s; + server.watcher.on('all', (_eventName, path) => { + for (let [id, watches] of virtualDeps) { + for (let watch of watches) { + if (path.startsWith(watch)) { + debug('Invalidate %s because %s', id, path); + server.moduleGraph.onFileChange(id); + let m = server.moduleGraph.getModuleById(id); + if (m) { + server.reloadModule(m); + } + } + } + } + }); + }, + async resolveId(source, importer, options) { let request = RollupModuleRequest.from(source, importer, options.custom); if (!request) { @@ -29,7 +53,10 @@ export function resolver(): Plugin { }, load(id) { if (id.startsWith(virtualPrefix)) { - return virtualContent(id.slice(virtualPrefix.length), resolverLoader.resolver); + let { src, watches } = virtualContent(id.slice(virtualPrefix.length), resolverLoader.resolver); + virtualDeps.set(id, watches); + server.watcher.add(watches); + return src; } }, }; diff --git a/packages/webpack/src/virtual-loader.ts b/packages/webpack/src/virtual-loader.ts index 2fc690567..67c9d4cf3 100644 --- a/packages/webpack/src/virtual-loader.ts +++ b/packages/webpack/src/virtual-loader.ts @@ -10,7 +10,7 @@ function setup(appRoot: string): ResolverLoader { return resolverLoader; } -export default function virtualLoader(this: LoaderContext) { +export default function virtualLoader(this: LoaderContext): string | undefined { if (typeof this.query === 'string' && this.query[0] === '?') { let params = new URLSearchParams(this.query); let filename = params.get('f'); @@ -20,7 +20,7 @@ export default function virtualLoader(this: LoaderContext) { } let { resolver } = setup(appRoot); this.resourcePath = filename; - return virtualContent(filename, resolver); + return virtualContent(filename, resolver).src; } throw new Error(`@embroider/webpack/src/virtual-loader received unexpected request: ${this.query}`); }