@jk2908/solas 0.15.1 → 0.16.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 +15 -0
- package/README.md +46 -13
- package/dist/adapters/cloudflare/vite.d.ts +1 -0
- package/dist/adapters/cloudflare/vite.js +11 -14
- package/dist/adapters/static.js +32 -0
- package/dist/config.js +32 -3
- package/dist/index.js +8 -2
- package/dist/internal/browser-router/router.js +9 -3
- package/dist/internal/csp.d.ts +37 -0
- package/dist/internal/csp.js +145 -0
- package/dist/internal/env/browser.js +11 -4
- package/dist/internal/env/request-context.d.ts +2 -0
- package/dist/internal/env/rsc.js +95 -15
- package/dist/internal/env/ssr.js +8 -2
- package/dist/internal/hash-inline-assets.d.ts +11 -0
- package/dist/internal/hash-inline-assets.js +103 -0
- package/dist/internal/http-router/router.d.ts +1 -1
- package/dist/internal/http-router/router.js +18 -6
- package/dist/internal/postbuild.d.ts +14 -8
- package/dist/internal/postbuild.js +41 -23
- package/dist/internal/prerender.d.ts +3 -1
- package/dist/internal/prerender.js +7 -2
- package/dist/internal/server/headers.d.ts +8 -0
- package/dist/internal/server/headers.js +29 -1
- package/dist/manifest.d.ts +6 -0
- package/dist/manifest.js +56 -13
- package/dist/types.d.ts +17 -5
- package/dist/utils/get-document-nonce.d.ts +11 -0
- package/dist/utils/get-document-nonce.js +16 -0
- package/dist/utils/validation.d.ts +2 -0
- package/dist/utils/validation.js +6 -0
- package/package.json +2 -2
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import type { HeaderEntries, HeadersInput } from '../../types.js';
|
|
1
2
|
/**
|
|
2
3
|
* Get the request headers as a read-only map.
|
|
3
4
|
* @returns A Promise resolving to a read-only map of request headers.
|
|
4
5
|
*/
|
|
5
6
|
export declare function headers(): Promise<ReadonlyMap<string, string>>;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the configured response headers once per request.
|
|
9
|
+
*
|
|
10
|
+
* The nonce extracted from a configured CSP and the headers applied to the
|
|
11
|
+
* response must come from the same evaluation.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createHeaderResolver(input: HeadersInput | undefined): (req: Request) => Promise<HeaderEntries>;
|
|
@@ -6,7 +6,7 @@ import { dynamic } from './dynamic.js';
|
|
|
6
6
|
*/
|
|
7
7
|
export async function headers() {
|
|
8
8
|
await dynamic();
|
|
9
|
-
const { req, cache } = RequestContext.use();
|
|
9
|
+
const { req, cache, nonce } = RequestContext.use();
|
|
10
10
|
// use request cache if possible to avoid reconstructing the map
|
|
11
11
|
if (cache.headers)
|
|
12
12
|
return cache.headers;
|
|
@@ -14,6 +14,34 @@ export async function headers() {
|
|
|
14
14
|
req.headers.forEach((value, key) => {
|
|
15
15
|
map.set(key, value);
|
|
16
16
|
});
|
|
17
|
+
// expose the effective CSP nonce to server components under the same header
|
|
18
|
+
// name used by the proxy pattern; a request header alone is not enough
|
|
19
|
+
// because the nonce may come from the configured `headers` fallback
|
|
20
|
+
map.delete('x-solas-nonce');
|
|
21
|
+
map.delete('x-nonce');
|
|
22
|
+
if (nonce)
|
|
23
|
+
map.set('x-nonce', nonce);
|
|
17
24
|
cache.headers = map;
|
|
18
25
|
return map;
|
|
19
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the configured response headers once per request.
|
|
29
|
+
*
|
|
30
|
+
* The nonce extracted from a configured CSP and the headers applied to the
|
|
31
|
+
* response must come from the same evaluation.
|
|
32
|
+
*/
|
|
33
|
+
export function createHeaderResolver(input) {
|
|
34
|
+
const cache = new WeakMap();
|
|
35
|
+
return (req) => {
|
|
36
|
+
if (!input)
|
|
37
|
+
return Promise.resolve({});
|
|
38
|
+
if (typeof input !== 'function')
|
|
39
|
+
return Promise.resolve(input);
|
|
40
|
+
let pending = cache.get(req);
|
|
41
|
+
if (!pending) {
|
|
42
|
+
pending = Promise.resolve().then(() => input(req));
|
|
43
|
+
cache.set(req, pending);
|
|
44
|
+
}
|
|
45
|
+
return pending;
|
|
46
|
+
};
|
|
47
|
+
}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { Assets } from './internal/assets.js';
|
|
2
|
+
import type { CspConfig } from './types.js';
|
|
3
|
+
import * as Config from './config.js';
|
|
2
4
|
import * as Prerender from './internal/prerender.js';
|
|
3
5
|
export type Manifest = {
|
|
4
6
|
artifacts: Prerender.ArtifactManifest;
|
|
5
7
|
publicFiles: ReadonlySet<string>;
|
|
8
|
+
csp?: CspConfig;
|
|
9
|
+
trailingSlash?: (typeof Config.TRAILING_SLASH_MODES)[number];
|
|
6
10
|
};
|
|
7
11
|
export declare function getManifestPath(outDir: string): string;
|
|
8
12
|
export declare function loadManifest(assets?: Assets): Promise<{
|
|
9
13
|
artifacts: Prerender.ArtifactManifest;
|
|
10
14
|
publicFiles: Set<string>;
|
|
15
|
+
csp: CspConfig | undefined;
|
|
16
|
+
trailingSlash: "always" | "ignore" | "never" | undefined;
|
|
11
17
|
} | null>;
|
package/dist/manifest.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord, isStringArray } from './utils/validation.js';
|
|
1
2
|
import * as Config from './config.js';
|
|
2
3
|
import { nodeAssets } from './internal/assets.js';
|
|
3
4
|
export function getManifestPath(outDir) {
|
|
@@ -24,38 +25,80 @@ export async function loadManifest(assets = nodeAssets) {
|
|
|
24
25
|
return null;
|
|
25
26
|
if (publicFiles !== undefined && !Array.isArray(publicFiles))
|
|
26
27
|
return null;
|
|
27
|
-
|
|
28
|
+
let trailingSlash;
|
|
29
|
+
const rawTrailingSlash = value.trailingSlash;
|
|
30
|
+
if (rawTrailingSlash !== undefined) {
|
|
31
|
+
if (rawTrailingSlash === 'always' ||
|
|
32
|
+
rawTrailingSlash === 'never' ||
|
|
33
|
+
rawTrailingSlash === 'ignore') {
|
|
34
|
+
trailingSlash = rawTrailingSlash;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
let cspConfig;
|
|
41
|
+
if (value.csp !== undefined) {
|
|
42
|
+
if (!isRecord(value.csp))
|
|
43
|
+
return null;
|
|
44
|
+
if (!isRecord(value.csp.directives))
|
|
45
|
+
return null;
|
|
46
|
+
const directives = {};
|
|
47
|
+
for (const [name, values] of Object.entries(value.csp.directives)) {
|
|
48
|
+
if (!isStringArray(values))
|
|
49
|
+
return null;
|
|
50
|
+
directives[name] = values;
|
|
51
|
+
}
|
|
52
|
+
cspConfig = { directives };
|
|
53
|
+
}
|
|
54
|
+
const parsedArtifacts = {};
|
|
55
|
+
for (const [route, entry] of Object.entries(artifacts)) {
|
|
28
56
|
if (!isRecord(entry))
|
|
29
57
|
return null;
|
|
30
|
-
const { mode, files } = entry;
|
|
58
|
+
const { mode, files, csp } = entry;
|
|
31
59
|
if (mode !== 'full' && mode !== 'ppr')
|
|
32
60
|
return null;
|
|
61
|
+
const artifact = { mode };
|
|
33
62
|
if (files !== undefined) {
|
|
34
63
|
if (!Array.isArray(files))
|
|
35
64
|
return null;
|
|
65
|
+
const parsedFiles = [];
|
|
36
66
|
for (const file of files) {
|
|
37
|
-
if (file
|
|
38
|
-
file !== 'prelude' &&
|
|
39
|
-
file !== 'postponed' &&
|
|
40
|
-
file !== 'metadata') {
|
|
67
|
+
if (!isArtifactFile(file))
|
|
41
68
|
return null;
|
|
42
|
-
|
|
69
|
+
parsedFiles.push(file);
|
|
43
70
|
}
|
|
71
|
+
artifact.files = parsedFiles;
|
|
72
|
+
}
|
|
73
|
+
if (csp !== undefined) {
|
|
74
|
+
if (!isRecord(csp))
|
|
75
|
+
return null;
|
|
76
|
+
if (!isStringArray(csp.script) || !isStringArray(csp.style))
|
|
77
|
+
return null;
|
|
78
|
+
artifact.csp = { script: csp.script, style: csp.style };
|
|
44
79
|
}
|
|
80
|
+
parsedArtifacts[route] = artifact;
|
|
45
81
|
}
|
|
46
|
-
|
|
47
|
-
|
|
82
|
+
const parsedPublicFiles = new Set();
|
|
83
|
+
for (const file of publicFiles ?? []) {
|
|
84
|
+
if (typeof file !== 'string' || !file.startsWith('/'))
|
|
48
85
|
return null;
|
|
86
|
+
parsedPublicFiles.add(file);
|
|
49
87
|
}
|
|
50
88
|
return {
|
|
51
|
-
artifacts:
|
|
52
|
-
publicFiles:
|
|
89
|
+
artifacts: parsedArtifacts,
|
|
90
|
+
publicFiles: parsedPublicFiles,
|
|
91
|
+
csp: cspConfig,
|
|
92
|
+
trailingSlash,
|
|
53
93
|
};
|
|
54
94
|
}
|
|
55
95
|
catch {
|
|
56
96
|
return null;
|
|
57
97
|
}
|
|
58
98
|
}
|
|
59
|
-
function
|
|
60
|
-
return
|
|
99
|
+
function isArtifactFile(value) {
|
|
100
|
+
return (value === 'html' ||
|
|
101
|
+
value === 'prelude' ||
|
|
102
|
+
value === 'postponed' ||
|
|
103
|
+
value === 'metadata');
|
|
61
104
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
type
|
|
2
|
-
params?: Record<string, string | string[]>;
|
|
3
|
-
};
|
|
4
|
-
import type { PluginOption } from 'vite';
|
|
1
|
+
import type { PluginOption, ResolvedConfig } from 'vite';
|
|
5
2
|
import { ExportReader } from './utils/export-reader.js';
|
|
6
3
|
import type * as BrowserRouter from './internal/browser-router/shared.js';
|
|
7
4
|
import type { HttpRouter } from './internal/http-router/router.js';
|
|
8
5
|
import type { Metadata } from './internal/metadata.js';
|
|
9
6
|
import type { HttpException } from './internal/navigation/http-exception.js';
|
|
7
|
+
import type { PrerenderImport } from './internal/postbuild.js';
|
|
10
8
|
import * as Config from './config.js';
|
|
11
9
|
import * as Build from './internal/build.js';
|
|
12
10
|
export type LogLevel = (typeof Config.LOG_LEVELS)[number];
|
|
@@ -18,6 +16,18 @@ export type Origin = `http://${string}` | `https://${string}`;
|
|
|
18
16
|
*/
|
|
19
17
|
export type HeaderEntries = Record<string, string>;
|
|
20
18
|
export type HeadersInput = HeaderEntries | ((req: Request) => HeaderEntries | Promise<HeaderEntries>);
|
|
19
|
+
/**
|
|
20
|
+
* CSP directives. Keywords such as `self` and `none` may be written without
|
|
21
|
+
* their surrounding quotes.
|
|
22
|
+
*/
|
|
23
|
+
export type CspDirectives = Record<string, readonly string[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Structured CSP configuration. Solas uses nonces for dynamic responses,
|
|
26
|
+
* hashes for prerendered responses, and both for PPR.
|
|
27
|
+
*/
|
|
28
|
+
export type CspConfig = {
|
|
29
|
+
directives: CspDirectives;
|
|
30
|
+
};
|
|
21
31
|
/**
|
|
22
32
|
* A platform deployment adapter. Solas registers `plugins` alongside its own,
|
|
23
33
|
* so adapters are configured here instead of being spread into the Vite plugins
|
|
@@ -31,6 +41,7 @@ export type Adapter = {
|
|
|
31
41
|
* option. A static host has no server, so it needs `'full'`.
|
|
32
42
|
*/
|
|
33
43
|
prerender?: Route.PrerenderMode | false;
|
|
44
|
+
postbuild?: (config: ResolvedConfig) => PrerenderImport | undefined | Promise<PrerenderImport | undefined>;
|
|
34
45
|
};
|
|
35
46
|
type PluginConfigBase = {
|
|
36
47
|
precompress?: boolean;
|
|
@@ -39,6 +50,7 @@ type PluginConfigBase = {
|
|
|
39
50
|
trailingSlash?: (typeof Config.TRAILING_SLASH_MODES)[number];
|
|
40
51
|
trustedOrigins?: readonly string[];
|
|
41
52
|
headers?: HeadersInput;
|
|
53
|
+
csp?: CspConfig;
|
|
42
54
|
adapter?: Adapter;
|
|
43
55
|
readonly logger?: {
|
|
44
56
|
level?: LogLevel;
|
|
@@ -116,7 +128,7 @@ export type MapEntry = {
|
|
|
116
128
|
'500s'?: readonly (DynamicImport | null)[];
|
|
117
129
|
loaders?: readonly (DynamicImport | null)[];
|
|
118
130
|
middlewares?: readonly (HttpRouter.Middleware | null)[];
|
|
119
|
-
endpoint?: (req?:
|
|
131
|
+
endpoint?: (req?: Request) => unknown;
|
|
120
132
|
};
|
|
121
133
|
export type ImportMap = Record<string, MapEntry>;
|
|
122
134
|
export type HttpMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CSP nonce for the current document.
|
|
3
|
+
*
|
|
4
|
+
* RSC navigations are part of the same document as the initial page load, so
|
|
5
|
+
* they must render with the same nonce. Read it from a framework script or
|
|
6
|
+
* style tag that already carries it.
|
|
7
|
+
*
|
|
8
|
+
* Use the `.nonce` property rather than a normal attribute: browsers hide the
|
|
9
|
+
* nonce content attribute after parsing so CSS cannot read it.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getDocumentNonce(): string | undefined;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CSP nonce for the current document.
|
|
3
|
+
*
|
|
4
|
+
* RSC navigations are part of the same document as the initial page load, so
|
|
5
|
+
* they must render with the same nonce. Read it from a framework script or
|
|
6
|
+
* style tag that already carries it.
|
|
7
|
+
*
|
|
8
|
+
* Use the `.nonce` property rather than a normal attribute: browsers hide the
|
|
9
|
+
* nonce content attribute after parsing so CSS cannot read it.
|
|
10
|
+
*/
|
|
11
|
+
export function getDocumentNonce() {
|
|
12
|
+
if (typeof document === 'undefined')
|
|
13
|
+
return;
|
|
14
|
+
const element = document.querySelector('script[nonce], style[nonce]');
|
|
15
|
+
return element?.nonce || element?.getAttribute('nonce') || undefined;
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jk2908/solas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "A Vite + React meta-framework exploring streaming, Server Components, and partial prerendering. Designed for simplicity and lightness",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -140,6 +140,6 @@
|
|
|
140
140
|
}
|
|
141
141
|
},
|
|
142
142
|
"engines": {
|
|
143
|
-
"node": "^
|
|
143
|
+
"node": "^22.23.4 || >=24.22.1"
|
|
144
144
|
}
|
|
145
145
|
}
|