@brightspot/ui-builder 2.0.0 → 2.0.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.
package/dist/commands/dev.js
CHANGED
|
@@ -13,6 +13,13 @@ export async function dev(opts) {
|
|
|
13
13
|
const config = await createDevConfig({ builder, target, port: opts.port });
|
|
14
14
|
const server = await createServer(config);
|
|
15
15
|
await server.listen();
|
|
16
|
+
// Proxy root rarely renders useful HTML; land cmd-click on the app path.
|
|
17
|
+
const openPath = builder.dev?.openPath ?? '/cms/';
|
|
18
|
+
if (server.resolvedUrls && openPath !== '/') {
|
|
19
|
+
const append = (u) => new URL(openPath, u).href;
|
|
20
|
+
server.resolvedUrls.local = server.resolvedUrls.local.map(append);
|
|
21
|
+
server.resolvedUrls.network = server.resolvedUrls.network.map(append);
|
|
22
|
+
}
|
|
16
23
|
server.printUrls();
|
|
17
24
|
log.success('Dev server running — proxying to ' + target);
|
|
18
25
|
}
|
|
@@ -14,6 +14,9 @@ const DEFAULTS = {
|
|
|
14
14
|
jsName: 'bundle.js',
|
|
15
15
|
cssName: 'style.css',
|
|
16
16
|
},
|
|
17
|
+
dev: {
|
|
18
|
+
openPath: '/cms/',
|
|
19
|
+
},
|
|
17
20
|
};
|
|
18
21
|
export function resolveConfig() {
|
|
19
22
|
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
@@ -38,7 +41,17 @@ export function resolveConfig() {
|
|
|
38
41
|
jsName: raw.output?.jsName ?? DEFAULTS.output.jsName,
|
|
39
42
|
cssName: raw.output?.cssName ?? DEFAULTS.output.cssName,
|
|
40
43
|
};
|
|
41
|
-
|
|
44
|
+
const openPath = resolveOpenPath(raw.dev?.openPath);
|
|
45
|
+
return { config: { basePath, entry, format, name, output, dev: { openPath } }, warnings };
|
|
46
|
+
}
|
|
47
|
+
function resolveOpenPath(explicit) {
|
|
48
|
+
const value = explicit ?? DEFAULTS.dev.openPath;
|
|
49
|
+
// Reject protocol-relative ('//host/...') so a misconfigured openPath
|
|
50
|
+
// can't redirect the printed dev URL off-site.
|
|
51
|
+
if (!value.startsWith('/') || value.startsWith('//')) {
|
|
52
|
+
throw new ConfigError(`dev.openPath "${value}" must start with a single "/".`);
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
42
55
|
}
|
|
43
56
|
function resolveBasePath(explicit, pkgName, warnings) {
|
|
44
57
|
if (explicit) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
/**
|
|
3
|
+
* Build a `proxyRes` handler that strips occurrences of the upstream
|
|
4
|
+
* origin from HTML response bodies. Brightspot embeds absolute URLs
|
|
5
|
+
* (e.g. `https://localhost/storage/resource_resource/foo.js`) for
|
|
6
|
+
* storage assets, generated from a CDN setting that ignores
|
|
7
|
+
* X-Forwarded-* headers. Without rewriting, the browser at the proxy
|
|
8
|
+
* origin (`https://localhost:5173`) would issue cross-origin requests
|
|
9
|
+
* back to the upstream — module scripts then fail CORS, and mixed
|
|
10
|
+
* content blocks plain-HTTP origins. Stripping the origin makes those
|
|
11
|
+
* URLs relative, so the browser routes them through the dev proxy.
|
|
12
|
+
*
|
|
13
|
+
* Non-HTML responses pass through untouched. Empty / no-content
|
|
14
|
+
* statuses pass through too.
|
|
15
|
+
*
|
|
16
|
+
* Requires `selfHandleResponse: true` on the proxy entry so the
|
|
17
|
+
* response stream isn't already piped to the client.
|
|
18
|
+
*/
|
|
19
|
+
export declare function rewriteHtmlBody(target: string): (proxyRes: IncomingMessage, _req: IncomingMessage, res: ServerResponse) => void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import zlib from 'node:zlib';
|
|
2
|
+
/**
|
|
3
|
+
* Build a `proxyRes` handler that strips occurrences of the upstream
|
|
4
|
+
* origin from HTML response bodies. Brightspot embeds absolute URLs
|
|
5
|
+
* (e.g. `https://localhost/storage/resource_resource/foo.js`) for
|
|
6
|
+
* storage assets, generated from a CDN setting that ignores
|
|
7
|
+
* X-Forwarded-* headers. Without rewriting, the browser at the proxy
|
|
8
|
+
* origin (`https://localhost:5173`) would issue cross-origin requests
|
|
9
|
+
* back to the upstream — module scripts then fail CORS, and mixed
|
|
10
|
+
* content blocks plain-HTTP origins. Stripping the origin makes those
|
|
11
|
+
* URLs relative, so the browser routes them through the dev proxy.
|
|
12
|
+
*
|
|
13
|
+
* Non-HTML responses pass through untouched. Empty / no-content
|
|
14
|
+
* statuses pass through too.
|
|
15
|
+
*
|
|
16
|
+
* Requires `selfHandleResponse: true` on the proxy entry so the
|
|
17
|
+
* response stream isn't already piped to the client.
|
|
18
|
+
*/
|
|
19
|
+
export function rewriteHtmlBody(target) {
|
|
20
|
+
const originPatterns = buildOriginPatterns(target);
|
|
21
|
+
return (proxyRes, _req, res) => {
|
|
22
|
+
const status = proxyRes.statusCode ?? 200;
|
|
23
|
+
const headers = sanitizeHeaders(proxyRes.headers);
|
|
24
|
+
// selfHandleResponse: true disables the proxy library's built-in
|
|
25
|
+
// autoRewrite/protocolRewrite, so redirect Location headers come
|
|
26
|
+
// through pointing at the upstream origin. Strip the upstream
|
|
27
|
+
// origin so the browser follows the redirect via the proxy, not
|
|
28
|
+
// directly to the upstream (which would be cross-origin / wrong
|
|
29
|
+
// port / wrong scheme).
|
|
30
|
+
const loc = headers['location'];
|
|
31
|
+
if (typeof loc === 'string') {
|
|
32
|
+
let rewritten = loc;
|
|
33
|
+
for (const re of originPatterns)
|
|
34
|
+
rewritten = rewritten.replace(re, '');
|
|
35
|
+
if (rewritten !== loc)
|
|
36
|
+
headers['location'] = rewritten;
|
|
37
|
+
}
|
|
38
|
+
const ct = String(headers['content-type'] ?? '').toLowerCase();
|
|
39
|
+
const isHtml = ct.includes('text/html');
|
|
40
|
+
const isEmpty = status === 204 || status === 304;
|
|
41
|
+
if (!isHtml || isEmpty) {
|
|
42
|
+
res.writeHead(status, headers);
|
|
43
|
+
proxyRes.pipe(res);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const stream = decodeStream(proxyRes, String(proxyRes.headers['content-encoding'] ?? ''));
|
|
47
|
+
const chunks = [];
|
|
48
|
+
stream.on('data', c => chunks.push(c));
|
|
49
|
+
stream.on('error', err => {
|
|
50
|
+
res.writeHead(502, { 'content-type': 'text/plain' });
|
|
51
|
+
res.end(`proxy decode error: ${err.message}`);
|
|
52
|
+
});
|
|
53
|
+
stream.on('end', () => {
|
|
54
|
+
let body = Buffer.concat(chunks).toString('utf8');
|
|
55
|
+
for (const re of originPatterns)
|
|
56
|
+
body = body.replace(re, '');
|
|
57
|
+
const out = Buffer.from(body, 'utf8');
|
|
58
|
+
headers['content-length'] = String(out.length);
|
|
59
|
+
res.writeHead(status, headers);
|
|
60
|
+
res.end(out);
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Vite serves over HTTP/2 (mkcert), but the upstream speaks HTTP/1.1.
|
|
65
|
+
// HTTP/2 forbids connection-specific headers and transfer-encoding;
|
|
66
|
+
// passing them through triggers ERR_HTTP2_INVALID_CONNECTION_HEADERS
|
|
67
|
+
// in node's writeHead. Also drop content-encoding because we always
|
|
68
|
+
// decompress and re-emit identity. content-length is recomputed by
|
|
69
|
+
// the caller after rewriting.
|
|
70
|
+
function sanitizeHeaders(input) {
|
|
71
|
+
const dropped = new Set([
|
|
72
|
+
'connection',
|
|
73
|
+
'transfer-encoding',
|
|
74
|
+
'keep-alive',
|
|
75
|
+
'proxy-authenticate',
|
|
76
|
+
'proxy-authorization',
|
|
77
|
+
'te',
|
|
78
|
+
'trailer',
|
|
79
|
+
'upgrade',
|
|
80
|
+
'http2-settings',
|
|
81
|
+
'content-encoding',
|
|
82
|
+
'content-length',
|
|
83
|
+
]);
|
|
84
|
+
const out = {};
|
|
85
|
+
for (const [k, v] of Object.entries(input)) {
|
|
86
|
+
if (v === undefined)
|
|
87
|
+
continue;
|
|
88
|
+
if (dropped.has(k.toLowerCase()))
|
|
89
|
+
continue;
|
|
90
|
+
out[k] = v;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
function decodeStream(stream, encoding) {
|
|
95
|
+
switch (encoding.toLowerCase()) {
|
|
96
|
+
case 'gzip':
|
|
97
|
+
return stream.pipe(zlib.createGunzip());
|
|
98
|
+
case 'br':
|
|
99
|
+
return stream.pipe(zlib.createBrotliDecompress());
|
|
100
|
+
case 'deflate':
|
|
101
|
+
return stream.pipe(zlib.createInflate());
|
|
102
|
+
default:
|
|
103
|
+
return stream;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Build regexes that match the upstream origin in a few common
|
|
107
|
+
// forms — both `http://` and `https://` so an http target still
|
|
108
|
+
// strips https variants the CMS may emit when X-Forwarded-Proto is
|
|
109
|
+
// honored, and an explicit-port form so e.g. http://localhost:8080
|
|
110
|
+
// strips both with and without :8080.
|
|
111
|
+
function buildOriginPatterns(target) {
|
|
112
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
113
|
+
const u = new URL(target);
|
|
114
|
+
const host = u.hostname;
|
|
115
|
+
const explicitPort = u.port;
|
|
116
|
+
// Form: scheme://host(:port)? — only strip when followed by a path
|
|
117
|
+
// boundary so we don't eat unrelated strings that happen to share
|
|
118
|
+
// the prefix.
|
|
119
|
+
const patterns = [
|
|
120
|
+
new RegExp(`https?://${escape(host)}(?=[/'"\\)\\s])`, 'g'),
|
|
121
|
+
new RegExp(`https?://${escape(host)}:\\d+(?=[/'"\\)\\s])`, 'g'),
|
|
122
|
+
];
|
|
123
|
+
void explicitPort;
|
|
124
|
+
return patterns;
|
|
125
|
+
}
|
package/dist/vite/vite-config.js
CHANGED
|
@@ -3,6 +3,11 @@ import { loadConfigFromFile, mergeConfig } from 'vite';
|
|
|
3
3
|
import { log } from '../lib/logger.js';
|
|
4
4
|
import { loadAutoPlugins } from './auto-plugins.js';
|
|
5
5
|
import { devEntryPlugin } from './plugin-html-inject.js';
|
|
6
|
+
// Mask define.amd around the IIFE so UMD-wrapped deps (e.g. loglevel) take
|
|
7
|
+
// the global branch — host-page RequireJS rejects anonymous define() calls.
|
|
8
|
+
// Namespaced var name reduces collision risk with bundled UMD code.
|
|
9
|
+
const AMD_GUARD_INTRO = 'var __bsp_amd=typeof define!=="undefined"&&define&&define.amd;if(__bsp_amd)define.amd=void 0;try{';
|
|
10
|
+
const AMD_GUARD_OUTRO = '}finally{if(__bsp_amd)define.amd=__bsp_amd;}';
|
|
6
11
|
export async function createDevConfig({ builder, target, port }) {
|
|
7
12
|
const cwd = process.cwd();
|
|
8
13
|
const { basePath, entry, output: { jsName, cssName }, } = builder;
|
|
@@ -13,6 +18,10 @@ export async function createDevConfig({ builder, target, port }) {
|
|
|
13
18
|
// that case so /index.ts isn't proxied to the upstream application.
|
|
14
19
|
const entryParts = entry.split('/');
|
|
15
20
|
const entryDirAlt = entryParts.length > 1 ? `|${escapeRegex(entryParts[0])}/` : '';
|
|
21
|
+
// x-forwarded-proto must match the upstream's scheme — otherwise an
|
|
22
|
+
// http CMS canonicalises to its toolUrlPrefix and autoRewrite loops
|
|
23
|
+
// the 302 back through the dev origin.
|
|
24
|
+
const targetScheme = new URL(target).protocol === 'https:' ? 'https' : 'http';
|
|
16
25
|
const [mkcert, autoPlugins] = await Promise.all([loadMkcertPlugin(), loadAutoPlugins(cwd)]);
|
|
17
26
|
const base = {
|
|
18
27
|
root: cwd,
|
|
@@ -27,14 +36,21 @@ export async function createDevConfig({ builder, target, port }) {
|
|
|
27
36
|
},
|
|
28
37
|
server: {
|
|
29
38
|
port,
|
|
39
|
+
// Default HMR path '/' falls through the proxy to the upstream CMS
|
|
40
|
+
// and 404s; '/@vite/*' is in the regex exclusion list below.
|
|
41
|
+
hmr: { path: '/@vite/hmr' },
|
|
30
42
|
proxy: {
|
|
31
43
|
[`^/(?!(${escaped}${entryDirAlt}|@vite|@fs|node_modules))`]: {
|
|
32
44
|
target,
|
|
45
|
+
// Local CMS dev servers use self-signed certs.
|
|
46
|
+
secure: false,
|
|
47
|
+
// protocolRewrite: Vite is mkcert-https only; http redirects would 404.
|
|
33
48
|
autoRewrite: true,
|
|
49
|
+
protocolRewrite: 'https',
|
|
34
50
|
changeOrigin: true,
|
|
35
51
|
configure: proxy => {
|
|
36
52
|
proxy.on('proxyReq', proxyReq => {
|
|
37
|
-
proxyReq.setHeader('x-forwarded-proto',
|
|
53
|
+
proxyReq.setHeader('x-forwarded-proto', targetScheme);
|
|
38
54
|
proxyReq.setHeader('x-brightspot-dev', '1');
|
|
39
55
|
});
|
|
40
56
|
},
|
|
@@ -50,6 +66,12 @@ export async function createBuildConfig({ builder }) {
|
|
|
50
66
|
const autoPlugins = await loadAutoPlugins(cwd);
|
|
51
67
|
const base = {
|
|
52
68
|
root: cwd,
|
|
69
|
+
// Lib mode doesn't replace process.env.NODE_ENV; pin it so deps with dev
|
|
70
|
+
// guards don't crash with `process is not defined`. Dev path skips this —
|
|
71
|
+
// Vite's optimizeDeps/esbuild substitutes it there.
|
|
72
|
+
define: {
|
|
73
|
+
'process.env.NODE_ENV': JSON.stringify('production'),
|
|
74
|
+
},
|
|
53
75
|
plugins: [...autoPlugins],
|
|
54
76
|
resolve: {
|
|
55
77
|
preserveSymlinks: true,
|
|
@@ -96,6 +118,7 @@ export async function createBuildConfig({ builder }) {
|
|
|
96
118
|
rollupOptions: {
|
|
97
119
|
output: {
|
|
98
120
|
assetFileNames: () => output.cssName,
|
|
121
|
+
...(format === 'iife' ? { intro: AMD_GUARD_INTRO, outro: AMD_GUARD_OUTRO } : {}),
|
|
99
122
|
},
|
|
100
123
|
},
|
|
101
124
|
},
|