@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 +49 -0
- package/README.md +12 -12
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +127 -14
- package/package.json +4 -4
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-
|
|
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
|
|
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-
|
|
433
|
-
| Node | 24 LTS,
|
|
432
|
+
| `remix` | 3.0.0-rc.2 |
|
|
433
|
+
| Node | 24 LTS, 26 |
|
|
434
434
|
|
|
435
|
-
Remix 3 is in
|
|
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
|
-
*
|
|
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
|
|
639
|
-
* `export
|
|
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
|
|
728
|
-
* - `export
|
|
738
|
+
* - `export let Name = clientEntry(url, (handle) => …)`
|
|
739
|
+
* - `export let Name = (handle) => () => <jsx/>`
|
|
729
740
|
*
|
|
730
|
-
* Both become `export
|
|
731
|
-
* / `export
|
|
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
|
|
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) {
|
|
@@ -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
|
|
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
|
|
870
|
-
* top level, with at least two arguments.
|
|
871
|
-
*
|
|
872
|
-
*
|
|
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.
|
|
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
|
|
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-
|
|
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-
|
|
58
|
+
"remix": "^3.0.0-rc.1",
|
|
59
59
|
"vite": ">=7.0.0"
|
|
60
60
|
},
|
|
61
61
|
"engines": {
|