Skip to content

Commit 7a5d4a8

Browse files
committed
wip router, needs string render
1 parent 9c5c198 commit 7a5d4a8

File tree

3 files changed

+221
-119
lines changed

3 files changed

+221
-119
lines changed
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import type { Render } from '@qwik.dev/core/server';
2+
import type { DocumentHeadValue, RendererOptions, RendererOutputOptions } from '@qwik.dev/router';
3+
import type { Connect, ModuleNode, Plugin, ViteDevServer } from 'vite';
4+
import type { BuildContext } from '../types';
5+
import { formatError } from './format-error';
6+
import { build } from '../build';
7+
8+
export const makeRouterDevMiddleware =
9+
(server: ViteDevServer, ctx: BuildContext): Connect.NextHandleFunction =>
10+
async (req, res, next) => {
11+
// TODO more flexible entry points, like importing `render` from `src/server`
12+
const mod = (await server.ssrLoadModule('src/entry.ssr')) as { default: Render };
13+
if (!mod.default) {
14+
console.error('No default export found in src/entry.ssr');
15+
return next();
16+
}
17+
const renderer = mod.default;
18+
if (ctx!.isDirty) {
19+
await build(ctx!);
20+
ctx!.isDirty = false;
21+
}
22+
23+
// entry.ts files
24+
const entry = ctx!.entries.find((e) => req.url === `${server.config.base}${e.chunkFileName}`);
25+
if (entry) {
26+
const entryContents = await server.transformRequest(
27+
`/@fs${entry.filePath.startsWith('/') ? '' : '/'}${entry.filePath}`
28+
);
29+
30+
if (entryContents) {
31+
res.setHeader('Content-Type', 'text/javascript');
32+
res.end(entryContents.code);
33+
} else {
34+
next();
35+
}
36+
return;
37+
}
38+
// in dev mode, serve a placeholder service worker
39+
if (req.url === `${server.config.base}service-worker.js`) {
40+
res.setHeader('Content-Type', 'text/javascript');
41+
res.end(
42+
`/* Qwik Router Dev Service Worker */` +
43+
`self.addEventListener('install', () => self.skipWaiting());` +
44+
`self.addEventListener('activate', (ev) => ev.waitUntil(self.clients.claim()));`
45+
);
46+
return;
47+
}
48+
49+
const documentHead = {
50+
/**
51+
* Vite normally injects imported CSS files into the HTML, but we render our own HTML so we
52+
* need to add them manually.
53+
*
54+
* Note: It's possible that new CSS files are created during render, we can't find those here.
55+
* For now, we ignore this possibility, it would mean needing a callback at the end of the
56+
* render before the `<body>` is closed.
57+
*/
58+
links: getCssUrls(server).map((url) => {
59+
return { rel: 'stylesheet', href: url };
60+
}),
61+
scripts: [
62+
// Vite normally injects this
63+
{ type: 'module', src: '/@vite/client' },
64+
],
65+
} satisfies DocumentHeadValue;
66+
67+
// Grab tags from other plugins
68+
await getExtraHeadContent(server, documentHead);
69+
70+
// Now we can stream the render
71+
const { createQwikRouter } = (await server.ssrLoadModule(
72+
'@qwik.dev/router/middleware/node'
73+
)) as typeof import('@qwik.dev/router/middleware/node');
74+
try {
75+
const render = (async (opts: RendererOptions) => {
76+
return await renderer({
77+
...opts,
78+
serverData: { ...opts.serverData, documentHead },
79+
} as RendererOutputOptions as any);
80+
}) as Render;
81+
const { router, staticFile, notFound } = createQwikRouter({ render });
82+
staticFile(req, res, () => {
83+
router(req, res, () => {
84+
notFound(req, res, next);
85+
});
86+
});
87+
} catch (e: any) {
88+
if (e instanceof Error) {
89+
server.ssrFixStacktrace(e);
90+
formatError(e);
91+
}
92+
next(e);
93+
return;
94+
}
95+
};
96+
97+
const CSS_EXTENSIONS = ['.css', '.scss', '.sass', '.less', '.styl', '.stylus'];
98+
const JS_EXTENSIONS = /\.[mc]?[tj]sx?$/;
99+
const isCssPath = (url: string) => CSS_EXTENSIONS.some((ext) => url.endsWith(ext));
100+
101+
function getCssUrls(server: ViteDevServer) {
102+
const cssModules = new Set<ModuleNode>();
103+
const cssImportedByCSS = new Set<string>();
104+
105+
Array.from(server.moduleGraph.fileToModulesMap.entries()).forEach(([_name, modules]) => {
106+
modules.forEach((mod) => {
107+
const [pathId, query] = mod.url.split('?');
108+
109+
if (!query && isCssPath(pathId)) {
110+
const isEntryCSS = mod.importers.size === 0;
111+
const hasCSSImporter = Array.from(mod.importers).some((importer) => {
112+
const importerPath = (importer as typeof mod).url || (importer as typeof mod).file;
113+
114+
const isCSS = importerPath && isCssPath(importerPath);
115+
116+
if (isCSS && mod.url) {
117+
cssImportedByCSS.add(mod.url);
118+
}
119+
120+
return isCSS;
121+
});
122+
123+
const hasJSImporter = Array.from(mod.importers).some((importer) => {
124+
const importerPath = (importer as typeof mod).url || (importer as typeof mod).file;
125+
return importerPath && JS_EXTENSIONS.test(importerPath);
126+
});
127+
128+
if ((isEntryCSS || hasJSImporter) && !hasCSSImporter && !cssImportedByCSS.has(mod.url)) {
129+
cssModules.add(mod);
130+
}
131+
}
132+
});
133+
});
134+
return [...cssModules].map(
135+
({ url, lastHMRTimestamp }) => `${url}${lastHMRTimestamp ? `?t=${lastHMRTimestamp}` : ''}`
136+
);
137+
}
138+
139+
async function getExtraHeadContent(server: ViteDevServer, documentHead: Record<string, unknown[]>) {
140+
const fakeHTML = '<!DOCTYPE html><html><head>HEAD</head><body>BODY</body></html>';
141+
for (const { name: pluginName, transformIndexHtml } of server.config.plugins) {
142+
const handler =
143+
transformIndexHtml && 'handler' in transformIndexHtml
144+
? transformIndexHtml.handler
145+
: transformIndexHtml;
146+
if (typeof handler === 'function') {
147+
const result = await (handler.call({} as any, fakeHTML, {
148+
server,
149+
path: '/',
150+
filename: 'index.html',
151+
command: 'serve',
152+
}) as ReturnType<Extract<Plugin['transformIndexHtml'], Function>>);
153+
if (result) {
154+
if (typeof result === 'string' || ('html' in result && result.html)) {
155+
console.warn(
156+
`qwik-router: plugin ${pluginName} returned a string for transformIndexHtml, unsupported by qwik-router:`,
157+
result
158+
);
159+
} else {
160+
const tags = 'tags' in result ? result.tags : result;
161+
if (!Array.isArray(tags)) {
162+
console.warn(
163+
`qwik-router: plugin ${pluginName} returned a non-array for tags in transformIndexHtml:`,
164+
result
165+
);
166+
} else {
167+
// Note that we don't support the injectTo option
168+
for (const { tag, attrs, children } of tags) {
169+
if (!attrs && !children) {
170+
console.warn(
171+
`qwik-router: plugin ${pluginName} returned a tag with no attrs in transformIndexHtml:`,
172+
tag,
173+
result
174+
);
175+
continue;
176+
}
177+
const collectionName =
178+
tag === 'link'
179+
? 'links'
180+
: tag === 'script'
181+
? 'scripts'
182+
: tag === 'style'
183+
? 'styles'
184+
: tag === 'meta'
185+
? 'meta'
186+
: null;
187+
if (collectionName) {
188+
if (children && typeof children !== 'string') {
189+
console.warn(
190+
`qwik-router: plugin ${pluginName} returned a tag with children that is not a string in transformIndexHtml:`,
191+
tag,
192+
result
193+
);
194+
} else {
195+
(documentHead[collectionName] ||= []).push(
196+
children ? { ...attrs, dangerouslySetInnerHTML: children } : attrs
197+
);
198+
}
199+
} else {
200+
console.warn(
201+
`qwik-router: plugin ${pluginName} returned an unsupported tag in transformIndexHtml:`,
202+
tag,
203+
result
204+
);
205+
}
206+
}
207+
}
208+
}
209+
}
210+
}
211+
}
212+
}

