Skip to content

Commit dac5772

Browse files
committed
Simplify runtime binding migration
1 parent 7b4f2a0 commit dac5772

144 files changed

Lines changed: 1561 additions & 1716 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

examples/kitchen-sink/e2e-tests/tests/prerender.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ test.describe("prerender", () => {
99

1010
await page.goto("/prerender");
1111

12-
await expect(page.getByRole("heading", { name: "Miho" })).toBeVisible();
12+
await expect(
13+
page.getByRole("heading", { name: "Kitchen Sink App" }),
14+
).toBeVisible();
1315
await expect(page.getByTestId("render-location")).toHaveText("server");
1416
});
1517

1618
test("prerender page hydrates on the client", async ({ page }) => {
1719
await page.goto("/prerender");
1820

19-
await expect(page.getByRole("heading", { name: "Miho" })).toBeVisible();
21+
await expect(
22+
page.getByRole("heading", { name: "Kitchen Sink App" }),
23+
).toBeVisible();
2024
await expect(page.getByTestId("render-location")).toHaveText("client");
2125
});
2226

examples/kitchen-sink/package-lock.json

Lines changed: 438 additions & 382 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

waspc/data/Generator/templates/sdk/wasp/client/env/runtimeBindings.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

waspc/data/Generator/templates/sdk/wasp/client/env/schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{{={= =}=}}
22
import * as z from "zod"
33
import { FromRegister } from "../../types/register";
4-
import { getClientEnvValidationSchema } from "./runtimeBindings.js"
4+
import { clientEnvValidationSchema } from "virtual:wasp/client-env-schema"
55

66
export type RegisteredClientEnvValidationSchema = FromRegister<"clientEnvValidationSchema", z.ZodObject<{}>>;
77
type UserClientEnvSchema = RegisteredClientEnvValidationSchema;
88

9-
const userClientEnvSchema = (getClientEnvValidationSchema() ?? z.object({})) as UserClientEnvSchema;
9+
const userClientEnvSchema = (clientEnvValidationSchema ?? z.object({})) as UserClientEnvSchema;
1010

