one 1.9.8 → 1.9.9
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/cjs/useLoader.cjs +34 -2
- package/dist/cjs/useLoader.js +34 -3
- package/dist/cjs/useLoader.js.map +2 -2
- package/dist/cjs/useLoader.native.js +40 -2
- package/dist/cjs/useLoader.native.js.map +1 -1
- package/dist/cjs/useMatches.cjs +10 -0
- package/dist/cjs/useMatches.js +8 -0
- package/dist/cjs/useMatches.js.map +1 -1
- package/dist/cjs/useMatches.native.js +28 -0
- package/dist/cjs/useMatches.native.js.map +1 -1
- package/dist/cjs/utils/cleanUrl.cjs +1 -1
- package/dist/cjs/utils/cleanUrl.js +1 -1
- package/dist/cjs/utils/cleanUrl.js.map +1 -1
- package/dist/cjs/utils/cleanUrl.native.js +1 -1
- package/dist/cjs/utils/cleanUrl.native.js.map +1 -1
- package/dist/cjs/vite/constants.cjs +7 -3
- package/dist/cjs/vite/constants.js +7 -2
- package/dist/cjs/vite/constants.js.map +1 -1
- package/dist/cjs/vite/constants.native.js +7 -3
- package/dist/cjs/vite/constants.native.js.map +1 -1
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.cjs +12 -3
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.js +12 -3
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.js.map +1 -1
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.native.js +11 -3
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.native.js.map +1 -1
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.test.cjs +81 -0
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.test.js +87 -0
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.test.js.map +1 -1
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.test.native.js +87 -0
- package/dist/cjs/vite/plugins/clientTreeShakePlugin.test.native.js.map +1 -1
- package/dist/cjs/vite/replaceLoader.cjs +4 -2
- package/dist/cjs/vite/replaceLoader.js +4 -3
- package/dist/cjs/vite/replaceLoader.js.map +1 -1
- package/dist/cjs/vite/replaceLoader.native.js +3 -1
- package/dist/cjs/vite/replaceLoader.native.js.map +1 -1
- package/dist/esm/useLoader.js +34 -2
- package/dist/esm/useLoader.js.map +2 -2
- package/dist/esm/useLoader.mjs +34 -3
- package/dist/esm/useLoader.mjs.map +1 -1
- package/dist/esm/useLoader.native.js +40 -3
- package/dist/esm/useLoader.native.js.map +1 -1
- package/dist/esm/useMatches.js +8 -0
- package/dist/esm/useMatches.js.map +1 -1
- package/dist/esm/useMatches.mjs +8 -1
- package/dist/esm/useMatches.mjs.map +1 -1
- package/dist/esm/useMatches.native.js +26 -1
- package/dist/esm/useMatches.native.js.map +1 -1
- package/dist/esm/utils/cleanUrl.js +1 -1
- package/dist/esm/utils/cleanUrl.js.map +1 -1
- package/dist/esm/utils/cleanUrl.mjs +1 -1
- package/dist/esm/utils/cleanUrl.mjs.map +1 -1
- package/dist/esm/utils/cleanUrl.native.js +1 -1
- package/dist/esm/utils/cleanUrl.native.js.map +1 -1
- package/dist/esm/vite/constants.js +7 -2
- package/dist/esm/vite/constants.js.map +1 -1
- package/dist/esm/vite/constants.mjs +6 -3
- package/dist/esm/vite/constants.mjs.map +1 -1
- package/dist/esm/vite/constants.native.js +6 -3
- package/dist/esm/vite/constants.native.js.map +1 -1
- package/dist/esm/vite/plugins/clientTreeShakePlugin.js +13 -4
- package/dist/esm/vite/plugins/clientTreeShakePlugin.js.map +1 -1
- package/dist/esm/vite/plugins/clientTreeShakePlugin.mjs +13 -4
- package/dist/esm/vite/plugins/clientTreeShakePlugin.mjs.map +1 -1
- package/dist/esm/vite/plugins/clientTreeShakePlugin.native.js +12 -4
- package/dist/esm/vite/plugins/clientTreeShakePlugin.native.js.map +1 -1
- package/dist/esm/vite/plugins/clientTreeShakePlugin.test.js +87 -0
- package/dist/esm/vite/plugins/clientTreeShakePlugin.test.js.map +1 -1
- package/dist/esm/vite/plugins/clientTreeShakePlugin.test.mjs +81 -0
- package/dist/esm/vite/plugins/clientTreeShakePlugin.test.mjs.map +1 -1
- package/dist/esm/vite/plugins/clientTreeShakePlugin.test.native.js +87 -0
- package/dist/esm/vite/plugins/clientTreeShakePlugin.test.native.js.map +1 -1
- package/dist/esm/vite/replaceLoader.js +4 -3
- package/dist/esm/vite/replaceLoader.js.map +1 -1
- package/dist/esm/vite/replaceLoader.mjs +4 -2
- package/dist/esm/vite/replaceLoader.mjs.map +1 -1
- package/dist/esm/vite/replaceLoader.native.js +3 -1
- package/dist/esm/vite/replaceLoader.native.js.map +1 -1
- package/package.json +9 -9
- package/src/useLoader.ts +69 -0
- package/src/useMatches.ts +13 -2
- package/src/utils/cleanUrl.ts +1 -1
- package/src/vite/constants.ts +4 -0
- package/src/vite/plugins/clientTreeShakePlugin.test.ts +121 -0
- package/src/vite/plugins/clientTreeShakePlugin.ts +13 -3
- package/src/vite/replaceLoader.ts +23 -9
- package/types/useLoader.d.ts +10 -0
- package/types/useLoader.d.ts.map +1 -1
- package/types/useMatches.d.ts +7 -0
- package/types/useMatches.d.ts.map +1 -1
- package/types/vite/constants.d.ts +1 -0
- package/types/vite/constants.d.ts.map +1 -1
- package/types/vite/plugins/clientTreeShakePlugin.d.ts +1 -1
- package/types/vite/plugins/clientTreeShakePlugin.d.ts.map +1 -1
- package/types/vite/replaceLoader.d.ts.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "one",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.9",
|
|
4
4
|
"license": "BSD-3-Clause",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"setup.mjs",
|
|
@@ -154,17 +154,17 @@
|
|
|
154
154
|
"@react-navigation/routers": "~7.5.1",
|
|
155
155
|
"@swc/core": "^1.14.0",
|
|
156
156
|
"@ungap/structured-clone": "^1.2.0",
|
|
157
|
-
"@vxrn/color-scheme": "1.9.
|
|
158
|
-
"@vxrn/compiler": "1.9.
|
|
159
|
-
"@vxrn/resolve": "1.9.
|
|
160
|
-
"@vxrn/tslib-lite": "1.9.
|
|
161
|
-
"@vxrn/use-isomorphic-layout-effect": "1.9.
|
|
162
|
-
"@vxrn/vite-plugin-metro": "1.9.
|
|
157
|
+
"@vxrn/color-scheme": "1.9.9",
|
|
158
|
+
"@vxrn/compiler": "1.9.9",
|
|
159
|
+
"@vxrn/resolve": "1.9.9",
|
|
160
|
+
"@vxrn/tslib-lite": "1.9.9",
|
|
161
|
+
"@vxrn/use-isomorphic-layout-effect": "1.9.9",
|
|
162
|
+
"@vxrn/vite-plugin-metro": "1.9.9",
|
|
163
163
|
"babel-dead-code-elimination": "1.0.10",
|
|
164
164
|
"babel-plugin-module-resolver": "^5.0.2",
|
|
165
165
|
"citty": "^0.1.6",
|
|
166
166
|
"core-js": "^3.38.1",
|
|
167
|
-
"create-vxrn": "1.9.
|
|
167
|
+
"create-vxrn": "1.9.9",
|
|
168
168
|
"escape-string-regexp": "^5.0.0",
|
|
169
169
|
"expo-linking": "~8.0.8",
|
|
170
170
|
"expo-modules-core": "~3.0.24",
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"use-latest-callback": "^0.2.3",
|
|
190
190
|
"vite": "^7.1.12",
|
|
191
191
|
"vite-tsconfig-paths": "^6.0.5",
|
|
192
|
-
"vxrn": "1.9.
|
|
192
|
+
"vxrn": "1.9.9",
|
|
193
193
|
"ws": "^8.18.0",
|
|
194
194
|
"xxhashjs": "^0.2.2"
|
|
195
195
|
},
|
package/src/useLoader.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { useParams, usePathname } from './hooks'
|
|
|
4
4
|
import { findNearestNotFoundRoute, setNotFoundState } from './notFoundState'
|
|
5
5
|
import { router } from './router/imperative-api'
|
|
6
6
|
import { preloadedLoaderData, preloadingLoader, routeNode } from './router/router'
|
|
7
|
+
import { subscribeToClientMatches, getClientMatchesSnapshot, updateMatchLoaderData } from './useMatches'
|
|
7
8
|
import { getLoaderPath } from './utils/cleanUrl'
|
|
8
9
|
import { dynamicImport } from './utils/dynamicImport'
|
|
9
10
|
import { weakKey } from './utils/weakKey'
|
|
@@ -192,6 +193,14 @@ export async function refetchLoader(pathname: string): Promise<void> {
|
|
|
192
193
|
hasLoadedOnce: true,
|
|
193
194
|
})
|
|
194
195
|
|
|
196
|
+
// also sync to the page match in clientMatches so components using the
|
|
197
|
+
// match-based data path (useLoader with routeId stub) see the update
|
|
198
|
+
const currentMatches = getClientMatchesSnapshot()
|
|
199
|
+
const pageMatch = currentMatches[currentMatches.length - 1]
|
|
200
|
+
if (pageMatch && pageMatch.pathname === pathname) {
|
|
201
|
+
updateMatchLoaderData(pageMatch.routeId, result)
|
|
202
|
+
}
|
|
203
|
+
|
|
195
204
|
recordLoaderTiming?.({
|
|
196
205
|
path: pathname,
|
|
197
206
|
startTime,
|
|
@@ -225,6 +234,28 @@ if (process.env.NODE_ENV === 'development' && typeof window !== 'undefined') {
|
|
|
225
234
|
;(window as any).__oneRefetchLoader = refetchLoader
|
|
226
235
|
}
|
|
227
236
|
|
|
237
|
+
/**
|
|
238
|
+
* Refetch the loader data for a specific match (identified by routeId) by fetching
|
|
239
|
+
* the current page's loader JS and updating only that match entry in clientMatches.
|
|
240
|
+
*
|
|
241
|
+
* Note: the server's loader JS endpoint runs the *page* loader, so for layout
|
|
242
|
+
* routeIds this fetches fresh page data and stores it on the layout match. A
|
|
243
|
+
* dedicated per-layout refetch endpoint would be needed to truly re-run a layout
|
|
244
|
+
* loader in isolation; that can be added in a follow-up.
|
|
245
|
+
*/
|
|
246
|
+
export async function refetchMatchLoader(routeId: string, currentPath: string): Promise<void> {
|
|
247
|
+
const cacheBust = `${Date.now()}`
|
|
248
|
+
const loaderJSUrl = getLoaderPath(currentPath, true, cacheBust)
|
|
249
|
+
|
|
250
|
+
const module = await dynamicImport(loaderJSUrl)?.catch(() => null)
|
|
251
|
+
if (!module?.loader) return
|
|
252
|
+
|
|
253
|
+
const result = await module.loader()
|
|
254
|
+
if (result?.__oneRedirect || result?.__oneError) return
|
|
255
|
+
|
|
256
|
+
updateMatchLoaderData(routeId, result)
|
|
257
|
+
}
|
|
258
|
+
|
|
228
259
|
/**
|
|
229
260
|
* Access loader data with full state control including refetch capability.
|
|
230
261
|
* Use this when you need loading state or refetch; use `useLoader` for just data.
|
|
@@ -281,11 +312,26 @@ export function useLoaderState<
|
|
|
281
312
|
return { data: serverData, refetch: async () => {}, state: 'idle' } as any
|
|
282
313
|
}
|
|
283
314
|
|
|
315
|
+
// detect if the loader stub returns a routeId string (set by clientTreeShakePlugin)
|
|
316
|
+
// this enables looking up data from the matches array (needed for layout loaders)
|
|
317
|
+
const matchRouteId = loader ? (() => {
|
|
318
|
+
const result = loader()
|
|
319
|
+
return typeof result === 'string' && result.startsWith('./') ? result : null
|
|
320
|
+
})() : null
|
|
321
|
+
|
|
322
|
+
// subscribe to clientMatches changes so refetch triggers re-render
|
|
323
|
+
const clientMatches = useSyncExternalStore(
|
|
324
|
+
subscribeToClientMatches,
|
|
325
|
+
getClientMatchesSnapshot,
|
|
326
|
+
getClientMatchesSnapshot
|
|
327
|
+
)
|
|
328
|
+
|
|
284
329
|
// preloaded data from SSR/SSG - only use if server context path matches current path
|
|
285
330
|
const serverContextPath = loaderPropsFromServerContext?.path
|
|
286
331
|
const preloadedData =
|
|
287
332
|
serverContextPath === currentPath ? loaderDataFromServerContext : undefined
|
|
288
333
|
|
|
334
|
+
// all hooks must be called unconditionally to satisfy React's rules of hooks
|
|
289
335
|
const loaderStateEntry = useSyncExternalStore(
|
|
290
336
|
subscribe,
|
|
291
337
|
() => getLoaderState(currentPath, preloadedData),
|
|
@@ -294,6 +340,29 @@ export function useLoaderState<
|
|
|
294
340
|
|
|
295
341
|
const refetch = useCallback(() => refetchLoader(currentPath), [currentPath])
|
|
296
342
|
|
|
343
|
+
// if the loader returns a routeId, look up data from matches instead of loaderState
|
|
344
|
+
// only use this path when the match has data (layouts always have data preserved from SSR;
|
|
345
|
+
// pages might have null loaderData during client navigation if preloading hasn't completed,
|
|
346
|
+
// so they fall through to the regular loaderState path which handles suspension/loading)
|
|
347
|
+
if (matchRouteId) {
|
|
348
|
+
const match = clientMatches.find((m) => m.routeId === matchRouteId)
|
|
349
|
+
if (match && match.loaderData != null) {
|
|
350
|
+
return {
|
|
351
|
+
data: match.loaderData,
|
|
352
|
+
// refetch updates both loaderState (for useLoaderState() consumers without a loader)
|
|
353
|
+
// and the match entry (for this component and useMatches consumers)
|
|
354
|
+
refetch: async () => {
|
|
355
|
+
await refetchLoader(currentPath)
|
|
356
|
+
const fresh = loaderState[currentPath]
|
|
357
|
+
if (fresh?.data != null) {
|
|
358
|
+
updateMatchLoaderData(matchRouteId, fresh.data)
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
state: loaderStateEntry.state,
|
|
362
|
+
} as any
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
297
366
|
// no loader, just return state/refetch for the path
|
|
298
367
|
if (!loader) {
|
|
299
368
|
return {
|
package/src/useMatches.ts
CHANGED
|
@@ -11,12 +11,12 @@ export type RouteMatch = One.RouteMatch
|
|
|
11
11
|
let clientMatches: RouteMatch[] = []
|
|
12
12
|
const clientMatchesListeners = new Set<() => void>()
|
|
13
13
|
|
|
14
|
-
function subscribeToClientMatches(callback: () => void) {
|
|
14
|
+
export function subscribeToClientMatches(callback: () => void) {
|
|
15
15
|
clientMatchesListeners.add(callback)
|
|
16
16
|
return () => clientMatchesListeners.delete(callback)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function getClientMatchesSnapshot() {
|
|
19
|
+
export function getClientMatchesSnapshot() {
|
|
20
20
|
return clientMatches
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -32,6 +32,17 @@ export function setClientMatches(matches: RouteMatch[]) {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Update the loaderData for a single match by routeId, leaving all other matches intact.
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export function updateMatchLoaderData(routeId: string, loaderData: unknown) {
|
|
40
|
+
clientMatches = clientMatches.map((m) => (m.routeId === routeId ? { ...m, loaderData } : m))
|
|
41
|
+
for (const listener of clientMatchesListeners) {
|
|
42
|
+
listener()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
/**
|
|
36
47
|
* Returns an array of all matched routes from root to the current page.
|
|
37
48
|
* Each match contains the route's loader data, params, and route ID.
|
package/src/utils/cleanUrl.ts
CHANGED
|
@@ -47,7 +47,7 @@ export function getPathFromLoaderPath(loaderPath: string) {
|
|
|
47
47
|
loaderPath
|
|
48
48
|
.replace(LOADER_JS_POSTFIX_REGEX, '')
|
|
49
49
|
.replace(/^(\/_one)?\/assets/, '')
|
|
50
|
-
.replace(/_refetch_\d+_
|
|
50
|
+
.replace(/_refetch_\d+_?/, '')
|
|
51
51
|
// decode: __ → _ (escaped underscore), _ → / (path separator)
|
|
52
52
|
.replace(/__|_/g, (match) => (match === '__' ? '_' : '/'))
|
|
53
53
|
)
|
package/src/vite/constants.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export const EMPTY_LOADER_STRING = `export function loader() {return "__vxrn__loader__"};`
|
|
2
2
|
|
|
3
|
+
export function makeLoaderRouteIdStub(routeId: string): string {
|
|
4
|
+
return `export function loader() {return ${JSON.stringify(routeId)}};`
|
|
5
|
+
}
|
|
6
|
+
|
|
3
7
|
export const LoaderDataCache = {}
|
|
4
8
|
|
|
5
9
|
export const SERVER_CONTEXT_POST_RENDER_STRING = `_one_post_render_data_`
|
|
@@ -123,6 +123,127 @@ export default function Page() {
|
|
|
123
123
|
expect(result).toBeUndefined()
|
|
124
124
|
})
|
|
125
125
|
|
|
126
|
+
it('should embed routeId in loader stub when root is provided', async () => {
|
|
127
|
+
const code = `
|
|
128
|
+
import { serverOnlyModule } from 'server-only-pkg'
|
|
129
|
+
import { useLoader } from 'one'
|
|
130
|
+
|
|
131
|
+
export async function loader() {
|
|
132
|
+
return serverOnlyModule()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export default function Layout() {
|
|
136
|
+
const data = useLoader(loader)
|
|
137
|
+
return data
|
|
138
|
+
}
|
|
139
|
+
`
|
|
140
|
+
const result = await transformTreeShakeClient(code, '/project/app/_layout.tsx', '/project')
|
|
141
|
+
expect(result).toBeDefined()
|
|
142
|
+
// routeId should be relative to app/ directory to match route contextKey format
|
|
143
|
+
expect(result!.code).toContain('export function loader() {return "./_layout.tsx"}')
|
|
144
|
+
expect(result!.code).not.toContain('__vxrn__loader__')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should embed routeId with render mode suffix in loader stub', async () => {
|
|
148
|
+
const code = `
|
|
149
|
+
import { serverOnlyModule } from 'server-only-pkg'
|
|
150
|
+
import { useLoader } from 'one'
|
|
151
|
+
|
|
152
|
+
export async function loader() {
|
|
153
|
+
return serverOnlyModule()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default function Layout() {
|
|
157
|
+
const data = useLoader(loader)
|
|
158
|
+
return data
|
|
159
|
+
}
|
|
160
|
+
`
|
|
161
|
+
const result = await transformTreeShakeClient(code, '/project/app/user+ssr.tsx', '/project')
|
|
162
|
+
expect(result).toBeDefined()
|
|
163
|
+
expect(result!.code).toContain('export function loader() {return "./user+ssr.tsx"}')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should fall back to __vxrn__loader__ stub when no root provided', async () => {
|
|
167
|
+
const code = `
|
|
168
|
+
import { serverOnlyModule } from 'server-only-pkg'
|
|
169
|
+
import { useLoader } from 'one'
|
|
170
|
+
|
|
171
|
+
export async function loader() {
|
|
172
|
+
return serverOnlyModule()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default function Page() {
|
|
176
|
+
const data = useLoader(loader)
|
|
177
|
+
return data
|
|
178
|
+
}
|
|
179
|
+
`
|
|
180
|
+
const result = await transformTreeShakeClient(code, '/app/index.tsx')
|
|
181
|
+
expect(result).toBeDefined()
|
|
182
|
+
expect(result!.code).toContain('__vxrn__loader__')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should not transform route file when loader is imported (no inline declaration)', async () => {
|
|
186
|
+
// importing a loader from another file is not a supported pattern
|
|
187
|
+
// the plugin only handles inline loader declarations
|
|
188
|
+
const code = `
|
|
189
|
+
import { loader } from './loaders/my-loader'
|
|
190
|
+
import { useLoader } from 'one'
|
|
191
|
+
|
|
192
|
+
export { loader }
|
|
193
|
+
|
|
194
|
+
export default function Layout() {
|
|
195
|
+
const data = useLoader(loader)
|
|
196
|
+
return data
|
|
197
|
+
}
|
|
198
|
+
`
|
|
199
|
+
const result = await transformTreeShakeClient(code, '/project/app/_layout.tsx', '/project')
|
|
200
|
+
expect(result).toBeUndefined()
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should tree-shake server imports from source file where loader is defined', async () => {
|
|
204
|
+
// when loader is defined in a separate file, that file gets its own transform pass
|
|
205
|
+
// server-only imports should be removed, loader replaced with routeId stub
|
|
206
|
+
const sourceCode = `
|
|
207
|
+
import { db } from 'server-only-db'
|
|
208
|
+
|
|
209
|
+
export async function loader() {
|
|
210
|
+
return db.query('SELECT * FROM users')
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function helperUsedByClient() {
|
|
214
|
+
return 'hello'
|
|
215
|
+
}
|
|
216
|
+
`
|
|
217
|
+
const result = await transformTreeShakeClient(
|
|
218
|
+
sourceCode,
|
|
219
|
+
'/project/app/loaders/my-loader.ts',
|
|
220
|
+
'/project'
|
|
221
|
+
)
|
|
222
|
+
expect(result).toBeDefined()
|
|
223
|
+
// server-only import removed
|
|
224
|
+
expect(result!.code).not.toContain('server-only-db')
|
|
225
|
+
// client-safe export preserved
|
|
226
|
+
expect(result!.code).toContain('helperUsedByClient')
|
|
227
|
+
// loader replaced with stub (routeId relative to app/ dir)
|
|
228
|
+
expect(result!.code).toContain(
|
|
229
|
+
'export function loader() {return "./loaders/my-loader.ts"}'
|
|
230
|
+
)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should not transform re-export from source syntax', async () => {
|
|
234
|
+
// importing/re-exporting a loader from another file is not a supported pattern
|
|
235
|
+
const code = `
|
|
236
|
+
export { loader } from './loaders/shared-loader'
|
|
237
|
+
import { useLoader } from 'one'
|
|
238
|
+
|
|
239
|
+
export default function Page() {
|
|
240
|
+
return 'hello'
|
|
241
|
+
}
|
|
242
|
+
`
|
|
243
|
+
const result = await transformTreeShakeClient(code, '/project/app/page.tsx', '/project')
|
|
244
|
+
expect(result).toBeUndefined()
|
|
245
|
+
})
|
|
246
|
+
|
|
126
247
|
it('should preserve type-only imports during tree shaking', async () => {
|
|
127
248
|
const code = `
|
|
128
249
|
import type { SomeType } from 'types-pkg'
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
findReferencedIdentifiers,
|
|
9
9
|
} from 'babel-dead-code-elimination'
|
|
10
10
|
import type { Plugin } from 'vite'
|
|
11
|
-
import { EMPTY_LOADER_STRING } from '../constants'
|
|
11
|
+
import { EMPTY_LOADER_STRING, makeLoaderRouteIdStub } from '../constants'
|
|
12
12
|
|
|
13
13
|
const traverse = (BabelTraverse['default'] || BabelTraverse) as typeof BabelTraverse
|
|
14
14
|
const generate = (BabelGenerate['default'] ||
|
|
@@ -74,7 +74,7 @@ export const clientTreeShakePlugin = (): Plugin => {
|
|
|
74
74
|
return
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const out = await transformTreeShakeClient(code, id)
|
|
77
|
+
const out = await transformTreeShakeClient(code, id, process.cwd())
|
|
78
78
|
|
|
79
79
|
return out
|
|
80
80
|
},
|
|
@@ -82,7 +82,7 @@ export const clientTreeShakePlugin = (): Plugin => {
|
|
|
82
82
|
} satisfies Plugin
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export async function transformTreeShakeClient(code: string, id: string) {
|
|
85
|
+
export async function transformTreeShakeClient(code: string, id: string, root?: string) {
|
|
86
86
|
if (!/generateStaticParams|loader/.test(code)) {
|
|
87
87
|
return
|
|
88
88
|
}
|
|
@@ -123,6 +123,9 @@ export async function transformTreeShakeClient(code: string, id: string) {
|
|
|
123
123
|
generateStaticParams: false,
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// note: only handles inline declarations (export function loader / export const loader)
|
|
127
|
+
// importing a loader from another file (export { loader } from './other') is not supported
|
|
128
|
+
// and will break client-side tree shaking. loaders must be defined in the route file.
|
|
126
129
|
try {
|
|
127
130
|
traverse(ast, {
|
|
128
131
|
ExportNamedDeclaration(path) {
|
|
@@ -188,6 +191,13 @@ export async function transformTreeShakeClient(code: string, id: string) {
|
|
|
188
191
|
removedFunctions
|
|
189
192
|
.map((key) => {
|
|
190
193
|
if (key === 'loader') {
|
|
194
|
+
if (root) {
|
|
195
|
+
// compute routeId relative to the app/ directory to match route contextKey format
|
|
196
|
+
// contextKeys are like "./_layout.tsx", "./matches-test/page1+ssg.tsx"
|
|
197
|
+
const fromRoot = relative(root, id).replace(/\\/g, '/')
|
|
198
|
+
const routeId = './' + fromRoot.replace(/^app\//, '')
|
|
199
|
+
return makeLoaderRouteIdStub(routeId)
|
|
200
|
+
}
|
|
191
201
|
return EMPTY_LOADER_STRING
|
|
192
202
|
}
|
|
193
203
|
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// matches the routeId stub return value in both minified and non-minified code:
|
|
2
|
+
// non-minified: return "./loader-refetch/index.tsx"
|
|
3
|
+
// minified: return"./loader-refetch/index.tsx"
|
|
4
|
+
const routeIdReturnRegex = /return\s*"\.\/[^"]+"/
|
|
5
|
+
|
|
1
6
|
export function replaceLoader({
|
|
2
7
|
code,
|
|
3
8
|
loaderData,
|
|
@@ -6,18 +11,27 @@ export function replaceLoader({
|
|
|
6
11
|
loaderData: object
|
|
7
12
|
}) {
|
|
8
13
|
const stringifiedData = JSON.stringify(loaderData)
|
|
14
|
+
const safeData = stringifiedData.replace(/\$/g, '$$$$')
|
|
9
15
|
|
|
10
16
|
const out = (() => {
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
// old-style placeholder stub
|
|
18
|
+
if (code.includes('__vxrn__loader__')) {
|
|
19
|
+
return code.replace(
|
|
20
|
+
/["']__vxrn__loader__['"]/,
|
|
21
|
+
// make sure this ' ' is added in front,
|
|
22
|
+
// minifiers will do `return"something"
|
|
23
|
+
// but if its null then it becomes returnnull
|
|
24
|
+
' ' + safeData
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// new-style routeId stub from clientTreeShakePlugin
|
|
29
|
+
// works with both minified (return"./path") and non-minified (return "./path") code
|
|
30
|
+
if (routeIdReturnRegex.test(code)) {
|
|
31
|
+
return code.replace(routeIdReturnRegex, 'return ' + safeData)
|
|
13
32
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// make sure this ' ' is added in front,
|
|
17
|
-
// minifiers will do `return"something"
|
|
18
|
-
// but if its null then it becomes returnnull
|
|
19
|
-
' ' + stringifiedData.replace(/\$/g, '$$$$')
|
|
20
|
-
)
|
|
33
|
+
|
|
34
|
+
return code + `\nexport const loader = () => (${stringifiedData})`
|
|
21
35
|
})()
|
|
22
36
|
|
|
23
37
|
return out
|
package/types/useLoader.d.ts
CHANGED
|
@@ -21,6 +21,16 @@ export declare function getLoaderTimingHistory(): LoaderTimingEntry[];
|
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
export declare function refetchLoader(pathname: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Refetch the loader data for a specific match (identified by routeId) by fetching
|
|
26
|
+
* the current page's loader JS and updating only that match entry in clientMatches.
|
|
27
|
+
*
|
|
28
|
+
* Note: the server's loader JS endpoint runs the *page* loader, so for layout
|
|
29
|
+
* routeIds this fetches fresh page data and stores it on the layout match. A
|
|
30
|
+
* dedicated per-layout refetch endpoint would be needed to truly re-run a layout
|
|
31
|
+
* loader in isolation; that can be added in a follow-up.
|
|
32
|
+
*/
|
|
33
|
+
export declare function refetchMatchLoader(routeId: string, currentPath: string): Promise<void>;
|
|
24
34
|
/**
|
|
25
35
|
* Access loader data with full state control including refetch capability.
|
|
26
36
|
* Use this when you need loading state or refetch; use `useLoader` for just data.
|
package/types/useLoader.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLoader.d.ts","sourceRoot":"","sources":["../src/useLoader.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useLoader.d.ts","sourceRoot":"","sources":["../src/useLoader.ts"],"names":[],"mappings":"AAsBA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAA;CAC1C,CAAA;AAuCD,wBAAgB,sBAAsB,IAAI,iBAAiB,EAAE,CAE5D;AAkCD;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgHnE;AAOD;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW5F;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,cAAc,CAC5B,MAAM,SAAS,QAAQ,GAAG,GAAG,EAC7B,QAAQ,GAAG,MAAM,SAAS,CAAC,CAAC,EAAE,GAAG,KAAK,GAAG,GAAG,UAAU,CAAC,MAAM,CAAC,GAAG,OAAO,EAExE,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,SAAS,SAAS,GACvB;IAAE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAC3D;IACE,IAAI,EAAE,QAAQ,SAAS,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAA;IAClE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5B,KAAK,EAAE,MAAM,GAAG,SAAS,CAAA;CAC1B,CAqWJ;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,SAAS,CACvB,MAAM,SAAS,QAAQ,EACvB,QAAQ,GAAG,MAAM,SAAS,CAAC,CAAC,EAAE,GAAG,KAAK,GAAG,GAAG,UAAU,CAAC,MAAM,CAAC,GAAG,OAAO,EACxE,MAAM,EAAE,MAAM,GAAG,QAAQ,SAAS,OAAO,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAG9E;AAKD,wBAAgB,gBAAgB,SAG/B"}
|
package/types/useMatches.d.ts
CHANGED
|
@@ -3,12 +3,19 @@ import type { One } from './vite/types';
|
|
|
3
3
|
* Re-export for convenience
|
|
4
4
|
*/
|
|
5
5
|
export type RouteMatch = One.RouteMatch;
|
|
6
|
+
export declare function subscribeToClientMatches(callback: () => void): () => boolean;
|
|
7
|
+
export declare function getClientMatchesSnapshot(): One.RouteMatch[];
|
|
6
8
|
/**
|
|
7
9
|
* Update the client-side matches store.
|
|
8
10
|
* Called after navigation to update the matches with new loader data.
|
|
9
11
|
* @internal
|
|
10
12
|
*/
|
|
11
13
|
export declare function setClientMatches(matches: RouteMatch[]): void;
|
|
14
|
+
/**
|
|
15
|
+
* Update the loaderData for a single match by routeId, leaving all other matches intact.
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export declare function updateMatchLoaderData(routeId: string, loaderData: unknown): void;
|
|
12
19
|
/**
|
|
13
20
|
* Returns an array of all matched routes from root to the current page.
|
|
14
21
|
* Each match contains the route's loader data, params, and route ID.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMatches.d.ts","sourceRoot":"","sources":["../src/useMatches.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AAEvC;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,GAAG,CAAC,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"useMatches.d.ts","sourceRoot":"","sources":["../src/useMatches.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AAEvC;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,GAAG,CAAC,UAAU,CAAA;AAMvC,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,IAAI,iBAG5D;AAED,wBAAgB,wBAAwB,qBAEvC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,UAAU,EAAE,QAKrD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,QAKzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,UAAU,IAAI,UAAU,EAAE,CAoBzC;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAGhE;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,IAAI,UAAU,GAAG,SAAS,CAGrD"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const EMPTY_LOADER_STRING = "export function loader() {return \"__vxrn__loader__\"};";
|
|
2
|
+
export declare function makeLoaderRouteIdStub(routeId: string): string;
|
|
2
3
|
export declare const LoaderDataCache: {};
|
|
3
4
|
export declare const SERVER_CONTEXT_POST_RENDER_STRING = "_one_post_render_data_";
|
|
4
5
|
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/vite/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,4DAA0D,CAAA;AAE1F,eAAO,MAAM,eAAe,IAAK,CAAA;AAEjC,eAAO,MAAM,iCAAiC,2BAA2B,CAAA"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/vite/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,4DAA0D,CAAA;AAE1F,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,eAAO,MAAM,eAAe,IAAK,CAAA;AAEjC,eAAO,MAAM,iCAAiC,2BAA2B,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
2
|
export declare const clientTreeShakePlugin: () => Plugin;
|
|
3
|
-
export declare function transformTreeShakeClient(code: string, id: string): Promise<{
|
|
3
|
+
export declare function transformTreeShakeClient(code: string, id: string, root?: string): Promise<{
|
|
4
4
|
code: string;
|
|
5
5
|
map: {
|
|
6
6
|
version: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clientTreeShakePlugin.d.ts","sourceRoot":"","sources":["../../../src/vite/plugins/clientTreeShakePlugin.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AA4ClC,eAAO,MAAM,qBAAqB,QAAO,MA6BxC,CAAA;AAED,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"clientTreeShakePlugin.d.ts","sourceRoot":"","sources":["../../../src/vite/plugins/clientTreeShakePlugin.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AA4ClC,eAAO,MAAM,qBAAqB,QAAO,MA6BxC,CAAA;AAED,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM;;;;;;;;;;;eA6IrF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replaceLoader.d.ts","sourceRoot":"","sources":["../../src/vite/replaceLoader.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"replaceLoader.d.ts","sourceRoot":"","sources":["../../src/vite/replaceLoader.ts"],"names":[],"mappings":"AAKA,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,UAAU,GACX,EAAE;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;CACnB,UA0BA"}
|