@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.3 → 3.9.0-ultramodern.5
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/cli/index.js +9 -0
- package/dist/cjs/cli/tanstackTypes/source.js +3 -1
- package/dist/cjs/runtime/basepathRewrite.js +13 -6
- package/dist/cjs/runtime/lifecycle.js +7 -7
- package/dist/cjs/runtime/plugin.js +1 -1
- package/dist/cjs/runtime/plugin.node.js +5 -1
- package/dist/cjs/runtime/prefetchLink.js +2 -7
- package/dist/cjs/runtime/register.js +2 -2
- package/dist/cjs/runtime/routeTree/construction.js +2 -0
- package/dist/cjs/runtime/router.js +25 -9
- package/dist/cjs/runtime/state.js +2 -2
- package/dist/cjs/runtime/utils.js +2 -0
- package/dist/esm/cli/index.mjs +9 -0
- package/dist/esm/cli/tanstackTypes/source.mjs +3 -1
- package/dist/esm/runtime/basepathRewrite.mjs +10 -3
- package/dist/esm/runtime/lifecycle.mjs +1 -1
- package/dist/esm/runtime/plugin.mjs +1 -1
- package/dist/esm/runtime/plugin.node.mjs +5 -1
- package/dist/esm/runtime/prefetchLink.mjs +1 -6
- package/dist/esm/runtime/register.mjs +1 -1
- package/dist/esm/runtime/routeTree/construction.mjs +2 -0
- package/dist/esm/runtime/router.mjs +27 -11
- package/dist/esm/runtime/state.mjs +1 -1
- package/dist/esm/runtime/utils.mjs +2 -0
- package/dist/esm-node/cli/index.mjs +9 -0
- package/dist/esm-node/cli/tanstackTypes/source.mjs +3 -1
- package/dist/esm-node/runtime/basepathRewrite.mjs +10 -3
- package/dist/esm-node/runtime/lifecycle.mjs +1 -1
- package/dist/esm-node/runtime/plugin.mjs +1 -1
- package/dist/esm-node/runtime/plugin.node.mjs +5 -1
- package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
- package/dist/esm-node/runtime/register.mjs +1 -1
- package/dist/esm-node/runtime/routeTree/construction.mjs +2 -0
- package/dist/esm-node/runtime/router.mjs +27 -11
- package/dist/esm-node/runtime/state.mjs +1 -1
- package/dist/esm-node/runtime/utils.mjs +2 -0
- package/dist/types/runtime/basepathRewrite.d.ts +2 -1
- package/dist/types/runtime/lifecycle.d.ts +9 -2
- package/dist/types/runtime/register.d.ts +1 -1
- package/dist/types/runtime/router.d.ts +2 -13
- package/dist/types/runtime/state.d.ts +1 -1
- package/dist/types/runtime/types.d.ts +2 -1
- package/package.json +12 -9
- package/rstest.config.mts +1 -6
- package/src/cli/index.ts +10 -0
- package/src/cli/tanstackTypes/source.ts +3 -1
- package/src/runtime/basepathRewrite.ts +11 -2
- package/src/runtime/lifecycle.ts +14 -3
- package/src/runtime/plugin.node.tsx +9 -2
- package/src/runtime/plugin.tsx +5 -1
- package/src/runtime/prefetchLink.tsx +1 -23
- package/src/runtime/register.ts +1 -1
- package/src/runtime/routeTree/construction.ts +2 -0
- package/src/runtime/router.ts +25 -40
- package/src/runtime/state.ts +1 -1
- package/src/runtime/types.ts +2 -2
- package/src/runtime/utils.tsx +3 -1
- package/tests/realm-isolation.test.ts +8 -2
- package/tests/router/cli.test.ts +108 -9
- package/tests/router/dataMutation.test.tsx +17 -121
- package/tests/router/flightSerialization.roundtrip.test.tsx +9 -6
- package/tests/router/generateRouteArtifacts.test.ts +69 -29
- package/tests/router/hooks.test.ts +1 -18
- package/tests/router/loaderBridge.test.ts +0 -77
- package/tests/router/localisedIdentity.test.tsx +95 -0
- package/tests/router/prefetchLink.test.tsx +28 -16
- package/tests/router/prefetchLinkPreload.test.tsx +21 -28
- package/tests/router/register.test.ts +1 -1
- package/tests/router/routeHooks.test.ts +41 -105
- package/tests/router/routeTree.test.ts +0 -212
- package/tests/router/routerCompatibility.test.ts +32 -9
- package/tests/router/rsc.test.tsx +118 -136
- package/tests/router/rscDecoderSecurity.test.ts +24 -53
- package/tests/router/serverPlugin.test.ts +43 -1
- package/tests/router/tanstackTypes.test.ts +24 -364
- package/tests/router/fastDefaults.test.ts +0 -32
- package/tests/router/hydrationBoundary.test.tsx +0 -23
- package/tests/router/packageSurface.test.ts +0 -24
- package/tests/router/preloadRedirect.test.ts +0 -64
- package/tests/router/rscPayloadRouterMatrix.test.ts +0 -197
package/tests/router/cli.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import { mergeConfig } from '@modern-js/plugin/cli';
|
|
7
|
+
import { compile } from '@modern-js/server-utils';
|
|
7
8
|
import type { Entrypoint } from '@modern-js/types';
|
|
8
9
|
import { fs, NESTED_ROUTE_SPEC_FILE } from '@modern-js/utils';
|
|
9
10
|
import {
|
|
@@ -20,6 +21,16 @@ const strictestTsconfigPath = path.resolve(
|
|
|
20
21
|
'../../node_modules/@tsconfig/strictest/tsconfig.json',
|
|
21
22
|
);
|
|
22
23
|
|
|
24
|
+
function createAppContextApi<T extends object>(initial: T) {
|
|
25
|
+
let context: T & { serverCompileExcludedFiles?: string[] } = { ...initial };
|
|
26
|
+
return {
|
|
27
|
+
getAppContext: () => context,
|
|
28
|
+
updateAppContext: (update: Partial<typeof context>) => {
|
|
29
|
+
context = { ...context, ...update };
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
async function typecheckGeneratedRegistration(options: {
|
|
24
35
|
entries: string[];
|
|
25
36
|
generatedDirName: string;
|
|
@@ -201,6 +212,92 @@ describe('tanstack router cli plugin', () => {
|
|
|
201
212
|
}
|
|
202
213
|
});
|
|
203
214
|
|
|
215
|
+
test.each([
|
|
216
|
+
{ sourceDirectory: 'src', generatedDirName: undefined },
|
|
217
|
+
{ sourceDirectory: 'client', generatedDirName: 'routing-types' },
|
|
218
|
+
])('excludes its generated declaration from server roots only when installed: %s', async ({
|
|
219
|
+
sourceDirectory,
|
|
220
|
+
generatedDirName,
|
|
221
|
+
}) => {
|
|
222
|
+
tempDir = await fs.realpath(
|
|
223
|
+
await mkdtemp(path.join(tmpdir(), 'tanstack-server-roots-')),
|
|
224
|
+
);
|
|
225
|
+
const srcDirectory = path.join(tempDir, sourceDirectory);
|
|
226
|
+
const generatedDirectory = generatedDirName ?? 'modern-tanstack';
|
|
227
|
+
const declaration = path.join(
|
|
228
|
+
srcDirectory,
|
|
229
|
+
generatedDirectory,
|
|
230
|
+
'register.gen.d.ts',
|
|
231
|
+
);
|
|
232
|
+
const apiDirectory = path.join(tempDir, 'api');
|
|
233
|
+
const compileOptions = {
|
|
234
|
+
sourceDirs: [apiDirectory],
|
|
235
|
+
distDir: path.join(tempDir, 'dist'),
|
|
236
|
+
tsconfigPath: path.join(tempDir, 'tsconfig.json'),
|
|
237
|
+
throwErrorInsteadOfExit: true,
|
|
238
|
+
};
|
|
239
|
+
await fs.outputJSON(compileOptions.tsconfigPath, {
|
|
240
|
+
compilerOptions: {
|
|
241
|
+
module: 'commonjs',
|
|
242
|
+
target: 'ES2022',
|
|
243
|
+
types: [],
|
|
244
|
+
noEmitOnError: true,
|
|
245
|
+
skipLibCheck: false,
|
|
246
|
+
},
|
|
247
|
+
include: ['api', sourceDirectory],
|
|
248
|
+
});
|
|
249
|
+
await fs.outputFile(
|
|
250
|
+
path.join(apiDirectory, 'index.ts'),
|
|
251
|
+
'export const server = true;\n',
|
|
252
|
+
);
|
|
253
|
+
await fs.outputFile(
|
|
254
|
+
path.join(srcDirectory, 'routes/page.tsx'),
|
|
255
|
+
"import type { Missing } from 'missing-browser-only-types';\nexport const page: Missing = undefined;\n",
|
|
256
|
+
);
|
|
257
|
+
await fs.outputFile(
|
|
258
|
+
path.join(srcDirectory, generatedDirectory, 'main/router.gen.ts'),
|
|
259
|
+
"import { page } from '../../routes/page';\nexport const router = { page };\n",
|
|
260
|
+
);
|
|
261
|
+
await writeTanstackRegisterFile({
|
|
262
|
+
entries: ['main'],
|
|
263
|
+
srcDirectory,
|
|
264
|
+
generatedDirName,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
await expect(compile(tempDir, {}, compileOptions)).rejects.toThrow(
|
|
268
|
+
/missing-browser-only-types/,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const previousExclusions = [path.join(tempDir, 'other-client.d.ts')];
|
|
272
|
+
const api = {
|
|
273
|
+
...createAppContextApi({
|
|
274
|
+
srcDirectory,
|
|
275
|
+
serverCompileExcludedFiles: previousExclusions,
|
|
276
|
+
}),
|
|
277
|
+
_internalRuntimePlugins: () => {},
|
|
278
|
+
checkEntryPoint: () => {},
|
|
279
|
+
config: () => {},
|
|
280
|
+
modifyEntrypoints: () => {},
|
|
281
|
+
generateEntryCode: () => {},
|
|
282
|
+
onFileChanged: () => {},
|
|
283
|
+
modifyFileSystemRoutes: () => {},
|
|
284
|
+
onBeforeGenerateRoutes: () => {},
|
|
285
|
+
};
|
|
286
|
+
tanstackRouterPlugin({ generatedDirName }).setup!(api as never);
|
|
287
|
+
const excludeFiles = api.getAppContext().serverCompileExcludedFiles;
|
|
288
|
+
expect(excludeFiles).toEqual([...previousExclusions, declaration]);
|
|
289
|
+
expect(previousExclusions).toEqual([
|
|
290
|
+
path.join(tempDir, 'other-client.d.ts'),
|
|
291
|
+
]);
|
|
292
|
+
await compile(tempDir, {}, { ...compileOptions, excludeFiles });
|
|
293
|
+
expect(
|
|
294
|
+
await fs.pathExists(path.join(compileOptions.distDir, 'api/index.js')),
|
|
295
|
+
).toBe(true);
|
|
296
|
+
expect(
|
|
297
|
+
await fs.pathExists(path.join(compileOptions.distDir, sourceDirectory)),
|
|
298
|
+
).toBe(false);
|
|
299
|
+
});
|
|
300
|
+
|
|
204
301
|
test('typechecks plugin-owned routers and register metadata under strictest', async () => {
|
|
205
302
|
tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
|
|
206
303
|
const srcDirectory = path.join(tempDir, 'src');
|
|
@@ -345,7 +442,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
345
442
|
|
|
346
443
|
const taps: Record<string, any> = {};
|
|
347
444
|
const api = {
|
|
348
|
-
|
|
445
|
+
...createAppContextApi({
|
|
349
446
|
srcDirectory,
|
|
350
447
|
distDirectory,
|
|
351
448
|
metaName: 'modern-js',
|
|
@@ -446,7 +543,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
446
543
|
test('injects the framework-resolving router wrapper for non-file-route entrypoints', async () => {
|
|
447
544
|
const taps: Record<string, any> = {};
|
|
448
545
|
const api = {
|
|
449
|
-
|
|
546
|
+
...createAppContextApi({
|
|
450
547
|
srcDirectory: '/tmp/app/src',
|
|
451
548
|
metaName: 'modern-js',
|
|
452
549
|
serverRoutes: [{ entryName: 'custom', urlPath: '/' }],
|
|
@@ -508,7 +605,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
508
605
|
test('leaves built-in and foreign-owned route entrypoints to their own router', () => {
|
|
509
606
|
const taps: Record<string, any> = {};
|
|
510
607
|
const api = {
|
|
511
|
-
|
|
608
|
+
...createAppContextApi({
|
|
512
609
|
srcDirectory: '/tmp/app/src',
|
|
513
610
|
metaName: 'modern-js',
|
|
514
611
|
serverRoutes: [{ entryName: 'home', urlPath: '/' }],
|
|
@@ -588,7 +685,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
588
685
|
test('source.include covers the package dist and TanStack runtime deps without string surgery', () => {
|
|
589
686
|
const taps: Record<string, any> = {};
|
|
590
687
|
const api = {
|
|
591
|
-
|
|
688
|
+
...createAppContextApi({
|
|
592
689
|
srcDirectory: '/tmp/app/src',
|
|
593
690
|
serverRoutes: [],
|
|
594
691
|
}),
|
|
@@ -624,7 +721,9 @@ describe('tanstack router cli plugin', () => {
|
|
|
624
721
|
// the old `.replace('cjs', 'esm')` never matched the bundled dist/esm
|
|
625
722
|
// runtime when the CLI was loaded through the ESM condition.
|
|
626
723
|
expect(stringEntries[0]).toBe(path.resolve(__dirname, '..', '..'));
|
|
627
|
-
expect(stringEntries[0]).not.
|
|
724
|
+
expect(stringEntries[0]).not.toMatch(
|
|
725
|
+
/[\\/]dist[\\/]esm(?:-node)?(?:[\\/]|$)/,
|
|
726
|
+
);
|
|
628
727
|
});
|
|
629
728
|
|
|
630
729
|
test('emits the plugin-i18n augmentation only when plugin-i18n is registered', async () => {
|
|
@@ -666,7 +765,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
666
765
|
|
|
667
766
|
const taps: Record<string, any> = {};
|
|
668
767
|
const api = {
|
|
669
|
-
|
|
768
|
+
...createAppContextApi({
|
|
670
769
|
srcDirectory,
|
|
671
770
|
internalSrcAlias: '@/_',
|
|
672
771
|
entrypoints: [entrypoint],
|
|
@@ -763,7 +862,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
763
862
|
|
|
764
863
|
const taps: Record<string, any> = {};
|
|
765
864
|
const api = {
|
|
766
|
-
|
|
865
|
+
...createAppContextApi({
|
|
767
866
|
srcDirectory,
|
|
768
867
|
internalSrcAlias: '@/_',
|
|
769
868
|
entrypoints: [entrypoint],
|
|
@@ -828,7 +927,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
828
927
|
__modernRoutesDir: 'views',
|
|
829
928
|
} as any as Entrypoint;
|
|
830
929
|
const api = {
|
|
831
|
-
|
|
930
|
+
...createAppContextApi({
|
|
832
931
|
srcDirectory: '/tmp/app/src',
|
|
833
932
|
internalSrcAlias: '@/_',
|
|
834
933
|
entrypoints: [entrypoint],
|
|
@@ -876,7 +975,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
876
975
|
test('can opt out of Modern-owned route code splitting', async () => {
|
|
877
976
|
const taps: Record<string, any> = {};
|
|
878
977
|
const api = {
|
|
879
|
-
|
|
978
|
+
...createAppContextApi({
|
|
880
979
|
srcDirectory: '/tmp/app/src',
|
|
881
980
|
serverRoutes: [],
|
|
882
981
|
}),
|
|
@@ -105,54 +105,14 @@ describe('tanstack data mutation fetcher', () => {
|
|
|
105
105
|
states.length = 0;
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
test(
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
render(<FetcherHarness />);
|
|
118
|
-
expect(screen.getByTestId('state').textContent).toBe('idle');
|
|
119
|
-
|
|
120
|
-
let submitPromise: Promise<void> | undefined;
|
|
121
|
-
act(() => {
|
|
122
|
-
submitPromise = latestFetcher!.submit(
|
|
123
|
-
{ amount: 2 },
|
|
124
|
-
{ method: 'post', action: '/mutation' },
|
|
125
|
-
);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
await waitFor(() => {
|
|
129
|
-
expect(screen.getByTestId('state').textContent).toBe('submitting');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
actionResult.resolve(
|
|
133
|
-
new Response(JSON.stringify({ count: 2 }), {
|
|
134
|
-
headers: {
|
|
135
|
-
'Content-Type': 'application/json',
|
|
136
|
-
},
|
|
137
|
-
}),
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
await waitFor(() => {
|
|
141
|
-
expect(screen.getByTestId('state').textContent).toBe('loading');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
invalidateResult.resolve();
|
|
145
|
-
|
|
146
|
-
await act(async () => {
|
|
147
|
-
await submitPromise;
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
expect(screen.getByTestId('state').textContent).toBe('idle');
|
|
151
|
-
expect(screen.getByTestId('data').textContent).toBe('{"count":2}');
|
|
152
|
-
expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('defaults fetcher submit without method to mutation state', async () => {
|
|
108
|
+
test.each([
|
|
109
|
+
{ amount: 2, count: 2, method: 'post' as const, name: 'explicit method' },
|
|
110
|
+
{ amount: 1, count: 1, method: undefined, name: 'default method' },
|
|
111
|
+
])('tracks submitting/loading phases with the $name', async ({
|
|
112
|
+
amount,
|
|
113
|
+
count,
|
|
114
|
+
method,
|
|
115
|
+
}) => {
|
|
156
116
|
const actionResult = createDeferred<Response>();
|
|
157
117
|
const invalidateResult = createDeferred<void>();
|
|
158
118
|
const action = rstest.fn(async () => actionResult.promise);
|
|
@@ -170,8 +130,11 @@ describe('tanstack data mutation fetcher', () => {
|
|
|
170
130
|
let submitPromise: Promise<void> | undefined;
|
|
171
131
|
act(() => {
|
|
172
132
|
submitPromise = latestFetcher!.submit(
|
|
173
|
-
{ amount
|
|
174
|
-
{
|
|
133
|
+
{ amount },
|
|
134
|
+
{
|
|
135
|
+
...(method === undefined ? {} : { method }),
|
|
136
|
+
action: '/mutation',
|
|
137
|
+
},
|
|
175
138
|
);
|
|
176
139
|
});
|
|
177
140
|
|
|
@@ -182,7 +145,7 @@ describe('tanstack data mutation fetcher', () => {
|
|
|
182
145
|
expect(loader).not.toHaveBeenCalled();
|
|
183
146
|
|
|
184
147
|
actionResult.resolve(
|
|
185
|
-
new Response(JSON.stringify({ count
|
|
148
|
+
new Response(JSON.stringify({ count }), {
|
|
186
149
|
headers: {
|
|
187
150
|
'Content-Type': 'application/json',
|
|
188
151
|
},
|
|
@@ -200,44 +163,10 @@ describe('tanstack data mutation fetcher', () => {
|
|
|
200
163
|
});
|
|
201
164
|
|
|
202
165
|
expect(screen.getByTestId('state').textContent).toBe('idle');
|
|
203
|
-
expect(screen.getByTestId('data').textContent).toBe(
|
|
204
|
-
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test('passes built href and matched route params to mutation actions', async () => {
|
|
208
|
-
const action = rstest.fn(
|
|
209
|
-
async ({ request, params }: Parameters<RouteHandler>[0]) => ({
|
|
210
|
-
params,
|
|
211
|
-
requestUrl: request.url,
|
|
212
|
-
}),
|
|
166
|
+
expect(screen.getByTestId('data').textContent).toBe(
|
|
167
|
+
JSON.stringify({ count }),
|
|
213
168
|
);
|
|
214
|
-
|
|
215
|
-
action,
|
|
216
|
-
params: { userId: '42' },
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
render(<FetcherHarness />);
|
|
220
|
-
|
|
221
|
-
await act(async () => {
|
|
222
|
-
await latestFetcher!.submit(
|
|
223
|
-
{ amount: 2 },
|
|
224
|
-
{
|
|
225
|
-
method: 'post',
|
|
226
|
-
action: '/users/42?tab=edit#details',
|
|
227
|
-
},
|
|
228
|
-
);
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
expect(action).toHaveBeenCalledTimes(1);
|
|
232
|
-
const actionArgs = action.mock.calls[0][0];
|
|
233
|
-
const expectedRequestUrl = `${window.location.origin}/users/42?tab=edit#details`;
|
|
234
|
-
expect(actionArgs.params).toEqual({ userId: '42' });
|
|
235
|
-
expect(actionArgs.request.url).toBe(expectedRequestUrl);
|
|
236
|
-
expect(latestFetcher?.data).toEqual({
|
|
237
|
-
params: { userId: '42' },
|
|
238
|
-
requestUrl: expectedRequestUrl,
|
|
239
|
-
});
|
|
240
|
-
expect(currentRouter.invalidate).toHaveBeenCalledTimes(1);
|
|
169
|
+
expect(states).toEqual(['idle', 'submitting', 'loading', 'idle']);
|
|
241
170
|
});
|
|
242
171
|
|
|
243
172
|
test('surfaces non-2xx action responses as fetcher errors', async () => {
|
|
@@ -460,37 +389,4 @@ describe('tanstack data mutation fetcher', () => {
|
|
|
460
389
|
globalThis.FormData = NativeFormData;
|
|
461
390
|
}
|
|
462
391
|
});
|
|
463
|
-
|
|
464
|
-
test('does not throw for Form submit with non-2xx response and still invalidates', async () => {
|
|
465
|
-
const action = rstest.fn(
|
|
466
|
-
async () =>
|
|
467
|
-
new Response(JSON.stringify({ message: 'invalid amount' }), {
|
|
468
|
-
status: 422,
|
|
469
|
-
headers: {
|
|
470
|
-
'Content-Type': 'application/json',
|
|
471
|
-
},
|
|
472
|
-
}),
|
|
473
|
-
);
|
|
474
|
-
currentRouter = createRouter({
|
|
475
|
-
action,
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
render(
|
|
479
|
-
<Form method="post" action="/mutation">
|
|
480
|
-
<button type="submit">submit</button>
|
|
481
|
-
</Form>,
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
const form = document.querySelector('form');
|
|
485
|
-
expect(form).toBeTruthy();
|
|
486
|
-
|
|
487
|
-
await act(async () => {
|
|
488
|
-
fireEvent.submit(form!);
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
await waitFor(() => {
|
|
492
|
-
expect(action).toHaveBeenCalledTimes(1);
|
|
493
|
-
expect(currentRouter.invalidate).toHaveBeenCalledTimes(1);
|
|
494
|
-
});
|
|
495
|
-
});
|
|
496
392
|
});
|
|
@@ -45,21 +45,22 @@ test('round-trips TanStack RSC flight values across supported value types', asyn
|
|
|
45
45
|
const circular: Record<string, unknown> = { marker: 'circular' };
|
|
46
46
|
circular.self = circular;
|
|
47
47
|
|
|
48
|
+
const rscProxy = createRscProxy(() => ({ slot: 'server' }), {
|
|
49
|
+
renderable: true,
|
|
50
|
+
stream: createStream(),
|
|
51
|
+
}) as React.ReactElement & Record<PropertyKey, unknown>;
|
|
52
|
+
|
|
48
53
|
const mapValue = new Map<unknown, unknown>([
|
|
49
54
|
['primitive', 1],
|
|
50
55
|
['shared', shared],
|
|
51
56
|
[shared, circular],
|
|
57
|
+
['rscProxy', rscProxy],
|
|
52
58
|
]);
|
|
53
59
|
mapValue.set('self', mapValue);
|
|
54
60
|
|
|
55
|
-
const setValue = new Set<unknown>(['set-item', shared, circular]);
|
|
61
|
+
const setValue = new Set<unknown>(['set-item', shared, circular, rscProxy]);
|
|
56
62
|
setValue.add(setValue);
|
|
57
63
|
|
|
58
|
-
const rscProxy = createRscProxy(() => ({ slot: 'server' }), {
|
|
59
|
-
renderable: true,
|
|
60
|
-
stream: createStream(),
|
|
61
|
-
}) as React.ReactElement & Record<PropertyKey, unknown>;
|
|
62
|
-
|
|
63
64
|
const fixture: RoundTripFixture = {
|
|
64
65
|
bigintValue: 9007199254740993n,
|
|
65
66
|
booleanValue: true,
|
|
@@ -115,12 +116,14 @@ test('round-trips TanStack RSC flight values across supported value types', asyn
|
|
|
115
116
|
expect(revived.mapValue.get('shared')).toBe(revived.sharedA);
|
|
116
117
|
expect(revived.mapValue.get(revived.sharedA)).toBe(revived.circular);
|
|
117
118
|
expect(revived.mapValue.get('self')).toBe(revived.mapValue);
|
|
119
|
+
expect(isValidElement(revived.mapValue.get('rscProxy'))).toBe(true);
|
|
118
120
|
|
|
119
121
|
expect(revived.setValue).toBeInstanceOf(Set);
|
|
120
122
|
expect(revived.setValue).not.toBe(setValue);
|
|
121
123
|
expect(revived.setValue.has('set-item')).toBe(true);
|
|
122
124
|
expect(revived.setValue.has(revived.sharedA)).toBe(true);
|
|
123
125
|
expect(revived.setValue.has(revived.circular)).toBe(true);
|
|
126
|
+
expect([...revived.setValue].some(value => isValidElement(value))).toBe(true);
|
|
124
127
|
expect(revived.setValue.has(revived.setValue)).toBe(true);
|
|
125
128
|
|
|
126
129
|
expect(serialized.rscProxy).not.toBe(rscProxy);
|
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// createRunOptions against the app dir, cli.init with build-command
|
|
5
|
-
// semantics, the MODERN_ARGV gate that makes analyze.onPrepare fire
|
|
6
|
-
// generateEntryCode, and the absolute config-file resolution that keeps
|
|
7
|
-
// headless runs (cwd != appDirectory) from feeding `false` into node's
|
|
8
|
-
// path APIs.
|
|
1
|
+
// The headless entry must drive the real TanStack artifact writer. Checking
|
|
2
|
+
// only the createRunOptions/cli.init choreography would allow a no-op CLI to
|
|
3
|
+
// pass while leaving router.gen.ts stale or missing.
|
|
9
4
|
const createRunOptionsMock = rstest.fn(async (options: unknown) => ({
|
|
10
5
|
mockRunOptions: true,
|
|
11
6
|
received: options,
|
|
12
7
|
}));
|
|
13
|
-
const cliInitMock = rstest.fn(async (
|
|
14
|
-
appContext: {},
|
|
15
|
-
received: options,
|
|
16
|
-
}));
|
|
8
|
+
const cliInitMock = rstest.fn(async () => ({ appContext: {} }));
|
|
17
9
|
|
|
18
10
|
rstest.mock('@modern-js/app-tools/cli/run', () => ({
|
|
19
11
|
createRunOptions: createRunOptionsMock,
|
|
@@ -26,7 +18,10 @@ import fs from 'node:fs';
|
|
|
26
18
|
import os from 'node:os';
|
|
27
19
|
import path from 'node:path';
|
|
28
20
|
import { INTERNAL_RUNTIME_PLUGINS } from '@modern-js/utils';
|
|
29
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
generateTanstackRouteArtifacts,
|
|
23
|
+
writeTanstackRouterTypesForEntries,
|
|
24
|
+
} from '../../src/cli';
|
|
30
25
|
|
|
31
26
|
function makeAppDir(withConfig: boolean): string {
|
|
32
27
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tanstack-routes-gen-'));
|
|
@@ -46,9 +41,55 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
|
46
41
|
cliInitMock.mockClear();
|
|
47
42
|
});
|
|
48
43
|
|
|
49
|
-
it('
|
|
44
|
+
it('writes a real route artifact through the prepare-only CLI drive', async () => {
|
|
50
45
|
const previousArgv = process.env.MODERN_ARGV;
|
|
51
46
|
const appDirectory = makeAppDir(true);
|
|
47
|
+
const srcDirectory = path.join(appDirectory, 'src');
|
|
48
|
+
const componentPath = path.join(srcDirectory, 'routes', 'page.tsx');
|
|
49
|
+
fs.mkdirSync(path.dirname(componentPath), { recursive: true });
|
|
50
|
+
fs.writeFileSync(
|
|
51
|
+
componentPath,
|
|
52
|
+
'export default function Page() { return null; }\n',
|
|
53
|
+
'utf-8',
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const runOptions = {
|
|
57
|
+
cwd: appDirectory,
|
|
58
|
+
version: '9.9.9-test',
|
|
59
|
+
internalPlugins: INTERNAL_RUNTIME_PLUGINS,
|
|
60
|
+
configFile: path.resolve(appDirectory, 'modern.config'),
|
|
61
|
+
};
|
|
62
|
+
createRunOptionsMock.mockResolvedValue({
|
|
63
|
+
mockRunOptions: true,
|
|
64
|
+
received: runOptions,
|
|
65
|
+
});
|
|
66
|
+
cliInitMock.mockImplementation(async () => {
|
|
67
|
+
await writeTanstackRouterTypesForEntries({
|
|
68
|
+
appContext: {
|
|
69
|
+
srcDirectory,
|
|
70
|
+
internalSrcAlias: '@/_',
|
|
71
|
+
entrypoints: [{ entryName: 'main', isMainEntry: true }],
|
|
72
|
+
} as any,
|
|
73
|
+
routesByEntry: {
|
|
74
|
+
main: [
|
|
75
|
+
{
|
|
76
|
+
type: 'nested',
|
|
77
|
+
id: 'layout',
|
|
78
|
+
isRoot: true,
|
|
79
|
+
children: [
|
|
80
|
+
{
|
|
81
|
+
type: 'nested',
|
|
82
|
+
id: 'page',
|
|
83
|
+
index: true,
|
|
84
|
+
_component: '@/_/routes/page',
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
] as any,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
return { appContext: {} };
|
|
92
|
+
});
|
|
52
93
|
|
|
53
94
|
try {
|
|
54
95
|
await generateTanstackRouteArtifacts({
|
|
@@ -61,26 +102,25 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
|
61
102
|
string,
|
|
62
103
|
unknown
|
|
63
104
|
>;
|
|
64
|
-
expect(runOptionsArg
|
|
65
|
-
expect(runOptionsArg.version).toBe('9.9.9-test');
|
|
66
|
-
expect(runOptionsArg.internalPlugins).toBe(INTERNAL_RUNTIME_PLUGINS);
|
|
67
|
-
|
|
68
|
-
// The config file is resolved to an absolute base path against the app
|
|
69
|
-
// directory (not process.cwd()). Without this, a headless run from a
|
|
70
|
-
// different cwd finds no config, `findExists` returns `false`, and that
|
|
71
|
-
// boolean reaches `path.isAbsolute` deep inside plugin/cli.
|
|
72
|
-
expect(runOptionsArg.configFile).toBe(
|
|
73
|
-
path.resolve(appDirectory, 'modern.config'),
|
|
74
|
-
);
|
|
105
|
+
expect(runOptionsArg).toEqual(runOptions);
|
|
75
106
|
|
|
76
107
|
expect(cliInitMock).toHaveBeenCalledTimes(1);
|
|
77
108
|
const initArg = cliInitMock.mock.calls[0][0] as Record<string, unknown>;
|
|
78
109
|
expect(initArg.command).toBe('build');
|
|
79
110
|
expect(initArg.mockRunOptions).toBe(true);
|
|
80
|
-
|
|
81
|
-
// checkIsBuildCommands() resolves the command from MODERN_ARGV — this
|
|
82
|
-
// is what lets analyze.onPrepare fire generateEntryCode headlessly.
|
|
83
111
|
expect(process.env.MODERN_ARGV).toBe('node modern build');
|
|
112
|
+
|
|
113
|
+
const routerArtifact = fs.readFileSync(
|
|
114
|
+
path.join(srcDirectory, 'modern-tanstack', 'main', 'router.gen.ts'),
|
|
115
|
+
'utf-8',
|
|
116
|
+
);
|
|
117
|
+
const registerArtifact = fs.readFileSync(
|
|
118
|
+
path.join(srcDirectory, 'modern-tanstack', 'register.gen.d.ts'),
|
|
119
|
+
'utf-8',
|
|
120
|
+
);
|
|
121
|
+
expect(routerArtifact).toContain('createRootRouteWithContext');
|
|
122
|
+
expect(routerArtifact).toContain('routeTree');
|
|
123
|
+
expect(registerArtifact).toContain('./main/router.gen');
|
|
84
124
|
} finally {
|
|
85
125
|
fs.rmSync(appDirectory, { force: true, recursive: true });
|
|
86
126
|
if (previousArgv === undefined) {
|
|
@@ -91,7 +131,7 @@ describe('generateTanstackRouteArtifacts (headless routes-generate)', () => {
|
|
|
91
131
|
}
|
|
92
132
|
});
|
|
93
133
|
|
|
94
|
-
it('throws a clear error
|
|
134
|
+
it('throws a clear error when no config file exists', async () => {
|
|
95
135
|
const appDirectory = makeAppDir(false);
|
|
96
136
|
|
|
97
137
|
try {
|
|
@@ -3,13 +3,11 @@ import {
|
|
|
3
3
|
routerProviderRegistryHooks,
|
|
4
4
|
} from '@modern-js/runtime/context';
|
|
5
5
|
import * as tanstackHooks from '../../src/runtime/hooks';
|
|
6
|
-
import { applyRouterRuntimeState } from '../../src/runtime/lifecycle';
|
|
7
6
|
import { tanstackRouterPlugin as browserPlugin } from '../../src/runtime/plugin';
|
|
8
7
|
import { tanstackRouterPlugin as nodePlugin } from '../../src/runtime/plugin.node';
|
|
9
|
-
import { getTanstackRouterState } from '../../src/runtime/state';
|
|
10
8
|
|
|
11
9
|
describe('tanstack router hooks single declaration source', () => {
|
|
12
|
-
test('re-exports the canonical hook instances
|
|
10
|
+
test('re-exports and registers the canonical hook instances', () => {
|
|
13
11
|
// Identity matters: separate instances would split the hook registry
|
|
14
12
|
// between the built-in router wrapper and this provider.
|
|
15
13
|
expect(tanstackHooks.modifyRoutes).toBe(canonicalModifyRoutes);
|
|
@@ -19,22 +17,7 @@ describe('tanstack router hooks single declaration source', () => {
|
|
|
19
17
|
expect(tanstackHooks.modifyRoutes).toBe(
|
|
20
18
|
routerProviderRegistryHooks.modifyRoutes,
|
|
21
19
|
);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('both runtime plugins register the canonical hook registry object', () => {
|
|
25
20
|
expect(browserPlugin().registryHooks).toBe(routerProviderRegistryHooks);
|
|
26
21
|
expect(nodePlugin().registryHooks).toBe(routerProviderRegistryHooks);
|
|
27
22
|
});
|
|
28
23
|
});
|
|
29
|
-
|
|
30
|
-
describe('tanstack router runtime state accessor', () => {
|
|
31
|
-
test('returns undefined before a tanstack router instance is created', () => {
|
|
32
|
-
const context = {};
|
|
33
|
-
|
|
34
|
-
applyRouterRuntimeState(context as never, {
|
|
35
|
-
framework: 'tanstack',
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
expect(getTanstackRouterState(context)).toBeUndefined();
|
|
39
|
-
});
|
|
40
|
-
});
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { isNotFound, isRedirect } from '@tanstack/react-router';
|
|
2
2
|
import {
|
|
3
|
-
createRouteStaticData,
|
|
4
|
-
isAbsoluteUrl,
|
|
5
|
-
mapSplatParamsForModernLoader,
|
|
6
3
|
modernLoaderToTanstack,
|
|
7
4
|
throwTanstackRedirect,
|
|
8
5
|
} from '../../src/runtime/loaderBridge';
|
|
@@ -23,73 +20,6 @@ function catchThrown(fn: () => unknown): unknown {
|
|
|
23
20
|
throw new Error('expected the function to throw');
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
describe('throwTanstackRedirect', () => {
|
|
27
|
-
test('absolute URLs redirect via href (external), not via to', () => {
|
|
28
|
-
// The old inline codegen handler threw `redirect({ href })` INSIDE a
|
|
29
|
-
// try block whose catch replaced it with `redirect({ to: absoluteUrl })`,
|
|
30
|
-
// making TanStack treat the absolute URL as an internal path.
|
|
31
|
-
const thrown = catchThrown(() =>
|
|
32
|
-
throwTanstackRedirect('https://example.com/external'),
|
|
33
|
-
) as RedirectLike;
|
|
34
|
-
|
|
35
|
-
expect(isRedirect(thrown)).toBe(true);
|
|
36
|
-
expect(thrown.options?.href).toBe('https://example.com/external');
|
|
37
|
-
expect(thrown.options?.to).toBeUndefined();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('relative paths redirect via to so the basepath rewrite applies', () => {
|
|
41
|
-
const thrown = catchThrown(() =>
|
|
42
|
-
throwTanstackRedirect('/dashboard'),
|
|
43
|
-
) as RedirectLike;
|
|
44
|
-
|
|
45
|
-
expect(isRedirect(thrown)).toBe(true);
|
|
46
|
-
expect(thrown.options?.to).toBe('/dashboard');
|
|
47
|
-
expect(thrown.options?.href).toBeUndefined();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test('empty location falls back to /', () => {
|
|
51
|
-
const thrown = catchThrown(() => throwTanstackRedirect('')) as RedirectLike;
|
|
52
|
-
expect(thrown.options?.to).toBe('/');
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('isAbsoluteUrl', () => {
|
|
57
|
-
test('detects absolute and relative URLs', () => {
|
|
58
|
-
expect(isAbsoluteUrl('https://example.com/a')).toBe(true);
|
|
59
|
-
expect(isAbsoluteUrl('mailto:x@example.com')).toBe(true);
|
|
60
|
-
expect(isAbsoluteUrl('/internal/path')).toBe(false);
|
|
61
|
-
expect(isAbsoluteUrl('relative')).toBe(false);
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe('mapSplatParamsForModernLoader', () => {
|
|
66
|
-
test('maps TanStack _splat to React Router * only for splat routes', () => {
|
|
67
|
-
expect(
|
|
68
|
-
mapSplatParamsForModernLoader({ _splat: 'a/b', id: '1' }, true),
|
|
69
|
-
).toEqual({ '*': 'a/b', id: '1' });
|
|
70
|
-
expect(
|
|
71
|
-
mapSplatParamsForModernLoader({ _splat: 'a/b', id: '1' }, false),
|
|
72
|
-
).toEqual({ _splat: 'a/b', id: '1' });
|
|
73
|
-
expect(mapSplatParamsForModernLoader({ id: '1' }, true)).toEqual({
|
|
74
|
-
id: '1',
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
describe('createRouteStaticData', () => {
|
|
80
|
-
test('drops empty fields', () => {
|
|
81
|
-
const loader = () => null;
|
|
82
|
-
expect(createRouteStaticData({})).toEqual({});
|
|
83
|
-
expect(createRouteStaticData({ modernRouteId: '' })).toEqual({});
|
|
84
|
-
expect(
|
|
85
|
-
createRouteStaticData({
|
|
86
|
-
modernRouteId: 'page',
|
|
87
|
-
modernRouteLoader: loader,
|
|
88
|
-
}),
|
|
89
|
-
).toEqual({ modernRouteId: 'page', modernRouteLoader: loader });
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
|
|
93
23
|
describe('modernLoaderToTanstack', () => {
|
|
94
24
|
const baseCtx = {
|
|
95
25
|
location: { href: 'http://localhost/products/1' },
|
|
@@ -189,13 +119,6 @@ describe('modernLoaderToTanstack', () => {
|
|
|
189
119
|
expect(isNotFound(thrown)).toBe(true);
|
|
190
120
|
});
|
|
191
121
|
|
|
192
|
-
test('preserves returned non-404 error Responses as loader results', async () => {
|
|
193
|
-
const response = new Response('loader exploded', { status: 500 });
|
|
194
|
-
const loader = modernLoaderToTanstack({ hasSplat: false }, () => response);
|
|
195
|
-
|
|
196
|
-
await expect(loader(baseCtx)).resolves.toBe(response);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
122
|
test('translates redirect Responses thrown synchronously by the loader', () => {
|
|
200
123
|
const loader = modernLoaderToTanstack({ hasSplat: false }, () => {
|
|
201
124
|
throw new Response(null, {
|