@pitlane/dev 0.5.1 → 0.6.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,54 @@
1
1
  # @pitlane/dev
2
2
 
3
+ ## 0.6.1
4
+
5
+ Target Remix `3.0.0-rc.2`.
6
+
7
+ - Component HMR no longer instruments a PascalCase export that is not a
8
+ Remix component setup. `remix/ui-hmr` matches any exported PascalCase
9
+ function whose body returns something, so an `export async function` had
10
+ its body moved into a plain arrow, where the `await` no longer parsed and
11
+ the browser failed the module and every importer with
12
+ `SyntaxError: Unexpected reserved word`. A generator broke the same way on
13
+ `yield`, a helper returning an element was rewritten to return a function,
14
+ and a `clientEntry()` setup with no render function threw inside the
15
+ transform. The plugin now checks the shape itself and leaves the module
16
+ alone when one of them is present, warning when a real component in that
17
+ file loses its hot swap as a result.
18
+ - The `remix` peer stays at `^3.0.0-rc.1`, which already admits rc.2. Tested
19
+ against Vite 8.1 (Rolldown), Vite+ 0.2 (`vp`), and `remix@3.0.0-rc.2`,
20
+ across the node, cloudflare, hmr, spa, and prerender fixtures.
21
+ - Now depends on `@pitlane/crawler@^0.2.1`, its rc.2 release. The manifest
22
+ carries `workspace:^` and the release workflow packs with pnpm, which
23
+ rewrites it to the version the monorepo resolved, so crawler publishes
24
+ first.
25
+
26
+ ## 0.6.0
27
+
28
+ Target Remix `3.0.0-rc.1`.
29
+
30
+ - Raised the `remix` peer dependency to `^3.0.0-rc.1` (from
31
+ `^3.0.0-beta.10`). The plugin itself is unchanged: `remix()`, its
32
+ `prerender` option, and `@pitlane/dev/runtime` all behave as they did in
33
+ 0.5.1.
34
+ - Now depends on `@pitlane/crawler@^0.2.0`, its Remix rc.1 release. The
35
+ manifest carries `workspace:^` and the release workflow packs with pnpm,
36
+ which rewrites it to the version the monorepo resolved. 0.5.1 shipped
37
+ `^0.1.0`, which `0.2.0` does not satisfy, so the crawler release only reaches
38
+ `remix({ prerender })` users through this bump.
39
+ - Two rc.1 breaking changes land in the app code this plugin's guides
40
+ document, not in the plugin. The documented `app/entry.browser.ts` reads the
41
+ frame-targeting attributes off a submit button, and rc.1 renamed all of them
42
+ into the `data-rmx-*` namespace, so `rmx-target`, `rmx-src`, and
43
+ `rmx-reset-scroll` become `data-rmx-target`, `data-rmx-src`, and
44
+ `data-rmx-reset-scroll`. Nothing raises an error when they are missed: rc.1's
45
+ runtime matches only the prefixed names, so a stale attribute is inert and
46
+ frame navigation silently falls back to a document load. `createAssetServer`
47
+ also replaced `fileMap` with directory-based `mounts`, which affects an app
48
+ that configures its own asset server rather than letting this plugin do it.
49
+ - Tested against Vite 8.1 (Rolldown), Vite+ 0.2 (`vp`), and
50
+ `remix@3.0.0-rc.1`, across the node, cloudflare, hmr, and prerender fixtures.
51
+
3
52
  ## 0.5.1
4
53
 
5
54
  Fixes an unusable 0.5.0 on npm.
package/README.md CHANGED
@@ -12,7 +12,7 @@ npm install --save-dev @pitlane/dev
12
12
  vp add -D @pitlane/dev