1111
const serverUrlSchema =
1212
z.string({

waspc/data/Generator/templates/sdk/wasp/client/runtime.ts

Lines changed: 0 additions & 27 deletions
This file was deleted.

waspc/data/Generator/templates/sdk/wasp/client/vite/plugins/validateEnv.ts

Lines changed: 60 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -20,53 +20,69 @@ export function validateEnv(): Plugin {
2020
},
2121
// We validate just before any artifacts are built.
2222
async buildStart() {
23-
await validateClientEnv(resolvedConfig);
24-
},
25-
};
26-
}
23+
// We need to import the client env schema validation module
24+
// through a Vite server, because both the user and the Wasp schema
25+
// modules may depend on bundler features.
26+
// Because of that we spin up a temporary Vite server.
27+
//
28+
// Alternatively, for `serve`, we could use the Vite server provided
29+
// through the `configureServer` hook, but that would complicate
30+
// the solution for negligible performance benefits.
31+
const tempServer = await createViteServer({
32+
root: resolvedConfig.root,
33+
mode: resolvedConfig.mode,
34+
// To ensure we pick up all user-defined plugins (resolution matches the main build)
35+
// while avoiding recursion. This includes the `wasp` plugin.
36+
configFile: false,
37+
plugins: resolvedConfig.plugins
38+
.filter((plugin) => plugin.name !== PLUGIN_NAME)
2739

28-
export async function validateClientEnv(resolvedConfig: ResolvedConfig): Promise<void> {
29-
// We need to import the client env schema validation module through a Vite
30-
// server because both the user and Wasp schema modules may use bundler features.
31-
const tempServer = await createViteServer({
32-
root: resolvedConfig.root,
33-
mode: resolvedConfig.mode,
34-
// Reuse user-defined resolution and transforms while avoiding validation recursion.
35-
configFile: false,
36-
plugins: resolvedConfig.plugins
37-
.filter((plugin) => plugin.name !== PLUGIN_NAME)
40+
// Ignore `vite:`-prefixed plugins since Vite will recreate them for
41+
// the temporary server anyway.
42+
.filter((plugin) => !plugin.name.startsWith("vite:"))
3843

39-
// Ignore `vite:`-prefixed plugins since Vite recreates them for the temporary server.
40-
.filter((plugin) => !plugin.name.startsWith("vite:"))
44+
// Vite's `configureServer`/`configurePreviewServer` hooks let plugins
45+
// wire long-lived behavior into a dev or preview server: middleware,
46+
// websocket handlers, file watchers, and similar background tasks.
47+
//
48+
// Plugins are supposed to clean these up by returning a teardown
49+
// function from the hook, but some forget to, so resources they
50+
// allocate end up outliving the server. This forces the original
51+
// Vite process to be alive indefinitely.
52+
//
53+
// We don't need either hook to validate the client env schema.
54+
// We only need module resolution and transforms.
55+
.map((plugin) => ({
56+
...plugin,
57+
configureServer: undefined,
58+
configurePreviewServer: undefined,
59+
})),
4160

42-
// Avoid starting middleware, watchers, and other long-lived plugin behavior.
43-
.map((plugin) => ({
44-
...plugin,
45-
configureServer: undefined,
46-
configurePreviewServer: undefined,
47-
})),
61+
// Minimize side effects from spinning up a temporary dev server.
62+
appType: 'custom', // avoid HTML handling
63+
server: {
64+
middlewareMode: true, // do not start an actual HTTP server
65+
watch: null,
66+
hmr: false
67+
},
68+
logLevel: "silent",
69+
optimizeDeps: { noDiscovery: true, include: [] },
70+
clearScreen: false,
71+
});
4872

49-
// Minimize side effects from spinning up a temporary dev server.
50-
appType: 'custom',
51-
server: {
52-
middlewareMode: true,
53-
watch: null,
54-
hmr: false
73+
try {
74+
// Vite's `ssr` means bundled for "backend JS runtime", like Node.
75+
// This environment is always runnable in vite dev server.
76+
if (!isRunnableDevEnvironment(tempServer.environments.ssr)) {
77+
throw new Error(`Expected ssr to be a runnable dev environment`)
78+
}
79+
// The imported module runs env schema validation as an import
80+
// side-effect and throws on failure.
81+
const moduleAbsPath = path.resolve(resolvedConfig.root, CLIENT_ENV_SCHEMA_VALIDATION_MODULE);
82+
await tempServer.environments.ssr.runner.import(moduleAbsPath);
83+
} finally {
84+
await tempServer.close();
85+
}
5586
},
56-
logLevel: "silent",
57-
optimizeDeps: { noDiscovery: true, include: [] },
58-
clearScreen: false,
59-
});
60-
61-
try {
62-
// Vite's `ssr` means bundled for a backend JS runtime such as Node.
63-
if (!isRunnableDevEnvironment(tempServer.environments.ssr)) {
64-
throw new Error(`Expected ssr to be a runnable dev environment`)
65-
}
66-
// Importing this module validates the client environment as a side effect.
67-
const moduleAbsPath = path.resolve(resolvedConfig.root, CLIENT_ENV_SCHEMA_VALIDATION_MODULE);
68-
await tempServer.environments.ssr.runner.import(moduleAbsPath);
69-
} finally {
70-
await tempServer.close();
71-
}
87+
};
7288
}
Lines changed: 3 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,33 @@
11
{{={= =}=}}
2-
import { type EnvironmentModuleNode, type Plugin, type ViteDevServer } from "vite";
2+
import { type Plugin } from "vite";
33
import {
4-
clientRuntimeBindingsFilePath,
54
getClientEntryTsxContent,
6-
getClientRuntimeBindingsTsContent,
5+
getClientEnvSchemaTsContent,
76
getRoutesTsxContent,
87
getSsrEntryTsxContent,
98
} from "../virtual-files/index.js";
109
import { makeVirtualFilesResolver, type VirtualFiles } from "../virtual-files/resolver.js";
11-
import { validateClientEnv } from "./validateEnv.js";
1210

1311
const resolveVirtualFiles = makeVirtualFilesResolver([
1412
{ id: "{= clientEntryPointPath =}", load: getClientEntryTsxContent },
15-
{ id: "{= clientRuntimeBindingsEntryPointPath =}", load: getClientRuntimeBindingsTsContent },
13+
{ id: "{= clientEnvSchemaEntryPointPath =}", load: getClientEnvSchemaTsContent },
1614
{ id: "{= routesEntryPointPath =}", load: getRoutesTsxContent },
1715
{ id: "{= ssrEntryPointPath =}", load: getSsrEntryTsxContent },
1816
]);
1917

