@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.4 → 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/runtime/lifecycle.js +7 -7
- package/dist/cjs/runtime/prefetchLink.js +2 -7
- package/dist/cjs/runtime/register.js +2 -2
- package/dist/cjs/runtime/router.js +25 -9
- package/dist/cjs/runtime/state.js +2 -2
- package/dist/esm/cli/index.mjs +9 -0
- package/dist/esm/runtime/lifecycle.mjs +1 -1
- package/dist/esm/runtime/prefetchLink.mjs +1 -6
- package/dist/esm/runtime/register.mjs +1 -1
- package/dist/esm/runtime/router.mjs +27 -11
- package/dist/esm/runtime/state.mjs +1 -1
- package/dist/esm-node/cli/index.mjs +9 -0
- package/dist/esm-node/runtime/lifecycle.mjs +1 -1
- package/dist/esm-node/runtime/prefetchLink.mjs +1 -6
- package/dist/esm-node/runtime/register.mjs +1 -1
- package/dist/esm-node/runtime/router.mjs +27 -11
- package/dist/esm-node/runtime/state.mjs +1 -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 -10
- package/rstest.config.mts +0 -6
- package/src/cli/index.ts +10 -0
- package/src/runtime/lifecycle.ts +14 -3
- package/src/runtime/plugin.node.tsx +1 -1
- package/src/runtime/prefetchLink.tsx +1 -23
- package/src/runtime/register.ts +1 -1
- package/src/runtime/router.ts +25 -40
- package/src/runtime/state.ts +1 -1
- package/src/runtime/types.ts +2 -2
- package/tests/realm-isolation.test.ts +8 -2
- package/tests/router/cli.test.ts +105 -8
- 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/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/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/src/runtime/router.ts
CHANGED
|
@@ -1,43 +1,28 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* The CLI plugin injects `{ name: 'router', path: '<pkg>/runtime/router' }`
|
|
7
|
-
* for those entries. This module creates a framework-resolving wrapper whose
|
|
8
|
-
* app-owned provider realm contains this module graph's TanStack factory.
|
|
9
|
-
*/
|
|
10
|
-
import * as runtimeRouter from '@modern-js/runtime/router/internal';
|
|
1
|
+
/** App-local composition for custom entries using runtime router selection. */
|
|
2
|
+
import { routerProviderRegistryHooks } from '@modern-js/runtime/context';
|
|
3
|
+
import { routerPlugin as nativeRouterPlugin } from '@modern-js/runtime/router/internal';
|
|
4
|
+
import { createRouterPlugin } from '@modern-js/runtime-extensions/router-provider';
|
|
5
|
+
import { createRouterStatePlugin } from '@modern-js/runtime-extensions/router-state-plugin';
|
|
11
6
|
import { tanstackRouterProviderFactory } from './register';
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
factory:
|
|
18
|
-
|
|
19
|
-
)
|
|
8
|
+
const createProviderPlugin = createRouterPlugin({
|
|
9
|
+
defaultProvider: { name: 'react-router', factory: nativeRouterPlugin },
|
|
10
|
+
registryHooks: routerProviderRegistryHooks,
|
|
11
|
+
localProviders: [
|
|
12
|
+
{ name: 'tanstack', factory: tanstackRouterProviderFactory },
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
const statePlugin = createRouterStatePlugin({
|
|
16
|
+
registryHooks: routerProviderRegistryHooks,
|
|
17
|
+
});
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const legacyRouterPlugin = Reflect.get(runtimeRouter, 'routerPlugin');
|
|
33
|
-
|
|
34
|
-
if (typeof legacyRouterPlugin === 'function') {
|
|
35
|
-
return tanstackRouterProviderFactory as unknown as LegacyRouterPlugin;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
throw new Error(
|
|
39
|
-
'[@modern-js/plugin-tanstack] The installed @modern-js/runtime/router/internal exports neither createRouterPlugin nor routerPlugin. Install a compatible @modern-js/runtime version.',
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const routerPlugin: LegacyRouterPlugin = getRouterPlugin();
|
|
19
|
+
export const routerPlugin: typeof nativeRouterPlugin = (userConfig = {}) => {
|
|
20
|
+
const providerPlugin = createProviderPlugin(userConfig);
|
|
21
|
+
return {
|
|
22
|
+
...providerPlugin,
|
|
23
|
+
setup(api) {
|
|
24
|
+
statePlugin.setup(api);
|
|
25
|
+
providerPlugin.setup(api);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
};
|
package/src/runtime/state.ts
CHANGED
package/src/runtime/types.ts
CHANGED
|
@@ -2,11 +2,11 @@ import type { ModernRoute } from '@modern-js/runtime/context';
|
|
|
2
2
|
import type { RouteObject } from '@modern-js/runtime-utils/router';
|
|
3
3
|
import type React from 'react';
|
|
4
4
|
|
|
5
|
+
export type { ModernRoute } from '@modern-js/runtime/context';
|
|
5
6
|
export type {
|
|
6
7
|
InternalRouterServerSnapshot,
|
|
7
|
-
ModernRoute,
|
|
8
8
|
RouterFramework,
|
|
9
|
-
} from '@modern-js/runtime/
|
|
9
|
+
} from '@modern-js/runtime-extensions/router-state';
|
|
10
10
|
|
|
11
11
|
/** TanStack-specific router config. */
|
|
12
12
|
export type RouterConfig = {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RouterProviderFactory } from '@modern-js/runtime/
|
|
1
|
+
import type { RouterProviderFactory } from '@modern-js/runtime-extensions/router-provider';
|
|
2
2
|
import { rstest } from '@rstest/core';
|
|
3
3
|
import type { RouterExtendsHooks } from '../src/runtime/hooks';
|
|
4
4
|
|
|
@@ -12,6 +12,7 @@ type RouterModule = typeof import('../src/runtime/router');
|
|
|
12
12
|
|
|
13
13
|
type Vertical = {
|
|
14
14
|
context: RuntimeContextModule;
|
|
15
|
+
providers: typeof import('@modern-js/runtime-extensions/router-provider');
|
|
15
16
|
factory: RouterProviderFactory;
|
|
16
17
|
hooks: RouterExtendsHooks;
|
|
17
18
|
Link: unknown;
|
|
@@ -32,8 +33,12 @@ async function loadVertical(): Promise<Vertical> {
|
|
|
32
33
|
const { Link } = await import('../src/runtime/prefetchLink');
|
|
33
34
|
const { useMatches } = await import('../src/runtime/routeHooks');
|
|
34
35
|
|
|
36
|
+
const providers = await import(
|
|
37
|
+
'@modern-js/runtime-extensions/router-provider'
|
|
38
|
+
);
|
|
35
39
|
return {
|
|
36
40
|
context,
|
|
41
|
+
providers,
|
|
37
42
|
factory: tanstackRouterProviderFactory,
|
|
38
43
|
hooks: routerProviderRegistryHooks,
|
|
39
44
|
Link,
|
|
@@ -50,6 +55,7 @@ function setupRouterWrapper(vertical: Vertical) {
|
|
|
50
55
|
const plugin = vertical.routerPlugin({ framework: 'tanstack' });
|
|
51
56
|
plugin.setup?.({
|
|
52
57
|
getHooks: () => vertical.hooks,
|
|
58
|
+
onAfterCreateRouter: () => undefined,
|
|
53
59
|
getRuntimeConfig: () => ({ router: { framework: 'tanstack' } }),
|
|
54
60
|
onBeforeRender: listener => {
|
|
55
61
|
onBeforeRender = listener;
|
|
@@ -101,7 +107,7 @@ describe('TanStack provider runtime-realm isolation', () => {
|
|
|
101
107
|
|
|
102
108
|
// Mixed-version callers that do not supply a realm still see the shared
|
|
103
109
|
// keep-first fallback, but production wrappers no longer consume it.
|
|
104
|
-
expect(verticalB.
|
|
110
|
+
expect(verticalB.providers.resolveRouterProvider('tanstack')).toBe(
|
|
105
111
|
verticalA.factory,
|
|
106
112
|
);
|
|
107
113
|
expect(warnSpy).toHaveBeenCalledWith(
|
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
|
}),
|
|
@@ -668,7 +765,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
668
765
|
|
|
669
766
|
const taps: Record<string, any> = {};
|
|
670
767
|
const api = {
|
|
671
|
-
|
|
768
|
+
...createAppContextApi({
|
|
672
769
|
srcDirectory,
|
|
673
770
|
internalSrcAlias: '@/_',
|
|
674
771
|
entrypoints: [entrypoint],
|
|
@@ -765,7 +862,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
765
862
|
|
|
766
863
|
const taps: Record<string, any> = {};
|
|
767
864
|
const api = {
|
|
768
|
-
|
|
865
|
+
...createAppContextApi({
|
|
769
866
|
srcDirectory,
|
|
770
867
|
internalSrcAlias: '@/_',
|
|
771
868
|
entrypoints: [entrypoint],
|
|
@@ -830,7 +927,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
830
927
|
__modernRoutesDir: 'views',
|
|
831
928
|
} as any as Entrypoint;
|
|
832
929
|
const api = {
|
|
833
|
-
|
|
930
|
+
...createAppContextApi({
|
|
834
931
|
srcDirectory: '/tmp/app/src',
|
|
835
932
|
internalSrcAlias: '@/_',
|
|
836
933
|
entrypoints: [entrypoint],
|
|
@@ -878,7 +975,7 @@ describe('tanstack router cli plugin', () => {
|
|
|
878
975
|
test('can opt out of Modern-owned route code splitting', async () => {
|
|
879
976
|
const taps: Record<string, any> = {};
|
|
880
977
|
const api = {
|
|
881
|
-
|
|
978
|
+
...createAppContextApi({
|
|
882
979
|
srcDirectory: '/tmp/app/src',
|
|
883
980
|
serverRoutes: [],
|
|
884
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 {
|