@hypit/hypit 0.1.2 → 0.1.4
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 +1 -1
- package/dist/public/studio-adapter.d.ts +31 -2
- package/package.json +4 -1
- package/packages/audio-track-studio/src/index.ts +7 -4
- package/packages/build-result/README.md +58 -0
- package/packages/build-result/package.json +2 -1
- package/packages/build-result/src/decode.ts +10 -8
- package/packages/build-result/src/file-reference.ts +42 -0
- package/packages/build-result/src/index.ts +3 -0
- package/packages/build-result/src/replace-file-windows.ts +58 -0
- package/packages/build-result/src/replace-file.ts +11 -0
- package/packages/build-result/src/store.ts +44 -13
- package/packages/build-result/src/types.ts +38 -8
- package/packages/build-result/src/writer.ts +12 -1
- package/packages/build-result-fs/README.md +23 -1
- package/packages/build-result-s3/README.md +5 -0
- package/packages/build-result-s3/src/repository.ts +19 -5
- package/packages/caption-fine/README.md +17 -2
- package/packages/caption-fine/src/manifest.ts +1 -1
- package/packages/caption-fine/src/render.ts +48 -36
- package/packages/caption-fine-studio/package.json +8 -3
- package/packages/caption-fine-studio/src/index.ts +10 -1
- package/packages/cli/README.md +25 -0
- package/packages/cli/src/arguments.ts +16 -11
- package/packages/cli/src/build-planning.ts +6 -1
- package/packages/cli/src/command.ts +1 -1
- package/packages/cli/src/commands/environment.ts +42 -13
- package/packages/cli/src/machine-view.ts +12 -3
- package/packages/cli/src/main.ts +4 -0
- package/packages/cli/src/output.ts +28 -12
- package/packages/cli/src/result-export.ts +22 -13
- package/packages/cli/src/run-file.ts +19 -8
- package/packages/comment-sticker-studio/src/index.ts +2 -1
- package/packages/compiler-node/src/compiler.ts +1 -0
- package/packages/estimate/README.md +56 -3
- package/packages/estimate/src/program.ts +7 -8
- package/packages/fonts-open/README.md +5 -0
- package/packages/fonts-open/package.json +5 -1
- package/packages/fonts-open/src/studio.ts +21 -0
- package/packages/gpt-image-kits/README.md +17 -12
- package/packages/gpt-image-kits/kits/phone-ugc-v1.svs +8 -8
- package/packages/media-track-studio/src/index.ts +7 -2
- package/packages/provider-whisperx-local/README.md +57 -0
- package/packages/ranking/src/render.ts +11 -9
- package/packages/ranking-studio/README.md +11 -0
- package/packages/ranking-studio/package.json +8 -3
- package/packages/ranking-studio/src/index.ts +11 -4
- package/packages/runtime/src/catalog.ts +2 -0
- package/packages/runtime-host-node/src/index.ts +1 -0
- package/packages/runtime-local/README.md +19 -0
- package/packages/runtime-local/src/programs.ts +39 -24
- package/packages/runtime-local/src/runtime.ts +2 -0
- package/packages/script/README.md +22 -0
- package/packages/script/src/edit.ts +123 -73
- package/packages/script/src/lexical.ts +40 -6
- package/packages/script/src/parser.ts +36 -8
- package/packages/script/src/types.ts +4 -0
- package/packages/seedance-kits/README.md +3 -1
- package/packages/seedance-kits/kits/speaker-v1.svs +5 -5
- package/packages/speech-alignment/src/align.ts +45 -21
- package/packages/studio/INSPECTOR.md +104 -0
- package/packages/studio/README.md +65 -4
- package/packages/studio/src/build-library.ts +99 -84
- package/packages/studio/src/library-media.ts +19 -0
- package/packages/studio/src/parameter-values.ts +41 -1
- package/packages/studio/src/parameters.ts +20 -21
- package/packages/studio/src/server.ts +58 -30
- package/packages/studio/src/shared.ts +20 -6
- package/packages/studio/src/studio-registry.ts +22 -0
- package/packages/studio/src/style.css +135 -144
- package/packages/studio/src/temporal-edit.ts +131 -0
- package/packages/studio/src/ui/artifact-name.ts +80 -0
- package/packages/studio/src/ui/artifact-preview.ts +99 -0
- package/packages/studio/src/ui/icons.ts +5 -1
- package/packages/studio/src/ui/library.ts +373 -192
- package/packages/studio/src/ui/main.ts +153 -31
- package/packages/studio/src/ui/material-preview.ts +1 -1
- package/packages/studio/src/ui/overlay.ts +21 -20
- package/packages/studio/src/ui/sidebar-panel.ts +31 -0
- package/packages/studio/src/ui/stage.ts +124 -8
- package/packages/studio/src/ui/timeline.ts +16 -179
- package/packages/studio/start.ts +15 -3
- package/packages/studio-adapter/README.md +21 -0
- package/packages/studio-adapter/src/index.ts +26 -1
- package/packages/temporal-markup/EDITING.md +183 -0
- package/packages/temporal-markup/README.md +4 -0
- package/packages/typography-track-studio/package.json +8 -3
- package/packages/typography-track-studio/src/index.ts +5 -1
- package/packages/video-cli/README.md +12 -3
- package/packages/video-cli/src/creation.ts +11 -5
- package/packages/whisperx/README.md +7 -0
- package/packages/workspace/src/index.ts +2 -0
- package/packages/workspace-fs-node/README.md +4 -1
- package/packages/workspace-fs-node/src/workspace.ts +3 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<p align="center">
|
|
27
27
|
<a href="https://hypit.ai"><img alt="Visit our website" src="https://img.shields.io/badge/Visit%20our%20website-DF3C68?style=for-the-badge&logo=data:image/svg%2Bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjI1Ny4wMyAyODcuNDggNDg2Ljk0IDQyNi4wNCI%2BPHBhdGggZmlsbD0iI2ZmZiIgZD0iTTczMy4wMDEsNDYyLjU5MmMtMTEuNDQtMTYuNTEtMzAuMjg1LTI2LjM0NC01MC4zNTMtMjYuMzQ0aC0xNzguNzA1Yy0yMy40OTMsMC00Ny42NzUsMTQuMjM0LTU2LjYyOCwzNS45NjdsLTU0LjA2NSwxNDUuMDUzYy04LjgsMjEuNTIzLTguNjg2LDQ2LjAxMSw0LjI0Nyw2NS4zNTIsMTIuOTksMTkuNDE4LDM3LjAzOCwzMC44OTcsNjAuMzk3LDMwLjg5N2gxNjQuNDljMjcuMDEzLDAsNTEuNDYzLTE2Ljk4OSw2MC45MTQtNDIuMjhsNTYuODAxLTE1Mi4yMjdjNy4wMjEtMTguODI1LDQuMzgxLTM5LjkwOC03LjA1OS01Ni4zOTl2LS4wMzhoLS4wMzh2LjAxOVpNNjMzLjk3OCw2NTIuODEzYy0xLjc5OCw0LjgyMS02LjQ4NSw4LjA3My0xMS42MzIsOC4wNzNoLTE1OS42NWMtOC4zMDMsMC0xMi43OC01LjM1Ny0xNC4zMjktNy42MzNzLTQuNzgzLTguNDc1LTEuNjA3LTE2LjE0N2w0NS42MjgtMTI2LjcyNWM0LjM4MS0xMi4xNDgsMTUuNDU4LTIwLjc5NiwyOC4zNTItMjEuNDY1LjYzMS0uMDM4LDEuMjgyLS4wMzgsMS45NTEtLjAzOGgxNDQuMjY5czIzLjkzMywyLjcxNywxNy43NzMsMjcuOTUxbC01MC43NTUsMTM2LjAwNGgwdi0uMDE5Wk0zNTYuMjUsNjIzLjEyMnMtNDIuNzc3LTIyLjE3My0zMy43ODYtNjIuNDgzbDU2Ljc2Mi0xNTcuMzE2YzkuMTQ1LTI1LjM0OSwzMy4yMTItNDIuMjYxLDYwLjE2OC00Mi4yNjFoMTgxLjc4NWMxMS45NTcsMCwyMy4xNDksNS43OTcsMzAuMDU1LDE1LjU3M2wyNi41NzMsMzcuNjVoLTIxNi4xMjVjLTE2LjY0NCwwLTMxLjU0NywxMC4zNS0zNy4zNDQsMjUuOTYxbC02OC4wODgsMTgyLjg5NWgwdi0uMDE5Wk0yOTIuMDY0LDU0OS41NDNzLTQyLjc5Ny0yMi4xNzMtMzMuNzg2LTYyLjQ4M2w1Ni43NjItMTU3LjMxNmM5LjE2NC0yNS4zNDksMzMuMjMxLTQyLjI2MSw2MC4xNjgtNDIuMjYxaDE3Mi4wMDljMTEuOTU3LDAsMjMuMTQ5LDUuNzk3LDMwLjA1NSwxNS41NzNsMjYuNTczLDM3LjY1aC0yMDYuMzExYy0xNi42NDQsMC0zMS41NDcsMTAuMzUtMzcuMzQ0LDI1Ljk2MWwtNjguMDg4LDE4Mi44OTVoLS4wMzh2LS4wMTlaIi8%2BPC9zdmc%2B"></a>
|
|
28
28
|
<a href="https://discord.gg/85hnyQnxpn"><img alt="Join our Discord" src="https://img.shields.io/badge/Join%20our%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white"></a>
|
|
29
|
-
<a href="https://t.me/
|
|
29
|
+
<a href="https://t.me/hypitai"><img alt="Join our Telegram" src="https://img.shields.io/badge/Join%20our%20Telegram-26A5E4?style=for-the-badge&logo=telegram&logoColor=white"></a>
|
|
30
30
|
<a href="https://x.com/hypitai"><img alt="Follow @hypitai on X" src="https://img.shields.io/badge/Follow%20%40hypitai-000000?style=for-the-badge&logo=x&logoColor=white"></a>
|
|
31
31
|
</p>
|
|
32
32
|
|
|
@@ -159,6 +159,28 @@ type StudioTemporalBinding = {
|
|
|
159
159
|
declare const studioParameterControls: readonly ["text", "number", "boolean", "select", "color", "list", "record"];
|
|
160
160
|
type StudioParameterControl = typeof studioParameterControls[number];
|
|
161
161
|
type StudioParameterLanguage = "svml" | "svs" | "svrun";
|
|
162
|
+
/** Data-only choices. Packages supply meaning; Studio owns all option rendering. */
|
|
163
|
+
type StudioParameterOption = string | {
|
|
164
|
+
readonly value: string | number | boolean;
|
|
165
|
+
readonly label: string;
|
|
166
|
+
readonly description?: string;
|
|
167
|
+
readonly preview?: {
|
|
168
|
+
readonly kind: "color";
|
|
169
|
+
readonly color: string;
|
|
170
|
+
} | {
|
|
171
|
+
readonly kind: "font";
|
|
172
|
+
readonly family: string;
|
|
173
|
+
readonly sample?: string;
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
/** Displayed number = authored number × scale. Suffixes are retained, never converted. */
|
|
177
|
+
type StudioNumberPresentation = {
|
|
178
|
+
readonly scale?: number;
|
|
179
|
+
readonly suffixes?: readonly string[];
|
|
180
|
+
readonly minimum?: number;
|
|
181
|
+
readonly maximum?: number;
|
|
182
|
+
readonly step?: number;
|
|
183
|
+
};
|
|
162
184
|
/** A real author endpoint. Its existence never implies Inspector visibility. */
|
|
163
185
|
type StudioSourceBinding = {
|
|
164
186
|
readonly id: string;
|
|
@@ -203,8 +225,12 @@ type StudioInspectorFieldDeclaration = {
|
|
|
203
225
|
readonly summary?: string;
|
|
204
226
|
/** Omit when the binding's public schema selects the finite Studio control. */
|
|
205
227
|
readonly control?: StudioParameterControl;
|
|
206
|
-
readonly options?: readonly
|
|
228
|
+
readonly options?: readonly StudioParameterOption[];
|
|
207
229
|
readonly unit?: string;
|
|
230
|
+
readonly number?: StudioNumberPresentation;
|
|
231
|
+
readonly multiline?: boolean;
|
|
232
|
+
/** Optional suggested colors, independent of the accepted color value. */
|
|
233
|
+
readonly swatches?: readonly string[];
|
|
208
234
|
};
|
|
209
235
|
/** Resolved Inspector DTO. Studio renders it and executes its exact source write. */
|
|
210
236
|
type StudioInspectorField = Omit<StudioInspectorFieldDeclaration, "control"> & {
|
|
@@ -314,6 +340,7 @@ type StudioEntityDisplay = {
|
|
|
314
340
|
readonly layers: readonly StudioDisplayLayer[];
|
|
315
341
|
};
|
|
316
342
|
type StudioLaneDescription = {
|
|
343
|
+
/** Height of this single timeline lane; overlapping entities share it. */
|
|
317
344
|
readonly heightPx: number;
|
|
318
345
|
readonly groupId?: string;
|
|
319
346
|
readonly attachedTo?: string;
|
|
@@ -484,10 +511,12 @@ type StudioEntityDraft = {
|
|
|
484
511
|
readonly display: StudioEntityDisplay;
|
|
485
512
|
readonly startFrame: number;
|
|
486
513
|
readonly endFrameExclusive: number;
|
|
514
|
+
/** Back-to-front timeline order. Ties preserve projection order. Selection raises only the timeline item. */
|
|
487
515
|
readonly stackOrder: number;
|
|
488
516
|
readonly elementRange?: Range;
|
|
489
517
|
readonly markerId?: string;
|
|
490
518
|
readonly presentId?: string;
|
|
519
|
+
/** Rendered parts belonging to this entity, including phases beyond its editable timeline interval. */
|
|
491
520
|
readonly renderIds?: readonly string[];
|
|
492
521
|
/** Resolved author references that differ per derived entity, such as one Cue's actual Style. */
|
|
493
522
|
readonly parameterReferences?: Readonly<Record<string, string>>;
|
|
@@ -683,4 +712,4 @@ declare function childEntities(context: StudioTrackCompanionContext, items: read
|
|
|
683
712
|
}[], entity: StudioTimelinePresentation["entity"], chrome: StudioTimelinePresentation["chrome"]): readonly StudioEntityDraft[];
|
|
684
713
|
|
|
685
714
|
export { artifactPreview, authoredChildFor, childEntities, createStudioCompanionHostFacet, createStudioTrackCompanionHostFacet, previewLayer, requiredReferencedValue, requiredSurfaceValue, sameSurfaceValue, studioCompanionHostAbi, studioContributionFromPackage, studioParameterControls, studioTrackCompanionsFromPackage, temporalBindingsFor, temporalLineageFor, temporalSemanticSource, textLayer };
|
|
686
|
-
export type { Range, StudioCandidateProvenance, StudioCompanionContribution, StudioCompanionHostFacet, StudioDisplayLayer, StudioEditCoordinate, StudioEditHandle, StudioEditSource, StudioEditSourceRole, StudioEntityDisplay, StudioEntityDraft, StudioFilmCompanion, StudioIcon, StudioInspectorDomain, StudioInspectorField, StudioInspectorFieldDeclaration, StudioInspectorPageDeclaration, StudioInspectorSectionDeclaration, StudioLaneAttachment, StudioLaneDescription, StudioMaterialPreview, StudioObservedValue, StudioPackageContribution, StudioParameterControl, StudioParameterLanguage, StudioPlacement, StudioPlacementChild, StudioPreviewSource, StudioRecipeBindingDeclaration, StudioRecipeReferenceBindingDeclaration, StudioResolvedTrack, StudioScriptAdjustment, StudioScriptCompanion, StudioScriptSourceMap, StudioSemanticAnchor, StudioSemanticEditTarget, StudioSemanticSegment, StudioSemanticTimeline, StudioSemanticToken, StudioSnapTarget, StudioSourceBinding, StudioSourceBindingDeclaration, StudioSpan, StudioTemporalAuthority, StudioTemporalBinding, StudioTemporalConsumer, StudioTemporalConsumerInput, StudioTemporalInstantProjection, StudioTemporalLineage, StudioTemporalPhase, StudioTemporalProjection, StudioTemporalSource, StudioTemporalWindowProjection, StudioTimelineGesture, StudioTimelinePresentation, StudioTimelineTone, StudioTrackCompanion, StudioTrackCompanionContext, StudioTrackFamily, StudioTrackTrace, StudioViewRole };
|
|
715
|
+
export type { Range, StudioCandidateProvenance, StudioCompanionContribution, StudioCompanionHostFacet, StudioDisplayLayer, StudioEditCoordinate, StudioEditHandle, StudioEditSource, StudioEditSourceRole, StudioEntityDisplay, StudioEntityDraft, StudioFilmCompanion, StudioIcon, StudioInspectorDomain, StudioInspectorField, StudioInspectorFieldDeclaration, StudioInspectorPageDeclaration, StudioInspectorSectionDeclaration, StudioLaneAttachment, StudioLaneDescription, StudioMaterialPreview, StudioNumberPresentation, StudioObservedValue, StudioPackageContribution, StudioParameterControl, StudioParameterLanguage, StudioParameterOption, StudioPlacement, StudioPlacementChild, StudioPreviewSource, StudioRecipeBindingDeclaration, StudioRecipeReferenceBindingDeclaration, StudioResolvedTrack, StudioScriptAdjustment, StudioScriptCompanion, StudioScriptSourceMap, StudioSemanticAnchor, StudioSemanticEditTarget, StudioSemanticSegment, StudioSemanticTimeline, StudioSemanticToken, StudioSnapTarget, StudioSourceBinding, StudioSourceBindingDeclaration, StudioSpan, StudioTemporalAuthority, StudioTemporalBinding, StudioTemporalConsumer, StudioTemporalConsumerInput, StudioTemporalInstantProjection, StudioTemporalLineage, StudioTemporalPhase, StudioTemporalProjection, StudioTemporalSource, StudioTemporalWindowProjection, StudioTimelineGesture, StudioTimelinePresentation, StudioTimelineTone, StudioTrackCompanion, StudioTrackCompanionContext, StudioTrackFamily, StudioTrackTrace, StudioViewRole };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypit/hypit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"homepage": "https://hypit.ai",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -115,6 +115,8 @@
|
|
|
115
115
|
"packages/*/*.html",
|
|
116
116
|
"packages/*/package.json",
|
|
117
117
|
"packages/*/README.md",
|
|
118
|
+
"packages/temporal-markup/EDITING.md",
|
|
119
|
+
"packages/studio/INSPECTOR.md",
|
|
118
120
|
"services/image-opencv/pyproject.toml",
|
|
119
121
|
"services/image-opencv/package.json",
|
|
120
122
|
"services/image-opencv/uv.lock",
|
|
@@ -259,6 +261,7 @@
|
|
|
259
261
|
},
|
|
260
262
|
"dependencies": {
|
|
261
263
|
"@puppeteer/browsers": "3.2.2",
|
|
264
|
+
"koffi": "3.2.1",
|
|
262
265
|
"puppeteer-core": "25.10.0",
|
|
263
266
|
"sharp": "0.35.3",
|
|
264
267
|
"cmu-pronouncing-dictionary": "3.0.0",
|
|
@@ -48,22 +48,25 @@ export const audioTrackStudioTrackCompanions: readonly StudioTrackCompanion[] =
|
|
|
48
48
|
],
|
|
49
49
|
inspector: [
|
|
50
50
|
{
|
|
51
|
-
binding: "playback", label: "Playback", domain: "
|
|
51
|
+
binding: "playback", label: "Playback", domain: "when",
|
|
52
52
|
page: { id: "playback", label: "Playback" }, section: { id: "playback", label: "Playback" },
|
|
53
53
|
control: "select", options: ["once", "once-start", "once-end", "loop", "loop-start", "loop-end", "stretch"],
|
|
54
54
|
},
|
|
55
55
|
...(["trim-start", "trim-end"] as const).map((binding) => ({
|
|
56
|
-
binding, label: binding === "trim-start" ? "Trim Start" : "Trim End", domain: "
|
|
57
|
-
page: { id: "playback", label: "Playback" }, section: { id: "trim", label: "Trim" }, control: "
|
|
56
|
+
binding, label: binding === "trim-start" ? "Trim Start" : "Trim End", domain: "when" as const,
|
|
57
|
+
page: { id: "playback", label: "Playback" }, section: { id: "trim", label: "Trim" }, control: "number" as const,
|
|
58
|
+
number: { suffixes: ["ms", "s", "f"], minimum: 0 },
|
|
58
59
|
})),
|
|
59
60
|
...(["min-rate", "max-rate", "gain"] as const).map((binding) => ({
|
|
60
61
|
binding, label: binding === "min-rate" ? "Minimum Rate" : binding === "max-rate" ? "Maximum Rate" : "Gain",
|
|
61
62
|
domain: "how" as const, page: { id: "mix", label: "Mix" }, section: { id: "mix", label: "Mix" },
|
|
62
63
|
control: "number" as const,
|
|
64
|
+
unit: "%", number: { scale: 100, minimum: 0, step: 1 },
|
|
63
65
|
})),
|
|
64
66
|
...(["fade-in", "fade-out"] as const).map((binding) => ({
|
|
65
67
|
binding, label: binding === "fade-in" ? "Fade In" : "Fade Out", domain: "when" as const,
|
|
66
|
-
page: { id: "fade", label: "Fade" }, section: { id: "fade", label: "Fade" }, control: "
|
|
68
|
+
page: { id: "fade", label: "Fade" }, section: { id: "fade", label: "Fade" }, control: "number" as const,
|
|
69
|
+
number: { suffixes: ["ms", "s", "f"], minimum: 0 },
|
|
67
70
|
})),
|
|
68
71
|
],
|
|
69
72
|
requiredValues: ["program"], project: projectAudio,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# `@hypit/build-result`
|
|
2
|
+
|
|
3
|
+
Build Results retain accepted public Outputs and the attempt's final outcome. Execution state belongs
|
|
4
|
+
to Runtime. A Result records which value satisfied each Output; a new Output record does not imply a
|
|
5
|
+
new media file.
|
|
6
|
+
|
|
7
|
+
## Values and file ownership
|
|
8
|
+
|
|
9
|
+
| Value source | Persisted representation |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| New Resource without an existing durable address | One file owned by this Build |
|
|
12
|
+
| Explicit Workspace file | `external-file` with the Workspace's URI |
|
|
13
|
+
| Resource from an earlier Result | `build-file` with its original `build` and `path` |
|
|
14
|
+
| Entire earlier public Output | `build-output` pointing to its terminal Build and Output |
|
|
15
|
+
| Composite value | A Value Document whose Resource bindings follow the same rules at every depth |
|
|
16
|
+
|
|
17
|
+
A local `build-file` omits `build` when its owner is the containing Result. When imported into another
|
|
18
|
+
Build, its owner becomes explicit. Arrays and nested objects do not change ownership: a new Composite
|
|
19
|
+
can save new structure while referencing old video, audio and images. Only newly produced Resources
|
|
20
|
+
get new files. The encoder recognizes Resource values, independently of their domain Type or field name.
|
|
21
|
+
|
|
22
|
+
An `external-file` stays live. Replacing that file changes subsequent reads; deleting it leaves an
|
|
23
|
+
unavailable dependency. The stored size describes the file when admitted; `describeFile` and public
|
|
24
|
+
Output resolution obtain its current size. Readers use the explicit address without searching for
|
|
25
|
+
alternatives or comparing file contents.
|
|
26
|
+
|
|
27
|
+
## Passing references through execution
|
|
28
|
+
|
|
29
|
+
Workspace attachments may supply a `location` URI. The Node compiler preserves it, and the CLI maps
|
|
30
|
+
these addresses and explicitly imported Result files to the compilation's Resource identities. It
|
|
31
|
+
passes that map as `BuildResultSeed.resourceReferences` alongside whole-Output `forwards`.
|
|
32
|
+
|
|
33
|
+
The Result writer keeps this per-Build map in its private writer state while publishing. It uses the
|
|
34
|
+
map for both direct Resource Outputs and all nested Resource bindings. Persistent Output documents
|
|
35
|
+
contain the references they actually use; private writer state is removed at the terminal outcome.
|
|
36
|
+
Core still receives ordinary typed Records, Candidates and Resource identities. It has no knowledge
|
|
37
|
+
of file paths or Result repositories. Runtime may stage input bytes for execution independently of
|
|
38
|
+
Result publication.
|
|
39
|
+
|
|
40
|
+
Repositories own address access. `describeFile` obtains metadata and `openFile` streams bytes or a byte
|
|
41
|
+
range. The filesystem and S3 implementations accept `ExternalFileAccess` for external addresses; the
|
|
42
|
+
Node default opens Workspace `file:` URIs. A custom adapter can supply a different address reader.
|
|
43
|
+
Selecting S3 for Result storage does not implicitly upload external file dependencies.
|
|
44
|
+
|
|
45
|
+
`fileReferenceIdentity` identifies the explicit URI or owning Build and path. Studio uses this identity
|
|
46
|
+
to group multiple uses of a file. Separate files remain separate even when their bytes happen to match.
|
|
47
|
+
|
|
48
|
+
An explicit `hypit get` export collects the selected Output's referenced bytes into its destination.
|
|
49
|
+
Composite exports rewrite bindings to local files, including references with different owners but the
|
|
50
|
+
same relative filename. This makes that export self-contained; ordinary Build publication preserves
|
|
51
|
+
references. Moving editable projects also requires preserving or updating their external file addresses.
|
|
52
|
+
|
|
53
|
+
Published Outputs can carry an optional author `displayName` for browsers such as Studio.
|
|
54
|
+
It is presentation text only: Output names, forwarding and file ownership do not change.
|
|
55
|
+
|
|
56
|
+
`updatePresentation` accepts `outputDisplayNames`, keyed by exact public Output name. Each
|
|
57
|
+
nonempty string sets that Output’s `displayName`; null removes it. Like title and note editing,
|
|
58
|
+
this applies to finished Results and preserves all values, file references and Output identifiers.
|
|
@@ -70,14 +70,9 @@ function resultPath(value: unknown, subject: string): string {
|
|
|
70
70
|
|
|
71
71
|
function outputValue(value: unknown, subject: string): BuildResultOutputValue {
|
|
72
72
|
const item = object(value, subject);
|
|
73
|
-
if (item.kind === "build-file") {
|
|
73
|
+
if (item.kind === "build-file" || item.kind === "external-file") {
|
|
74
74
|
assertBuildResultFileRef(item, subject);
|
|
75
|
-
return {
|
|
76
|
-
kind: "build-file",
|
|
77
|
-
path: item.path,
|
|
78
|
-
size: item.size,
|
|
79
|
-
mediaType: item.mediaType,
|
|
80
|
-
};
|
|
75
|
+
return { ...item };
|
|
81
76
|
}
|
|
82
77
|
if (item.kind === "build-output") {
|
|
83
78
|
return {
|
|
@@ -106,6 +101,7 @@ function outputs(value: unknown, subject: string): Readonly<Record<string, Build
|
|
|
106
101
|
text(name, `${subject} Output name`);
|
|
107
102
|
const item = object(value, `${subject}[${JSON.stringify(name)}]`);
|
|
108
103
|
return [name, {
|
|
104
|
+
...(item.displayName === undefined ? {} : { displayName: text(item.displayName, `${subject}.displayName`) }),
|
|
109
105
|
type: typeRef(item.type, `${subject}[${JSON.stringify(name)}].type`),
|
|
110
106
|
value: outputValue(item.value, `${subject}[${JSON.stringify(name)}].value`),
|
|
111
107
|
}] as const;
|
|
@@ -255,7 +251,7 @@ export function decodeBuildResultWriterState(
|
|
|
255
251
|
if (logicalOutputs.has(output)) throw new Error(`${subject} publishes Logical Output ${output} more than once`);
|
|
256
252
|
names.add(name);
|
|
257
253
|
logicalOutputs.add(output);
|
|
258
|
-
return { name, output };
|
|
254
|
+
return { name, output, ...(published.displayName === undefined ? {} : { displayName: text(published.displayName, `${subject}.displayName`) }) };
|
|
259
255
|
});
|
|
260
256
|
const forwardedOutputs = new Set<string>();
|
|
261
257
|
const forwards = item.forwards.map((value, index) => {
|
|
@@ -270,6 +266,12 @@ export function decodeBuildResultWriterState(
|
|
|
270
266
|
};
|
|
271
267
|
});
|
|
272
268
|
return {
|
|
269
|
+
...(item.resourceReferences === undefined ? {} : { resourceReferences: Object.fromEntries(
|
|
270
|
+
Object.entries(object(item.resourceReferences, `${subject}.resourceReferences`)).map(([resource, reference]) => {
|
|
271
|
+
assertBuildResultFileRef(reference, `${subject}.resourceReferences.${resource}`);
|
|
272
|
+
return [resource, reference];
|
|
273
|
+
}),
|
|
274
|
+
) }),
|
|
273
275
|
resources: pathMap(item.resources, `${subject}.resources`),
|
|
274
276
|
values: pathMap(item.values, `${subject}.values`),
|
|
275
277
|
publishedOutputs,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { BuildResultFileRange, BuildResultFileRef } from "./types.js";
|
|
5
|
+
|
|
6
|
+
/** Address access belongs to the Repository adapter, not the value or domain graph. */
|
|
7
|
+
export type ExternalFileAccess = {
|
|
8
|
+
size(uri: string): Promise<number>;
|
|
9
|
+
open(uri: string, range?: BuildResultFileRange): Promise<AsyncIterable<Uint8Array>>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** The Node distribution's external files are explicit file: URLs supplied by its Workspace. */
|
|
13
|
+
export const localExternalFiles: ExternalFileAccess = {
|
|
14
|
+
async size(uri) { return (await stat(fileURLToPath(uri))).size; },
|
|
15
|
+
async open(uri, range) {
|
|
16
|
+
if (range !== undefined) {
|
|
17
|
+
const size = await localExternalFiles.size(uri);
|
|
18
|
+
if (!Number.isSafeInteger(range.start) || range.start < 0
|
|
19
|
+
|| !Number.isSafeInteger(range.endExclusive) || range.endExclusive <= range.start
|
|
20
|
+
|| range.endExclusive > size) throw new Error(`Invalid file range for ${uri}`);
|
|
21
|
+
}
|
|
22
|
+
return createReadStream(fileURLToPath(uri), range === undefined ? {} : { start: range.start, end: range.endExclusive - 1 });
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function fileReferenceIdentity(owner: string, file: BuildResultFileRef): string {
|
|
27
|
+
return file.kind === "external-file" ? JSON.stringify(["external", file.uri])
|
|
28
|
+
: JSON.stringify(["result", file.build ?? owner, file.path]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Make a relative Result file reference independent of its enclosing value document. */
|
|
32
|
+
export function ownedFileReference(owner: string, file: BuildResultFileRef): BuildResultFileRef {
|
|
33
|
+
return file.kind === "build-file" ? { ...file, build: file.build ?? owner } : file;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Reopening an external reference reads its current metadata, without comparing versions. */
|
|
37
|
+
export async function currentFileReference(
|
|
38
|
+
file: BuildResultFileRef,
|
|
39
|
+
access: ExternalFileAccess = localExternalFiles,
|
|
40
|
+
): Promise<BuildResultFileRef> {
|
|
41
|
+
return file.kind === "external-file" ? { ...file, size: await access.size(file.uri) } : file;
|
|
42
|
+
}
|
|
@@ -23,3 +23,6 @@ export {
|
|
|
23
23
|
} from "./store.js";
|
|
24
24
|
export type * from "./types.js";
|
|
25
25
|
export type * from "./writer.js";
|
|
26
|
+
|
|
27
|
+
export { currentFileReference, fileReferenceIdentity, ownedFileReference, localExternalFiles } from "./file-reference.js";
|
|
28
|
+
export type { ExternalFileAccess } from "./file-reference.js";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { toNamespacedPath } from "node:path";
|
|
2
|
+
import koffi from "koffi";
|
|
3
|
+
|
|
4
|
+
const kernel = koffi.load("kernel32.dll");
|
|
5
|
+
const createFile = kernel.func("intptr_t __stdcall CreateFileW(const char16_t *path, uint32_t access, uint32_t share, void *security, uint32_t disposition, uint32_t attributes, intptr_t templateFile)");
|
|
6
|
+
const setInformation = kernel.func("int __stdcall SetFileInformationByHandle(intptr_t file, int infoClass, void *info, uint32_t size)");
|
|
7
|
+
const closeHandle = kernel.func("int __stdcall CloseHandle(intptr_t file)");
|
|
8
|
+
const getLastError = kernel.func("uint32_t __stdcall GetLastError()");
|
|
9
|
+
|
|
10
|
+
const renameInfo = koffi.struct({
|
|
11
|
+
Flags: "uint32_t",
|
|
12
|
+
RootDirectory: "void *",
|
|
13
|
+
FileNameLength: "uint32_t",
|
|
14
|
+
FileName: koffi.array("uint16_t", 1),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const DELETE = 0x00010000;
|
|
18
|
+
const SHARE_READ_WRITE_DELETE = 0x7;
|
|
19
|
+
const OPEN_EXISTING = 3;
|
|
20
|
+
const FILE_ATTRIBUTE_NORMAL = 0x80;
|
|
21
|
+
const FileRenameInfoEx = 22;
|
|
22
|
+
const REPLACE_IF_EXISTS = 0x1;
|
|
23
|
+
const POSIX_SEMANTICS = 0x2;
|
|
24
|
+
|
|
25
|
+
function failure(syscall: string, from: string, to: string): Error {
|
|
26
|
+
const win32Code = getLastError() as number;
|
|
27
|
+
return Object.assign(new Error(`Windows file replacement failed (${win32Code}): '${from}' -> '${to}'`), {
|
|
28
|
+
code: `WIN32_${win32Code}`, win32Code, syscall, path: from, dest: to,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* FileRenameInfoEx preserves open readers of the old file while publishing the new name.
|
|
34
|
+
* Node's Windows rename uses MoveFileExW, whose replacement semantics differ here.
|
|
35
|
+
* https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/4217551b-d2c0-42cb-9dc1-69a716cf6d0c
|
|
36
|
+
*/
|
|
37
|
+
export function replaceWindowsFile(from: string, to: string): void {
|
|
38
|
+
const filename = Buffer.from(toNamespacedPath(to), "utf16le");
|
|
39
|
+
const nameOffset = koffi.offsetof(renameInfo, "FileName");
|
|
40
|
+
// Win32 requires a NUL-terminated FileName even though FileNameLength excludes
|
|
41
|
+
// that terminator. Buffer.alloc leaves the final WCHAR zero-initialized.
|
|
42
|
+
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_rename_info
|
|
43
|
+
const info = Buffer.alloc(Math.max(koffi.sizeof(renameInfo), nameOffset + filename.length + 2));
|
|
44
|
+
info.writeUInt32LE(REPLACE_IF_EXISTS | POSIX_SEMANTICS, koffi.offsetof(renameInfo, "Flags"));
|
|
45
|
+
info.writeUInt32LE(filename.length, koffi.offsetof(renameInfo, "FileNameLength"));
|
|
46
|
+
filename.copy(info, nameOffset);
|
|
47
|
+
|
|
48
|
+
const handle = createFile(toNamespacedPath(from), DELETE, SHARE_READ_WRITE_DELETE,
|
|
49
|
+
null, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) as number | bigint;
|
|
50
|
+
if (handle === -1 || handle === -1n) throw failure("CreateFileW", from, to);
|
|
51
|
+
try {
|
|
52
|
+
if (!setInformation(handle, FileRenameInfoEx, info, info.length)) {
|
|
53
|
+
throw failure("SetFileInformationByHandle", from, to);
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
closeHandle(handle);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { rename } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
/** Publish a complete file without changing the bytes held by existing readers. */
|
|
4
|
+
export async function replaceFile(from: string, to: string): Promise<void> {
|
|
5
|
+
if (process.platform === "win32") {
|
|
6
|
+
const { replaceWindowsFile } = await import("./replace-file-windows.js");
|
|
7
|
+
replaceWindowsFile(from, to);
|
|
8
|
+
} else {
|
|
9
|
+
await rename(from, to);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { currentFileReference, localExternalFiles } from "./file-reference.js";
|
|
2
|
+
import type { ExternalFileAccess } from "./file-reference.js";
|
|
1
3
|
import { randomUUID } from "node:crypto";
|
|
2
4
|
import {
|
|
3
5
|
mkdir,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
encodeBuildResultManifest,
|
|
41
43
|
} from "./decode.js";
|
|
42
44
|
import { syncBuildResultOutputs } from "./writer.js";
|
|
45
|
+
import { replaceFile } from "./replace-file.js";
|
|
43
46
|
|
|
44
47
|
const manifestName = "result.json";
|
|
45
48
|
const writerStateName = ".writer.json";
|
|
@@ -83,6 +86,22 @@ export function applyBuildResultPresentation(
|
|
|
83
86
|
if (highlighted.length === 0) delete base.highlightedOutputs;
|
|
84
87
|
else base.highlightedOutputs = highlighted;
|
|
85
88
|
}
|
|
89
|
+
if (update.outputDisplayNames !== undefined) {
|
|
90
|
+
const outputs = { ...manifest.outputs };
|
|
91
|
+
for (const [name, value] of Object.entries(update.outputDisplayNames)) {
|
|
92
|
+
const entry = manifest.outputs[name];
|
|
93
|
+
assert(Object.hasOwn(manifest.outputs, name) && entry !== undefined,
|
|
94
|
+
`Build Result ${manifest.id} has no Output ${name}`);
|
|
95
|
+
const { displayName: _priorName, ...rest } = entry;
|
|
96
|
+
if (value === null) outputs[name] = rest;
|
|
97
|
+
else {
|
|
98
|
+
const displayName = value.trim();
|
|
99
|
+
assert(displayName.length > 0, "Output display name must not be empty");
|
|
100
|
+
outputs[name] = { ...rest, displayName };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return { ...base, outputs };
|
|
104
|
+
}
|
|
86
105
|
return base;
|
|
87
106
|
}
|
|
88
107
|
|
|
@@ -95,7 +114,7 @@ async function writeJsonAtomic(path: string, value: unknown): Promise<void> {
|
|
|
95
114
|
const temporary = `${path}.part-${randomUUID()}`;
|
|
96
115
|
try {
|
|
97
116
|
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: "wx" });
|
|
98
|
-
await
|
|
117
|
+
await replaceFile(temporary, path);
|
|
99
118
|
} catch (error) {
|
|
100
119
|
await rm(temporary, { force: true });
|
|
101
120
|
throw error;
|
|
@@ -123,7 +142,7 @@ async function copyArtifactAtomic(
|
|
|
123
142
|
await output.write(chunk);
|
|
124
143
|
}
|
|
125
144
|
await output.close();
|
|
126
|
-
await
|
|
145
|
+
await replaceFile(temporary, destination);
|
|
127
146
|
} catch (error) {
|
|
128
147
|
await output.close().catch(() => undefined);
|
|
129
148
|
await rm(temporary, { force: true });
|
|
@@ -221,6 +240,7 @@ export async function resolveBuildResultOutput(
|
|
|
221
240
|
root: string,
|
|
222
241
|
build: string,
|
|
223
242
|
output: string,
|
|
243
|
+
externalFiles: ExternalFileAccess = localExternalFiles,
|
|
224
244
|
): Promise<ResolvedBuildResultOutput | undefined> {
|
|
225
245
|
const seen = new Set<string>();
|
|
226
246
|
let currentBuild = build;
|
|
@@ -254,14 +274,14 @@ export async function resolveBuildResultOutput(
|
|
|
254
274
|
};
|
|
255
275
|
}
|
|
256
276
|
const terminalKind = (entry.value as { readonly kind?: unknown }).kind;
|
|
257
|
-
assert(terminalKind === "build-file" || terminalKind === "inline",
|
|
277
|
+
assert(terminalKind === "build-file" || terminalKind === "external-file" || terminalKind === "inline",
|
|
258
278
|
`Build ${currentBuild} Output ${currentOutput} has unsupported Result value kind ${String(terminalKind)}`);
|
|
259
279
|
return {
|
|
260
280
|
build: currentBuild,
|
|
261
281
|
output: currentOutput,
|
|
262
282
|
directory,
|
|
263
283
|
type: entry.type,
|
|
264
|
-
value: entry.value,
|
|
284
|
+
value: entry.value.kind === "external-file" ? await currentFileReference(entry.value, externalFiles) : entry.value,
|
|
265
285
|
};
|
|
266
286
|
}
|
|
267
287
|
}
|
|
@@ -270,6 +290,7 @@ export async function describeBuildResultOutput(
|
|
|
270
290
|
root: string,
|
|
271
291
|
build: string,
|
|
272
292
|
output: string,
|
|
293
|
+
externalFiles: ExternalFileAccess = localExternalFiles,
|
|
273
294
|
): Promise<RepositoryBuildResultOutputDescription | undefined> {
|
|
274
295
|
const seen = new Set<string>();
|
|
275
296
|
let currentBuild = build;
|
|
@@ -286,8 +307,9 @@ export async function describeBuildResultOutput(
|
|
|
286
307
|
currentOutput = entry.value.output;
|
|
287
308
|
continue;
|
|
288
309
|
}
|
|
289
|
-
|
|
290
|
-
|
|
310
|
+
const file = entry.value.kind === "external-file" ? await currentFileReference(entry.value, externalFiles) : entry.value;
|
|
311
|
+
return file.kind === "build-file" || file.kind === "external-file"
|
|
312
|
+
? { type: entry.type, kind: "resource", size: file.size, mediaType: file.mediaType }
|
|
291
313
|
: entry.value.kind === "value"
|
|
292
314
|
? { type: entry.type, kind: "composite" }
|
|
293
315
|
: { type: entry.type, kind: "scalar" };
|
|
@@ -334,12 +356,12 @@ export class FileBuildResult {
|
|
|
334
356
|
this.directory = resolve(directory);
|
|
335
357
|
}
|
|
336
358
|
|
|
337
|
-
static async create(root: string, seed: BuildResultSeed): Promise<FileBuildResult> {
|
|
359
|
+
static async create(root: string, seed: BuildResultSeed, externalFiles: ExternalFileAccess = localExternalFiles): Promise<FileBuildResult> {
|
|
338
360
|
assertOrderedBuildId(seed.id);
|
|
339
361
|
assertBuildResultSeed(seed);
|
|
340
362
|
const forwards = await normalizeBuildResultForwards({
|
|
341
363
|
read: async (build) => await readBuildResult(buildResultDirectory(root, build)),
|
|
342
|
-
resolve: async (build, output) => await resolveBuildResultOutput(root, build, output),
|
|
364
|
+
resolve: async (build, output) => await resolveBuildResultOutput(root, build, output, externalFiles),
|
|
343
365
|
}, seed.forwards ?? []);
|
|
344
366
|
const directory = buildResultDirectory(root, seed.id);
|
|
345
367
|
await mkdir(dirname(directory), { recursive: true });
|
|
@@ -360,6 +382,7 @@ export class FileBuildResult {
|
|
|
360
382
|
resources: {},
|
|
361
383
|
values: {},
|
|
362
384
|
publishedOutputs: seed.publishedOutputs,
|
|
385
|
+
...(seed.resourceReferences === undefined ? {} : { resourceReferences: seed.resourceReferences }),
|
|
363
386
|
forwards,
|
|
364
387
|
});
|
|
365
388
|
await rename(temporary, directory);
|
|
@@ -405,6 +428,7 @@ export class FileBuildResult {
|
|
|
405
428
|
},
|
|
406
429
|
},
|
|
407
430
|
});
|
|
431
|
+
if (!updated.changed) return manifest;
|
|
408
432
|
await writeJsonAtomic(join(this.directory, writerStateName), updated.writer);
|
|
409
433
|
await writeManifestAtomic(join(this.directory, manifestName), updated.manifest);
|
|
410
434
|
return updated.manifest;
|
|
@@ -434,15 +458,17 @@ export class FileBuildResult {
|
|
|
434
458
|
|
|
435
459
|
/** Default zero-configuration project repository backed by one ordinary directory tree. */
|
|
436
460
|
export class FileBuildResultRepository implements BuildResultRepository {
|
|
461
|
+
readonly externalFiles: ExternalFileAccess;
|
|
437
462
|
readonly root: string;
|
|
438
463
|
|
|
439
|
-
constructor(root: string) {
|
|
464
|
+
constructor(root: string, externalFiles: ExternalFileAccess = localExternalFiles) {
|
|
465
|
+
this.externalFiles = externalFiles;
|
|
440
466
|
assert(root.trim().length > 0, "Build Result root must not be empty");
|
|
441
467
|
this.root = resolve(root);
|
|
442
468
|
}
|
|
443
469
|
|
|
444
470
|
async create(seed: BuildResultSeed): Promise<FileBuildResult> {
|
|
445
|
-
return await FileBuildResult.create(this.root, seed);
|
|
471
|
+
return await FileBuildResult.create(this.root, seed, this.externalFiles);
|
|
446
472
|
}
|
|
447
473
|
|
|
448
474
|
async openWriter(build: string): Promise<FileBuildResult | undefined> {
|
|
@@ -477,22 +503,27 @@ export class FileBuildResultRepository implements BuildResultRepository {
|
|
|
477
503
|
}
|
|
478
504
|
|
|
479
505
|
async describeOutput(build: string, output: string): Promise<RepositoryBuildResultOutputDescription | undefined> {
|
|
480
|
-
return await describeBuildResultOutput(this.root, build, output);
|
|
506
|
+
return await describeBuildResultOutput(this.root, build, output, this.externalFiles);
|
|
481
507
|
}
|
|
482
508
|
|
|
483
509
|
async resolve(build: string, output: string): Promise<RepositoryBuildResultOutput | undefined> {
|
|
484
|
-
const resolvedOutput = await resolveBuildResultOutput(this.root, build, output);
|
|
510
|
+
const resolvedOutput = await resolveBuildResultOutput(this.root, build, output, this.externalFiles);
|
|
485
511
|
if (resolvedOutput === undefined) return undefined;
|
|
486
512
|
const { directory: _directory, ...portable } = resolvedOutput;
|
|
487
513
|
return portable;
|
|
488
514
|
}
|
|
489
515
|
|
|
516
|
+
async describeFile(_build: string, file: BuildResultFileRef): Promise<BuildResultFileRef> {
|
|
517
|
+
return await currentFileReference(file, this.externalFiles);
|
|
518
|
+
}
|
|
519
|
+
|
|
490
520
|
async openFile(
|
|
491
521
|
build: string,
|
|
492
522
|
file: BuildResultFileRef,
|
|
493
523
|
range?: BuildResultFileRange,
|
|
494
524
|
): Promise<AsyncIterable<Uint8Array> | undefined> {
|
|
495
|
-
|
|
525
|
+
if (file.kind === "external-file") return await this.externalFiles.open(file.uri, range);
|
|
526
|
+
const directory = buildResultDirectory(this.root, file.build ?? build);
|
|
496
527
|
const path = containedResultPath(directory, file.path);
|
|
497
528
|
if (!await exists(path)) return undefined;
|
|
498
529
|
if (range === undefined) return createReadStream(path);
|