@bleedingdev/modern-js-plugin-tanstack 3.9.0-ultramodern.6 → 3.9.0-ultramodern.8

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.
@@ -1,19 +1,12 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { mkdir, mkdtemp, rm } from 'node:fs/promises';
2
+ import { mkdtemp, rm } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { promisify } from 'node:util';
6
- import { mergeConfig } from '@modern-js/plugin/cli';
7
6
  import { compile } from '@modern-js/server-utils';
8
7
  import type { Entrypoint } from '@modern-js/types';
9
- import { fs, NESTED_ROUTE_SPEC_FILE } from '@modern-js/utils';
10
- import {
11
- createTanstackRsbuildRouteSplittingProfile,
12
- generateTanstackRouteArtifacts,
13
- tanstackRouterPlugin,
14
- writeTanstackRegisterFile,
15
- writeTanstackRouterTypesForEntries,
16
- } from '../../src/cli';
8
+ import { fs } from '@modern-js/utils';
9
+ import { tanstackRouterPlugin, writeTanstackRegisterFile } from '../../src/cli';
17
10
 
18
11
  const execFileAsync = promisify(execFile);
19
12
  const strictestTsconfigPath = path.resolve(
@@ -31,11 +24,14 @@ function createAppContextApi<T extends object>(initial: T) {
31
24
  };
32
25
  }
33
26
 