packages/qwik-router/src/buildtime/vite/plugin.ts

Lines changed: 3 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import swRegister from '@qwik-router-sw-register-build';
22
import type { QwikVitePlugin } from '@qwik.dev/core/optimizer';
3-
import type { Render } from '@qwik.dev/core/server';
4-
import type { DocumentHeadValue, RendererOptions, RendererOutputOptions } from '@qwik.dev/router';
53
import fs from 'node:fs';
64
import { basename, extname, join, resolve } from 'node:path';
75
import type { Plugin, PluginOption, Rollup, UserConfig } from 'vite';
@@ -23,7 +21,7 @@ import type {
2321
QwikRouterVitePluginOptions,
2422
} from './types';
2523
import { validatePlugin } from './validate-plugin';
26-
import { formatError } from './format-error';
24+
import { makeRouterDevMiddleware } from './dev-middleware';
2725

2826
export const QWIK_ROUTER_CONFIG_ID = '@qwik-router-config';
2927
const QWIK_ROUTER_ENTRIES_ID = '@qwik-router-entries';
@@ -51,8 +49,6 @@ function qwikRouterPlugin(userOpts?: QwikRouterVitePluginOptions): any {
5149
let ssrFormat: 'esm' | 'cjs' = 'esm';
5250
let outDir: string | null = null;
5351

54-
globalThis.__qwikRouterNew = true;
55-
5652
const api: QwikRouterPluginApi = {
5753
getBasePathname: () => ctx?.opts.basePathname ?? '/',
5854
getRoutes: () => {
@@ -172,121 +168,9 @@ function qwikRouterPlugin(userOpts?: QwikRouterVitePluginOptions): any {
172168
}
173169
});
174170

171+
// this callback runs after all other middlewares have been added, so we can SSR as the last middleware
175172
return () => {
176-
// this hits only when Vite hasn't handled client assets
177-
server.middlewares.use(async (req, res, next) => {
178-
// TODO more flexible entry points, like importing `render` from `src/server`
179-
const mod = (await server.ssrLoadModule('src/entry.ssr')) as { default: Render };
180-
if (!mod.default) {
181-
console.error('No default export found in src/entry.ssr');
182-
return next();
183-
}
184-
if (ctx!.isDirty) {
185-
await build(ctx!);
186-
ctx!.isDirty = false;
187-
}
188-
189-
// entry.ts files
190-
const entry = ctx!.entries.find(
191-
(e) => req.url === `${server.config.base}${e.chunkFileName}`
192-
);
193-
if (entry) {
194-
const entryContents = await server.transformRequest(
195-
`/@fs${entry.filePath.startsWith('/') ? '' : '/'}${entry.filePath}`
196-
);
197-
198-
if (entryContents) {
199-
res.setHeader('Content-Type', 'text/javascript');
200-
res.end(entryContents.code);
201-
} else {
202-
next();
203-
}
204-
return;
205-
}
206-
// in dev mode, serve a placeholder service worker
207-
if (req.url === `${server.config.base}service-worker.js`) {
208-
res.setHeader('Content-Type', 'text/javascript');
209-
res.end(
210-
`/* Qwik Router Dev Service Worker */` +
211-
`self.addEventListener('install', () => self.skipWaiting());` +
212-
`self.addEventListener('activate', (ev) => ev.waitUntil(self.clients.claim()));`
213-
);
214-
return;
215-
}
216-
217-
const documentHead: DocumentHeadValue = {
218-
// Vite normally injects these
219-
links: [...server.moduleGraph.idToModuleMap.keys()]
220-
.filter((id) => id.endsWith('.css'))
221-
.map((id) => {
222-
const { url } = server.moduleGraph.idToModuleMap.get(id)!;
223-
return {
224-
key: id,
225-
rel: 'stylesheet',
226-
href: url,
227-
};
228-
}),
229-
scripts: [
230-
{
231-
key: 'vite-dev-client',
232-
props: {
233-
type: 'module',
234-
src: '/@vite/client',
235-
},
236-
},
237-
],
238-
};
239-
240-
// Grab tags from other plugins
241-
const fakeHTML = '<!DOCTYPE html><html><head>HEAD</head><body>BODY</body></html>';
242-
for (const plugin of server.config.plugins) {
243-
const handler =
244-
(plugin.transformIndexHtml as any)?.handler || plugin.transformIndexHtml;
245-
if (typeof handler === 'function') {
246-
const result = await (handler(fakeHTML, {}) as ReturnType<
247-
Extract<Plugin['transformIndexHtml'], Function>
248-
>);
249-
if (result) {
250-
if (typeof result === 'string' || 'html' in result) {
251-
console.warn(
252-
'plugin',
253-
plugin.name,
254-
'returned a string for transformIndexHtml, which is not supported by qwik-router'
255-
);
256-
} else {
257-
// TODO add these tags to the document
258-
console.warn('not implemented yet: adding these tags to the document', result);
259-
}
260-
}
261-
}
262-
}
263-
264-
const render = (async (opts: RendererOptions) => {
265-
return await mod.default({
266-
...opts,
267-
serverData: { ...opts.serverData, documentHead },
268-
} as RendererOutputOptions as any);
269-
}) as Render;
270-
271-
const { createQwikRouter } = (await server.ssrLoadModule(
272-
'@qwik.dev/router/middleware/node'
273-
)) as typeof import('@qwik.dev/router/middleware/node');
274-
try {
275-
const { router, staticFile, notFound } = createQwikRouter({ render });
276-
staticFile(req, res, () => {
277-
router(req, res, () => {
278-
notFound(req, res, next);
279-
});
280-
});
281-
} catch (e: any) {
282-
if (e instanceof Error) {
283-
server.ssrFixStacktrace(e);
284-
formatError(e);
285-
}
286-
next(e);
287-
return;
288-
}
289-
});
173+
server.middlewares.use(makeRouterDevMiddleware(server, ctx!));
290174
};
291175
},
292176

packages/qwik/src/core/shared/shared-serialization.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ class DeserializationHandler implements ProxyHandler<object> {
128128

129129
const container = this.$container$;
130130
let propValue = allocate(container, typeId, value);
131+
132+
// This should be here but it makes tests fail
133+
// Reflect.set(target, property, propValue);
134+
// this.$data$[idx] = undefined;
135+
// this.$data$[idx + 1] = propValue;
136+
131137
/** We stored the reference, so now we can inflate, allowing cycles. */
132138
if (typeId >= TypeIds.Error) {
133139
propValue = inflate(container, propValue, typeId, value);

0 commit comments

Comments
 (0)