@bleedingdev/modern-js-plugin-i18n 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.
Files changed (79) hide show
  1. package/dist/cjs/cli/index.js +0 -22
  2. package/dist/cjs/runtime/Link.js +19 -32
  3. package/dist/cjs/runtime/context.js +26 -61
  4. package/dist/cjs/runtime/contextHelpers.js +5 -5
  5. package/dist/cjs/runtime/core.js +17 -14
  6. package/dist/cjs/runtime/hooks.js +7 -8
  7. package/dist/cjs/runtime/localizedPaths.js +11 -9
  8. package/dist/cjs/runtime/providerComposition.js +37 -13
  9. package/dist/cjs/runtime/routerAdapter.js +35 -140
  10. package/dist/cjs/runtime/utils.js +17 -19
  11. package/dist/cjs/server/index.js +10 -7
  12. package/dist/cjs/server/redirectPolicy.js +36 -31
  13. package/dist/cjs/shared/urlStrategy.js +18 -0
  14. package/dist/esm/cli/index.mjs +0 -22
  15. package/dist/esm/runtime/Link.mjs +19 -32
  16. package/dist/esm/runtime/context.mjs +18 -62
  17. package/dist/esm/runtime/contextHelpers.mjs +5 -5
  18. package/dist/esm/runtime/core.mjs +8 -5
  19. package/dist/esm/runtime/hooks.mjs +7 -8
  20. package/dist/esm/runtime/localizedPaths.mjs +11 -9
  21. package/dist/esm/runtime/providerComposition.mjs +38 -14
  22. package/dist/esm/runtime/routerAdapter.mjs +30 -141
  23. package/dist/esm/runtime/utils.mjs +15 -17
  24. package/dist/esm/server/index.mjs +10 -7
  25. package/dist/esm/server/redirectPolicy.mjs +29 -24
  26. package/dist/esm/shared/urlStrategy.mjs +0 -0
  27. package/dist/esm-node/cli/index.mjs +0 -22
  28. package/dist/esm-node/runtime/Link.mjs +19 -32
  29. package/dist/esm-node/runtime/context.mjs +18 -62
  30. package/dist/esm-node/runtime/contextHelpers.mjs +5 -5
  31. package/dist/esm-node/runtime/core.mjs +8 -5
  32. package/dist/esm-node/runtime/hooks.mjs +7 -8
  33. package/dist/esm-node/runtime/localizedPaths.mjs +11 -9
  34. package/dist/esm-node/runtime/providerComposition.mjs +38 -14
  35. package/dist/esm-node/runtime/routerAdapter.mjs +30 -141
  36. package/dist/esm-node/runtime/utils.mjs +15 -17
  37. package/dist/esm-node/server/index.mjs +10 -7
  38. package/dist/esm-node/server/redirectPolicy.mjs +29 -24
  39. package/dist/esm-node/shared/urlStrategy.mjs +1 -0
  40. package/dist/types/runtime/context.d.ts +8 -11
  41. package/dist/types/runtime/contextHelpers.d.ts +2 -2
  42. package/dist/types/runtime/core.d.ts +11 -2
  43. package/dist/types/runtime/hooks.d.ts +4 -5
  44. package/dist/types/runtime/localizedPaths.d.ts +4 -4
  45. package/dist/types/runtime/providerComposition.d.ts +13 -1
  46. package/dist/types/runtime/routerAdapter.d.ts +23 -6
  47. package/dist/types/runtime/utils.d.ts +4 -12
  48. package/dist/types/server/index.d.ts +2 -0
  49. package/dist/types/server/redirectPolicy.d.ts +5 -13
  50. package/dist/types/shared/type.d.ts +0 -14
  51. package/dist/types/shared/urlStrategy.d.ts +7 -0
  52. package/package.json +21 -11
  53. package/rstest.config.mts +2 -8
  54. package/src/cli/index.ts +1 -43
  55. package/src/runtime/Link.tsx +20 -40
  56. package/src/runtime/context.tsx +34 -133
  57. package/src/runtime/contextHelpers.ts +7 -5
  58. package/src/runtime/core.tsx +26 -6
  59. package/src/runtime/hooks.ts +14 -9
  60. package/src/runtime/localizedPaths.ts +24 -19
  61. package/src/runtime/providerComposition.tsx +71 -23
  62. package/src/runtime/routerAdapter.tsx +73 -338
  63. package/src/runtime/utils.ts +40 -31
  64. package/src/server/index.ts +44 -9
  65. package/src/server/redirectPolicy.ts +70 -70
  66. package/src/shared/type.ts +0 -14
  67. package/src/shared/urlStrategy.ts +11 -0
  68. package/tests/federatedI18nBoundaryResolver.test.tsx +71 -0
  69. package/tests/i18nUtils.test.ts +31 -65
  70. package/tests/link.test.tsx +13 -511
  71. package/tests/linkTypes.test.ts +12 -3
  72. package/tests/localisedUrlRewriteMatrix.fork.test.ts +4 -96
  73. package/tests/localisedUrls.test.ts +7 -216
  74. package/tests/reactI18nextRuntimeBoundary.test.ts +1 -82
  75. package/tests/redirectPolicy.test.ts +39 -30
  76. package/tests/routerAdapter.test.tsx +95 -1055
  77. package/tests/federatedI18nBoundary.client.test.tsx +0 -129
  78. package/tests/federatedI18nBoundary.test.tsx +0 -132
  79. package/tests/peerCohort.test.ts +0 -25
