@pylonsync/functions 0.11.4 → 0.11.6

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.
@@ -24,6 +24,14 @@ interface BundleClientMessage {
24
24
  * import into a loud build failure that names the offending importer.
25
25
  */
26
26
  export declare function assertNotServerOnly(specifier: string, importer: string): void;
27
+ /**
28
+ * A module file → its project-relative, extension-less component path
29
+ * (`<cwd>/app/blog/page.tsx` → `app/blog/page`). Always "/"-separated, on
30
+ * Windows too: the result is a manifest key, the input to the SSR boundary
31
+ * walk, and is spliced into generated `import` specifiers, where a backslash
32
+ * is a string escape (`"../app\page"` resolves `../apppage`).
33
+ */
34
+ export declare function componentPathFor(path: any, cwd: string, file: string): string;
27
35
  /**
28
36
  * Every route-level `loading` module under the app dir, as project-relative
29
37
  * component paths (`app/dashboard/loading`), sorted for a stable build.
@@ -108,6 +116,7 @@ export declare function buildClientBundle(appDirRel?: string): Promise<BuildOutp
108
116
  * project to need Tailwind installed.
109
117
  */
110
118
  export declare function buildTailwind(fs: any, path: any, cwd: string, outdir: string, appDirRel: string): Promise<string | null>;
119
+ export declare function _doBuildInner(fs: any, path: any, cwd: string, appDirRel: string): Promise<BuildOutput>;
111
120
  /**
112
121
  * Return the bundle manifest. If a fresh manifest exists on disk,
113
122
  * use it (caching parse output across requests). Otherwise build
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pylonsync/functions",
3
- "version": "0.11.4",
3
+ "version": "0.11.6",
4
4
  "description": "TypeScript function runtime for pylon — defines server-side queries, mutations, and actions.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -25,6 +25,8 @@ import {
25
25
  buildClientBundle,
26
26
  buildTailwind,
27
27
  assertNotServerOnly,
28
+ componentPathFor,
29
+ _doBuildInner,
28
30
  discoverLoadingModules,
29
31
  generateLoadingRegistry,
30
32
  type PylonBundleManifest,
@@ -234,6 +236,55 @@ describe("ssr-client-bundler (Phase 1.5e)", () => {
234
236
  }
235
237
  });
236
238
 
239
+ test("Windows separators: the build succeeds and every manifest path uses /", async () => {
240
+ // node:path on Windows returns "\"-separated relative paths. They used to
241
+ // reach the generated entries as `import Page from "../app\page"`, where
242
+ // `\p` is a string escape, so Bun.build failed with "Bundle failed" and
243
+ // every page shipped without hydration (#15).
244
+ tempDir = makeFixture(
245
+ {
246
+ "page.tsx": PAGE_BODY("Home"),
247
+ "blog/page.tsx": PAGE_BODY("Blog"),
248
+ "not-found.tsx": PAGE_BODY("Missing"),
249
+ "loading.tsx": LOADING_BODY,
250
+ },
251
+ { "layout.tsx": LAYOUT_BODY },
252
+ );
253
+ const winPath = {
254
+ ...path,
255
+ relative: (from: string, to: string) =>
256
+ path.relative(from, to).replace(/\//g, "\\"),
257
+ };
258
+
259
+ const { manifestPath } = await _doBuildInner(fs, winPath, tempDir, "app");
260
+ const manifest = JSON.parse(
261
+ fs.readFileSync(manifestPath, "utf8"),
262
+ ) as PylonBundleManifest;
263
+
264
+ expect(Object.keys(manifest.routes).sort()).toEqual([
265
+ "app/blog/page",
266
+ "app/not-found",
267
+ "app/page",
268
+ ]);
269
+ expect(manifest.outdir).toBe(".pylon/client-build");
270
+ for (const route of Object.values(manifest.routes)) {
271
+ expect(route.file).not.toContain("\\");
272
+ for (const imp of route.imports) expect(imp).not.toContain("\\");
273
+ }
274
+ const entry = fs.readFileSync(
275
+ path.join(tempDir, ".pylon", "client-entry-app__blog__page.tsx"),
276
+ "utf8",
277
+ );
278
+ expect(entry).toContain('import Page from "../app/blog/page";');
279
+ expect(entry).toContain('import L0 from "../app/layout";');
280
+ });
281
+
282
+ test("componentPathFor returns a /-separated path for win32 input", () => {
283
+ expect(
284
+ componentPathFor(path.win32, "C:\\proj", "C:\\proj\\app\\(shop)\\cart\\page.tsx"),
285
+ ).toBe("app/(shop)/cart/page");
286
+ });
287
+
237
288
  test("adding a route grows the manifest by one and stays small per-entry", async () => {
238
289
  tempDir = makeFixture(
239
290
  {
@@ -141,6 +141,20 @@ export function assertNotServerOnly(specifier: string, importer: string): void {
141
141
  }
142
142
  }
143
143
 
144
+ /**
145
+ * A module file → its project-relative, extension-less component path
146
+ * (`<cwd>/app/blog/page.tsx` → `app/blog/page`). Always "/"-separated, on
147
+ * Windows too: the result is a manifest key, the input to the SSR boundary
148
+ * walk, and is spliced into generated `import` specifiers, where a backslash
149
+ * is a string escape (`"../app\page"` resolves `../apppage`).
150
+ */
151
+ export function componentPathFor(path: any, cwd: string, file: string): string {
152
+ return path
153
+ .relative(cwd, file)
154
+ .replace(/\.(tsx?|jsx?)$/, "")
155
+ .replace(/\\/g, "/");
156
+ }
157
+
144
158
  /**
145
159
  * Synchronously walk the route dir (`<appDirRel>` under cwd, e.g.
146
160
  * `app` or `web/app`) and return one entry per discovered page, each
@@ -182,7 +196,7 @@ function discoverRoutes(
182
196
  .map((n: string) => path.join(dir, n))
183
197
  .find((p: string) => fs.existsSync(p));
184
198
  const nextLayouts = layoutHere
185
- ? [...layouts, path.relative(cwd, layoutHere).replace(/\.(tsx?|jsx?)$/, "")]
199
+ ? [...layouts, componentPathFor(path, cwd, layoutHere)]
186
200
  : layouts;
187
201
  const pageHere = ["page.tsx", "page.ts", "page.jsx", "page.js"]
188
202
  .map((n: string) => path.join(dir, n))
@@ -190,7 +204,7 @@ function discoverRoutes(
190
204
  if (pageHere) {
191
205
  pages.push({
192
206
  segments: [...segments],
193
- component: path.relative(cwd, pageHere).replace(/\.(tsx?|jsx?)$/, ""),
207
+ component: componentPathFor(path, cwd, pageHere),
194
208
  layouts: nextLayouts,
195
209
  // e.g. [] → "/", ["p","[slug]"] → "/p/[slug]".
196
210
  pattern: "/" + segments.join("/"),
@@ -207,7 +221,7 @@ function discoverRoutes(
207
221
  if (bHere) {
208
222
  pages.push({
209
223
  segments: [...segments],
210
- component: path.relative(cwd, bHere).replace(/\.(tsx?|jsx?)$/, ""),
224
+ component: componentPathFor(path, cwd, bHere),
211
225
  layouts: nextLayouts,
212
226
  });
213
227
  }
@@ -255,9 +269,7 @@ export function discoverLoadingModules(
255
269
  .map((n: string) => path.join(dir, n))
256
270
  .find((p: string) => fs.existsSync(p));
257
271
  if (here) {
258
- found.push(
259
- path.relative(cwd, here).replace(/\.(tsx?|jsx?)$/, "").replace(/\\/g, "/"),
260
- );
272
+ found.push(componentPathFor(path, cwd, here));
261
273
  }
262
274
  for (const e of entries) {
263
275
  if (!e.isDirectory()) continue;
@@ -1381,7 +1393,9 @@ export async function buildTailwind(
1381
1393
  return stylesName;
1382
1394
  }
1383
1395
 
1384
- async function _doBuildInner(
1396
+ // Exported for the Windows-separator regression test (ssr-client-bundler.test.ts),
1397
+ // which injects a `path` whose `relative` returns "\"-separated paths.
1398
+ export async function _doBuildInner(
1385
1399
  fs: any,
1386
1400
  path: any,
1387
1401
  cwd: string,
@@ -1557,7 +1571,10 @@ async function _doBuildInner(
1557
1571
  // - chunks (kind === "chunk") — looked up by basename when
1558
1572
  // scanning entry files for static `import "./chunks/..."`
1559
1573
  // specifiers.
1560
- const outdirRel = path.relative(cwd, outdir);
1574
+ // Manifest paths are URL paths under `public_prefix` ("/"-separated). The
1575
+ // runtime's /_pylon/build/ handler rejects any request with a backslash.
1576
+ const toUrlPath = (p: string) => p.replace(/\\/g, "/");
1577
+ const outdirRel = toUrlPath(path.relative(cwd, outdir));
1561
1578
  const entriesByStem = new Map<
1562
1579
  string,
1563
1580
  { absPath: string; relPath: string }
@@ -1568,7 +1585,7 @@ async function _doBuildInner(
1568
1585
  >();
1569
1586
  for (const o of result.outputs) {
1570
1587
  const absPath: string = o.path;
1571
- const relPath = path.relative(outdir, absPath);
1588
+ const relPath = toUrlPath(path.relative(outdir, absPath));
1572
1589
  const base = path.basename(absPath);
1573
1590
  // Strip `-<hash>.js` to recover the entry source's stem
1574
1591
  // (e.g. `client-entry-app__hello__page`). The hash is