@pitlane/dev 0.6.0 → 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,28 @@
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
+
3
26
  ## 0.6.0
4
27
 
5
28
  Target Remix `3.0.0-rc.1`.
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-rc.1` and `vite@>=7` as peers. Tested against **Vite 8.1** (Rolldown), **Vite+ 0.2** (`vp`), and `remix@3.0.0-rc.1` — 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
 
@@ -429,7 +429,7 @@ dist/
429
429
  | ----------- | -------------- |
430
430
  | `vite` | 8.1.5 |
431
431
  | `vite-plus` | 0.2.6 |
432
- | `remix` | 3.0.0-rc.1 |
432
+ | `remix` | 3.0.0-rc.2 |
433
433
  | Node | 24 LTS, 26 |
434
434
 
435
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+.
package/dist/index.mjs CHANGED
@@ -641,6 +641,12 @@ const SERVER_UPDATE_SETTLE_MS = 50;
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,
@@ -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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pitlane/dev",
3
- "version": "0.6.0",
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,13 +42,13 @@
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.2.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-rc.1",
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",