@pajecawav/yamf 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/dist/client/index.d.ts +1 -0
  3. package/dist/client/index.js +1 -0
  4. package/dist/components/Head.d.ts +8 -0
  5. package/dist/components/Head.d.ts.map +1 -0
  6. package/dist/components/Head.js +9 -0
  7. package/dist/components/Head.js.map +1 -0
  8. package/dist/context/ssr.d.ts +13 -0
  9. package/dist/context/ssr.d.ts.map +1 -0
  10. package/dist/context/ssr.js +10 -0
  11. package/dist/context/ssr.js.map +1 -0
  12. package/dist/hooks/useEvent.d.ts +8 -0
  13. package/dist/hooks/useEvent.d.ts.map +1 -0
  14. package/dist/hooks/useEvent.js +11 -0
  15. package/dist/hooks/useEvent.js.map +1 -0
  16. package/dist/hooks/useHead.d.ts +13 -0
  17. package/dist/hooks/useHead.d.ts.map +1 -0
  18. package/dist/hooks/useHead.js +15 -0
  19. package/dist/hooks/useHead.js.map +1 -0
  20. package/dist/index.d.ts +8 -0
  21. package/dist/index.js +6 -0
  22. package/dist/island/client.js +59 -0
  23. package/dist/island/client.js.map +1 -0
  24. package/dist/island/types.d.ts +8 -0
  25. package/dist/island/types.d.ts.map +1 -0
  26. package/dist/page.d.ts +24 -0
  27. package/dist/page.d.ts.map +1 -0
  28. package/dist/page.js +51 -0
  29. package/dist/page.js.map +1 -0
  30. package/dist/server/entry.d.mts +14 -0
  31. package/dist/server/entry.d.mts.map +1 -0
  32. package/dist/server/entry.mjs +38 -0
  33. package/dist/server/entry.mjs.map +1 -0
  34. package/dist/server/index.d.mts +3 -0
  35. package/dist/server/index.mjs +3 -0
  36. package/dist/server/island/server.d.mts +23 -0
  37. package/dist/server/island/server.d.mts.map +1 -0
  38. package/dist/server/island/server.mjs +22 -0
  39. package/dist/server/island/server.mjs.map +1 -0
  40. package/dist/server/island/types.d.mts +5 -0
  41. package/dist/server/island/types.d.mts.map +1 -0
  42. package/dist/server/shared/assets.d.mts +14 -0
  43. package/dist/server/shared/assets.d.mts.map +1 -0
  44. package/dist/shared/assets.d.ts +17 -0
  45. package/dist/shared/assets.d.ts.map +1 -0
  46. package/dist/vite/index.d.mts +11 -0
  47. package/dist/vite/index.d.mts.map +1 -0
  48. package/dist/vite/index.mjs +54 -0
  49. package/dist/vite/index.mjs.map +1 -0
  50. package/dist/vite/islands.mjs +85 -0
  51. package/dist/vite/islands.mjs.map +1 -0
  52. package/dist/vite/shared/utils.mjs +10 -0
  53. package/dist/vite/shared/utils.mjs.map +1 -0
  54. package/dist/vite/virtual-assets.mjs +25 -0
  55. package/dist/vite/virtual-assets.mjs.map +1 -0
  56. package/dist/vite/virtual-pages.mjs +35 -0
  57. package/dist/vite/virtual-pages.mjs.map +1 -0
  58. package/dist/vite/virtual-template.mjs +45 -0
  59. package/dist/vite/virtual-template.mjs.map +1 -0
  60. package/package.json +84 -0
  61. package/src/client/index.ts +1 -0
  62. package/src/components/Head.tsx +8 -0
  63. package/src/context/ssr.ts +17 -0
  64. package/src/hooks/useEvent.tsx +13 -0
  65. package/src/hooks/useHead.tsx +27 -0
  66. package/src/index.ts +12 -0
  67. package/src/island/client.tsx +92 -0
  68. package/src/island/server.tsx +51 -0
  69. package/src/island/types.ts +5 -0
  70. package/src/page.tsx +92 -0
  71. package/src/server/entry.tsx +80 -0
  72. package/src/server/index.ts +4 -0
  73. package/src/shared/assets.ts +10 -0
  74. package/src/shared/utils.ts +5 -0
  75. package/src/virtual.d.ts +14 -0
  76. package/src/vite/index.ts +83 -0
  77. package/src/vite/islands.ts +229 -0
  78. package/src/vite/virtual-assets.ts +30 -0
  79. package/src/vite/virtual-pages.ts +40 -0
  80. package/src/vite/virtual-template.ts +62 -0