2018
export function virtualWaspModules(): Plugin {
2119
let virtualFiles!: VirtualFiles;
22-
let devServer: ViteDevServer | undefined;
23-
let restartTimer: ReturnType<typeof setTimeout> | undefined;
24-
let lastUpdateKey: string | undefined;
25-
let lastUpdatePromise: Promise<void> | undefined;
2620

2721
return {
2822
name: "wasp:virtual-wasp-modules",
2923
enforce: "pre",
3024
configResolved(config) {
3125
virtualFiles = resolveVirtualFiles(config.root);
3226
},
33-
configureServer(server) {
34-
devServer = server;
35-
server.watcher.add(clientRuntimeBindingsFilePath);
36-
},
3727
resolveId: (id) => virtualFiles.ids.get(id),
3828
load(id) {
3929
const loader = virtualFiles.loaders.get(id);
4030
return loader?.();
4131
},
42-
async hotUpdate({ file, modules, timestamp }) {
43-
if (!devServer || !shouldRestartClient(file, modules, virtualFiles)) {
44-
return;
45-
}
46-
47-
const updateKey = `${timestamp}:${file}`;
48-
if (lastUpdateKey !== updateKey) {
49-
lastUpdateKey = updateKey;
50-
lastUpdatePromise = validateClientEnv(devServer.config).then(() => {
51-
scheduleRestart();
52-
});
53-
}
54-
55-
await lastUpdatePromise;
56-
return [];
57-
},
5832
};
59-
60-
function scheduleRestart(): void {
61-
// Restart after Vite finishes the current HMR transaction. Restarting from
62-
// inside this hook replaces the environments that Vite is still iterating.
63-
restartTimer ??= setTimeout(() => {
64-
void devServer
65-
?.restart()
66-
.catch((error) => {
67-
devServer?.config.logger.error(error);
68-
})
69-
.finally(() => {
70-
restartTimer = undefined;
71-
});
72-
});
73-
}
74-
}
75-
76-
function shouldRestartClient(
77-
changedFile: string,
78-
changedModules: EnvironmentModuleNode[],
79-
virtualFiles: VirtualFiles,
80-
): boolean {
81-
if (changedFile === clientRuntimeBindingsFilePath) {
82-
return true;
83-
}
84-
85-
const bindingsModuleId = virtualFiles.ids.get("{= clientRuntimeBindingsEntryPointPath =}");
86-
return bindingsModuleId !== undefined && changedModules.some(
87-
(module) => hasImporter(module, bindingsModuleId, new Set()),
88-
);
89-
}
90-
91-
function hasImporter(
92-
module: EnvironmentModuleNode,
93-
importerId: string,
94-
visited: Set<EnvironmentModuleNode>,
95-
): boolean {
96-
if (module.id === importerId) {
97-
return true;
98-
}
99-
if (visited.has(module)) {
100-
return false;
101-
}
102-
103-
visited.add(module);
104-
return [...module.importers].some((importer) =>
105-
hasImporter(importer, importerId, visited),
106-
);
10733
}

waspc/data/Generator/templates/sdk/wasp/client/vite/virtual-files/files/client-entry.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
{{={= =}=}}
2-
import "virtual:wasp/client-runtime-bindings";
3-
42
import { startTransition } from "react";
53
import { hydrateRoot } from "react-dom/client";
64
import { createBrowserRouter } from "react-router";
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{{={= =}=}}
2+
{=# clientEnvValidationSchema.isDefined =}
3+
{=& clientEnvValidationSchema.importStatement =}
4+
{=/ clientEnvValidationSchema.isDefined =}
5+
6+
export const clientEnvValidationSchema = {=# clientEnvValidationSchema.isDefined =}{= clientEnvValidationSchema.importIdentifier =}{=/ clientEnvValidationSchema.isDefined =}{=^ clientEnvValidationSchema.isDefined =}undefined{=/ clientEnvValidationSchema.isDefined =};

waspc/data/Generator/templates/sdk/wasp/client/vite/virtual-files/files/client-runtime-bindings.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)