@tinvar/renderer 0.0.0-stage → 0.4.1
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/LICENSE +21 -0
- package/bin/dev-renderer.js +117 -0
- package/bin/renderer-probe.js +52 -0
- package/bin/renderer.js +327 -0
- package/package.json +44 -4
- package/src/asset-origin.js +306 -0
- package/src/build-profile.js +108 -0
- package/src/can-branch.js +1666 -0
- package/src/classify.js +157 -0
- package/src/client-chunks.js +54 -0
- package/src/client-props.js +540 -0
- package/src/codec.js +297 -0
- package/src/dev-app.js +45 -0
- package/src/dev-guard.js +33 -0
- package/src/dev-server.js +460 -0
- package/src/eslint.js +469 -0
- package/src/head.js +524 -0
- package/src/html-stream.js +479 -0
- package/src/log.js +131 -0
- package/src/mint-client.js +142 -0
- package/src/panel-styles.js +254 -0
- package/src/pool.js +664 -0
- package/src/protocol.js +71 -0
- package/src/query-client.js +198 -0
- package/src/query-map.js +116 -0
- package/src/render-session.js +580 -0
- package/src/sandbox.js +710 -0
- package/src/server.js +466 -0
- package/src/socket-group.js +72 -0
- package/src/vite.js +2416 -0
- package/src/vitest.d.ts +20 -0
- package/src/vitest.js +179 -0
- package/src/warm.d.ts +9 -0
- package/src/warm.js +60 -0
- package/src/workbench.d.ts +146 -0
- package/src/workbench.js +754 -0
- package/README.md +0 -3
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// Where the browser loads the build's client assets from, and what each
|
|
2
|
+
// script's bytes must be (ADR 0097).
|
|
3
|
+
//
|
|
4
|
+
// tinvar-spec.md, "Deployment": "Static assets from Vite go to a CDN or are
|
|
5
|
+
// served by PHP with long cache headers." TINVAR_ASSET_ORIGIN, at build
|
|
6
|
+
// time, names the CDN: an https origin and nothing after it. The build's
|
|
7
|
+
// base becomes that origin, so every URL the build writes -- the client
|
|
8
|
+
// entry, each client component's chunk in the payload, every preload and
|
|
9
|
+
// stylesheet, a chunk's import of another -- is on it. Unset, the base is
|
|
10
|
+
// `/` and the assets are this origin's, served by Caddy or, without it, by
|
|
11
|
+
// PHP (Tinvar\Http\BuiltAssets).
|
|
12
|
+
//
|
|
13
|
+
// A CDN is another party's server between the build and the browser, so a
|
|
14
|
+
// build for one also records what every script it wrote is: a SHA-384
|
|
15
|
+
// digest of each file as written, put in the document's import map
|
|
16
|
+
// (`integrity`), which the browser holds every module it loads to --
|
|
17
|
+
// statically, dynamically, or preloaded by a <link> after the map. A CDN
|
|
18
|
+
// that serves different bytes for a script serves a page that does not
|
|
19
|
+
// run, rather than one that runs what it chose. `integrity: true` adds the
|
|
20
|
+
// map to a same-origin build as well; `false` leaves it out of either.
|
|
21
|
+
//
|
|
22
|
+
// Stylesheets are held to theirs too (ADR 0108). An import map covers
|
|
23
|
+
// modules alone, so the digest goes on the <link> itself: the RSC plugin's
|
|
24
|
+
// loadCss() writes each stylesheet's link from its assets manifest, and the
|
|
25
|
+
// module it makes is wrapped here to add the link's `integrity` from the
|
|
26
|
+
// same manifest, which the client build fills in -- on the server render and
|
|
27
|
+
// in every payload, so a stylesheet React inserts on a client navigation is
|
|
28
|
+
// held to it as well. A stylesheet a client component imports itself is
|
|
29
|
+
// preinited by the plugin's own code, which no digest reaches: a build with
|
|
30
|
+
// digests that has one is refused, not shipped unchecked.
|
|
31
|
+
//
|
|
32
|
+
// The origin is recorded in the rsc output, `rsc/assets.json`, where PHP
|
|
33
|
+
// reads it (Tinvar\Http\BuiltAssets) to name it in the page's
|
|
34
|
+
// Content-Security-Policy -- and, once the client build is written, the
|
|
35
|
+
// digests and what every page loads first: the client entry, its imports and
|
|
36
|
+
// the stylesheets every page links. PHP sends those as 103 Early Hints
|
|
37
|
+
// before a page renders, and the page's own modules in its Link header
|
|
38
|
+
// after, each with its digest, so a preload fetched from a header -- before
|
|
39
|
+
// the document's import map is read -- is held to it too. Chromium honours
|
|
40
|
+
// `integrity` on a header's modulepreload and on an Early Hint; a header
|
|
41
|
+
// preload without one put the module in the module map unchecked, and the
|
|
42
|
+
// import map's digest was never asked. The import map reaches the ssr entry
|
|
43
|
+
// through a module the build makes, `virtual:tinvar/assets`: the ssr
|
|
44
|
+
// environment is built after the client one, so the digests are those of
|
|
45
|
+
// the files the client build wrote.
|
|
46
|
+
|
|
47
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
48
|
+
import { createHash } from 'node:crypto';
|
|
49
|
+
import { join, resolve } from 'node:path';
|
|
50
|
+
import { getPluginApi } from '@vitejs/plugin-rsc';
|
|
51
|
+
|
|
52
|
+
/** The record's name, in the rsc environment's output. */
|
|
53
|
+
export const ASSETS_FILE = 'assets.json';
|
|
54
|
+
|
|
55
|
+
/** The module the ssr entry reads the import map from. */
|
|
56
|
+
export const ASSETS_MODULE = 'virtual:tinvar/assets';
|
|
57
|
+
|
|
58
|
+
const RESOLVED = `\0${ASSETS_MODULE}`;
|
|
59
|
+
|
|
60
|
+
/** The modules the RSC plugin makes for each loadCss(). */
|
|
61
|
+
const CSS_MODULE = '\0virtual:vite-rsc/css?';
|
|
62
|
+
|
|
63
|
+
/** The component loadCss() renders, as the plugin's module exports it. */
|
|
64
|
+
const RESOURCES_EXPORT = 'export const Resources = (';
|
|
65
|
+
|
|
66
|
+
/** Where the client build's digests go in the plugin's assets manifest. */
|
|
67
|
+
export const MANIFEST_INTEGRITY = 'tinvarIntegrity';
|
|
68
|
+
|
|
69
|
+
/** A build asked for an asset origin it may not have. */
|
|
70
|
+
export class AssetOriginRefused extends Error {}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The asset origin $env names, or null for none: https, a host, perhaps a
|
|
74
|
+
* port, and nothing else -- no path, no query, no credentials, written as
|
|
75
|
+
* a browser writes an origin.
|
|
76
|
+
*
|
|
77
|
+
* @param {Record<string, string | undefined>} env
|
|
78
|
+
* @returns {string | null}
|
|
79
|
+
*/
|
|
80
|
+
export function assetOriginOf(env) {
|
|
81
|
+
const raw = (env.TINVAR_ASSET_ORIGIN ?? '').trim();
|
|
82
|
+
if (raw === '') return null;
|
|
83
|
+
|
|
84
|
+
let url;
|
|
85
|
+
try {
|
|
86
|
+
url = new URL(raw);
|
|
87
|
+
} catch {
|
|
88
|
+
url = null;
|
|
89
|
+
}
|
|
90
|
+
if (url === null || url.protocol !== 'https:' || url.origin !== raw) {
|
|
91
|
+
throw new AssetOriginRefused(
|
|
92
|
+
`TINVAR_ASSET_ORIGIN='${raw}' is not an origin assets are served from: https, a host and perhaps a port, ` +
|
|
93
|
+
'as https://cdn.example.com, with no path and no trailing slash.',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return raw;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether TINVAR_ASSET_INTEGRITY asks for the digests: `1` for them on any
|
|
102
|
+
* build, `0` for none, or null to leave it to the asset origin.
|
|
103
|
+
*
|
|
104
|
+
* @param {Record<string, string | undefined>} env
|
|
105
|
+
* @returns {boolean | null}
|
|
106
|
+
*/
|
|
107
|
+
export function integrityAskedOf(env) {
|
|
108
|
+
const raw = (env.TINVAR_ASSET_INTEGRITY ?? '').trim();
|
|
109
|
+
if (raw === '') return null;
|
|
110
|
+
if (raw === '1') return true;
|
|
111
|
+
if (raw === '0') return false;
|
|
112
|
+
throw new AssetOriginRefused(`TINVAR_ASSET_INTEGRITY='${raw}' is neither 1 nor 0.`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The digest of $bytes, as an integrity value.
|
|
117
|
+
*
|
|
118
|
+
* @param {Uint8Array | string} bytes
|
|
119
|
+
*/
|
|
120
|
+
export function integrityOf(bytes) {
|
|
121
|
+
return `sha384-${createHash('sha384').update(bytes).digest('base64')}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The plugin.
|
|
126
|
+
*
|
|
127
|
+
* @param {{ env?: Record<string, string | undefined>, integrity?: boolean | undefined }} [options]
|
|
128
|
+
* the environment, the process's own by default; and whether to hold
|
|
129
|
+
* the assets to their digests -- the import map, the stylesheets'
|
|
130
|
+
* links and the preload headers -- which TINVAR_ASSET_INTEGRITY says
|
|
131
|
+
* where this does not, and which is otherwise on with an asset origin
|
|
132
|
+
* and off without
|
|
133
|
+
* @returns {import('vite').Plugin}
|
|
134
|
+
*/
|
|
135
|
+
export function assetOrigin({ env = process.env, integrity } = {}) {
|
|
136
|
+
const origin = assetOriginOf(env);
|
|
137
|
+
const withIntegrity = integrity ?? integrityAskedOf(env) ?? origin !== null;
|
|
138
|
+
/** @type {Record<string, string>} */
|
|
139
|
+
const digests = {};
|
|
140
|
+
let base = '/';
|
|
141
|
+
/** @type {import('vite').ResolvedConfig | null} */
|
|
142
|
+
let resolved = null;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
name: 'tinvar:asset-origin',
|
|
146
|
+
// After every other plugin has had the bundle, so what is hashed is
|
|
147
|
+
// what was written.
|
|
148
|
+
enforce: 'post',
|
|
149
|
+
config(_config, { command }) {
|
|
150
|
+
if (command === 'build' && origin !== null) return { base: `${origin}/` };
|
|
151
|
+
return undefined;
|
|
152
|
+
},
|
|
153
|
+
configResolved(config) {
|
|
154
|
+
base = config.base;
|
|
155
|
+
resolved = config;
|
|
156
|
+
},
|
|
157
|
+
resolveId(id) {
|
|
158
|
+
return id === ASSETS_MODULE ? RESOLVED : undefined;
|
|
159
|
+
},
|
|
160
|
+
transform(code, id) {
|
|
161
|
+
if (!withIntegrity || !id.startsWith(CSS_MODULE) || this.environment?.name !== 'rsc') return undefined;
|
|
162
|
+
if (this.environment.config.command !== 'build') return undefined;
|
|
163
|
+
// The plugin's first pass, which reads the module graph alone, strips
|
|
164
|
+
// every module to its imports: nothing of it is shipped.
|
|
165
|
+
if (resolved !== null && getPluginApi(resolved)?.manager?.isScanBuild === true) return undefined;
|
|
166
|
+
return withStylesheetIntegrity(code, id);
|
|
167
|
+
},
|
|
168
|
+
load(id) {
|
|
169
|
+
if (id !== RESOLVED) return undefined;
|
|
170
|
+
// Modules alone: an import map says nothing about a stylesheet.
|
|
171
|
+
const scripts = Object.fromEntries(Object.entries(digests).filter(([url]) => url.endsWith('.js')));
|
|
172
|
+
const map = withIntegrity && Object.keys(scripts).length > 0 ? { integrity: scripts } : undefined;
|
|
173
|
+
|
|
174
|
+
return `export const importMap = ${JSON.stringify(map)};\nexport const assetOrigin = ${JSON.stringify(origin)};\n`;
|
|
175
|
+
},
|
|
176
|
+
generateBundle() {
|
|
177
|
+
if (this.environment?.name !== 'rsc' || this.environment.config.command !== 'build') return;
|
|
178
|
+
this.emitFile({
|
|
179
|
+
type: 'asset',
|
|
180
|
+
fileName: ASSETS_FILE,
|
|
181
|
+
source: JSON.stringify({ format: 1, origin }) + '\n',
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
writeBundle(options, bundle) {
|
|
185
|
+
if (this.environment?.name !== 'client') return;
|
|
186
|
+
const outDir = options.dir ?? this.environment.config.build.outDir;
|
|
187
|
+
if (withIntegrity) {
|
|
188
|
+
// Read back as written: a plugin before this one may have appended
|
|
189
|
+
// to a stylesheet as it wrote it, as StyleX does.
|
|
190
|
+
for (const fileName of Object.keys(bundle)) {
|
|
191
|
+
if (!fileName.endsWith('.js') && !fileName.endsWith('.css')) continue;
|
|
192
|
+
digests[`${base}${fileName}`] = integrityOf(readFileSync(join(outDir, fileName)));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const manifest = resolved === null ? undefined : getPluginApi(resolved)?.manager?.buildAssetsManifest;
|
|
197
|
+
if (manifest === undefined) return;
|
|
198
|
+
if (withIntegrity) {
|
|
199
|
+
refuseUncheckedStylesheets(manifest);
|
|
200
|
+
// Read where loadCss() renders each stylesheet's link: the manifest
|
|
201
|
+
// is written for the server builds once every environment is built.
|
|
202
|
+
manifest[MANIFEST_INTEGRITY] = Object.fromEntries(
|
|
203
|
+
Object.entries(digests).filter(([url]) => url.endsWith('.css')),
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const rscOut = resolve(resolved?.root ?? '.', resolved?.environments?.rsc?.build?.outDir ?? 'dist/rsc');
|
|
208
|
+
writeFileSync(
|
|
209
|
+
join(rscOut, ASSETS_FILE),
|
|
210
|
+
JSON.stringify({
|
|
211
|
+
format: 1,
|
|
212
|
+
origin,
|
|
213
|
+
...(withIntegrity ? { integrity: { ...digests } } : {}),
|
|
214
|
+
hints: hintsOf(manifest),
|
|
215
|
+
}) + '\n',
|
|
216
|
+
);
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* What every page loads first, from the RSC plugin's assets manifest: the
|
|
223
|
+
* client entry and what it imports, and the stylesheets every loadCss()
|
|
224
|
+
* links -- those all of them share, so a stylesheet one view links alone,
|
|
225
|
+
* a panel's, is never hinted to another's pages.
|
|
226
|
+
*
|
|
227
|
+
* @param {Record<string, any>} manifest
|
|
228
|
+
* @returns {{ modules: string[], styles: string[] }}
|
|
229
|
+
*/
|
|
230
|
+
export function hintsOf(manifest) {
|
|
231
|
+
const modules = [manifest.clientEntryUrl, ...(manifest.clientEntryDeps?.js ?? [])].filter(
|
|
232
|
+
(url) => typeof url === 'string',
|
|
233
|
+
);
|
|
234
|
+
/** @type {string[][]} */
|
|
235
|
+
const sheets = Object.values(manifest.serverResources ?? {}).map((deps) =>
|
|
236
|
+
(deps?.css ?? []).filter((/** @type {unknown} */ url) => typeof url === 'string'),
|
|
237
|
+
);
|
|
238
|
+
const styles =
|
|
239
|
+
sheets.length === 0 ? [] : sheets[0].filter((url) => sheets.every((list) => list.includes(url)));
|
|
240
|
+
|
|
241
|
+
return { modules: [...new Set(modules)], styles: [...new Set(styles)] };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Refuses a build with digests that has a stylesheet the RSC plugin links
|
|
246
|
+
* itself: one a client component or the client entry imports, which its
|
|
247
|
+
* runtime preinits by URL alone.
|
|
248
|
+
*
|
|
249
|
+
* @param {Record<string, any>} manifest
|
|
250
|
+
*/
|
|
251
|
+
function refuseUncheckedStylesheets(manifest) {
|
|
252
|
+
const unchecked = new Set([
|
|
253
|
+
...(manifest.clientEntryDeps?.css ?? []),
|
|
254
|
+
...Object.values(manifest.clientReferenceDeps ?? {}).flatMap((deps) => deps?.css ?? []),
|
|
255
|
+
]);
|
|
256
|
+
if (unchecked.size > 0) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`[tinvar] A build held to its digests has stylesheets client code imports, which nothing would hold to theirs: ` +
|
|
259
|
+
`${[...unchecked].join(', ')}. Import them from a server component, whose loadCss() links them with their digest.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The module the RSC plugin makes for a loadCss(), with each stylesheet's
|
|
266
|
+
* link given its digest from the assets manifest. A stylesheet the manifest
|
|
267
|
+
* has no digest for is refused as the page renders, never linked unchecked.
|
|
268
|
+
*
|
|
269
|
+
* @param {string} code the plugin's module
|
|
270
|
+
* @param {string} id
|
|
271
|
+
* @returns {string}
|
|
272
|
+
*/
|
|
273
|
+
export function withStylesheetIntegrity(code, id) {
|
|
274
|
+
if (!code.includes(RESOURCES_EXPORT) || !code.includes('__vite_rsc_assets_manifest__')) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`[tinvar] The RSC plugin's stylesheet module (${id}) is not of the shape assetOrigin() holds to digests; ` +
|
|
277
|
+
'its links would go out unchecked.',
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
code.replace(RESOURCES_EXPORT, 'const __tinvar_unchecked_resources = (') +
|
|
283
|
+
`
|
|
284
|
+
const __tinvar_digests = __vite_rsc_assets_manifest__[${JSON.stringify(MANIFEST_INTEGRITY)}] ?? {};
|
|
285
|
+
function __tinvar_held(element) {
|
|
286
|
+
if (element === null || typeof element !== 'object' || element.props == null) return element;
|
|
287
|
+
const { rel, href } = element.props;
|
|
288
|
+
if (element.type === 'link' && rel === 'stylesheet') {
|
|
289
|
+
const digest = typeof href === 'string' && Object.hasOwn(__tinvar_digests, href) ? __tinvar_digests[href] : null;
|
|
290
|
+
if (digest === null) throw new Error('[tinvar] No digest for the stylesheet ' + String(href) + '; build again.');
|
|
291
|
+
return __vite_rsc_react__.cloneElement(element, { integrity: digest });
|
|
292
|
+
}
|
|
293
|
+
const children = element.props.children;
|
|
294
|
+
if (children === undefined) return element;
|
|
295
|
+
return __vite_rsc_react__.cloneElement(
|
|
296
|
+
element,
|
|
297
|
+
undefined,
|
|
298
|
+
...(Array.isArray(children) ? children.map(__tinvar_held) : [__tinvar_held(children)]),
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
export function Resources(props) {
|
|
302
|
+
return __tinvar_held(__tinvar_unchecked_resources(props));
|
|
303
|
+
}
|
|
304
|
+
`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// The build's profile, written into the build (ADR 0066).
|
|
2
|
+
//
|
|
3
|
+
// tinvar-spec.md, "Dev and test tooling": tooling is controlled by the build
|
|
4
|
+
// profile -- only `--profile=test` has the browser test helpers. PHP is not
|
|
5
|
+
// built, so the helpers' code cannot be left out of it; what can be made a
|
|
6
|
+
// property of the artefact is the JavaScript build. `tinvar build
|
|
7
|
+
// --profile=test` runs Vite with TINVAR_PROFILE=test, and this plugin records
|
|
8
|
+
// the profile in the rsc environment's output, `rsc/profile.json`, where PHP
|
|
9
|
+
// reads it (Tinvar\Support\BuildProfile): a server whose profile is test
|
|
10
|
+
// refuses a build that does not say test, and a server whose profile is not
|
|
11
|
+
// refuses one that does. A build with no record is a production build, so an
|
|
12
|
+
// application without this plugin simply cannot serve the test profile.
|
|
13
|
+
//
|
|
14
|
+
// TINVAR_BUILD_DIR, when set, is where the build goes -- `<dir>/client`,
|
|
15
|
+
// `<dir>/ssr`, `<dir>/rsc` -- so a test build can sit beside the production
|
|
16
|
+
// one rather than over it.
|
|
17
|
+
|
|
18
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
19
|
+
|
|
20
|
+
/** The record's name, in the rsc environment's output. */
|
|
21
|
+
export const PROFILE_FILE = 'profile.json';
|
|
22
|
+
|
|
23
|
+
export const PROFILES = ['production', 'staging', 'test'];
|
|
24
|
+
|
|
25
|
+
/** The variables any of which, saying production, rule the test and staging profiles out. */
|
|
26
|
+
const PRODUCTION_MARKERS = ['TINVAR_ENV', 'APP_ENV'];
|
|
27
|
+
|
|
28
|
+
/** A build asked for a profile it may not have. */
|
|
29
|
+
export class BuildProfileRefused extends Error {}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The profile $env asks for: production unless TINVAR_PROFILE says staging
|
|
33
|
+
* or test.
|
|
34
|
+
*
|
|
35
|
+
* NODE_ENV is not read as a production marker here: Vite sets it to
|
|
36
|
+
* production for every build. The server reads it, beside the others, when
|
|
37
|
+
* it decides whether to serve.
|
|
38
|
+
*
|
|
39
|
+
* @param {Record<string, string | undefined>} env
|
|
40
|
+
* @returns {'production' | 'staging' | 'test'}
|
|
41
|
+
*/
|
|
42
|
+
export function profileOf(env) {
|
|
43
|
+
const profile = (env.TINVAR_PROFILE ?? '').trim().toLowerCase() || 'production';
|
|
44
|
+
if (!PROFILES.includes(profile)) {
|
|
45
|
+
throw new BuildProfileRefused(
|
|
46
|
+
`TINVAR_PROFILE='${profile}' is not a profile: it is production, staging or test.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (profile !== 'production') {
|
|
50
|
+
for (const name of PRODUCTION_MARKERS) {
|
|
51
|
+
if ((env[name] ?? '').trim().toLowerCase() === 'production') {
|
|
52
|
+
throw new BuildProfileRefused(
|
|
53
|
+
`A ${profile} build is refused: ${name}=production. ` +
|
|
54
|
+
(profile === 'test'
|
|
55
|
+
? 'The test profile serves the browser test endpoint, which never runs in production.'
|
|
56
|
+
: 'A staging build is served under keys bound to staging, which production never has.'),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return /** @type {'production' | 'staging' | 'test'} */ (profile);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The plugin: the profile recorded in the rsc output, and the build put in
|
|
67
|
+
* TINVAR_BUILD_DIR when that is set. Listed after the RSC plugin, so the
|
|
68
|
+
* output directories it gives win over that plugin's defaults.
|
|
69
|
+
*
|
|
70
|
+
* @param {{ env?: Record<string, string | undefined> }} [options] the environment; the process's own by default
|
|
71
|
+
* @returns {import('vite').Plugin}
|
|
72
|
+
*/
|
|
73
|
+
export function buildProfile({ env = process.env } = {}) {
|
|
74
|
+
const profile = profileOf(env);
|
|
75
|
+
const out = (env.TINVAR_BUILD_DIR ?? '').trim();
|
|
76
|
+
// Absolute, as the server requires: each process that reads the build
|
|
77
|
+
// runs in a directory of its own.
|
|
78
|
+
if (out !== '' && !isAbsolute(out)) {
|
|
79
|
+
throw new BuildProfileRefused(
|
|
80
|
+
`TINVAR_BUILD_DIR='${out}' is not an absolute path; name the build by one.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
name: 'tinvar:build-profile',
|
|
86
|
+
apply: 'build',
|
|
87
|
+
config() {
|
|
88
|
+
if (out === '') return undefined;
|
|
89
|
+
const directory = resolve(out);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
environments: {
|
|
93
|
+
client: { build: { outDir: resolve(directory, 'client') } },
|
|
94
|
+
ssr: { build: { outDir: resolve(directory, 'ssr') } },
|
|
95
|
+
rsc: { build: { outDir: resolve(directory, 'rsc') } },
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
generateBundle() {
|
|
100
|
+
if (this.environment?.name !== 'rsc') return;
|
|
101
|
+
this.emitFile({
|
|
102
|
+
type: 'asset',
|
|
103
|
+
fileName: PROFILE_FILE,
|
|
104
|
+
source: JSON.stringify({ format: 1, profile }) + '\n',
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|