27
+ /**
28
+ * Compiles the generated registration output with `tsgo` under
29
+ * @tsconfig/strictest — the way a consumer's own type check would see it.
30
+ */
34
31
  async function typecheckGeneratedRegistration(options: {
35
32
  entries: string[];
36
33
  generatedDirName: string;
37
34
  srcDirectory: string;
38
- runtimeModule?: string;
39
35
  i18nRuntimeModule?: string;
40
36
  canonicalTypeChecks?: string[];
41
37
  }) {
@@ -43,10 +39,10 @@ async function typecheckGeneratedRegistration(options: {
43
39
  entries,
44
40
  generatedDirName,
45
41
  srcDirectory,
46
- runtimeModule = '@modern-js/plugin-tanstack/runtime',
47
42
  i18nRuntimeModule,
48
43
  canonicalTypeChecks = [],
49
44
  } = options;
45
+ const runtimeModule = '@modern-js/plugin-tanstack/runtime';
50
46
  const projectDirectory = path.dirname(srcDirectory);
51
47
 
52
48
  for (const entry of entries) {
@@ -75,33 +71,28 @@ async function typecheckGeneratedRegistration(options: {
75
71
  ' export function modernLoaderToTanstack<TLoader extends (...args: never[]) => unknown>(options: { hasSplat: boolean }, loader: TLoader): (context: unknown) => Promise<Awaited<ReturnType<TLoader>>>;',
76
72
  ' export const modernTanstackRouterFastDefaults: Record<string, unknown>;',
77
73
  '}',
74
+ ...(i18nRuntimeModule
75
+ ? [
76
+ `declare module '${i18nRuntimeModule}' { export interface UltramodernCanonicalRoutes {} }`,
77
+ ]
78
+ : []),
78
79
  ];
79
- if (i18nRuntimeModule) {
80
- runtimeDeclaration.push(
81
- `declare module '${i18nRuntimeModule}' { export interface UltramodernCanonicalRoutes {} }`,
82
- );
83
- }
84
80
  await fs.outputFile(
85
81
  path.join(srcDirectory, 'generated-runtime-shim.d.ts'),
86
82
  runtimeDeclaration.join('\n'),
87
83
  );
88
84
 
89
- const contractLines = [
90
- `import type { Register } from '${runtimeModule}';`,
91
- ...entries.map(
92
- (entry, index) =>
93
- `import { router as router${index} } from './${generatedDirName}/${entry}/router.gen';`,
94
- ),
95
- ...entries.map(
96
- (_, index) =>
97
- `const registeredRouter${index}: Register['router'] = router${index};`,
98
- ),
99
- ...entries.map((_, index) => `void registeredRouter${index};`),
100
- ...canonicalTypeChecks,
101
- ];
102
85
  await fs.outputFile(
103
86
  path.join(srcDirectory, 'registration-contract.ts'),
104
- contractLines.join('\n'),
87
+ [
88
+ `import type { Register } from '${runtimeModule}';`,
89
+ ...entries.flatMap((entry, index) => [
90
+ `import { router as router${index} } from './${generatedDirName}/${entry}/router.gen';`,
91
+ `const registeredRouter${index}: Register['router'] = router${index};`,
92
+ `void registeredRouter${index};`,
93
+ ]),
94
+ ...canonicalTypeChecks,
95
+ ].join('\n'),
105
96
  );
106
97
  await fs.outputJSON(path.join(projectDirectory, 'tsconfig.json'), {
107
98
  extends: strictestTsconfigPath,
@@ -120,26 +111,16 @@ async function typecheckGeneratedRegistration(options: {
120
111
  await execFileAsync(
121
112
  process.platform === 'win32' ? 'tsgo.cmd' : 'tsgo',
122
113
  ['-p', 'tsconfig.json'],
123
- {
124
- cwd: projectDirectory,
125
- shell: process.platform === 'win32',
126
- },
114
+ { cwd: projectDirectory, shell: process.platform === 'win32' },
127
115
  );
128
- } catch (error) {
129
- if (
130
- error &&
131
- typeof error === 'object' &&
132
- 'stdout' in error &&
133
- typeof error.stdout === 'string'
134
- ) {
135
- throw new Error(error.stdout, { cause: error });
136
- }
137
- throw error;
116
+ } catch (error: any) {
117
+ throw typeof error?.stdout === 'string'
118
+ ? new Error(error.stdout, { cause: error })
119
+ : error;
138
120
  }
139
121
  }
140
122
 
141
123
  const runtimeCliMocks = {
142
- handleFileChange: rstest.fn(),
143
124
  handleGeneratorEntryCode: rstest.fn(),
144
125
  };
145
126
 
@@ -162,40 +143,14 @@ rstest.mock('@modern-js/runtime/cli', () => {
162
143
  : null),
163
144
  getEntrypointRoutesOwner: (entrypoint: any) =>
164
145
  entrypoint.__modernRoutesOwner || null,
165
- // Forward through arrows: the mock factory is hoisted above the
146
+ // Forward through an arrow: the mock factory is hoisted above the
166
147
  // `runtimeCliMocks` initializer, so it must not dereference it eagerly.
167
- handleFileChange: (...args: unknown[]) =>
168
- runtimeCliMocks.handleFileChange(...args),
169
148
  handleGeneratorEntryCode: (...args: unknown[]) =>
170
149
  runtimeCliMocks.handleGeneratorEntryCode(...args),
171
- handleModifyEntrypoints: async (
172
- entrypoints: Entrypoint[],
173
- routesDir = 'routes',
174
- ) =>
175
- entrypoints.map(entrypoint => {
176
- const routesEntry = path.join(entrypoint.absoluteEntryDir!, routesDir);
177
- return {
178
- ...entrypoint,
179
- nestedRoutesEntry: routesEntry,
180
- [routesDirMetaKey]: routesDir,
181
- };
182
- }),
183
- isRouteEntry: (dir: string, routesDir = 'routes') => {
184
- const routesEntry = path.join(dir, routesDir);
185
- return fs.existsSync(routesEntry) ? routesEntry : false;
186
- },
187
- updateNestedRoutesSpec: async (
188
- specPath: string,
189
- nextRoutes: Record<string, unknown>,
190
- ) => {
191
- const existingRoutes = fs.existsSync(specPath)
192
- ? await fs.readJSON(specPath)
193
- : {};
194
- await fs.outputJSON(specPath, {
195
- ...existingRoutes,
196
- ...nextRoutes,
197
- });
198
- },
150
+ handleFileChange: async () => {},
151
+ handleModifyEntrypoints: async (entrypoints: Entrypoint[]) => entrypoints,
152
+ isRouteEntry: () => false,
153
+ updateNestedRoutesSpec: async () => {},
199
154
  };
200
155
  });
201
156
 
@@ -203,30 +158,21 @@ describe('tanstack router cli plugin', () => {
203
158
  let tempDir: string | undefined;
204
159
 
205
160
  afterEach(async () => {
206
- runtimeCliMocks.handleFileChange.mockReset();
207
161
  runtimeCliMocks.handleGeneratorEntryCode.mockReset();
208
-
209
162
  if (tempDir) {
210
163
  await rm(tempDir, { recursive: true, force: true });
211
164
  tempDir = undefined;
212
165
  }
213
166
  });
214
167
 
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
- }) => {
168
+ test('excludes its generated declaration from server roots only when installed', async () => {
222
169
  tempDir = await fs.realpath(
223
170
  await mkdtemp(path.join(tmpdir(), 'tanstack-server-roots-')),
224
171
  );
225
- const srcDirectory = path.join(tempDir, sourceDirectory);
226
- const generatedDirectory = generatedDirName ?? 'modern-tanstack';
172
+ const srcDirectory = path.join(tempDir, 'src');
227
173
  const declaration = path.join(
228
174
  srcDirectory,
229
- generatedDirectory,
175
+ 'modern-tanstack',
230
176
  'register.gen.d.ts',
231
177
  );
232
178
  const apiDirectory = path.join(tempDir, 'api');
@@ -244,7 +190,7 @@ describe('tanstack router cli plugin', () => {
244
190
  noEmitOnError: true,
245
191
  skipLibCheck: false,
246
192
  },
247
- include: ['api', sourceDirectory],
193
+ include: ['api', 'src'],
248
194
  });
249
195
  await fs.outputFile(
250
196
  path.join(apiDirectory, 'index.ts'),
@@ -255,14 +201,10 @@ describe('tanstack router cli plugin', () => {
255
201
  "import type { Missing } from 'missing-browser-only-types';\nexport const page: Missing = undefined;\n",
256
202
  );
257
203
  await fs.outputFile(
258
- path.join(srcDirectory, generatedDirectory, 'main/router.gen.ts'),
204
+ path.join(srcDirectory, 'modern-tanstack', 'main/router.gen.ts'),
259
205
  "import { page } from '../../routes/page';\nexport const router = { page };\n",
260
206
  );
261
- await writeTanstackRegisterFile({
262
- entries: ['main'],
263
- srcDirectory,
264
- generatedDirName,
265
- });
207
+ await writeTanstackRegisterFile({ entries: ['main'], srcDirectory });
266
208
 
267
209
  await expect(compile(tempDir, {}, compileOptions)).rejects.toThrow(
268
210
  /missing-browser-only-types/,
@@ -283,270 +225,28 @@ describe('tanstack router cli plugin', () => {
283
225
  modifyFileSystemRoutes: () => {},
284
226
  onBeforeGenerateRoutes: () => {},
285
227
  };
286
- tanstackRouterPlugin({ generatedDirName }).setup!(api as never);
228
+ tanstackRouterPlugin().setup!(api as never);
287
229
  const excludeFiles = api.getAppContext().serverCompileExcludedFiles;
288
230
  expect(excludeFiles).toEqual([...previousExclusions, declaration]);
289
- expect(previousExclusions).toEqual([
290
- path.join(tempDir, 'other-client.d.ts'),
291
- ]);
292
231
  await compile(tempDir, {}, { ...compileOptions, excludeFiles });
293
232
  expect(
294
233
  await fs.pathExists(path.join(compileOptions.distDir, 'api/index.js')),
295
234
  ).toBe(true);
296
- expect(
297
- await fs.pathExists(path.join(compileOptions.distDir, sourceDirectory)),
298
- ).toBe(false);
299
- });
300
-
301
- test('typechecks plugin-owned routers and register metadata under strictest', async () => {
302
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
303
- const srcDirectory = path.join(tempDir, 'src');
304
- await mkdir(srcDirectory, { recursive: true });
305
-
306
- await writeTanstackRouterTypesForEntries({
307
- appContext: {
308
- srcDirectory,
309
- internalSrcAlias: '@/_',
310
- entrypoints: [
311
- { entryName: 'dashboard', isMainEntry: false },
312
- { entryName: 'main', isMainEntry: true },
313
- ],
314
- } as any,
315
- generatedDirName: 'generated-router',
316
- routesByEntry: {
317
- dashboard: [
318
- {
319
- type: 'nested',
320
- id: 'layout',
321
- isRoot: true,
322
- children: [{ type: 'nested', id: 'page', index: true }],
323
- },
324
- ],
325
- main: [
326
- {
327
- type: 'nested',
328
- id: 'layout',
329
- isRoot: true,
330
- children: [{ type: 'nested', id: 'page', index: true }],
331
- },
332
- ],
333
- },
334
- });
335
-
336
- await typecheckGeneratedRegistration({
337
- entries: ['main', 'dashboard'],
338
- generatedDirName: 'generated-router',
339
- srcDirectory,
340
- });
341
- });
342
-
343
- test('typechecks register metadata for custom entry lists without routes', async () => {
344
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
345
- const srcDirectory = path.join(tempDir, 'src');
346
-
347
- await writeTanstackRegisterFile({
348
- entries: ['main'],
349
- generatedDirName: 'tanstack',
350
- srcDirectory,
351
- });
352
-
353
- await typecheckGeneratedRegistration({
354
- entries: ['main'],
355
- generatedDirName: 'tanstack',
356
- srcDirectory,
357
- });
358
- });
359
-
360
- test('exposes canonical route params through plugin-i18n types', async () => {
361
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
362
- const srcDirectory = path.join(tempDir, 'src');
363
-
364
- await writeTanstackRegisterFile({
365
- entries: ['main'],
366
- generatedDirName: 'tanstack',
367
- srcDirectory,
368
- canonicalRoutes: {
369
- '/': 'Record<string, never>',
370
- '/products/$slug': '{ "slug": string }',
371
- },
372
- });
373
-
374
- await typecheckGeneratedRegistration({
375
- entries: ['main'],
376
- generatedDirName: 'tanstack',
377
- srcDirectory,
378
- i18nRuntimeModule: '@modern-js/plugin-i18n/runtime',
379
- canonicalTypeChecks: [
380
- "import type { UltramodernCanonicalRoutes } from '@modern-js/plugin-i18n/runtime';",
381
- "const rootParams: UltramodernCanonicalRoutes['/'] = {};",
382
- "const productParams: UltramodernCanonicalRoutes['/products/$slug'] = { slug: 'tractor' };",
383
- 'void rootParams;',
384
- 'void productParams;',
385
- ],
386
- });
387
- });
388
-
389
- test('typechecks a plain TanStack register without plugin-i18n installed', async () => {
390
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
391
- const srcDirectory = path.join(tempDir, 'src');
392
-
393
- await writeTanstackRegisterFile({
394
- entries: ['main'],
395
- generatedDirName: 'tanstack',
396
- srcDirectory,
397
- });
398
-
399
- await typecheckGeneratedRegistration({
400
- entries: ['main'],
401
- generatedDirName: 'tanstack',
402
- srcDirectory,
403
- });
404
- });
405
-
406
- test('typechecks custom runtime module augmentations', async () => {
407
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
408
- const srcDirectory = path.join(tempDir, 'src');
409
-
410
- await writeTanstackRegisterFile({
411
- entries: ['main'],
412
- generatedDirName: 'tanstack',
413
- runtimeModule: '@my-org/tanstack/runtime',
414
- srcDirectory,
415
- canonicalRoutes: {
416
- '/talks': 'Record<string, never>',
417
- },
418
- i18nRuntimeModule: '@my-org/i18n/runtime',
419
- });
420
-
421
- await typecheckGeneratedRegistration({
422
- entries: ['main'],
423
- generatedDirName: 'tanstack',
424
- srcDirectory,
425
- runtimeModule: '@my-org/tanstack/runtime',
426
- i18nRuntimeModule: '@my-org/i18n/runtime',
427
- canonicalTypeChecks: [
428
- "import type { UltramodernCanonicalRoutes } from '@my-org/i18n/runtime';",
429
- "const talksParams: UltramodernCanonicalRoutes['/talks'] = {};",
430
- 'void talksParams;',
431
- ],
432
- });
433
- });
434
-
435
- test('claims custom routes, injects runtime plugin, and merges route specs', async () => {
436
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
437
- const srcDirectory = path.join(tempDir, 'src');
438
- const distDirectory = path.join(tempDir, 'dist');
439
- const entryDir = path.join(srcDirectory, 'main');
440
- const viewsDir = path.join(entryDir, 'views');
441
- await mkdir(viewsDir, { recursive: true });
442
-
443
- const taps: Record<string, any> = {};
444
- const api = {
445
- ...createAppContextApi({
446
- srcDirectory,
447
- distDirectory,
448
- metaName: 'modern-js',
449
- serverRoutes: [{ entryName: 'main', urlPath: '/dashboard' }],
450
- }),
451
- _internalRuntimePlugins: (tap: any) => {
452
- taps.internalRuntimePlugins = tap;
453
- },
454
- checkEntryPoint: (tap: any) => {
455
- taps.checkEntryPoint = tap;
456
- },
457
- config: (tap: any) => {
458
- taps.config = tap;
459
- },
460
- modifyEntrypoints: (tap: any) => {
461
- taps.modifyEntrypoints = tap;
462
- },
463
- generateEntryCode: (tap: any) => {
464
- taps.generateEntryCode = tap;
465
- },
466
- onFileChanged: (tap: any) => {
467
- taps.onFileChanged = tap;
468
- },
469
- modifyFileSystemRoutes: (tap: any) => {
470
- taps.modifyFileSystemRoutes = tap;
471
- },
472
- onBeforeGenerateRoutes: (tap: any) => {
473
- taps.onBeforeGenerateRoutes = tap;
474
- },
475
- };
476
-
477
- tanstackRouterPlugin({ routesDir: 'views' }).setup!(api as any);
478
-
479
- expect(taps.checkEntryPoint({ path: entryDir, entry: false })).toEqual({
480
- path: entryDir,
481
- entry: viewsDir,
482
- });
483
-
484
- const { entrypoints } = await taps.modifyEntrypoints({
485
- entrypoints: [
486
- {
487
- entryName: 'main',
488
- entry: entryDir,
489
- absoluteEntryDir: entryDir,
490
- isAutoMount: true,
491
- isMainEntry: true,
492
- } as Entrypoint,
493
- ],
494
- });
495
- const [entrypoint] = entrypoints;
496
- expect(entrypoint.nestedRoutesEntry).toBe(viewsDir);
497
-
498
- expect(
499
- taps.internalRuntimePlugins({ entrypoint, plugins: [] }).plugins,
500
- ).toEqual([
501
- {
502
- name: 'tanstackRouter',
503
- path: '@modern-js/plugin-tanstack/runtime',
504
- config: { serverBase: ['/dashboard'] },
505
- },
506
- ]);
507
-
508
- expect(taps.config()).toMatchObject({
509
- output: {
510
- splitRouteChunks: true,
511
- },
512
- });
513
-
514
- const specPath = path.join(distDirectory, NESTED_ROUTE_SPEC_FILE);
515
- await fs.outputJSON(specPath, {
516
- existing: [{ id: 'keep-me' }],
517
- });
518
-
519
- await taps.modifyFileSystemRoutes({
520
- entrypoint,
521
- routes: [
522
- {
523
- id: 'main-route',
524
- type: 'nested',
525
- origin: 'file-system',
526
- },
527
- ],
528
- });
529
- await taps.onBeforeGenerateRoutes({ entrypoint, code: '' });
530
-
531
- expect(await fs.readJSON(specPath)).toEqual({
532
- existing: [{ id: 'keep-me' }],
533
- main: [
534
- {
535
- id: 'main-route',
536
- type: 'nested',
537
- origin: 'file-system',
538
- },
539
- ],
540
- });
235
+ expect(await fs.pathExists(path.join(compileOptions.distDir, 'src'))).toBe(
236
+ false,
237
+ );
541
238
  });
542
239
 
543
- test('injects the framework-resolving router wrapper for non-file-route entrypoints', async () => {
240
+ test('installs the TanStack router only for entrypoints it owns', () => {
544
241
  const taps: Record<string, any> = {};
545
242
  const api = {
546
243
  ...createAppContextApi({
547
244
  srcDirectory: '/tmp/app/src',
548
245
  metaName: 'modern-js',
549
- serverRoutes: [{ entryName: 'custom', urlPath: '/' }],
246
+ serverRoutes: [
247
+ { entryName: 'custom', urlPath: '/' },
248
+ { entryName: 'home', urlPath: '/' },
249
+ ],
550
250
  }),
551
251
  _internalRuntimePlugins: (tap: any) => {
552
252
  taps.internalRuntimePlugins = tap;
@@ -560,170 +260,70 @@ describe('tanstack router cli plugin', () => {
560
260
  onBeforeGenerateRoutes: () => {},
561
261
  };
562
262
 
563
- tanstackRouterPlugin().setup!(api as any);
263
+ tanstackRouterPlugin({ routesDir: 'ts-routes' }).setup!(api as any);
564
264
 
565
265
  // Custom entry without a routes dir (e.g. `createRoutes` in
566
- // modern.runtime.ts): installing the plugin is the explicit opt-in, no
567
- // source sniffing — the wrapper plus the provider registration is
568
- // injected through the package's own runtime/router module.
266
+ // modern.runtime.ts): installing the plugin is the explicit opt-in, so
267
+ // the wrapper plus the provider registration is injected through the
268
+ // package's own runtime/router module.
569
269
  const customEntrypoint = {
570
270
  entryName: 'custom',
571
271
  isAutoMount: true,
572
272
  } as Entrypoint;
273
+ const tanstackRouterEntry = {
274
+ name: 'router',
275
+ path: '@modern-js/plugin-tanstack/runtime/router',
276
+ config: { serverBase: ['/'] },
277
+ };
573
278
  expect(
574
279
  taps.internalRuntimePlugins({ entrypoint: customEntrypoint, plugins: [] })
575
280
  .plugins,
576
- ).toEqual([
577
- {
578
- name: 'router',
579
- path: '@modern-js/plugin-tanstack/runtime/router',
580
- config: { serverBase: ['/'] },
581
- },
582
- ]);
281
+ ).toEqual([tanstackRouterEntry]);
583
282
 
584
283
  // If the built-in router CLI already installed the internal router for
585
284
  // this custom entry (explicit `runtime.router` config), only the module
586
- // path is redirected so the TanStack provider registration is
587
- // value-imported with it.
588
- const existingRouterPlugin = {
285
+ // path is redirected — pushing a second entry installs two routers.
286
+ const builtInPlugin = {
589
287
  name: 'router',
590
288
  path: '@modern-js/runtime/router/internal',
591
289
  config: { serverBase: ['/'] },
592
290
  };
593
- const { plugins } = taps.internalRuntimePlugins({
594
- entrypoint: customEntrypoint,
595
- plugins: [existingRouterPlugin],
596
- });
597
- expect(plugins).toHaveLength(1);
598
- expect(plugins[0]).toEqual({
599
- name: 'router',
600
- path: '@modern-js/plugin-tanstack/runtime/router',
601
- config: { serverBase: ['/'] },
602
- });
603
- });
604
-
605
- test('leaves built-in and foreign-owned route entrypoints to their own router', () => {
606
- const taps: Record<string, any> = {};
607
- const api = {
608
- ...createAppContextApi({
609
- srcDirectory: '/tmp/app/src',
610
- metaName: 'modern-js',
611
- serverRoutes: [{ entryName: 'home', urlPath: '/' }],
612
- }),
613
- _internalRuntimePlugins: (tap: any) => {
614
- taps.internalRuntimePlugins = tap;
615
- },
616
- checkEntryPoint: () => {},
617
- config: () => {},
618
- modifyEntrypoints: () => {},
619
- generateEntryCode: () => {},
620
- onFileChanged: () => {},
621
- modifyFileSystemRoutes: () => {},
622
- onBeforeGenerateRoutes: () => {},
623
- };
624
-
625
- tanstackRouterPlugin({ routesDir: 'ts-routes' }).setup!(api as any);
291
+ expect(
292
+ taps.internalRuntimePlugins({
293
+ entrypoint: customEntrypoint,
294
+ plugins: [builtInPlugin],
295
+ }).plugins,
296
+ ).toEqual([tanstackRouterEntry]);
626
297
 
627
298
  // A classic react-router file-route entry (src/<entry>/routes) living
628
299
  // next to the TanStack entries: its internal router plugin must be left
629
- // untouched — redirecting it through the TanStack wrapper would pull
630
- // @tanstack/react-router into a pure react-router bundle.
631
- const builtInEntrypoint = {
632
- entryName: 'home',
633
- isAutoMount: true,
634
- nestedRoutesEntry: '/tmp/app/src/home/routes',
635
- __modernRoutesDir: 'routes',
636
- } as Entrypoint;
637
- const { plugins: builtInPlugins } = taps.internalRuntimePlugins({
638
- entrypoint: builtInEntrypoint,
639
- plugins: [
640
- {
641
- name: 'router',
642
- path: '@modern-js/runtime/router/internal',
643
- config: { serverBase: ['/'] },
644
- },
645
- ],
646
- });
647
- expect(builtInPlugins).toEqual([
648
- {
649
- name: 'router',
650
- path: '@modern-js/runtime/router/internal',
651
- config: { serverBase: ['/'] },
652
- },
653
- ]);
300
+ // untouched — redirecting it would pull @tanstack/react-router into a
301
+ // pure react-router bundle.
302
+ expect(
303
+ taps.internalRuntimePlugins({
304
+ entrypoint: {
305
+ entryName: 'home',
306
+ isAutoMount: true,
307
+ nestedRoutesEntry: '/tmp/app/src/home/routes',
308
+ __modernRoutesDir: 'routes',
309
+ } as Entrypoint,
310
+ plugins: [builtInPlugin],
311
+ }).plugins,
312
+ ).toEqual([builtInPlugin]);
654
313
 
655
314
  // An entry tagged by another routes-owner plugin: nothing is pushed.
656
- // The replaced sniffing path in @modern-js/runtime excluded
657
- // plugin-owned entrypoints for the same reason — pushing a second
658
- // `router` plugin can install two routers for one entry.
659
- const foreignEntrypoint = {
660
- entryName: 'home',
661
- isAutoMount: true,
662
- nestedRoutesEntry: '/tmp/app/src/home/acme-routes',
663
- __modernRoutesDir: 'acme-routes',
664
- __modernRoutesOwner: '@acme/plugin-file-router',
665
- } as Entrypoint;
666
315
  expect(
667
316
  taps.internalRuntimePlugins({
668
- entrypoint: foreignEntrypoint,
317
+ entrypoint: {
318
+ entryName: 'home',
319
+ isAutoMount: true,
320
+ nestedRoutesEntry: '/tmp/app/src/home/acme-routes',
321
+ __modernRoutesDir: 'acme-routes',
322
+ __modernRoutesOwner: '@acme/plugin-file-router',
323
+ } as Entrypoint,
669
324
  plugins: [],
670
325
  }).plugins,
671
326
  ).toEqual([]);
672
-
673
- // The built-in pages/ convention is foreign too.
674
- const pagesEntrypoint = {
675
- entryName: 'home',
676
- isAutoMount: true,
677
- pageRoutesEntry: '/tmp/app/src/home/pages',
678
- } as Entrypoint;
679
- expect(
680
- taps.internalRuntimePlugins({ entrypoint: pagesEntrypoint, plugins: [] })
681
- .plugins,
682
- ).toEqual([]);
683
- });
684
-
685
- test('source.include covers the package dist and TanStack runtime deps without string surgery', () => {
686
- const taps: Record<string, any> = {};
687
- const api = {
688
- ...createAppContextApi({
689
- srcDirectory: '/tmp/app/src',
690
- serverRoutes: [],
691
- }),
692
- _internalRuntimePlugins: () => {},
693
- checkEntryPoint: () => {},
694
- config: (tap: any) => {
695
- taps.config = tap;
696
- },
697
- modifyEntrypoints: () => {},
698
- generateEntryCode: () => {},
699
- onFileChanged: () => {},
700
- modifyFileSystemRoutes: () => {},
701
- onBeforeGenerateRoutes: () => {},
702
- };
703
-
704
- tanstackRouterPlugin().setup!(api as any);
705
-
706
- const include = taps.config().source.include as Array<RegExp | string>;
707
- const regexes = include.filter(
708
- (entry): entry is RegExp => entry instanceof RegExp,
709
- );
710
- for (const dep of ['react-router', 'router-core', 'react-store']) {
711
- const sample = `/repo/node_modules/@tanstack/${dep}/dist/esm/index.js`;
712
- expect(regexes.some(regex => regex.test(sample))).toBe(true);
713
- }
714
-
715
- const stringEntries = include.filter(
716
- (entry): entry is string => typeof entry === 'string',
717
- );
718
- expect(stringEntries).toHaveLength(1);
719
- // The include must point at the package root (two levels above the cli
720
- // build dir) so dist/esm, dist/esm-node and dist/cjs are all covered —
721
- // the old `.replace('cjs', 'esm')` never matched the bundled dist/esm
722
- // runtime when the CLI was loaded through the ESM condition.
723
- expect(stringEntries[0]).toBe(path.resolve(__dirname, '..', '..'));
724
- expect(stringEntries[0]).not.toMatch(
725
- /[\\/]dist[\\/]esm(?:-node)?(?:[\\/]|$)/,
726
- );
727
327
  });
728
328
 
729
329
  test('emits the plugin-i18n augmentation only when plugin-i18n is registered', async () => {
@@ -817,379 +417,4 @@ describe('tanstack router cli plugin', () => {
817
417
  { name: '@modern-js/plugin-i18n' },
818
418
  ]);
819
419
  });
820
-
821
- test('generates plugin-owned TanStack route files through core route generation', async () => {
822
- tempDir = await mkdtemp(path.join(tmpdir(), 'modern-tanstack-cli-'));
823
- const srcDirectory = path.join(tempDir, 'src');
824
- const entryDir = path.join(srcDirectory, 'main');
825
- const viewsDir = path.join(entryDir, 'views');
826
- await mkdir(path.join(viewsDir, 'mf'), { recursive: true });
827
- await fs.outputFile(
828
- path.join(viewsDir, 'mf', 'page.data.ts'),
829
- [
830
- 'export const loader = () => ({ count: 0 });',
831
- 'export const action = () => Response.json({ count: 1 });',
832
- ].join('\n'),
833
- );
834
-
835
- const entrypoint = {
836
- entryName: 'main',
837
- entry: entryDir,
838
- absoluteEntryDir: entryDir,
839
- isAutoMount: true,
840
- isMainEntry: true,
841
- nestedRoutesEntry: viewsDir,
842
- __modernRoutesDir: 'views',
843
- } as Entrypoint;
844
- runtimeCliMocks.handleGeneratorEntryCode.mockResolvedValue({
845
- main: [
846
- {
847
- type: 'nested',
848
- id: 'layout',
849
- isRoot: true,
850
- children: [
851
- {
852
- type: 'nested',
853
- id: 'mf/page',
854
- path: 'mf',
855
- data: '@/_/main/views/mf/page.data',
856
- action: '@/_/main/views/mf/page.data',
857
- },
858
- ],
859
- },
860
- ],
861
- });
862
-
863
- const taps: Record<string, any> = {};
864
- const api = {
865
- ...createAppContextApi({
866
- srcDirectory,
867
- internalSrcAlias: '@/_',
868
- entrypoints: [entrypoint],
869
- }),
870
- _internalRuntimePlugins: () => {},
871
- checkEntryPoint: (tap: any) => {
872
- taps.checkEntryPoint = tap;
873
- },
874
- config: (tap: any) => {
875
- taps.config = tap;
876
- },
877
- modifyEntrypoints: (tap: any) => {
878
- taps.modifyEntrypoints = tap;
879
- },
880
- generateEntryCode: (tap: any) => {
881
- taps.generateEntryCode = tap;
882
- },
883
- onFileChanged: (tap: any) => {
884
- taps.onFileChanged = tap;
885
- },
886
- modifyFileSystemRoutes: (tap: any) => {
887
- taps.modifyFileSystemRoutes = tap;
888
- },
889
- onBeforeGenerateRoutes: (tap: any) => {
890
- taps.onBeforeGenerateRoutes = tap;
891
- },
892
- };
893
-
894
- tanstackRouterPlugin({
895
- generatedDirName: 'tanstack-generated',
896
- routesDir: 'views',
897
- }).setup!(api as any);
898
-
899
- await taps.generateEntryCode({ entrypoints: [entrypoint] });
900
-
901
- expect(runtimeCliMocks.handleGeneratorEntryCode).toHaveBeenCalledWith(
902
- api,
903
- [entrypoint],
904
- {
905
- entrypointsKey: '@modern-js/plugin-tanstack',
906
- hydrateRscClientRoutes: true,
907
- includeRouteServerLoadersInSsrEntry: false,
908
- isolateRouteDataInRscLayer: true,
909
- serverRoutesFileName: 'tanstack-routes.server.js',
910
- },
911
- );
912
-
913
- await typecheckGeneratedRegistration({
914
- entries: ['main'],
915
- generatedDirName: 'tanstack-generated',
916
- srcDirectory,
917
- });
918
- });
919
-
920
- test('regenerates plugin-owned TanStack files for scoped file changes', async () => {
921
- const regenerateEvent = {
922
- eventType: 'add',
923
- filename: 'src/main/views/page.tsx',
924
- };
925
- const entrypoint = {
926
- entryName: 'main',
927
- __modernRoutesDir: 'views',
928
- } as any as Entrypoint;
929
- const api = {
930
- ...createAppContextApi({
931
- srcDirectory: '/tmp/app/src',
932
- internalSrcAlias: '@/_',
933
- entrypoints: [entrypoint],
934
- }),
935
- _internalRuntimePlugins: () => {},
936
- checkEntryPoint: () => {},
937
- config: () => {},
938
- modifyEntrypoints: () => {},
939
- generateEntryCode: () => {},
940
- onFileChanged: (tap: any) => {
941
- api.onFileChangedTap = tap;
942
- },
943
- modifyFileSystemRoutes: () => {},
944
- onBeforeGenerateRoutes: () => {},
945
- onFileChangedTap: undefined as any,
946
- };
947
-
948
- tanstackRouterPlugin({ routesDir: 'views' }).setup!(api as any);
949
-
950
- runtimeCliMocks.handleFileChange.mockImplementationOnce(
951
- async (_api, _event, options) => {
952
- expect(options.entrypointsKey).toBe('@modern-js/plugin-tanstack');
953
- expect(options.includeEntry(entrypoint)).toBe(true);
954
- expect(
955
- options.includeEntry({
956
- ...entrypoint,
957
- __modernRoutesDir: 'routes',
958
- }),
959
- ).toBe(false);
960
- expect(typeof options.regenerate).toBe('function');
961
- },
962
- );
963
-
964
- await api.onFileChangedTap(regenerateEvent);
965
-
966
- expect(runtimeCliMocks.handleFileChange).toHaveBeenCalledWith(
967
- api,
968
- regenerateEvent,
969
- expect.objectContaining({
970
- entrypointsKey: '@modern-js/plugin-tanstack',
971
- }),
972
- );
973
- });
974
-
975
- test('can opt out of Modern-owned route code splitting', async () => {
976
- const taps: Record<string, any> = {};
977
- const api = {
978
- ...createAppContextApi({
979
- srcDirectory: '/tmp/app/src',
980
- serverRoutes: [],
981
- }),
982
- _internalRuntimePlugins: () => {},
983
- checkEntryPoint: () => {},
984
- config: (tap: any) => {
985
- taps.config = tap;
986
- },
987
- modifyEntrypoints: () => {},
988
- generateEntryCode: () => {},
989
- onFileChanged: () => {},
990
- modifyFileSystemRoutes: () => {},
991
- onBeforeGenerateRoutes: () => {},
992
- };
993
-
994
- tanstackRouterPlugin({ routeCodeSplitting: false }).setup!(api as any);
995
-
996
- expect(taps.config()).toMatchObject({
997
- output: {
998
- splitRouteChunks: false,
999
- },
1000
- });
1001
- });
1002
-
1003
- test('route splitting profile carries only the rsbuild config production consumes', () => {
1004
- expect(createTanstackRsbuildRouteSplittingProfile({})).toEqual({
1005
- defaultConfig: {
1006
- output: {
1007
- splitRouteChunks: true,
1008
- },
1009
- },
1010
- });
1011
- expect(
1012
- createTanstackRsbuildRouteSplittingProfile({ routeCodeSplitting: false }),
1013
- ).toEqual({
1014
- defaultConfig: {
1015
- output: {
1016
- splitRouteChunks: false,
1017
- },
1018
- },
1019
- });
1020
- });
1021
-
1022
- test('preserves user-selected route and builder chunk splitting modes', () => {
1023
- const pluginDefaults = createTanstackRsbuildRouteSplittingProfile(
1024
- {},
1025
- ).defaultConfig;
1026
- const chunkSplits = [
1027
- { strategy: 'split-by-module' },
1028
- { strategy: 'split-by-experience' },
1029
- { strategy: 'all-in-one' },
1030
- { strategy: 'single-vendor' },
1031
- { strategy: 'split-by-size', minSize: 10_000, maxSize: 60_000 },
1032
- {
1033
- strategy: 'custom',
1034
- splitChunks: {
1035
- chunks: 'all',
1036
- cacheGroups: {
1037
- tractors: {
1038
- name: 'tractors',
1039
- test: /tractors/u,
1040
- },
1041
- },
1042
- },
1043
- },
1044
- ];
1045
-
1046
- for (const chunkSplit of chunkSplits) {
1047
- expect(
1048
- mergeConfig([
1049
- pluginDefaults,
1050
- {
1051
- output: {
1052
- splitRouteChunks: false,
1053
- },
1054
- performance: {
1055
- chunkSplit,
1056
- },
1057
- splitChunks: false,
1058
- },
1059
- ]),
1060
- ).toMatchObject({
1061
- output: {
1062
- splitRouteChunks: false,
1063
- },
1064
- performance: {
1065
- chunkSplit,
1066
- },
1067
- splitChunks: false,
1068
- });
1069
- }
1070
-
1071
- const pageSplitWithManualAsyncChunks = mergeConfig([
1072
- pluginDefaults,
1073
- {
1074
- performance: {
1075
- chunkSplit: {
1076
- strategy: 'custom',
1077
- splitChunks: {
1078
- chunks: 'async',
1079
- },
1080
- },
1081
- },
1082
- },
1083
- ]);
1084
-
1085
- expect(pageSplitWithManualAsyncChunks).toMatchObject({
1086
- output: {
1087
- splitRouteChunks: true,
1088
- },
1089
- performance: {
1090
- chunkSplit: {
1091
- strategy: 'custom',
1092
- splitChunks: {
1093
- chunks: 'async',
1094
- },
1095
- },
1096
- },
1097
- });
1098
- });
1099
-
1100
- test('keeps custom cache group details intact', () => {
1101
- const pluginDefaults = createTanstackRsbuildRouteSplittingProfile(
1102
- {},
1103
- ).defaultConfig;
1104
-
1105
- const mergedConfig = mergeConfig([
1106
- pluginDefaults,
1107
- {
1108
- performance: {
1109
- chunkSplit: {
1110
- strategy: 'custom',
1111
- splitChunks: {
1112
- chunks: 'all',
1113
- cacheGroups: {
1114
- tractors: {
1115
- name: 'tractors',
1116
- test: /tractors/u,
1117
- },
1118
- },
1119
- },
1120
- },
1121
- },
1122
- },
1123
- ]);
1124
-
1125
- expect(
1126
- (
1127
- mergedConfig as {
1128
- performance?: {
1129
- chunkSplit?: {
1130
- splitChunks?: {
1131
- cacheGroups?: {
1132
- tractors?: {
1133
- test?: RegExp;
1134
- };
1135
- };
1136
- };
1137
- };
1138
- };
1139
- }
1140
- ).performance?.chunkSplit?.splitChunks?.cacheGroups?.tractors?.test,
1141
- ).toEqual(/tractors/u);
1142
- expect(mergedConfig).toMatchObject({
1143
- output: {
1144
- splitRouteChunks: true,
1145
- },
1146
- performance: {
1147
- chunkSplit: {
1148
- strategy: 'custom',
1149
- splitChunks: {
1150
- chunks: 'all',
1151
- cacheGroups: {
1152
- tractors: {
1153
- name: 'tractors',
1154
- },
1155
- },
1156
- },
1157
- },
1158
- },
1159
- });
1160
- });
1161
-
1162
- test('plugin opt-out can still combine with manual builder chunking', () => {
1163
- const pluginDefaults = createTanstackRsbuildRouteSplittingProfile({
1164
- routeCodeSplitting: false,
1165
- }).defaultConfig;
1166
-
1167
- expect(
1168
- mergeConfig([
1169
- pluginDefaults,
1170
- {
1171
- performance: {
1172
- chunkSplit: {
1173
- strategy: 'single-vendor',
1174
- },
1175
- },
1176
- },
1177
- ]),
1178
- ).toMatchObject({
1179
- output: {
1180
- splitRouteChunks: false,
1181
- },
1182
- performance: {
1183
- chunkSplit: {
1184
- strategy: 'single-vendor',
1185
- },
1186
- },
1187
- });
1188
- });
1189
- });
1190
-
1191
- describe('generateTanstackRouteArtifacts export', () => {
1192
- test('is exported as a function', () => {
1193
- expect(typeof generateTanstackRouteArtifacts).toBe('function');
1194
- });
1195
420
  });