@pylonsync/functions 0.3.294 → 0.3.296
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.
|
@@ -58,6 +58,14 @@ export interface BuildOutput {
|
|
|
58
58
|
* from `getManifest` (in-process SSR path).
|
|
59
59
|
*/
|
|
60
60
|
export declare function buildClientBundle(appDirRel?: string): Promise<BuildOutput>;
|
|
61
|
+
/**
|
|
62
|
+
* Compile `app/globals.css` through Tailwind v4 (`@tailwindcss/cli`)
|
|
63
|
+
* if both are present. Returns the relative output path (under
|
|
64
|
+
* outdir) when produced, else null. Skipped silently when the
|
|
65
|
+
* project hasn't opted in to Tailwind — we don't want every SSR
|
|
66
|
+
* project to need Tailwind installed.
|
|
67
|
+
*/
|
|
68
|
+
export declare function buildTailwind(fs: any, path: any, cwd: string, outdir: string, appDirRel: string): Promise<string | null>;
|
|
61
69
|
/**
|
|
62
70
|
* Return the bundle manifest. If a fresh manifest exists on disk,
|
|
63
71
|
* use it (caching parse output across requests). Otherwise build
|
package/package.json
CHANGED
|
@@ -23,6 +23,7 @@ import * as os from "node:os";
|
|
|
23
23
|
|
|
24
24
|
import {
|
|
25
25
|
buildClientBundle,
|
|
26
|
+
buildTailwind,
|
|
26
27
|
type PylonBundleManifest,
|
|
27
28
|
} from "./ssr-client-bundler";
|
|
28
29
|
import { nearestBoundaryComponent } from "./ssr-client-boundary";
|
|
@@ -379,3 +380,53 @@ describe("client boundary is wired into the build", () => {
|
|
|
379
380
|
expect(allShared).toContain("getDerivedStateFromError");
|
|
380
381
|
});
|
|
381
382
|
});
|
|
383
|
+
|
|
384
|
+
describe("Tailwind compile is concurrency-safe", () => {
|
|
385
|
+
// Regression: `pylon dev` warms the SSR bundle in one runner process while the
|
|
386
|
+
// first requests drive their own rebuild in another. Both compiled Tailwind to
|
|
387
|
+
// a SHARED temp file (`.styles.build.css`); whichever renamed it first won, and
|
|
388
|
+
// the other's `renameSync` ENOENT'd → the compile threw → the route shipped
|
|
389
|
+
// with no `css` → the page rendered UNSTYLED. Reproduced ~25% of cold boots
|
|
390
|
+
// under concurrent load. The fix gives each compile a process-unique temp path
|
|
391
|
+
// and publishes (rename) before pruning others. Drive several compiles at once
|
|
392
|
+
// against one outdir and prove they all succeed.
|
|
393
|
+
test("concurrent compiles against one outdir all produce the stylesheet", async () => {
|
|
394
|
+
const root = makeFixture(
|
|
395
|
+
{ "page.tsx": PAGE_BODY("Home") },
|
|
396
|
+
{ "layout.tsx": LAYOUT_BODY },
|
|
397
|
+
);
|
|
398
|
+
tempDir = root; // registers it for afterEach cleanup
|
|
399
|
+
// Opt into Tailwind: a real globals.css the CLI can compile. `@tailwindcss/cli`
|
|
400
|
+
// + `tailwindcss` resolve through the fixture's node_modules symlink (it points
|
|
401
|
+
// at examples/ssr-hello/node_modules, which declares both).
|
|
402
|
+
fs.writeFileSync(
|
|
403
|
+
path.join(root, "app", "globals.css"),
|
|
404
|
+
'@import "tailwindcss";\n@source "../app/**/*.{tsx,ts,jsx,js}";\n',
|
|
405
|
+
"utf8",
|
|
406
|
+
);
|
|
407
|
+
originalCwd = process.cwd();
|
|
408
|
+
process.chdir(root);
|
|
409
|
+
|
|
410
|
+
const outdir = path.join(root, ".pylon", "client-build");
|
|
411
|
+
fs.mkdirSync(outdir, { recursive: true });
|
|
412
|
+
|
|
413
|
+
// Old code: at least one of these rejects with the rename ENOENT.
|
|
414
|
+
const results = await Promise.all(
|
|
415
|
+
Array.from({ length: 4 }, () =>
|
|
416
|
+
buildTailwind(fs, path, root, outdir, "app"),
|
|
417
|
+
),
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
for (const name of results) {
|
|
421
|
+
expect(name).toMatch(/^styles-[a-z0-9]+\.css$/);
|
|
422
|
+
expect(fs.existsSync(path.join(outdir, name as string))).toBe(true);
|
|
423
|
+
}
|
|
424
|
+
// Deterministic output → identical hash → one published asset, no leftover
|
|
425
|
+
// temp files stranded in the outdir.
|
|
426
|
+
expect(new Set(results).size).toBe(1);
|
|
427
|
+
const stranded = (fs.readdirSync(outdir) as string[]).filter((f) =>
|
|
428
|
+
f.startsWith(".styles.build."),
|
|
429
|
+
);
|
|
430
|
+
expect(stranded).toEqual([]);
|
|
431
|
+
}, 20_000);
|
|
432
|
+
});
|
|
@@ -868,6 +868,13 @@ async function _doBuild(appDirRel: string): Promise<BuildOutput> {
|
|
|
868
868
|
return _doBuildInner(fs, path, cwd, appDirRel);
|
|
869
869
|
}
|
|
870
870
|
|
|
871
|
+
// Monotonic per-process counter for the Tailwind temp filename. `pylon dev`
|
|
872
|
+
// warms the SSR bundle in one runner process while incoming requests can drive
|
|
873
|
+
// their own rebuild in another, so two `buildTailwind` calls may run against the
|
|
874
|
+
// same outdir at once. Combined with the pid it gives every compile a unique
|
|
875
|
+
// temp path, so concurrent builds never rename each other's file away.
|
|
876
|
+
let _styleBuildSeq = 0;
|
|
877
|
+
|
|
871
878
|
/**
|
|
872
879
|
* Compile `app/globals.css` through Tailwind v4 (`@tailwindcss/cli`)
|
|
873
880
|
* if both are present. Returns the relative output path (under
|
|
@@ -875,7 +882,10 @@ async function _doBuild(appDirRel: string): Promise<BuildOutput> {
|
|
|
875
882
|
* project hasn't opted in to Tailwind — we don't want every SSR
|
|
876
883
|
* project to need Tailwind installed.
|
|
877
884
|
*/
|
|
878
|
-
|
|
885
|
+
// Exported for the concurrency regression test (ssr-client-bundler.test.ts):
|
|
886
|
+
// it drives several compiles at once against one outdir to prove they no longer
|
|
887
|
+
// race on a shared temp file.
|
|
888
|
+
export async function buildTailwind(
|
|
879
889
|
fs: any,
|
|
880
890
|
path: any,
|
|
881
891
|
cwd: string,
|
|
@@ -915,7 +925,15 @@ async function buildTailwind(
|
|
|
915
925
|
//
|
|
916
926
|
// Spawn the CLI. Bun is already running; reuse it as the interpreter so the
|
|
917
927
|
// user doesn't need node on PATH.
|
|
918
|
-
|
|
928
|
+
// PROCESS-UNIQUE temp file. A shared name (`.styles.build.css`) let a
|
|
929
|
+
// concurrent build rename the file out from under this one — its `renameSync`
|
|
930
|
+
// then ENOENT'd, the compile threw, the route shipped with no `css`, and the
|
|
931
|
+
// page rendered UNSTYLED. Worst under cold-boot traffic (a warm build racing
|
|
932
|
+
// the first requests), which is exactly when it must not happen.
|
|
933
|
+
const tmpPath = path.join(
|
|
934
|
+
outdir,
|
|
935
|
+
`.styles.build.${process.pid}.${_styleBuildSeq++}.css`,
|
|
936
|
+
);
|
|
919
937
|
const proc = (Bun as any).spawn({
|
|
920
938
|
cmd: [process.execPath, cliPath, "-i", globalsPath, "-o", tmpPath, "--minify"],
|
|
921
939
|
cwd,
|
|
@@ -925,6 +943,11 @@ async function buildTailwind(
|
|
|
925
943
|
const exitCode = await proc.exited;
|
|
926
944
|
if (exitCode !== 0) {
|
|
927
945
|
const err = await new Response(proc.stderr).text();
|
|
946
|
+
try {
|
|
947
|
+
fs.rmSync(tmpPath, { force: true });
|
|
948
|
+
} catch {
|
|
949
|
+
/* nothing to clean up */
|
|
950
|
+
}
|
|
928
951
|
throw new Error(`tailwindcss build failed (exit ${exitCode}): ${err}`);
|
|
929
952
|
}
|
|
930
953
|
|
|
@@ -942,19 +965,22 @@ async function buildTailwind(
|
|
|
942
965
|
const stylesName = `styles-${hash.toString(36).padStart(8, "0")}.css`;
|
|
943
966
|
const outPath = path.join(outdir, stylesName);
|
|
944
967
|
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
968
|
+
// Publish our freshly-compiled CSS FIRST (rename replaces atomically on
|
|
969
|
+
// POSIX, so the asset is never momentarily absent), THEN prune OTHER stale
|
|
970
|
+
// styles-*.css. The old order pruned before renaming, so a concurrent build
|
|
971
|
+
// could delete the file we were about to publish; pruning our own name could
|
|
972
|
+
// delete a concurrent winner's identical output. Excluding `stylesName` keeps
|
|
973
|
+
// both safe while still clearing builds from earlier class changes.
|
|
974
|
+
fs.renameSync(tmpPath, outPath);
|
|
948
975
|
try {
|
|
949
976
|
for (const f of fs.readdirSync(outdir) as string[]) {
|
|
950
|
-
if (f.startsWith("styles-") && f.endsWith(".css")) {
|
|
977
|
+
if (f.startsWith("styles-") && f.endsWith(".css") && f !== stylesName) {
|
|
951
978
|
fs.rmSync(path.join(outdir, f), { force: true });
|
|
952
979
|
}
|
|
953
980
|
}
|
|
954
981
|
} catch {
|
|
955
982
|
/* outdir may not be listable on the first build — ignore */
|
|
956
983
|
}
|
|
957
|
-
fs.renameSync(tmpPath, outPath);
|
|
958
984
|
return stylesName;
|
|
959
985
|
}
|
|
960
986
|
|