@brightspot/ui-builder 2.0.2 → 5.0.4-pre.20260624

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspot/ui-builder",
3
- "version": "2.0.2",
3
+ "version": "5.0.4-pre.20260624",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "description": "Zero-config build toolkit for Brightspot CMS front-end development.",
@@ -8,6 +8,10 @@
8
8
  "brightspot-ui": "./dist/index.js"
9
9
  },
10
10
  "exports": {
11
+ "./vite": {
12
+ "types": "./dist/vite/vite-config.d.ts",
13
+ "default": "./dist/vite/vite-config.js"
14
+ },
11
15
  "./configs/tsconfig.base.json": "./configs/tsconfig.base.json",
12
16
  "./configs/eslint.config.mjs": "./configs/eslint.config.mjs",
13
17
  "./configs/prettier.config.mjs": "./configs/prettier.config.mjs"
@@ -1,2 +0,0 @@
1
- import { type BuilderConfig } from '../lib/resolve-config.js';
2
- export declare function loadOrExit(): BuilderConfig;
@@ -1,23 +0,0 @@
1
- import { log } from '../lib/logger.js';
2
- import { ConfigError, resolveConfig } from '../lib/resolve-config.js';
3
- // CLI-side wrapper around resolveConfig: prints warnings + a one-line
4
- // summary, and on ConfigError logs the message and exits 1. Lives next
5
- // to commands so the lib module stays free of process termination.
6
- export function loadOrExit() {
7
- try {
8
- const { config, warnings } = resolveConfig();
9
- for (const w of warnings)
10
- log.warn(w);
11
- log.info(`basePath: ${config.basePath}`);
12
- log.info(`entry: ${config.entry}`);
13
- log.info(`format: ${config.format}${config.format === 'iife' && config.name ? ` (name: ${config.name})` : ''}`);
14
- return config;
15
- }
16
- catch (e) {
17
- if (e instanceof ConfigError) {
18
- log.error(e.message);
19
- process.exit(1);
20
- }
21
- throw e;
22
- }
23
- }
@@ -1 +0,0 @@
1
- export declare function build(): Promise<void>;
@@ -1,11 +0,0 @@
1
- import { build as viteBuild } from 'vite';
2
- import { log } from '../lib/logger.js';
3
- import { createBuildConfig } from '../vite/vite-config.js';
4
- import { loadOrExit } from './_load-config.js';
5
- export async function build() {
6
- const builder = loadOrExit();
7
- log.info(`Building ${builder.format.toUpperCase()} bundle...`);
8
- const config = await createBuildConfig({ builder });
9
- await viteBuild(config);
10
- log.success(`Build complete — output in ${builder.output.dir}/`);
11
- }
@@ -1,6 +0,0 @@
1
- interface DevOptions {
2
- port?: number;
3
- url?: string;
4
- }
5
- export declare function dev(opts: DevOptions): Promise<void>;
6
- export {};
@@ -1,38 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { createServer } from 'vite';
5
- import { log } from '../lib/logger.js';
6
- import { createDevConfig } from '../vite/vite-config.js';
7
- import { loadOrExit } from './_load-config.js';
8
- const LOCAL_URL_PATH = path.join(os.homedir(), '.brightspot', 'local-url');
9
- const DEFAULT_TARGET = 'http://localhost';
10
- export async function dev(opts) {
11
- const builder = loadOrExit();
12
- const target = opts.url ?? readLocalTarget();
13
- const config = await createDevConfig({ builder, target, port: opts.port });
14
- const server = await createServer(config);
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
- }
23
- server.printUrls();
24
- log.success('Dev server running — proxying to ' + target);
25
- }
26
- function readLocalTarget() {
27
- try {
28
- const url = fs.readFileSync(LOCAL_URL_PATH, 'utf8').trim();
29
- if (url) {
30
- log.info(`Proxy target: ${url}`);
31
- return url;
32
- }
33
- }
34
- catch {
35
- log.warn(`${LOCAL_URL_PATH} not found — falling back to ${DEFAULT_TARGET}`);
36
- }
37
- return DEFAULT_TARGET;
38
- }
@@ -1 +0,0 @@
1
- export declare function init(): Promise<void>;
@@ -1,71 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- import { log } from '../lib/logger.js';
4
- const cwd = process.cwd();
5
- export async function init() {
6
- log.info('Initializing Brightspot UI project...');
7
- writeTsConfig();
8
- writeEslintConfig();
9
- patchPackageJson();
10
- scaffoldEntryPoint();
11
- log.success('Project initialized. Run `brightspot-ui dev` to start developing.');
12
- }
13
- function writeTsConfig() {
14
- const tsConfigPath = path.join(cwd, 'tsconfig.json');
15
- if (fs.existsSync(tsConfigPath)) {
16
- log.warn('tsconfig.json already exists — skipping');
17
- return;
18
- }
19
- const config = {
20
- extends: '@brightspot/ui-builder/configs/tsconfig.base.json',
21
- };
22
- fs.writeFileSync(tsConfigPath, JSON.stringify(config, null, 2) + '\n');
23
- log.success('Created tsconfig.json');
24
- }
25
- function writeEslintConfig() {
26
- const eslintConfigPath = path.join(cwd, 'eslint.config.mjs');
27
- if (fs.existsSync(eslintConfigPath)) {
28
- log.warn('eslint.config.mjs already exists — skipping');
29
- return;
30
- }
31
- const content = `import config from '@brightspot/ui-builder/configs/eslint.config.mjs'
32
-
33
- export default [...config]
34
- `;
35
- fs.writeFileSync(eslintConfigPath, content);
36
- log.success('Created eslint.config.mjs');
37
- }
38
- function patchPackageJson() {
39
- const pkgPath = path.join(cwd, 'package.json');
40
- if (!fs.existsSync(pkgPath)) {
41
- log.warn('No package.json found — skipping script injection');
42
- return;
43
- }
44
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
45
- pkg.scripts = pkg.scripts ?? {};
46
- pkg.scripts.dev = 'brightspot-ui dev';
47
- pkg.scripts.build = 'brightspot-ui build';
48
- pkg.prettier = '@brightspot/ui-builder/configs/prettier.config.mjs';
49
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
50
- log.success('Patched package.json (scripts + prettier)');
51
- }
52
- function scaffoldEntryPoint() {
53
- const srcDir = path.join(cwd, 'src');
54
- const entryPath = path.join(srcDir, 'index.ts');
55
- if (fs.existsSync(entryPath)) {
56
- log.warn('src/index.ts already exists — skipping');
57
- return;
58
- }
59
- if (!fs.existsSync(srcDir)) {
60
- fs.mkdirSync(srcDir, { recursive: true });
61
- }
62
- const template = `// Entry point for Brightspot CMS bundle
63
- // Import your styles:
64
- // import './styles/main.css'
65
-
66
- // Import your modules:
67
- // import './tour'
68
- `;
69
- fs.writeFileSync(entryPath, template);
70
- log.success('Created src/index.ts');
71
- }
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/index.js DELETED
@@ -1,26 +0,0 @@
1
- #!/usr/bin/env node
2
- import { program } from 'commander';
3
- program.name('brightspot-ui').description('Zero-config build toolkit for Brightspot CMS front-end development.');
4
- program
5
- .command('init')
6
- .description('Scaffold config files for a new Brightspot front-end project')
7
- .action(async () => {
8
- const { init } = await import('./commands/init.js');
9
- await init();
10
- });
11
- program
12
- .command('dev [url]')
13
- .description('Start Vite dev server with HTTPS proxy to Brightspot')
14
- .option('--port <port>', 'Dev server port', parseInt)
15
- .action(async (url, opts) => {
16
- const { dev } = await import('./commands/dev.js');
17
- await dev({ ...opts, url });
18
- });
19
- program
20
- .command('build')
21
- .description('Build production bundle')
22
- .action(async () => {
23
- const { build } = await import('./commands/build.js');
24
- await build();
25
- });
26
- program.parse();
@@ -1,6 +0,0 @@
1
- export declare const log: {
2
- info(msg: string): void;
3
- success(msg: string): void;
4
- warn(msg: string): void;
5
- error(msg: string): void;
6
- };
@@ -1,15 +0,0 @@
1
- import pc from 'picocolors';
2
- export const log = {
3
- info(msg) {
4
- console.log(pc.cyan('ℹ'), msg);
5
- },
6
- success(msg) {
7
- console.log(pc.green('✔'), msg);
8
- },
9
- warn(msg) {
10
- console.log(pc.yellow('⚠'), msg);
11
- },
12
- error(msg) {
13
- console.error(pc.red('✖'), msg);
14
- },
15
- };
@@ -1,23 +0,0 @@
1
- export interface BuilderConfig {
2
- basePath: string;
3
- entry: string;
4
- format: 'es' | 'iife';
5
- name?: string;
6
- output: {
7
- dir: string;
8
- jsName: string;
9
- cssName: string;
10
- };
11
- dev?: {
12
- /** Path appended to printed dev URLs. "/" disables. */
13
- openPath: string;
14
- };
15
- }
16
- export interface ResolveResult {
17
- config: BuilderConfig;
18
- warnings: string[];
19
- }
20
- export declare class ConfigError extends Error {
21
- constructor(message: string);
22
- }
23
- export declare function resolveConfig(): ResolveResult;
@@ -1,128 +0,0 @@
1
- import fs from 'node:fs';
2
- import path from 'node:path';
3
- export class ConfigError extends Error {
4
- constructor(message) {
5
- super(message);
6
- this.name = 'ConfigError';
7
- }
8
- }
9
- const DEFAULTS = {
10
- entry: 'src/index.ts',
11
- format: 'es',
12
- output: {
13
- dir: 'dist',
14
- jsName: 'bundle.js',
15
- cssName: 'style.css',
16
- },
17
- dev: {
18
- openPath: '/cms/',
19
- },
20
- };
21
- export function resolveConfig() {
22
- const pkgPath = path.resolve(process.cwd(), 'package.json');
23
- let pkg;
24
- try {
25
- pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
26
- }
27
- catch {
28
- throw new ConfigError(`Could not read ${pkgPath}.`);
29
- }
30
- const raw = pkg['brightspot-ui'] ?? {};
31
- const warnings = [];
32
- const pkgName = pkg.name;
33
- const basePath = resolveBasePath(raw.basePath, pkgName, warnings);
34
- // Normalize leading `./` so downstream consumers (the proxy regex,
35
- // the dev-entry bootstrap import URL) get a canonical form.
36
- const entry = (raw.entry ?? DEFAULTS.entry).replace(/^\.\//, '');
37
- const format = resolveFormat(raw.format);
38
- const name = resolveIifeName(raw.name, format, pkgName, warnings);
39
- const output = {
40
- dir: raw.output?.dir ?? DEFAULTS.output.dir,
41
- jsName: raw.output?.jsName ?? DEFAULTS.output.jsName,
42
- cssName: raw.output?.cssName ?? DEFAULTS.output.cssName,
43
- };
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;
55
- }
56
- function resolveBasePath(explicit, pkgName, warnings) {
57
- if (explicit) {
58
- if (!explicit.startsWith('/') || !explicit.endsWith('/')) {
59
- throw new ConfigError(`basePath "${explicit}" must start and end with "/".`);
60
- }
61
- return explicit;
62
- }
63
- if (!pkgName) {
64
- // BREAKING (2.0): pre-2.0 builds silently fell back to /_resource/dist/
65
- // when both knobs were absent. We now exit so misconfigured monorepo
66
- // packages don't ship to the wrong URL. Migration paths below.
67
- throw new ConfigError('No basePath set in package.json#brightspot-ui.basePath, and no package.json#name to derive one from. ' +
68
- 'To migrate from @brightspot/ui-builder 1.x: ' +
69
- 'either set "name": "<slug>" in package.json (basePath will be inferred as /_resource/<slug>/cms/dist/), ' +
70
- 'or set "brightspot-ui": { "basePath": "/_resource/<slug>/cms/dist/" } explicitly.');
71
- }
72
- const inferred = `/_resource/${stripScope(pkgName)}/cms/dist/`;
73
- warnings.push(`basePath was not set — inferred "${inferred}" from package.json#name "${pkgName}". ` +
74
- `Set "brightspot-ui": { "basePath": "..." } in package.json to silence this warning.`);
75
- return inferred;
76
- }
77
- function resolveFormat(value) {
78
- if (value === undefined)
79
- return DEFAULTS.format;
80
- if (value !== 'es' && value !== 'iife') {
81
- throw new ConfigError(`format "${value}" must be "es" or "iife".`);
82
- }
83
- return value;
84
- }
85
- // Vite library mode requires `name` whenever formats include 'iife' or
86
- // 'umd' — it's a config-time check, not a function-of-whether-the-entry-
87
- // has-exports check. So when the consumer chooses iife and hasn't set
88
- // name, we derive a PascalCase identifier from package.json#name
89
- // (@brightspot/platform-tours -> PlatformTours) and warn. Returns
90
- // undefined when format is es (name is irrelevant). Throws when iife
91
- // is chosen but no valid identifier can be derived.
92
- function resolveIifeName(explicit, format, pkgName, warnings) {
93
- if (explicit !== undefined) {
94
- if (!isValidJsIdentifier(explicit)) {
95
- throw new ConfigError(`name "${explicit}" is not a valid JavaScript identifier — Vite library mode emits ` +
96
- `\`var ${explicit} = ...\` for iife output, which would produce a syntax error. ` +
97
- `Set "brightspot-ui": { "name": "..." } to a PascalCase identifier (e.g. "PlatformTours").`);
98
- }
99
- return explicit;
100
- }
101
- if (format !== 'iife')
102
- return undefined;
103
- const derived = deriveIifeName(pkgName);
104
- if (derived === null) {
105
- throw new ConfigError(`format "iife" requires a "name" — could not derive a valid JavaScript identifier from package.json#name "${pkgName ?? '<unset>'}". ` +
106
- `Set "brightspot-ui": { "name": "..." } in package.json (e.g. "PlatformTours").`);
107
- }
108
- warnings.push(`name was not set — derived "${derived}" from package.json#name "${pkgName}". ` +
109
- `Set "brightspot-ui": { "name": "..." } in package.json to silence this warning.`);
110
- return derived;
111
- }
112
- function deriveIifeName(pkgName) {
113
- if (!pkgName)
114
- return null;
115
- const parts = stripScope(pkgName).split(/[-_.]/).filter(Boolean);
116
- if (parts.length === 0)
117
- return null;
118
- const pascal = parts.map(p => p[0].toUpperCase() + p.slice(1)).join('');
119
- return isValidJsIdentifier(pascal) ? pascal : null;
120
- }
121
- // ASCII-only by design: matches what minifiers and bundlers handle
122
- // without surprises. ECMAScript permits a wider Unicode range.
123
- function isValidJsIdentifier(s) {
124
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s);
125
- }
126
- function stripScope(pkgName) {
127
- return pkgName.replace(/^@[^/]+\//, '');
128
- }
@@ -1 +0,0 @@
1
- export declare function resolveTarget(): string;
@@ -1,19 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { log } from './logger.js';
5
- const LOCAL_URL_PATH = path.join(os.homedir(), '.brightspot', 'local-url');
6
- const DEFAULT_TARGET = 'http://localhost';
7
- export function resolveTarget() {
8
- try {
9
- const url = fs.readFileSync(LOCAL_URL_PATH, 'utf8').trim();
10
- if (url) {
11
- log.info(`Proxy target: ${url}`);
12
- return url;
13
- }
14
- }
15
- catch {
16
- log.warn(`${LOCAL_URL_PATH} not found — falling back to ${DEFAULT_TARGET}`);
17
- }
18
- return DEFAULT_TARGET;
19
- }
@@ -1,2 +0,0 @@
1
- import type { PluginOption } from 'vite';
2
- export declare function loadAutoPlugins(cwd: string): Promise<PluginOption[]>;
@@ -1,41 +0,0 @@
1
- import path from 'node:path';
2
- import { pathToFileURL } from 'node:url';
3
- import { resolve as resolveEsm } from 'import-meta-resolve';
4
- import { log } from '../lib/logger.js';
5
- export async function loadAutoPlugins(cwd) {
6
- const candidates = await Promise.all([loadSveltePlugin(cwd)]);
7
- return candidates.filter(p => p !== null);
8
- }
9
- // Resolve peer-optional plugins from the consumer's cwd, not the builder's
10
- // own location. A bare `await import(specifier)` resolves relative to this
11
- // file, which only works under npm flat hoisting. It breaks for yarn 1
12
- // link, yarn 2+ workspaces, and pnpm strict — exactly the layouts
13
- // consumers use during development. We use ESM resolution rooted at the
14
- // consumer's package.json so the `import` condition is honored, unlike CJS
15
- // require.resolve which trips on ESM-only packages with an `exports`
16
- // block. Node's native import.meta.resolve has a 2-arg form that does
17
- // exactly this, but it remains flag-gated behind
18
- // --experimental-import-meta-resolve in Node 22; the import-meta-resolve
19
- // polyfill mirrors the same semantics without the flag. Only
20
- // module-not-found resolution failures become silent skips; other
21
- // resolution failures (broken package.json#exports, malformed
22
- // node_modules) and factory errors propagate so the consumer sees a real
23
- // diagnostic instead of a confusing "plugin not installed".
24
- async function loadOptionalPlugin(specifier, cwd, detectedMessage, invoke) {
25
- const parent = pathToFileURL(path.join(cwd, 'package.json')).href;
26
- let resolved;
27
- try {
28
- resolved = resolveEsm(specifier, parent);
29
- }
30
- catch (e) {
31
- if (e.code === 'ERR_MODULE_NOT_FOUND')
32
- return null;
33
- throw e;
34
- }
35
- log.info(detectedMessage);
36
- const mod = (await import(resolved));
37
- return invoke(mod);
38
- }
39
- function loadSveltePlugin(cwd) {
40
- return loadOptionalPlugin('@sveltejs/vite-plugin-svelte', cwd, 'Detected @sveltejs/vite-plugin-svelte — registering Svelte plugin.', mod => mod.svelte());
41
- }
@@ -1,16 +0,0 @@
1
- import type { Plugin } from 'vite';
2
- export interface DevEntryOptions {
3
- basePath: string;
4
- entry: string;
5
- jsName: string;
6
- cssName: string;
7
- }
8
- /**
9
- * Serves a dev bootstrap script at the bundle path so that Vite's
10
- * HMR client and the real entry point are loaded as ES modules.
11
- * Also serves an empty stylesheet at the css path since Vite injects
12
- * CSS via JS in dev. Both filenames mirror the build output knobs
13
- * (output.jsName / output.cssName) so a consumer who renames them
14
- * keeps dev/build parity.
15
- */
16
- export declare function devEntryPlugin({ basePath, entry, jsName, cssName }: DevEntryOptions): Plugin;
@@ -1,31 +0,0 @@
1
- /**
2
- * Serves a dev bootstrap script at the bundle path so that Vite's
3
- * HMR client and the real entry point are loaded as ES modules.
4
- * Also serves an empty stylesheet at the css path since Vite injects
5
- * CSS via JS in dev. Both filenames mirror the build output knobs
6
- * (output.jsName / output.cssName) so a consumer who renames them
7
- * keeps dev/build parity.
8
- */
9
- export function devEntryPlugin({ basePath, entry, jsName, cssName }) {
10
- const bundlePath = `${basePath}${jsName}`;
11
- const cssPath = `${basePath}${cssName}`;
12
- return {
13
- name: 'brightspot-dev-entry',
14
- configureServer(server) {
15
- server.middlewares.use((req, res, next) => {
16
- const pathname = req.url?.split('?')[0];
17
- if (pathname === bundlePath) {
18
- res.setHeader('Content-Type', 'application/javascript');
19
- res.end(`import('/@vite/client');import('/${entry}');`);
20
- return;
21
- }
22
- if (pathname === cssPath) {
23
- res.setHeader('Content-Type', 'text/css');
24
- res.end('');
25
- return;
26
- }
27
- next();
28
- });
29
- },
30
- };
31
- }
@@ -1,19 +0,0 @@
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;
@@ -1,125 +0,0 @@
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
- }
@@ -1,12 +0,0 @@
1
- import { type InlineConfig } from 'vite';
2
- import type { BuilderConfig } from '../lib/resolve-config.js';
3
- export interface DevConfigOptions {
4
- builder: BuilderConfig;
5
- target: string;
6
- port?: number;
7
- }
8
- export declare function createDevConfig({ builder, target, port }: DevConfigOptions): Promise<InlineConfig>;
9
- export interface BuildConfigOptions {
10
- builder: BuilderConfig;
11
- }
12
- export declare function createBuildConfig({ builder }: BuildConfigOptions): Promise<InlineConfig>;
@@ -1,141 +0,0 @@
1
- import path from 'node:path';
2
- import { loadConfigFromFile, mergeConfig } from 'vite';
3
- import { log } from '../lib/logger.js';
4
- import { loadAutoPlugins } from './auto-plugins.js';
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;}';
11
- export async function createDevConfig({ builder, target, port }) {
12
- const cwd = process.cwd();
13
- const { basePath, entry, output: { jsName, cssName }, } = builder;
14
- const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15
- const escaped = escapeRegex(basePath.replace(/^\//, ''));
16
- // entry.split('/')[0] yields the file itself (e.g. 'index.ts') for a
17
- // root-level entry, which never matches a URL — skip the segment in
18
- // that case so /index.ts isn't proxied to the upstream application.
19
- const entryParts = entry.split('/');
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';
25
- const [mkcert, autoPlugins] = await Promise.all([loadMkcertPlugin(), loadAutoPlugins(cwd)]);
26
- const base = {
27
- root: cwd,
28
- base: '/',
29
- plugins: [mkcert, devEntryPlugin({ basePath, entry, jsName, cssName }), ...autoPlugins],
30
- resolve: {
31
- preserveSymlinks: true,
32
- // Vite 8 native: replaces the deprecated vite-tsconfig-paths plugin.
33
- // Follows tsconfig `extends` chains and applies to all importers
34
- // (including .svelte / .vue) without the plugin's loose-mode dance.
35
- tsconfigPaths: true,
36
- },
37
- server: {
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' },
42
- proxy: {
43
- [`^/(?!(${escaped}${entryDirAlt}|@vite|@fs|node_modules))`]: {
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.
48
- autoRewrite: true,
49
- protocolRewrite: 'https',
50
- changeOrigin: true,
51
- configure: proxy => {
52
- proxy.on('proxyReq', proxyReq => {
53
- proxyReq.setHeader('x-forwarded-proto', targetScheme);
54
- proxyReq.setHeader('x-brightspot-dev', '1');
55
- });
56
- },
57
- },
58
- },
59
- },
60
- };
61
- return applyUserConfig(base, cwd, 'serve');
62
- }
63
- export async function createBuildConfig({ builder }) {
64
- const cwd = process.cwd();
65
- const { basePath, entry, format, name, output } = builder;
66
- const autoPlugins = await loadAutoPlugins(cwd);
67
- const base = {
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
- },
75
- plugins: [...autoPlugins],
76
- resolve: {
77
- preserveSymlinks: true,
78
- // Vite 8 native: replaces the deprecated vite-tsconfig-paths plugin.
79
- // Follows tsconfig `extends` chains and applies to all importers
80
- // (including .svelte / .vue) without the plugin's loose-mode dance.
81
- tsconfigPaths: true,
82
- },
83
- build: {
84
- cssCodeSplit: false,
85
- // Vite 7's `build.lib` implied a single bundle. Vite 8 dropped that
86
- // default, so any consumer with `await import(...)` in its graph
87
- // emits sidecar `*-<hash>.mjs` chunks that the toolkit's promised
88
- // two-file output (bundle.js + style.css) doesn't account for —
89
- // and that the gradle processResources task doesn't pattern-match
90
- // against, so chunks 404 in production. Default-off here restores
91
- // the contract; consumers who want code-splitting can opt back in
92
- // via Layer 3 (vite.config.ts → build.rollupOptions.output.
93
- // codeSplitting = true). Note: Rolldown deprecated the equivalent
94
- // `inlineDynamicImports: true` in favor of `codeSplitting: false`.
95
- rollupOptions: {
96
- output: {
97
- codeSplitting: false,
98
- },
99
- },
100
- },
101
- };
102
- const knobs = {
103
- base: basePath,
104
- build: {
105
- lib: {
106
- entry: path.resolve(cwd, entry),
107
- formats: [format],
108
- fileName: () => output.jsName,
109
- // Vite library mode requires `name` whenever format is 'iife'.
110
- // resolveConfig guarantees this is set when format is iife (explicit
111
- // or derived from package.json#name), so this guard is defensive
112
- // against a manually-constructed BuilderConfig that violates the
113
- // invariant — we propagate undefined to Vite rather than inject a
114
- // silent default; Vite then surfaces its own clear error.
115
- ...(format === 'iife' && name ? { name } : {}),
116
- },
117
- outDir: output.dir,
118
- rollupOptions: {
119
- output: {
120
- assetFileNames: () => output.cssName,
121
- ...(format === 'iife' ? { intro: AMD_GUARD_INTRO, outro: AMD_GUARD_OUTRO } : {}),
122
- },
123
- },
124
- },
125
- };
126
- return applyUserConfig(mergeConfig(base, knobs), cwd, 'build');
127
- }
128
- async function applyUserConfig(merged, cwd, command) {
129
- const loaded = await loadConfigFromFile({ command, mode: command === 'serve' ? 'development' : 'production' }, undefined, cwd);
130
- if (loaded)
131
- log.info(`Merging consumer Vite config from ${path.relative(cwd, loaded.path)}.`);
132
- const base = loaded ? mergeConfig(merged, loaded.config) : merged;
133
- // configFile: false prevents Vite from re-resolving + double-merging
134
- // the consumer's vite.config when the InlineConfig is passed to
135
- // viteBuild / createServer.
136
- return { ...base, configFile: false };
137
- }
138
- async function loadMkcertPlugin() {
139
- const mkcert = await import('vite-plugin-mkcert');
140
- return mkcert.default();
141
- }