@anansi/core 0.20.44 → 0.21.0
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/CHANGELOG.md +11 -0
- package/dist/client.js +120 -2
- package/dist/server.js +72 -4
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -1
- package/lib/index.server.d.ts +3 -0
- package/lib/index.server.d.ts.map +1 -1
- package/lib/index.server.js +3 -1
- package/lib/scripts/index.server.js +3 -1
- package/lib/scripts/spouts/antd.js +19 -0
- package/lib/scripts/spouts/antd.server.js +4 -3
- package/lib/scripts/spouts/navigator.context.js +32 -0
- package/lib/scripts/spouts/navigator.js +24 -0
- package/lib/scripts/spouts/navigator.server.js +27 -0
- package/lib/scripts/spouts/prefetch.server.js +5 -1
- package/lib/spouts/antd.d.ts +3 -0
- package/lib/spouts/antd.d.ts.map +1 -0
- package/lib/spouts/antd.js +19 -0
- package/lib/spouts/antd.server.js +4 -3
- package/lib/spouts/navigator.context.d.ts +11 -0
- package/lib/spouts/navigator.context.d.ts.map +1 -0
- package/lib/spouts/navigator.context.js +33 -0
- package/lib/spouts/navigator.d.ts +5 -0
- package/lib/spouts/navigator.d.ts.map +1 -0
- package/lib/spouts/navigator.js +24 -0
- package/lib/spouts/navigator.server.d.ts +10 -0
- package/lib/spouts/navigator.server.d.ts.map +1 -0
- package/lib/spouts/navigator.server.js +27 -0
- package/lib/spouts/prefetch.server.d.ts.map +1 -1
- package/lib/spouts/prefetch.server.js +6 -1
- package/package.json +2 -2
- package/src/index.server.ts +3 -0
- package/src/index.ts +4 -0
- package/src/spouts/__tests__/navigator.test.tsx +103 -0
- package/src/spouts/antd.server.tsx +5 -5
- package/src/spouts/antd.tsx +15 -0
- package/src/spouts/navigator.context.tsx +42 -0
- package/src/spouts/navigator.server.tsx +40 -0
- package/src/spouts/navigator.tsx +35 -0
- package/src/spouts/prefetch.server.tsx +4 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { renderHook } from '@testing-library/react';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
NavigatorContext,
|
|
9
|
+
parseAcceptLanguage,
|
|
10
|
+
useNavigator,
|
|
11
|
+
type NavigatorProperties,
|
|
12
|
+
} from '../navigator.context';
|
|
13
|
+
|
|
14
|
+
describe('parseAcceptLanguage', () => {
|
|
15
|
+
it('should parse a simple language header', () => {
|
|
16
|
+
const result = parseAcceptLanguage('en-US');
|
|
17
|
+
expect(result.language).toBe('en-US');
|
|
18
|
+
expect(result.languages).toEqual(['en-US']);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should parse multiple languages without quality factors', () => {
|
|
22
|
+
const result = parseAcceptLanguage('en-US, fr, de');
|
|
23
|
+
expect(result.language).toBe('en-US');
|
|
24
|
+
expect(result.languages).toEqual(['en-US', 'fr', 'de']);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should sort languages by quality factor', () => {
|
|
28
|
+
const result = parseAcceptLanguage('en-US;q=0.7, fr;q=0.9, de;q=0.8');
|
|
29
|
+
expect(result.language).toBe('fr');
|
|
30
|
+
expect(result.languages).toEqual(['fr', 'de', 'en-US']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should handle mixed quality factors (some with, some without)', () => {
|
|
34
|
+
const result = parseAcceptLanguage('en-US, fr;q=0.9, de;q=0.8');
|
|
35
|
+
expect(result.language).toBe('en-US');
|
|
36
|
+
expect(result.languages).toEqual(['en-US', 'fr', 'de']);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should handle a realistic Accept-Language header', () => {
|
|
40
|
+
const result = parseAcceptLanguage('en-US,en;q=0.9,es;q=0.8,fr;q=0.7');
|
|
41
|
+
expect(result.language).toBe('en-US');
|
|
42
|
+
expect(result.languages).toEqual(['en-US', 'en', 'es', 'fr']);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return default "en" for undefined header', () => {
|
|
46
|
+
const result = parseAcceptLanguage(undefined);
|
|
47
|
+
expect(result.language).toBe('en');
|
|
48
|
+
expect(result.languages).toEqual(['en']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return default "en" for empty header', () => {
|
|
52
|
+
const result = parseAcceptLanguage('');
|
|
53
|
+
expect(result.language).toBe('en');
|
|
54
|
+
expect(result.languages).toEqual(['en']);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should handle whitespace in header', () => {
|
|
58
|
+
const result = parseAcceptLanguage(' en-US , fr ; q=0.5 ');
|
|
59
|
+
expect(result.language).toBe('en-US');
|
|
60
|
+
expect(result.languages).toEqual(['en-US', 'fr']);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle q=1 explicitly', () => {
|
|
64
|
+
const result = parseAcceptLanguage('en-US;q=1, fr;q=0.5');
|
|
65
|
+
expect(result.language).toBe('en-US');
|
|
66
|
+
expect(result.languages).toEqual(['en-US', 'fr']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should handle wildcard language', () => {
|
|
70
|
+
const result = parseAcceptLanguage('en-US, *;q=0.1');
|
|
71
|
+
expect(result.language).toBe('en-US');
|
|
72
|
+
expect(result.languages).toEqual(['en-US', '*']);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('useNavigator', () => {
|
|
77
|
+
it('should return navigator properties when used within context', () => {
|
|
78
|
+
const navigatorProps: NavigatorProperties = {
|
|
79
|
+
language: 'fr-FR',
|
|
80
|
+
languages: ['fr-FR', 'en-US'],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
84
|
+
<NavigatorContext value={navigatorProps}>{children}</NavigatorContext>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const { result } = renderHook(() => useNavigator(), { wrapper });
|
|
88
|
+
|
|
89
|
+
expect(result.current.language).toBe('fr-FR');
|
|
90
|
+
expect(result.current.languages).toEqual(['fr-FR', 'en-US']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should throw when used outside context', () => {
|
|
94
|
+
// Suppress console.error for this test since we expect an error
|
|
95
|
+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
96
|
+
|
|
97
|
+
expect(() => {
|
|
98
|
+
renderHook(() => useNavigator());
|
|
99
|
+
}).toThrow('useNavigator must be used within a NavigatorProvider');
|
|
100
|
+
|
|
101
|
+
consoleSpy.mockRestore();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -18,11 +18,6 @@ export default function antdSpout(): ServerSpout<
|
|
|
18
18
|
/* webpackIgnore: true */ '@ant-design/cssinjs'
|
|
19
19
|
);
|
|
20
20
|
const cache = createCache();
|
|
21
|
-
|
|
22
|
-
const nextProps = await next(props);
|
|
23
|
-
|
|
24
|
-
const scripts: React.ReactNode[] = nextProps.scripts ?? [];
|
|
25
|
-
|
|
26
21
|
const AntdSheets = (): JSX.Element => {
|
|
27
22
|
return (
|
|
28
23
|
<script
|
|
@@ -32,6 +27,11 @@ export default function antdSpout(): ServerSpout<
|
|
|
32
27
|
/>
|
|
33
28
|
);
|
|
34
29
|
};
|
|
30
|
+
|
|
31
|
+
const nextProps = await next(props);
|
|
32
|
+
|
|
33
|
+
const scripts: React.ReactNode[] = nextProps.scripts ?? [];
|
|
34
|
+
|
|
35
35
|
// unfortunately we have to inject this after the entire content has streamed in or it doesn't correctly populate
|
|
36
36
|
// see: https://github.com/ant-design/cssinjs/issues/79
|
|
37
37
|
scripts.push(<AntdSheets key="antd-sheets" />);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ClientSpout } from './types.js';
|
|
2
|
+
|
|
3
|
+
export default function antdSpout(): ClientSpout {
|
|
4
|
+
return next => async props => {
|
|
5
|
+
const { createCache, StyleProvider } = await import('@ant-design/cssinjs');
|
|
6
|
+
const cache = createCache();
|
|
7
|
+
|
|
8
|
+
const nextProps = await next(props);
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
...nextProps,
|
|
12
|
+
app: <StyleProvider cache={cache}>{nextProps.app}</StyleProvider>,
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface NavigatorProperties {
|
|
4
|
+
language: string;
|
|
5
|
+
languages: readonly string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const NavigatorContext = createContext<NavigatorProperties | undefined>(
|
|
9
|
+
undefined,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export function useNavigator(): NavigatorProperties {
|
|
13
|
+
const context = useContext(NavigatorContext);
|
|
14
|
+
if (context === undefined) {
|
|
15
|
+
throw new Error('useNavigator must be used within a NavigatorProvider');
|
|
16
|
+
}
|
|
17
|
+
return context;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function parseAcceptLanguage(header: string | undefined): {
|
|
21
|
+
language: string;
|
|
22
|
+
languages: readonly string[];
|
|
23
|
+
} {
|
|
24
|
+
if (!header) {
|
|
25
|
+
return { language: 'en', languages: ['en'] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parsed = header
|
|
29
|
+
.split(',')
|
|
30
|
+
.map(part => {
|
|
31
|
+
const [lang, qPart] = part.trim().split(';');
|
|
32
|
+
const q = qPart ? parseFloat(qPart.replace('q=', '')) : 1;
|
|
33
|
+
return { lang: lang.trim(), q };
|
|
34
|
+
})
|
|
35
|
+
.sort((a, b) => b.q - a.q)
|
|
36
|
+
.map(({ lang }) => lang);
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
language: parsed[0] ?? 'en',
|
|
40
|
+
languages: parsed.length > 0 ? parsed : ['en'],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { NavigatorContext, parseAcceptLanguage } from './navigator.context.js';
|
|
4
|
+
import type { ServerSpout } from './types.js';
|
|
5
|
+
|
|
6
|
+
type NeededNext = {
|
|
7
|
+
initData?: Record<string, () => unknown>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function navigatorSpout(): ServerSpout<
|
|
11
|
+
Record<string, unknown>,
|
|
12
|
+
{ language: string; languages: readonly string[] },
|
|
13
|
+
NeededNext
|
|
14
|
+
> {
|
|
15
|
+
return next => async props => {
|
|
16
|
+
const acceptLanguage = props.req.headers['accept-language'];
|
|
17
|
+
const header =
|
|
18
|
+
typeof acceptLanguage === 'string' ? acceptLanguage : acceptLanguage?.[0];
|
|
19
|
+
const navigatorProps = parseAcceptLanguage(header);
|
|
20
|
+
|
|
21
|
+
const nextProps = await next({
|
|
22
|
+
...props,
|
|
23
|
+
...navigatorProps,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...nextProps,
|
|
28
|
+
...navigatorProps,
|
|
29
|
+
initData: {
|
|
30
|
+
...nextProps.initData,
|
|
31
|
+
navigator: () => navigatorProps,
|
|
32
|
+
},
|
|
33
|
+
app: (
|
|
34
|
+
<NavigatorContext value={navigatorProps}>
|
|
35
|
+
{nextProps.app}
|
|
36
|
+
</NavigatorContext>
|
|
37
|
+
),
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { NavigatorContext } from './navigator.context.js';
|
|
4
|
+
import type { NavigatorProperties } from './navigator.context.js';
|
|
5
|
+
import type { ClientSpout } from './types.js';
|
|
6
|
+
|
|
7
|
+
export default function navigatorSpout(): ClientSpout<{
|
|
8
|
+
getInitialData: (key: string) => Promise<any>;
|
|
9
|
+
}> {
|
|
10
|
+
return next => async props => {
|
|
11
|
+
const nextProps = await next(props);
|
|
12
|
+
const navigatorProps: NavigatorProperties = await props
|
|
13
|
+
.getInitialData('navigator')
|
|
14
|
+
.catch(e => {
|
|
15
|
+
console.warn(
|
|
16
|
+
'Navigator initial data could not load, using client navigator. Error:',
|
|
17
|
+
e,
|
|
18
|
+
);
|
|
19
|
+
return {
|
|
20
|
+
language: navigator.language,
|
|
21
|
+
languages: [...navigator.languages],
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
...nextProps,
|
|
27
|
+
navigator: navigatorProps,
|
|
28
|
+
app: (
|
|
29
|
+
<NavigatorContext value={navigatorProps}>
|
|
30
|
+
{nextProps.app}
|
|
31
|
+
</NavigatorContext>
|
|
32
|
+
),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -25,6 +25,10 @@ export default function prefetchSpout<F extends string>(field: F) {
|
|
|
25
25
|
try {
|
|
26
26
|
const toFetch: Promise<unknown>[] = [];
|
|
27
27
|
nextProps.matchedRoutes.forEach(route => {
|
|
28
|
+
// Preload lazy component so it's ready for SSR render
|
|
29
|
+
if (typeof route.component?.preload === 'function') {
|
|
30
|
+
toFetch.push(route.component.preload());
|
|
31
|
+
}
|
|
28
32
|
if (typeof route.resolveData === 'function') {
|
|
29
33
|
toFetch.push(
|
|
30
34
|
route.resolveData(
|