@wendoo/bridge-app 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/app-bridge.d.ts +77 -0
- package/dist/app-bridge.d.ts.map +1 -0
- package/dist/app-bridge.js +205 -0
- package/dist/app-environment-host.d.ts +524 -0
- package/dist/app-environment-host.d.ts.map +1 -0
- package/dist/app-environment-host.js +1560 -0
- package/dist/brain-diagnostics.d.ts +47 -0
- package/dist/brain-diagnostics.d.ts.map +1 -0
- package/dist/brain-diagnostics.js +100 -0
- package/dist/bridge-project.d.ts +32 -0
- package/dist/bridge-project.d.ts.map +1 -0
- package/dist/bridge-project.js +69 -0
- package/dist/compilation.d.ts +145 -0
- package/dist/compilation.d.ts.map +1 -0
- package/dist/compilation.js +404 -0
- package/dist/core-extension.d.ts +10 -0
- package/dist/core-extension.d.ts.map +1 -0
- package/dist/core-extension.js +9 -0
- package/dist/embedded-extension-id-gate.d.ts +38 -0
- package/dist/embedded-extension-id-gate.d.ts.map +1 -0
- package/dist/embedded-extension-id-gate.js +55 -0
- package/dist/embedded-extension-loader.d.ts +33 -0
- package/dist/embedded-extension-loader.d.ts.map +1 -0
- package/dist/embedded-extension-loader.js +90 -0
- package/dist/embedded-extension-vite-plugin.d.ts +34 -0
- package/dist/embedded-extension-vite-plugin.d.ts.map +1 -0
- package/dist/embedded-extension-vite-plugin.js +39 -0
- package/dist/embedded-extensions.d.ts +293 -0
- package/dist/embedded-extensions.d.ts.map +1 -0
- package/dist/embedded-extensions.js +526 -0
- package/dist/extension-catalog.d.ts +243 -0
- package/dist/extension-catalog.d.ts.map +1 -0
- package/dist/extension-catalog.js +408 -0
- package/dist/extension-install-log.d.ts +54 -0
- package/dist/extension-install-log.d.ts.map +1 -0
- package/dist/extension-install-log.js +22 -0
- package/dist/extension-install.d.ts +162 -0
- package/dist/extension-install.d.ts.map +1 -0
- package/dist/extension-install.js +412 -0
- package/dist/extension-report-presenter.d.ts +40 -0
- package/dist/extension-report-presenter.d.ts.map +1 -0
- package/dist/extension-report-presenter.js +36 -0
- package/dist/fetched-extension-snapshots.d.ts +66 -0
- package/dist/fetched-extension-snapshots.d.ts.map +1 -0
- package/dist/fetched-extension-snapshots.js +137 -0
- package/dist/folder-host-session.d.ts +85 -0
- package/dist/folder-host-session.d.ts.map +1 -0
- package/dist/folder-host-session.js +210 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/library-offer.d.ts +60 -0
- package/dist/library-offer.d.ts.map +1 -0
- package/dist/library-offer.js +47 -0
- package/dist/library-uninstall-guard.d.ts +66 -0
- package/dist/library-uninstall-guard.d.ts.map +1 -0
- package/dist/library-uninstall-guard.js +103 -0
- package/dist/manifest-files.d.ts +53 -0
- package/dist/manifest-files.d.ts.map +1 -0
- package/dist/manifest-files.js +242 -0
- package/dist/node.d.ts +4 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +2 -0
- package/dist/project-file-bridge.d.ts +10 -0
- package/dist/project-file-bridge.d.ts.map +1 -0
- package/dist/project-file-bridge.js +87 -0
- package/dist/user-tile-registration.d.ts +89 -0
- package/dist/user-tile-registration.d.ts.map +1 -0
- package/dist/user-tile-registration.js +100 -0
- package/dist/vfs-asset-url-provider.d.ts +22 -0
- package/dist/vfs-asset-url-provider.d.ts.map +1 -0
- package/dist/vfs-asset-url-provider.js +52 -0
- package/dist/workspace-folder-project-store.d.ts +125 -0
- package/dist/workspace-folder-project-store.d.ts.map +1 -0
- package/dist/workspace-folder-project-store.js +400 -0
- package/package.json +103 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ExtensionResolutionWarning } from "./embedded-extensions.js";
|
|
2
|
+
/** App-data key holding a project's extension install log. */
|
|
3
|
+
export declare const EXTENSION_INSTALL_LOG_APP_DATA_KEY = "extension-install-log";
|
|
4
|
+
/**
|
|
5
|
+
* One entry of a project's install log. The log is a record: nothing in it
|
|
6
|
+
* requires action, and where an application surfaces it is the application's
|
|
7
|
+
* choice.
|
|
8
|
+
*/
|
|
9
|
+
export type ExtensionInstallLogEvent = {
|
|
10
|
+
/** An origin entered the project's resolved closure. */
|
|
11
|
+
readonly kind: "install";
|
|
12
|
+
/** Unix epoch milliseconds. */
|
|
13
|
+
readonly at: number;
|
|
14
|
+
/** The origin's `<owner>/<repo>` coordinate. */
|
|
15
|
+
readonly origin: string;
|
|
16
|
+
/** The winning reference the origin resolved through. */
|
|
17
|
+
readonly reference: string;
|
|
18
|
+
/** The immutable routing specifier a fetched origin was installed at; absent for other transports. */
|
|
19
|
+
readonly specifier?: string;
|
|
20
|
+
} | {
|
|
21
|
+
/** An origin left the project's resolved closure. */
|
|
22
|
+
readonly kind: "remove";
|
|
23
|
+
/** Unix epoch milliseconds. */
|
|
24
|
+
readonly at: number;
|
|
25
|
+
/** The origin's `<owner>/<repo>` coordinate. */
|
|
26
|
+
readonly origin: string;
|
|
27
|
+
} | {
|
|
28
|
+
/** An install transaction was refused because a reference's fetch failed; the project was left unchanged. */
|
|
29
|
+
readonly kind: "fetch-refusal";
|
|
30
|
+
/** Unix epoch milliseconds. */
|
|
31
|
+
readonly at: number;
|
|
32
|
+
/** The reference whose fetch failed, as written. */
|
|
33
|
+
readonly reference: string;
|
|
34
|
+
/** Stable failure code of the fetch attempt. */
|
|
35
|
+
readonly code: string;
|
|
36
|
+
/** Human-readable failure message. */
|
|
37
|
+
readonly message: string;
|
|
38
|
+
} | {
|
|
39
|
+
/** Resolution selected between conflicting requesters (take-newer or reference tie-break). */
|
|
40
|
+
readonly kind: "resolution-warning";
|
|
41
|
+
/** Unix epoch milliseconds. */
|
|
42
|
+
readonly at: number;
|
|
43
|
+
/** The structured resolution warning. */
|
|
44
|
+
readonly warning: ExtensionResolutionWarning;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Parse a project's install log from its app-data text. Returns an empty log
|
|
48
|
+
* when the text is absent or malformed. Entries with event kinds no longer
|
|
49
|
+
* produced (historical logs) parse and round-trip unchanged.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseExtensionInstallLog(raw: string | undefined): readonly ExtensionInstallLogEvent[];
|
|
52
|
+
/** Append events to a log's app-data text, returning the updated text. */
|
|
53
|
+
export declare function appendExtensionInstallLog(raw: string | undefined, events: readonly ExtensionInstallLogEvent[]): string;
|
|
54
|
+
//# sourceMappingURL=extension-install-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension-install-log.d.ts","sourceRoot":"","sources":["../src/extension-install-log.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAE3E,8DAA8D;AAC9D,eAAO,MAAM,kCAAkC,0BAA0B,CAAC;AAE1E;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAChC;IACE,wDAAwD;IACxD,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,sGAAsG;IACtG,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B,GACD;IACE,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB,GACD;IACE,6GAA6G;IAC7G,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,GACD;IACE,8FAA8F;IAC9F,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,QAAQ,CAAC,OAAO,EAAE,0BAA0B,CAAC;CAC9C,CAAC;AAEN;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,wBAAwB,EAAE,CAQrG;AAED,0EAA0E;AAC1E,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,MAAM,EAAE,SAAS,wBAAwB,EAAE,GAC1C,MAAM,CAER"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** App-data key holding a project's extension install log. */
|
|
2
|
+
export const EXTENSION_INSTALL_LOG_APP_DATA_KEY = "extension-install-log";
|
|
3
|
+
/**
|
|
4
|
+
* Parse a project's install log from its app-data text. Returns an empty log
|
|
5
|
+
* when the text is absent or malformed. Entries with event kinds no longer
|
|
6
|
+
* produced (historical logs) parse and round-trip unchanged.
|
|
7
|
+
*/
|
|
8
|
+
export function parseExtensionInstallLog(raw) {
|
|
9
|
+
if (raw === undefined)
|
|
10
|
+
return [];
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Append events to a log's app-data text, returning the updated text. */
|
|
20
|
+
export function appendExtensionInstallLog(raw, events) {
|
|
21
|
+
return JSON.stringify([...parseExtensionInstallLog(raw), ...events]);
|
|
22
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { AppHostErrorCode, ExtensionCatalogMoves, ExtensionFetchError, ExtensionFetchResult, ExtensionVersionListResult } from "@wendoo/app-host";
|
|
2
|
+
import type { IBrainDef } from "@wendoo/core/app";
|
|
3
|
+
import type { WorkspaceDiagnosticEntry } from "@wendoo/ts-compiler";
|
|
4
|
+
import type { EmbeddedExtension, ExtensionResolutionWarning } from "./embedded-extensions.js";
|
|
5
|
+
import type { InstalledExtensionSnapshot, InstalledExtensionSnapshots } from "./fetched-extension-snapshots.js";
|
|
6
|
+
/**
|
|
7
|
+
* The diagnostic state of a project at one extension resolution: per-file
|
|
8
|
+
* compiler diagnostics keyed by workspace path, and per-brain typecheck
|
|
9
|
+
* problems keyed by brain key. Two states diff into an
|
|
10
|
+
* {@link ExtensionInstallOutcome} regardless of what mutation produced them.
|
|
11
|
+
*/
|
|
12
|
+
export interface ProjectDiagnosticsState {
|
|
13
|
+
/** Compiler diagnostics keyed by workspace path (host files and `.libraries/` files alike). */
|
|
14
|
+
readonly files: ReadonlyMap<string, readonly WorkspaceDiagnosticEntry[]>;
|
|
15
|
+
/** Brain typecheck problem renderings keyed by brain key. */
|
|
16
|
+
readonly brains: ReadonlyMap<string, readonly string[]>;
|
|
17
|
+
}
|
|
18
|
+
/** How an install transaction's end state compares to its baseline. */
|
|
19
|
+
export type ExtensionInstallOutcomeKind = "improved" | "unchanged" | "worsened";
|
|
20
|
+
/** One problem named by an install outcome. */
|
|
21
|
+
export interface ExtensionInstallProblem {
|
|
22
|
+
/** Workspace path of the file, or the brain key, the problem is located at. */
|
|
23
|
+
readonly location: string;
|
|
24
|
+
/** Rendering of the problem. */
|
|
25
|
+
readonly description: string;
|
|
26
|
+
}
|
|
27
|
+
/** The outcome of one install transaction: the diagnostic difference against its baseline. */
|
|
28
|
+
export interface ExtensionInstallOutcome {
|
|
29
|
+
/** `worsened` when new problems appeared; `improved` when none appeared and some resolved; `unchanged` otherwise. */
|
|
30
|
+
readonly kind: ExtensionInstallOutcomeKind;
|
|
31
|
+
/** Problems present after the transaction that were absent before. */
|
|
32
|
+
readonly newProblems: readonly ExtensionInstallProblem[];
|
|
33
|
+
/** Problems present before the transaction that are gone after. */
|
|
34
|
+
readonly resolvedProblems: readonly ExtensionInstallProblem[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Diff two diagnostic states into an install outcome. Problems are compared as
|
|
38
|
+
* per-location multisets of severity, code, and message; a problem that only
|
|
39
|
+
* moved within its file is unchanged.
|
|
40
|
+
*/
|
|
41
|
+
export declare function diffProjectDiagnostics(before: ProjectDiagnosticsState, after: ProjectDiagnosticsState): ExtensionInstallOutcome;
|
|
42
|
+
/**
|
|
43
|
+
* Re-typecheck a brain against the current catalogs and collect its problems,
|
|
44
|
+
* one rendering per diagnostic. Every rule is marked dirty first, so the
|
|
45
|
+
* typecheck recomputes each rule's result.
|
|
46
|
+
*/
|
|
47
|
+
export declare function typecheckBrainProblems(brain: IBrainDef): readonly string[];
|
|
48
|
+
/**
|
|
49
|
+
* A mechanics failure that refuses an install transaction outright: no
|
|
50
|
+
* coherent state exists to commit, and the project is left unchanged.
|
|
51
|
+
*/
|
|
52
|
+
export type ExtensionInstallRefusal = {
|
|
53
|
+
/** A dependency's content could not be fetched. */
|
|
54
|
+
readonly kind: "fetch";
|
|
55
|
+
/** The failure, carrying its {@link ExtensionFetchError.code} and the reference it was for. */
|
|
56
|
+
readonly error: ExtensionFetchError;
|
|
57
|
+
} | {
|
|
58
|
+
/** The extension graph contains a dependency cycle. */
|
|
59
|
+
readonly kind: "cycle";
|
|
60
|
+
/** The origins on the cycle, in traversal order, with the first origin repeated at the end. */
|
|
61
|
+
readonly cycle: readonly string[];
|
|
62
|
+
/** Human-readable refusal message. */
|
|
63
|
+
readonly message: string;
|
|
64
|
+
} | {
|
|
65
|
+
/** The project's stored extension records could not be read, so no write may replace them. */
|
|
66
|
+
readonly kind: "store";
|
|
67
|
+
/** Stable code of the read failure. */
|
|
68
|
+
readonly code: AppHostErrorCode;
|
|
69
|
+
/** Human-readable refusal message. */
|
|
70
|
+
readonly message: string;
|
|
71
|
+
};
|
|
72
|
+
/** The result of one install transaction. */
|
|
73
|
+
export type ExtensionInstallReport = {
|
|
74
|
+
/** True: the transaction committed (improved, unchanged, and worsened outcomes all commit). */
|
|
75
|
+
readonly committed: true;
|
|
76
|
+
/** The diagnostic difference against the pre-install baseline. */
|
|
77
|
+
readonly outcome: ExtensionInstallOutcome;
|
|
78
|
+
/** Non-fatal resolution conflicts encountered, in encounter order. */
|
|
79
|
+
readonly warnings: readonly ExtensionResolutionWarning[];
|
|
80
|
+
} | {
|
|
81
|
+
/** False: a mechanics failure refused the transaction; nothing changed. */
|
|
82
|
+
readonly committed: false;
|
|
83
|
+
/** The refusal. */
|
|
84
|
+
readonly refusal: ExtensionInstallRefusal;
|
|
85
|
+
};
|
|
86
|
+
/** Result of {@link collectExtensionFetchClosure}. */
|
|
87
|
+
export type ExtensionFetchClosureResult = {
|
|
88
|
+
/** True when content is available for every reachable, non-skipped `gh:` reference. */
|
|
89
|
+
readonly ok: true;
|
|
90
|
+
/** Snapshot record per reachable `gh:` reference: reused stored records and fresh fetches. */
|
|
91
|
+
readonly snapshotsByReference: ReadonlyMap<string, InstalledExtensionSnapshot>;
|
|
92
|
+
/** Non-fatal catalog-move findings: edges skipped this walk, retried on the next load. */
|
|
93
|
+
readonly warnings: readonly ExtensionResolutionWarning[];
|
|
94
|
+
/**
|
|
95
|
+
* Pinned `gh:` references for coordinates whose moves resolved through a
|
|
96
|
+
* floating destination during this walk, keyed by lowercased coordinate.
|
|
97
|
+
*/
|
|
98
|
+
readonly floatingPins: ReadonlyMap<string, string>;
|
|
99
|
+
} | {
|
|
100
|
+
/** False when a fetch of a reference as written failed; the transaction must refuse. */
|
|
101
|
+
readonly ok: false;
|
|
102
|
+
/** The failure. */
|
|
103
|
+
readonly error: ExtensionFetchError;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* The stored snapshot records' pinned `gh:` references, keyed by lowercased
|
|
107
|
+
* coordinate. A floating catalog-move destination resolves through this map
|
|
108
|
+
* before any network lookup, so an already-migrated coordinate keeps its pin.
|
|
109
|
+
*
|
|
110
|
+
* @param stored - The project's stored snapshot records, keyed by coordinate.
|
|
111
|
+
*/
|
|
112
|
+
export declare function floatingPinsFromSnapshots(stored: InstalledExtensionSnapshots): Map<string, string>;
|
|
113
|
+
/**
|
|
114
|
+
* Walk every reference reachable from an extensions map and ensure content is
|
|
115
|
+
* available for each reachable `gh:` reference: a usable stored record whose
|
|
116
|
+
* reference matches is reused, and anything else is fetched. Catalog moves
|
|
117
|
+
* apply at every dependency edge through the apply/fetch/re-apply loop of the
|
|
118
|
+
* shared walk; a moved edge that cannot complete this walk (fetch failure,
|
|
119
|
+
* unresolvable floating destination, undeterminable version) surfaces a
|
|
120
|
+
* stable-coded warning and is skipped, while a fetch failure for a reference
|
|
121
|
+
* as written fails the whole walk. A dependency set thus fetches as one unit
|
|
122
|
+
* before one resolution.
|
|
123
|
+
*
|
|
124
|
+
* @param options.extensions - The extensions map to walk; root references are walked as written.
|
|
125
|
+
* @param options.embedded - The host application's bundled embedded extensions.
|
|
126
|
+
* @param options.stored - The project's stored snapshot records, reused by matching reference when usable.
|
|
127
|
+
* @param options.refetch - References fetched fresh even when a stored record matches.
|
|
128
|
+
* @param options.moves - Curated catalog moves applied at every dependency edge.
|
|
129
|
+
* @param options.fetchSnapshot - Fetches a snapshot for a `gh:` reference.
|
|
130
|
+
* @param options.listVersions - Lists a repository's published versions, resolving floating move destinations.
|
|
131
|
+
*/
|
|
132
|
+
export declare function collectExtensionFetchClosure(options: {
|
|
133
|
+
extensions: Readonly<Record<string, string>>;
|
|
134
|
+
embedded: readonly EmbeddedExtension[];
|
|
135
|
+
stored: InstalledExtensionSnapshots;
|
|
136
|
+
refetch?: ReadonlySet<string>;
|
|
137
|
+
moves?: ExtensionCatalogMoves;
|
|
138
|
+
fetchSnapshot: (reference: string) => Promise<ExtensionFetchResult>;
|
|
139
|
+
listVersions?: (owner: string, repo: string) => Promise<ExtensionVersionListResult>;
|
|
140
|
+
}): Promise<ExtensionFetchClosureResult>;
|
|
141
|
+
/**
|
|
142
|
+
* Report whether the moved closure of an extensions map reaches a `gh:`
|
|
143
|
+
* reference whose content is not already available -- neither bundled as an
|
|
144
|
+
* embedded extension nor held in the stored snapshot records. Runs the shared
|
|
145
|
+
* closure walk in dry-run mode: moves apply at every edge with the versions
|
|
146
|
+
* the stored content determines, a floating destination with no stored pin
|
|
147
|
+
* counts as missing content, and nothing is fetched. It answers whether a
|
|
148
|
+
* load must run a fetch transaction to heal a moved dependency whose content
|
|
149
|
+
* the project never persisted, or persisted in an unusable record.
|
|
150
|
+
*
|
|
151
|
+
* @param options.extensions - The extensions map to walk, with any top-level moves already applied.
|
|
152
|
+
* @param options.embedded - The host application's bundled embedded extensions.
|
|
153
|
+
* @param options.stored - The project's stored snapshot records, matched by reference.
|
|
154
|
+
* @param options.moves - Curated catalog moves applied at every dependency edge.
|
|
155
|
+
*/
|
|
156
|
+
export declare function movedClosureHasMissingContent(options: {
|
|
157
|
+
extensions: Readonly<Record<string, string>>;
|
|
158
|
+
embedded: readonly EmbeddedExtension[];
|
|
159
|
+
stored: InstalledExtensionSnapshots;
|
|
160
|
+
moves?: ExtensionCatalogMoves;
|
|
161
|
+
}): Promise<boolean>;
|
|
162
|
+
//# sourceMappingURL=extension-install.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extension-install.d.ts","sourceRoot":"","sources":["../src/extension-install.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { applyCatalogMove, fileContentText, highestListedRelease, parseCatalogMoveReference, parseExtensionReference, parseProjectContentManifest, WENDOO_JSON_PATH, } from "@wendoo/app-host";
|
|
2
|
+
import { BrainRuleDef } from "@wendoo/core/brain/model";
|
|
3
|
+
import { CatalogMoveWarningCode, createCatalogMoveVersionLookup } from "./embedded-extensions.js";
|
|
4
|
+
import { decodeInstalledSnapshotFiles, installedSnapshotFromFetched } from "./fetched-extension-snapshots.js";
|
|
5
|
+
/** Render a compiler diagnostic as a location-independent problem key. */
|
|
6
|
+
function diagnosticProblem(entry) {
|
|
7
|
+
return `${entry.severity} ${entry.code}: ${entry.message}`;
|
|
8
|
+
}
|
|
9
|
+
/** Count problem occurrences per location. */
|
|
10
|
+
function problemCounts(state) {
|
|
11
|
+
const counts = new Map();
|
|
12
|
+
const add = (location, problem) => {
|
|
13
|
+
let atLocation = counts.get(location);
|
|
14
|
+
if (!atLocation) {
|
|
15
|
+
atLocation = new Map();
|
|
16
|
+
counts.set(location, atLocation);
|
|
17
|
+
}
|
|
18
|
+
atLocation.set(problem, (atLocation.get(problem) ?? 0) + 1);
|
|
19
|
+
};
|
|
20
|
+
for (const [path, entries] of state.files) {
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
add(path, diagnosticProblem(entry));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const [key, problems] of state.brains) {
|
|
26
|
+
for (const problem of problems) {
|
|
27
|
+
add(key, problem);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return counts;
|
|
31
|
+
}
|
|
32
|
+
/** Problems whose per-location count grew from `from` to `to`, one entry per additional occurrence. */
|
|
33
|
+
function grownProblems(from, to) {
|
|
34
|
+
const grown = [];
|
|
35
|
+
for (const [location, toCounts] of to) {
|
|
36
|
+
const fromCounts = from.get(location);
|
|
37
|
+
for (const [problem, count] of toCounts) {
|
|
38
|
+
const before = fromCounts?.get(problem) ?? 0;
|
|
39
|
+
for (let i = before; i < count; i++) {
|
|
40
|
+
grown.push({ location, description: problem });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return grown;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Diff two diagnostic states into an install outcome. Problems are compared as
|
|
48
|
+
* per-location multisets of severity, code, and message; a problem that only
|
|
49
|
+
* moved within its file is unchanged.
|
|
50
|
+
*/
|
|
51
|
+
export function diffProjectDiagnostics(before, after) {
|
|
52
|
+
const beforeCounts = problemCounts(before);
|
|
53
|
+
const afterCounts = problemCounts(after);
|
|
54
|
+
const newProblems = grownProblems(beforeCounts, afterCounts);
|
|
55
|
+
const resolvedProblems = grownProblems(afterCounts, beforeCounts);
|
|
56
|
+
const kind = newProblems.length > 0 ? "worsened" : resolvedProblems.length > 0 ? "improved" : "unchanged";
|
|
57
|
+
return { kind, newProblems, resolvedProblems };
|
|
58
|
+
}
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Brain typecheck problems
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
function collectRuleProblems(rule, problems) {
|
|
63
|
+
if (rule instanceof BrainRuleDef) {
|
|
64
|
+
const result = rule.when().typecheckResult();
|
|
65
|
+
if (result) {
|
|
66
|
+
const path = rule.getLocationPath();
|
|
67
|
+
result.whenParseResult.diags.forEach((diag) => {
|
|
68
|
+
problems.push(`${path} when parse ${diag.code}: ${diag.message}`);
|
|
69
|
+
});
|
|
70
|
+
result.doParseResult.diags.forEach((diag) => {
|
|
71
|
+
problems.push(`${path} do parse ${diag.code}: ${diag.message}`);
|
|
72
|
+
});
|
|
73
|
+
result.typeInfo.diags.forEach((diag) => {
|
|
74
|
+
problems.push(`${path} type ${diag.code}: ${diag.message}`);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
rule.children().forEach((child) => {
|
|
79
|
+
collectRuleProblems(child, problems);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Re-typecheck a brain against the current catalogs and collect its problems,
|
|
84
|
+
* one rendering per diagnostic. Every rule is marked dirty first, so the
|
|
85
|
+
* typecheck recomputes each rule's result.
|
|
86
|
+
*/
|
|
87
|
+
export function typecheckBrainProblems(brain) {
|
|
88
|
+
brain.pages().forEach((page) => {
|
|
89
|
+
page.children().forEach((rule) => {
|
|
90
|
+
rule.markDirty();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
brain.typecheck();
|
|
94
|
+
const problems = [];
|
|
95
|
+
brain.pages().forEach((page) => {
|
|
96
|
+
page.children().forEach((rule) => {
|
|
97
|
+
collectRuleProblems(rule, problems);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
return problems;
|
|
101
|
+
}
|
|
102
|
+
/** Read the extensions map an origin's own content manifest declares. */
|
|
103
|
+
function ownExtensions(files) {
|
|
104
|
+
const manifestContent = manifestTextOf(files);
|
|
105
|
+
if (manifestContent === undefined)
|
|
106
|
+
return {};
|
|
107
|
+
const parsed = parseProjectContentManifest(manifestContent);
|
|
108
|
+
return parsed.ok ? parsed.manifest.extensions : {};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Report whether a stored snapshot record holds usable content: its decoded
|
|
112
|
+
* files carry a parseable `wendoo.json`. Every fetched snapshot is written
|
|
113
|
+
* with one, so a record without it is corrupt and its content must be treated
|
|
114
|
+
* as missing.
|
|
115
|
+
*/
|
|
116
|
+
function isUsableSnapshotRecord(record) {
|
|
117
|
+
const manifestContent = manifestTextOf(decodeInstalledSnapshotFiles(record));
|
|
118
|
+
if (manifestContent === undefined)
|
|
119
|
+
return false;
|
|
120
|
+
return parseProjectContentManifest(manifestContent).ok;
|
|
121
|
+
}
|
|
122
|
+
/** The text of an origin's own `wendoo.json`, at either path spelling. */
|
|
123
|
+
function manifestTextOf(files) {
|
|
124
|
+
const entry = files.get(`/${WENDOO_JSON_PATH}`) ?? files.get(WENDOO_JSON_PATH);
|
|
125
|
+
return entry === undefined ? undefined : fileContentText(entry);
|
|
126
|
+
}
|
|
127
|
+
/** Build the leading-slash file map of an embedded extension. */
|
|
128
|
+
function embeddedContent(extension) {
|
|
129
|
+
const files = new Map();
|
|
130
|
+
for (const file of extension.files) {
|
|
131
|
+
files.set(file.path.startsWith("/") ? file.path : `/${file.path}`, file.content);
|
|
132
|
+
}
|
|
133
|
+
return files;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The stored snapshot records' pinned `gh:` references, keyed by lowercased
|
|
137
|
+
* coordinate. A floating catalog-move destination resolves through this map
|
|
138
|
+
* before any network lookup, so an already-migrated coordinate keeps its pin.
|
|
139
|
+
*
|
|
140
|
+
* @param stored - The project's stored snapshot records, keyed by coordinate.
|
|
141
|
+
*/
|
|
142
|
+
export function floatingPinsFromSnapshots(stored) {
|
|
143
|
+
const pins = new Map();
|
|
144
|
+
for (const [coordinate, record] of Object.entries(stored)) {
|
|
145
|
+
const parsed = parseExtensionReference(record.reference);
|
|
146
|
+
if (parsed?.transport === "gh" && parsed.routing.kind === "pin") {
|
|
147
|
+
pins.set(coordinate.toLowerCase(), record.reference);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return pins;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Walk every reference reachable from an extensions map -- through embedded
|
|
154
|
+
* extensions' own manifests and through fetched snapshots' manifests alike --
|
|
155
|
+
* applying catalog moves at every dependency edge. The walk iterates
|
|
156
|
+
* apply/fetch/re-apply: content fetched during a pass can determine versions
|
|
157
|
+
* that let a `packageVersion` selector capture on the next pass, until no edge
|
|
158
|
+
* rewrite changes. Root references are walked as written.
|
|
159
|
+
*
|
|
160
|
+
* A stored record is reused only when it is usable (its files carry a
|
|
161
|
+
* parseable `wendoo.json`); an unusable record counts as missing content in
|
|
162
|
+
* both modes.
|
|
163
|
+
*
|
|
164
|
+
* In fetch mode (a `fetchSnapshot` callback present), content missing for a
|
|
165
|
+
* reference as written fails the walk; content missing for a move-rewritten
|
|
166
|
+
* edge, an unresolvable floating destination, and a permanently undeterminable
|
|
167
|
+
* version each surface a stable-coded `catalog-move-failed` warning, skipping
|
|
168
|
+
* that edge for this walk. In dry-run mode nothing is fetched and the walk
|
|
169
|
+
* reports whether any reachable content is missing.
|
|
170
|
+
*/
|
|
171
|
+
async function walkExtensionClosure(options) {
|
|
172
|
+
const moves = options.moves ?? {};
|
|
173
|
+
const dryRun = options.fetchSnapshot === undefined;
|
|
174
|
+
const embeddedByCoordinate = new Map(options.embedded.map((extension) => [extension.canonicalOrigin, extension]));
|
|
175
|
+
const storedByReference = new Map();
|
|
176
|
+
for (const record of Object.values(options.stored)) {
|
|
177
|
+
if (options.refetch?.has(record.reference))
|
|
178
|
+
continue;
|
|
179
|
+
// An unusable record is treated as missing content: a dry run reports it
|
|
180
|
+
// and a fetch walk refetches and replaces it.
|
|
181
|
+
if (!isUsableSnapshotRecord(record))
|
|
182
|
+
continue;
|
|
183
|
+
storedByReference.set(record.reference, record);
|
|
184
|
+
}
|
|
185
|
+
const fetchedRecords = new Map();
|
|
186
|
+
const floatingPins = floatingPinsFromSnapshots(options.stored);
|
|
187
|
+
const unresolvableFloating = new Set();
|
|
188
|
+
const versionLookup = createCatalogMoveVersionLookup({
|
|
189
|
+
embedded: options.embedded,
|
|
190
|
+
contentForReference: (reference) => {
|
|
191
|
+
const record = fetchedRecords.get(reference) ?? storedByReference.get(reference);
|
|
192
|
+
return record !== undefined ? decodeInstalledSnapshotFiles(record) : undefined;
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
const warnings = [];
|
|
196
|
+
const warned = new Set();
|
|
197
|
+
const warn = (reference, code, message) => {
|
|
198
|
+
const key = `${reference} ${code}`;
|
|
199
|
+
if (warned.has(key))
|
|
200
|
+
return;
|
|
201
|
+
warned.add(key);
|
|
202
|
+
warnings.push({
|
|
203
|
+
kind: "catalog-move-failed",
|
|
204
|
+
origin: parseCatalogMoveReference(reference)?.coordinate ?? reference,
|
|
205
|
+
reference,
|
|
206
|
+
code,
|
|
207
|
+
message,
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
/** Resolve a floating reference to a pinned one: a known pin, else the highest stable published version. */
|
|
211
|
+
const resolveFloating = async (coordinate) => {
|
|
212
|
+
const key = coordinate.toLowerCase();
|
|
213
|
+
const known = floatingPins.get(key);
|
|
214
|
+
if (known !== undefined) {
|
|
215
|
+
return known;
|
|
216
|
+
}
|
|
217
|
+
if (dryRun || options.listVersions === undefined || unresolvableFloating.has(key)) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
const slash = coordinate.indexOf("/");
|
|
221
|
+
const listed = await options.listVersions(coordinate.slice(0, slash), coordinate.slice(slash + 1));
|
|
222
|
+
const version = listed.ok ? highestListedRelease(listed.versions) : undefined;
|
|
223
|
+
if (version === undefined) {
|
|
224
|
+
unresolvableFloating.add(key);
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
const pin = `gh:${coordinate}@${version}`;
|
|
228
|
+
floatingPins.set(key, pin);
|
|
229
|
+
return pin;
|
|
230
|
+
};
|
|
231
|
+
/**
|
|
232
|
+
* Redirect one declared edge through the moves with the versions known so
|
|
233
|
+
* far. A floating result resolves to its pin, and the moves re-apply from
|
|
234
|
+
* the pin, until the reference is concrete and uncaptured. Returns the walk
|
|
235
|
+
* item, or `undefined` when the edge is skipped this walk. `pendingRefs`
|
|
236
|
+
* collects declared references whose version-range evaluation is still
|
|
237
|
+
* pending, and `outputs` records the edge's final reference for the fixpoint
|
|
238
|
+
* comparison.
|
|
239
|
+
*/
|
|
240
|
+
const redirectEdge = async (declaredReference, pendingRefs, outputs) => {
|
|
241
|
+
let reference = declaredReference;
|
|
242
|
+
let movedByCatalog = false;
|
|
243
|
+
const seenPins = new Set();
|
|
244
|
+
while (true) {
|
|
245
|
+
const applied = applyCatalogMove(reference, moves, versionLookup);
|
|
246
|
+
if (!applied.ok) {
|
|
247
|
+
warn(declaredReference, applied.code, applied.message);
|
|
248
|
+
outputs.set(declaredReference, declaredReference);
|
|
249
|
+
return { reference: declaredReference, movedByCatalog: false };
|
|
250
|
+
}
|
|
251
|
+
if (applied.moved) {
|
|
252
|
+
reference = applied.reference;
|
|
253
|
+
movedByCatalog = true;
|
|
254
|
+
}
|
|
255
|
+
const parts = parseCatalogMoveReference(reference);
|
|
256
|
+
if (!parts?.floating) {
|
|
257
|
+
// Pending only counts at the edge's final concrete reference: a range
|
|
258
|
+
// blocked at an intermediate floating hop is resolved by the pin
|
|
259
|
+
// substitution and re-application.
|
|
260
|
+
if (applied.pendingVersion) {
|
|
261
|
+
pendingRefs.add(declaredReference);
|
|
262
|
+
}
|
|
263
|
+
outputs.set(declaredReference, reference);
|
|
264
|
+
return { reference, movedByCatalog };
|
|
265
|
+
}
|
|
266
|
+
const pin = await resolveFloating(parts.coordinate);
|
|
267
|
+
if (pin === undefined) {
|
|
268
|
+
outputs.set(declaredReference, reference);
|
|
269
|
+
if (dryRun) {
|
|
270
|
+
// A floating destination with no stored pin needs a fetch
|
|
271
|
+
// transaction to resolve and persist one.
|
|
272
|
+
missingContent = true;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
warn(declaredReference, CatalogMoveWarningCode.FLOATING_UNRESOLVED, `Catalog move for "${declaredReference}" resolves to the floating "${reference}" and no stable ` +
|
|
276
|
+
"published version could be pinned; the move is skipped this load.");
|
|
277
|
+
}
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
if (seenPins.has(pin)) {
|
|
281
|
+
warn(declaredReference, CatalogMoveWarningCode.CYCLE, `Catalog moves for "${declaredReference}" revisited the pinned "${pin}".`);
|
|
282
|
+
outputs.set(declaredReference, declaredReference);
|
|
283
|
+
return { reference: declaredReference, movedByCatalog: false };
|
|
284
|
+
}
|
|
285
|
+
seenPins.add(pin);
|
|
286
|
+
reference = pin;
|
|
287
|
+
movedByCatalog = true;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
let previousOutputs;
|
|
291
|
+
let missingContent = false;
|
|
292
|
+
while (true) {
|
|
293
|
+
const outputs = new Map();
|
|
294
|
+
const pendingRefs = new Set();
|
|
295
|
+
const snapshotsByReference = new Map();
|
|
296
|
+
missingContent = false;
|
|
297
|
+
const visited = new Set();
|
|
298
|
+
const queue = Object.values(options.extensions).map((reference) => ({
|
|
299
|
+
reference,
|
|
300
|
+
movedByCatalog: false,
|
|
301
|
+
}));
|
|
302
|
+
while (queue.length > 0) {
|
|
303
|
+
const item = queue.shift();
|
|
304
|
+
if (visited.has(item.reference))
|
|
305
|
+
continue;
|
|
306
|
+
visited.add(item.reference);
|
|
307
|
+
const parsed = parseExtensionReference(item.reference);
|
|
308
|
+
if (parsed === undefined)
|
|
309
|
+
continue;
|
|
310
|
+
let files;
|
|
311
|
+
if (parsed.transport === "embedded") {
|
|
312
|
+
const extension = embeddedByCoordinate.get(parsed.coordinate);
|
|
313
|
+
if (extension === undefined)
|
|
314
|
+
continue;
|
|
315
|
+
files = embeddedContent(extension);
|
|
316
|
+
}
|
|
317
|
+
else if (parsed.transport === "gh") {
|
|
318
|
+
let record = fetchedRecords.get(item.reference) ?? storedByReference.get(item.reference);
|
|
319
|
+
if (record === undefined) {
|
|
320
|
+
if (dryRun) {
|
|
321
|
+
missingContent = true;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const fetched = await options.fetchSnapshot(item.reference);
|
|
325
|
+
if (!fetched.ok) {
|
|
326
|
+
if (item.movedByCatalog) {
|
|
327
|
+
warn(item.reference, CatalogMoveWarningCode.FETCH_FAILED, `Moved content "${item.reference}" could not be fetched (${fetched.error.code}); ` +
|
|
328
|
+
"the move is skipped this load.");
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
return { ok: false, error: fetched.error };
|
|
332
|
+
}
|
|
333
|
+
record = installedSnapshotFromFetched(fetched.snapshot);
|
|
334
|
+
fetchedRecords.set(item.reference, record);
|
|
335
|
+
}
|
|
336
|
+
snapshotsByReference.set(item.reference, record);
|
|
337
|
+
files = decodeInstalledSnapshotFiles(record);
|
|
338
|
+
}
|
|
339
|
+
if (files === undefined)
|
|
340
|
+
continue;
|
|
341
|
+
for (const declaredReference of Object.values(ownExtensions(files))) {
|
|
342
|
+
const child = await redirectEdge(declaredReference, pendingRefs, outputs);
|
|
343
|
+
if (child !== undefined) {
|
|
344
|
+
queue.push(child);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const stable = previousOutputs !== undefined &&
|
|
349
|
+
outputs.size === previousOutputs.size &&
|
|
350
|
+
[...outputs].every(([key, value]) => previousOutputs?.get(key) === value);
|
|
351
|
+
if (stable) {
|
|
352
|
+
if (!dryRun) {
|
|
353
|
+
for (const declaredReference of pendingRefs) {
|
|
354
|
+
warn(declaredReference, CatalogMoveWarningCode.VERSION_UNKNOWN, `The version of "${declaredReference}" could not be determined, so its version-scoped catalog move ` +
|
|
355
|
+
"cannot be evaluated; the move is skipped this load.");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return { ok: true, snapshotsByReference, warnings, floatingPins, missingContent };
|
|
359
|
+
}
|
|
360
|
+
previousOutputs = outputs;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Walk every reference reachable from an extensions map and ensure content is
|
|
365
|
+
* available for each reachable `gh:` reference: a usable stored record whose
|
|
366
|
+
* reference matches is reused, and anything else is fetched. Catalog moves
|
|
367
|
+
* apply at every dependency edge through the apply/fetch/re-apply loop of the
|
|
368
|
+
* shared walk; a moved edge that cannot complete this walk (fetch failure,
|
|
369
|
+
* unresolvable floating destination, undeterminable version) surfaces a
|
|
370
|
+
* stable-coded warning and is skipped, while a fetch failure for a reference
|
|
371
|
+
* as written fails the whole walk. A dependency set thus fetches as one unit
|
|
372
|
+
* before one resolution.
|
|
373
|
+
*
|
|
374
|
+
* @param options.extensions - The extensions map to walk; root references are walked as written.
|
|
375
|
+
* @param options.embedded - The host application's bundled embedded extensions.
|
|
376
|
+
* @param options.stored - The project's stored snapshot records, reused by matching reference when usable.
|
|
377
|
+
* @param options.refetch - References fetched fresh even when a stored record matches.
|
|
378
|
+
* @param options.moves - Curated catalog moves applied at every dependency edge.
|
|
379
|
+
* @param options.fetchSnapshot - Fetches a snapshot for a `gh:` reference.
|
|
380
|
+
* @param options.listVersions - Lists a repository's published versions, resolving floating move destinations.
|
|
381
|
+
*/
|
|
382
|
+
export async function collectExtensionFetchClosure(options) {
|
|
383
|
+
const outcome = await walkExtensionClosure(options);
|
|
384
|
+
if (!outcome.ok) {
|
|
385
|
+
return outcome;
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
ok: true,
|
|
389
|
+
snapshotsByReference: outcome.snapshotsByReference,
|
|
390
|
+
warnings: outcome.warnings,
|
|
391
|
+
floatingPins: outcome.floatingPins,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Report whether the moved closure of an extensions map reaches a `gh:`
|
|
396
|
+
* reference whose content is not already available -- neither bundled as an
|
|
397
|
+
* embedded extension nor held in the stored snapshot records. Runs the shared
|
|
398
|
+
* closure walk in dry-run mode: moves apply at every edge with the versions
|
|
399
|
+
* the stored content determines, a floating destination with no stored pin
|
|
400
|
+
* counts as missing content, and nothing is fetched. It answers whether a
|
|
401
|
+
* load must run a fetch transaction to heal a moved dependency whose content
|
|
402
|
+
* the project never persisted, or persisted in an unusable record.
|
|
403
|
+
*
|
|
404
|
+
* @param options.extensions - The extensions map to walk, with any top-level moves already applied.
|
|
405
|
+
* @param options.embedded - The host application's bundled embedded extensions.
|
|
406
|
+
* @param options.stored - The project's stored snapshot records, matched by reference.
|
|
407
|
+
* @param options.moves - Curated catalog moves applied at every dependency edge.
|
|
408
|
+
*/
|
|
409
|
+
export async function movedClosureHasMissingContent(options) {
|
|
410
|
+
const outcome = await walkExtensionClosure(options);
|
|
411
|
+
return outcome.ok ? outcome.missingContent : true;
|
|
412
|
+
}
|