fix(dev-server,security): repair two defects that only appear in a published build
The dev server shipped two entries, index and serve-entry, bundled independently because the publish build set splitting:false. They share pipeline.ts, which holds mutable module state -- compileCacheDir, set once at startup by the bootstrap, and browserArtifactPaths, populated during compilation and read when serving /__wrnexus/client/*. Duplicating the module duplicated the state, so the writer and the reader addressed different copies: every component client module 404'd and .wrn compilation wrote nothing. It works from source, where there is one module instance, which is why it reached a release. Emitting a shared chunk fixes it for every package at once. resetDevCache also ran several hundred lines after the plugin virtual modules were written into the same directory, deleting them at every boot. An app with no plugins never noticed; an app with one lost them every time. Separately, secureCookieOptions spread ...options after its path default, and setSecureCookie always forwards an explicit path key -- so omitting path emitted a cookie with no Path at all, which the browser then scoped to the request's directory. Verified end to end against a real app installing the published packages: 17 artifacts written, client modules 200, and the sign-in form submits from the UI and reaches /dashboard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -302,6 +302,17 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
// Keep generated artifacts under the single framework state directory.
|
||||
// Startup clears this cache and removes legacy PID-suffixed cache folders.
|
||||
const cacheDir = join(appRoot, ".wrnexus", "cache");
|
||||
|
||||
// Clear the cache BEFORE anything writes into it. This used to run several
|
||||
// hundred lines below, which deleted the plugin virtual modules written just
|
||||
// after this point -- so an app with a plugin (auth, say) lost its generated
|
||||
// modules at every boot, while an app with none never noticed.
|
||||
resetDevCache({
|
||||
rootDir: appRoot,
|
||||
cacheDir,
|
||||
enabled: process.env.WRNEXUS_PRESERVE_CACHE !== "1",
|
||||
});
|
||||
|
||||
const virtualDir = join(cacheDir, "virtual");
|
||||
mkdirSync(virtualDir, { recursive: true });
|
||||
for (const [index, module] of pluginContributions.virtualModules.entries()) {
|
||||
@@ -360,12 +371,6 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
? createDevToolbarCollector()
|
||||
: undefined;
|
||||
|
||||
resetDevCache({
|
||||
rootDir: appRoot,
|
||||
cacheDir,
|
||||
enabled: process.env.WRNEXUS_PRESERVE_CACHE !== "1",
|
||||
});
|
||||
|
||||
// Compile every `.wrn` into ONE cache dir at the project root, instead of a
|
||||
// `.wrnexus/` next to each source file (and inside node_modules UI dirs).
|
||||
setCompileCacheDir(cacheDir);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const repoRoot = join(import.meta.dir, "..", "..", "..");
|
||||
const publishScript = join(repoRoot, "scripts", "publish-packages.ts");
|
||||
|
||||
/**
|
||||
* `@wrnexus/dev-server` ships two entries — `index` (the dev server bootstrap)
|
||||
* and `serve-entry` (the request path that compiles `.wrn` files). They share
|
||||
* `pipeline.ts`, which holds MUTABLE module state: `compileCacheDir`, set once
|
||||
* at startup by the bootstrap, and `browserArtifactPaths`, populated during
|
||||
* compilation and read when serving `/__wrnexus/client/*`.
|
||||
*
|
||||
* Bundling each entry independently gives each its own copy of that state. The
|
||||
* bootstrap then sets a cache dir the compiler never sees, and the compiler
|
||||
* records artifact paths the server never sees — so every client module 404s
|
||||
* and `.wrn` compilation writes nothing. It works from source (one module
|
||||
* instance) and fails only once published, which is why it reached a release.
|
||||
*/
|
||||
test("the publish build shares chunks so module state is not duplicated per entry", () => {
|
||||
const source = readFileSync(publishScript, "utf8");
|
||||
|
||||
expect(source).not.toMatch(/splitting:\s*false/);
|
||||
expect(source).toMatch(/splitting:\s*true/);
|
||||
});
|
||||
|
||||
test("a built dev-server dist declares its mutable pipeline state exactly once", () => {
|
||||
const dist = join(repoRoot, "packages", "dev-server", "dist");
|
||||
if (!existsSync(dist)) {
|
||||
// The dist is a build artifact, absent on a clean checkout. The
|
||||
// configuration assertion above is the guard that always runs.
|
||||
return;
|
||||
}
|
||||
|
||||
const bundles = readdirSync(dist).filter((file) => file.endsWith(".js"));
|
||||
expect(bundles.length).toBeGreaterThan(0);
|
||||
|
||||
for (const declaration of [
|
||||
"compileCacheDir",
|
||||
"browserArtifactPaths",
|
||||
"serveWrnBrowserArtifact",
|
||||
]) {
|
||||
const owners = bundles.filter((file) =>
|
||||
readFileSync(join(dist, file), "utf8").includes(declaration),
|
||||
);
|
||||
expect({ declaration, owners }).toEqual({ declaration, owners: owners.slice(0, 1) });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user