@anansi/core 0.8.0 → 0.11.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 +62 -0
- package/dist/client.js +31 -22
- package/dist/client.js.map +1 -1
- package/dist/server.js +205 -30
- package/dist/server.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +6 -2
- package/lib/index.server.d.ts +1 -0
- package/lib/index.server.d.ts.map +1 -1
- package/lib/index.server.js +6 -2
- package/lib/laySpouts.d.ts.map +1 -1
- package/lib/laySpouts.js +11 -2
- package/lib/scripts/getProxyMiddlewares.d.ts +3 -0
- package/lib/scripts/getProxyMiddlewares.d.ts.map +1 -0
- package/lib/scripts/getProxyMiddlewares.js +106 -0
- package/lib/scripts/serve.d.ts +4 -1
- package/lib/scripts/serve.d.ts.map +1 -1
- package/lib/scripts/serve.js +51 -25
- package/lib/scripts/startDevserver.d.ts.map +1 -1
- package/lib/scripts/startDevserver.js +19 -29
- package/lib/spouts/DocumentComponent.d.ts +6 -1
- package/lib/spouts/DocumentComponent.d.ts.map +1 -1
- package/lib/spouts/DocumentComponent.js +32 -9
- package/lib/spouts/csp.d.ts +5 -0
- package/lib/spouts/csp.d.ts.map +1 -0
- package/lib/spouts/csp.js +20 -0
- package/lib/spouts/document.d.ts +1 -1
- package/lib/spouts/document.d.ts.map +1 -1
- package/lib/spouts/document.js +3 -3
- package/lib/spouts/document.server.d.ts +5 -3
- package/lib/spouts/document.server.d.ts.map +1 -1
- package/lib/spouts/document.server.js +5 -2
- package/lib/spouts/json.d.ts +5 -0
- package/lib/spouts/json.d.ts.map +1 -0
- package/lib/spouts/json.js +22 -0
- package/lib/spouts/json.server.d.ts +13 -0
- package/lib/spouts/json.server.d.ts.map +1 -0
- package/lib/spouts/json.server.js +74 -0
- package/lib/spouts/restHooks.d.ts +1 -1
- package/lib/spouts/restHooks.d.ts.map +1 -1
- package/lib/spouts/restHooks.js +5 -9
- package/lib/spouts/restHooks.server.d.ts +41 -1
- package/lib/spouts/restHooks.server.d.ts.map +1 -1
- package/lib/spouts/restHooks.server.js +6 -3
- package/lib/spouts/rhHelp.d.ts +40 -0
- package/lib/spouts/rhHelp.d.ts.map +1 -0
- package/lib/spouts/rhHelp.js +47 -0
- package/lib/spouts/router.d.ts +3 -1
- package/lib/spouts/router.d.ts.map +1 -1
- package/lib/spouts/router.js +5 -4
- package/lib/spouts/types.d.ts +1 -0
- package/lib/spouts/types.d.ts.map +1 -1
- package/lib/spouts/types.js +1 -1
- package/package.json +10 -8
- package/src/index.server.ts +1 -0
- package/src/index.ts +1 -0
- package/src/laySpouts.tsx +5 -1
- package/src/scripts/getProxyMiddlewares.ts +129 -0
- package/src/scripts/serve.ts +66 -23
- package/src/scripts/startDevserver.ts +16 -28
- package/src/spouts/DocumentComponent.tsx +29 -6
- package/src/spouts/csp.ts +25 -0
- package/src/spouts/document.server.tsx +8 -3
- package/src/spouts/document.tsx +5 -3
- package/src/spouts/json.server.tsx +77 -0
- package/src/spouts/json.tsx +25 -0
- package/src/spouts/restHooks.server.tsx +7 -3
- package/src/spouts/restHooks.tsx +12 -7
- package/src/spouts/rhHelp.tsx +37 -0
- package/src/spouts/router.tsx +12 -4
- package/src/spouts/types.ts +1 -0
package/src/scripts/serve.ts
CHANGED
|
@@ -11,6 +11,7 @@ import compress from 'compression';
|
|
|
11
11
|
|
|
12
12
|
import 'cross-fetch/polyfill';
|
|
13
13
|
import { Render } from './types';
|
|
14
|
+
import getProxyMiddlewares from './getProxyMiddlewares';
|
|
14
15
|
|
|
15
16
|
// run directly from node
|
|
16
17
|
if (require.main === module) {
|
|
@@ -23,12 +24,26 @@ if (require.main === module) {
|
|
|
23
24
|
serve(entrypoint);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export default function serve(
|
|
27
|
+
export default function serve(
|
|
28
|
+
entrypoint: string,
|
|
29
|
+
options: { serveAssets?: boolean; serveProxy?: boolean } = {},
|
|
30
|
+
) {
|
|
27
31
|
const PORT = process.env.PORT || 8080;
|
|
28
32
|
|
|
29
33
|
const loader = ora('Initializing').start();
|
|
30
34
|
|
|
31
|
-
const
|
|
35
|
+
const webpackConfig: (
|
|
36
|
+
env: any,
|
|
37
|
+
argv: any,
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
39
|
+
) => webpack.Configuration = require(require.resolve(
|
|
40
|
+
// TODO: use normal resolution algorithm to find webpack file
|
|
41
|
+
path.join(process.cwd(), 'webpack.config'),
|
|
42
|
+
));
|
|
43
|
+
|
|
44
|
+
const manifestPath = getManifestPathFromWebpackconfig(
|
|
45
|
+
webpackConfig({}, { mode: 'production' }),
|
|
46
|
+
);
|
|
32
47
|
|
|
33
48
|
const readFile = promisify(diskFs.readFile);
|
|
34
49
|
let server: Server | undefined;
|
|
@@ -67,23 +82,53 @@ export default function serve(entrypoint: string) {
|
|
|
67
82
|
wrappingApp.use(compress());
|
|
68
83
|
|
|
69
84
|
// ASSETS
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
85
|
+
if (options.serveAssets) {
|
|
86
|
+
wrappingApp.use(
|
|
87
|
+
async (
|
|
88
|
+
req: Request | IncomingMessage,
|
|
89
|
+
res: any,
|
|
90
|
+
next: NextFunction,
|
|
91
|
+
) => {
|
|
92
|
+
const filename =
|
|
93
|
+
req.url?.substr(
|
|
94
|
+
(process.env.WEBPACK_PUBLIC_PATH as string).length,
|
|
95
|
+
) ?? '';
|
|
96
|
+
const assetPath = path.join(
|
|
97
|
+
clientManifest.outputPath ?? '',
|
|
98
|
+
filename,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
diskFs.existsSync(assetPath) &&
|
|
103
|
+
!diskFs.lstatSync(assetPath).isDirectory()
|
|
104
|
+
) {
|
|
105
|
+
try {
|
|
106
|
+
const fileContent = (await readFile(assetPath)).toString();
|
|
107
|
+
res.contentType(filename);
|
|
108
|
+
res.send(fileContent);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
return next();
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
next();
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
}
|
|
75
118
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
119
|
+
// PROXIES
|
|
120
|
+
if (options.serveProxy) {
|
|
121
|
+
const devConfig: webpack.Configuration = webpackConfig(
|
|
122
|
+
{},
|
|
123
|
+
{ mode: 'development' },
|
|
124
|
+
);
|
|
125
|
+
if (devConfig.devServer?.proxy) {
|
|
126
|
+
const middlewares = getProxyMiddlewares(devConfig.devServer?.proxy);
|
|
127
|
+
if (middlewares) {
|
|
128
|
+
wrappingApp.use(...middlewares.map(({ middleware }) => middleware));
|
|
129
|
+
}
|
|
84
130
|
}
|
|
85
|
-
}
|
|
86
|
-
wrappingApp.get(`${process.env.WEBPACK_PUBLIC_PATH}*`, assetRoute);
|
|
131
|
+
}
|
|
87
132
|
|
|
88
133
|
// SERVER SIDE RENDERING
|
|
89
134
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
@@ -91,6 +136,7 @@ export default function serve(entrypoint: string) {
|
|
|
91
136
|
process.cwd(),
|
|
92
137
|
entrypoint,
|
|
93
138
|
)).default;
|
|
139
|
+
|
|
94
140
|
wrappingApp.get(
|
|
95
141
|
'/*',
|
|
96
142
|
handleErrors(async function (req: any, res: any) {
|
|
@@ -147,12 +193,9 @@ export default function serve(entrypoint: string) {
|
|
|
147
193
|
});
|
|
148
194
|
}
|
|
149
195
|
|
|
150
|
-
function getManifestPathFromWebpackconfig(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// TODO: use normal resolution algorithm to find webpack file
|
|
154
|
-
path.join(process.cwd(), 'webpack.config'),
|
|
155
|
-
))({}, { mode: 'production' });
|
|
196
|
+
function getManifestPathFromWebpackconfig(
|
|
197
|
+
webpackConfig: webpack.Configuration,
|
|
198
|
+
) {
|
|
156
199
|
const manifestFilename: string =
|
|
157
200
|
(
|
|
158
201
|
webpackConfig?.plugins?.find(plugin => {
|
|
@@ -7,13 +7,12 @@ import webpack, { MultiCompiler } from 'webpack';
|
|
|
7
7
|
import { createFsFromVolume, Volume } from 'memfs';
|
|
8
8
|
import { Server, IncomingMessage, ServerResponse } from 'http';
|
|
9
9
|
import type { NextFunction } from 'express';
|
|
10
|
-
import { patchRequire } from 'fs-monkey';
|
|
11
10
|
import tmp from 'tmp';
|
|
12
11
|
import sourceMapSupport from 'source-map-support';
|
|
13
12
|
import { ufs } from 'unionfs';
|
|
14
13
|
import WebpackDevServer from 'webpack-dev-server';
|
|
15
|
-
import importFresh from 'import-fresh';
|
|
16
14
|
import logging from 'webpack/lib/logging/runtime';
|
|
15
|
+
import { createFsRequire } from 'fs-require';
|
|
17
16
|
|
|
18
17
|
import 'cross-fetch/polyfill';
|
|
19
18
|
import { BoundRender } from './types';
|
|
@@ -21,8 +20,6 @@ import { BoundRender } from './types';
|
|
|
21
20
|
// run directly from node
|
|
22
21
|
if (require.main === module) {
|
|
23
22
|
const entrypoint = process.argv[2];
|
|
24
|
-
//process.env.WEBPACK_PUBLIC_HOST = `http://localhost:${PORT}`; this breaks compatibility with stackblitz
|
|
25
|
-
process.env.WEBPACK_PUBLIC_PATH = '/assets/';
|
|
26
23
|
|
|
27
24
|
if (!entrypoint) {
|
|
28
25
|
console.log(`Usage: start-anansi <entrypoint-file>`);
|
|
@@ -49,7 +46,7 @@ export default function startDevServer(
|
|
|
49
46
|
const fs = createFsFromVolume(volume);
|
|
50
47
|
ufs.use(diskFs).use(fs as any);
|
|
51
48
|
|
|
52
|
-
|
|
49
|
+
const fsRequire = createFsRequire(ufs);
|
|
53
50
|
const readFile = promisify(ufs.readFile);
|
|
54
51
|
let server: Server | undefined;
|
|
55
52
|
|
|
@@ -92,10 +89,7 @@ export default function startDevServer(
|
|
|
92
89
|
{ mode: 'development', target: 'node' },
|
|
93
90
|
),
|
|
94
91
|
] as const;
|
|
95
|
-
|
|
96
|
-
webpackConfigs[1].plugins.push(
|
|
97
|
-
new webpack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
|
|
98
|
-
);
|
|
92
|
+
|
|
99
93
|
// initialize the webpack compiler
|
|
100
94
|
const compiler: MultiCompiler = webpack(webpackConfigs);
|
|
101
95
|
|
|
@@ -124,12 +118,12 @@ export default function startDevServer(
|
|
|
124
118
|
};
|
|
125
119
|
}
|
|
126
120
|
|
|
127
|
-
|
|
121
|
+
let initRender:
|
|
128
122
|
| { args: Parameters<BoundRender>; resolve: () => void }[]
|
|
129
123
|
| undefined = [];
|
|
130
124
|
let render: BoundRender = (...args) =>
|
|
131
125
|
new Promise(resolve => {
|
|
132
|
-
initRender
|
|
126
|
+
initRender?.push({ args, resolve });
|
|
133
127
|
});
|
|
134
128
|
|
|
135
129
|
function importRender(stats: webpack.Stats[]) {
|
|
@@ -149,19 +143,20 @@ export default function startDevServer(
|
|
|
149
143
|
// ASSETS
|
|
150
144
|
const clientManifest = clientStats.toJson();
|
|
151
145
|
|
|
146
|
+
const serverEntry = getServerBundle(serverStats);
|
|
147
|
+
// reload modules
|
|
148
|
+
Object.keys(fsRequire.cache).forEach(key => {
|
|
149
|
+
delete fsRequire.cache[key];
|
|
150
|
+
});
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
152
|
+
render = (fsRequire(serverEntry) as any).default.bind(
|
|
153
|
+
undefined,
|
|
154
|
+
clientManifest,
|
|
155
|
+
);
|
|
152
156
|
// SERVER SIDE ENTRYPOINT
|
|
153
157
|
if (Array.isArray(initRender)) {
|
|
154
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
155
|
-
render = (require(getServerBundle(serverStats)) as any).default.bind(
|
|
156
|
-
undefined,
|
|
157
|
-
clientManifest,
|
|
158
|
-
);
|
|
159
158
|
initRender.forEach(init => render(...init.args).then(init.resolve));
|
|
160
|
-
|
|
161
|
-
render = (importFresh(getServerBundle(serverStats)) as any).default.bind(
|
|
162
|
-
undefined,
|
|
163
|
-
clientManifest,
|
|
164
|
-
);
|
|
159
|
+
initRender = undefined;
|
|
165
160
|
}
|
|
166
161
|
}
|
|
167
162
|
|
|
@@ -169,13 +164,6 @@ export default function startDevServer(
|
|
|
169
164
|
// write to memory filesystem so we can import
|
|
170
165
|
{
|
|
171
166
|
...webpackConfigs[0].devServer,
|
|
172
|
-
/*client: {
|
|
173
|
-
...webpackConfigs[0].devServer?.client,
|
|
174
|
-
webSocketURL: {
|
|
175
|
-
...webpackConfigs[0].devServer?.client.webSocketURL,
|
|
176
|
-
port: 8080,
|
|
177
|
-
},
|
|
178
|
-
},*/
|
|
179
167
|
devMiddleware: {
|
|
180
168
|
...webpackConfigs[0]?.devServer?.devMiddleware,
|
|
181
169
|
outputFileSystem: {
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import type { Policy } from './csp';
|
|
2
|
+
import { buildPolicy } from './csp';
|
|
3
|
+
|
|
1
4
|
type Props = {
|
|
2
5
|
children: React.ReactNode;
|
|
3
6
|
assets: { href: string; as?: string; rel?: string }[];
|
|
4
7
|
head: React.ReactNode;
|
|
8
|
+
scripts: React.ReactNode;
|
|
5
9
|
title: string;
|
|
6
10
|
rootId: string;
|
|
7
11
|
charSet: string;
|
|
12
|
+
csPolicy?: Policy;
|
|
13
|
+
nonce?: string | undefined;
|
|
8
14
|
};
|
|
9
15
|
|
|
10
16
|
export default function Document({
|
|
@@ -14,11 +20,32 @@ export default function Document({
|
|
|
14
20
|
title,
|
|
15
21
|
rootId,
|
|
16
22
|
charSet,
|
|
23
|
+
csPolicy,
|
|
24
|
+
nonce,
|
|
25
|
+
scripts,
|
|
17
26
|
}: Props) {
|
|
27
|
+
let cspMeta: null | React.ReactNode = null;
|
|
28
|
+
if (csPolicy) {
|
|
29
|
+
// add nonce to policy
|
|
30
|
+
const policy = {
|
|
31
|
+
...csPolicy,
|
|
32
|
+
};
|
|
33
|
+
if (nonce) {
|
|
34
|
+
if (typeof policy['script-src'] === 'string') {
|
|
35
|
+
policy['script-src'] = [policy['script-src'], `'nonce-${nonce}'`];
|
|
36
|
+
} else {
|
|
37
|
+
policy['script-src'] = [...policy['script-src'], `'nonce-${nonce}'`];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
cspMeta = (
|
|
41
|
+
<meta httpEquiv="Content-Security-Policy" content={buildPolicy(policy)} />
|
|
42
|
+
);
|
|
43
|
+
}
|
|
18
44
|
return (
|
|
19
45
|
<html>
|
|
20
46
|
<head>
|
|
21
47
|
<meta charSet={charSet} />
|
|
48
|
+
{cspMeta}
|
|
22
49
|
{head}
|
|
23
50
|
{assets.map((asset, i) => (
|
|
24
51
|
<link key={i} rel="preload" {...asset} />
|
|
@@ -27,12 +54,7 @@ export default function Document({
|
|
|
27
54
|
</head>
|
|
28
55
|
<body>
|
|
29
56
|
<div id={rootId}>{children}</div>
|
|
30
|
-
{
|
|
31
|
-
<script
|
|
32
|
-
dangerouslySetInnerHTML={{
|
|
33
|
-
__html: `assetManifest = ${JSON.stringify(assets)};`,
|
|
34
|
-
}}
|
|
35
|
-
/>
|
|
57
|
+
{scripts}
|
|
36
58
|
{assets
|
|
37
59
|
.filter(({ href }) => href.endsWith('.js'))
|
|
38
60
|
.map(({ href }, i) => (
|
|
@@ -54,4 +76,5 @@ Document.defaultProps = {
|
|
|
54
76
|
),
|
|
55
77
|
charSet: 'utf-8',
|
|
56
78
|
rootId: 'anansi-root',
|
|
79
|
+
scripts: null,
|
|
57
80
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface Policy {
|
|
2
|
+
[directive: string]: string | string[];
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
// TODO: memoize this
|
|
6
|
+
export function buildPolicy(policyObj: Policy) {
|
|
7
|
+
return Object.keys(policyObj)
|
|
8
|
+
.map(key => {
|
|
9
|
+
const val = Array.isArray(policyObj[key])
|
|
10
|
+
? [...new Set(policyObj[key]).values()].filter(v => v).join(' ')
|
|
11
|
+
: policyObj[key];
|
|
12
|
+
|
|
13
|
+
// move strict dynamic to the end of the policy if it exists to be backwards compatible with csp2
|
|
14
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src#strict-dynamic
|
|
15
|
+
if (typeof val === 'string' && val.includes("'strict-dynamic'")) {
|
|
16
|
+
const newVal = `${val
|
|
17
|
+
.replace(/\s?'strict-dynamic'\s?/gi, ' ')
|
|
18
|
+
.trim()} 'strict-dynamic'`;
|
|
19
|
+
return `${key} ${newVal}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `${key} ${val}`;
|
|
23
|
+
})
|
|
24
|
+
.join('; ');
|
|
25
|
+
}
|
|
@@ -3,19 +3,21 @@ import type { Route } from '@anansi/router';
|
|
|
3
3
|
import { StatsChunkGroup } from 'webpack';
|
|
4
4
|
|
|
5
5
|
import type { ServerProps, ResolveProps } from './types';
|
|
6
|
+
import type { Policy } from './csp';
|
|
6
7
|
import Document from './DocumentComponent';
|
|
7
8
|
|
|
8
9
|
type NeededProps = {
|
|
9
10
|
matchedRoutes: Route<any>[];
|
|
10
11
|
title?: string;
|
|
11
|
-
|
|
12
|
+
scripts?: React.ReactNode[];
|
|
12
13
|
} & ResolveProps;
|
|
13
14
|
|
|
14
15
|
export default function DocumentSpout(options: {
|
|
15
16
|
head?: React.ReactNode;
|
|
16
17
|
title: string;
|
|
17
|
-
rootId
|
|
18
|
-
charSet
|
|
18
|
+
rootId?: string;
|
|
19
|
+
charSet?: string;
|
|
20
|
+
csPolicy?: Policy;
|
|
19
21
|
}) {
|
|
20
22
|
return function <T extends NeededProps>(
|
|
21
23
|
next: (props: ServerProps) => Promise<T>,
|
|
@@ -79,6 +81,9 @@ export default function DocumentSpout(options: {
|
|
|
79
81
|
title={nextProps.title ?? options.title}
|
|
80
82
|
assets={assets}
|
|
81
83
|
rootId={options.rootId}
|
|
84
|
+
nonce={props.nonce}
|
|
85
|
+
csPolicy={options.csPolicy}
|
|
86
|
+
scripts={nextProps.scripts}
|
|
82
87
|
>
|
|
83
88
|
{nextProps.app}
|
|
84
89
|
</Document>
|
package/src/spouts/document.tsx
CHANGED
|
@@ -12,9 +12,11 @@ export default function documentSpout(options: {
|
|
|
12
12
|
head?: React.ReactNode;
|
|
13
13
|
title: string;
|
|
14
14
|
}) {
|
|
15
|
-
return function <T extends NeededProps>(
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
return function <T extends NeededProps>(
|
|
16
|
+
next: (initData: Record<string, unknown>) => Promise<T>,
|
|
17
|
+
) {
|
|
18
|
+
return async (initData: Record<string, unknown>) => {
|
|
19
|
+
const nextProps = await next(initData);
|
|
18
20
|
|
|
19
21
|
return nextProps;
|
|
20
22
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Route } from '@anansi/router';
|
|
3
|
+
import { StatsChunkGroup } from 'webpack';
|
|
4
|
+
|
|
5
|
+
import type { ServerProps, ResolveProps } from './types';
|
|
6
|
+
import type { Policy } from './csp';
|
|
7
|
+
import Document from './DocumentComponent';
|
|
8
|
+
|
|
9
|
+
type NeededProps = {
|
|
10
|
+
initData?: Record<string, () => unknown>;
|
|
11
|
+
scripts?: React.ReactNode[];
|
|
12
|
+
} & ResolveProps;
|
|
13
|
+
|
|
14
|
+
export default function JSONSpout({
|
|
15
|
+
id = 'anansi-json',
|
|
16
|
+
}: { id?: string } = {}) {
|
|
17
|
+
return function <T extends NeededProps>(
|
|
18
|
+
next: (props: ServerProps) => Promise<T>,
|
|
19
|
+
) {
|
|
20
|
+
return async (props: ServerProps) => {
|
|
21
|
+
const nextProps = await next(props);
|
|
22
|
+
|
|
23
|
+
const scripts: React.ReactNode[] = nextProps.scripts ?? [];
|
|
24
|
+
/*
|
|
25
|
+
Object.entries(nextProps.initData ?? {}).forEach(([key, data]) => {
|
|
26
|
+
try {
|
|
27
|
+
const encoded = JSON.stringify(data);
|
|
28
|
+
scripts.push(
|
|
29
|
+
<script
|
|
30
|
+
key={key}
|
|
31
|
+
id={`${id}-${key}`}
|
|
32
|
+
type="application/json"
|
|
33
|
+
dangerouslySetInnerHTML={{
|
|
34
|
+
__html: encoded,
|
|
35
|
+
}}
|
|
36
|
+
nonce={props.nonce}
|
|
37
|
+
/>,
|
|
38
|
+
);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
// TODO: Use unified logging
|
|
41
|
+
console.error(e);
|
|
42
|
+
}
|
|
43
|
+
});*/
|
|
44
|
+
const Script = () => {
|
|
45
|
+
try {
|
|
46
|
+
const data: any = {};
|
|
47
|
+
Object.entries(nextProps.initData ?? {}).forEach(([key, getData]) => {
|
|
48
|
+
data[key] = getData();
|
|
49
|
+
});
|
|
50
|
+
const encoded = JSON.stringify(data);
|
|
51
|
+
return (
|
|
52
|
+
<script
|
|
53
|
+
key={id}
|
|
54
|
+
id={id}
|
|
55
|
+
type="application/json"
|
|
56
|
+
dangerouslySetInnerHTML={{
|
|
57
|
+
__html: encoded,
|
|
58
|
+
}}
|
|
59
|
+
nonce={props.nonce}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
// TODO: Use unified logging
|
|
64
|
+
console.error('Error serializing json');
|
|
65
|
+
console.error(e);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
scripts.push(<Script />);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...nextProps,
|
|
73
|
+
scripts,
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Route } from '@anansi/router';
|
|
3
|
+
|
|
4
|
+
import type { ResolveProps } from './types';
|
|
5
|
+
|
|
6
|
+
type NeededProps = ResolveProps;
|
|
7
|
+
|
|
8
|
+
export default function JSONSpout({
|
|
9
|
+
id = 'anansi-json',
|
|
10
|
+
}: { id?: string } = {}) {
|
|
11
|
+
return function <T extends NeededProps>(
|
|
12
|
+
next: (initData: Record<string, unknown>) => Promise<T>,
|
|
13
|
+
) {
|
|
14
|
+
return async () => {
|
|
15
|
+
const initData = getDatafromDOM(id);
|
|
16
|
+
const nextProps = await next(initData);
|
|
17
|
+
|
|
18
|
+
return nextProps;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function getDatafromDOM(id: string): Record<string, unknown> {
|
|
23
|
+
const element = document.querySelector(`#${id}`);
|
|
24
|
+
return element?.innerHTML ? JSON.parse(element?.innerHTML) : undefined;
|
|
25
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Manager, NetworkManager } from '@rest-hooks/core';
|
|
2
|
-
import { createPersistedStore } from '@rest-hooks/ssr';
|
|
3
2
|
|
|
3
|
+
import { createPersistedStore } from './rhHelp';
|
|
4
4
|
import type { ResolveProps, ServerProps } from './types';
|
|
5
5
|
|
|
6
|
-
type NeededProps = ResolveProps;
|
|
6
|
+
type NeededProps = { initData?: Record<string, () => unknown> } & ResolveProps;
|
|
7
7
|
|
|
8
8
|
export default function restHooksSpout(
|
|
9
9
|
options: {
|
|
@@ -14,7 +14,7 @@ export default function restHooksSpout(
|
|
|
14
14
|
next: (props: ServerProps) => Promise<T>,
|
|
15
15
|
) {
|
|
16
16
|
return async (props: ServerProps) => {
|
|
17
|
-
const [ServerCacheProvider, controller] = createPersistedStore(
|
|
17
|
+
const [ServerCacheProvider, controller, store] = createPersistedStore(
|
|
18
18
|
options.getManagers(),
|
|
19
19
|
);
|
|
20
20
|
|
|
@@ -23,6 +23,10 @@ export default function restHooksSpout(
|
|
|
23
23
|
return {
|
|
24
24
|
...nextProps,
|
|
25
25
|
controller,
|
|
26
|
+
initData: {
|
|
27
|
+
...nextProps.initData,
|
|
28
|
+
resthooks: () => store.getState(),
|
|
29
|
+
},
|
|
26
30
|
app: <ServerCacheProvider>{nextProps.app}</ServerCacheProvider>,
|
|
27
31
|
};
|
|
28
32
|
};
|
package/src/spouts/restHooks.tsx
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
CacheProvider,
|
|
3
|
+
Manager,
|
|
4
|
+
NetworkManager,
|
|
5
|
+
State,
|
|
6
|
+
} from '@rest-hooks/core';
|
|
3
7
|
|
|
4
8
|
import type { ResolveProps } from './types';
|
|
5
9
|
|
|
@@ -10,18 +14,19 @@ export default function restHooksSpout(
|
|
|
10
14
|
getManagers: () => Manager[];
|
|
11
15
|
} = { getManagers: () => [new NetworkManager()] },
|
|
12
16
|
) {
|
|
13
|
-
return function <T extends NeededProps>(
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
return function <T extends NeededProps>(
|
|
18
|
+
next: (initData: Record<string, unknown>) => Promise<T>,
|
|
19
|
+
) {
|
|
20
|
+
return async (initData: Record<string, unknown>) => {
|
|
21
|
+
const data = initData.resthooks as State<unknown>;
|
|
16
22
|
|
|
17
|
-
const nextProps = await next();
|
|
23
|
+
const nextProps = await next(initData);
|
|
18
24
|
|
|
19
25
|
return {
|
|
20
26
|
...nextProps,
|
|
21
27
|
app: (
|
|
22
28
|
<CacheProvider initialState={data} managers={options.getManagers()}>
|
|
23
29
|
{nextProps.app}
|
|
24
|
-
<ServerDataComponent data={data} />
|
|
25
30
|
</CacheProvider>
|
|
26
31
|
),
|
|
27
32
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ExternalCacheProvider, PromiseifyMiddleware } from 'rest-hooks';
|
|
2
|
+
import {
|
|
3
|
+
Controller,
|
|
4
|
+
createReducer,
|
|
5
|
+
initialState,
|
|
6
|
+
Manager,
|
|
7
|
+
applyManager,
|
|
8
|
+
NetworkManager,
|
|
9
|
+
} from '@rest-hooks/core';
|
|
10
|
+
import { createStore, applyMiddleware } from 'redux';
|
|
11
|
+
|
|
12
|
+
// TODO: Rework this and upstream to rest hooks
|
|
13
|
+
export function createPersistedStore(managers?: Manager[]) {
|
|
14
|
+
const controller = new Controller();
|
|
15
|
+
managers = managers ?? [new NetworkManager()];
|
|
16
|
+
const reducer = createReducer(controller);
|
|
17
|
+
const enhancer = applyMiddleware(
|
|
18
|
+
...applyManager(managers, controller),
|
|
19
|
+
PromiseifyMiddleware as any,
|
|
20
|
+
);
|
|
21
|
+
const store = createStore(reducer, initialState as any, enhancer);
|
|
22
|
+
managers.forEach(manager => manager.init?.(store.getState()));
|
|
23
|
+
|
|
24
|
+
const selector = (state: any) => state;
|
|
25
|
+
function ServerCacheProvider({ children }: { children: React.ReactNode }) {
|
|
26
|
+
return (
|
|
27
|
+
<ExternalCacheProvider
|
|
28
|
+
store={store}
|
|
29
|
+
selector={selector}
|
|
30
|
+
controller={controller}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</ExternalCacheProvider>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return [ServerCacheProvider, controller, store] as const;
|
|
37
|
+
}
|
package/src/spouts/router.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Route, RouteProvider, RouteController } from '@anansi/router';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { createBrowserHistory } from 'history';
|
|
4
|
+
import type { Update } from 'history';
|
|
4
5
|
|
|
5
6
|
import type { ResolveProps, CreateRouter } from './types';
|
|
6
7
|
|
|
@@ -10,6 +11,7 @@ export default function routerSpout<ResolveWith>(options: {
|
|
|
10
11
|
resolveWith?: any;
|
|
11
12
|
useResolveWith: () => ResolveWith;
|
|
12
13
|
createRouter: CreateRouter<ResolveWith>;
|
|
14
|
+
onChange?: (update: Update, callback: () => void | undefined) => void;
|
|
13
15
|
}) {
|
|
14
16
|
const createRouteComponent = (
|
|
15
17
|
router: RouteController<Route<ResolveWith, any>>,
|
|
@@ -18,19 +20,25 @@ export default function routerSpout<ResolveWith>(options: {
|
|
|
18
20
|
const resolveWith = options.useResolveWith();
|
|
19
21
|
|
|
20
22
|
return (
|
|
21
|
-
<RouteProvider
|
|
23
|
+
<RouteProvider
|
|
24
|
+
router={router}
|
|
25
|
+
resolveWith={resolveWith}
|
|
26
|
+
onChange={options.onChange}
|
|
27
|
+
>
|
|
22
28
|
{children}
|
|
23
29
|
</RouteProvider>
|
|
24
30
|
);
|
|
25
31
|
};
|
|
26
32
|
|
|
27
|
-
return function <T extends NeededProps>(
|
|
28
|
-
|
|
33
|
+
return function <T extends NeededProps>(
|
|
34
|
+
next: (initData: Record<string, unknown>) => Promise<T>,
|
|
35
|
+
) {
|
|
36
|
+
return async (initData: Record<string, unknown>) => {
|
|
29
37
|
const history = createBrowserHistory();
|
|
30
38
|
const router = options.createRouter(history);
|
|
31
39
|
const matchedRoutes = router.getMatchedRoutes(history.location.pathname);
|
|
32
40
|
|
|
33
|
-
const nextProps = await next();
|
|
41
|
+
const nextProps = await next(initData);
|
|
34
42
|
|
|
35
43
|
const Router = createRouteComponent(router);
|
|
36
44
|
return {
|