screenci 0.0.75 → 0.0.78
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/dist/cli.d.ts +62 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +699 -229
- package/dist/cli.js.map +1 -1
- package/dist/docs/manifest.d.ts +277 -91
- package/dist/docs/manifest.d.ts.map +1 -1
- package/dist/docs/manifest.js +101 -30
- package/dist/docs/manifest.js.map +1 -1
- package/dist/docs/video-sources/camera-and-zooming.screenci.d.ts +2 -0
- package/dist/docs/video-sources/camera-and-zooming.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/camera-and-zooming.screenci.js +57 -0
- package/dist/docs/video-sources/camera-and-zooming.screenci.js.map +1 -0
- package/dist/docs/video-sources/installation.screenci.js +19 -10
- package/dist/docs/video-sources/installation.screenci.js.map +1 -1
- package/dist/docs/video-sources/landing.screenci.js +6 -8
- package/dist/docs/video-sources/landing.screenci.js.map +1 -1
- package/dist/docs/video-sources/languages.screenci.d.ts +2 -0
- package/dist/docs/video-sources/languages.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/languages.screenci.js +68 -0
- package/dist/docs/video-sources/languages.screenci.js.map +1 -0
- package/dist/docs/video-sources/locator-highlight-animated.screenci.js +4 -5
- package/dist/docs/video-sources/locator-highlight-animated.screenci.js.map +1 -1
- package/dist/docs/video-sources/locator-highlight-still.screenci.js +3 -4
- package/dist/docs/video-sources/locator-highlight-still.screenci.js.map +1 -1
- package/dist/docs/video-sources/narration.screenci.d.ts +2 -0
- package/dist/docs/video-sources/narration.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/narration.screenci.js +71 -0
- package/dist/docs/video-sources/narration.screenci.js.map +1 -0
- package/dist/docs/video-sources/public-urls-and-embeds.screenci.d.ts +2 -0
- package/dist/docs/video-sources/public-urls-and-embeds.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/public-urls-and-embeds.screenci.js +60 -0
- package/dist/docs/video-sources/public-urls-and-embeds.screenci.js.map +1 -0
- package/dist/docs/video-sources/speaking-style.screenci.d.ts +2 -0
- package/dist/docs/video-sources/speaking-style.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/speaking-style.screenci.js +76 -0
- package/dist/docs/video-sources/speaking-style.screenci.js.map +1 -0
- package/dist/docs/video-sources/studio.screenci.js +8 -10
- package/dist/docs/video-sources/studio.screenci.js.map +1 -1
- package/dist/docs/video-sources/vertical-video.screenci.d.ts +2 -0
- package/dist/docs/video-sources/vertical-video.screenci.d.ts.map +1 -0
- package/dist/docs/video-sources/vertical-video.screenci.js +58 -0
- package/dist/docs/video-sources/vertical-video.screenci.js.map +1 -0
- package/dist/docs/videos.d.ts +20 -0
- package/dist/docs/videos.d.ts.map +1 -1
- package/dist/docs/videos.js +25 -0
- package/dist/docs/videos.js.map +1 -1
- package/dist/e2e/instrument.e2e.js +60 -1
- package/dist/e2e/instrument.e2e.js.map +1 -1
- package/dist/e2e/redact.e2e.d.ts +2 -0
- package/dist/e2e/redact.e2e.d.ts.map +1 -0
- package/dist/e2e/redact.e2e.js +161 -0
- package/dist/e2e/redact.e2e.js.map +1 -0
- package/dist/index.d.ts +12 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/src/asset.d.ts +174 -14
- package/dist/src/asset.d.ts.map +1 -1
- package/dist/src/asset.js +320 -39
- package/dist/src/asset.js.map +1 -1
- package/dist/src/assetHash.d.ts +27 -0
- package/dist/src/assetHash.d.ts.map +1 -0
- package/dist/src/assetHash.js +0 -0
- package/dist/src/assetHash.js.map +1 -0
- package/dist/src/audio.d.ts +20 -1
- package/dist/src/audio.d.ts.map +1 -1
- package/dist/src/audio.js +126 -24
- package/dist/src/audio.js.map +1 -1
- package/dist/src/autoZoom.d.ts.map +1 -1
- package/dist/src/autoZoom.js +3 -1
- package/dist/src/autoZoom.js.map +1 -1
- package/dist/src/browserLaunchOptions.d.ts +24 -2
- package/dist/src/browserLaunchOptions.d.ts.map +1 -1
- package/dist/src/browserLaunchOptions.js +57 -3
- package/dist/src/browserLaunchOptions.js.map +1 -1
- package/dist/src/builder.d.ts +229 -136
- package/dist/src/builder.d.ts.map +1 -1
- package/dist/src/builder.js +264 -77
- package/dist/src/builder.js.map +1 -1
- package/dist/src/callerFile.d.ts +12 -0
- package/dist/src/callerFile.d.ts.map +1 -0
- package/dist/src/callerFile.js +46 -0
- package/dist/src/callerFile.js.map +1 -0
- package/dist/src/changeFocus.d.ts.map +1 -1
- package/dist/src/changeFocus.js +109 -36
- package/dist/src/changeFocus.js.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +15 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/cue.d.ts +29 -17
- package/dist/src/cue.d.ts.map +1 -1
- package/dist/src/cue.js +138 -61
- package/dist/src/cue.js.map +1 -1
- package/dist/src/declare.d.ts +63 -0
- package/dist/src/declare.d.ts.map +1 -0
- package/dist/src/declare.js +111 -0
- package/dist/src/declare.js.map +1 -0
- package/dist/src/defaults.d.ts +23 -0
- package/dist/src/defaults.d.ts.map +1 -1
- package/dist/src/defaults.js +27 -3
- package/dist/src/defaults.js.map +1 -1
- package/dist/src/events.d.ts +339 -25
- package/dist/src/events.d.ts.map +1 -1
- package/dist/src/events.js +250 -26
- package/dist/src/events.js.map +1 -1
- package/dist/src/htmlRasterizer.d.ts +10 -0
- package/dist/src/htmlRasterizer.d.ts.map +1 -1
- package/dist/src/htmlRasterizer.js +12 -2
- package/dist/src/htmlRasterizer.js.map +1 -1
- package/dist/src/init.d.ts +2 -0
- package/dist/src/init.d.ts.map +1 -1
- package/dist/src/init.js +81 -33
- package/dist/src/init.js.map +1 -1
- package/dist/src/instrument.d.ts.map +1 -1
- package/dist/src/instrument.js +164 -8
- package/dist/src/instrument.js.map +1 -1
- package/dist/src/locales.d.ts.map +1 -1
- package/dist/src/locales.js +1 -0
- package/dist/src/locales.js.map +1 -1
- package/dist/src/localize.d.ts +26 -17
- package/dist/src/localize.d.ts.map +1 -1
- package/dist/src/localize.js +19 -11
- package/dist/src/localize.js.map +1 -1
- package/dist/src/localizeRuntime.d.ts +29 -33
- package/dist/src/localizeRuntime.d.ts.map +1 -1
- package/dist/src/localizeRuntime.js +74 -49
- package/dist/src/localizeRuntime.js.map +1 -1
- package/dist/src/logger.d.ts +5 -0
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/logger.js +7 -0
- package/dist/src/logger.js.map +1 -1
- package/dist/src/manualZoom.d.ts.map +1 -1
- package/dist/src/manualZoom.js +8 -5
- package/dist/src/manualZoom.js.map +1 -1
- package/dist/src/narrationVisibility.d.ts +19 -0
- package/dist/src/narrationVisibility.d.ts.map +1 -0
- package/dist/src/narrationVisibility.js +26 -0
- package/dist/src/narrationVisibility.js.map +1 -0
- package/dist/src/openBrowser.d.ts +34 -0
- package/dist/src/openBrowser.d.ts.map +1 -0
- package/dist/src/openBrowser.js +62 -0
- package/dist/src/openBrowser.js.map +1 -0
- package/dist/src/recordingData.d.ts +27 -0
- package/dist/src/recordingData.d.ts.map +1 -1
- package/dist/src/recordingSize.d.ts +32 -0
- package/dist/src/recordingSize.d.ts.map +1 -0
- package/dist/src/recordingSize.js +54 -0
- package/dist/src/recordingSize.js.map +1 -0
- package/dist/src/redact.d.ts +68 -0
- package/dist/src/redact.d.ts.map +1 -0
- package/dist/src/redact.js +138 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/src/redactController.d.ts +61 -0
- package/dist/src/redactController.d.ts.map +1 -0
- package/dist/src/redactController.js +232 -0
- package/dist/src/redactController.js.map +1 -0
- package/dist/src/runtimeContext.d.ts +32 -0
- package/dist/src/runtimeContext.d.ts.map +1 -1
- package/dist/src/runtimeContext.js +21 -0
- package/dist/src/runtimeContext.js.map +1 -1
- package/dist/src/runtimeMode.d.ts +15 -7
- package/dist/src/runtimeMode.d.ts.map +1 -1
- package/dist/src/runtimeMode.js +17 -6
- package/dist/src/runtimeMode.js.map +1 -1
- package/dist/src/screenAudio.d.ts +88 -0
- package/dist/src/screenAudio.d.ts.map +1 -0
- package/dist/src/screenAudio.js +0 -0
- package/dist/src/screenAudio.js.map +1 -0
- package/dist/src/screenAudioSink.d.ts +46 -0
- package/dist/src/screenAudioSink.d.ts.map +1 -0
- package/dist/src/screenAudioSink.js +77 -0
- package/dist/src/screenAudioSink.js.map +1 -0
- package/dist/src/screenshot.d.ts +27 -29
- package/dist/src/screenshot.d.ts.map +1 -1
- package/dist/src/screenshot.js +39 -24
- package/dist/src/screenshot.js.map +1 -1
- package/dist/src/sourceTrim.d.ts +29 -0
- package/dist/src/sourceTrim.d.ts.map +1 -0
- package/dist/src/sourceTrim.js +76 -0
- package/dist/src/sourceTrim.js.map +1 -0
- package/dist/src/stillCapture.d.ts +5 -0
- package/dist/src/stillCapture.d.ts.map +1 -1
- package/dist/src/stillCapture.js +11 -1
- package/dist/src/stillCapture.js.map +1 -1
- package/dist/src/studio.d.ts +69 -57
- package/dist/src/studio.d.ts.map +1 -1
- package/dist/src/studio.js +16 -38
- package/dist/src/studio.js.map +1 -1
- package/dist/src/timelineOffset.d.ts +46 -0
- package/dist/src/timelineOffset.d.ts.map +1 -0
- package/dist/src/timelineOffset.js +66 -0
- package/dist/src/timelineOffset.js.map +1 -0
- package/dist/src/titleValidation.d.ts +38 -0
- package/dist/src/titleValidation.d.ts.map +1 -1
- package/dist/src/titleValidation.js +42 -0
- package/dist/src/titleValidation.js.map +1 -1
- package/dist/src/types.d.ts +218 -2
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +1 -1
- package/dist/src/types.js.map +1 -1
- package/dist/src/video.d.ts +74 -60
- package/dist/src/video.d.ts.map +1 -1
- package/dist/src/video.js +201 -39
- package/dist/src/video.js.map +1 -1
- package/dist/src/voiceSampleAudio.d.ts +34 -0
- package/dist/src/voiceSampleAudio.d.ts.map +1 -0
- package/dist/src/voiceSampleAudio.js +105 -0
- package/dist/src/voiceSampleAudio.js.map +1 -0
- package/dist/src/voices.d.ts +2 -2
- package/dist/src/voices.d.ts.map +1 -1
- package/dist/src/voices.js +8 -3
- package/dist/src/voices.js.map +1 -1
- package/dist/src/zoom.d.ts +1 -0
- package/dist/src/zoom.d.ts.map +1 -1
- package/dist/src/zoom.js +3 -0
- package/dist/src/zoom.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -2
- package/skills/screenci/SKILL.md +2 -2
- package/skills/screenci/references/init.md +4 -3
- package/skills/screenci/references/record.md +1 -1
- package/templates/recordings/assets/logo.png +0 -0
- package/dist/docs/video-sources/public-urls.screenci.d.ts +0 -2
- package/dist/docs/video-sources/public-urls.screenci.d.ts.map +0 -1
- package/dist/docs/video-sources/public-urls.screenci.js +0 -41
- package/dist/docs/video-sources/public-urls.screenci.js.map +0 -1
package/dist/src/asset.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import { timelineAnchorFields, } from './events.js';
|
|
2
|
+
import { parseTimelineOffset } from './timelineOffset.js';
|
|
3
|
+
import { validateCrop, resolveSourceTrim } from './sourceTrim.js';
|
|
1
4
|
import { overlayRect } from './overlayRect.js';
|
|
5
|
+
import { captureCallerFile } from './callerFile.js';
|
|
6
|
+
import { logger } from './logger.js';
|
|
2
7
|
import { access, readFile } from 'fs/promises';
|
|
3
8
|
import { dirname, resolve } from 'path';
|
|
4
9
|
import { resolveRecordingTimingDuration } from './runtimeMode.js';
|
|
@@ -10,7 +15,66 @@ import { getScreenCIRuntimeContext, getRuntimeAssetRecorder, getRuntimePage, get
|
|
|
10
15
|
* headroom for a boost while guarding against accidental extreme distortion.
|
|
11
16
|
*/
|
|
12
17
|
export const MAX_AUDIO_LEVEL = 4;
|
|
13
|
-
|
|
18
|
+
/** Brand identifying a {@link selected} render-dependency overlay input. */
|
|
19
|
+
const DEPENDENCY_INPUT_BRAND = '__screenciSelectedDependency';
|
|
20
|
+
/**
|
|
21
|
+
* Embed another render's output as an overlay (a "render dependency"). Pass the
|
|
22
|
+
* project-unique `name` of a video or screenshot; screenci embeds that target's
|
|
23
|
+
* selected render for the matching language (falling back to its latest finished
|
|
24
|
+
* render before anything is selected). When the target's selection changes, this
|
|
25
|
+
* recording's dependents automatically re-render to embed the new output.
|
|
26
|
+
*
|
|
27
|
+
* No local file is read for a `selected(...)` overlay: the medium and concrete
|
|
28
|
+
* output are looked up by the backend at render time. Screenshots may only embed
|
|
29
|
+
* other screenshots; videos may embed either.
|
|
30
|
+
*
|
|
31
|
+
* The embed plays the target's audio. Pass `{ inheritSubtitles: true }` to also
|
|
32
|
+
* carry the target's narration subtitles up into the surrounding video's subtitle
|
|
33
|
+
* track for the window it plays, wherever the surrounding video has no competing
|
|
34
|
+
* narration of its own (off by default).
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* video.overlays({ intro: selected('Intro Clip') })(
|
|
39
|
+
* 'Full Demo',
|
|
40
|
+
* async ({ page, overlays }) => {
|
|
41
|
+
* await overlays.intro()
|
|
42
|
+
* await page.goto('/dashboard')
|
|
43
|
+
* }
|
|
44
|
+
* )
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function selected(name, options) {
|
|
48
|
+
if (typeof name !== 'string' || name.trim().length === 0) {
|
|
49
|
+
throw new Error('[screenci] selected(name) requires the non-empty name of a video or screenshot to embed.');
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
[DEPENDENCY_INPUT_BRAND]: true,
|
|
53
|
+
name,
|
|
54
|
+
config: options ?? {},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function isDependencyOverlayInput(value) {
|
|
58
|
+
return (typeof value === 'object' &&
|
|
59
|
+
value !== null &&
|
|
60
|
+
value[DEPENDENCY_INPUT_BRAND] === true);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Overlay file paths registered by {@link createOverlays}, each attributed to
|
|
64
|
+
* the `.screenci` script that declared it (or `null` when the caller could not
|
|
65
|
+
* be determined). Attribution lets {@link validateRegisteredAssetPaths} check
|
|
66
|
+
* only the assets a given recording's script declared, so unrelated assets
|
|
67
|
+
* registered by other test files sharing the worker are not validated against
|
|
68
|
+
* the wrong test file.
|
|
69
|
+
*/
|
|
70
|
+
const registeredAssets = [];
|
|
71
|
+
function registerAssetPath(path) {
|
|
72
|
+
const ownerFile = captureCallerFile(import.meta.url);
|
|
73
|
+
if (registeredAssets.some((entry) => entry.path === path && entry.ownerFile === ownerFile)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
registeredAssets.push({ ownerFile, path });
|
|
77
|
+
}
|
|
14
78
|
// One frame at 24fps — ensures at least one rendered frame captures each asset
|
|
15
79
|
// state when an overlay is started and ended back-to-back.
|
|
16
80
|
const ONE_FRAME_MS = 1000 / 24;
|
|
@@ -35,14 +99,26 @@ export function setActiveAssetRecorder(recorder) {
|
|
|
35
99
|
resetAssetRuntimeState();
|
|
36
100
|
}
|
|
37
101
|
export function resetRegisteredAssetPaths() {
|
|
38
|
-
|
|
102
|
+
registeredAssets.length = 0;
|
|
39
103
|
}
|
|
40
104
|
export function resetAssetChain() {
|
|
41
105
|
resetAssetRuntimeState();
|
|
42
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* Validates the overlay files declared by the `.screenci` script at
|
|
109
|
+
* {@link testFilePath} (plus any unattributed registrations), resolving each
|
|
110
|
+
* as-is and relative to that file. Throws "Asset file not found" on the first
|
|
111
|
+
* missing file. Wired into the record flow so a missing overlay fails fast
|
|
112
|
+
* before the test body runs, rather than only when the overlay is first shown.
|
|
113
|
+
*
|
|
114
|
+
* Assets attributed to a different test file are skipped: another script sharing
|
|
115
|
+
* the worker may legitimately reference a file that does not resolve here.
|
|
116
|
+
*/
|
|
43
117
|
export async function validateRegisteredAssetPaths(testFilePath) {
|
|
44
|
-
for (const
|
|
45
|
-
|
|
118
|
+
for (const { ownerFile, path } of registeredAssets) {
|
|
119
|
+
if (ownerFile !== null && ownerFile !== testFilePath)
|
|
120
|
+
continue;
|
|
121
|
+
await validateAssetPath(path, testFilePath);
|
|
46
122
|
}
|
|
47
123
|
}
|
|
48
124
|
/**
|
|
@@ -65,8 +141,27 @@ async function resolveExistingAssetPath(assetPath, testFilePath) {
|
|
|
65
141
|
}
|
|
66
142
|
throw new Error(`Asset file not found: ${assetPath}`);
|
|
67
143
|
}
|
|
144
|
+
const warnedMissingOverlayPaths = new Set();
|
|
145
|
+
export function resetMissingOverlayWarnings() {
|
|
146
|
+
warnedMissingOverlayPaths.clear();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Checks that an overlay file exists. A missing file is not fatal: the overlay
|
|
150
|
+
* is recovered from a previous upload of this video (matched by name/path) at
|
|
151
|
+
* upload time, so a gitignored overlay file does not have to be committed. The
|
|
152
|
+
* overlay is composited by the renderer, not into the local recording, so a
|
|
153
|
+
* missing file does not change what is captured locally.
|
|
154
|
+
*/
|
|
68
155
|
async function validateAssetPath(assetPath, testFilePath) {
|
|
69
|
-
|
|
156
|
+
try {
|
|
157
|
+
await resolveExistingAssetPath(assetPath, testFilePath);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
if (warnedMissingOverlayPaths.has(assetPath))
|
|
161
|
+
return;
|
|
162
|
+
warnedMissingOverlayPaths.add(assetPath);
|
|
163
|
+
logger.warn(`Locally missing overlay: ${assetPath}. It will be reused from a previous upload of this video if available, otherwise the upload fails.`);
|
|
164
|
+
}
|
|
70
165
|
}
|
|
71
166
|
/**
|
|
72
167
|
* Creates a set of typed overlay controllers, one per key in the map. Each value
|
|
@@ -124,6 +219,12 @@ function isReactElementLike(value) {
|
|
|
124
219
|
return value.__pw_type === 'jsx';
|
|
125
220
|
}
|
|
126
221
|
function buildOverlayController(name, input) {
|
|
222
|
+
// A render dependency (selected(...)) embeds another render's output. It is a
|
|
223
|
+
// plain (branded) object, so it is matched before the generic config path,
|
|
224
|
+
// which would otherwise reject it for having no path/element/html.
|
|
225
|
+
if (isDependencyOverlayInput(input)) {
|
|
226
|
+
return createDependencyOverlayController(name, input);
|
|
227
|
+
}
|
|
127
228
|
// A factory is the only callable input. React elements and config objects are
|
|
128
229
|
// plain objects, so this branch never captures them. The config (and its
|
|
129
230
|
// validation) is built per call so props can vary placement and content.
|
|
@@ -158,7 +259,10 @@ function buildOverlayFromConfig(name, config) {
|
|
|
158
259
|
hasPath,
|
|
159
260
|
});
|
|
160
261
|
const fullScreen = config.fill === 'screen';
|
|
262
|
+
const pinToScreen = config.pinToScreen === true;
|
|
161
263
|
const animate = config.animate === true;
|
|
264
|
+
// Parse the relative `duration` string into ms once; reused by every branch.
|
|
265
|
+
const configDurationMs = resolveConfigDuration(name, config.duration);
|
|
162
266
|
if (config.fps !== undefined && !animate) {
|
|
163
267
|
throw new Error(`[screenci] Overlay "${name}" sets "fps" without "animate: true". "fps" only applies to animated overlays.`);
|
|
164
268
|
}
|
|
@@ -191,9 +295,9 @@ function buildOverlayFromConfig(name, config) {
|
|
|
191
295
|
? () => renderElementToMarkup(name, config.element)
|
|
192
296
|
: () => Promise.resolve(config.html);
|
|
193
297
|
if (animate) {
|
|
194
|
-
return createAnimatedOverlayController(name, getMarkup, placementSource, fullScreen, config.fps,
|
|
298
|
+
return createAnimatedOverlayController(name, getMarkup, placementSource, fullScreen, pinToScreen, config.fps, configDurationMs, renderOpts);
|
|
195
299
|
}
|
|
196
|
-
return createRenderedOverlayController(name, getMarkup, placementSource, fullScreen,
|
|
300
|
+
return createRenderedOverlayController(name, getMarkup, placementSource, fullScreen, pinToScreen, configDurationMs, renderOpts);
|
|
197
301
|
}
|
|
198
302
|
const path = config.path;
|
|
199
303
|
const extension = getAssetExtension(path);
|
|
@@ -210,35 +314,46 @@ function buildOverlayFromConfig(name, config) {
|
|
|
210
314
|
extension !== '.html') {
|
|
211
315
|
throw new Error(`[screenci] Overlay "${name}" (${path}) cannot use "css" or "capturePadding": they are only supported for HTML files and React elements.`);
|
|
212
316
|
}
|
|
317
|
+
// crop applies to image and video files (not rasterized .html); source trim
|
|
318
|
+
// (start/end) re-times a moving picture, so it applies to .mp4 videos only.
|
|
319
|
+
if (config.crop !== undefined && extension === '.html') {
|
|
320
|
+
throw new Error(`[screenci] Overlay "${name}" (${path}) cannot use "crop": crop is only supported for image (.svg/.png) and video (.mp4) file overlays.`);
|
|
321
|
+
}
|
|
322
|
+
if ((config.start !== undefined || config.end !== undefined) &&
|
|
323
|
+
extension !== '.mp4') {
|
|
324
|
+
throw new Error(`[screenci] Overlay "${name}" (${path}) cannot use "start"/"end": source trim is only supported for .mp4 video overlays.`);
|
|
325
|
+
}
|
|
213
326
|
// HTML file: read + rasterize to a transparent PNG (or an animated clip).
|
|
214
327
|
if (extension === '.html') {
|
|
215
|
-
|
|
328
|
+
registerAssetPath(path);
|
|
216
329
|
const getMarkup = () => readHtmlOverlayFile(path);
|
|
217
330
|
if (animate) {
|
|
218
|
-
return createAnimatedOverlayController(name, getMarkup, placementSource, fullScreen, config.fps,
|
|
331
|
+
return createAnimatedOverlayController(name, getMarkup, placementSource, fullScreen, pinToScreen, config.fps, configDurationMs, renderOpts);
|
|
219
332
|
}
|
|
220
|
-
return createRenderedOverlayController(name, getMarkup, placementSource, fullScreen,
|
|
333
|
+
return createRenderedOverlayController(name, getMarkup, placementSource, fullScreen, pinToScreen, configDurationMs, renderOpts);
|
|
221
334
|
}
|
|
222
335
|
// File-backed image / video overlays.
|
|
223
336
|
if (extension === '.svg' || extension === '.png') {
|
|
224
337
|
if (config.volume !== undefined) {
|
|
225
|
-
throw new Error(`[screenci] Overlay "${name}" (${path}) is an image and must not provide volume. Use
|
|
338
|
+
throw new Error(`[screenci] Overlay "${name}" (${path}) is an image and must not provide volume. Use duration instead.`);
|
|
226
339
|
}
|
|
227
|
-
if (config.
|
|
228
|
-
|
|
340
|
+
if (config.crop !== undefined) {
|
|
341
|
+
validateCrop(`Overlay "${name}" (${path})`, config.crop);
|
|
229
342
|
}
|
|
230
|
-
|
|
343
|
+
registerAssetPath(path);
|
|
231
344
|
return createFileOverlayController(name, {
|
|
232
345
|
kind: 'image',
|
|
233
346
|
path,
|
|
234
347
|
...(placement !== undefined && { placement }),
|
|
235
348
|
fullScreen,
|
|
236
|
-
...(
|
|
349
|
+
...(pinToScreen && { pinToScreen: true }),
|
|
350
|
+
...(configDurationMs !== undefined && { durationMs: configDurationMs }),
|
|
351
|
+
...(config.crop !== undefined && { crop: config.crop }),
|
|
237
352
|
});
|
|
238
353
|
}
|
|
239
354
|
if (extension === '.mp4') {
|
|
240
|
-
if (config.
|
|
241
|
-
throw new Error(`[screenci] Overlay "${name}" (${path}) is a video and must not provide
|
|
355
|
+
if (config.duration !== undefined) {
|
|
356
|
+
throw new Error(`[screenci] Overlay "${name}" (${path}) is a video and must not provide duration. Its natural media duration is used instead.`);
|
|
242
357
|
}
|
|
243
358
|
if (config.volume !== undefined &&
|
|
244
359
|
(!Number.isFinite(config.volume) ||
|
|
@@ -247,15 +362,23 @@ function buildOverlayFromConfig(name, config) {
|
|
|
247
362
|
throw new Error(`[screenci] Overlay "${name}" (${path}) must provide a finite volume between 0 and ${MAX_AUDIO_LEVEL} for .mp4 overlays. 1 is the natural level, 0 is silent, and values above 1 boost it.`);
|
|
248
363
|
}
|
|
249
364
|
validateSpeedTime(`Overlay "${name}" (${path})`, config.speed, config.time);
|
|
250
|
-
|
|
365
|
+
if (config.crop !== undefined) {
|
|
366
|
+
validateCrop(`Overlay "${name}" (${path})`, config.crop);
|
|
367
|
+
}
|
|
368
|
+
const { sourceStart, sourceEnd } = resolveSourceTrim(`Overlay "${name}" (${path})`, config.start, config.end);
|
|
369
|
+
registerAssetPath(path);
|
|
251
370
|
return createFileOverlayController(name, {
|
|
252
371
|
kind: 'video',
|
|
253
372
|
path,
|
|
254
373
|
...(placement !== undefined && { placement }),
|
|
255
374
|
fullScreen,
|
|
375
|
+
...(pinToScreen && { pinToScreen: true }),
|
|
256
376
|
...(config.volume !== undefined && { audio: config.volume }),
|
|
257
377
|
...(config.speed !== undefined && { speed: config.speed }),
|
|
258
378
|
...(config.time !== undefined && { time: config.time }),
|
|
379
|
+
...(config.crop !== undefined && { crop: config.crop }),
|
|
380
|
+
...(sourceStart !== undefined && { sourceStart }),
|
|
381
|
+
...(sourceEnd !== undefined && { sourceEnd }),
|
|
259
382
|
});
|
|
260
383
|
}
|
|
261
384
|
throw new Error(`[screenci] Overlay "${name}" must use one of: .html, .svg, .png, .mp4. Received: ${path}`);
|
|
@@ -428,6 +551,65 @@ export function buildStudioOverlays(names) {
|
|
|
428
551
|
}
|
|
429
552
|
return result;
|
|
430
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Build overlay controllers for a `video.overlays(...)` declaration. Studio
|
|
556
|
+
* (array) names become Studio-managed controllers; code (object) names resolve
|
|
557
|
+
* their config for the active language (`byLang[language] ?? shared`) and become
|
|
558
|
+
* regular overlay controllers. Per-language mode realizes one language per pass,
|
|
559
|
+
* so the active-language config is the one captured.
|
|
560
|
+
*/
|
|
561
|
+
export function buildOverlays(feature, language) {
|
|
562
|
+
const result = {};
|
|
563
|
+
if (!feature)
|
|
564
|
+
return result;
|
|
565
|
+
for (const name of feature.studioNames) {
|
|
566
|
+
result[name] = createStudioAssetController(name);
|
|
567
|
+
}
|
|
568
|
+
for (const name of feature.codeNames) {
|
|
569
|
+
const input = (language !== undefined ? feature.byLang[language]?.[name] : undefined) ??
|
|
570
|
+
feature.shared[name];
|
|
571
|
+
if (input === undefined)
|
|
572
|
+
continue;
|
|
573
|
+
result[name] = buildOverlayController(name, input);
|
|
574
|
+
}
|
|
575
|
+
return result;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Resolves a string overlay position into the anchor recorded on the asset start.
|
|
579
|
+
* Throws on a non-string value so callers that meant a numeric duration are not
|
|
580
|
+
* silently misrouted into the parser.
|
|
581
|
+
*/
|
|
582
|
+
function resolveOverlayAnchor(until) {
|
|
583
|
+
if (typeof until !== 'string') {
|
|
584
|
+
throw new Error(`overlay positions must be a string such as '0:10' or '56%', got ${typeof until}`);
|
|
585
|
+
}
|
|
586
|
+
const parsed = parseTimelineOffset(until);
|
|
587
|
+
return parsed.kind === 'percent'
|
|
588
|
+
? { percent: parsed.fraction }
|
|
589
|
+
: { outputMs: parsed.ms };
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Resolves a relative length string (`.for('2s')` or a config `duration`) into
|
|
593
|
+
* milliseconds. Seconds and timecodes only: a percentage is rejected because a
|
|
594
|
+
* relative length has nothing to take a percentage of (use `.until('<n>%')` for
|
|
595
|
+
* an absolute position instead).
|
|
596
|
+
*/
|
|
597
|
+
function resolveRelativeDuration(value, label) {
|
|
598
|
+
if (typeof value !== 'string') {
|
|
599
|
+
throw new Error(`[screenci] ${label} must be a time string such as '2s' or '0:02', got ${typeof value}.`);
|
|
600
|
+
}
|
|
601
|
+
const parsed = parseTimelineOffset(value);
|
|
602
|
+
if (parsed.kind === 'percent') {
|
|
603
|
+
throw new Error(`[screenci] ${label} cannot be a percentage ('${value}'); a relative length needs a concrete time like '2s' or '0:02'. Use .until('${value}') for an absolute position.`);
|
|
604
|
+
}
|
|
605
|
+
return parsed.ms;
|
|
606
|
+
}
|
|
607
|
+
/** Parses an optional config `duration` (a relative time string) into ms. */
|
|
608
|
+
function resolveConfigDuration(name, duration) {
|
|
609
|
+
if (duration === undefined)
|
|
610
|
+
return undefined;
|
|
611
|
+
return resolveRelativeDuration(duration, `Overlay "${name}" duration`);
|
|
612
|
+
}
|
|
431
613
|
function createActiveAssetRun(startedWithExplicitStart) {
|
|
432
614
|
let resolve;
|
|
433
615
|
const finished = new Promise((resolveFn) => {
|
|
@@ -478,19 +660,27 @@ prepare) {
|
|
|
478
660
|
endLiveAsset(name, 'wait');
|
|
479
661
|
await run.finished;
|
|
480
662
|
};
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const mode = {
|
|
486
|
-
type: 'blocking',
|
|
487
|
-
...(durationMs !== undefined && { durationMs }),
|
|
488
|
-
};
|
|
663
|
+
// A blocking overlay holds a frozen frame. It never registers a live run and
|
|
664
|
+
// never ends overlays that are already live, so it can run while other overlays
|
|
665
|
+
// stay composited across the frame.
|
|
666
|
+
const runBlocking = async (mode) => {
|
|
489
667
|
await validate();
|
|
490
668
|
await prepare?.(mode);
|
|
491
669
|
const recorder = getRuntimeAssetRecorder();
|
|
492
670
|
emitStart(recorder, mode);
|
|
671
|
+
};
|
|
672
|
+
const controller = (async () => {
|
|
673
|
+
// Bare call: hold for the source's natural length. Length-less overlays
|
|
674
|
+
// (image/html/element) reject this downstream and must use .for()/.until().
|
|
675
|
+
await runBlocking({ type: 'blocking' });
|
|
676
|
+
});
|
|
677
|
+
controller.for = (duration) => runBlocking({
|
|
678
|
+
type: 'blocking',
|
|
679
|
+
durationMs: resolveRelativeDuration(duration, `Overlay "${name}" .for(duration)`),
|
|
493
680
|
});
|
|
681
|
+
// A string position sets an absolute point to stay visible until (resolved at
|
|
682
|
+
// render time, so a percentage is kept symbolic).
|
|
683
|
+
controller.until = (position) => runBlocking({ type: 'blocking', until: resolveOverlayAnchor(position) });
|
|
494
684
|
controller.start = () => start(true);
|
|
495
685
|
controller.end = end;
|
|
496
686
|
return controller;
|
|
@@ -498,6 +688,60 @@ prepare) {
|
|
|
498
688
|
function createStudioAssetController(name) {
|
|
499
689
|
return createAssetControllerCore(name, () => Promise.resolve(), (recorder) => recorder.addStudioAssetStart(name));
|
|
500
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* Builds the controller for a {@link selected} render-dependency overlay. No
|
|
693
|
+
* local file is read: the controller emits an `assetStart` carrying the target's
|
|
694
|
+
* name (an {@link OverlayDependencyRef}); the backend resolves it to a concrete
|
|
695
|
+
* output at render time. Placement is fixed (no `over`), so it is resolved up
|
|
696
|
+
* front. A blocking call needs a duration (from the call or config); a
|
|
697
|
+
* `start()`/`end()` window needs none.
|
|
698
|
+
*/
|
|
699
|
+
function createDependencyOverlayController(name, input) {
|
|
700
|
+
// `over`/`margin` are not in DependencyOverlayOptions, so placement is always
|
|
701
|
+
// fixed here (an embedded render has no live element to size against).
|
|
702
|
+
const placement = resolveOverlayPlacement(name, input.config);
|
|
703
|
+
const fullScreen = input.config.fill === 'screen';
|
|
704
|
+
const pinToScreen = input.config.pinToScreen === true;
|
|
705
|
+
const configDurationMs = resolveConfigDuration(name, input.config.duration);
|
|
706
|
+
if (input.config.crop !== undefined) {
|
|
707
|
+
validateCrop(`Dependency overlay "${name}"`, input.config.crop);
|
|
708
|
+
}
|
|
709
|
+
// start/end are valid only when the target resolves to a VIDEO; the backend
|
|
710
|
+
// rejects them for a screenshot dependency (the medium is unknown until then).
|
|
711
|
+
const { sourceStart, sourceEnd } = resolveSourceTrim(`Dependency overlay "${name}"`, input.config.start, input.config.end);
|
|
712
|
+
return createAssetControllerCore(name, () => Promise.resolve(), (recorder, mode) => {
|
|
713
|
+
let durationMs;
|
|
714
|
+
let until;
|
|
715
|
+
if (mode.type === 'blocking') {
|
|
716
|
+
if (mode.until !== undefined) {
|
|
717
|
+
until = mode.until;
|
|
718
|
+
}
|
|
719
|
+
else {
|
|
720
|
+
// No length => natural duration. Valid for a video dependency; the
|
|
721
|
+
// backend rejects a screenshot dependency with no length when it
|
|
722
|
+
// resolves the concrete medium.
|
|
723
|
+
durationMs = mode.durationMs ?? configDurationMs;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
recorder.addAssetStart(name, {
|
|
727
|
+
kind: 'dependency',
|
|
728
|
+
dependency: {
|
|
729
|
+
name: input.name,
|
|
730
|
+
...(input.config.inheritSubtitles === true && {
|
|
731
|
+
inheritSubtitles: true,
|
|
732
|
+
}),
|
|
733
|
+
},
|
|
734
|
+
...(durationMs !== undefined && { durationMs }),
|
|
735
|
+
...timelineAnchorFields(until),
|
|
736
|
+
fullScreen,
|
|
737
|
+
...(pinToScreen && { pinToScreen: true }),
|
|
738
|
+
...(placement !== undefined && { placement }),
|
|
739
|
+
...(input.config.crop !== undefined && { crop: input.config.crop }),
|
|
740
|
+
...(sourceStart !== undefined && { sourceStart }),
|
|
741
|
+
...(sourceEnd !== undefined && { sourceEnd }),
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
}
|
|
501
745
|
function createFileOverlayController(name, resolved) {
|
|
502
746
|
return createAssetControllerCore(name, async () => {
|
|
503
747
|
const testFilePath = getScreenCIRuntimeContext().testFilePath;
|
|
@@ -512,7 +756,7 @@ function createFileOverlayController(name, resolved) {
|
|
|
512
756
|
* An overlay rendered to a transparent PNG at recording time, from either an
|
|
513
757
|
* HTML file or a React element. `getMarkup` produces the HTML to rasterize.
|
|
514
758
|
*/
|
|
515
|
-
function createRenderedOverlayController(name, getMarkup, placementSource, fullScreen, durationMs, renderOpts = {}) {
|
|
759
|
+
function createRenderedOverlayController(name, getMarkup, placementSource, fullScreen, pinToScreen, durationMs, renderOpts = {}) {
|
|
516
760
|
// The markup and placement are resolved during the test (cheap:
|
|
517
761
|
// renderToStaticMarkup / a string, plus a boundingBox read for `over`), but
|
|
518
762
|
// rasterization (a browser screenshot) is deferred to after the test so
|
|
@@ -527,19 +771,27 @@ function createRenderedOverlayController(name, getMarkup, placementSource, fullS
|
|
|
527
771
|
if (skipped || resolvedHtml === undefined)
|
|
528
772
|
return;
|
|
529
773
|
let durationMsForEvent;
|
|
774
|
+
let until;
|
|
530
775
|
if (mode.type === 'blocking') {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
776
|
+
if (mode.until !== undefined) {
|
|
777
|
+
until = mode.until;
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
durationMsForEvent = mode.durationMs ?? durationMs;
|
|
781
|
+
if (durationMsForEvent === undefined) {
|
|
782
|
+
throw new Error(`[screenci] Overlay "${name}" needs a length: use .for('2s'), .until('0:05'), set "duration" in the config, or drive it with .start()/.end().`);
|
|
783
|
+
}
|
|
784
|
+
validateDurationMs(name, `overlay "${name}"`, durationMsForEvent);
|
|
534
785
|
}
|
|
535
|
-
validateDurationMs(name, `overlay "${name}"`, durationMsForEvent);
|
|
536
786
|
}
|
|
537
787
|
recorder.addPendingAssetStart(name, {
|
|
538
788
|
kind: 'image',
|
|
539
789
|
...(durationMsForEvent !== undefined && {
|
|
540
790
|
durationMs: durationMsForEvent,
|
|
541
791
|
}),
|
|
792
|
+
...timelineAnchorFields(until),
|
|
542
793
|
fullScreen,
|
|
794
|
+
...(pinToScreen && { pinToScreen: true }),
|
|
543
795
|
...(resolvedPlacement !== undefined && {
|
|
544
796
|
placement: resolvedPlacement,
|
|
545
797
|
}),
|
|
@@ -580,20 +832,23 @@ function createRenderedOverlayController(name, getMarkup, placementSource, fullS
|
|
|
580
832
|
* the recording, the old "hide block" that cut capture wall-clock from the
|
|
581
833
|
* timeline is no longer needed.
|
|
582
834
|
*/
|
|
583
|
-
function createAnimatedOverlayController(name, getMarkup, placementSource, fullScreen, fps, configDurationMs, renderOpts = {}) {
|
|
835
|
+
function createAnimatedOverlayController(name, getMarkup, placementSource, fullScreen, pinToScreen, fps, configDurationMs, renderOpts = {}) {
|
|
584
836
|
let resolved;
|
|
585
837
|
let skipped = false;
|
|
586
838
|
const resolveDurationMs = (mode) => {
|
|
587
839
|
if (mode.type === 'blocking') {
|
|
840
|
+
if (mode.until !== undefined) {
|
|
841
|
+
throw new Error(`[screenci] Animated overlay "${name}" cannot use .until('0:10'); its capture length must be fixed. Use .for('2s'), set "duration" in the config, or drive it with .start()/.end() (with "duration" in the config).`);
|
|
842
|
+
}
|
|
588
843
|
const durationMs = mode.durationMs ?? configDurationMs;
|
|
589
844
|
if (durationMs === undefined) {
|
|
590
|
-
throw new Error(`[screenci] Animated overlay "${name}" needs a
|
|
845
|
+
throw new Error(`[screenci] Animated overlay "${name}" needs a length: use .for('2s'), set "duration" in the config, or drive it with .start()/.end() (with "duration" in the config).`);
|
|
591
846
|
}
|
|
592
847
|
validateDurationMs(name, `overlay "${name}"`, durationMs);
|
|
593
848
|
return durationMs;
|
|
594
849
|
}
|
|
595
850
|
if (configDurationMs === undefined) {
|
|
596
|
-
throw new Error(`[screenci] Animated overlay "${name}" driven with .start()/.end() needs
|
|
851
|
+
throw new Error(`[screenci] Animated overlay "${name}" driven with .start()/.end() needs "duration" in its config (the capture length is otherwise unknown).`);
|
|
597
852
|
}
|
|
598
853
|
validateDurationMs(name, `overlay "${name}"`, configDurationMs);
|
|
599
854
|
return configDurationMs;
|
|
@@ -603,8 +858,13 @@ function createAnimatedOverlayController(name, getMarkup, placementSource, fullS
|
|
|
603
858
|
return;
|
|
604
859
|
recorder.addPendingAssetStart(name, {
|
|
605
860
|
kind: 'animation',
|
|
606
|
-
|
|
861
|
+
// Always carry the capture length, for blocking and live overlays alike.
|
|
862
|
+
// A live animated overlay plays out to this length (the renderer holds a
|
|
863
|
+
// frozen-frame tail when it is longer than the start()/end() window), so
|
|
864
|
+
// it needs the duration even when an assetEnd also bounds the window.
|
|
865
|
+
durationMs: resolved.durationMs,
|
|
607
866
|
fullScreen,
|
|
867
|
+
...(pinToScreen && { pinToScreen: true }),
|
|
608
868
|
...(resolved.placement !== undefined && {
|
|
609
869
|
placement: resolved.placement,
|
|
610
870
|
}),
|
|
@@ -836,31 +1096,52 @@ function sizeWrapMarkup(html, size) {
|
|
|
836
1096
|
function toRecordedFileStart(name, resolved, mode) {
|
|
837
1097
|
if (resolved.kind === 'image') {
|
|
838
1098
|
let durationMs;
|
|
1099
|
+
let until;
|
|
839
1100
|
if (mode.type === 'blocking') {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1101
|
+
if (mode.until !== undefined) {
|
|
1102
|
+
until = mode.until;
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
durationMs = mode.durationMs ?? resolved.durationMs;
|
|
1106
|
+
if (durationMs === undefined) {
|
|
1107
|
+
throw new Error(`[screenci] Overlay "${name}" (${resolved.path}) needs a length: use .for('2s'), .until('0:05'), set "duration" in the config, or drive it with .start()/.end().`);
|
|
1108
|
+
}
|
|
1109
|
+
validateDurationMs(name, resolved.path, durationMs);
|
|
843
1110
|
}
|
|
844
|
-
validateDurationMs(name, resolved.path, durationMs);
|
|
845
1111
|
}
|
|
846
1112
|
return {
|
|
847
1113
|
kind: 'image',
|
|
848
1114
|
path: resolved.path,
|
|
849
1115
|
...(durationMs !== undefined && { durationMs }),
|
|
1116
|
+
...timelineAnchorFields(until),
|
|
850
1117
|
fullScreen: resolved.fullScreen,
|
|
1118
|
+
...(resolved.pinToScreen && { pinToScreen: true }),
|
|
851
1119
|
...(resolved.placement !== undefined && {
|
|
852
1120
|
placement: resolved.placement,
|
|
853
1121
|
}),
|
|
1122
|
+
...(resolved.crop !== undefined && { crop: resolved.crop }),
|
|
854
1123
|
};
|
|
855
1124
|
}
|
|
1125
|
+
if (mode.type === 'blocking' && mode.until !== undefined) {
|
|
1126
|
+
throw new Error(`[screenci] Overlay "${name}" (${resolved.path}) is a video and cannot use .until('0:10'); a video overlay plays for its natural length. Drive it with .start()/.end() to control its window.`);
|
|
1127
|
+
}
|
|
1128
|
+
if (mode.type === 'blocking' && mode.durationMs !== undefined) {
|
|
1129
|
+
throw new Error(`[screenci] Overlay "${name}" (${resolved.path}) is a video and cannot use .for('2s'); a video overlay plays for its natural length. Use a bare call (overlays.${name}()), speed/time to re-time it, or .start()/.end() to control its window.`);
|
|
1130
|
+
}
|
|
856
1131
|
return {
|
|
857
1132
|
kind: 'video',
|
|
858
1133
|
path: resolved.path,
|
|
859
1134
|
audio: resolved.audio ?? 1,
|
|
860
1135
|
fullScreen: resolved.fullScreen,
|
|
1136
|
+
...(resolved.pinToScreen && { pinToScreen: true }),
|
|
861
1137
|
...(resolved.placement !== undefined && { placement: resolved.placement }),
|
|
862
1138
|
...(resolved.speed !== undefined && { speed: resolved.speed }),
|
|
863
1139
|
...(resolved.time !== undefined && { time: resolved.time }),
|
|
1140
|
+
...(resolved.crop !== undefined && { crop: resolved.crop }),
|
|
1141
|
+
...(resolved.sourceStart !== undefined && {
|
|
1142
|
+
sourceStart: resolved.sourceStart,
|
|
1143
|
+
}),
|
|
1144
|
+
...(resolved.sourceEnd !== undefined && { sourceEnd: resolved.sourceEnd }),
|
|
864
1145
|
};
|
|
865
1146
|
}
|
|
866
1147
|
//# sourceMappingURL=asset.js.map
|