@bleedingdev/modern-js-runtime-extensions 3.9.0-ultramodern.9 → 3.9.3-ultramodern.2

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 (124) hide show
  1. package/dist/cjs/{reactContext.js → localisedUrls/config.js} +14 -10
  2. package/dist/cjs/localisedUrls/index.js +95 -0
  3. package/dist/cjs/localisedUrls/normalise.js +87 -0
  4. package/dist/cjs/localisedUrls/pathname.js +68 -0
  5. package/dist/cjs/localisedUrls/patterns.js +214 -0
  6. package/dist/cjs/localisedUrls/redirect.js +104 -0
  7. package/dist/cjs/localisedUrls/resolve.js +78 -0
  8. package/dist/cjs/localisedUrls/routeIdentity.js +92 -0
  9. package/dist/cjs/localisedUrls/routes.js +217 -0
  10. package/dist/cjs/localisedUrls/types.js +18 -0
  11. package/dist/cjs/rendererHead.js +18 -5
  12. package/dist/cjs/request-policy/policyCore.js +1 -1
  13. package/dist/cjs/request-policy/transport.js +2 -2
  14. package/dist/cjs/routerState.js +85 -77
  15. package/dist/cjs/routerStatePlugin.js +63 -9
  16. package/dist/cjs/server-config/serverSource.js +18 -0
  17. package/dist/esm/localisedUrls/config.mjs +11 -0
  18. package/dist/esm/localisedUrls/index.mjs +8 -0
  19. package/dist/esm/localisedUrls/normalise.mjs +37 -0
  20. package/dist/esm/localisedUrls/pathname.mjs +24 -0
  21. package/dist/esm/localisedUrls/patterns.mjs +152 -0
  22. package/dist/esm/localisedUrls/redirect.mjs +57 -0
  23. package/dist/esm/localisedUrls/resolve.mjs +37 -0
  24. package/dist/esm/localisedUrls/routeIdentity.mjs +54 -0
  25. package/dist/esm/localisedUrls/routes.mjs +176 -0
  26. package/dist/esm/localisedUrls/types.mjs +0 -0
  27. package/dist/esm/rendererHead.mjs +14 -5
  28. package/dist/esm/request-policy/policyCore.mjs +1 -1
  29. package/dist/esm/request-policy/transport.mjs +2 -2
  30. package/dist/esm/routerState.mjs +81 -73
  31. package/dist/esm/routerStatePlugin.mjs +62 -11
  32. package/dist/esm/server-config/serverSource.mjs +0 -0
  33. package/dist/esm-node/localisedUrls/config.mjs +12 -0
  34. package/dist/esm-node/localisedUrls/index.mjs +9 -0
  35. package/dist/esm-node/localisedUrls/normalise.mjs +38 -0
  36. package/dist/esm-node/localisedUrls/pathname.mjs +25 -0
  37. package/dist/esm-node/localisedUrls/patterns.mjs +153 -0
  38. package/dist/esm-node/localisedUrls/redirect.mjs +58 -0
  39. package/dist/esm-node/localisedUrls/resolve.mjs +38 -0
  40. package/dist/esm-node/localisedUrls/routeIdentity.mjs +55 -0
  41. package/dist/esm-node/localisedUrls/routes.mjs +177 -0
  42. package/dist/esm-node/localisedUrls/types.mjs +1 -0
  43. package/dist/esm-node/rendererHead.mjs +14 -5
  44. package/dist/esm-node/request-policy/policyCore.mjs +1 -1
  45. package/dist/esm-node/request-policy/transport.mjs +2 -2
  46. package/dist/esm-node/routerState.mjs +81 -73
  47. package/dist/esm-node/routerStatePlugin.mjs +62 -11
  48. package/dist/esm-node/server-config/serverSource.mjs +1 -0
  49. package/dist/types/helmetCompat.d.ts +1 -1
  50. package/dist/types/localisedUrls/config.d.ts +9 -0
  51. package/dist/types/localisedUrls/index.d.ts +10 -0
  52. package/dist/types/localisedUrls/normalise.d.ts +10 -0
  53. package/dist/types/localisedUrls/pathname.d.ts +4 -0
  54. package/dist/types/localisedUrls/patterns.d.ts +11 -0
  55. package/dist/types/localisedUrls/redirect.d.ts +19 -0
  56. package/dist/types/localisedUrls/resolve.d.ts +7 -0
  57. package/dist/types/localisedUrls/routeIdentity.d.ts +22 -0
  58. package/dist/types/localisedUrls/routes.d.ts +3 -0
  59. package/dist/types/localisedUrls/types.d.ts +14 -0
  60. package/dist/types/rendererHead.d.ts +5 -3
  61. package/dist/types/request-policy/types.d.ts +1 -1
  62. package/dist/types/routeHydration.d.ts +2 -2
  63. package/dist/types/routerProvider.d.ts +1 -3
  64. package/dist/types/routerState.d.ts +11 -6
  65. package/dist/types/routerStatePlugin.d.ts +12 -0
  66. package/dist/types/routerStateTypes.d.ts +47 -17
  67. package/dist/types/server-config/index.d.ts +1 -0
  68. package/dist/types/server-config/serverSource.d.ts +16 -0
  69. package/package.json +42 -44
  70. package/src/helmetCompat.ts +1 -1
  71. package/src/localisedUrls/config.ts +18 -0
  72. package/src/localisedUrls/index.ts +25 -0
  73. package/src/localisedUrls/normalise.ts +62 -0
  74. package/src/localisedUrls/pathname.ts +60 -0
  75. package/src/localisedUrls/patterns.ts +283 -0
  76. package/src/localisedUrls/redirect.ts +110 -0
  77. package/src/localisedUrls/resolve.ts +106 -0
  78. package/src/localisedUrls/routeIdentity.ts +83 -0
  79. package/src/localisedUrls/routes.ts +451 -0
  80. package/src/localisedUrls/types.ts +18 -0
  81. package/src/rendererHead.ts +25 -8
  82. package/src/request-policy/policyCore.ts +1 -1
  83. package/src/request-policy/transport.ts +2 -2
  84. package/src/request-policy/types.ts +1 -1
  85. package/src/routeHydration.ts +2 -2
  86. package/src/routerProvider.ts +1 -1
  87. package/src/routerState.ts +127 -115
  88. package/src/routerStatePlugin.ts +106 -10
  89. package/src/routerStateTypes.ts +47 -17
  90. package/src/server-config/index.ts +1 -0
  91. package/src/server-config/serverSource.ts +23 -0
  92. package/dist/cjs/buildIdentity.js +0 -73
  93. package/dist/esm/buildIdentity.mjs +0 -35
  94. package/dist/esm/reactContext.mjs +0 -7
  95. package/dist/esm-node/buildIdentity.mjs +0 -36
  96. package/dist/esm-node/reactContext.mjs +0 -8
  97. package/dist/types/buildIdentity.d.ts +0 -37
  98. package/dist/types/reactContext.d.ts +0 -6
  99. package/rslib.config.mts +0 -19
  100. package/rstest.config.mts +0 -38
  101. package/src/buildIdentity.ts +0 -61
  102. package/src/reactContext.ts +0 -16
  103. package/tests/buildIdentity.test.ts +0 -51
  104. package/tests/contextExtensions.test.ts +0 -27
  105. package/tests/headRuntime.test.tsx +0 -229
  106. package/tests/rendererHead.test.ts +0 -187
  107. package/tests/request-policy/browser-policy.test.ts +0 -108
  108. package/tests/request-policy/client-isolation.test.ts +0 -109
  109. package/tests/request-policy/node-policy.test.ts +0 -447
  110. package/tests/request-policy/policyCore.test.ts +0 -129
  111. package/tests/request-policy/requestContext.test.ts +0 -66
  112. package/tests/request-policy/requestFactory.outbound-contract.test.ts +0 -290
  113. package/tests/request-policy/traceparent.test.ts +0 -52
  114. package/tests/request-policy/transport-retry.test.ts +0 -367
  115. package/tests/routeHydration.test.ts +0 -180
  116. package/tests/routerPrefetchPolicy.test.ts +0 -102
  117. package/tests/routerProvider.test.ts +0 -29
  118. package/tests/routerState.test.ts +0 -78
  119. package/tests/routerStatePlugin.test.ts +0 -63
  120. package/tests/runtimeContextProjection.test.ts +0 -83
  121. package/tests/safeFailure.test.ts +0 -136
  122. package/tests/shellStream.test.ts +0 -122
  123. package/tests/workspaceEvents.test.ts +0 -221
  124. package/tsconfig.json +0 -10
