@volter/live 0.5.210 → 0.5.212
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/BUNDLED_NOTICES +13 -13
- package/README.md +7 -11
- package/dist/chunk-7SZ3C4CC.js +768 -0
- package/dist/chunk-7SZ3C4CC.js.map +7 -0
- package/dist/chunk-7WKVJGHZ.js +15 -0
- package/dist/chunk-7WKVJGHZ.js.map +7 -0
- package/dist/chunk-SDIWRX5X.js +189 -0
- package/dist/chunk-SDIWRX5X.js.map +7 -0
- package/dist/chunk-USXOIBVX.js +81 -0
- package/dist/chunk-USXOIBVX.js.map +7 -0
- package/dist/chunk-Y5W77HYT.js +123 -0
- package/dist/chunk-Y5W77HYT.js.map +7 -0
- package/dist/chunk-YPAOTPIF.js +202 -0
- package/dist/chunk-YPAOTPIF.js.map +7 -0
- package/dist/client.d.ts +370 -0
- package/dist/client.js +9 -0
- package/dist/client.js.map +7 -0
- package/dist/discovery.d.ts +32 -0
- package/dist/discovery.js +19 -0
- package/dist/discovery.js.map +7 -0
- package/dist/editor-document.d.ts +12 -47
- package/dist/editor.d.ts +34 -181
- package/dist/http-transport.d.ts +6 -0
- package/dist/index.d.ts +1 -5
- package/dist/index.js +76 -1722
- package/dist/index.js.map +4 -4
- package/dist/installed-package.d.ts +6 -0
- package/dist/installed-package.js +7 -0
- package/dist/installed-package.js.map +7 -0
- package/dist/lazy-proxy.d.ts +11 -11
- package/dist/member-hints.d.ts +6 -9
- package/dist/product-create.d.ts +29 -0
- package/dist/product-create.js +13 -0
- package/dist/product-create.js.map +7 -0
- package/dist/product-locator.d.ts +100 -0
- package/dist/product-locator.js +25 -0
- package/dist/product-locator.js.map +7 -0
- package/dist/registry-format.d.ts +44 -0
- package/dist/registry-format.js +21 -0
- package/dist/registry-format.js.map +7 -0
- package/dist/session.d.ts +10 -3
- package/dist/singleton.d.ts +1 -13
- package/dist/volter-manifest.d.ts +55 -0
- package/dist/volter-manifest.js +9 -0
- package/dist/volter-manifest.js.map +7 -0
- package/dist/workbench-locator.d.ts +84 -0
- package/dist/workbench-locator.js +416 -0
- package/dist/workbench-locator.js.map +7 -0
- package/package.json +35 -3
- package/src/client.ts +1052 -0
- package/src/discovery.ts +105 -0
- package/src/editor-document.ts +12 -49
- package/src/editor.ts +45 -212
- package/src/http-transport.ts +19 -0
- package/src/index.ts +3 -7
- package/src/installed-package.ts +15 -0
- package/src/lazy-proxy.ts +11 -11
- package/src/member-hints.ts +8 -12
- package/src/product-create.ts +38 -0
- package/src/product-locator.ts +327 -0
- package/src/registry-format.ts +185 -0
- package/src/session.ts +38 -66
- package/src/singleton.ts +1 -16
- package/src/volter-manifest.ts +97 -0
- package/src/workbench-locator.ts +625 -0
- package/dist/tools.d.ts +0 -23
- package/src/tools.ts +0 -45
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/** Attach to an existing editor session; this package never launches one. */
|
|
2
|
-
import { EditorClient } from '
|
|
2
|
+
import { EditorClient } from './client.js';
|
|
3
3
|
import { LiveEditor } from './editor.js';
|
|
4
|
-
import { LiveTools } from './tools.js';
|
|
5
4
|
import { lazyChainProxy } from './lazy-proxy.js';
|
|
6
5
|
import { resolveSession, type ResolvedSession, type SessionResolutionDeps } from './session.js';
|
|
7
6
|
import { createLazySession } from './singleton.js';
|
|
@@ -10,22 +9,20 @@ import { editorMemberHint } from './member-hints.js';
|
|
|
10
9
|
export { LiveEditor, inferAssetKind, type PanelName } from './editor.js';
|
|
11
10
|
export { LiveEditorDocument } from './editor-document.js';
|
|
12
11
|
export type { DocumentGestureOptions, DocumentKeyOptions, DocumentPasteOptions } from './editor-document.js';
|
|
13
|
-
export { LiveTools } from './tools.js';
|
|
14
12
|
export { editorMemberHint, withEditorMemberHints } from './member-hints.js';
|
|
15
|
-
export { resolveSession, findProjectRootFrom, NO_SESSION_FOR_PROJECT } from './session.js';
|
|
13
|
+
export { resolveSession, findProjectRootFrom, NO_SESSION_FOR_PROJECT, SESSION_MANIFEST_UNREADABLE } from './session.js';
|
|
16
14
|
export type { ResolvedSession, SessionResolutionDeps, SessionListingTransport, ProjectSessionHint } from './session.js';
|
|
17
15
|
export type { ActiveDocumentCapture, EditorView, PresentedEditorView } from '@volter/sdk';
|
|
18
16
|
|
|
19
17
|
export interface LiveBindings {
|
|
20
18
|
editor: LiveEditor;
|
|
21
|
-
tools: LiveTools;
|
|
22
19
|
}
|
|
23
20
|
export interface LiveSession extends LiveBindings {
|
|
24
21
|
session: ResolvedSession;
|
|
25
22
|
}
|
|
26
23
|
function bindTo(port: number): LiveBindings {
|
|
27
24
|
const client = new EditorClient({ url: `http://127.0.0.1:${port}` });
|
|
28
|
-
return { editor: new LiveEditor(client)
|
|
25
|
+
return { editor: new LiveEditor(client) };
|
|
29
26
|
}
|
|
30
27
|
/** Inspect the callable surface without connecting; calls fail on port zero. */
|
|
31
28
|
export function unconnectedBindings(): LiveBindings { return bindTo(0); }
|
|
@@ -37,4 +34,3 @@ export async function connect(projectDir?: string, deps?: SessionResolutionDeps)
|
|
|
37
34
|
}
|
|
38
35
|
const lazySession = createLazySession(() => connect());
|
|
39
36
|
export const editor: LiveEditor = lazyChainProxy<LiveEditor>(() => lazySession.ensure().then(s => s.editor), [], editorMemberHint);
|
|
40
|
-
export const tools: LiveTools = lazyChainProxy<LiveTools>(() => lazySession.ensure().then(s => s.tools));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The real directory of installed package `name` as Node finds it from `fromDir`: up the `node_modules` folders, or
|
|
6
|
+
* null when none holds it. It is found by its folder rather than by resolving `<name>/package.json`, which fails for a
|
|
7
|
+
* package whose `exports` does not list that file (`@volter/sdk`).
|
|
8
|
+
*/
|
|
9
|
+
export function installedPackageDir(fromDir: string, name: string): string | null {
|
|
10
|
+
for (let dir = fromDir; ; dir = dirname(dir)) {
|
|
11
|
+
const candidate = join(dir, 'node_modules', ...name.split('/'));
|
|
12
|
+
if (existsSync(join(candidate, 'package.json'))) return realpathSync(candidate);
|
|
13
|
+
if (dirname(dir) === dir) return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/lazy-proxy.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `lazyChainProxy` — turns an async "resolve the real object" function into
|
|
3
3
|
* a synchronously-importable stand-in that supports the SAME call shape as
|
|
4
|
-
* the real thing, including nested member access
|
|
5
|
-
* `editor.document.query(...)`), by recording the property-access PATH and only
|
|
4
|
+
* the real thing, including nested member access
|
|
5
|
+
* (`editor.document.query(...)`), by recording the property-access PATH and only
|
|
6
6
|
* resolving + walking it down at the point of an actual function CALL.
|
|
7
7
|
*
|
|
8
|
-
* This is what makes `index.ts`'s top-level `export const editor = ...`
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* This is what makes `index.ts`'s top-level `export const editor = ...` work
|
|
9
|
+
* as a plain value a script can `import { editor } from '@volter/live'` and
|
|
10
|
+
* call immediately — each call transparently awaits the memoized `connect()`
|
|
11
|
+
* first.
|
|
12
12
|
*
|
|
13
13
|
* Limitation (by design, documented on `index.ts`'s exports too): only
|
|
14
|
-
* FUNCTION-shaped access resolves through this proxy — `
|
|
15
|
-
* works, but reading a plain data property
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* FUNCTION-shaped access resolves through this proxy — `editor.document.query(s)`
|
|
15
|
+
* works, but reading a plain data property would return another inert proxy,
|
|
16
|
+
* not the real value, since there is no function CALL to trigger resolution.
|
|
17
|
+
* Every documented `editor` member is a method, so this never bites the
|
|
18
|
+
* documented surface.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
type AnyFn = (...args: unknown[]) => unknown;
|
package/src/member-hints.ts
CHANGED
|
@@ -2,13 +2,10 @@
|
|
|
2
2
|
* NAMES AN AGENT REACHES FOR THAT THE FAÇADE DOES NOT HAVE, and where the
|
|
3
3
|
* thing it wanted actually lives.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the whole time — through `editor.present`'s `viewport.camera` — but the
|
|
10
|
-
* failure said nothing about it. The refusal is the one moment the caller is
|
|
11
|
-
* certainly reading, so that is where the pointer goes.
|
|
5
|
+
* A call to `editor.setCamera(...)` would otherwise fail with the engine's bare
|
|
6
|
+
* "editor.setCamera is not a function", though the camera is reachable through
|
|
7
|
+
* `editor.present`'s `viewport.camera`. The refusal is the one moment the
|
|
8
|
+
* caller is certainly reading, so that is where the pointer goes.
|
|
12
9
|
*
|
|
13
10
|
* TARGETED, NOT A CATCH-ALL. Only the names below are intercepted, each with
|
|
14
11
|
* the door it was looking for. Every other missing member keeps JavaScript's
|
|
@@ -19,9 +16,8 @@
|
|
|
19
16
|
const CAMERA_HINT =
|
|
20
17
|
'The viewport camera is posed through a presented view: ' +
|
|
21
18
|
'await editor.present({ version: 1, viewport: { camera: { position: { x, y, z }, target: { x, y, z }, fov } } }) ' +
|
|
22
|
-
'(stage space: metres, Y up
|
|
19
|
+
'(stage space: metres, Y up), ' +
|
|
23
20
|
'and read back with (await editor.currentView()).viewport.camera. ' +
|
|
24
|
-
'From Cyclotron\'s shell: cyclotron camera --position x,y,z --target x,y,z [--fov n] (Blender coordinates). ' +
|
|
25
21
|
'editor.frame() and editor.orbit() frame or swing around the subject instead.';
|
|
26
22
|
|
|
27
23
|
/** Missing `editor` member → what to use instead. */
|
|
@@ -43,19 +39,19 @@ export function editorMemberHint(member: PropertyKey): string | undefined {
|
|
|
43
39
|
|
|
44
40
|
/**
|
|
45
41
|
* `editor` as `eval` binds it: the same object, except that reading one of the
|
|
46
|
-
* hinted names it does NOT have throws the hint, naming `
|
|
42
|
+
* hinted names it does NOT have throws the hint, naming `editor.member`.
|
|
47
43
|
* Thrown on READ, not on call, so `editor.camera.position` is caught as well as
|
|
48
44
|
* `editor.setCamera(...)`.
|
|
49
45
|
*
|
|
50
46
|
* Methods come back bound to the real object: `LiveEditor` keeps its client in
|
|
51
47
|
* a `#`-private field, which a call whose `this` is the proxy cannot read.
|
|
52
48
|
*/
|
|
53
|
-
export function withEditorMemberHints<T extends object>(target: T
|
|
49
|
+
export function withEditorMemberHints<T extends object>(target: T): T {
|
|
54
50
|
return new Proxy(target, {
|
|
55
51
|
get(object, key) {
|
|
56
52
|
if (!(key in object)) {
|
|
57
53
|
const hint = editorMemberHint(key);
|
|
58
|
-
if (hint !== undefined) throw new TypeError(
|
|
54
|
+
if (hint !== undefined) throw new TypeError(`editor.${String(key)} does not exist. ${hint}`);
|
|
59
55
|
}
|
|
60
56
|
const value: unknown = Reflect.get(object, key, object);
|
|
61
57
|
return typeof value === 'function' ? (value as (...args: unknown[]) => unknown).bind(object) : value;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Project creation is executable product code, shared by its CLI and the host. */
|
|
2
|
+
export interface ProductCreateRequest {
|
|
3
|
+
name: string;
|
|
4
|
+
targetDir: string;
|
|
5
|
+
/** A starter the product offers (its `templates`), or none for its default. */
|
|
6
|
+
template?: string;
|
|
7
|
+
/** One of the product's example projects (its `examples`), in place of a template. */
|
|
8
|
+
example?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ProductCreateResult {
|
|
11
|
+
targetDir: string;
|
|
12
|
+
manifest: unknown;
|
|
13
|
+
}
|
|
14
|
+
/** A starter or an example a product creates, as a picker lists it. */
|
|
15
|
+
interface ProductCreateChoice {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly description?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ProductCreateDeclaration {
|
|
21
|
+
readonly product: string;
|
|
22
|
+
/** The starters a new project begins from. */
|
|
23
|
+
readonly templates: readonly ProductCreateChoice[];
|
|
24
|
+
/** Finished projects to learn from, each made whole (`create --example <id>`). */
|
|
25
|
+
readonly examples?: readonly ProductCreateChoice[];
|
|
26
|
+
create(request: ProductCreateRequest): Promise<ProductCreateResult>;
|
|
27
|
+
}
|
|
28
|
+
export function assertProductCreateDeclaration(value: unknown, source: string): ProductCreateDeclaration {
|
|
29
|
+
const declaration = value as Partial<ProductCreateDeclaration> | null;
|
|
30
|
+
const choices = (list: unknown): boolean =>
|
|
31
|
+
Array.isArray(list) && !list.some(t => typeof t?.id !== 'string' || typeof t?.name !== 'string');
|
|
32
|
+
if (!declaration || typeof declaration.product !== 'string' ||
|
|
33
|
+
typeof declaration.create !== 'function' || !choices(declaration.templates) ||
|
|
34
|
+
(declaration.examples !== undefined && !choices(declaration.examples))) {
|
|
35
|
+
throw new Error(`${source}: expected a product, templates (and any examples) and create(request) function`);
|
|
36
|
+
}
|
|
37
|
+
return declaration as ProductCreateDeclaration;
|
|
38
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { installedPackageDir } from './installed-package';
|
|
6
|
+
import { declaredContributions, type ProductDeclaration, readVolterManifest } from './volter-manifest';
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
export const PRODUCT_DECLARATION_KEY = 'contributes.volter.product';
|
|
10
|
+
|
|
11
|
+
/** Light or dark — the two the workbench's own `ColorScheme` has plain names
|
|
12
|
+
* for, and the only two a product needs to state (a high-contrast scheme is a
|
|
13
|
+
* person's accessibility choice, never a product's declaration). */
|
|
14
|
+
export type ProductColorTheme = 'dark' | 'light';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export interface ProductWorkbench {
|
|
18
|
+
/** The GitHub Release tag on `volter-ai/code-oss`. */
|
|
19
|
+
readonly release: string;
|
|
20
|
+
/** The sha256 of that release's tarball asset, hex. */
|
|
21
|
+
readonly tarballSha256: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A product package, as everything outside it needs to know it. */
|
|
25
|
+
export interface ProductIdentity {
|
|
26
|
+
|
|
27
|
+
readonly name: string;
|
|
28
|
+
/** Its version, reported beside the workbench by the editor's `status` command. */
|
|
29
|
+
readonly version: string;
|
|
30
|
+
|
|
31
|
+
readonly command: string;
|
|
32
|
+
/** The name a person sees — `contributes.volter.product.displayName`. */
|
|
33
|
+
readonly displayName: string;
|
|
34
|
+
/**
|
|
35
|
+
* `contributes.volter.product.upgrade: true` — its command has `upgrade [version]`
|
|
36
|
+
* (the product CLI's own), so a pinned-engine refusal
|
|
37
|
+
* may tell a person to run it. Optional: a product that says nothing has none.
|
|
38
|
+
*/
|
|
39
|
+
readonly upgrade?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* `contributes.volter.product.adapterRequired: true` — a project this product opens must have its own
|
|
42
|
+
* adapter module (`editor/volter.adapter.ts`), because the product's layout and document
|
|
43
|
+
* finders are declared there and the native default shows none of them. Such a product refuses
|
|
44
|
+
* a project without one. Optional: a product that says nothing opens it on the native default.
|
|
45
|
+
*/
|
|
46
|
+
readonly adapterRequired?: boolean;
|
|
47
|
+
/** Its package root, absolute. */
|
|
48
|
+
readonly dir: string;
|
|
49
|
+
/**
|
|
50
|
+
* Its ONE source entry, package-relative and POSIX (`./src/index.ts`). That
|
|
51
|
+
* entry IS the product's frame entry: it composes and re-exports `mountVolter`,
|
|
52
|
+
* which is the name the fork's contribution reads off the served module.
|
|
53
|
+
*/
|
|
54
|
+
readonly entry: string;
|
|
55
|
+
/**
|
|
56
|
+
* The packages this product mounts, in order — `contributes.volter.product.composes`. The ONE statement of
|
|
57
|
+
* the composition: the build serves it to the entry as `volter:composition`, and the session
|
|
58
|
+
* reads it here for the catalog and the serving doors.
|
|
59
|
+
*/
|
|
60
|
+
readonly composes: readonly string[];
|
|
61
|
+
|
|
62
|
+
readonly colorTheme: ProductColorTheme;
|
|
63
|
+
/** WHICH WORKBENCH THIS PRODUCT IS — see {@link ProductWorkbench}. */
|
|
64
|
+
readonly workbench: ProductWorkbench | null;
|
|
65
|
+
/**
|
|
66
|
+
* The ONE LINE that installs this product and starts a project in it —
|
|
67
|
+
* `contributes.volter.product.install` (`npx <name> create my-project`). A limited view quotes
|
|
68
|
+
* it wherever it cannot do what the local editor does (its agent answers with it, and every
|
|
69
|
+
* route it does not carry names it), so it is a declaration read without running the product.
|
|
70
|
+
* An undeclared one is `npx <name> create my-project`, which is what every product's `create`
|
|
71
|
+
* verb accepts.
|
|
72
|
+
*/
|
|
73
|
+
readonly install: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** What a project without a product is told to add: the declaration this module reads, naming no package. */
|
|
77
|
+
export const PRODUCT_DECLARATION_HINT =
|
|
78
|
+
`a product package as a dev dependency: one whose package.json declares \`${PRODUCT_DECLARATION_KEY}\``;
|
|
79
|
+
|
|
80
|
+
const PRODUCT_COLOR_THEMES: readonly ProductColorTheme[] = ['dark', 'light'];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read one product package's declaration, or `null` when the manifest is not a
|
|
84
|
+
* product's. Throws when it declares `contributes.volter.product` and the declaration is
|
|
85
|
+
* unusable — a half-written product must be named, never skipped.
|
|
86
|
+
*/
|
|
87
|
+
export function readProductManifest(packageDir: string): ProductIdentity | null {
|
|
88
|
+
const manifestPath = join(packageDir, 'package.json');
|
|
89
|
+
const manifest = readVolterManifest(manifestPath);
|
|
90
|
+
const declared = manifest?.product;
|
|
91
|
+
if (manifest === null || declared === undefined) return null;
|
|
92
|
+
const entry = declared.entry;
|
|
93
|
+
if (typeof entry !== 'string' || !entry.startsWith('./'))
|
|
94
|
+
throw new Error(
|
|
95
|
+
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "entry" is ` +
|
|
96
|
+
`${JSON.stringify(entry)}. It must be a ./-relative module — the product's ONE source ` +
|
|
97
|
+
'entry, which is also its frame entry (e.g. "./src/index.ts").',
|
|
98
|
+
);
|
|
99
|
+
const resolved = join(packageDir, entry);
|
|
100
|
+
if (!existsSync(resolved))
|
|
101
|
+
throw new Error(
|
|
102
|
+
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY}.entry ${JSON.stringify(entry)}, ` +
|
|
103
|
+
`which does not exist (${resolved}).`,
|
|
104
|
+
);
|
|
105
|
+
const colorTheme = declared.colorTheme;
|
|
106
|
+
// REQUIRED, like `entry`, and for the same reason: a product that does not
|
|
107
|
+
// say which way round it paints gets the workbench's web default, which is
|
|
108
|
+
// LIGHT, and its cover is white until its real theme loads. There is no
|
|
109
|
+
// defensible default to invent here — a dark product and a light product are
|
|
110
|
+
// both ordinary — so an unstated one is named rather than guessed.
|
|
111
|
+
if (typeof colorTheme !== 'string' || !PRODUCT_COLOR_THEMES.includes(colorTheme as never))
|
|
112
|
+
throw new Error(
|
|
113
|
+
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "colorTheme" is ` +
|
|
114
|
+
`${JSON.stringify(colorTheme)}. It must be ${PRODUCT_COLOR_THEMES.map((one) =>
|
|
115
|
+
JSON.stringify(one),
|
|
116
|
+
).join(' or ')} — which way round this product paints, read before any of its code ` +
|
|
117
|
+
"runs so the workbench's first frame is already the product's own background.",
|
|
118
|
+
);
|
|
119
|
+
// REQUIRED, for the same reason as `entry`: the kit names verbs in every
|
|
120
|
+
// message it writes, and there is no command to name but the product's own.
|
|
121
|
+
const composes = productComposes(manifestPath, declared);
|
|
122
|
+
const bin = manifest.bin;
|
|
123
|
+
const commands = bin !== null && typeof bin === 'object' ? Object.keys(bin) : [];
|
|
124
|
+
if (commands.length !== 1)
|
|
125
|
+
throw new Error(
|
|
126
|
+
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "bin" names ` +
|
|
127
|
+
`${commands.length === 0 ? 'no command' : `${commands.length} commands`}. A product has ` +
|
|
128
|
+
'exactly one command — the one every session message tells a person to run.',
|
|
129
|
+
);
|
|
130
|
+
const displayName = declared.displayName;
|
|
131
|
+
if (typeof displayName !== 'string' || displayName.trim() === '')
|
|
132
|
+
throw new Error(
|
|
133
|
+
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "displayName" is ` +
|
|
134
|
+
`${JSON.stringify(displayName)}. It must be the name a person sees (e.g. "Volter Editor").`,
|
|
135
|
+
);
|
|
136
|
+
const name = manifest.name ?? packageDir;
|
|
137
|
+
const install = declared.install;
|
|
138
|
+
if (install !== undefined && (typeof install !== 'string' || install.trim() === ''))
|
|
139
|
+
throw new Error(
|
|
140
|
+
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "install" is ` +
|
|
141
|
+
`${JSON.stringify(install)}. It is the one line that installs this product and starts a ` +
|
|
142
|
+
`project (e.g. "npx ${name} create my-project"), or absent.`,
|
|
143
|
+
);
|
|
144
|
+
return {
|
|
145
|
+
name,
|
|
146
|
+
version: manifest.version ?? 'unknown',
|
|
147
|
+
command: commands[0]!,
|
|
148
|
+
displayName,
|
|
149
|
+
upgrade: declared.upgrade === true,
|
|
150
|
+
adapterRequired: declared.adapterRequired === true,
|
|
151
|
+
dir: packageDir,
|
|
152
|
+
entry,
|
|
153
|
+
composes,
|
|
154
|
+
colorTheme: colorTheme as ProductColorTheme,
|
|
155
|
+
// A private source product may use an explicitly selected matching checkout.
|
|
156
|
+
// Public packages must always carry a downloadable, checksummed workbench.
|
|
157
|
+
workbench: manifest.private && declared.workbench === undefined
|
|
158
|
+
? null : readProductWorkbench(manifestPath, declared.workbench),
|
|
159
|
+
install: typeof install === 'string' ? install.trim() : `npx ${name} create my-project`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A product declaration's `composes`, checked: the packages it mounts, by name, in order. The
|
|
164
|
+
* build's composition module and the session read it through this one check. */
|
|
165
|
+
export function productComposes(manifestPath: string, declared: ProductDeclaration): string[] {
|
|
166
|
+
const composes = declared.composes;
|
|
167
|
+
if (!Array.isArray(composes) || composes.some((name) => typeof name !== 'string' || name === ''))
|
|
168
|
+
throw new Error(
|
|
169
|
+
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "composes" is ` +
|
|
170
|
+
`${JSON.stringify(composes)}. It must list the packages this product mounts, by name ` +
|
|
171
|
+
'(e.g. ["<package>"]); each is a dependency of the product package.',
|
|
172
|
+
);
|
|
173
|
+
return composes as string[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The sha256 a release pins its tarball with, as `BUILD.json` writes it. */
|
|
177
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* REQUIRED, like `entry` and `colorTheme`, and for the sharpest reason of all
|
|
181
|
+
* three of them: a product that does not say which workbench it is cannot be INSTALLED
|
|
182
|
+
* — `npx @volter/<product> create <name>` on a machine with nothing else on it
|
|
183
|
+
* has no other way to find the bytes it runs in, and the person is left writing
|
|
184
|
+
* a path to a directory they would have to build first. There is no default to
|
|
185
|
+
* invent (the tag names a fork commit nobody can guess), so an unstated one is
|
|
186
|
+
* named rather than guessed.
|
|
187
|
+
*/
|
|
188
|
+
function readProductWorkbench(manifestPath: string, declared: unknown): ProductWorkbench {
|
|
189
|
+
const example =
|
|
190
|
+
' "contributes": { "volter": { "product": { "workbench": { "release": "<product>-<fork sha 12>-<platform>", ' +
|
|
191
|
+
'"tarballSha256": "<64 hex>" } } } }\n' +
|
|
192
|
+
'`node scripts/workbench/build-release.mjs --publish --out <release dir>` cuts the release ' +
|
|
193
|
+
'and prints exactly that block.';
|
|
194
|
+
if (typeof declared !== 'object' || declared === null || Array.isArray(declared))
|
|
195
|
+
throw new Error(
|
|
196
|
+
`${manifestPath} declares ${PRODUCT_DECLARATION_KEY} but its "workbench" is ` +
|
|
197
|
+
`${JSON.stringify(declared)}. A product IS the Code-OSS workbench it was compiled into, ` +
|
|
198
|
+
`so it names the published release those bytes are:\n${example}`,
|
|
199
|
+
);
|
|
200
|
+
const platforms = declared as Record<string, unknown>;
|
|
201
|
+
if (!('release' in platforms) && !('tarballSha256' in platforms)) {
|
|
202
|
+
const machine = `${process.platform}-${process.arch}`;
|
|
203
|
+
const declaredHere = platforms[machine];
|
|
204
|
+
if (declaredHere === undefined)
|
|
205
|
+
throw new Error(
|
|
206
|
+
`${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench declares releases for ` +
|
|
207
|
+
`${Object.keys(platforms).join(', ') || 'no platform'} and this machine is ${machine}. A ` +
|
|
208
|
+
"Code-OSS server package carries its own platform's node binary and native modules, so " +
|
|
209
|
+
`there is nothing this product can run in here until a ${machine} release is cut:\n` +
|
|
210
|
+
` node scripts/workbench/build-release.mjs --product <id> --platform ${machine} --checkout <fork dir> --publish`,
|
|
211
|
+
);
|
|
212
|
+
return readPlatformWorkbench(manifestPath, declaredHere, example);
|
|
213
|
+
}
|
|
214
|
+
return readPlatformWorkbench(manifestPath, declared, example);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function readPlatformWorkbench(manifestPath: string, declared: unknown, example: string): ProductWorkbench {
|
|
218
|
+
if (typeof declared !== 'object' || declared === null || Array.isArray(declared))
|
|
219
|
+
throw new Error(
|
|
220
|
+
`${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench names ${JSON.stringify(declared)} ` +
|
|
221
|
+
`for this platform, which is not a release:\n${example}`,
|
|
222
|
+
);
|
|
223
|
+
const record = declared as Record<string, unknown>;
|
|
224
|
+
const extra = Object.keys(record).filter((key) => key !== 'release' && key !== 'tarballSha256');
|
|
225
|
+
if (extra.length > 0)
|
|
226
|
+
throw new Error(
|
|
227
|
+
`${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench carries ` +
|
|
228
|
+
`${extra.map((key) => `"${key}"`).join(', ')}, which this declaration has no meaning for. ` +
|
|
229
|
+
`It names the release and the pin, and nothing else:\n${example}`,
|
|
230
|
+
);
|
|
231
|
+
const release = record['release'];
|
|
232
|
+
if (typeof release !== 'string' || release.trim() === '')
|
|
233
|
+
throw new Error(
|
|
234
|
+
`${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench names no "release" — the GitHub ` +
|
|
235
|
+
`Release tag its workbench was published under:\n${example}`,
|
|
236
|
+
);
|
|
237
|
+
const tarballSha256 = record['tarballSha256'];
|
|
238
|
+
if (typeof tarballSha256 !== 'string' || !SHA256_HEX.test(tarballSha256))
|
|
239
|
+
throw new Error(
|
|
240
|
+
`${manifestPath}'s ${PRODUCT_DECLARATION_KEY}.workbench has tarballSha256 ` +
|
|
241
|
+
`${JSON.stringify(tarballSha256)}, which is not 64 hex characters. It is the PIN — the ` +
|
|
242
|
+
'hash the downloaded tarball is checked against before a byte of it is extracted, so it ' +
|
|
243
|
+
`is never approximate:\n${example}`,
|
|
244
|
+
);
|
|
245
|
+
return { release, tarballSha256 };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* A product's SHORT id — `@<scope>/<name>` → `<name>`.
|
|
250
|
+
*
|
|
251
|
+
* It is the product's own `product({ id })`, the directory its workbench half
|
|
252
|
+
* lives in (`packages/<id>/workbench`), the `--product` flag the overlay and the
|
|
253
|
+
* release build take, and what a release records in `BUILD.json`. One derivation
|
|
254
|
+
* rather than four literals: the package name is the only place the product is
|
|
255
|
+
* NAMED, and everything else is that name's last segment.
|
|
256
|
+
*/
|
|
257
|
+
export function workbenchProductId(packageName: string): string {
|
|
258
|
+
return packageName.split('/').pop() ?? packageName;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** A product's entry as a path relative to `fromDir`, POSIX — the spelling a
|
|
262
|
+
* Vite manifest key and a dev-server URL both use. */
|
|
263
|
+
export function productEntryPath(product: ProductIdentity, fromDir: string): string {
|
|
264
|
+
const absolute = join(product.dir, product.entry);
|
|
265
|
+
const prefix = fromDir.endsWith('/') ? fromDir : `${fromDir}/`;
|
|
266
|
+
const relative = absolute.startsWith(prefix) ? absolute.slice(prefix.length) : absolute;
|
|
267
|
+
return relative.split('\\').join('/');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Every `@volter/*` this project declares, in either section, sorted. */
|
|
271
|
+
function declaredVolterPackages(projectRoot: string): string[] {
|
|
272
|
+
const manifest = readVolterManifest(join(projectRoot, 'package.json'));
|
|
273
|
+
return (manifest?.dependencyNames ?? []).filter((name) => name.startsWith('@volter/')).sort();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The packages `product` mounts that are installed and contribute: its `contributes.volter.product.composes`,
|
|
278
|
+
* each resolved from the product's own directory and kept when its manifest declares
|
|
279
|
+
* a `contributes.volter` object (its serving half, `contributes.volter.serving`, may be all a package contributes).
|
|
280
|
+
*/
|
|
281
|
+
export function productComposedPackages(product: ProductIdentity): string[] {
|
|
282
|
+
return product.composes.filter((name) => {
|
|
283
|
+
const dir = installedPackageDir(product.dir, name);
|
|
284
|
+
if (dir === null) return false;
|
|
285
|
+
const manifest = readVolterManifest(join(dir, 'package.json'));
|
|
286
|
+
return declaredContributions(manifest) !== null;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** The products this project declares AND resolves, in declaration order. */
|
|
291
|
+
export function declaredProducts(projectRoot: string): ProductIdentity[] {
|
|
292
|
+
const found: ProductIdentity[] = [];
|
|
293
|
+
for (const name of declaredVolterPackages(projectRoot)) {
|
|
294
|
+
const dir = installedPackageDir(projectRoot, name);
|
|
295
|
+
if (dir === null) continue;
|
|
296
|
+
const product = readProductManifest(dir);
|
|
297
|
+
if (product !== null) found.push(product);
|
|
298
|
+
}
|
|
299
|
+
return found;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* THE product this project opens in, or a throw naming what is wrong.
|
|
304
|
+
*
|
|
305
|
+
* The refusals are the whole point: a project with none and a project with two
|
|
306
|
+
* are both states a person can be in, and neither has an answer this code may
|
|
307
|
+
* invent.
|
|
308
|
+
*/
|
|
309
|
+
export function resolveProductForProject(projectRoot: string): ProductIdentity {
|
|
310
|
+
const products = declaredProducts(projectRoot);
|
|
311
|
+
if (products.length === 1) return products[0] as ProductIdentity;
|
|
312
|
+
if (products.length === 0)
|
|
313
|
+
throw new Error(
|
|
314
|
+
`${projectRoot} declares no editor product, so there is no editor to open it in.\n` +
|
|
315
|
+
'The installed product supplies the editor,\n' +
|
|
316
|
+
'and which one runs is what the project installed, never a flag.\n' +
|
|
317
|
+
`For THIS folder, install ${PRODUCT_DECLARATION_HINT},\n` +
|
|
318
|
+
"then run that product's `edit` command again. For a NEW project, the product's own `create` verb makes it.",
|
|
319
|
+
);
|
|
320
|
+
throw new Error(
|
|
321
|
+
`${projectRoot} declares ${products.length} editor products — ` +
|
|
322
|
+
`${products.map((product) => product.name).join(' and ')}.\n` +
|
|
323
|
+
'One page runs one product, and nothing here may choose between them. Remove the one you\n' +
|
|
324
|
+
'are not building with:\n' +
|
|
325
|
+
products.map((product) => ` npm uninstall ${product.name}`).join('\n'),
|
|
326
|
+
);
|
|
327
|
+
}
|