@seanhogg/builderforce-webcontainers-core 2026.10.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/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/attribution.d.ts +22 -0
- package/dist/attribution.d.ts.map +1 -0
- package/dist/attribution.js +50 -0
- package/dist/attribution.js.map +1 -0
- package/dist/compileScript.d.ts +31 -0
- package/dist/compileScript.d.ts.map +1 -0
- package/dist/compileScript.js +75 -0
- package/dist/compileScript.js.map +1 -0
- package/dist/detectProject.d.ts +16 -0
- package/dist/detectProject.d.ts.map +1 -0
- package/dist/detectProject.js +49 -0
- package/dist/detectProject.js.map +1 -0
- package/dist/devServer.d.ts +46 -0
- package/dist/devServer.d.ts.map +1 -0
- package/dist/devServer.js +136 -0
- package/dist/devServer.js.map +1 -0
- package/dist/html.d.ts +17 -0
- package/dist/html.d.ts.map +1 -0
- package/dist/html.js +46 -0
- package/dist/html.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/mime.d.ts +3 -0
- package/dist/mime.d.ts.map +1 -0
- package/dist/mime.js +35 -0
- package/dist/mime.js.map +1 -0
- package/dist/packageCdn.d.ts +19 -0
- package/dist/packageCdn.d.ts.map +1 -0
- package/dist/packageCdn.js +29 -0
- package/dist/packageCdn.js.map +1 -0
- package/dist/paths.d.ts +12 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +36 -0
- package/dist/paths.js.map +1 -0
- package/dist/projectConfig.d.ts +36 -0
- package/dist/projectConfig.d.ts.map +1 -0
- package/dist/projectConfig.js +155 -0
- package/dist/projectConfig.js.map +1 -0
- package/dist/resolve.d.ts +19 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +62 -0
- package/dist/resolve.js.map +1 -0
- package/dist/rewriteImports.d.ts +11 -0
- package/dist/rewriteImports.d.ts.map +1 -0
- package/dist/rewriteImports.js +28 -0
- package/dist/rewriteImports.js.map +1 -0
- package/dist/styleModules.d.ts +12 -0
- package/dist/styleModules.d.ts.map +1 -0
- package/dist/styleModules.js +53 -0
- package/dist/styleModules.js.map +1 -0
- package/dist/transformer.d.ts +42 -0
- package/dist/transformer.d.ts.map +1 -0
- package/dist/transformer.js +39 -0
- package/dist/transformer.js.map +1 -0
- package/dist/vfs.d.ts +57 -0
- package/dist/vfs.d.ts.map +1 -0
- package/dist/vfs.js +114 -0
- package/dist/vfs.js.map +1 -0
- package/package.json +67 -0
- package/src/attribution.ts +57 -0
- package/src/compileScript.ts +87 -0
- package/src/detectProject.ts +66 -0
- package/src/devServer.ts +156 -0
- package/src/html.ts +58 -0
- package/src/index.ts +20 -0
- package/src/mime.ts +36 -0
- package/src/packageCdn.ts +48 -0
- package/src/paths.ts +37 -0
- package/src/projectConfig.ts +157 -0
- package/src/resolve.ts +60 -0
- package/src/rewriteImports.ts +26 -0
- package/src/styleModules.ts +57 -0
- package/src/transformer.ts +76 -0
- package/src/vfs.ts +135 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The compiler, as a PORT. The core never imports esbuild: the browser runtime
|
|
3
|
+
* plugs in esbuild-wasm, tests plug in native esbuild, and a fork can plug in SWC
|
|
4
|
+
* or anything else that turns TS/JSX into an ES module.
|
|
5
|
+
*/
|
|
6
|
+
export type Loader = 'js' | 'jsx' | 'ts' | 'tsx';
|
|
7
|
+
export interface TransformRequest {
|
|
8
|
+
loader: Loader;
|
|
9
|
+
/** The VFS path — shows up in error messages and source maps. */
|
|
10
|
+
sourcefile: string;
|
|
11
|
+
/** Compile-time replacements, esbuild `define` semantics (values are JS source). */
|
|
12
|
+
define: Record<string, string>;
|
|
13
|
+
jsx: {
|
|
14
|
+
runtime: 'automatic' | 'classic';
|
|
15
|
+
importSource: string;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface Transformer {
|
|
19
|
+
transform(code: string, request: TransformRequest): Promise<{
|
|
20
|
+
code: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
/** The subset of esbuild's (and esbuild-wasm's) API the adapter uses. */
|
|
24
|
+
export interface EsbuildLike {
|
|
25
|
+
transform(code: string, options: {
|
|
26
|
+
loader: Loader;
|
|
27
|
+
sourcefile: string;
|
|
28
|
+
format: 'esm';
|
|
29
|
+
target: string;
|
|
30
|
+
define: Record<string, string>;
|
|
31
|
+
jsx: 'automatic' | 'transform';
|
|
32
|
+
jsxImportSource?: string;
|
|
33
|
+
sourcemap: 'inline';
|
|
34
|
+
}): Promise<{
|
|
35
|
+
code: string;
|
|
36
|
+
}>;
|
|
37
|
+
}
|
|
38
|
+
/** Adapt esbuild or esbuild-wasm to {@link Transformer}. */
|
|
39
|
+
export declare function createEsbuildTransformer(esbuild: EsbuildLike): Transformer;
|
|
40
|
+
/** The loader for a source extension, or undefined when the file is not compiled. */
|
|
41
|
+
export declare function loaderFor(ext: string): Loader | undefined;
|
|
42
|
+
//# sourceMappingURL=transformer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformer.d.ts","sourceRoot":"","sources":["../src/transformer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,GAAG,EAAE;QAAE,OAAO,EAAE,WAAW,GAAG,SAAS,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/E;AAED,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC1B,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;QACP,MAAM,EAAE,MAAM,CAAC;QACf,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,KAAK,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAC/B,GAAG,EAAE,WAAW,GAAG,WAAW,CAAC;QAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,SAAS,EAAE,QAAQ,CAAC;KACrB,GACA,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9B;AAED,4DAA4D;AAC5D,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,CAe1E;AAED,qFAAqF;AACrF,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAmBzD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Adapt esbuild or esbuild-wasm to {@link Transformer}. */
|
|
2
|
+
export function createEsbuildTransformer(esbuild) {
|
|
3
|
+
return {
|
|
4
|
+
transform(code, request) {
|
|
5
|
+
return esbuild.transform(code, {
|
|
6
|
+
loader: request.loader,
|
|
7
|
+
sourcefile: request.sourcefile,
|
|
8
|
+
format: 'esm',
|
|
9
|
+
target: 'es2020',
|
|
10
|
+
define: request.define,
|
|
11
|
+
jsx: request.jsx.runtime === 'automatic' ? 'automatic' : 'transform',
|
|
12
|
+
...(request.jsx.runtime === 'automatic' ? { jsxImportSource: request.jsx.importSource } : {}),
|
|
13
|
+
sourcemap: 'inline',
|
|
14
|
+
});
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** The loader for a source extension, or undefined when the file is not compiled. */
|
|
19
|
+
export function loaderFor(ext) {
|
|
20
|
+
switch (ext) {
|
|
21
|
+
case '.ts':
|
|
22
|
+
case '.mts':
|
|
23
|
+
case '.cts':
|
|
24
|
+
return 'ts';
|
|
25
|
+
case '.tsx':
|
|
26
|
+
return 'tsx';
|
|
27
|
+
// `.js` gets the JSX loader: Create React App puts JSX in `.js` files, and the
|
|
28
|
+
// JSX loader accepts plain JavaScript unchanged.
|
|
29
|
+
case '.js':
|
|
30
|
+
case '.jsx':
|
|
31
|
+
return 'jsx';
|
|
32
|
+
case '.mjs':
|
|
33
|
+
case '.cjs':
|
|
34
|
+
return 'js';
|
|
35
|
+
default:
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=transformer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transformer.js","sourceRoot":"","sources":["../src/transformer.ts"],"names":[],"mappings":"AAqCA,4DAA4D;AAC5D,MAAM,UAAU,wBAAwB,CAAC,OAAoB;IAC3D,OAAO;QACL,SAAS,CAAC,IAAI,EAAE,OAAO;YACrB,OAAO,OAAO,CAAC,SAAS,CAAC,IAAI,EAAE;gBAC7B,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,UAAU,EAAE,OAAO,CAAC,UAAU;gBAC9B,MAAM,EAAE,KAAK;gBACb,MAAM,EAAE,QAAQ;gBAChB,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW;gBACpE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,WAAW,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7F,SAAS,EAAE,QAAQ;aACpB,CAAC,CAAC;QACL,CAAC;KACF,CAAC;AACJ,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,KAAK,CAAC;QACX,KAAK,MAAM,CAAC;QACZ,KAAK,MAAM;YACT,OAAO,IAAI,CAAC;QACd,KAAK,MAAM;YACT,OAAO,KAAK,CAAC;QACf,+EAA+E;QAC/E,iDAAiD;QACjD,KAAK,KAAK,CAAC;QACX,KAAK,MAAM;YACT,OAAO,KAAK,CAAC;QACf,KAAK,MAAM,CAAC;QACZ,KAAK,MAAM;YACT,OAAO,IAAI,CAAC;QACd;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC"}
|
package/dist/vfs.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type FileContent = string | Uint8Array;
|
|
2
|
+
/** The nested tree shape `@webcontainer/api`'s `mount()` takes — accepted as-is. */
|
|
3
|
+
export interface FileSystemTree {
|
|
4
|
+
[name: string]: {
|
|
5
|
+
file: {
|
|
6
|
+
contents: FileContent;
|
|
7
|
+
};
|
|
8
|
+
} | {
|
|
9
|
+
directory: FileSystemTree;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
/** A flat `{ 'src/main.tsx': '...' }` map, the other shape callers commonly hold. */
|
|
13
|
+
export type FlatFiles = Record<string, FileContent>;
|
|
14
|
+
export interface FsChange {
|
|
15
|
+
type: 'write' | 'remove';
|
|
16
|
+
path: string;
|
|
17
|
+
/** For a write: the file did not exist before. A new file can change how imports resolve. */
|
|
18
|
+
created?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* An in-memory file system. Files only — a directory exists while it contains a
|
|
22
|
+
* file, which keeps the store a single map and makes "is this a directory?" a
|
|
23
|
+
* prefix check rather than a second structure to keep consistent.
|
|
24
|
+
*
|
|
25
|
+
* Every write bumps a per-file version, so a transform cache can key on
|
|
26
|
+
* (path, version) instead of hashing contents.
|
|
27
|
+
*/
|
|
28
|
+
export declare class VirtualFileSystem {
|
|
29
|
+
private readonly files;
|
|
30
|
+
private readonly versions;
|
|
31
|
+
private readonly listeners;
|
|
32
|
+
private clock;
|
|
33
|
+
writeFile(path: string, contents: FileContent): void;
|
|
34
|
+
readFile(path: string): FileContent | undefined;
|
|
35
|
+
readText(path: string): string | undefined;
|
|
36
|
+
isFile(path: string): boolean;
|
|
37
|
+
isDirectory(path: string): boolean;
|
|
38
|
+
exists(path: string): boolean;
|
|
39
|
+
/** Direct children of a directory (files and sub-directories), sorted. */
|
|
40
|
+
readdir(path: string): string[];
|
|
41
|
+
/** Every file path, sorted. */
|
|
42
|
+
list(): string[];
|
|
43
|
+
/** Remove a file, or a directory and everything under it. */
|
|
44
|
+
rm(path: string): void;
|
|
45
|
+
/** The file's version (bumped on every write), or undefined when absent. */
|
|
46
|
+
version(path: string): number | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Write many files at once. Takes either the nested WebContainer tree or a flat
|
|
49
|
+
* path→contents map — or a mix, since each entry is judged on its own.
|
|
50
|
+
*/
|
|
51
|
+
mount(files: FileSystemTree | FlatFiles, at?: string): void;
|
|
52
|
+
/** Flat snapshot keyed without the leading slash (`src/main.tsx`). */
|
|
53
|
+
toFlat(): FlatFiles;
|
|
54
|
+
watch(listener: (change: FsChange) => void): () => void;
|
|
55
|
+
private emit;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=vfs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vfs.d.ts","sourceRoot":"","sources":["../src/vfs.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,UAAU,CAAC;AAE9C,oFAAoF;AACpF,MAAM,WAAW,cAAc;IAC7B,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,IAAI,EAAE;YAAE,QAAQ,EAAE,WAAW,CAAA;SAAE,CAAA;KAAE,GAAG;QAAE,SAAS,EAAE,cAAc,CAAA;KAAE,CAAC;CACrF;AAED,qFAAqF;AACrF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAEpD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,6FAA6F;IAC7F,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAID;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;IACxD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA6B;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyC;IACnE,OAAO,CAAC,KAAK,CAAK;IAElB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,GAAG,IAAI;IAQpD,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS;IAI/C,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAM1C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI7B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAQlC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI7B,0EAA0E;IAC1E,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE;IAa/B,+BAA+B;IAC/B,IAAI,IAAI,MAAM,EAAE;IAIhB,6DAA6D;IAC7D,EAAE,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAYtB,4EAA4E;IAC5E,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIzC;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,EAAE,EAAE,SAAM,GAAG,IAAI;IASxD,sEAAsE;IACtE,MAAM,IAAI,SAAS;IAMnB,KAAK,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI;IAKvD,OAAO,CAAC,IAAI;CAGb"}
|
package/dist/vfs.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { normalizePath, join } from './paths.js';
|
|
2
|
+
const decoder = new TextDecoder();
|
|
3
|
+
/**
|
|
4
|
+
* An in-memory file system. Files only — a directory exists while it contains a
|
|
5
|
+
* file, which keeps the store a single map and makes "is this a directory?" a
|
|
6
|
+
* prefix check rather than a second structure to keep consistent.
|
|
7
|
+
*
|
|
8
|
+
* Every write bumps a per-file version, so a transform cache can key on
|
|
9
|
+
* (path, version) instead of hashing contents.
|
|
10
|
+
*/
|
|
11
|
+
export class VirtualFileSystem {
|
|
12
|
+
files = new Map();
|
|
13
|
+
versions = new Map();
|
|
14
|
+
listeners = new Set();
|
|
15
|
+
clock = 0;
|
|
16
|
+
writeFile(path, contents) {
|
|
17
|
+
const key = normalizePath(path);
|
|
18
|
+
const created = !this.files.has(key);
|
|
19
|
+
this.files.set(key, contents);
|
|
20
|
+
this.versions.set(key, ++this.clock);
|
|
21
|
+
this.emit({ type: 'write', path: key, created });
|
|
22
|
+
}
|
|
23
|
+
readFile(path) {
|
|
24
|
+
return this.files.get(normalizePath(path));
|
|
25
|
+
}
|
|
26
|
+
readText(path) {
|
|
27
|
+
const contents = this.readFile(path);
|
|
28
|
+
if (contents === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
return typeof contents === 'string' ? contents : decoder.decode(contents);
|
|
31
|
+
}
|
|
32
|
+
isFile(path) {
|
|
33
|
+
return this.files.has(normalizePath(path));
|
|
34
|
+
}
|
|
35
|
+
isDirectory(path) {
|
|
36
|
+
const key = normalizePath(path);
|
|
37
|
+
if (key === '/')
|
|
38
|
+
return true;
|
|
39
|
+
const prefix = key + '/';
|
|
40
|
+
for (const file of this.files.keys())
|
|
41
|
+
if (file.startsWith(prefix))
|
|
42
|
+
return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
exists(path) {
|
|
46
|
+
return this.isFile(path) || this.isDirectory(path);
|
|
47
|
+
}
|
|
48
|
+
/** Direct children of a directory (files and sub-directories), sorted. */
|
|
49
|
+
readdir(path) {
|
|
50
|
+
const key = normalizePath(path);
|
|
51
|
+
const prefix = key === '/' ? '/' : key + '/';
|
|
52
|
+
const names = new Set();
|
|
53
|
+
for (const file of this.files.keys()) {
|
|
54
|
+
if (!file.startsWith(prefix))
|
|
55
|
+
continue;
|
|
56
|
+
const rest = file.slice(prefix.length);
|
|
57
|
+
const name = rest.split('/')[0];
|
|
58
|
+
if (name)
|
|
59
|
+
names.add(name);
|
|
60
|
+
}
|
|
61
|
+
return [...names].sort();
|
|
62
|
+
}
|
|
63
|
+
/** Every file path, sorted. */
|
|
64
|
+
list() {
|
|
65
|
+
return [...this.files.keys()].sort();
|
|
66
|
+
}
|
|
67
|
+
/** Remove a file, or a directory and everything under it. */
|
|
68
|
+
rm(path) {
|
|
69
|
+
const key = normalizePath(path);
|
|
70
|
+
const prefix = key === '/' ? '/' : key + '/';
|
|
71
|
+
for (const file of [...this.files.keys()]) {
|
|
72
|
+
if (file === key || file.startsWith(prefix)) {
|
|
73
|
+
this.files.delete(file);
|
|
74
|
+
this.versions.delete(file);
|
|
75
|
+
this.emit({ type: 'remove', path: file });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** The file's version (bumped on every write), or undefined when absent. */
|
|
80
|
+
version(path) {
|
|
81
|
+
return this.versions.get(normalizePath(path));
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Write many files at once. Takes either the nested WebContainer tree or a flat
|
|
85
|
+
* path→contents map — or a mix, since each entry is judged on its own.
|
|
86
|
+
*/
|
|
87
|
+
mount(files, at = '/') {
|
|
88
|
+
for (const [name, entry] of Object.entries(files)) {
|
|
89
|
+
const path = join(at, name);
|
|
90
|
+
if (typeof entry === 'string' || entry instanceof Uint8Array)
|
|
91
|
+
this.writeFile(path, entry);
|
|
92
|
+
else if ('file' in entry)
|
|
93
|
+
this.writeFile(path, entry.file.contents);
|
|
94
|
+
else
|
|
95
|
+
this.mount(entry.directory, path);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Flat snapshot keyed without the leading slash (`src/main.tsx`). */
|
|
99
|
+
toFlat() {
|
|
100
|
+
const out = {};
|
|
101
|
+
for (const [path, contents] of this.files)
|
|
102
|
+
out[path.slice(1)] = contents;
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
watch(listener) {
|
|
106
|
+
this.listeners.add(listener);
|
|
107
|
+
return () => this.listeners.delete(listener);
|
|
108
|
+
}
|
|
109
|
+
emit(change) {
|
|
110
|
+
for (const listener of this.listeners)
|
|
111
|
+
listener(change);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=vfs.js.map
|
package/dist/vfs.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vfs.js","sourceRoot":"","sources":["../src/vfs.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@seanhogg/builderforce-webcontainers-core",
|
|
3
|
+
"version": "2026.10.0",
|
|
4
|
+
"description": "BuilderForce WebContainers — core. A runtime-agnostic in-browser dev server: virtual file system, module resolution, import rewriting to a package CDN, CSS/JSON/asset modules and project detection. No COOP/COEP required.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"webcontainer",
|
|
24
|
+
"in-browser",
|
|
25
|
+
"dev-server",
|
|
26
|
+
"esbuild",
|
|
27
|
+
"vite",
|
|
28
|
+
"sandbox",
|
|
29
|
+
"preview",
|
|
30
|
+
"builderforce"
|
|
31
|
+
],
|
|
32
|
+
"author": {
|
|
33
|
+
"name": "Sean Hogg",
|
|
34
|
+
"email": "seanhogg@gmail.com",
|
|
35
|
+
"url": "https://builderforce.ai"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/SeanHogg/builderforce-webcontainers.git",
|
|
41
|
+
"directory": "packages/core"
|
|
42
|
+
},
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/SeanHogg/builderforce-webcontainers/issues"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/SeanHogg/builderforce-webcontainers/tree/main/packages/core#readme",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18.0.0"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"registry": "https://registry.npmjs.org"
|
|
53
|
+
},
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"es-module-lexer": "^1.5.4"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"esbuild": "^0.24.0",
|
|
59
|
+
"typescript": "^5.5.0",
|
|
60
|
+
"vitest": "^2.1.0"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsc -p tsconfig.build.json",
|
|
64
|
+
"typecheck": "tsc --noEmit",
|
|
65
|
+
"test": "vitest run"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "Built with Builderforce.ai" badge, on by default in every preview.
|
|
3
|
+
*
|
|
4
|
+
* It is a DEFAULT, not a licence condition: the project stays MIT, and a host turns
|
|
5
|
+
* it off with `attribution: false`. Keeping it on is how this runtime pays for
|
|
6
|
+
* itself, and the README asks forks to leave it.
|
|
7
|
+
*
|
|
8
|
+
* The badge lives in a shadow root, so the previewed app's CSS can't restyle or hide
|
|
9
|
+
* it by accident and the badge's styles can't leak into the app. It follows the
|
|
10
|
+
* system light/dark preference and sits in the bottom-right corner, small enough
|
|
11
|
+
* not to cover app UI on a phone-width screen.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const ATTRIBUTION_URL = 'https://builderforce.ai/?utm_source=webcontainers&utm_medium=badge&utm_campaign=preview';
|
|
15
|
+
export const ATTRIBUTION_LABEL = 'Built with Builderforce.ai';
|
|
16
|
+
|
|
17
|
+
export interface AttributionOptions {
|
|
18
|
+
href?: string;
|
|
19
|
+
label?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function attributionScript(options: AttributionOptions = {}): string {
|
|
23
|
+
const href = JSON.stringify(options.href ?? ATTRIBUTION_URL);
|
|
24
|
+
const label = JSON.stringify(options.label ?? ATTRIBUTION_LABEL);
|
|
25
|
+
const css = [
|
|
26
|
+
':host{all:initial;position:fixed;right:8px;bottom:8px;z-index:2147483647}',
|
|
27
|
+
'a{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;',
|
|
28
|
+
'font:500 12px/1.4 system-ui,-apple-system,Segoe UI,sans-serif;text-decoration:none;',
|
|
29
|
+
'background:rgba(255,255,255,.92);color:#1f2328;border:1px solid rgba(31,35,40,.15);',
|
|
30
|
+
'box-shadow:0 1px 3px rgba(0,0,0,.12)}',
|
|
31
|
+
'a:hover{text-decoration:underline}',
|
|
32
|
+
'@media (prefers-color-scheme:dark){a{background:rgba(22,27,34,.92);color:#e6edf3;border-color:rgba(230,237,243,.18)}}',
|
|
33
|
+
'@media print{:host{display:none}}',
|
|
34
|
+
].join('');
|
|
35
|
+
return `<script>(function(){
|
|
36
|
+
if (document.getElementById('bfwc-attribution')) return;
|
|
37
|
+
var mount = function () {
|
|
38
|
+
var host = document.createElement('bfwc-attribution');
|
|
39
|
+
host.id = 'bfwc-attribution';
|
|
40
|
+
var root = host.attachShadow({ mode: 'closed' });
|
|
41
|
+
var style = document.createElement('style');
|
|
42
|
+
style.textContent = ${JSON.stringify(css)};
|
|
43
|
+
var a = document.createElement('a');
|
|
44
|
+
a.href = ${href}; a.target = '_blank'; a.rel = 'noopener';
|
|
45
|
+
a.textContent = ${label};
|
|
46
|
+
root.appendChild(style); root.appendChild(a);
|
|
47
|
+
document.body.appendChild(host);
|
|
48
|
+
};
|
|
49
|
+
if (document.body) mount(); else addEventListener('DOMContentLoaded', mount);
|
|
50
|
+
})();</script>`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Append the badge before `</body>` (or at the end of a fragment). */
|
|
54
|
+
export function injectAttribution(html: string, options?: AttributionOptions): string {
|
|
55
|
+
const badge = attributionScript(options);
|
|
56
|
+
return /<\/body>/i.test(html) ? html.replace(/<\/body>(?![\s\S]*<\/body>)/i, `${badge}\n</body>`) : html + badge;
|
|
57
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { VirtualFileSystem } from './vfs.js';
|
|
2
|
+
import type { ProjectConfig } from './projectConfig.js';
|
|
3
|
+
import type { PackageCdn } from './packageCdn.js';
|
|
4
|
+
import type { Transformer } from './transformer.js';
|
|
5
|
+
import { loaderFor } from './transformer.js';
|
|
6
|
+
import { dirname, extname } from './paths.js';
|
|
7
|
+
import { isBareSpecifier, resolveAlias, resolveLocal, splitPackageSpecifier } from './resolve.js';
|
|
8
|
+
import { rewriteImports } from './rewriteImports.js';
|
|
9
|
+
|
|
10
|
+
export interface CompileContext {
|
|
11
|
+
fs: VirtualFileSystem;
|
|
12
|
+
config: ProjectConfig;
|
|
13
|
+
cdn: PackageCdn;
|
|
14
|
+
transformer: Transformer;
|
|
15
|
+
/** Preview base URL, ending in `/`. */
|
|
16
|
+
base: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Vite-style import queries this runtime honours. */
|
|
20
|
+
export type ImportQuery = 'import' | 'raw' | 'url';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The URL a resolved VFS file is imported by. Scripts are served as-is; CSS and
|
|
24
|
+
* JSON need the `?import` JS wrapper; anything else imported from JS (images,
|
|
25
|
+
* fonts) exports its URL, as in Vite.
|
|
26
|
+
*/
|
|
27
|
+
export function moduleUrl(base: string, path: string, query?: ImportQuery): string {
|
|
28
|
+
const url = base + path.slice(1);
|
|
29
|
+
if (query === 'raw' || query === 'url') return `${url}?${query}`;
|
|
30
|
+
const ext = extname(path);
|
|
31
|
+
if (loaderFor(ext)) return url;
|
|
32
|
+
if (ext === '.css' || ext === '.json') return `${url}?import`;
|
|
33
|
+
return `${url}?url`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function splitQuery(specifier: string): { path: string; query?: ImportQuery } {
|
|
37
|
+
const at = specifier.indexOf('?');
|
|
38
|
+
if (at < 0) return { path: specifier };
|
|
39
|
+
const query = specifier.slice(at + 1);
|
|
40
|
+
return { path: specifier.slice(0, at), query: query === 'raw' || query === 'url' ? query : undefined };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Map one import specifier to the URL the browser should fetch. Undefined leaves
|
|
45
|
+
* it unchanged: URLs, `node:` builtins, and local paths that do not resolve (the
|
|
46
|
+
* browser's 404 then surfaces through the error bridge).
|
|
47
|
+
*/
|
|
48
|
+
export function mapSpecifier(ctx: CompileContext, fromDir: string, specifier: string): string | undefined {
|
|
49
|
+
if (/^(https?:|data:|blob:|node:)/i.test(specifier)) return undefined;
|
|
50
|
+
const { path, query } = splitQuery(specifier);
|
|
51
|
+
|
|
52
|
+
if (isBareSpecifier(path)) {
|
|
53
|
+
const aliased = resolveAlias(ctx.fs, ctx.config.aliases, path);
|
|
54
|
+
if (aliased) return moduleUrl(ctx.base, aliased, query);
|
|
55
|
+
const { name, subpath } = splitPackageSpecifier(path);
|
|
56
|
+
return ctx.cdn.urlFor(name, subpath);
|
|
57
|
+
}
|
|
58
|
+
const resolved = resolveLocal(ctx.fs, fromDir, path);
|
|
59
|
+
return resolved ? moduleUrl(ctx.base, resolved, query) : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Compile-time constants: Vite's `import.meta.env` and CRA's `process.env.REACT_APP_*`. */
|
|
63
|
+
export function buildDefine(config: ProjectConfig, base: string): Record<string, string> {
|
|
64
|
+
const env: Record<string, unknown> = { MODE: 'development', DEV: true, PROD: false, SSR: false, BASE_URL: base };
|
|
65
|
+
const define: Record<string, string> = { 'process.env.NODE_ENV': '"development"' };
|
|
66
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
67
|
+
if (key.startsWith('VITE_')) env[key] = value;
|
|
68
|
+
if (key.startsWith('VITE_') || key.startsWith('REACT_APP_')) define[`process.env.${key}`] = JSON.stringify(value);
|
|
69
|
+
}
|
|
70
|
+
define['import.meta.env'] = JSON.stringify(env);
|
|
71
|
+
return define;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** TS/JSX → an ES module whose imports point at preview and CDN URLs. */
|
|
75
|
+
export async function compileScript(ctx: CompileContext, path: string): Promise<string> {
|
|
76
|
+
const loader = loaderFor(extname(path));
|
|
77
|
+
if (!loader) throw new Error(`Not a script: ${path}`);
|
|
78
|
+
const source = ctx.fs.readText(path) ?? '';
|
|
79
|
+
const { code } = await ctx.transformer.transform(source, {
|
|
80
|
+
loader,
|
|
81
|
+
sourcefile: path,
|
|
82
|
+
define: buildDefine(ctx.config, ctx.base),
|
|
83
|
+
jsx: ctx.config.jsx,
|
|
84
|
+
});
|
|
85
|
+
const fromDir = dirname(path);
|
|
86
|
+
return rewriteImports(code, (specifier) => mapSpecifier(ctx, fromDir, specifier));
|
|
87
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { VirtualFileSystem } from './vfs.js';
|
|
2
|
+
import { RESOLVE_EXTENSIONS } from './resolve.js';
|
|
3
|
+
|
|
4
|
+
export type ProjectKind = 'vite' | 'cra' | 'static';
|
|
5
|
+
|
|
6
|
+
export interface ProjectProfile {
|
|
7
|
+
supported: boolean;
|
|
8
|
+
kind?: ProjectKind;
|
|
9
|
+
/** The HTML document served at `/`. */
|
|
10
|
+
htmlPath?: string;
|
|
11
|
+
/** Script injected when the HTML has no module entry of its own (CRA). */
|
|
12
|
+
entry?: string;
|
|
13
|
+
/** Files served from the site root, Vite/CRA `public/` convention. */
|
|
14
|
+
publicDir: string;
|
|
15
|
+
/** Why the project needs a full Node runtime instead — shown to the user. */
|
|
16
|
+
reason?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Frameworks that need a server or a compiler this runtime does not have yet.
|
|
21
|
+
* Data, not branches: supporting one later means deleting its row.
|
|
22
|
+
*/
|
|
23
|
+
const NEEDS_NODE: ReadonlyArray<{ dependency: string; reason: string }> = [
|
|
24
|
+
{ dependency: 'next', reason: 'Next.js needs its Node server.' },
|
|
25
|
+
{ dependency: 'nuxt', reason: 'Nuxt needs its Node server.' },
|
|
26
|
+
{ dependency: '@remix-run/dev', reason: 'Remix needs its Node server.' },
|
|
27
|
+
{ dependency: '@sveltejs/kit', reason: 'SvelteKit needs its Node server.' },
|
|
28
|
+
{ dependency: 'astro', reason: 'Astro needs its build pipeline.' },
|
|
29
|
+
{ dependency: 'svelte', reason: 'Svelte components need the Svelte compiler.' },
|
|
30
|
+
{ dependency: 'vue', reason: 'Vue single-file components need the Vue compiler.' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const SERVER_ONLY = ['express', 'fastify', 'koa', '@nestjs/core', '@hapi/hapi'];
|
|
34
|
+
|
|
35
|
+
function readDependencyNames(fs: VirtualFileSystem): Set<string> {
|
|
36
|
+
try {
|
|
37
|
+
const pkg = JSON.parse(fs.readText('/package.json') ?? '{}') as Record<string, Record<string, string> | undefined>;
|
|
38
|
+
return new Set([...Object.keys(pkg.dependencies ?? {}), ...Object.keys(pkg.devDependencies ?? {})]);
|
|
39
|
+
} catch {
|
|
40
|
+
return new Set();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findEntry(fs: VirtualFileSystem): string | undefined {
|
|
45
|
+
for (const stem of ['/src/index', '/src/main']) {
|
|
46
|
+
for (const ext of RESOLVE_EXTENSIONS) if (fs.isFile(stem + ext)) return stem + ext;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function detectProject(fs: VirtualFileSystem): ProjectProfile {
|
|
52
|
+
const deps = readDependencyNames(fs);
|
|
53
|
+
const blocker = NEEDS_NODE.find((row) => deps.has(row.dependency));
|
|
54
|
+
if (blocker) return { supported: false, publicDir: '/public', reason: blocker.reason };
|
|
55
|
+
|
|
56
|
+
if (fs.isFile('/index.html')) {
|
|
57
|
+
return { supported: true, kind: deps.has('vite') ? 'vite' : 'static', htmlPath: '/index.html', publicDir: '/public' };
|
|
58
|
+
}
|
|
59
|
+
if (fs.isFile('/public/index.html')) {
|
|
60
|
+
return { supported: true, kind: 'cra', htmlPath: '/public/index.html', entry: findEntry(fs), publicDir: '/public' };
|
|
61
|
+
}
|
|
62
|
+
if (SERVER_ONLY.some((name) => deps.has(name))) {
|
|
63
|
+
return { supported: false, publicDir: '/public', reason: 'This is a Node server app with no browser entry page.' };
|
|
64
|
+
}
|
|
65
|
+
return { supported: false, publicDir: '/public', reason: 'No index.html found at the project root or in public/.' };
|
|
66
|
+
}
|