@timber-js/app 0.1.18 → 0.1.20
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/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +68 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/router-ref.d.ts.map +1 -1
- package/dist/client/use-params.d.ts +21 -2
- package/dist/client/use-params.d.ts.map +1 -1
- package/dist/client/use-router.d.ts.map +1 -1
- package/dist/index.js +4 -35
- package/dist/index.js.map +1 -1
- package/dist/plugins/shims.d.ts +8 -13
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/shims/link.d.ts +5 -2
- package/dist/shims/link.d.ts.map +1 -1
- package/dist/shims/navigation-client.d.ts +7 -6
- package/dist/shims/navigation-client.d.ts.map +1 -1
- package/dist/shims/navigation.d.ts +4 -9
- package/dist/shims/navigation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/browser-entry.ts +16 -4
- package/src/client/index.ts +1 -1
- package/src/client/router-ref.ts +6 -5
- package/src/client/use-params.ts +71 -7
- package/src/client/use-router.ts +1 -3
- package/src/plugins/shims.ts +12 -67
- package/src/shims/link.ts +5 -2
- package/src/shims/navigation-client.ts +12 -7
- package/src/shims/navigation.ts +10 -11
package/dist/plugins/shims.d.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* timber-shims — Vite sub-plugin for next/* → timber shim resolution
|
|
3
|
-
* and #/ subpath import canonicalization.
|
|
2
|
+
* timber-shims — Vite sub-plugin for next/* → timber shim resolution.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* (nuqs, next-intl, etc.) to work unmodified.
|
|
9
|
-
* 2. Canonicalizes #/* subpath imports (package.json "imports" field) to
|
|
10
|
-
* absolute file paths, preventing Vite dev from creating duplicate
|
|
11
|
-
* module instances when the same file is reached via different import
|
|
12
|
-
* paths (e.g., #/client/router-ref.js vs ./router-ref.js).
|
|
4
|
+
* Intercepts imports of next/* modules and redirects them to timber.js
|
|
5
|
+
* shim implementations. This enables Next.js-compatible libraries
|
|
6
|
+
* (nuqs, next-intl, etc.) to work unmodified.
|
|
13
7
|
*
|
|
14
|
-
* NOTE: This plugin does NOT resolve @timber-js/app/* subpath imports
|
|
15
|
-
*
|
|
16
|
-
*
|
|
8
|
+
* NOTE: This plugin does NOT resolve @timber-js/app/* subpath imports.
|
|
9
|
+
* Those are handled by Vite's native package.json `exports` resolution,
|
|
10
|
+
* which maps them to dist/ files. This ensures a single module instance
|
|
11
|
+
* for shared modules like request-context (ALS singleton).
|
|
17
12
|
*
|
|
18
13
|
* Design doc: 18-build-system.md §"Shim Map"
|
|
19
14
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shims.d.ts","sourceRoot":"","sources":["../../src/plugins/shims.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"shims.d.ts","sourceRoot":"","sources":["../../src/plugins/shims.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAGnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAiDhD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAoIvD"}
|
package/dist/shims/link.d.ts
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Re-exports timber's Link component so libraries that import next/link
|
|
5
5
|
* get the timber equivalent without modification.
|
|
6
|
+
*
|
|
7
|
+
* Imports from @timber-js/app/client (not #/) so the component resolves
|
|
8
|
+
* to the same module instance as user code in Vite dev.
|
|
6
9
|
*/
|
|
7
|
-
export { Link as default, Link } from '
|
|
8
|
-
export type { LinkProps } from '
|
|
10
|
+
export { Link as default, Link } from '@timber-js/app/client';
|
|
11
|
+
export type { LinkProps } from '@timber-js/app/client';
|
|
9
12
|
//# sourceMappingURL=link.d.ts.map
|
package/dist/shims/link.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/shims/link.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"link.d.ts","sourceRoot":"","sources":["../../src/shims/link.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,IAAI,EAAE,MAAM,uBAAuB,CAAC;AAC9D,YAAY,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shim: next/navigation (client environment only)
|
|
3
3
|
*
|
|
4
|
-
* Re-exports only the client-side hooks from timber's
|
|
4
|
+
* Re-exports only the client-side hooks from timber's client API.
|
|
5
5
|
* Server-only functions (redirect, notFound, etc.) are excluded to prevent
|
|
6
6
|
* server/primitives.ts from being pulled into the browser bundle.
|
|
7
7
|
*
|
|
8
8
|
* The full shim (navigation.ts) is still used in the RSC and SSR environments
|
|
9
9
|
* where both client hooks and server functions are needed.
|
|
10
10
|
*
|
|
11
|
+
* Imports use @timber-js/app/client (not #/ subpath imports) so they resolve
|
|
12
|
+
* to the same module instances as user code in Vite dev — preventing module
|
|
13
|
+
* duplication where setGlobalRouter() writes to one instance and useRouter()
|
|
14
|
+
* reads from another.
|
|
15
|
+
*
|
|
11
16
|
* See design/14-ecosystem.md §"next/navigation" for the full shim audit.
|
|
12
17
|
*/
|
|
13
|
-
export { useParams } from '
|
|
14
|
-
export { usePathname } from '#/client/use-pathname.js';
|
|
15
|
-
export { useSearchParams } from '#/client/use-search-params.js';
|
|
16
|
-
export { useRouter } from '#/client/use-router.js';
|
|
17
|
-
export { useSelectedLayoutSegment, useSelectedLayoutSegments, } from '#/client/use-selected-layout-segment.js';
|
|
18
|
+
export { useParams, usePathname, useSearchParams, useRouter, useSelectedLayoutSegment, useSelectedLayoutSegments, } from '@timber-js/app/client';
|
|
18
19
|
export declare const RedirectType: {
|
|
19
20
|
readonly push: "push";
|
|
20
21
|
readonly replace: "replace";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation-client.d.ts","sourceRoot":"","sources":["../../src/shims/navigation-client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"navigation-client.d.ts","sourceRoot":"","sources":["../../src/shims/navigation-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EACL,SAAS,EACT,WAAW,EACX,eAAe,EACf,SAAS,EACT,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAI/B,eAAO,MAAM,YAAY;;;CAGf,CAAC;AAKX,wBAAgB,QAAQ,IAAI,KAAK,CAKhC;AAED,wBAAgB,iBAAiB,IAAI,KAAK,CAKzC;AAED,wBAAgB,QAAQ,IAAI,KAAK,CAIhC"}
|
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shim: next/navigation → timber navigation primitives
|
|
3
3
|
*
|
|
4
|
-
* Client hooks
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* for ALS singleton consistency.
|
|
4
|
+
* Client hooks import from @timber-js/app/client (the public barrel) so they
|
|
5
|
+
* resolve to the same module instances as user code in Vite dev. Server
|
|
6
|
+
* functions import from @timber-js/app/server for ALS singleton consistency.
|
|
8
7
|
*/
|
|
9
|
-
export { useParams } from '
|
|
10
|
-
export { usePathname } from '#/client/use-pathname.js';
|
|
11
|
-
export { useSearchParams } from '#/client/use-search-params.js';
|
|
12
|
-
export { useRouter } from '#/client/use-router.js';
|
|
13
|
-
export { useSelectedLayoutSegment, useSelectedLayoutSegments, } from '#/client/use-selected-layout-segment.js';
|
|
8
|
+
export { useParams, usePathname, useSearchParams, useRouter, useSelectedLayoutSegment, useSelectedLayoutSegments, } from '@timber-js/app/client';
|
|
14
9
|
export { redirect, permanentRedirect, notFound, RedirectType } from '@timber-js/app/server';
|
|
15
10
|
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/shims/navigation.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../src/shims/navigation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EACL,SAAS,EACT,WAAW,EACX,eAAe,EACf,SAAS,EACT,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
|
package/package.json
CHANGED
|
@@ -30,13 +30,25 @@ import {
|
|
|
30
30
|
setServerCallback,
|
|
31
31
|
encodeReply,
|
|
32
32
|
} from '@vitejs/plugin-rsc/browser';
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// Shared-state modules MUST be imported from @timber-js/app/client (the public
|
|
34
|
+
// barrel) so they resolve to the same module instances as user code. In Vite dev,
|
|
35
|
+
// user code imports @timber-js/app/client from dist/ via package.json exports.
|
|
36
|
+
// If we used relative imports (./router-ref.js), Vite would load separate src/
|
|
37
|
+
// copies with separate module-level state — e.g., globalRouter set here but
|
|
38
|
+
// read as null from the dist/ copy used by useRouter().
|
|
39
|
+
import {
|
|
40
|
+
createRouter,
|
|
41
|
+
setGlobalRouter,
|
|
42
|
+
getRouter,
|
|
43
|
+
setCurrentParams,
|
|
44
|
+
} from '@timber-js/app/client';
|
|
45
|
+
import type { RouterDeps, RouterInstance } from '@timber-js/app/client';
|
|
46
|
+
|
|
47
|
+
// Internal-only modules (no shared mutable state with user code) use relative
|
|
48
|
+
// imports — they don't need singleton behavior across module graphs.
|
|
35
49
|
import { applyHeadElements } from './head.js';
|
|
36
|
-
import { setGlobalRouter, getRouter } from './router-ref.js';
|
|
37
50
|
import { TimberNuqsAdapter } from './nuqs-adapter.js';
|
|
38
51
|
import { isPageUnloading } from './unload-guard.js';
|
|
39
|
-
import { setCurrentParams } from './use-params.js';
|
|
40
52
|
import { ON_NAVIGATE_KEY } from './link-navigate-interceptor.js';
|
|
41
53
|
|
|
42
54
|
// ─── Server Action Dispatch ──────────────────────────────────────
|
package/src/client/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ export type {
|
|
|
18
18
|
export { useNavigationPending } from './use-navigation-pending';
|
|
19
19
|
export { useLinkStatus, LinkStatusContext } from './use-link-status';
|
|
20
20
|
export type { LinkStatus } from './use-link-status';
|
|
21
|
-
export { getRouter } from './router-ref';
|
|
21
|
+
export { getRouter, setGlobalRouter } from './router-ref';
|
|
22
22
|
export { useRouter } from './use-router';
|
|
23
23
|
export type { AppRouterInstance } from './use-router';
|
|
24
24
|
export { usePathname } from './use-pathname';
|
package/src/client/router-ref.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// This module has no dependencies on virtual modules, so it can be safely
|
|
3
3
|
// imported by client hooks without pulling in browser-entry's virtual imports.
|
|
4
4
|
//
|
|
5
|
-
// The router is stored as a module-level variable.
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// resolve to the same module instance
|
|
5
|
+
// The router is stored as a module-level variable. browser-entry.ts and all
|
|
6
|
+
// shim files import from @timber-js/app/client (the public barrel) rather
|
|
7
|
+
// than relative paths or #/ subpath imports. This ensures all import chains
|
|
8
|
+
// resolve to the same module instance via Vite's dep optimizer, preventing
|
|
9
|
+
// the duplication that previously required a window.__timber_router workaround.
|
|
9
10
|
//
|
|
10
|
-
// See design/18-build-system.md §"
|
|
11
|
+
// See design/18-build-system.md §"Module Singleton Strategy"
|
|
11
12
|
|
|
12
13
|
import type { RouterInstance } from './router.js';
|
|
13
14
|
|
package/src/client/use-params.ts
CHANGED
|
@@ -18,18 +18,59 @@
|
|
|
18
18
|
* (populated by ssr-entry.ts) to ensure correct per-request isolation
|
|
19
19
|
* across concurrent requests with streaming Suspense.
|
|
20
20
|
*
|
|
21
|
+
* Reactivity: useParams() uses useSyncExternalStore so that components
|
|
22
|
+
* in unchanged layouts (e.g., sidebar items) re-render atomically when
|
|
23
|
+
* params change during client-side navigation. This matches the pattern
|
|
24
|
+
* used by usePathname() and useSearchParams().
|
|
25
|
+
*
|
|
21
26
|
* Design doc: design/09-typescript.md §"Typed Routes"
|
|
22
27
|
*/
|
|
23
28
|
|
|
29
|
+
import { useSyncExternalStore } from 'react';
|
|
24
30
|
import type { Routes } from '#/index.js';
|
|
25
31
|
import { getSsrData } from './ssr-data.js';
|
|
26
32
|
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Module-level state + subscribe/notify pattern
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
// The current params snapshot. Replaced (not mutated) on each navigation
|
|
38
|
+
// so that React's Object.is check on the snapshot detects changes.
|
|
31
39
|
let currentParams: Record<string, string | string[]> = {};
|
|
32
40
|
|
|
41
|
+
// Listeners notified when currentParams changes.
|
|
42
|
+
const listeners = new Set<() => void>();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Subscribe to params changes. Called by useSyncExternalStore.
|
|
46
|
+
* Exported for testing — not intended for direct use by app code.
|
|
47
|
+
*/
|
|
48
|
+
export function subscribe(callback: () => void): () => void {
|
|
49
|
+
listeners.add(callback);
|
|
50
|
+
return () => listeners.delete(callback);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the current params snapshot (client).
|
|
55
|
+
* Exported for testing — not intended for direct use by app code.
|
|
56
|
+
*/
|
|
57
|
+
export function getSnapshot(): Record<string, string | string[]> {
|
|
58
|
+
return currentParams;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the server-side params snapshot (SSR).
|
|
63
|
+
* Falls back to the module-level currentParams if no SSR context
|
|
64
|
+
* is available (shouldn't happen, but defensive).
|
|
65
|
+
*/
|
|
66
|
+
function getServerSnapshot(): Record<string, string | string[]> {
|
|
67
|
+
return getSsrData()?.params ?? currentParams;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Framework API — called by the segment router on each navigation
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
33
74
|
/**
|
|
34
75
|
* Set the current route params. Called by the framework internals
|
|
35
76
|
* during navigation — not intended for direct use by app code.
|
|
@@ -38,11 +79,22 @@ let currentParams: Record<string, string | string[]> = {};
|
|
|
38
79
|
* During SSR, params are also available via getSsrData().params
|
|
39
80
|
* (ALS-backed), but setCurrentParams is still called for the
|
|
40
81
|
* module-level fallback path.
|
|
82
|
+
*
|
|
83
|
+
* After mutation, all useSyncExternalStore subscribers are notified
|
|
84
|
+
* so that every mounted useParams() consumer re-renders in the same
|
|
85
|
+
* React commit — even components in unchanged layouts.
|
|
41
86
|
*/
|
|
42
87
|
export function setCurrentParams(params: Record<string, string | string[]>): void {
|
|
43
88
|
currentParams = params;
|
|
89
|
+
for (const listener of listeners) {
|
|
90
|
+
listener();
|
|
91
|
+
}
|
|
44
92
|
}
|
|
45
93
|
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Public hook
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
46
98
|
/**
|
|
47
99
|
* Read the current route's dynamic params.
|
|
48
100
|
*
|
|
@@ -50,8 +102,8 @@ export function setCurrentParams(params: Record<string, string | string[]>): voi
|
|
|
50
102
|
* it does not affect the runtime return value.
|
|
51
103
|
*
|
|
52
104
|
* During SSR, reads from the ALS-backed SSR data context to ensure
|
|
53
|
-
* per-request isolation. On the client,
|
|
54
|
-
*
|
|
105
|
+
* per-request isolation. On the client, subscribes to the module-level
|
|
106
|
+
* params store via useSyncExternalStore.
|
|
55
107
|
*
|
|
56
108
|
* @overload Typed — when a known route path is passed, returns the
|
|
57
109
|
* exact params shape from the generated Routes interface.
|
|
@@ -67,5 +119,17 @@ export function useParams(_route?: string): Record<string, string | string[]> {
|
|
|
67
119
|
if (ssrData) {
|
|
68
120
|
return ssrData.params;
|
|
69
121
|
}
|
|
70
|
-
|
|
122
|
+
|
|
123
|
+
// useSyncExternalStore requires a React dispatcher (i.e., must be called
|
|
124
|
+
// inside a component render). When called outside a component (e.g., in
|
|
125
|
+
// tests or setup code), fall back to reading the snapshot directly.
|
|
126
|
+
// This mirrors React's own behavior — hooks only work during rendering.
|
|
127
|
+
try {
|
|
128
|
+
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
129
|
+
} catch {
|
|
130
|
+
// No React dispatcher available — return the snapshot directly.
|
|
131
|
+
// This path is hit when useParams() is called outside a component,
|
|
132
|
+
// e.g. in test assertions that verify the current params value.
|
|
133
|
+
return getSnapshot();
|
|
134
|
+
}
|
|
71
135
|
}
|
package/src/client/use-router.ts
CHANGED
|
@@ -50,9 +50,7 @@ export function useRouter(): AppRouterInstance {
|
|
|
50
50
|
const router = getRouterOrNull();
|
|
51
51
|
if (!router) {
|
|
52
52
|
if (process.env.NODE_ENV === 'development') {
|
|
53
|
-
console.error(
|
|
54
|
-
'[timber] useRouter().push() called but router is not initialized. This is a bug — please report it.'
|
|
55
|
-
);
|
|
53
|
+
console.error('[timber] useRouter().push() called but router is not initialized. This is a bug — please report it.');
|
|
56
54
|
}
|
|
57
55
|
return;
|
|
58
56
|
}
|
package/src/plugins/shims.ts
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* timber-shims — Vite sub-plugin for next/* → timber shim resolution
|
|
3
|
-
* and #/ subpath import canonicalization.
|
|
2
|
+
* timber-shims — Vite sub-plugin for next/* → timber shim resolution.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* (nuqs, next-intl, etc.) to work unmodified.
|
|
9
|
-
* 2. Canonicalizes #/* subpath imports (package.json "imports" field) to
|
|
10
|
-
* absolute file paths, preventing Vite dev from creating duplicate
|
|
11
|
-
* module instances when the same file is reached via different import
|
|
12
|
-
* paths (e.g., #/client/router-ref.js vs ./router-ref.js).
|
|
4
|
+
* Intercepts imports of next/* modules and redirects them to timber.js
|
|
5
|
+
* shim implementations. This enables Next.js-compatible libraries
|
|
6
|
+
* (nuqs, next-intl, etc.) to work unmodified.
|
|
13
7
|
*
|
|
14
|
-
* NOTE: This plugin does NOT resolve @timber-js/app/* subpath imports
|
|
15
|
-
*
|
|
16
|
-
*
|
|
8
|
+
* NOTE: This plugin does NOT resolve @timber-js/app/* subpath imports.
|
|
9
|
+
* Those are handled by Vite's native package.json `exports` resolution,
|
|
10
|
+
* which maps them to dist/ files. This ensures a single module instance
|
|
11
|
+
* for shared modules like request-context (ALS singleton).
|
|
17
12
|
*
|
|
18
13
|
* Design doc: 18-build-system.md §"Shim Map"
|
|
19
14
|
*/
|
|
20
15
|
|
|
21
16
|
import type { Plugin } from 'vite';
|
|
22
17
|
import { resolve, dirname } from 'node:path';
|
|
23
|
-
import { existsSync } from 'node:fs';
|
|
24
18
|
import { fileURLToPath } from 'node:url';
|
|
25
19
|
import type { PluginContext } from '#/index.js';
|
|
26
20
|
|
|
@@ -32,7 +26,6 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
32
26
|
const PKG_ROOT = __dirname.endsWith('plugins')
|
|
33
27
|
? resolve(__dirname, '..', '..')
|
|
34
28
|
: resolve(__dirname, '..');
|
|
35
|
-
const SRC_DIR = resolve(PKG_ROOT, 'src');
|
|
36
29
|
const SHIMS_DIR = resolve(PKG_ROOT, 'src', 'shims');
|
|
37
30
|
|
|
38
31
|
/**
|
|
@@ -72,39 +65,6 @@ function stripJsExtension(id: string): string {
|
|
|
72
65
|
return id.endsWith('.js') ? id.slice(0, -3) : id;
|
|
73
66
|
}
|
|
74
67
|
|
|
75
|
-
/**
|
|
76
|
-
* Resolve a #/* subpath import to an absolute file path.
|
|
77
|
-
*
|
|
78
|
-
* The package.json "imports" field maps #/* → ./src/*. Vite's resolver
|
|
79
|
-
* handles this via Node.js subpath imports, but in dev mode the resulting
|
|
80
|
-
* module URL can differ from a relative import to the same file. This
|
|
81
|
-
* causes module duplication — the same file loaded as two separate ES
|
|
82
|
-
* modules with separate module-level state.
|
|
83
|
-
*
|
|
84
|
-
* This function resolves #/foo/bar.js to <PKG_ROOT>/src/foo/bar.ts
|
|
85
|
-
* (trying .ts first, then .tsx, then .js, then the raw path).
|
|
86
|
-
* Returns null if the import is not a #/ import or the file doesn't exist.
|
|
87
|
-
*/
|
|
88
|
-
function resolveSubpathImport(id: string): string | null {
|
|
89
|
-
if (!id.startsWith('#/')) return null;
|
|
90
|
-
|
|
91
|
-
// Strip the #/ prefix and map to src/
|
|
92
|
-
const subpath = id.slice(2);
|
|
93
|
-
const basePath = resolve(SRC_DIR, subpath);
|
|
94
|
-
|
|
95
|
-
// Strip .js extension and try TypeScript extensions first
|
|
96
|
-
const withoutJs = stripJsExtension(basePath);
|
|
97
|
-
for (const ext of ['.ts', '.tsx', '.js']) {
|
|
98
|
-
const candidate = withoutJs + ext;
|
|
99
|
-
if (existsSync(candidate)) return candidate;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Try the raw path (e.g., if it already has the right extension)
|
|
103
|
-
if (existsSync(basePath)) return basePath;
|
|
104
|
-
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
68
|
/**
|
|
109
69
|
* Create the timber-shims Vite plugin.
|
|
110
70
|
*
|
|
@@ -120,21 +80,13 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
120
80
|
enforce: 'pre',
|
|
121
81
|
|
|
122
82
|
/**
|
|
123
|
-
* Resolve imports to
|
|
83
|
+
* Resolve next/* imports to shim files.
|
|
124
84
|
*
|
|
125
85
|
* Resolution order:
|
|
126
86
|
* 1. Check server-only / client-only poison pill packages
|
|
127
|
-
* 2.
|
|
128
|
-
* 3.
|
|
129
|
-
* 4.
|
|
130
|
-
* 5. Handle @timber-js/app/server
|
|
131
|
-
* 6. Return null (pass through) for everything else
|
|
132
|
-
*
|
|
133
|
-
* #/* canonicalization (step 2) prevents module duplication in Vite dev.
|
|
134
|
-
* Package.json "imports" maps #/* → ./src/*, but Vite can resolve the
|
|
135
|
-
* same file to different module URLs depending on the import chain.
|
|
136
|
-
* By resolving #/ imports to absolute paths here, all import paths
|
|
137
|
-
* converge to a single module instance.
|
|
87
|
+
* 2. Strip .js extension from the import specifier
|
|
88
|
+
* 3. Check next/* shim map
|
|
89
|
+
* 4. Return null (pass through) for everything else
|
|
138
90
|
*
|
|
139
91
|
* @timber-js/app/server is resolved to src/ so it shares the same module
|
|
140
92
|
* instance as framework internals (which import via #/). This ensures
|
|
@@ -148,13 +100,6 @@ export function timberShims(_ctx: PluginContext): Plugin {
|
|
|
148
100
|
if (id === 'server-only') return SERVER_ONLY_VIRTUAL;
|
|
149
101
|
if (id === 'client-only') return CLIENT_ONLY_VIRTUAL;
|
|
150
102
|
|
|
151
|
-
// Canonicalize #/* subpath imports to absolute file paths.
|
|
152
|
-
// This is the fix for module duplication (LOCAL-302): ensures that
|
|
153
|
-
// #/client/router-ref.js and ./router-ref.js from the same directory
|
|
154
|
-
// resolve to the same module URL in Vite's module graph.
|
|
155
|
-
const subpathResolved = resolveSubpathImport(id);
|
|
156
|
-
if (subpathResolved) return subpathResolved;
|
|
157
|
-
|
|
158
103
|
const cleanId = stripJsExtension(id);
|
|
159
104
|
|
|
160
105
|
// Check next/* shim map.
|
package/src/shims/link.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Re-exports timber's Link component so libraries that import next/link
|
|
7
7
|
* get the timber equivalent without modification.
|
|
8
|
+
*
|
|
9
|
+
* Imports from @timber-js/app/client (not #/) so the component resolves
|
|
10
|
+
* to the same module instance as user code in Vite dev.
|
|
8
11
|
*/
|
|
9
12
|
|
|
10
|
-
export { Link as default, Link } from '
|
|
11
|
-
export type { LinkProps } from '
|
|
13
|
+
export { Link as default, Link } from '@timber-js/app/client';
|
|
14
|
+
export type { LinkProps } from '@timber-js/app/client';
|
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shim: next/navigation (client environment only)
|
|
3
3
|
*
|
|
4
|
-
* Re-exports only the client-side hooks from timber's
|
|
4
|
+
* Re-exports only the client-side hooks from timber's client API.
|
|
5
5
|
* Server-only functions (redirect, notFound, etc.) are excluded to prevent
|
|
6
6
|
* server/primitives.ts from being pulled into the browser bundle.
|
|
7
7
|
*
|
|
8
8
|
* The full shim (navigation.ts) is still used in the RSC and SSR environments
|
|
9
9
|
* where both client hooks and server functions are needed.
|
|
10
10
|
*
|
|
11
|
+
* Imports use @timber-js/app/client (not #/ subpath imports) so they resolve
|
|
12
|
+
* to the same module instances as user code in Vite dev — preventing module
|
|
13
|
+
* duplication where setGlobalRouter() writes to one instance and useRouter()
|
|
14
|
+
* reads from another.
|
|
15
|
+
*
|
|
11
16
|
* See design/14-ecosystem.md §"next/navigation" for the full shim audit.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
|
-
// Hooks
|
|
15
|
-
export { useParams } from '#/client/use-params.js';
|
|
16
|
-
export { usePathname } from '#/client/use-pathname.js';
|
|
17
|
-
export { useSearchParams } from '#/client/use-search-params.js';
|
|
18
|
-
export { useRouter } from '#/client/use-router.js';
|
|
19
|
+
// Hooks — imported from the public barrel for module singleton consistency.
|
|
19
20
|
export {
|
|
21
|
+
useParams,
|
|
22
|
+
usePathname,
|
|
23
|
+
useSearchParams,
|
|
24
|
+
useRouter,
|
|
20
25
|
useSelectedLayoutSegment,
|
|
21
26
|
useSelectedLayoutSegments,
|
|
22
|
-
} from '
|
|
27
|
+
} from '@timber-js/app/client';
|
|
23
28
|
|
|
24
29
|
// RedirectType enum is safe (no server code dependency) and used by some
|
|
25
30
|
// client-side libraries for type checking.
|
package/src/shims/navigation.ts
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shim: next/navigation → timber navigation primitives
|
|
3
3
|
*
|
|
4
|
-
* Client hooks
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* for ALS singleton consistency.
|
|
4
|
+
* Client hooks import from @timber-js/app/client (the public barrel) so they
|
|
5
|
+
* resolve to the same module instances as user code in Vite dev. Server
|
|
6
|
+
* functions import from @timber-js/app/server for ALS singleton consistency.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
// Hooks (client-side —
|
|
11
|
-
export { useParams } from '#/client/use-params.js';
|
|
12
|
-
export { usePathname } from '#/client/use-pathname.js';
|
|
13
|
-
export { useSearchParams } from '#/client/use-search-params.js';
|
|
14
|
-
export { useRouter } from '#/client/use-router.js';
|
|
9
|
+
// Hooks (client-side — imported from public barrel for module singleton)
|
|
15
10
|
export {
|
|
11
|
+
useParams,
|
|
12
|
+
usePathname,
|
|
13
|
+
useSearchParams,
|
|
14
|
+
useRouter,
|
|
16
15
|
useSelectedLayoutSegment,
|
|
17
16
|
useSelectedLayoutSegments,
|
|
18
|
-
} from '
|
|
17
|
+
} from '@timber-js/app/client';
|
|
19
18
|
|
|
20
|
-
// Functions (server-side
|
|
19
|
+
// Functions (server-side)
|
|
21
20
|
export { redirect, permanentRedirect, notFound, RedirectType } from '@timber-js/app/server';
|