package/src/page.tsx ADDED
@@ -0,0 +1,92 @@
1
+ import type { Child, FC, PropsWithChildren } from "hono/jsx";
2
+ import { Fragment } from "hono/jsx";
3
+ import { renderToReadableStream } from "hono/jsx/streaming";
4
+ import { Hono } from "hono/tiny";
5
+ import type { EventHandlerRequest, EventHandlerResponse, H3Event } from "nitro/h3";
6
+ import { HTTPResponse, withServerTiming } from "nitro/h3";
7
+ import type { Unhead } from "unhead/server";
8
+ import { transformHtmlTemplate } from "unhead/server";
9
+ import { createStreamableHead, wrapStream } from "unhead/stream/server";
10
+ import type { ResolvableHead, ResolvableLink } from "unhead/types";
11
+ import { clientAssets } from "virtual:yamf:assets";
12
+ import { template } from "virtual:yamf:template";
13
+ import { SSRContext } from "./context/ssr";
14
+ import type { ImportAssetsResult } from "./shared/assets";
15
+
16
+ export type PageHandler = (
17
+ event: H3Event<EventHandlerRequest>,
18
+ params: {
19
+ serverAssets?: ImportAssetsResult;
20
+ head?: ResolvableHead;
21
+ Layout?: FC<PropsWithChildren>;
22
+ },
23
+ ) => EventHandlerResponse;
24
+
25
+ export type PageRenderer = (
26
+ event: H3Event<EventHandlerRequest>,
27
+ params: { head: Unhead },
28
+ ) => HTTPResponse | Promise<HTTPResponse> | Child | Promise<Child>;
29
+
30
+ interface DefinePageOptions {
31
+ render: PageRenderer;
32
+ stream?: boolean;
33
+ Layout?: FC<PropsWithChildren>;
34
+ }
35
+
36
+ export const definePage = (options: DefinePageOptions): PageHandler => {
37
+ return async (event, { serverAssets, head: headInit, Layout = options.Layout ?? Fragment }) => {
38
+ const assets = serverAssets ? clientAssets.merge(serverAssets) : clientAssets;
39
+
40
+ const { head } = createStreamableHead({ init: [headInit] });
41
+
42
+ head.push({
43
+ link: [
44
+ ...assets.js.map((attrs): ResolvableLink => ({ rel: "modulepreload", ...attrs })),
45
+ ...assets.css.map((attrs): ResolvableLink => ({ rel: "stylesheet", ...attrs })),
46
+ ],
47
+ script: [{ type: "module", src: assets.entry }],
48
+ });
49
+
50
+ const content = await options.render(event, { head });
51
+
52
+ if (content instanceof HTTPResponse) {
53
+ return content;
54
+ }
55
+
56
+ const App = async () => (
57
+ <SSRContext value={{ head, event }}>
58
+ <Layout>{content}</Layout>
59
+ </SSRContext>
60
+ );
61
+
62
+ const responseInit = {
63
+ headers: {
64
+ "Content-Type": "text/html; charset=utf-8",
65
+ },
66
+ };
67
+
68
+ if (!options.stream) {
69
+ // TODO: figure out how expensive this is
70
+ return new Hono()
71
+ .get("/", async c =>
72
+ withServerTiming(event, "#render", async () => {
73
+ const response = await c.html(<App />);
74
+
75
+ let html = await response.text();
76
+
77
+ html = transformHtmlTemplate(
78
+ head,
79
+ template.replace("<!--ssr-outlet-->", html ?? ""),
80
+ );
81
+
82
+ return new Response(html, responseInit);
83
+ }),
84
+ )
85
+ .request("/");
86
+ }
87
+
88
+ const stream = wrapStream(head, renderToReadableStream(<App />), template);
89
+
90
+ return new Response(stream, responseInit);
91
+ };
92
+ };
@@ -0,0 +1,80 @@
1
+ import type { PageHandler } from "#/page";
2
+ import type { FC, PropsWithChildren } from "hono/jsx";
3
+ import type { EventHandlerRequest, EventHandlerWithFetch, H3Event } from "nitro/h3";
4
+ import { defineHandler, HTTPError, writeEarlyHints } from "nitro/h3";
5
+ import path from "node:path";
6
+ import { addRoute, createRouter, findRoute } from "rou3";
7
+ import { withLeadingSlash, withoutTrailingSlash } from "ufo";
8
+ import type { ResolvableHead } from "unhead/types";
9
+ import { clientAssets } from "virtual:yamf:assets";
10
+ import { pages, assets as serverAssets } from "virtual:yamf:pages";
11
+
12
+ interface Route {
13
+ handler: () => Promise<PageHandler>;
14
+ serverAssets?: ImportAssetsResult;
15
+ }
16
+
17
+ const router = createRouter<Route>();
18
+
19
+ for (const [relativePath, handler] of Object.entries(pages).toSorted((a, b) =>
20
+ a[0].localeCompare(b[0]),
21
+ )) {
22
+ const ext = path.extname(relativePath);
23
+
24
+ let route = path.relative("/src/pages", relativePath);
25
+
26
+ if (ext.length) {
27
+ route = route.slice(0, -ext.length);
28
+ }
29
+
30
+ route =
31
+ route
32
+ .replace(/\.[A-Za-z]+$/, "")
33
+ .replace(/\(([^(/\\]+)\)[/\\]/g, "")
34
+ .replace(/\[\.{3}]/g, "**")
35
+ .replace(/\[\.{3}([^\]]+)]/g, (_, p: string) => "**:" + p.replace(/[^\w-]/g, "_"))
36
+ .replace(/\[([^/\]]+)]/g, (_, p: string) => ":" + p.replace(/[^\w-]/g, "_"))
37
+ .replace(/(\/|^)index$/, "") || "/";
38
+
39
+ route = withLeadingSlash(withoutTrailingSlash(route));
40
+
41
+ addRoute(router, "GET", route, {
42
+ handler,
43
+ serverAssets: serverAssets[relativePath],
44
+ });
45
+ }
46
+
47
+ export type ServerEntry = EventHandlerWithFetch<EventHandlerRequest, Promise<unknown>>;
48
+
49
+ export interface DefineServerEntryOptions {
50
+ head?: ResolvableHead | ((event: H3Event<EventHandlerRequest>) => ResolvableHead);
51
+ Layout?: FC<PropsWithChildren>;
52
+ }
53
+
54
+ export const defineServerEntry = (options?: DefineServerEntryOptions): ServerEntry => {
55
+ return defineHandler(async event => {
56
+ const route = findRoute(router, "GET", event.url.pathname);
57
+
58
+ if (!route) {
59
+ throw new HTTPError("Not found", { status: 404 });
60
+ }
61
+
62
+ const { handler, serverAssets } = route.data;
63
+ event.context.params = route.params;
64
+
65
+ const assets = serverAssets ? serverAssets.merge(clientAssets) : clientAssets;
66
+
67
+ await writeEarlyHints(event, {
68
+ link: [
69
+ ...assets.js.map(script => `<${script.href}>; rel=modulepreload`),
70
+ ...assets.css.map(style => `<${style.href}>; rel=preload; as=style`),
71
+ ],
72
+ });
73
+
74
+ return (await handler())(event, {
75
+ serverAssets,
76
+ head: typeof options?.head === "function" ? options.head(event) : options?.head,
77
+ Layout: options?.Layout,
78
+ });
79
+ });
80
+ };
@@ -0,0 +1,4 @@
1
+ export { createIsland } from "#/island/server";
2
+
3
+ export { defineServerEntry } from "./entry";
4
+ export type { DefineServerEntryOptions, ServerEntry } from "./entry";
@@ -0,0 +1,10 @@
1
+ // https://github.com/nitrojs/nitro/blob/bfbb207c720ce10ec7ad7887c7a8269d493eec55/lib/vite.types.d.mts
2
+ export type ImportAssetsResult = ImportAssetsResultRaw & {
3
+ merge(...args: ImportAssetsResultRaw[]): ImportAssetsResult;
4
+ };
5
+
6
+ export type ImportAssetsResultRaw = {
7
+ entry?: string;
8
+ js: { href: string }[];
9
+ css: { href: string; "data-vite-dev-id"?: string }[];
10
+ };
@@ -0,0 +1,5 @@
1
+ export const js = (str: TemplateStringsArray, ...args: unknown[]): string => {
2
+ return str.reduce((acc, cur, i) => {
3
+ return acc + cur + String(args[i]);
4
+ }, "");
5
+ };
@@ -0,0 +1,14 @@
1
+ declare module "virtual:yamf:assets" {
2
+ // eslint-disable-next-line @typescript-eslint/consistent-type-imports
3
+ export const clientAssets: import("./shared/assets").ImportAssetsResult;
4
+ }
5
+
6
+ declare module "virtual:yamf:pages" {
7
+ export const pages: Record<string, () => Promise<PageHandler>>;
8
+
9
+ export const assets: Record<string, ImportAssetsResult>;
10
+ }
11
+
12
+ declare module "virtual:yamf:template" {
13
+ export const template: string;
14
+ }
@@ -0,0 +1,83 @@
1
+ import type { NitroPluginConfig } from "nitro/vite";
2
+ import { nitro } from "nitro/vite";
3
+ import { existsSync } from "node:fs";
4
+ import type { EnvironmentOptions, PluginOption } from "vite";
5
+ import { islands } from "./islands";
6
+ import { virtualAssets } from "./virtual-assets";
7
+ import { virtualPages } from "./virtual-pages";
8
+ import { virtualTemplate } from "./virtual-template";
9
+
10
+ export interface YamfOptions {
11
+ nitro?: NitroPluginConfig;
12
+ }
13
+
14
+ const yamf = (options?: YamfOptions): PluginOption[] => {
15
+ const plugins: PluginOption[] = [];
16
+
17
+ // TODO: extendable config
18
+ plugins.push({
19
+ name: "yamf:config",
20
+ config() {
21
+ const ssrEnv: EnvironmentOptions = {
22
+ build: {
23
+ // TODO: figure out if this should be true
24
+ cssCodeSplit: false,
25
+ rollupOptions: {
26
+ input: "./src/server.tsx",
27
+ },
28
+ },
29
+ };
30
+
31
+ return {
32
+ optimizeDeps: {
33
+ include: [
34
+ "hono",
35
+ "hono/jsx/dom/client",
36
+ "hono/jsx/jsx-runtime",
37
+ "devalue",
38
+ "ufo",
39
+ ],
40
+ },
41
+ environments: {
42
+ ...(existsSync("./src/server.tsx") ? { ssr: ssrEnv } : {}),
43
+ client: {
44
+ build: {
45
+ rolldownOptions: {
46
+ input: "./src/client/index.ts",
47
+ },
48
+ },
49
+ },
50
+ },
51
+ };
52
+ },
53
+ });
54
+
55
+ plugins.push(islands());
56
+
57
+ plugins.push(virtualAssets());
58
+ plugins.push(virtualPages());
59
+ plugins.push(virtualTemplate());
60
+
61
+ plugins.push(
62
+ nitro({
63
+ serverDir: "./src",
64
+ renderer: false,
65
+ ...options?.nitro,
66
+ compressPublicAssets: {
67
+ gzip: true,
68
+ brotli: true,
69
+ },
70
+ publicAssets: [
71
+ {
72
+ baseURL: "assets",
73
+ dir: "./public/assets",
74
+ maxAge: 365 * 24 * 60 * 60,
75
+ },
76
+ ],
77
+ }),
78
+ );
79
+
80
+ return plugins;
81
+ };
82
+
83
+ export default yamf;
@@ -0,0 +1,229 @@
1
+ // reference https://github.com/hi-ogawa/vite-plugin-fullstack/blob/28e9540a68529c58842e9a3bf17d2193a065d524/examples/island/src/framework/island/plugin.ts
2
+ import { generate } from "@babel/generator";
3
+ import { parse } from "@babel/parser";
4
+ import _traverse from "@babel/traverse";
5
+ import {
6
+ callExpression,
7
+ exportDefaultDeclaration,
8
+ exportNamedDeclaration,
9
+ functionDeclaration,
10
+ functionExpression,
11
+ identifier,
12
+ importDeclaration,
13
+ importDefaultSpecifier,
14
+ importNamespaceSpecifier,
15
+ memberExpression,
16
+ stringLiteral,
17
+ variableDeclaration,
18
+ variableDeclarator,
19
+ } from "@babel/types";
20
+ import type { Plugin } from "vite";
21
+
22
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
23
+ // @ts-ignore
24
+ const traverse = (_traverse.default as typeof _traverse) ?? _traverse;
25
+
26
+ const ISLAND_REGEX = /\.island\.(j|t)sx?$/;
27
+
28
+ export const islands = (): Plugin[] => {
29
+ return [
30
+ {
31
+ name: "yamf:islands",
32
+ // enforce: "pre",
33
+ transform: {
34
+ filter: { id: ISLAND_REGEX },
35
+ handler(code, id) {
36
+ if (this.environment.name !== "ssr") {
37
+ return;
38
+ }
39
+
40
+ const seenExports = new Set<string>();
41
+
42
+ const ast = parse(code, {
43
+ sourceType: "module",
44
+ plugins: ["typescript", "jsx"],
45
+ });
46
+
47
+ traverse(ast, {
48
+ Program(path) {
49
+ // prepends server island runtime
50
+ // import * as __runtime from "yamf/server";
51
+ path.unshiftContainer(
52
+ "body",
53
+ importDeclaration(
54
+ [importNamespaceSpecifier(identifier("__runtime"))],
55
+ stringLiteral("@pajecawav/yamf/server"),
56
+ ),
57
+ );
58
+
59
+ // prepends asset imports for the island:
60
+ // import __assets from "MODULE_ID?assets=client";
61
+ path.unshiftContainer(
62
+ "body",
63
+ importDeclaration(
64
+ [importDefaultSpecifier(identifier("__assets"))],
65
+ stringLiteral(`${id}?assets=client`),
66
+ ),
67
+ );
68
+ },
69
+ ExportDefaultDeclaration(path) {
70
+ const declarationType = path.node.declaration.type;
71
+
72
+ if (
73
+ !(
74
+ declarationType === "FunctionDeclaration" ||
75
+ declarationType === "FunctionExpression" ||
76
+ declarationType === "ArrowFunctionExpression"
77
+ )
78
+ ) {
79
+ return;
80
+ }
81
+
82
+ const originalFunction =
83
+ path.node.declaration.type === "FunctionExpression" ||
84
+ path.node.declaration.type === "ArrowFunctionExpression"
85
+ ? path.node.declaration
86
+ : functionExpression(
87
+ null,
88
+ path.node.declaration.params,
89
+ path.node.declaration.body,
90
+ undefined,
91
+ path.node.declaration.async,
92
+ );
93
+
94
+ const islandIdentifier = identifier("__ISLAND__");
95
+
96
+ path.insertBefore(
97
+ variableDeclaration("const", [
98
+ variableDeclarator(islandIdentifier, originalFunction),
99
+ ]),
100
+ );
101
+
102
+ path.replaceWith(
103
+ exportDefaultDeclaration(
104
+ callExpression(
105
+ memberExpression(
106
+ identifier("__runtime"),
107
+ identifier("createIsland"),
108
+ ),
109
+ [
110
+ islandIdentifier,
111
+ stringLiteral("default"),
112
+ identifier("__assets"),
113
+ ],
114
+ ),
115
+ ),
116
+ );
117
+ },
118
+ ExportNamedDeclaration(path) {
119
+ if (path.node.declaration?.type === "VariableDeclaration") {
120
+ const declaration = path.node.declaration.declarations.at(0);
121
+
122
+ if (
123
+ !declaration ||
124
+ declaration.id.type !== "Identifier" ||
125
+ seenExports.has(declaration.id.name)
126
+ ) {
127
+ return;
128
+ }
129
+ const exportName = declaration.id.name;
130
+ seenExports.add(exportName);
131
+
132
+ const islandIdentifier = identifier(`__wrap_${exportName}`);
133
+
134
+ path.insertBefore(
135
+ variableDeclaration("const", [
136
+ variableDeclarator(islandIdentifier, declaration.init),
137
+ ]),
138
+ );
139
+
140
+ path.replaceWith(
141
+ exportNamedDeclaration(
142
+ variableDeclaration("const", [
143
+ variableDeclarator(
144
+ identifier(exportName),
145
+ callExpression(
146
+ memberExpression(
147
+ identifier("__runtime"),
148
+ identifier("createIsland"),
149
+ ),
150
+ [
151
+ islandIdentifier,
152
+ stringLiteral(exportName),
153
+ identifier("__assets"),
154
+ ],
155
+ ),
156
+ ),
157
+ ]),
158
+ ),
159
+ );
160
+ } else if (path.node.declaration?.type === "FunctionDeclaration") {
161
+ const declaration = path.node.declaration;
162
+
163
+ if (
164
+ !declaration ||
165
+ declaration.id?.type !== "Identifier" ||
166
+ seenExports.has(declaration.id.name)
167
+ ) {
168
+ return;
169
+ }
170
+ const exportName = declaration.id.name;
171
+ seenExports.add(exportName);
172
+
173
+ const islandIdentifier = identifier(`__wrap_${exportName}`);
174
+
175
+ path.insertBefore(
176
+ functionDeclaration(
177
+ islandIdentifier,
178
+ declaration.params,
179
+ declaration.body,
180
+ declaration.generator,
181
+ declaration.async,
182
+ ),
183
+ );
184
+
185
+ path.replaceWith(
186
+ exportNamedDeclaration(
187
+ variableDeclaration("const", [
188
+ variableDeclarator(
189
+ identifier(exportName),
190
+ callExpression(
191
+ memberExpression(
192
+ identifier("__runtime"),
193
+ identifier("createIsland"),
194
+ ),
195
+ [
196
+ islandIdentifier,
197
+ stringLiteral(exportName),
198
+ identifier("__assets"),
199
+ ],
200
+ ),
201
+ ),
202
+ ]),
203
+ ),
204
+ );
205
+ }
206
+ },
207
+ });
208
+
209
+ const result = generate(ast);
210
+
211
+ return { code: result.code, map: result.map };
212
+ },
213
+ },
214
+ },
215
+ {
216
+ name: "yamf:islands:raw-import",
217
+ transform: {
218
+ order: "post",
219
+ handler(code) {
220
+ if (code.includes("__island_raw_import__")) {
221
+ return code.replaceAll("__island_raw_import__", "import");
222
+ }
223
+
224
+ return undefined;
225
+ },
226
+ },
227
+ },
228
+ ];
229
+ };
@@ -0,0 +1,30 @@
1
+ import type { Plugin } from "vite";
2
+ import { js } from "../shared/utils";
3
+
4
+ export const virtualAssets = (): Plugin => {
5
+ const virtualModuleId = "virtual:yamf:assets";
6
+ const resolvedVirtualModuleId = "\0" + virtualModuleId;
7
+
8
+ return {
9
+ name: "yamf:virtual-assets",
10
+ enforce: "pre",
11
+ resolveId(id) {
12
+ if (id === virtualModuleId) {
13
+ return resolvedVirtualModuleId;
14
+ }
15
+
16
+ return undefined;
17
+ },
18
+ load(id) {
19
+ if (id !== resolvedVirtualModuleId) {
20
+ return;
21
+ }
22
+
23
+ return js`
24
+ import clientAssets from "./src/client/index.ts?assets=client";
25
+
26
+ export { clientAssets };
27
+ `.trim();
28
+ },
29
+ };
30
+ };
@@ -0,0 +1,40 @@
1
+ import type { Plugin } from "vite";
2
+ import { js } from "../shared/utils";
3
+
4
+ export const virtualPages = (): Plugin => {
5
+ const virtualModuleId = "virtual:yamf:pages";
6
+ const resolvedVirtualModuleId = "\0" + virtualModuleId;
7
+
8
+ return {
9
+ name: "yamf:virtual-pages",
10
+ enforce: "pre",
11
+ resolveId(id) {
12
+ if (id === virtualModuleId) {
13
+ return resolvedVirtualModuleId;
14
+ }
15
+
16
+ return undefined;
17
+ },
18
+ load(id) {
19
+ if (id !== resolvedVirtualModuleId) {
20
+ return;
21
+ }
22
+
23
+ return js`
24
+ const pages = import.meta.glob("/src/pages/**/*.{js,mjs,cjs,ts,mts,cts,tsx,jsx}", {
25
+ import: "default",
26
+ });
27
+ const assets = import.meta.glob(
28
+ "/src/pages/**/*.{js,mjs,cjs,ts,mts,cts,tsx,jsx}",
29
+ {
30
+ import: "default",
31
+ query: "?assets=ssr",
32
+ eager: true,
33
+ },
34
+ );
35
+
36
+ export { pages, assets };
37
+ `.trim();
38
+ },
39
+ };
40
+ };
@@ -0,0 +1,62 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { relative } from "node:path";
3
+ import type { Plugin } from "vite";
4
+ import { js } from "../shared/utils";
5
+
6
+ const TEMPLATE_PATH = "./src/template.html";
7
+
8
+ const DEFAULT_TEMPLATE = /* html */ `
9
+ <!DOCTYPE html>
10
+ <html>
11
+ <head></head>
12
+ <body>
13
+ <!--ssr-outlet-->
14
+ </body>
15
+ </html>
16
+ `.trim();
17
+
18
+ export const virtualTemplate = (): Plugin => {
19
+ const virtualModuleId = "virtual:yamf:template";
20
+ const resolvedVirtualModuleId = "\0" + virtualModuleId;
21
+
22
+ return {
23
+ name: "yamf:virtual-template",
24
+ enforce: "pre",
25
+ resolveId(id) {
26
+ if (id === virtualModuleId) {
27
+ return resolvedVirtualModuleId;
28
+ }
29
+
30
+ return undefined;
31
+ },
32
+ async load(id) {
33
+ if (id !== resolvedVirtualModuleId) {
34
+ return;
35
+ }
36
+
37
+ try {
38
+ const template = await readFile(TEMPLATE_PATH, "utf8");
39
+
40
+ return js`export const template = ${JSON.stringify(template)};`;
41
+ } catch (error) {
42
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
43
+ return js`export const template = ${JSON.stringify(DEFAULT_TEMPLATE)};`;
44
+ }
45
+
46
+ throw error;
47
+ }
48
+ },
49
+ handleHotUpdate({ file, server }) {
50
+ if (relative(file, TEMPLATE_PATH) === "") {
51
+ const mod = server.moduleGraph.getModuleById(resolvedVirtualModuleId);
52
+
53
+ if (mod) {
54
+ server.moduleGraph.invalidateModule(mod);
55
+ }
56
+
57
+ // TODO: should reload?
58
+ // server.ws.send({ type: "full-reload" });
59
+ }
60
+ },
61
+ };
62
+ };