@@ -1,4 +1,3 @@
1
- import type { NestedRouteForCli } from '@modern-js/types';
2
1
  import { describe, expect, test } from '@rstest/core';
3
2
  import { i18nPlugin as i18nCliPlugin } from '../src/cli';
4
3
  import {
@@ -7,19 +6,6 @@ import {
7
6
  matchesApiPrefix,
8
7
  } from '../src/server';
9
8
 
10
- const createRoute = (
11
- path: string,
12
- children?: NestedRouteForCli[],
13
- ): NestedRouteForCli => ({
14
- id: path,
15
- path,
16
- type: 'nested',
17
- origin: 'file-system',
18
- routeType: children ? 'layout' : 'page',
19
- _component: `${path}.tsx`,
20
- children,
21
- });
22
-
23
9
  const createRequestContext = (pathname: string) =>
24
10
  ({
25
11
  req: {
@@ -63,149 +49,16 @@ describe('cli modifyFileSystemRoutes', () => {
63
49
  expect(runtimePlugin?.config.reactI18next).toBe(false);
64
50
  });
65
51
 
66
- const setupModifyRoutes = (localeDetection: Record<string, unknown>) => {
67
- let modifyRoutes:
68
- | ((args: { entrypoint: any; routes: any[] }) => {
69
- entrypoint: any;
70
- routes: any[];
71
- })
72
- | undefined;
73
-
74
- i18nCliPlugin({ localeDetection }).setup({
52
+ test('upstream-style configs without a map keep routes untouched', () => {
53
+ const modifyRoutes = rstest.fn();
54
+ i18nCliPlugin({
55
+ localeDetection: { localePathRedirect: true, languages: ['en', 'cs'] },
56
+ }).setup({
75
57
  _internalRuntimePlugins: () => {},
76
- modifyFileSystemRoutes: (fn: any) => {
77
- modifyRoutes = fn;
78
- },
79
58
  _internalServerPlugins: () => {},
59
+ modifyFileSystemRoutes: modifyRoutes,
80
60
  } as any);
81
-
82
- expect(modifyRoutes).toBeDefined();
83
- return modifyRoutes!;
84
- };
85
-
86
- const generateLocalisedRoutes = () => {
87
- const modifyRoutes = setupModifyRoutes({
88
- localePathRedirect: true,
89
- languages: ['en', 'cs', 'de'],
90
- localisedUrls: {
91
- '/about': {
92
- en: '/about',
93
- cs: '/o-nas',
94
- de: '/ueber-uns',
95
- },
96
- '/products': {
97
- en: '/products',
98
- cs: '/produkty',
99
- de: '/produkte',
100
- },
101
- '/products/:slug': {
102
- en: '/products/:slug',
103
- cs: '/produkty/:slug',
104
- de: '/produkte/:slug',
105
- },
106
- '/docs': {
107
- en: '/docs',
108
- cs: '/dokumenty',
109
- de: '/dokumente',
110
- },
111
- '/docs/*': {
112
- en: '/docs/*',
113
- cs: '/dokumenty/*',
114
- de: '/dokumente/*',
115
- },
116
- },
117
- });
118
- const routes = [
119
- createRoute(':lang', [
120
- createRoute('about'),
121
- createRoute('products', [createRoute(':slug')]),
122
- createRoute('docs', [createRoute('*')]),
123
- ]),
124
- ];
125
-
126
- const result = modifyRoutes({
127
- entrypoint: { entryName: 'main' },
128
- routes,
129
- });
130
-
131
- return result.routes;
132
- };
133
-
134
- test('creates deterministic localized aliases with canonical route identity', () => {
135
- const localisedRoutes = generateLocalisedRoutes();
136
- expect(generateLocalisedRoutes()).toEqual(localisedRoutes);
137
-
138
- const [localeLayout] = localisedRoutes;
139
- expect(localeLayout.children?.map(route => route.path)).toEqual([
140
- 'about',
141
- 'o-nas',
142
- 'ueber-uns',
143
- 'products',
144
- 'produkty',
145
- 'produkte',
146
- 'docs',
147
- 'dokumenty',
148
- 'dokumente',
149
- ]);
150
- expect(localeLayout.children?.slice(0, 3)).toMatchObject([
151
- {
152
- _component: 'about.tsx',
153
- modernCanonicalPath: '/about',
154
- path: 'about',
155
- },
156
- {
157
- _component: 'about.tsx',
158
- modernCanonicalPath: '/about',
159
- path: 'o-nas',
160
- },
161
- {
162
- _component: 'about.tsx',
163
- modernCanonicalPath: '/about',
164
- path: 'ueber-uns',
165
- },
166
- ]);
167
- for (const productRoute of localeLayout.children?.slice(3, 6) ?? []) {
168
- expect(productRoute).toMatchObject({
169
- _component: 'products.tsx',
170
- modernCanonicalPath: '/products',
171
- });
172
- expect(productRoute.children?.[0]).toMatchObject({
173
- _component: ':slug.tsx',
174
- modernCanonicalPath: '/products/:slug',
175
- path: ':slug',
176
- });
177
- }
178
- });
179
-
180
- test('upstream-style configs without a map keep routes untouched', () => {
181
- const modifyRoutes = setupModifyRoutes({
182
- localePathRedirect: true,
183
- languages: ['en', 'cs'],
184
- });
185
- const routes = [createRoute(':lang', [createRoute('about')])];
186
-
187
- const result = modifyRoutes({ entrypoint: { entryName: 'main' }, routes });
188
-
189
- expect(result.routes).toBe(routes);
190
- });
191
-
192
- test('a configured map still expands localised route aliases', () => {
193
- const modifyRoutes = setupModifyRoutes({
194
- localePathRedirect: true,
195
- languages: ['en', 'cs'],
196
- localisedUrls: {
197
- '/about': { en: '/about', cs: '/o-nas' },
198
- },
199
- });
200
- const routes = [createRoute(':lang', [createRoute('about')])];
201
-
202
- const result = modifyRoutes({ entrypoint: { entryName: 'main' }, routes });
203
-
204
- const localeRoute = result.routes[0] as NestedRouteForCli;
205
- expect(localeRoute.children?.map(route => route.path)).toEqual([
206
- 'about',
207
- 'o-nas',
208
- ]);
61
+ expect(modifyRoutes).not.toHaveBeenCalled();
209
62
  });
210
63
  });
211
64
 
@@ -283,68 +136,6 @@ describe('i18n server API prefix skips', () => {
283
136
  }
284
137
  });
285
138
 
286
- test('canonical redirect survives malformed percent-encoding', async () => {
287
- const middlewares: any[] = [];
288
- const routes = [{ entryName: 'main', entryPath: '', urlPath: '/' }];
289
- let prepare: (() => void) | undefined;
290
-
291
- i18nServerPlugin({
292
- localeDetection: {
293
- fallbackLanguage: 'en',
294
- languages: ['en', 'cs'],
295
- localePathRedirect: true,
296
- localisedUrls: {
297
- '/products/:slug': {
298
- en: '/products/:slug',
299
- cs: '/produkty/:slug',
300
- },
301
- },
302
- },
303
- staticRoutePrefixes: [],
304
- }).setup({
305
- getServerConfig: () => ({}),
306
- getServerContext: () => ({ middlewares, routes }),
307
- onPrepare: fn => {
308
- prepare = fn;
309
- },
310
- } as any);
311
-
312
- prepare?.();
313
-
314
- const redirectMiddleware = middlewares.find(
315
- middleware => middleware.name === 'i18n-server-middleware',
316
- );
317
- const createContext = (pathname: string) =>
318
- ({
319
- req: {
320
- url: `http://localhost${pathname}`,
321
- header: () => ({ host: 'localhost' }),
322
- },
323
- get: () => null,
324
- }) as any;
325
-
326
- // Sanity: well-formed non-canonical slugs still redirect.
327
- const redirected = await redirectMiddleware.handler(
328
- createContext('/cs/products/bota'),
329
- async () => {},
330
- );
331
- expect(redirected.status).toBe(302);
332
- expect(redirected.headers.get('location')).toBe('/cs/produkty/bota');
333
- expect(redirected.headers.get('cache-control')).toBe('private, no-store');
334
- expect(redirected.headers.get('vary')).toBe('Accept-Language, Cookie');
335
-
336
- // Malformed encoding must fall through to next() instead of throwing.
337
- let nextCalls = 0;
338
- const response = await redirectMiddleware.handler(
339
- createContext('/cs/produkty/%E0%A4%A'),
340
- async () => {
341
- nextCalls++;
342
- },
343
- );
344
- expect(response).toBeUndefined();
345
- expect(nextCalls).toBe(1);
346
- });
347
-
348
139
  test('uses /api as the BFF prefix when BFF config is present without prefix', async () => {
349
140
  const middlewares: any[] = [];
350
141
  const routes = [{ entryName: 'main', entryPath: '', urlPath: '/' }];
@@ -1,13 +1,12 @@
1
1
  import { mkdtemp, rm } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
- import { basename, resolve } from 'node:path';
3
+ import { resolve } from 'node:path';
4
4
  import { pathToFileURL } from 'node:url';
5
5
  import { describe, expect, test } from '@rstest/core';
6
6
  import { build } from 'esbuild';
7
7
  import i18next from 'i18next';
8
8
  import { createElement } from 'react';
9
9
  import { renderToStaticMarkup } from 'react-dom/server';
10
- import { i18nPlugin as noReactI18nextPlugin } from '../src/runtime/no-react-i18next';
11
10
 
12
11
  const runtimeRoot = resolve(__dirname, '../src/runtime');
13
12
 
@@ -37,31 +36,6 @@ describe('react-i18next runtime boundary', () => {
37
36
  ).resolves.toBeDefined();
38
37
  });
39
38
 
40
- test('bundles the default runtime entry with the react-i18next adapter', async () => {
41
- let adapterResolutions = 0;
42
- await build({
43
- bundle: true,
44
- entryPoints: [resolve(runtimeRoot, 'index.tsx')],
45
- format: 'esm',
46
- packages: 'external',
47
- platform: 'node',
48
- plugins: [
49
- {
50
- name: 'observe-react-i18next',
51
- setup(buildApi) {
52
- buildApi.onResolve({ filter: /^react-i18next$/ }, args => {
53
- adapterResolutions += 1;
54
- return { external: true, path: args.path };
55
- });
56
- },
57
- },
58
- ],
59
- write: false,
60
- });
61
-
62
- expect(adapterResolutions).toBeGreaterThan(0);
63
- });
64
-
65
39
  test('gets router capabilities from the selected runtime provider', async () => {
66
40
  await expect(
67
41
  build({
@@ -90,61 +64,6 @@ describe('react-i18next runtime boundary', () => {
90
64
  ).resolves.toBeDefined();
91
65
  });
92
66
 
93
- test('keeps expensive lifecycle helpers behind asynchronous bundle edges', async () => {
94
- const result = await build({
95
- bundle: true,
96
- entryPoints: [resolve(runtimeRoot, 'pluginSetup.ts')],
97
- format: 'esm',
98
- metafile: true,
99
- outdir: 'out',
100
- packages: 'external',
101
- platform: 'node',
102
- splitting: true,
103
- write: false,
104
- });
105
-
106
- const entryOutput = Object.values(result.metafile.outputs).find(output =>
107
- output.entryPoint?.endsWith('/pluginSetup.ts'),
108
- );
109
- if (!entryOutput) {
110
- throw new Error('pluginSetup entry output was not generated');
111
- }
112
- const dynamicOutputNames = entryOutput.imports
113
- .filter(moduleImport => moduleImport.kind === 'dynamic-import')
114
- .map(moduleImport => basename(moduleImport.path));
115
- const dynamicOutputs = Object.entries(result.metafile.outputs)
116
- .filter(([outputPath]) =>
117
- dynamicOutputNames.includes(basename(outputPath)),
118
- )
119
- .map(([, output]) => output);
120
- const dynamicEntries = dynamicOutputs.map(output =>
121
- basename(output.entryPoint ?? ''),
122
- );
123
-
124
- expect(dynamicEntries).toEqual(
125
- expect.arrayContaining(['middleware.ts', 'utils.ts']),
126
- );
127
- });
128
-
129
- test('registers plugin lifecycle hooks synchronously', () => {
130
- let beforeRender: unknown;
131
- let rootWrapper: unknown;
132
- const plugin = noReactI18nextPlugin({ reactI18next: false });
133
- const setupResult = plugin.setup?.({
134
- getRuntimeConfig: () => ({}),
135
- onBeforeRender: hook => {
136
- beforeRender = hook;
137
- },
138
- wrapRoot: wrapper => {
139
- rootWrapper = wrapper;
140
- },
141
- } as any);
142
-
143
- expect(setupResult).toBeUndefined();
144
- expect(typeof beforeRender).toBe('function');
145
- expect(typeof rootWrapper).toBe('function');
146
- });
147
-
148
67
  test('shares Modern i18n context across independently bundled runtime copies', async () => {
149
68
  const tempDir = await mkdtemp(
150
69
  resolve(__dirname, '.modern-i18n-runtime-boundary-'),
@@ -5,41 +5,50 @@ import {
5
5
  shouldIgnoreRedirect as shouldIgnoreServerRedirect,
6
6
  } from '../src/server/redirectPolicy';
7
7
 
8
- const languages = ['en', 'cs'];
9
-
10
- const defaultSkippedPaths = [
11
- '/backend-mf-manifest.json',
12
- '/backendRemoteEntry.cjs',
13
- '/mf-manifest.json',
14
- '/mf-stats.json',
15
- '/remoteEntry.js',
16
- '/static/app.js',
17
- '/upload/avatar.png',
18
- ];
19
-
20
- describe('locale redirect default skip policy', () => {
21
- test('skips ADR-0002 Module Federation and static endpoints on the server', () => {
22
- for (const pathname of defaultSkippedPaths) {
23
- expect(isStaticResourceRequest(pathname, [], languages)).toBe(true);
24
- expect(shouldIgnoreServerRedirect(pathname, '/', undefined)).toBe(true);
25
- }
26
- });
27
-
28
- test('skips language-prefixed static and upload endpoints', () => {
29
- expect(isStaticResourceRequest('/cs/static/app.js', [], languages)).toBe(
30
- true,
31
- );
8
+ describe('native locale redirect safeguards', () => {
9
+ test('retains configured ignores across runtime and server entry prefixes', () => {
10
+ const rules = ['/private'];
32
11
  expect(
33
- isStaticResourceRequest('/en/upload/avatar.png', [], languages),
12
+ shouldIgnoreRuntimeRedirect('/cs/private/data', ['en', 'cs'], rules),
34
13
  ).toBe(true);
14
+ expect(
15
+ shouldIgnoreRuntimeRedirect('/cs/private-data', ['en', 'cs'], rules),
16
+ ).toBe(false);
17
+ expect(
18
+ shouldIgnoreServerRedirect(
19
+ '/app/cs/private/data',
20
+ '/app/*',
21
+ rules,
22
+ undefined,
23
+ ['en', 'cs'],
24
+ ),
25
+ ).toBe(true);
26
+ expect(
27
+ shouldIgnoreServerRedirect(
28
+ '/app/cs/private-data',
29
+ '/app/*',
30
+ rules,
31
+ undefined,
32
+ ['en', 'cs'],
33
+ ),
34
+ ).toBe(false);
35
35
  });
36
-
37
- test('uses the same default skip policy in runtime redirects', () => {
38
- for (const pathname of defaultSkippedPaths) {
39
- expect(shouldIgnoreRuntimeRedirect(pathname, languages)).toBe(true);
40
- expect(shouldIgnoreRuntimeRedirect(`/cs${pathname}`, languages)).toBe(
36
+ test('retains configured and native static paths without fork policy', () => {
37
+ for (const pathname of [
38
+ '/static/app.js',
39
+ '/upload/a.png',
40
+ '/cs/assets/main.js',
41
+ ]) {
42
+ expect(isStaticResourceRequest(pathname, ['/assets'], ['en', 'cs'])).toBe(
41
43
  true,
42
44
  );
43
45
  }
46
+ expect(
47
+ isStaticResourceRequest(
48
+ '/assets-other/main.js',
49
+ ['/assets'],
50
+ ['en', 'cs'],
51
+ ),
52
+ ).toBe(false);
44
53
  });
45
54
  });