@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 +23 -0
- package/README.md +2 -2
- package/dist/index.mjs +115 -3
- package/package.json +3 -3
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.
|
|
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.
|
|
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
|
|
774
|
-
|
|
775
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|