diff --git a/.changeset/olive-cameras-collect.md b/.changeset/olive-cameras-collect.md new file mode 100644 index 000000000000..4a5cdb5c658d --- /dev/null +++ b/.changeset/olive-cameras-collect.md @@ -0,0 +1,5 @@ +--- +'@qwik.dev/core': patch +--- + +CHORE: replace the `_hW` export in segments with a shared export `_task` in core. This opens up using QRLs from core. diff --git a/packages/qwik/handlers.mjs b/packages/qwik/handlers.mjs new file mode 100644 index 000000000000..de787e31b57a --- /dev/null +++ b/packages/qwik/handlers.mjs @@ -0,0 +1,9 @@ +/** + * This re-exports the QRL handlers so that they can be used as QRLs. + * + * In vite dev mode, this file is referenced directly. This ensures that the correct path to core is + * used so that there's only one instance of Qwik. + * + * Make sure that these handlers are listed in manifest.ts + */ +export { _task } from '@qwik.dev/core'; diff --git a/packages/qwik/package.json b/packages/qwik/package.json index 186a29452609..4889e800d24e 100644 --- a/packages/qwik/package.json +++ b/packages/qwik/package.json @@ -41,6 +41,7 @@ "./cli": { "require": "./dist/cli.cjs" }, + "./handlers.mjs": "./handlers.mjs", "./internal": { "types": "./core-internal.d.ts", "import": { @@ -144,6 +145,7 @@ "bindings", "build.d.ts", "cli.d.ts", + "handlers.mjs", "jsx-dev-runtime.d.ts", "jsx-runtime.d.ts", "loader.d.ts", diff --git a/packages/qwik/src/core/api.md b/packages/qwik/src/core/api.md index 95487f0d77a2..c8f59ea16c92 100644 --- a/packages/qwik/src/core/api.md +++ b/packages/qwik/src/core/api.md @@ -316,9 +316,6 @@ function h, PROPS extends {} = {} export { h as createElement } export { h } -// @internal -export const _hW: () => void; - // @internal @deprecated (undocumented) export const _IMMUTABLE: unique symbol; @@ -997,6 +994,9 @@ export interface SyncQRL extends QRL { resolved: TYPE; } +// @internal +export const _task: () => void; + // @public (undocumented) export interface TaskCtx { // (undocumented) diff --git a/packages/qwik/src/core/internal.ts b/packages/qwik/src/core/internal.ts index d173f70544b1..e5947a255075 100644 --- a/packages/qwik/src/core/internal.ts +++ b/packages/qwik/src/core/internal.ts @@ -1,7 +1,7 @@ export { _noopQrl, _noopQrlDEV, _regSymbol } from './shared/qrl/qrl'; export { _walkJSX } from './ssr/ssr-render-jsx'; export { _SharedContainer } from './shared/shared-container'; -export { _hW } from './use/use-task'; +export { scheduleTask as _task } from './use/use-task'; export { _wrapSignal, _wrapProp } from './signal/signal-utils'; export { _restProps } from './shared/utils/prop'; export { _IMMUTABLE } from './shared/utils/constants'; diff --git a/packages/qwik/src/core/shared/qrl/qrl-class.ts b/packages/qwik/src/core/shared/qrl/qrl-class.ts index 3deda85ec9b7..ac61c449e992 100644 --- a/packages/qwik/src/core/shared/qrl/qrl-class.ts +++ b/packages/qwik/src/core/shared/qrl/qrl-class.ts @@ -68,6 +68,7 @@ export type QRLInternalMethods = { export type QRLInternal = QRL & QRLInternalMethods; +// TODO remove refSymbol, it's not used export const createQRL = ( chunk: string | null, symbol: string, diff --git a/packages/qwik/src/core/use/use-task.ts b/packages/qwik/src/core/use/use-task.ts index b8a68a8e6020..48a9b59eac38 100644 --- a/packages/qwik/src/core/use/use-task.ts +++ b/packages/qwik/src/core/use/use-task.ts @@ -275,20 +275,15 @@ export const useRunTask = ( }; const getTaskHandlerQrl = (task: Task): QRL<(ev: Event) => void> => { - const taskQrl = task.$qrl$; const taskHandler = createQRL<(ev: Event) => void>( - taskQrl.$chunk$, - '_hW', - _hW, + null, + '_task', + scheduleTask, null, null, [task], - taskQrl.$symbol$ + null ); - // Needed for chunk lookup in dev mode - if (taskQrl.dev) { - taskHandler.dev = taskQrl.dev; - } return taskHandler; }; @@ -318,7 +313,7 @@ export const isTask = (value: any): value is Task => { * * @internal */ -export const _hW = () => { +export const scheduleTask = () => { const [task] = useLexicalScope<[Task]>(); const container = getDomContainer(task.$el$ as VNode); const type = task.$flags$ & TaskFlags.VISIBLE_TASK ? ChoreType.VISIBLE : ChoreType.TASK; diff --git a/packages/qwik/src/optimizer/src/manifest.ts b/packages/qwik/src/optimizer/src/manifest.ts index 286397b17769..c8a292649888 100644 --- a/packages/qwik/src/optimizer/src/manifest.ts +++ b/packages/qwik/src/optimizer/src/manifest.ts @@ -2,6 +2,10 @@ import type { OutputBundle } from 'rollup'; import { type NormalizedQwikPluginOptions } from './plugins/plugin'; import type { GlobalInjections, SegmentAnalysis, Path, QwikBundle, QwikManifest } from './types'; +// The handlers that are exported by the core package +// See handlers.mjs +const extraSymbols = new Set(['_task']); + // This is just the initial prioritization of the symbols and entries // at build time so there's less work during each SSR. However, SSR should // still further optimize the priorities depending on the user/document. @@ -270,7 +274,13 @@ export function generateManifestFromBundles( return canonPath(bundle.fileName); }; // We need to find our QRL exports - const qrlNames = new Set([...segments.map((h) => h.name)]); + const qrlNames = new Set(segments.map((h) => h.name)); + for (const symbol of extraSymbols) { + qrlNames.add(symbol); + } + const taskNames = new Set( + segments.filter((h) => /use[a-zA-Z]*Task(_\d+)?$/.test(h.displayName)).map((h) => h.name) + ); for (const outputBundle of Object.values(outputBundles)) { if (outputBundle.type !== 'chunk') { continue; @@ -281,25 +291,18 @@ export function generateManifestFromBundles( size: outputBundle.code.length, }; - let hasSymbols = false; - let hasHW = false; for (const symbol of outputBundle.exports) { if (qrlNames.has(symbol)) { // When not minifying we see both the entry and the segment file // The segment file will only have 1 export, we want the entry if (!manifest.mapping[symbol] || outputBundle.exports.length !== 1) { - hasSymbols = true; + if (taskNames.has(symbol)) { + bundle.isTask = true; + } manifest.mapping[symbol] = bundleFileName; } } - if (symbol === '_hW') { - hasHW = true; - } - } - if (hasSymbols && hasHW) { - bundle.isTask = true; } - const bundleImports = outputBundle.imports // Tree shaking might remove imports .filter((i) => outputBundle.code.includes(path.basename(i))) @@ -349,6 +352,19 @@ export function generateManifestFromBundles( loc: segment.loc, }; } + for (const symbol of extraSymbols) { + manifest.symbols[symbol] = { + origin: 'Qwik core', + displayName: symbol, + canonicalFilename: '', + hash: symbol, + ctxKind: 'function', + ctxName: symbol, + captures: false, + parent: null, + loc: [0, 0], + }; + } // To inspect the bundles, uncomment the following lines // and temporarily add the writeFileSync import from fs // writeFileSync( diff --git a/packages/qwik/src/optimizer/src/plugins/plugin.ts b/packages/qwik/src/optimizer/src/plugins/plugin.ts index 964134b42c1d..b0e7bf41a4dd 100644 --- a/packages/qwik/src/optimizer/src/plugins/plugin.ts +++ b/packages/qwik/src/optimizer/src/plugins/plugin.ts @@ -373,9 +373,11 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { }; let optimizer: Optimizer; + let shouldAddHandlers = false; const buildStart = async (_ctx: Rollup.PluginContext) => { debug(`buildStart()`, opts.buildMode, opts.scope, opts.target, opts.rootDir, opts.srcDir); optimizer = getOptimizer(); + shouldAddHandlers = !devServer; if (optimizer.sys.env === 'node' && opts.target === 'ssr' && opts.lint) { try { linter = await createLinter(optimizer.sys, opts.rootDir, opts.tsconfigFileNames); @@ -484,7 +486,29 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { id: QWIK_CLIENT_MANIFEST_ID, moduleSideEffects: false, }; + } else if (pathId.endsWith(QWIK_HANDLERS_ID)) { + debug(`resolveId(${count})`, 'Resolved', QWIK_HANDLERS_ID); + result = { + id: QWIK_HANDLERS_ID, + moduleSideEffects: false, + }; } else { + // If qwik core is loaded, also add the handlers + if (!isServer && shouldAddHandlers && id.endsWith('@qwik.dev/core')) { + shouldAddHandlers = false; + const key = await ctx.resolve('@qwik.dev/core/handlers.mjs', importerId, { + skipSelf: true, + }); + if (!key) { + throw new Error('Failed to resolve @qwik.dev/core/handlers.mjs'); + } + ctx.emitFile({ + id: key.id, + type: 'chunk', + preserveSignature: 'allow-extension', + }); + } + const qrlMatch = /^(?.*\.[mc]?[jt]sx?)_(?[^/]+)\.js(?$|\?.*$)/.exec(id) ?.groups as { parent: string; name: string; query: string } | undefined; @@ -566,6 +590,18 @@ export function createPlugin(optimizerOptions: OptimizerOptions = {}) { code: await getQwikServerManifestModule(isServer), }; } + /** + * In dev mode, we need a path to core for qrls. However, we don't know what that is. By + * re-exporting the core symbols, we let Vite provide the correct path to core and we prevent + * duplicate Qwik instances. + */ + if (id === QWIK_HANDLERS_ID) { + debug(`load(${count})`, QWIK_HANDLERS_ID, opts.buildMode); + return { + moduleSideEffects: false, + code: `export * from '@qwik.dev/core';`, + }; + } // QRL segments const parsedId = parseId(id); @@ -978,6 +1014,8 @@ export const QWIK_CORE_SERVER = '@qwik.dev/core/server'; export const QWIK_CLIENT_MANIFEST_ID = '@qwik-client-manifest'; +export const QWIK_HANDLERS_ID = '@qwik-handlers'; + export const SRC_DIR_DEFAULT = 'src'; export const CLIENT_OUT_DIR = 'dist'; diff --git a/packages/qwik/src/optimizer/src/plugins/vite-dev-server.ts b/packages/qwik/src/optimizer/src/plugins/vite-dev-server.ts index 212b5978f7b6..7d802d87f8b0 100644 --- a/packages/qwik/src/optimizer/src/plugins/vite-dev-server.ts +++ b/packages/qwik/src/optimizer/src/plugins/vite-dev-server.ts @@ -10,7 +10,7 @@ import clickToComponent from './click-to-component.html?raw'; import errorHost from './error-host.html?raw'; import imageDevTools from './image-size-runtime.html?raw'; import perfWarning from './perf-warning.html?raw'; -import { type NormalizedQwikPluginOptions, parseId } from './plugin'; +import { type NormalizedQwikPluginOptions, parseId, QWIK_HANDLERS_ID } from './plugin'; import type { QwikViteDevResponse } from './vite'; import { VITE_ERROR_OVERLAY_STYLES } from './vite-error'; import { formatError } from './vite-utils'; @@ -37,6 +37,10 @@ function createSymbolMapper(base: string): SymbolMapperFn { return [symbolName, '']; } if (!parent) { + // Core symbols + if (symbolName.startsWith('_')) { + return [symbolName, `${base}${QWIK_HANDLERS_ID}`]; + } console.error( 'qwik vite-dev-server symbolMapper: unknown qrl requested without parent:', symbolName diff --git a/packages/qwik/src/qwikloader.ts b/packages/qwik/src/qwikloader.ts index 7bb3dd588b97..c96484f2dc6c 100644 --- a/packages/qwik/src/qwikloader.ts +++ b/packages/qwik/src/qwikloader.ts @@ -25,6 +25,7 @@ export const qwikLoader = ( const roots = new Set([doc]); // Some shortenings for minification + // TODO use more, like hasAttribute const replace = 'replace'; const forEach = 'forEach'; const target = 'target';