@@ -1,7 +0,0 @@
1
- function getRuntimeReactContext(kind, create) {
2
- const key = Symbol.for(`@modern-js/runtime:react-context:v1:${kind}`);
3
- const contexts = globalThis;
4
- contexts[key] ??= create();
5
- return contexts[key];
6
- }
7
- export { getRuntimeReactContext };
@@ -1,36 +0,0 @@
1
- import "node:module";
2
- function readBuildValue(read, fallback) {
3
- try {
4
- return read();
5
- } catch (error) {
6
- if (null !== error && 'object' == typeof error && 'name' in error && 'ReferenceError' === error.name) return fallback;
7
- throw error;
8
- }
9
- }
10
- function resolveUltramodernBuildArtifact(artifact, readers) {
11
- const buildMarker = readers ? readBuildValue(readers.buildMarker, artifact.deliveryUnit.buildMarker) : artifact.deliveryUnit.buildMarker;
12
- const sourceRevision = readers ? readBuildValue(readers.sourceRevision, artifact.deliveryUnit.sourceRevision) : artifact.deliveryUnit.sourceRevision;
13
- const identity = {
14
- build: buildMarker,
15
- buildMarker,
16
- sourceRevision
17
- };
18
- return {
19
- ...artifact,
20
- deliveryUnit: {
21
- ...artifact.deliveryUnit,
22
- ...identity
23
- },
24
- surfaces: {
25
- api: {
26
- ...artifact.surfaces.api,
27
- ...identity
28
- },
29
- ui: {
30
- ...artifact.surfaces.ui,
31
- ...identity
32
- }
33
- }
34
- };
35
- }
36
- export { resolveUltramodernBuildArtifact };
@@ -1,8 +0,0 @@
1
- import "node:module";
2
- function getRuntimeReactContext(kind, create) {
3
- const key = Symbol.for(`@modern-js/runtime:react-context:v1:${kind}`);
4
- const contexts = globalThis;
5
- contexts[key] ??= create();
6
- return contexts[key];
7
- }
8
- export { getRuntimeReactContext };
@@ -1,37 +0,0 @@
1
- interface BuildIdentity {
2
- readonly build: string;
3
- readonly buildMarker: string;
4
- readonly sourceRevision: string;
5
- }
6
- interface BuildArtifact {
7
- readonly deliveryUnit: BuildIdentity;
8
- readonly surfaces: {
9
- readonly api: BuildIdentity;
10
- readonly ui: BuildIdentity;
11
- };
12
- }
13
- interface BuildIdentityReaders {
14
- readonly buildMarker: () => string;
15
- readonly sourceRevision: () => string;
16
- }
17
- /** Read compiler constants in the caller, even when this package is external. */
18
- export declare function resolveUltramodernBuildArtifact<Artifact extends BuildArtifact>(artifact: Artifact, readers?: BuildIdentityReaders): Artifact & {
19
- deliveryUnit: {
20
- build: string;
21
- buildMarker: string;
22
- sourceRevision: string;
23
- };
24
- surfaces: {
25
- api: {
26
- build: string;
27
- buildMarker: string;
28
- sourceRevision: string;
29
- };
30
- ui: {
31
- build: string;
32
- buildMarker: string;
33
- sourceRevision: string;
34
- };
35
- };
36
- };
37
- export {};
@@ -1,6 +0,0 @@
1
- /**
2
- * Federation can bundle a runtime subpath separately from its shared root.
3
- * Share React context identity across those copies, leaving request values to
4
- * React's providers. Public and internal contexts retain separate identities.
5
- */
6
- export declare function getRuntimeReactContext<T>(kind: 'public' | 'internal', create: () => T): T;
package/rslib.config.mts DELETED
@@ -1,19 +0,0 @@
1
- import { RSLIB_CODE_ENTRY_GLOB, rslibConfig } from '@modern-js/rslib';
2
- import { defineConfig } from '@rslib/core';
3
-
4
- export default defineConfig({
5
- ...rslibConfig,
6
- lib: rslibConfig.lib.map(lib =>
7
- lib.id === 'esm-web'
8
- ? {
9
- ...lib,
10
- source: {
11
- ...lib.source,
12
- entry: {
13
- index: [RSLIB_CODE_ENTRY_GLOB, '!./src/request-policy/node.ts'],
14
- },
15
- },
16
- }
17
- : lib,
18
- ),
19
- });
package/rstest.config.mts DELETED
@@ -1,38 +0,0 @@
1
- import type { ProjectConfig } from '@rstest/core';
2
- import { withTestPreset } from '@scripts/rstest-config';
3
-
4
- const commonConfig: ProjectConfig = {
5
- root: __dirname,
6
- globals: true,
7
- setupFiles: ['@scripts/rstest-config/setup.ts'],
8
- tools: {
9
- swc: {
10
- jsc: {
11
- transform: {
12
- react: {
13
- runtime: 'automatic',
14
- },
15
- },
16
- },
17
- },
18
- },
19
- };
20
-
21
- const browserPolicyProject = withTestPreset({
22
- name: 'runtime-extensions-request-policy-browser',
23
- testEnvironment: 'happy-dom',
24
- extends: commonConfig,
25
- include: ['tests/request-policy/browser-policy.test.ts'],
26
- });
27
-
28
- export default {
29
- projects: [
30
- withTestPreset({
31
- name: 'runtime-extensions-node',
32
- exclude: ['tests/request-policy/browser-policy.test.ts'],
33
- testEnvironment: 'node',
34
- extends: commonConfig,
35
- }),
36
- browserPolicyProject,
37
- ],
38
- };
@@ -1,61 +0,0 @@
1
- interface BuildIdentity {
2
- readonly build: string;
3
- readonly buildMarker: string;
4
- readonly sourceRevision: string;
5
- }
6
-
7
- interface BuildArtifact {
8
- readonly deliveryUnit: BuildIdentity;
9
- readonly surfaces: {
10
- readonly api: BuildIdentity;
11
- readonly ui: BuildIdentity;
12
- };
13
- }
14
-
15
- interface BuildIdentityReaders {
16
- readonly buildMarker: () => string;
17
- readonly sourceRevision: () => string;
18
- }
19
-
20
- function readBuildValue(read: () => string, fallback: string): string {
21
- try {
22
- return read();
23
- } catch (error) {
24
- // An uncompiled generated module has no injected constants. Check the
25
- // error tag across realms, since source validators may evaluate in a VM.
26
- if (
27
- error !== null &&
28
- typeof error === 'object' &&
29
- 'name' in error &&
30
- error.name === 'ReferenceError'
31
- ) {
32
- return fallback;
33
- }
34
- throw error;
35
- }
36
- }
37
-
38
- /** Read compiler constants in the caller, even when this package is external. */
39
- export function resolveUltramodernBuildArtifact<Artifact extends BuildArtifact>(
40
- artifact: Artifact,
41
- readers?: BuildIdentityReaders,
42
- ) {
43
- const buildMarker = readers
44
- ? readBuildValue(readers.buildMarker, artifact.deliveryUnit.buildMarker)
45
- : artifact.deliveryUnit.buildMarker;
46
- const sourceRevision = readers
47
- ? readBuildValue(
48
- readers.sourceRevision,
49
- artifact.deliveryUnit.sourceRevision,
50
- )
51
- : artifact.deliveryUnit.sourceRevision;
52
- const identity = { build: buildMarker, buildMarker, sourceRevision };
53
- return {
54
- ...artifact,
55
- deliveryUnit: { ...artifact.deliveryUnit, ...identity },
56
- surfaces: {
57
- api: { ...artifact.surfaces.api, ...identity },
58
- ui: { ...artifact.surfaces.ui, ...identity },
59
- },
60
- };
61
- }
@@ -1,16 +0,0 @@
1
- /**
2
- * Federation can bundle a runtime subpath separately from its shared root.
3
- * Share React context identity across those copies, leaving request values to
4
- * React's providers. Public and internal contexts retain separate identities.
5
- */
6
- export function getRuntimeReactContext<T>(
7
- kind: 'public' | 'internal',
8
- create: () => T,
9
- ): T {
10
- const key = Symbol.for(`@modern-js/runtime:react-context:v1:${kind}`);
11
- const contexts = globalThis as typeof globalThis & {
12
- [key: symbol]: unknown;
13
- };
14
- contexts[key] ??= create();
15
- return contexts[key] as T;
16
- }
@@ -1,51 +0,0 @@
1
- import { resolveUltramodernBuildArtifact } from '../src/buildIdentity';
2
-
3
- const artifact = {
4
- deliveryUnit: {
5
- build: 'original',
6
- buildMarker: 'original',
7
- sourceRevision: 'workspace',
8
- unitId: 'shop/catalog',
9
- },
10
- surfaces: {
11
- api: {
12
- build: 'original',
13
- buildMarker: 'original',
14
- sourceRevision: 'workspace',
15
- surface: 'api',
16
- },
17
- ui: {
18
- build: 'original',
19
- buildMarker: 'original',
20
- sourceRevision: 'workspace',
21
- surface: 'ui',
22
- },
23
- },
24
- } as const;
25
-
26
- const readers = {
27
- buildMarker: () => 'compiled-build',
28
- sourceRevision: () => 'compiled-revision',
29
- };
30
-
31
- it('falls back only for missing compiler references and preserves each supplied value', () => {
32
- expect(resolveUltramodernBuildArtifact(artifact)).toEqual(artifact);
33
-
34
- const result = resolveUltramodernBuildArtifact(artifact, {
35
- buildMarker: readers.buildMarker,
36
- sourceRevision: () => {
37
- throw new ReferenceError('ULTRAMODERN_SOURCE_REVISION is not defined');
38
- },
39
- });
40
- expect(result.deliveryUnit.buildMarker).toBe('compiled-build');
41
- expect(result.deliveryUnit.sourceRevision).toBe('workspace');
42
- const failure = new Error('reader failed');
43
- expect(() =>
44
- resolveUltramodernBuildArtifact(artifact, {
45
- ...readers,
46
- buildMarker: () => {
47
- throw failure;
48
- },
49
- }),
50
- ).toThrow(failure);
51
- });
@@ -1,27 +0,0 @@
1
- import { createRuntimeContextExtension } from '../src/contextExtensions';
2
-
3
- describe('runtime context extensions', () => {
4
- it('does not leak into string-key enumeration of the context', () => {
5
- const extension = createRuntimeContextExtension<string>('test:hidden');
6
- const context = {};
7
- const keysBefore = Object.keys(context);
8
-
9
- extension.set(context, 'secret');
10
-
11
- expect(Object.keys(context)).toEqual(keysBefore);
12
- expect(JSON.stringify(context)).not.toContain('secret');
13
- for (const key in context) {
14
- expect(typeof key).toBe('string');
15
- expect((context as Record<string, unknown>)[key]).not.toBe('secret');
16
- }
17
- });
18
-
19
- it('survives object spreads so SSR context copies keep their extensions', () => {
20
- const extension = createRuntimeContextExtension<string>('test:spread');
21
- const context = {};
22
- extension.set(context, 'carried');
23
-
24
- const copy = { ...context };
25
- expect(extension.get(copy)).toBe('carried');
26
- });
27
- });
@@ -1,229 +0,0 @@
1
- import React from 'react';
2
- import { renderToString } from 'react-dom/server';
3
- import { createHeadRuntime } from '../src/headRuntime';
4
- import { ensureHelmetContext, getHelmetContext } from '../src/helmetContext';
5
- import * as rendererHead from '../src/rendererHead';
6
-
7
- const { Head: Helmet, wrapServerRoot } = createHeadRuntime();
8
- const getHelmetData = (context: object) => getHelmetContext(context)?.helmet;
9
-
10
- const createServerContext = () => ({ isBrowser: false }) as any;
11
-
12
- const renderWithContext = (context: any, node: React.ReactNode) => {
13
- rendererHead.beginHeadRender(context);
14
- try {
15
- const html = renderToString(
16
- wrapServerRoot(node, {
17
- runtimeContext: context,
18
- helmetContext: ensureHelmetContext(context),
19
- }),
20
- );
21
- return rendererHead.completeHeadRender(context, html);
22
- } catch (error) {
23
- rendererHead.abortHeadRender(context);
24
- throw error;
25
- }
26
- };
27
-
28
- describe('server Helmet collection', () => {
29
- it('keeps independently registered adapters scoped to their own providers', () => {
30
- const outer = createHeadRuntime();
31
- const inner = createHeadRuntime();
32
- const outerContext = {};
33
- const innerContext = {};
34
-
35
- renderToString(
36
- outer.wrapServerRoot(
37
- inner.wrapServerRoot(
38
- <>
39
- <outer.Head>
40
- <title>outer application</title>
41
- </outer.Head>
42
- <inner.Head>
43
- <title>inner application</title>
44
- </inner.Head>
45
- </>,
46
- {
47
- runtimeContext: innerContext,
48
- helmetContext: ensureHelmetContext(innerContext),
49
- },
50
- ),
51
- {
52
- runtimeContext: outerContext,
53
- helmetContext: ensureHelmetContext(outerContext),
54
- },
55
- ),
56
- );
57
-
58
- expect(getHelmetData(outerContext)?.title.toString()).toBe(
59
- '<title data-rh="true">outer application</title>',
60
- );
61
- expect(getHelmetData(innerContext)?.title.toString()).toBe(
62
- '<title data-rh="true">inner application</title>',
63
- );
64
- });
65
-
66
- it('excludes Helmet records from a discarded Suspense primary branch', () => {
67
- const context = createServerContext();
68
- const never = new Promise<never>(() => {});
69
-
70
- const SuspendForever = (): null => {
71
- throw never;
72
- };
73
-
74
- const html = renderWithContext(
75
- context,
76
- <>
77
- <Helmet>
78
- <meta name="outside" content="committed" />
79
- </Helmet>
80
- <React.Suspense
81
- fallback={
82
- <>
83
- <Helmet>
84
- <meta name="fallback" content="committed" />
85
- </Helmet>
86
- <span>fallback rendered</span>
87
- </>
88
- }
89
- >
90
- <Helmet>
91
- <meta name="abandoned-unique" content="ghost" />
92
- </Helmet>
93
- <SuspendForever />
94
- </React.Suspense>
95
- </>,
96
- );
97
-
98
- expect(html).toContain('fallback rendered');
99
- const meta = getHelmetData(context)!.meta.toString();
100
- expect(meta).toContain('name="outside"');
101
- expect(meta).toContain('name="fallback"');
102
- expect(meta).not.toContain('name="abandoned-unique"');
103
- });
104
-
105
- it('collects title/meta during SSR and renders nothing inline', () => {
106
- const context = createServerContext();
107
- const html = renderWithContext(
108
- context,
109
- <main>
110
- <Helmet>
111
- <title>Server Title</title>
112
- <meta name="description" content="hello" />
113
- </Helmet>
114
- body
115
- </main>,
116
- );
117
-
118
- expect(html).not.toContain('Server Title');
119
- const helmet = getHelmetData(context)!;
120
- expect(helmet.title.toString()).toBe(
121
- '<title data-rh="true">Server Title</title>',
122
- );
123
- expect(helmet.meta.toString()).toBe(
124
- '<meta data-rh="true" name="description" content="hello">',
125
- );
126
- });
127
-
128
- it('dedupes meta by primary attribute across nested Helmets, inner wins', () => {
129
- const context = createServerContext();
130
- renderWithContext(
131
- context,
132
- <>
133
- <Helmet>
134
- <meta name="description" content="outer" />
135
- <meta name="keywords" content="modernjs" />
136
- </Helmet>
137
- <section>
138
- <Helmet>
139
- <meta name="description" content="inner" />
140
- </Helmet>
141
- </section>
142
- </>,
143
- );
144
-
145
- const meta = getHelmetData(context)!.meta.toString();
146
- expect(meta).toContain('content="inner"');
147
- expect(meta).not.toContain('content="outer"');
148
- expect(meta).toContain('content="modernjs"');
149
- });
150
-
151
- it('applies the innermost title and the innermost titleTemplate', () => {
152
- const context = createServerContext();
153
- renderWithContext(
154
- context,
155
- <>
156
- <Helmet titleTemplate="%s | Site">
157
- <title>Outer</title>
158
- </Helmet>
159
- <Helmet>
160
- <title>Inner</title>
161
- </Helmet>
162
- </>,
163
- );
164
-
165
- expect(getHelmetData(context)!.title.toString()).toBe(
166
- '<title data-rh="true">Inner | Site</title>',
167
- );
168
- });
169
-
170
- it('publishes prioritized SEO tags separately with component parity', () => {
171
- const context = createServerContext();
172
- renderWithContext(
173
- context,
174
- <Helmet prioritizeSeoTags htmlAttributes={{ class: 'app' } as any}>
175
- <title>Priority Page</title>
176
- <meta name="description" content="priority description" />
177
- <meta name="keywords" content="ordinary keywords" />
178
- <link rel="canonical" href="https://example.com/page" />
179
- <link rel="stylesheet" href="/page.css" />
180
- <script type="application/ld+json">{'{"name":"page"}'}</script>
181
- <script src="/ordinary.js" />
182
- </Helmet>,
183
- );
184
-
185
- const helmet = getHelmetData(context)!;
186
- expect(helmet.priority.toString()).toContain('priority description');
187
- expect(helmet.priority.toString()).toContain('https://example.com/page');
188
- expect(helmet.priority.toString()).toContain('application/ld+json');
189
- expect(helmet.meta.toString()).toContain('ordinary keywords');
190
- expect(helmet.meta.toString()).not.toContain('priority description');
191
- expect(helmet.link.toString()).toContain('/page.css');
192
- expect(helmet.link.toString()).not.toContain('https://example.com/page');
193
- expect(helmet.script.toString()).toContain('/ordinary.js');
194
- expect(helmet.script.toString()).not.toContain('application/ld+json');
195
- });
196
-
197
- it('is idempotent when React replays the tree (streaming SSR retry)', () => {
198
- const context = createServerContext();
199
- const app = (
200
- <>
201
- <Helmet titleTemplate="%s | Site">
202
- <title>Page</title>
203
- <meta name="description" content="stable" />
204
- <html lang="en" />
205
- </Helmet>
206
- <Helmet>
207
- <meta property="og:title" content="Page" />
208
- </Helmet>
209
- </>
210
- );
211
-
212
- renderWithContext(context, app);
213
- const first = getHelmetData(context)!;
214
- const firstSnapshot = {
215
- title: first.title.toString(),
216
- meta: first.meta.toString(),
217
- htmlAttributes: first.htmlAttributes.toString(),
218
- };
219
-
220
- // Simulate a replayed render against the same per-request context.
221
- renderWithContext(context, app);
222
- const second = getHelmetData(context)!;
223
-
224
- expect(second.title.toString()).toBe(firstSnapshot.title);
225
- expect(second.meta.toString()).toBe(firstSnapshot.meta);
226
- expect(second.htmlAttributes.toString()).toBe(firstSnapshot.htmlAttributes);
227
- expect(second.meta.toString().match(/description/g)).toHaveLength(1);
228
- });
229
- });