13
13
  ```
14
14
 
15
- Requires `remix@^3.0.0-beta.10` and `vite@>=7` as peers. Tested against **Vite 8.1** (Rolldown), **Vite+ 0.2** (`vp`), and `remix@3.0.0-beta.10` — the [templates](https://github.com/pitlane-tools/templates) are the continuously tested reference.
15
+ Requires `remix@^3.0.0-rc.1` and `vite@>=7` as peers. Tested against **Vite 8.1** (Rolldown), **Vite+ 0.2** (`vp`), and `remix@3.0.0-rc.2` — the [templates](https://github.com/pitlane-tools/templates) are the continuously tested reference.
16
16
 
17
17
  ## Quick start
18
18
 
@@ -123,14 +123,14 @@ remix({
123
123
  });
124
124
  ```
125
125
 
126
- | Option | Type | Default | Purpose |
127
- | -------------------- | ---------------------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
128
- | `server` | `boolean` | `true` | Whether the app has a server. Pass `false` for [SPA mode](#spa-mode), which ignores every option below. |
129
- | `prerender` | `boolean \| string[] \| fn \| obj` | none | Render paths to static HTML at build time. See [Prerendering](#prerendering). |
130
- | `clientEntry` | `string \| false` | `"app/entry.browser"` | Client entry module. Pass `false` for fully server-rendered apps with no hydration. |
131
- | `serverEntry` | `string` | `"app/entry.server"` | Server entry module, built as `dist/ssr/index.js`. |
132
- | `serverEnvironments` | `string[]` | `["ssr"]` | Environment names the `clientEntry()` transform treats as "server". |
133
- | `serverHandler` | `boolean` | `true` | Serve dev requests through your server entry. Set `false` when `@cloudflare/vite-plugin`, `@netlify/vite-plugin`, or `nitro/vite` owns dev-time request handling. |
126
+ | Option | Type | Default | Purpose |
127
+ | -------------------- | ---------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
128
+ | `server` | `boolean` | `true` | Whether the app has a server. Pass `false` for [SPA mode](#spa-mode), which ignores every option below. |
129
+ | `prerender` | `boolean \| string[] \| fn \| obj` | none | Render paths to static HTML at build time. See [Prerendering](#prerendering). |
130
+ | `clientEntry` | `string \| false` | `"app/entry.browser"` | Client entry module. Pass `false` for fully server-rendered apps with no hydration. |
131
+ | `serverEntry` | `string` | `"app/entry.server"` | Server entry module, built as `dist/ssr/index.js`. |
132
+ | `serverEnvironments` | `string[]` | `["ssr"]` | Environment names the `clientEntry()` transform treats as "server". |
133
+ | `serverHandler` | `boolean` | `true` | Serve dev requests through your server entry. Set `false` when `@cloudflare/vite-plugin` or `nitro/vite` owns dev-time request handling. Netlify's plugin does not serve SSR, so keep the default there. |
134
134
 
135
135
  ## Prerendering
136
136
 
@@ -429,10 +429,10 @@ dist/
429
429
  | ----------- | -------------- |
430
430
  | `vite` | 8.1.5 |
431
431
  | `vite-plus` | 0.2.6 |
432
- | `remix` | 3.0.0-beta.10 |
433
- | Node | 24 LTS, 25 |
432
+ | `remix` | 3.0.0-rc.2 |
433
+ | Node | 24 LTS, 26 |
434
434
 
435
- Remix 3 is in beta; each `@pitlane/dev` release records the exact beta it was verified against. Rolldown is not required — the transform runs identically on generic Vite and Vite+.
435
+ Remix 3 is in prerelease; each `@pitlane/dev` release records the exact prerelease it was verified against. Rolldown is not required — the transform runs identically on generic Vite and Vite+.
436
436
 
437
437
  ### Troubleshooting
438
438
 
package/dist/index.d.mts CHANGED
@@ -94,7 +94,9 @@ interface RemixPluginOptions {
94
94
  /**
95
95
  * Serve dev-server requests through the server entry's fetch handler.
96
96
  * Set to `false` when another plugin owns dev-time request handling —
97
- * e.g. `@cloudflare/vite-plugin`, `@netlify/vite-plugin`, or `nitro/vite`.
97
+ * `@cloudflare/vite-plugin` (workerd) or `nitro/vite`. Keep the default
98
+ * with `@netlify/vite-plugin`, which emulates platform primitives around
99
+ * the dev server but leaves SSR to the app's fetch handler.
98
100
  *
99
101
  * Ignored when `server` is `false`, which has no fetch handler.
100
102
  *
package/dist/index.mjs CHANGED
@@ -635,12 +635,18 @@ const SERVER_UPDATE_SETTLE_MS = 50;
635
635
  * preserving live component state.
636
636
  *
637
637
  * `ui-hmr` only recognizes named-function component forms, so arrow-form
638
- * exports (`export const Name = clientEntry(url, (handle) => …)` and
639
- * `export const Name = (handle) => …`) are first normalized to named function
638
+ * exports (`export let Name = clientEntry(url, (handle) => …)` and
639
+ * `export let Name = (handle) => …`) are first normalized to named function
640
640
  * expressions — the idiomatic Remix authoring style then hot-swaps without any
641
641
  * source changes. The normalization is discarded when `ui-hmr` does not
642
642
  * instrument the module, so non-component arrows are never rewritten.
643
643
  *
644
+ * `ui-hmr` instruments any exported PascalCase function whose body returns
645
+ * something, which is a wider net than a Remix component: an `async` function,
646
+ * a generator, or a plain JSX helper all match, and all three are miscompiled
647
+ * by it. Modules holding one are left alone entirely. See
648
+ * {@link findComponentExports}.
649
+ *
644
650
  * The browser transform runs in the client environment and the server transform
645
651
  * in the server environment(s); both emit the standard `import.meta.hot.accept()`
646
652
  * protocol that Vite's own HMR runtime drives.
@@ -659,6 +665,11 @@ function componentHmr(serverEnvironments) {
659
665
  } },
660
666
  handler(code, id) {
661
667
  let source = normalizeArrowComponents(code, id) ?? code;
668
+ let { supported, unsupported } = findComponentExports(source, id);
669
+ if (unsupported.length > 0) {
670
+ if (supported.length > 0) this.warn(`Component HMR is off for this module. These exports are PascalCase but are not Remix component setups, which are synchronous and return a render function: ${unsupported.join(", ")}. Moving or renaming them lets ${supported.join(", ")} hot-swap again.`);
671
+ return;
672
+ }
662
673
  let result = serverEnvironments.has(this.environment.name) ? transformComponentsForServer(source, {
663
674
  importSource: "remix",
664
675
  moduleUrl: id,
@@ -724,11 +735,11 @@ function serverDataHmr(serverEnvironments) {
724
735
  *
725
736
  * Handles the two idiomatic Remix arrow forms:
726
737
  *
727
- * - `export const Name = clientEntry(url, (handle) => …)`
728
- * - `export const Name = (handle) => () => <jsx/>`
738
+ * - `export let Name = clientEntry(url, (handle) => …)`
739
+ * - `export let Name = (handle) => () => <jsx/>`
729
740
  *
730
- * Both become `export const Name = clientEntry(url, function Name(handle) { … })`
731
- * / `export const Name = function Name(handle) { … }`, which is behavior-
741
+ * Both become `export let Name = clientEntry(url, function Name(handle) { … })`
742
+ * / `export let Name = function Name(handle) { … }`, which is behavior-
732
743
  * identical for component setup functions (they never rely on a lexical `this`
733
744
  * or `arguments`).
734
745
  */
@@ -770,9 +781,110 @@ function getNormalizableArrow(init) {
770
781
  /** A Remix component setup returns a render function; that is the HMR signal. */
771
782
  function returnsRenderFunction(arrow) {
772
783
  let body = arrow.body;
773
- if (body.type === "ArrowFunctionExpression" || body.type === "FunctionExpression") return true;
774
- if (body.type === "BlockStatement") return body.body.some((statement) => statement.type === "ReturnStatement" && (statement.argument?.type === "ArrowFunctionExpression" || statement.argument?.type === "FunctionExpression"));
775
- return false;
784
+ if (body.type !== "BlockStatement") return isRenderFunction(body);
785
+ return isRenderFunction(getRenderArgument(body));
786
+ }
787
+ /**
788
+ * The expression `ui-hmr` hoists out of a setup body and re-registers on every
789
+ * update: the argument of the first top-level `return`. Mirrors its own
790
+ * `getRenderArgument`, so this module agrees with it about what it will match.
791
+ */
792
+ function getRenderArgument(body) {
793
+ return body.body.find((node) => node.type === "ReturnStatement")?.argument ?? void 0;
794
+ }
795
+ function isRenderFunction(node) {
796
+ return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
797
+ }
798
+ /**
799
+ * Splits a module's PascalCase exports into the ones `remix/ui-hmr` can
800
+ * instrument and the ones it would instrument but miscompile.
801
+ *
802
+ * All `ui-hmr` asks is whether an exported PascalCase function returns
803
+ * something, which catches three shapes it cannot handle. An `async` setup or
804
+ * a generator has its body moved into a plain arrow, so the `await` or `yield`
805
+ * stops parsing and the module — along with everything importing it — fails to
806
+ * load. A function returning an element rather than a render function is
807
+ * rewritten to return a function, so anything calling it directly gets the
808
+ * wrong value back. A `clientEntry()` setup with no `return` at all throws
809
+ * inside the transform.
810
+ *
811
+ * Instrumentation is per module, so a single one of those turns component HMR
812
+ * off for the whole file. Losing the hot swap beats emitting a module that
813
+ * does not run.
814
+ */
815
+ function findComponentExports(code, id) {
816
+ let program = parseSync(id, code).program;
817
+ let exportedNames = getExportedNames(program);
818
+ let supported = [];
819
+ let unsupported = [];
820
+ for (let item of program.body) {
821
+ let statement = item.type === "ExportNamedDeclaration" && item.declaration ? item.declaration : item;
822
+ let directExport = statement !== item;
823
+ if (statement.type === "FunctionDeclaration") {
824
+ let name = statement.id?.name;
825
+ if (!name || !isPascalCase(name)) continue;
826
+ if (!directExport && !exportedNames.has(name)) continue;
827
+ if (!statement.body || !getRenderArgument(statement.body)) continue;
828
+ if (isComponentSetup(statement)) supported.push(name);
829
+ else unsupported.push(name);
830
+ continue;
831
+ }
832
+ if (statement.type !== "VariableDeclaration") continue;
833
+ for (let declarator of statement.declarations) {
834
+ if (declarator.id.type !== "Identifier") continue;
835
+ let name = declarator.id.name;
836
+ if (!isPascalCase(name)) continue;
837
+ if (!directExport && !exportedNames.has(name)) continue;
838
+ let init = declarator.init;
839
+ if (!init) continue;
840
+ let setup = getClientEntrySetup(init);
841
+ if (!setup) {
842
+ if (init.type !== "FunctionExpression") continue;
843
+ if (!init.body || !getRenderArgument(init.body)) continue;
844
+ setup = init;
845
+ }
846
+ if (isComponentSetup(setup)) supported.push(name);
847
+ else unsupported.push(name);
848
+ }
849
+ }
850
+ return {
851
+ supported,
852
+ unsupported
853
+ };
854
+ }
855
+ /** The documented shape: a synchronous setup that returns a render function. */
856
+ function isComponentSetup(setup) {
857
+ if (setup.async || setup.generator || !setup.body) return false;
858
+ return isRenderFunction(getRenderArgument(setup.body));
859
+ }
860
+ /**
861
+ * The setup inside `clientEntry(url, setup)`, including the
862
+ * `clientEntry(url, wrap(setup))` form `ui-hmr` also unwraps.
863
+ */
864
+ function getClientEntrySetup(init) {
865
+ if (init.type !== "CallExpression") return void 0;
866
+ if (init.callee.type !== "Identifier" || init.callee.name !== "clientEntry") return void 0;
867
+ let candidate = init.arguments[1];
868
+ if (candidate?.type === "FunctionExpression") return candidate;
869
+ if (candidate?.type !== "CallExpression") return void 0;
870
+ let inner = candidate.arguments[0];
871
+ return inner?.type === "FunctionExpression" ? inner : void 0;
872
+ }
873
+ /**
874
+ * PascalCase names re-exported under their own name (`export { Card }`), which
875
+ * `ui-hmr` instruments alongside `export function`. An alias
876
+ * (`export { CardImpl as Card }`) is not one of them.
877
+ */
878
+ function getExportedNames(program) {
879
+ let names = /* @__PURE__ */ new Set();
880
+ for (let item of program.body) {
881
+ if (item.type !== "ExportNamedDeclaration") continue;
882
+ for (let { exported, local } of item.specifiers) {
883
+ if (local.type !== "Identifier" || exported.type !== "Identifier") continue;
884
+ if (local.name === exported.name && isPascalCase(local.name)) names.add(local.name);
885
+ }
886
+ }
887
+ return names;
776
888
  }
777
889
  /** Source of the arrow's parameter list, always parenthesized. */
778
890
  function getParamsSource(code, arrow) {
@@ -825,7 +937,7 @@ function preview$1() {
825
937
  //#region src/transform.ts
826
938
  const CLIENT_ENTRY_PATTERN = /\bclientEntry\b/;
827
939
  /**
828
- * Rewrites `export const Name = clientEntry(import.meta.url, …)` so the first
940
+ * Rewrites `export let Name = clientEntry(import.meta.url, …)` so the first
829
941
  * argument resolves to a production asset URL carrying an `#ExportName`
830
942
  * fragment.
831
943
  *
@@ -866,10 +978,11 @@ function clientEntryTransform(serverEnvironments) {
866
978
  };
867
979
  }
868
980
  /**
869
- * Matches exactly `export const Name = clientEntry(import.meta.url, …)` at the
870
- * top level, with at least two arguments. Default exports, aliased callees,
871
- * and non-exported calls are intentionally ignored the `#Name` fragment
872
- * requires a named export.
981
+ * Matches exactly `export let Name = clientEntry(import.meta.url, …)` at the
982
+ * top level, with at least two arguments. Any declaration kind (`let`, `const`,
983
+ * `var`) qualifies; the export name is what matters. Default exports, aliased
984
+ * callees, and non-exported calls are intentionally ignored — the `#Name`
985
+ * fragment requires a named export.
873
986
  */
874
987
  function findClientEntryCalls(program) {
875
988
  let results = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pitlane/dev",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "remix() — the Remix 3 Vite plugin: build orchestration, clientEntry() hydration transform, dev server with component and server-data HMR, SPA mode, build-time prerendering, and preview for any Vite or Vite+ project.",
5
5
  "keywords": [
6
6
  "pitlane",
@@ -42,20 +42,20 @@
42
42
  "@hiogawa/vite-plugin-fullstack": "0.0.11",
43
43
  "magic-string": "^0.30.21",
44
44
  "oxc-parser": "^0.141.0",
45
- "@pitlane/crawler": "^0.1.0"
45
+ "@pitlane/crawler": "^0.2.1"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@cloudflare/vite-plugin": "^1.31.0",
49
49
  "@types/node": "^25.5.0",
50
50
  "playwright": "1.61.1",
51
- "remix": "3.0.0-beta.10",
51
+ "remix": "3.0.0-rc.2",
52
52
  "typescript": "^7.0.2",
53
53
  "vite": "^8.1.5",
54
54
  "vite-plus": "^0.2.6",
55
55
  "wrangler": "^4.114.0"
56
56
  },
57
57
  "peerDependencies": {
58
- "remix": "^3.0.0-beta.10",
58
+ "remix": "^3.0.0-rc.1",
59
59
  "vite": ">=7.0.0"
60
60
  },
61
61
  "engines": {