@getvitops/utils 8.0.1 → 8.2.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +289 -8
- package/dist/{adc-BFrzpMHq.d.mts → adc-BoEoogyo.d.mts} +2 -3
- package/dist/adc-BoEoogyo.d.mts.map +1 -0
- package/dist/ads/index.d.mts +50 -17
- package/dist/ads/index.d.mts.map +1 -1
- package/dist/ads/index.mjs +77 -8
- package/dist/ads/index.mjs.map +1 -1
- package/dist/cloudflare-BiK61Oq4.d.mts.map +1 -1
- package/dist/{cloudflare-CgAsXiqe.mjs → cloudflare-CD8vwrgj.mjs} +2 -24
- package/dist/cloudflare-CD8vwrgj.mjs.map +1 -0
- package/dist/color/index.d.mts +34 -35
- package/dist/color/index.d.mts.map +1 -1
- package/dist/color/index.mjs +3 -1
- package/dist/color/index.mjs.map +1 -1
- package/dist/crm/index.d.mts +288 -0
- package/dist/crm/index.d.mts.map +1 -0
- package/dist/crm/index.mjs +731 -0
- package/dist/crm/index.mjs.map +1 -0
- package/dist/email/index.d.mts +216 -0
- package/dist/email/index.d.mts.map +1 -0
- package/dist/email/index.mjs +331 -0
- package/dist/email/index.mjs.map +1 -0
- package/dist/favicon.d.mts +8 -9
- package/dist/favicon.d.mts.map +1 -1
- package/dist/git/index.d.mts +65 -0
- package/dist/git/index.d.mts.map +1 -0
- package/dist/git/index.mjs +107 -0
- package/dist/git/index.mjs.map +1 -0
- package/dist/hosting/index.d.mts +184 -0
- package/dist/hosting/index.d.mts.map +1 -0
- package/dist/hosting/index.mjs +553 -0
- package/dist/hosting/index.mjs.map +1 -0
- package/dist/identity-BoRddTx3.mjs +35 -0
- package/dist/identity-BoRddTx3.mjs.map +1 -0
- package/dist/index.d.mts +24 -25
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +5 -5
- package/dist/index.mjs.map +1 -1
- package/dist/indexing/index.d.mts +23 -24
- package/dist/indexing/index.d.mts.map +1 -1
- package/dist/media/index.d.mts +24 -24
- package/dist/media/index.d.mts.map +1 -1
- package/dist/media/index.mjs +3 -3
- package/dist/media/index.mjs.map +1 -1
- package/dist/notify/index.d.mts +26 -15
- package/dist/notify/index.d.mts.map +1 -1
- package/dist/notify/index.mjs +50 -20
- package/dist/notify/index.mjs.map +1 -1
- package/dist/onboarding/index.d.mts +10 -141
- package/dist/onboarding/index.d.mts.map +1 -1
- package/dist/onboarding/index.mjs +2 -144
- package/dist/onboarding/index.mjs.map +1 -1
- package/dist/plan-BLKikoOG.mjs +150 -0
- package/dist/plan-BLKikoOG.mjs.map +1 -0
- package/dist/plan-Hq8Pb0Uw.d.mts +133 -0
- package/dist/plan-Hq8Pb0Uw.d.mts.map +1 -0
- package/dist/plan-format-A18SvJ7C.mjs +24 -0
- package/dist/plan-format-A18SvJ7C.mjs.map +1 -0
- package/dist/reviews/index.d.mts +12 -13
- package/dist/reviews/index.d.mts.map +1 -1
- package/dist/schema/index.d.mts +7 -7
- package/dist/schema/index.d.mts.map +1 -1
- package/dist/security/index.d.mts +2 -2
- package/dist/security/index.d.mts.map +1 -1
- package/dist/{token-Bz_-igw-.d.mts → token-DmmoQ7um.d.mts} +6 -3
- package/dist/token-DmmoQ7um.d.mts.map +1 -0
- package/dist/tracking/index.d.mts +43 -17
- package/dist/tracking/index.d.mts.map +1 -1
- package/dist/tracking/index.mjs +2 -1
- package/dist/tracking/index.mjs.map +1 -1
- package/dist/{types-CHHjzRrv.d.mts → types-7ELci8RQ.d.mts} +4 -2
- package/dist/types-7ELci8RQ.d.mts.map +1 -0
- package/package.json +18 -4
- package/dist/adc-BFrzpMHq.d.mts.map +0 -1
- package/dist/cloudflare-CgAsXiqe.mjs.map +0 -1
- package/dist/token-Bz_-igw-.d.mts.map +0 -1
- package/dist/types-CHHjzRrv.d.mts.map +0 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { a as SCOPES, c as googleHeaders, i as GoogleOAuth, n as GoogleAuthLike, o as ServiceAccount, r as GoogleCredential, s as googleAccessToken, t as GoogleAuth } from "../token-
|
|
2
|
-
import { n as adcQuotaProject, r as parseAdcUser, t as adcCredentialsPath } from "../adc-
|
|
3
|
-
|
|
1
|
+
import { a as SCOPES, c as googleHeaders, i as GoogleOAuth, n as GoogleAuthLike, o as ServiceAccount, r as GoogleCredential, s as googleAccessToken, t as GoogleAuth } from "../token-DmmoQ7um.mjs";
|
|
2
|
+
import { n as adcQuotaProject, r as parseAdcUser, t as adcCredentialsPath } from "../adc-BoEoogyo.mjs";
|
|
4
3
|
//#region src/indexing/types.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* The indexing module's own option surface.
|
|
@@ -61,13 +60,13 @@ interface Snapshot {
|
|
|
61
60
|
}
|
|
62
61
|
//#endregion
|
|
63
62
|
//#region src/indexing/plan.d.ts
|
|
64
|
-
declare const INDEXNOW_ENDPOINT = "https://api.indexnow.org/indexnow";
|
|
63
|
+
export declare const INDEXNOW_ENDPOINT = "https://api.indexnow.org/indexnow";
|
|
65
64
|
/**
|
|
66
65
|
* IndexNow's own cap per submission. Batches larger than this are split rather
|
|
67
66
|
* than truncated — a silently dropped tail is the failure mode this command is
|
|
68
67
|
* meant to eliminate, not reproduce.
|
|
69
68
|
*/
|
|
70
|
-
declare const INDEXNOW_BATCH = 10000;
|
|
69
|
+
export declare const INDEXNOW_BATCH = 10000;
|
|
71
70
|
/** Why the URL set is what it is. */
|
|
72
71
|
type UrlReason = 'explicit' | 'all' | 'cold' | 'diff';
|
|
73
72
|
/** A channel's decision: on, or off with a stated reason. */
|
|
@@ -121,9 +120,9 @@ interface PlanInput {
|
|
|
121
120
|
* a flat `<urlset>` on a small site, which `parseSitemap` handles by reading the
|
|
122
121
|
* root element rather than the name.
|
|
123
122
|
*/
|
|
124
|
-
declare function resolveSitemapUrl(config: IndexingConfig): string | undefined;
|
|
123
|
+
export declare function resolveSitemapUrl(config: IndexingConfig): string | undefined;
|
|
125
124
|
/** Resolve the IndexNow key file URL. */
|
|
126
|
-
declare function resolveKeyLocation(config: IndexingConfig): string | undefined;
|
|
125
|
+
export declare function resolveKeyLocation(config: IndexingConfig): string | undefined;
|
|
127
126
|
/**
|
|
128
127
|
* Decide what this run should do.
|
|
129
128
|
*
|
|
@@ -132,9 +131,9 @@ declare function resolveKeyLocation(config: IndexingConfig): string | undefined;
|
|
|
132
131
|
* the diff, a cold snapshot is the tool having no basis to narrow, and the diff is
|
|
133
132
|
* the normal path. Each is reported in `reason` so `--dry` explains itself.
|
|
134
133
|
*/
|
|
135
|
-
declare function plan(input: PlanInput): IndexingPlan;
|
|
134
|
+
export declare function plan(input: PlanInput): IndexingPlan;
|
|
136
135
|
/** Render a plan as the human-readable block `--dry` prints. */
|
|
137
|
-
declare function formatPlan(p: IndexingPlan): string;
|
|
136
|
+
export declare function formatPlan(p: IndexingPlan): string;
|
|
138
137
|
//#endregion
|
|
139
138
|
//#region src/indexing/sitemap.d.ts
|
|
140
139
|
/** What a sitemap document turned out to be. */
|
|
@@ -152,7 +151,7 @@ interface ParsedSitemap {
|
|
|
152
151
|
* (@astrojs/sitemap emits an index only past `entryLimit`) that keying on the name
|
|
153
152
|
* would misread the single-file case as "no URLs".
|
|
154
153
|
*/
|
|
155
|
-
declare function parseSitemap(xml: string): ParsedSitemap;
|
|
154
|
+
export declare function parseSitemap(xml: string): ParsedSitemap;
|
|
156
155
|
/** Reads a sitemap from a URL or a local path. */
|
|
157
156
|
type SitemapReader = (source: string) => Promise<string>;
|
|
158
157
|
/**
|
|
@@ -163,14 +162,14 @@ type SitemapReader = (source: string) => Promise<string>;
|
|
|
163
162
|
* Child fetches run sequentially: a large site's index can list dozens of files,
|
|
164
163
|
* and hammering the consumer's own origin to save a second is a poor trade.
|
|
165
164
|
*/
|
|
166
|
-
declare function collectEntries(source: string, read: SitemapReader): Promise<{
|
|
165
|
+
export declare function collectEntries(source: string, read: SitemapReader): Promise<{
|
|
167
166
|
entries: SitemapEntry[];
|
|
168
167
|
sources: string[];
|
|
169
168
|
}>;
|
|
170
169
|
//#endregion
|
|
171
170
|
//#region src/indexing/snapshot.d.ts
|
|
172
171
|
/** Default location, relative to the consumer's project root. */
|
|
173
|
-
declare const SNAPSHOT_PATH = ".vitops/sitemap-snapshot.json";
|
|
172
|
+
export declare const SNAPSHOT_PATH = ".vitops/sitemap-snapshot.json";
|
|
174
173
|
/**
|
|
175
174
|
* Read a snapshot, or `undefined` when there isn't a usable one.
|
|
176
175
|
*
|
|
@@ -179,9 +178,9 @@ declare const SNAPSHOT_PATH = ".vitops/sitemap-snapshot.json";
|
|
|
179
178
|
* a site that legitimately has no URLs and submit nothing — the silent-success
|
|
180
179
|
* failure this whole command exists to remove.
|
|
181
180
|
*/
|
|
182
|
-
declare function readSnapshot(path: string): Snapshot | undefined;
|
|
181
|
+
export declare function readSnapshot(path: string): Snapshot | undefined;
|
|
183
182
|
/** Build a snapshot from the entries just read. */
|
|
184
|
-
declare function toSnapshot(sitemapUrl: string, entries: SitemapEntry[], takenAt: string): Snapshot;
|
|
183
|
+
export declare function toSnapshot(sitemapUrl: string, entries: SitemapEntry[], takenAt: string): Snapshot;
|
|
185
184
|
/**
|
|
186
185
|
* Write the snapshot, creating its directory.
|
|
187
186
|
*
|
|
@@ -190,13 +189,13 @@ declare function toSnapshot(sitemapUrl: string, entries: SitemapEntry[], takenAt
|
|
|
190
189
|
* it, those URLs would never be submitted again — a single transient 503 would
|
|
191
190
|
* silently drop a page from every future run.
|
|
192
191
|
*/
|
|
193
|
-
declare function writeSnapshot(path: string, snapshot: Snapshot): void;
|
|
192
|
+
export declare function writeSnapshot(path: string, snapshot: Snapshot): void;
|
|
194
193
|
//#endregion
|
|
195
194
|
//#region src/indexing/indexnow.d.ts
|
|
196
195
|
/** Generate a key. 32 hex characters, the conventional shape. */
|
|
197
|
-
declare function newKey(): string;
|
|
196
|
+
export declare function newKey(): string;
|
|
198
197
|
/** The body of the key file served at `keyLocation`: the key, and nothing else. */
|
|
199
|
-
declare const keyFileContents: (key: string) => string;
|
|
198
|
+
export declare const keyFileContents: (key: string) => string;
|
|
200
199
|
interface SubmitResult {
|
|
201
200
|
ok: boolean;
|
|
202
201
|
status: number;
|
|
@@ -212,7 +211,7 @@ interface SubmitResult {
|
|
|
212
211
|
* contents are stale is accepted with `202` and then silently discarded. Checking
|
|
213
212
|
* first is the only way to tell "submitted" from "submitted and ignored".
|
|
214
213
|
*/
|
|
215
|
-
declare function verifyKeyFile(keyLocation: string, key: string, fetchImpl?: typeof fetch): Promise<{
|
|
214
|
+
export declare function verifyKeyFile(keyLocation: string, key: string, fetchImpl?: typeof fetch): Promise<{
|
|
216
215
|
ok: boolean;
|
|
217
216
|
reason?: string;
|
|
218
217
|
}>;
|
|
@@ -220,7 +219,7 @@ declare function verifyKeyFile(keyLocation: string, key: string, fetchImpl?: typ
|
|
|
220
219
|
* Submit one batch. Callers iterate `plan.batches` — the split is the planner's
|
|
221
220
|
* job, so that what gets sent is decided in the pure layer and visible in `--dry`.
|
|
222
221
|
*/
|
|
223
|
-
declare function submitBatch(plan: IndexNowPlan, urls: string[], fetchImpl?: typeof fetch): Promise<SubmitResult>;
|
|
222
|
+
export declare function submitBatch(plan: IndexNowPlan, urls: string[], fetchImpl?: typeof fetch): Promise<SubmitResult>;
|
|
224
223
|
//#endregion
|
|
225
224
|
//#region src/indexing/gsc.d.ts
|
|
226
225
|
/**
|
|
@@ -231,13 +230,13 @@ declare function submitBatch(plan: IndexNowPlan, urls: string[], fetchImpl?: typ
|
|
|
231
230
|
* convention, and what a local `gcloud` setup already has). Supporting both means
|
|
232
231
|
* neither environment has to be reshaped to fit the tool.
|
|
233
232
|
*/
|
|
234
|
-
declare function parseServiceAccount(raw: string): ServiceAccount;
|
|
233
|
+
export declare function parseServiceAccount(raw: string): ServiceAccount;
|
|
235
234
|
/**
|
|
236
235
|
* Exchange a service-account key for an access token (RS256 JWT bearer flow).
|
|
237
236
|
*
|
|
238
237
|
* `now` is a parameter so this is testable without mocking the clock.
|
|
239
238
|
*/
|
|
240
|
-
declare function serviceAccountToken(account: ServiceAccount, fetchImpl?: typeof fetch, now?: number): Promise<string>;
|
|
239
|
+
export declare function serviceAccountToken(account: ServiceAccount, fetchImpl?: typeof fetch, now?: number): Promise<string>;
|
|
241
240
|
/**
|
|
242
241
|
* Re-submit a sitemap — the automated form of the manual resubmit in the UI.
|
|
243
242
|
*
|
|
@@ -245,7 +244,7 @@ declare function serviceAccountToken(account: ServiceAccount, fetchImpl?: typeof
|
|
|
245
244
|
* (`sc-domain:acme.ca` vs `https://acme.ca/`) or the service account isn't an
|
|
246
245
|
* owner of it, rather than that the sitemap is bad.
|
|
247
246
|
*/
|
|
248
|
-
declare function submitSitemap(auth: GoogleAuthLike, siteUrl: string, sitemapUrl: string, fetchImpl?: typeof fetch): Promise<{
|
|
247
|
+
export declare function submitSitemap(auth: GoogleAuthLike, siteUrl: string, sitemapUrl: string, fetchImpl?: typeof fetch): Promise<{
|
|
249
248
|
ok: boolean;
|
|
250
249
|
status: number;
|
|
251
250
|
message?: string;
|
|
@@ -267,7 +266,7 @@ interface InspectionResult {
|
|
|
267
266
|
* anything. Quota is 2000 queries/day per property, which is why the caller works
|
|
268
267
|
* from an explicit `priorityUrls` list rather than the whole sitemap.
|
|
269
268
|
*/
|
|
270
|
-
declare function inspectUrl(auth: GoogleAuthLike, siteUrl: string, inspectionUrl: string, fetchImpl?: typeof fetch): Promise<InspectionResult>;
|
|
269
|
+
export declare function inspectUrl(auth: GoogleAuthLike, siteUrl: string, inspectionUrl: string, fetchImpl?: typeof fetch): Promise<InspectionResult>;
|
|
271
270
|
//#endregion
|
|
272
|
-
export { type ChannelPlan, type GoogleAuth, type GoogleAuthLike, type GoogleCredential, type GoogleOAuth,
|
|
271
|
+
export { type ChannelPlan, type GoogleAuth, type GoogleAuthLike, type GoogleCredential, type GoogleOAuth, type IndexNowConfig, type IndexNowPlan, type IndexingConfig, type IndexingPlan, type InspectionResult, type ParsedSitemap, type PlanInput, SCOPES, type SearchConsoleConfig, type SearchConsolePlan, type ServiceAccount, type SitemapEntry, type SitemapReader, type Snapshot, type SubmitResult, type UrlReason, adcCredentialsPath, adcQuotaProject, googleAccessToken, googleHeaders, parseAdcUser };
|
|
273
272
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/indexing/types.ts","../../src/indexing/plan.ts","../../src/indexing/sitemap.ts","../../src/indexing/snapshot.ts","../../src/indexing/indexnow.ts","../../src/indexing/gsc.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/indexing/types.ts","../../src/indexing/plan.ts","../../src/indexing/sitemap.ts","../../src/indexing/snapshot.ts","../../src/indexing/indexnow.ts","../../src/indexing/gsc.ts"],"mappings":";;;;;;;;;;;;;UAoBiB;;;;;EAKf;;EAEA;;EAEA;;;UAIe;;EAEf;;EAEA;;;UAIe;;EAEf;;EAEA;EACA,WAAW;EACX,gBAAgB;;EAEhB;;;;;EAKA;;;UAIe;EACf;EACA;;;UAIe;EACf;EACA;EACA;;EAEA,SAAS;;;;qBCtDE;;;;;;qBAOA;;KAGD;;UAGK;EACf;;EAEA;;UAGe,qBAAqB;EACpC;EACA;EACA;EACA;;EAEA;;UAGe,0BAA0B;EACzC;;UAGe;;EAEf;;EAEA;;EAEA;EACA,QAAQ;;EAER;EACA,UAAU;EACV,eAAe;;EAEf;;EAEA;;UAGe;EACf,QAAQ;EACR,SAAS;EACT,WAAW;;EAEX;;EAEA;;;;;;;;;;wBA4Bc,kBAAkB,QAAQ;;wBAO1B,mBAAmB,QAAQ;;;;;;;;;wBA6B3B,KAAK,OAAO,YAAY;;wBA6KxB,WAAW,GAAG;;;;UCxSb;;EAEf,SAAS;;EAET;;;;;;;;;;wBA6Cc,aAAa,cAAc;;KAoB/B,iBAAiB,mBAAmB;;;;;;;;;wBAU1B,eACpB,gBACA,MAAM,gBACL;EAAU,SAAS;EAAgB;;;;;qBCjFzB;;;;;;;;;wBAUG,aAAa,eAAe;;wBAK5B,WAAW,oBAAoB,SAAS,gBAAgB,kBAAkB;;;;;;;;;wBAc1E,cAAc,cAAc,UAAU;;;;wBC5BtC;;qBAKH,kBAAmB;UAEf;EACf;EACA;EACA;;EAEA;;;;;;;;;;wBAmCoB,cACpB,qBACA,aACA,mBAAkB,QACjB;EAAU;EAAa;;;;;;wBAqBJ,YACpB,MAAM,cACN,gBACA,mBAAkB,QACjB,QAAQ;;;;;;;;;;;wBCpDK,oBAAoB,cAAc;;;;;;wBAiBlC,oBACd,SAAS,gBACT,mBAAkB,OAClB,eACC;;;;;;;;wBAemB,cACpB,MAAM,gBACN,iBACA,oBACA,mBAAkB,QACjB;EAAU;EAAa;EAAgB;;;UAgBzB;EACf;;EAEA;;EAEA;EACA;;EAEA;EACA;;;;;;;wBAQoB,WACpB,MAAM,gBACN,iBACA,uBACA,mBAAkB,QACjB,QAAQ"}
|
package/dist/media/index.d.mts
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
/** What a source video is turned into. */
|
|
11
11
|
type OutputKind = 'webm' | 'mp4' | 'poster';
|
|
12
12
|
/** The default set: a modern codec, a universal fallback, and a poster frame. */
|
|
13
|
-
declare const DEFAULT_OUTPUTS: OutputKind[];
|
|
13
|
+
export declare const DEFAULT_OUTPUTS: OutputKind[];
|
|
14
14
|
/** File extension per output kind. */
|
|
15
|
-
declare const OUTPUT_EXT: Record<OutputKind, string>;
|
|
15
|
+
export declare const OUTPUT_EXT: Record<OutputKind, string>;
|
|
16
16
|
/**
|
|
17
17
|
* Bumped when the ffmpeg arguments in `ffmpeg.ts` change in a way that alters the
|
|
18
18
|
* output bytes. It is part of the settings hash, so bumping it re-encodes every
|
|
@@ -22,7 +22,7 @@ declare const OUTPUT_EXT: Record<OutputKind, string>;
|
|
|
22
22
|
* the README), so every bump is a large binary diff in someone's repository —
|
|
23
23
|
* that cost is worth paying for a real encoder fix and not for a tidy-up.
|
|
24
24
|
*/
|
|
25
|
-
declare const ENCODER_VERSION = 1;
|
|
25
|
+
export declare const ENCODER_VERSION = 1;
|
|
26
26
|
/** How to encode. Every field here feeds the settings hash. */
|
|
27
27
|
interface MediaConfig {
|
|
28
28
|
/**
|
|
@@ -116,18 +116,18 @@ interface MediaManifest {
|
|
|
116
116
|
//#endregion
|
|
117
117
|
//#region src/media/discover.d.ts
|
|
118
118
|
/** Container formats worth accepting as input. */
|
|
119
|
-
declare const VIDEO_EXT: ReadonlySet<string>;
|
|
119
|
+
export declare const VIDEO_EXT: ReadonlySet<string>;
|
|
120
120
|
/** Directories that never hold hand-managed source assets. */
|
|
121
|
-
declare const DISCOVER_SKIP: ReadonlySet<string>;
|
|
121
|
+
export declare const DISCOVER_SKIP: ReadonlySet<string>;
|
|
122
122
|
/** sha256 of the file contents, streamed. Truncated — 16 hex chars is 64 bits. */
|
|
123
|
-
declare function hashFile(path: string): Promise<string>;
|
|
123
|
+
export declare function hashFile(path: string): Promise<string>;
|
|
124
124
|
/**
|
|
125
125
|
* Every video under `dir`, with its content hash.
|
|
126
126
|
*
|
|
127
127
|
* Sorted by `id` so a plan — and therefore the manifest and the log — comes out in
|
|
128
128
|
* the same order on every machine, regardless of what the filesystem hands back.
|
|
129
129
|
*/
|
|
130
|
-
declare function discoverVideos(dir: string): Promise<MediaSource[]>;
|
|
130
|
+
export declare function discoverVideos(dir: string): Promise<MediaSource[]>;
|
|
131
131
|
//#endregion
|
|
132
132
|
//#region src/media/plan.d.ts
|
|
133
133
|
/** Defaults, resolved once so the settings hash is computed over real values. */
|
|
@@ -157,14 +157,14 @@ interface MediaPlanInput {
|
|
|
157
157
|
force?: boolean | undefined;
|
|
158
158
|
}
|
|
159
159
|
/** Fill in defaults. Exported because the encoder and the hash must agree on them. */
|
|
160
|
-
declare function resolveConfig(config?: MediaConfig): ResolvedMediaConfig;
|
|
160
|
+
export declare function resolveConfig(config?: MediaConfig): ResolvedMediaConfig;
|
|
161
161
|
/**
|
|
162
162
|
* Hash the settings that decide the output bytes.
|
|
163
163
|
*
|
|
164
164
|
* Order-independent in the one place it could bite (`outputs` is sorted), so
|
|
165
165
|
* re-ordering the list in a config doesn't re-encode a whole library.
|
|
166
166
|
*/
|
|
167
|
-
declare function settingsHash(config: ResolvedMediaConfig): string;
|
|
167
|
+
export declare function settingsHash(config: ResolvedMediaConfig): string;
|
|
168
168
|
/**
|
|
169
169
|
* Decide what to encode.
|
|
170
170
|
*
|
|
@@ -178,11 +178,11 @@ declare function settingsHash(config: ResolvedMediaConfig): string;
|
|
|
178
178
|
* the failure it would otherwise cause — two sources overwriting each other every
|
|
179
179
|
* run, each invalidating the other's cache entry — is invisible from the outside.
|
|
180
180
|
*/
|
|
181
|
-
declare function planMedia(input: MediaPlanInput): MediaPlan;
|
|
181
|
+
export declare function planMedia(input: MediaPlanInput): MediaPlan;
|
|
182
182
|
/** The jobs that will actually run. */
|
|
183
|
-
declare const pendingJobs: (plan: MediaPlan) => MediaJob[];
|
|
183
|
+
export declare const pendingJobs: (plan: MediaPlan) => MediaJob[];
|
|
184
184
|
/** Human-readable account of a plan, for `--dry` and for the run summary. */
|
|
185
|
-
declare function formatPlan(plan: MediaPlan): string;
|
|
185
|
+
export declare function formatPlan(plan: MediaPlan): string;
|
|
186
186
|
//#endregion
|
|
187
187
|
//#region src/media/ffmpeg.d.ts
|
|
188
188
|
/**
|
|
@@ -194,7 +194,7 @@ declare function formatPlan(plan: MediaPlan): string;
|
|
|
194
194
|
* output at all, and a build that carries on would ship the page referencing a
|
|
195
195
|
* video that was never encoded.
|
|
196
196
|
*/
|
|
197
|
-
declare function requireFfmpeg(): Promise<void>;
|
|
197
|
+
export declare function requireFfmpeg(): Promise<void>;
|
|
198
198
|
/**
|
|
199
199
|
* VP9/WebM, two passes.
|
|
200
200
|
*
|
|
@@ -203,7 +203,7 @@ declare function requireFfmpeg(): Promise<void>;
|
|
|
203
203
|
* makes two concurrent encodes silently share one log — the second encode's rate
|
|
204
204
|
* control then reads the first clip's statistics.
|
|
205
205
|
*/
|
|
206
|
-
declare function encodeWebm(input: string, output: string, config: ResolvedMediaConfig): Promise<void>;
|
|
206
|
+
export declare function encodeWebm(input: string, output: string, config: ResolvedMediaConfig): Promise<void>;
|
|
207
207
|
/**
|
|
208
208
|
* H.264/MP4 — the fallback that makes `<video>` work on older iOS and in the
|
|
209
209
|
* in-app webviews that still don't decode VP9.
|
|
@@ -212,13 +212,13 @@ declare function encodeWebm(input: string, output: string, config: ResolvedMedia
|
|
|
212
212
|
* download the whole file before it can show frame one, which on a hero video
|
|
213
213
|
* looks exactly like the optimisation didn't work.
|
|
214
214
|
*/
|
|
215
|
-
declare function encodeMp4(input: string, output: string, config: ResolvedMediaConfig): Promise<void>;
|
|
215
|
+
export declare function encodeMp4(input: string, output: string, config: ResolvedMediaConfig): Promise<void>;
|
|
216
216
|
/** The poster frame. `-ss` before `-i` so the seek is a fast one. */
|
|
217
|
-
declare function extractPoster(input: string, output: string, config: ResolvedMediaConfig): Promise<void>;
|
|
217
|
+
export declare function extractPoster(input: string, output: string, config: ResolvedMediaConfig): Promise<void>;
|
|
218
218
|
//#endregion
|
|
219
219
|
//#region src/media/manifest.d.ts
|
|
220
220
|
/** Default location, relative to the consumer's project root. */
|
|
221
|
-
declare const MEDIA_MANIFEST_PATH = ".vitops/media-manifest.json";
|
|
221
|
+
export declare const MEDIA_MANIFEST_PATH = ".vitops/media-manifest.json";
|
|
222
222
|
/**
|
|
223
223
|
* Read the manifest, or `undefined` when there isn't a usable one.
|
|
224
224
|
*
|
|
@@ -227,9 +227,9 @@ declare const MEDIA_MANIFEST_PATH = ".vitops/media-manifest.json";
|
|
|
227
227
|
* shape of bug `readSnapshot` avoids, except here it would be silent: nothing
|
|
228
228
|
* would encode, and the build would reference outputs that were never written.
|
|
229
229
|
*/
|
|
230
|
-
declare function readManifest(path: string): MediaManifest | undefined;
|
|
230
|
+
export declare function readManifest(path: string): MediaManifest | undefined;
|
|
231
231
|
/** Build a manifest with its keys and output lists in a stable order. */
|
|
232
|
-
declare function toManifest(entries: Record<string, MediaManifestEntry>): MediaManifest;
|
|
232
|
+
export declare function toManifest(entries: Record<string, MediaManifestEntry>): MediaManifest;
|
|
233
233
|
/**
|
|
234
234
|
* Write the manifest, creating its directory.
|
|
235
235
|
*
|
|
@@ -237,10 +237,10 @@ declare function toManifest(entries: Record<string, MediaManifestEntry>): MediaM
|
|
|
237
237
|
* source as encoded that isn't, and because the next run trusts this file, the
|
|
238
238
|
* result is a missing video that no rebuild ever fixes.
|
|
239
239
|
*/
|
|
240
|
-
declare function writeManifest(path: string, manifest: MediaManifest): void;
|
|
240
|
+
export declare function writeManifest(path: string, manifest: MediaManifest): void;
|
|
241
241
|
//#endregion
|
|
242
242
|
//#region src/media/index.d.ts
|
|
243
|
-
interface ProcessMediaOptions {
|
|
243
|
+
export interface ProcessMediaOptions {
|
|
244
244
|
/** Directory of unprocessed sources. Walked recursively. */
|
|
245
245
|
raw: string;
|
|
246
246
|
/** Where the encoded outputs go. Structure under `raw` is preserved. */
|
|
@@ -255,7 +255,7 @@ interface ProcessMediaOptions {
|
|
|
255
255
|
/** Called before each source is encoded — these take minutes, so say so. */
|
|
256
256
|
onProgress?: ((message: string) => void) | undefined;
|
|
257
257
|
}
|
|
258
|
-
interface ProcessMediaResult {
|
|
258
|
+
export interface ProcessMediaResult {
|
|
259
259
|
plan: MediaPlan;
|
|
260
260
|
/** Outputs written this run, relative to `out`. */
|
|
261
261
|
written: string[];
|
|
@@ -269,7 +269,7 @@ interface ProcessMediaResult {
|
|
|
269
269
|
* them: `vitops media` and the Astro integration are equally incremental, since
|
|
270
270
|
* they call this same function.
|
|
271
271
|
*/
|
|
272
|
-
declare function processMedia(options: ProcessMediaOptions): Promise<ProcessMediaResult>;
|
|
272
|
+
export declare function processMedia(options: ProcessMediaOptions): Promise<ProcessMediaResult>;
|
|
273
273
|
//#endregion
|
|
274
|
-
export {
|
|
274
|
+
export type { MediaConfig, MediaJob, MediaManifest, MediaManifestEntry, MediaOutput, MediaPlan, MediaPlanInput, MediaSource, OutputKind, OutputProbe, ResolvedMediaConfig };
|
|
275
275
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/media/types.ts","../../src/media/discover.ts","../../src/media/plan.ts","../../src/media/ffmpeg.ts","../../src/media/manifest.ts","../../src/media/index.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/media/types.ts","../../src/media/discover.ts","../../src/media/plan.ts","../../src/media/ffmpeg.ts","../../src/media/manifest.ts","../../src/media/index.ts"],"mappings":";;;;;;;;;;KAUY;;qBAGC,iBAAiB;;qBAGjB,YAAY,OAAO;;;;;;;;;;qBAenB;;UAGI;;;;;;;EAOf;;;;;;;;;EASA;;;;;;;EAOA;;;;;EAKA;;;;;;;EAOA;;EAEA,UAAU;;;UAIK;EACf;;;;;;EAMA;;EAEA;;;UAIe;EACf,MAAM;;EAEN;;;UAIe;EACf,QAAQ;EACR,SAAS;;EAET;;EAEA;;;UAIe;EACf,MAAM;;EAEN;;;UAIe;;EAEf;EACA;;EAEA;;;;;;;UAQe;EACf;EACA,SAAS,eAAe;;;;;qBCjHb,WAAW;;qBAYX,eAAe;;wBAGZ,SAAS,eAAe;;;;;;;wBA4BlB,eAAe,cAAc,QAAQ;;;;UC9B1C;EACf;EACA;EACA;EACA;EACA;EACA,SAAS;;;;;;;;KASC,eAAe;UAEV;EACf,SAAS;EACT,SAAS;;EAET,WAAW;;EAEX,QAAQ;;EAER;;;wBAMc,cAAc,SAAQ,cAAmB;;;;;;;wBAiBzC,aAAa,QAAQ;;;;;;;;;;;;;;wBAuCrB,UAAU,OAAO,iBAAiB;;qBA0CrC,cAAe,MAAM,cAAY;;wBAG9B,WAAW,MAAM;;;;;;;;;;;;wBC1HjB,iBAAiB;;;;;;;;;wBAqEX,WACpB,eACA,gBACA,QAAQ,sBACP;;;;;;;;;wBA2DmB,UACpB,eACA,gBACA,QAAQ,sBACP;;wBAyBmB,cACpB,eACA,gBACA,QAAQ,sBACP;;;;qBC7LU;;;;;;;;;wBAUG,aAAa,eAAe;;wBAK5B,WAAW,SAAS,eAAe,sBAAsB;;;;;;;;wBAgBzD,cAAc,cAAc,UAAU;;;iBCrBrC;;EAEf;;EAEA;EACA,SAAS;;EAET;;EAEA;;EAEA;;EAEA,eAAe;;iBAGA;EACf,MAAM;;EAEN;;EAEA;;;;;;;;;wBAUoB,aAAa,SAAS,sBAAsB,QAAQ"}
|
package/dist/media/index.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { tmpdir } from "node:os";
|
|
|
16
16
|
* why the hash is streamed rather than taken over a `readFileSync`.
|
|
17
17
|
*/
|
|
18
18
|
/** Container formats worth accepting as input. */
|
|
19
|
-
const VIDEO_EXT = new Set([
|
|
19
|
+
const VIDEO_EXT = /* @__PURE__ */ new Set([
|
|
20
20
|
".mp4",
|
|
21
21
|
".mov",
|
|
22
22
|
".m4v",
|
|
@@ -27,7 +27,7 @@ const VIDEO_EXT = new Set([
|
|
|
27
27
|
".mpeg"
|
|
28
28
|
]);
|
|
29
29
|
/** Directories that never hold hand-managed source assets. */
|
|
30
|
-
const DISCOVER_SKIP = new Set([
|
|
30
|
+
const DISCOVER_SKIP = /* @__PURE__ */ new Set([
|
|
31
31
|
"node_modules",
|
|
32
32
|
"dist",
|
|
33
33
|
".git"
|
|
@@ -78,7 +78,7 @@ async function discoverVideos(dir) {
|
|
|
78
78
|
*/
|
|
79
79
|
const exec = promisify(execFile);
|
|
80
80
|
/** `-loglevel error` keeps this small; the ceiling is for a pathological failure. */
|
|
81
|
-
const MAX_BUFFER =
|
|
81
|
+
const MAX_BUFFER = 16777216;
|
|
82
82
|
/**
|
|
83
83
|
* x264 and VP9 use different CRF scales (0–51 against 0–63), so one `crf` knob has
|
|
84
84
|
* to be mapped. 0.72 puts the default 32 at x264's own default of 23, which is the
|
package/dist/media/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/media/discover.ts","../../src/media/ffmpeg.ts","../../src/media/manifest.ts","../../src/media/types.ts","../../src/media/plan.ts","../../src/media/index.ts"],"sourcesContent":["/**\n * Finding the source videos and fingerprinting them.\n *\n * Deliberately not `scanFiles` from `../scan.ts`, which reads every match into\n * memory as a string. That is right for templates and catastrophic for a video —\n * the files this walks are routinely tens or hundreds of megabytes, which is also\n * why the hash is streamed rather than taken over a `readFileSync`.\n */\nimport { createHash } from 'node:crypto';\nimport { createReadStream, readdirSync } from 'node:fs';\nimport { extname, join, relative, sep } from 'node:path';\nimport type { MediaSource } from './types.ts';\n\n/** Container formats worth accepting as input. */\nexport const VIDEO_EXT: ReadonlySet<string> = new Set([\n '.mp4',\n '.mov',\n '.m4v',\n '.mkv',\n '.avi',\n '.webm',\n '.mpg',\n '.mpeg',\n]);\n\n/** Directories that never hold hand-managed source assets. */\nexport const DISCOVER_SKIP: ReadonlySet<string> = new Set(['node_modules', 'dist', '.git']);\n\n/** sha256 of the file contents, streamed. Truncated — 16 hex chars is 64 bits. */\nexport function hashFile(path: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const hash = createHash('sha256');\n const stream = createReadStream(path);\n stream.on('error', reject);\n stream.on('data', (chunk) => hash.update(chunk));\n stream.on('end', () => resolve(hash.digest('hex').slice(0, 16)));\n });\n}\n\nfunction walk(dir: string, acc: string[]): void {\n for (const e of readdirSync(dir, { withFileTypes: true })) {\n if (e.name.startsWith('.')) continue;\n const p = join(dir, e.name);\n if (e.isDirectory()) {\n if (!DISCOVER_SKIP.has(e.name)) walk(p, acc);\n } else if (VIDEO_EXT.has(extname(e.name).toLowerCase())) {\n acc.push(p);\n }\n }\n}\n\n/**\n * Every video under `dir`, with its content hash.\n *\n * Sorted by `id` so a plan — and therefore the manifest and the log — comes out in\n * the same order on every machine, regardless of what the filesystem hands back.\n */\nexport async function discoverVideos(dir: string): Promise<MediaSource[]> {\n const paths: string[] = [];\n walk(dir, paths);\n const sources = await Promise.all(\n paths.map(async (path) => ({\n path,\n id: relative(dir, path).split(sep).join('/'),\n hash: await hashFile(path),\n })),\n );\n return sources.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));\n}\n","/**\n * The encoder. Executes a job and decides nothing.\n *\n * Every invocation goes through `execFile` with an argv array rather than a shell\n * string: a source path containing a space or a quote is ordinary, and the\n * shell-interpolated form these commands are usually written in breaks on both.\n */\nimport { execFile } from 'node:child_process';\nimport { existsSync, mkdtempSync, rmSync, statSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { promisify } from 'node:util';\nimport type { ResolvedMediaConfig } from './plan.ts';\n\nconst exec = promisify(execFile);\n\n/** `-loglevel error` keeps this small; the ceiling is for a pathological failure. */\nconst MAX_BUFFER = 16 * 1024 * 1024;\n\n/**\n * x264 and VP9 use different CRF scales (0–51 against 0–63), so one `crf` knob has\n * to be mapped. 0.72 puts the default 32 at x264's own default of 23, which is the\n * pairing VP9's encoding guide uses for comparable quality.\n */\nconst X264_CRF_RATIO = 0.72;\n\nlet probe: Promise<void> | undefined;\n\n/**\n * Fail early and clearly when ffmpeg isn't installed.\n *\n * Deliberately not the `optimizePng` shape in `favicon.ts`, which swallows a\n * missing `oxipng` and warns once. That is right for a best-effort lossless crush,\n * where skipping costs a few kilobytes. It is wrong here: skipping produces no\n * output at all, and a build that carries on would ship the page referencing a\n * video that was never encoded.\n */\nexport function requireFfmpeg(): Promise<void> {\n probe ??= exec('ffmpeg', ['-version'], { maxBuffer: MAX_BUFFER }).then(\n () => undefined,\n () => {\n probe = undefined; // so a later call re-probes rather than caching a stale failure\n throw new Error(\n 'media: ffmpeg not found on PATH. It is an external tool, not an npm dependency — ' +\n 'install it (macOS: `brew install ffmpeg`, Debian/Ubuntu: `apt install ffmpeg`, ' +\n 'Windows: `winget install Gyan.FFmpeg`) and run this again.',\n );\n },\n );\n return probe;\n}\n\nasync function run(args: string[]): Promise<void> {\n try {\n await exec('ffmpeg', args, { maxBuffer: MAX_BUFFER });\n } catch (err) {\n const stderr = String((err as { stderr?: string }).stderr ?? '').trim();\n const tail = stderr.split('\\n').slice(-6).join('\\n');\n throw new Error(`media: ffmpeg failed${tail ? `\\n${tail}` : ''}`);\n }\n}\n\n/**\n * An encode that \"succeeds\" but writes nothing is the failure mode worth guarding:\n * seeking a poster past the end of a clip does exactly that, and ffmpeg exits 0.\n * Without this the manifest would record the job as done and never retry it.\n */\nfunction assertWritten(file: string, what: string): void {\n if (!existsSync(file) || statSync(file).size === 0)\n throw new Error(\n `media: ${what} produced no output (${file}). Check --poster-time and the source.`,\n );\n}\n\n/** Scale filter, capping width and keeping the height even (yuv420p requires it). */\nfunction scaleArgs(maxWidth: number): string[] {\n if (maxWidth <= 0) return [];\n // ffmpeg parses the quotes itself, so the commas inside min() aren't read as a\n // filter separator. There is no shell here to strip them.\n return ['-vf', `scale='min(${maxWidth},iw)':-2`];\n}\n\n/** `2M` → `4M`. A cap needs a buffer, and twice the rate is the usual choice. */\nfunction bufsize(maxBitrate: string): string {\n const m = /^(\\d+(?:\\.\\d+)?)([kKmM]?)$/.exec(maxBitrate.trim());\n if (!m)\n throw new Error(`media: maxBitrate \"${maxBitrate}\" is not a bitrate (try \"2M\" or \"800k\").`);\n return `${Number(m[1]) * 2}${m[2] ?? ''}`;\n}\n\nconst rateArgs = (maxBitrate: string | undefined): string[] =>\n maxBitrate ? ['-maxrate', maxBitrate, '-bufsize', bufsize(maxBitrate)] : [];\n\nconst audioArgs = (config: ResolvedMediaConfig, codec: string): string[] =>\n config.audio ? ['-c:a', codec, '-b:a', '128k'] : ['-an'];\n\nconst BASE = ['-y', '-nostdin', '-loglevel', 'error'];\n\n/**\n * VP9/WebM, two passes.\n *\n * The pass log goes into a fresh temp directory rather than the working directory.\n * ffmpeg's default (`ffmpeg2pass-0.log`, in cwd) makes cleanup cwd-dependent and\n * makes two concurrent encodes silently share one log — the second encode's rate\n * control then reads the first clip's statistics.\n */\nexport async function encodeWebm(\n input: string,\n output: string,\n config: ResolvedMediaConfig,\n): Promise<void> {\n const dir = mkdtempSync(join(tmpdir(), 'vitops-media-'));\n const passlog = join(dir, 'pass');\n const video = [\n ...scaleArgs(config.maxWidth),\n '-c:v',\n 'libvpx-vp9',\n // -b:v 0 with -crf is VP9's constant-quality mode; with -maxrate it becomes\n // constrained quality. A fixed bitrate would be right at one resolution only.\n '-b:v',\n '0',\n '-crf',\n String(config.crf),\n '-row-mt',\n '1',\n ...rateArgs(config.maxBitrate),\n ];\n try {\n // Pass 1 only gathers statistics, so it never encodes audio and writes no file.\n await run([\n ...BASE,\n '-i',\n input,\n ...video,\n '-an',\n '-pass',\n '1',\n '-passlogfile',\n passlog,\n '-f',\n 'null',\n '-',\n ]);\n await run([\n ...BASE,\n '-i',\n input,\n ...video,\n ...audioArgs(config, 'libopus'),\n '-pass',\n '2',\n '-passlogfile',\n passlog,\n output,\n ]);\n } finally {\n rmSync(dir, { recursive: true, force: true });\n }\n assertWritten(output, 'webm encode');\n}\n\n/**\n * H.264/MP4 — the fallback that makes `<video>` work on older iOS and in the\n * in-app webviews that still don't decode VP9.\n *\n * `+faststart` moves the moov atom to the front. Without it the browser has to\n * download the whole file before it can show frame one, which on a hero video\n * looks exactly like the optimisation didn't work.\n */\nexport async function encodeMp4(\n input: string,\n output: string,\n config: ResolvedMediaConfig,\n): Promise<void> {\n await run([\n ...BASE,\n '-i',\n input,\n ...scaleArgs(config.maxWidth),\n '-c:v',\n 'libx264',\n '-preset',\n 'slow',\n '-crf',\n String(Math.round(config.crf * X264_CRF_RATIO)),\n // Required by Safari and by every hardware decoder worth naming.\n '-pix_fmt',\n 'yuv420p',\n '-movflags',\n '+faststart',\n ...rateArgs(config.maxBitrate),\n ...audioArgs(config, 'aac'),\n output,\n ]);\n assertWritten(output, 'mp4 encode');\n}\n\n/** The poster frame. `-ss` before `-i` so the seek is a fast one. */\nexport async function extractPoster(\n input: string,\n output: string,\n config: ResolvedMediaConfig,\n): Promise<void> {\n await run([\n ...BASE,\n ...(config.posterTime > 0 ? ['-ss', String(config.posterTime)] : []),\n '-i',\n input,\n ...scaleArgs(config.maxWidth),\n '-frames:v',\n '1',\n '-q:v',\n '2',\n output,\n ]);\n assertWritten(output, 'poster extraction');\n}\n","/**\n * The cache — what makes a second run free.\n *\n * A committed artifact, unlike `indexing/`'s snapshot: the recommended layout\n * checks in both the processed outputs and this file, so CI never needs ffmpeg\n * and the outputs only change when someone deliberately re-encodes. That is why\n * `writeManifest` sorts and carries no timestamp — an unchanged run has to produce\n * a byte-identical file, or every build shows up as a diff.\n */\nimport type { MediaManifest, MediaManifestEntry } from './types.ts';\nimport { readVersioned, writeVersioned } from '../json-store.ts';\n\n/** Default location, relative to the consumer's project root. */\nexport const MEDIA_MANIFEST_PATH = '.vitops/media-manifest.json';\n\n/**\n * Read the manifest, or `undefined` when there isn't a usable one.\n *\n * A corrupt or wrong-version file reads as absent, which re-encodes everything.\n * The opposite reading — treating it as an empty but valid manifest — is the same\n * shape of bug `readSnapshot` avoids, except here it would be silent: nothing\n * would encode, and the build would reference outputs that were never written.\n */\nexport function readManifest(path: string): MediaManifest | undefined {\n return readVersioned<MediaManifest, 'entries'>(path, 'entries');\n}\n\n/** Build a manifest with its keys and output lists in a stable order. */\nexport function toManifest(entries: Record<string, MediaManifestEntry>): MediaManifest {\n const sorted: Record<string, MediaManifestEntry> = {};\n for (const key of Object.keys(entries).sort()) {\n const entry = entries[key] as MediaManifestEntry;\n sorted[key] = { ...entry, outputs: [...entry.outputs].sort() };\n }\n return { version: 1, entries: sorted };\n}\n\n/**\n * Write the manifest, creating its directory.\n *\n * Record a job only once its outputs are on disk. Writing eagerly would mark a\n * source as encoded that isn't, and because the next run trusts this file, the\n * result is a missing video that no rebuild ever fixes.\n */\nexport function writeManifest(path: string, manifest: MediaManifest): void {\n writeVersioned(path, manifest);\n}\n","/**\n * The media module's own option surface.\n *\n * Everything here describes *how to encode*, never *what the site is* — which is\n * why there is no `Config` block behind it. `legal`, `icons` and `indexing`\n * are anchored to a site config because what they emit describes the site; an\n * encoder setting describes a build step.\n */\n\n/** What a source video is turned into. */\nexport type OutputKind = 'webm' | 'mp4' | 'poster';\n\n/** The default set: a modern codec, a universal fallback, and a poster frame. */\nexport const DEFAULT_OUTPUTS: OutputKind[] = ['webm', 'mp4', 'poster'];\n\n/** File extension per output kind. */\nexport const OUTPUT_EXT: Record<OutputKind, string> = {\n webm: '.webm',\n mp4: '.mp4',\n poster: '.jpg',\n};\n\n/**\n * Bumped when the ffmpeg arguments in `ffmpeg.ts` change in a way that alters the\n * output bytes. It is part of the settings hash, so bumping it re-encodes every\n * source on the next run.\n *\n * Bump it deliberately and rarely. Consumers commit their processed outputs (see\n * the README), so every bump is a large binary diff in someone's repository —\n * that cost is worth paying for a real encoder fix and not for a tidy-up.\n */\nexport const ENCODER_VERSION = 1;\n\n/** How to encode. Every field here feeds the settings hash. */\nexport interface MediaConfig {\n /**\n * Cap the output width in pixels, preserving aspect ratio. Default 1920.\n *\n * A source larger than its display size is the usual reason a hero video is\n * enormous, so this is on by default rather than opt-in. `0` disables scaling.\n */\n maxWidth?: number | undefined;\n /**\n * Constant-quality level, on **VP9's CRF scale (0–63, lower is better)**.\n * Default 32, which is the range VP9's own guidance recommends for 1080p.\n *\n * H.264 uses a different scale (0–51), so the MP4 fallback is encoded at\n * `round(crf * 0.72)` — 23 at the default, x264's own default. One knob, because\n * two would drift apart and nobody would know which one mattered.\n */\n crf?: number | undefined;\n /**\n * Optional ceiling, e.g. `'2M'`. Absent by default: a bitrate cap makes the\n * encode constrained-quality rather than constant-quality, which is right when\n * you are protecting a data budget and wrong as a default, because the correct\n * cap depends on the resolution you are capping.\n */\n maxBitrate?: string | undefined;\n /**\n * Keep the audio track. Default false — the common case is a muted autoplay\n * loop, where the audio stream is bytes nobody will ever hear.\n */\n audio?: boolean | undefined;\n /**\n * Timestamp (seconds) the poster frame is taken from. Default 0.\n *\n * Frame 0 is often black on a clip that fades in, which is worth knowing before\n * concluding the poster is broken.\n */\n posterTime?: number | undefined;\n /** Which outputs to produce. Default `DEFAULT_OUTPUTS`. */\n outputs?: OutputKind[] | undefined;\n}\n\n/** A discovered source video. */\nexport interface MediaSource {\n path: string;\n /**\n * Path relative to the raw directory, POSIX-separated — the manifest key.\n * Relative so a manifest stays valid when the project moves or is cloned\n * somewhere else, which is the whole point of committing it.\n */\n id: string;\n /** sha256 of the file contents, truncated. */\n hash: string;\n}\n\n/** One file an encode will write. */\nexport interface MediaOutput {\n kind: OutputKind;\n /** Path relative to the output directory, POSIX-separated. */\n file: string;\n}\n\n/** One source and everything decided about it. */\nexport interface MediaJob {\n source: MediaSource;\n outputs: MediaOutput[];\n /** Hash of the settings these outputs were produced under. */\n settings: string;\n /** Why this job needs no work. Present iff it is being skipped. */\n skip?: string | undefined;\n}\n\n/** The complete account of a run, decided before any of it happens. */\nexport interface MediaPlan {\n jobs: MediaJob[];\n /** Things the operator needs to read. Printed by `--dry` and by a real run. */\n notes: string[];\n}\n\n/** One manifest record: what these outputs were made from, and under what settings. */\nexport interface MediaManifestEntry {\n /** `MediaSource.hash` at the time of encoding. */\n source: string;\n settings: string;\n /** Output paths relative to the output directory. */\n outputs: string[];\n}\n\n/**\n * The cache. A committed artifact, so it carries no timestamp and its keys are\n * sorted — a rebuild of unchanged sources must produce a byte-identical file, or\n * it shows up as noise in every diff.\n */\nexport interface MediaManifest {\n version: 1;\n entries: Record<string, MediaManifestEntry>;\n}\n","/**\n * Every decision `vitops media` makes, as one pure function.\n *\n * Pure in the load-bearing sense: no `fs`, no clock, no subprocess. Whether a\n * source needs re-encoding — the only thing here that can be wrong in a way that\n * costs minutes or ships a stale video — is decided from data passed in, and\n * asserted in `plan.test.ts` on a machine with no ffmpeg installed.\n *\n * That split is the same one `indexing/plan.ts` makes against its I/O modules, and\n * for the same reason: `--dry` should be a complete account of a run rather than\n * an approximation of one.\n */\nimport { createHash } from 'node:crypto';\nimport {\n DEFAULT_OUTPUTS,\n ENCODER_VERSION,\n type MediaConfig,\n type MediaJob,\n type MediaManifest,\n type MediaOutput,\n type MediaPlan,\n type MediaSource,\n OUTPUT_EXT,\n type OutputKind,\n} from './types.ts';\n\n/** Defaults, resolved once so the settings hash is computed over real values. */\nexport interface ResolvedMediaConfig {\n maxWidth: number;\n crf: number;\n maxBitrate: string | undefined;\n audio: boolean;\n posterTime: number;\n outputs: OutputKind[];\n}\n\n/**\n * Injected existence probe — `plan.ts` touches no filesystem itself.\n *\n * Takes a path relative to the output directory. The same injection shape as\n * `collectEntries(source, read)` in `indexing/sitemap.ts`.\n */\nexport type OutputProbe = (relativePath: string) => boolean;\n\nexport interface MediaPlanInput {\n sources: MediaSource[];\n config?: MediaConfig | undefined;\n /** The previous run's manifest, if there is a usable one. */\n previous?: MediaManifest | undefined;\n /** Whether an output file is already on disk. Defaults to \"nothing exists\". */\n probe?: OutputProbe | undefined;\n /** `--force`: re-encode regardless of the cache. */\n force?: boolean | undefined;\n}\n\nconst sha = (s: string) => createHash('sha256').update(s).digest('hex').slice(0, 16);\n\n/** Fill in defaults. Exported because the encoder and the hash must agree on them. */\nexport function resolveConfig(config: MediaConfig = {}): ResolvedMediaConfig {\n return {\n maxWidth: config.maxWidth ?? 1920,\n crf: config.crf ?? 32,\n maxBitrate: config.maxBitrate,\n audio: config.audio ?? false,\n posterTime: config.posterTime ?? 0,\n outputs: config.outputs ?? DEFAULT_OUTPUTS,\n };\n}\n\n/**\n * Hash the settings that decide the output bytes.\n *\n * Order-independent in the one place it could bite (`outputs` is sorted), so\n * re-ordering the list in a config doesn't re-encode a whole library.\n */\nexport function settingsHash(config: ResolvedMediaConfig): string {\n return sha(\n JSON.stringify({\n v: ENCODER_VERSION,\n maxWidth: config.maxWidth,\n crf: config.crf,\n maxBitrate: config.maxBitrate ?? null,\n audio: config.audio,\n posterTime: config.posterTime,\n outputs: [...config.outputs].sort(),\n }),\n );\n}\n\n/** `a/b/clip.mp4` → `a/b/clip` */\nfunction stem(id: string): string {\n const dot = id.lastIndexOf('.');\n const slash = id.lastIndexOf('/');\n return dot > slash ? id.slice(0, dot) : id;\n}\n\nfunction outputsFor(source: MediaSource, config: ResolvedMediaConfig): MediaOutput[] {\n const base = stem(source.id);\n return config.outputs.map((kind) => ({ kind, file: `${base}${OUTPUT_EXT[kind]}` }));\n}\n\n/**\n * Decide what to encode.\n *\n * A job is skipped only when all three hold: the source contents hash the same as\n * last time, the settings hash the same, and every declared output is still on\n * disk. Anything else re-encodes — including an absent or unreadable manifest,\n * which reads as \"encode everything\" and never as \"everything is done\".\n *\n * Throws on an output collision (`hero.mp4` and `hero.mov` in one directory both\n * want `hero.webm`). That is a layout mistake rather than a runtime condition, and\n * the failure it would otherwise cause — two sources overwriting each other every\n * run, each invalidating the other's cache entry — is invisible from the outside.\n */\nexport function planMedia(input: MediaPlanInput): MediaPlan {\n const config = resolveConfig(input.config);\n const settings = settingsHash(config);\n const probe = input.probe ?? (() => false);\n const notes: string[] = [];\n\n const jobs: MediaJob[] = [];\n const claimed = new Map<string, string>();\n\n for (const source of input.sources) {\n const outputs = outputsFor(source, config);\n for (const out of outputs) {\n const owner = claimed.get(out.file);\n if (owner !== undefined && owner !== source.id)\n throw new Error(\n `media: \"${source.id}\" and \"${owner}\" both produce \"${out.file}\". ` +\n 'Two sources with the same name in one directory overwrite each other — rename one.',\n );\n claimed.set(out.file, source.id);\n }\n\n const prev = input.previous?.entries[source.id];\n const cached =\n !input.force &&\n prev !== undefined &&\n prev.source === source.hash &&\n prev.settings === settings &&\n outputs.every((o) => probe(o.file));\n\n jobs.push({ source, outputs, settings, ...(cached ? { skip: 'unchanged' } : {}) });\n }\n\n const pending = jobs.filter((j) => !j.skip).length;\n if (input.sources.length === 0) notes.push('no video files found');\n else if (input.force && pending > 0) notes.push(`--force: re-encoding all ${pending}`);\n else if (!input.previous && pending > 0)\n notes.push('no manifest — encoding everything (a first run, or a cache that was not restored)');\n\n return { jobs, notes };\n}\n\n/** The jobs that will actually run. */\nexport const pendingJobs = (plan: MediaPlan): MediaJob[] => plan.jobs.filter((j) => !j.skip);\n\n/** Human-readable account of a plan, for `--dry` and for the run summary. */\nexport function formatPlan(plan: MediaPlan): string {\n const lines: string[] = [];\n const pending = pendingJobs(plan);\n const skipped = plan.jobs.length - pending.length;\n\n lines.push(`${plan.jobs.length} source${plan.jobs.length === 1 ? '' : 's'}`);\n lines.push(` encode: ${pending.length}`);\n lines.push(` skipped: ${skipped} (unchanged)`);\n\n for (const job of pending)\n lines.push(` • ${job.source.id} → ${job.outputs.map((o) => o.file).join(', ')}`);\n\n for (const note of plan.notes) lines.push(` ! ${note}`);\n return lines.join('\\n');\n}\n","/**\n * `vitops media` — turning a directory of raw video into web-ready assets.\n *\n * A separate subpath (`@getvitops/utils/media`) rather than part of the package\n * index, matching `./favicon` and `./indexing`: this module shells out to an\n * external encoder, and a consumer importing the content helpers shouldn't carry\n * it.\n *\n * The layering is the point, and it is the same one `indexing/` makes. `plan.ts`\n * decides everything and touches nothing; `ffmpeg.ts` executes and decides\n * nothing; `discover.ts` and `manifest.ts` are the filesystem either side. That is\n * what lets `--dry` be a complete account of a run, and what lets the cache — the\n * part with real consequences, since getting it wrong means either minutes of\n * needless encoding or a stale video shipped — be tested with no ffmpeg installed.\n */\nimport { existsSync, mkdirSync, statSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { discoverVideos } from './discover.ts';\nimport { encodeMp4, encodeWebm, extractPoster, requireFfmpeg } from './ffmpeg.ts';\nimport { MEDIA_MANIFEST_PATH, readManifest, toManifest, writeManifest } from './manifest.ts';\nimport { pendingJobs, planMedia, resolveConfig } from './plan.ts';\nimport type { MediaConfig, MediaManifestEntry, MediaPlan } from './types.ts';\n\nexport interface ProcessMediaOptions {\n /** Directory of unprocessed sources. Walked recursively. */\n raw: string;\n /** Where the encoded outputs go. Structure under `raw` is preserved. */\n out: string;\n config?: MediaConfig | undefined;\n /** Cache location. Default `.vitops/media-manifest.json`. */\n manifest?: string | undefined;\n /** Re-encode regardless of the cache. */\n force?: boolean | undefined;\n /** Decide everything, run nothing. */\n dry?: boolean | undefined;\n /** Called before each source is encoded — these take minutes, so say so. */\n onProgress?: ((message: string) => void) | undefined;\n}\n\nexport interface ProcessMediaResult {\n plan: MediaPlan;\n /** Outputs written this run, relative to `out`. */\n written: string[];\n /** Sources that needed no work. */\n skipped: number;\n}\n\n/**\n * Encode everything under `raw` that has changed since the last run.\n *\n * Callers get the cache for free because it lives here rather than in either of\n * them: `vitops media` and the Astro integration are equally incremental, since\n * they call this same function.\n */\nexport async function processMedia(options: ProcessMediaOptions): Promise<ProcessMediaResult> {\n const rawDir = resolve(options.raw);\n const outDir = resolve(options.out);\n const manifestPath = resolve(options.manifest ?? MEDIA_MANIFEST_PATH);\n\n if (!existsSync(rawDir) || !statSync(rawDir).isDirectory())\n throw new Error(`media: raw directory not found: ${rawDir}`);\n // Sharing one directory would make each run's outputs the next run's inputs —\n // re-encoding a re-encode, forever, losing quality every time.\n if (rawDir === outDir)\n throw new Error(`media: --raw and --out are the same directory (${rawDir}). They must differ.`);\n\n const config = resolveConfig(options.config);\n const sources = await discoverVideos(rawDir);\n const previous = readManifest(manifestPath);\n const plan = planMedia({\n sources,\n ...(options.config ? { config: options.config } : {}),\n ...(previous ? { previous } : {}),\n ...(options.force ? { force: options.force } : {}),\n probe: (file) => existsSync(join(outDir, file)),\n });\n\n const pending = pendingJobs(plan);\n const skipped = plan.jobs.length - pending.length;\n if (options.dry) return { plan, written: [], skipped };\n\n // Carry forward what is still valid, so the manifest on disk is a truthful\n // account at every moment — including after a crash halfway through.\n const entries: Record<string, MediaManifestEntry> = {};\n for (const job of plan.jobs) {\n const prev = previous?.entries[job.source.id];\n if (job.skip && prev) entries[job.source.id] = prev;\n }\n\n if (pending.length > 0) await requireFfmpeg();\n\n const written: string[] = [];\n for (const job of pending) {\n options.onProgress?.(`encoding ${job.source.id}`);\n for (const out of job.outputs) {\n const target = join(outDir, out.file);\n mkdirSync(dirname(target), { recursive: true });\n if (out.kind === 'webm') await encodeWebm(job.source.path, target, config);\n else if (out.kind === 'mp4') await encodeMp4(job.source.path, target, config);\n else await extractPoster(job.source.path, target, config);\n written.push(out.file);\n }\n entries[job.source.id] = {\n source: job.source.hash,\n settings: job.settings,\n outputs: job.outputs.map((o) => o.file),\n };\n writeManifest(manifestPath, toManifest(entries));\n }\n\n // Also write when nothing encoded, which is what prunes entries for sources\n // that have since been deleted. Skipped when there is nothing to record either\n // way, so an empty raw directory doesn't create a manifest.\n if (pending.length === 0 && (previous || Object.keys(entries).length > 0))\n writeManifest(manifestPath, toManifest(entries));\n\n return { plan, written, skipped };\n}\n\nexport { DISCOVER_SKIP, VIDEO_EXT, discoverVideos, hashFile } from './discover.ts';\nexport { encodeMp4, encodeWebm, extractPoster, requireFfmpeg } from './ffmpeg.ts';\nexport { MEDIA_MANIFEST_PATH, readManifest, toManifest, writeManifest } from './manifest.ts';\nexport { formatPlan, pendingJobs, planMedia, resolveConfig, settingsHash } from './plan.ts';\nexport type { MediaPlanInput, OutputProbe, ResolvedMediaConfig } from './plan.ts';\nexport { DEFAULT_OUTPUTS, ENCODER_VERSION, OUTPUT_EXT } from './types.ts';\nexport type {\n MediaConfig,\n MediaJob,\n MediaManifest,\n MediaManifestEntry,\n MediaOutput,\n MediaPlan,\n MediaSource,\n OutputKind,\n} from './types.ts';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/media/discover.ts","../../src/media/ffmpeg.ts","../../src/media/manifest.ts","../../src/media/types.ts","../../src/media/plan.ts","../../src/media/index.ts"],"sourcesContent":["/**\n * Finding the source videos and fingerprinting them.\n *\n * Deliberately not `scanFiles` from `../scan.ts`, which reads every match into\n * memory as a string. That is right for templates and catastrophic for a video —\n * the files this walks are routinely tens or hundreds of megabytes, which is also\n * why the hash is streamed rather than taken over a `readFileSync`.\n */\nimport { createHash } from 'node:crypto';\nimport { createReadStream, readdirSync } from 'node:fs';\nimport { extname, join, relative, sep } from 'node:path';\nimport type { MediaSource } from './types.ts';\n\n/** Container formats worth accepting as input. */\nexport const VIDEO_EXT: ReadonlySet<string> = new Set([\n '.mp4',\n '.mov',\n '.m4v',\n '.mkv',\n '.avi',\n '.webm',\n '.mpg',\n '.mpeg',\n]);\n\n/** Directories that never hold hand-managed source assets. */\nexport const DISCOVER_SKIP: ReadonlySet<string> = new Set(['node_modules', 'dist', '.git']);\n\n/** sha256 of the file contents, streamed. Truncated — 16 hex chars is 64 bits. */\nexport function hashFile(path: string): Promise<string> {\n return new Promise((resolve, reject) => {\n const hash = createHash('sha256');\n const stream = createReadStream(path);\n stream.on('error', reject);\n stream.on('data', (chunk) => hash.update(chunk));\n stream.on('end', () => resolve(hash.digest('hex').slice(0, 16)));\n });\n}\n\nfunction walk(dir: string, acc: string[]): void {\n for (const e of readdirSync(dir, { withFileTypes: true })) {\n if (e.name.startsWith('.')) continue;\n const p = join(dir, e.name);\n if (e.isDirectory()) {\n if (!DISCOVER_SKIP.has(e.name)) walk(p, acc);\n } else if (VIDEO_EXT.has(extname(e.name).toLowerCase())) {\n acc.push(p);\n }\n }\n}\n\n/**\n * Every video under `dir`, with its content hash.\n *\n * Sorted by `id` so a plan — and therefore the manifest and the log — comes out in\n * the same order on every machine, regardless of what the filesystem hands back.\n */\nexport async function discoverVideos(dir: string): Promise<MediaSource[]> {\n const paths: string[] = [];\n walk(dir, paths);\n const sources = await Promise.all(\n paths.map(async (path) => ({\n path,\n id: relative(dir, path).split(sep).join('/'),\n hash: await hashFile(path),\n })),\n );\n return sources.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));\n}\n","/**\n * The encoder. Executes a job and decides nothing.\n *\n * Every invocation goes through `execFile` with an argv array rather than a shell\n * string: a source path containing a space or a quote is ordinary, and the\n * shell-interpolated form these commands are usually written in breaks on both.\n */\nimport { execFile } from 'node:child_process';\nimport { existsSync, mkdtempSync, rmSync, statSync } from 'node:fs';\nimport { tmpdir } from 'node:os';\nimport { join } from 'node:path';\nimport { promisify } from 'node:util';\nimport type { ResolvedMediaConfig } from './plan.ts';\n\nconst exec = promisify(execFile);\n\n/** `-loglevel error` keeps this small; the ceiling is for a pathological failure. */\nconst MAX_BUFFER = 16 * 1024 * 1024;\n\n/**\n * x264 and VP9 use different CRF scales (0–51 against 0–63), so one `crf` knob has\n * to be mapped. 0.72 puts the default 32 at x264's own default of 23, which is the\n * pairing VP9's encoding guide uses for comparable quality.\n */\nconst X264_CRF_RATIO = 0.72;\n\nlet probe: Promise<void> | undefined;\n\n/**\n * Fail early and clearly when ffmpeg isn't installed.\n *\n * Deliberately not the `optimizePng` shape in `favicon.ts`, which swallows a\n * missing `oxipng` and warns once. That is right for a best-effort lossless crush,\n * where skipping costs a few kilobytes. It is wrong here: skipping produces no\n * output at all, and a build that carries on would ship the page referencing a\n * video that was never encoded.\n */\nexport function requireFfmpeg(): Promise<void> {\n probe ??= exec('ffmpeg', ['-version'], { maxBuffer: MAX_BUFFER }).then(\n () => undefined,\n () => {\n probe = undefined; // so a later call re-probes rather than caching a stale failure\n throw new Error(\n 'media: ffmpeg not found on PATH. It is an external tool, not an npm dependency — ' +\n 'install it (macOS: `brew install ffmpeg`, Debian/Ubuntu: `apt install ffmpeg`, ' +\n 'Windows: `winget install Gyan.FFmpeg`) and run this again.',\n );\n },\n );\n return probe;\n}\n\nasync function run(args: string[]): Promise<void> {\n try {\n await exec('ffmpeg', args, { maxBuffer: MAX_BUFFER });\n } catch (err) {\n const stderr = String((err as { stderr?: string }).stderr ?? '').trim();\n const tail = stderr.split('\\n').slice(-6).join('\\n');\n throw new Error(`media: ffmpeg failed${tail ? `\\n${tail}` : ''}`);\n }\n}\n\n/**\n * An encode that \"succeeds\" but writes nothing is the failure mode worth guarding:\n * seeking a poster past the end of a clip does exactly that, and ffmpeg exits 0.\n * Without this the manifest would record the job as done and never retry it.\n */\nfunction assertWritten(file: string, what: string): void {\n if (!existsSync(file) || statSync(file).size === 0)\n throw new Error(\n `media: ${what} produced no output (${file}). Check --poster-time and the source.`,\n );\n}\n\n/** Scale filter, capping width and keeping the height even (yuv420p requires it). */\nfunction scaleArgs(maxWidth: number): string[] {\n if (maxWidth <= 0) return [];\n // ffmpeg parses the quotes itself, so the commas inside min() aren't read as a\n // filter separator. There is no shell here to strip them.\n return ['-vf', `scale='min(${maxWidth},iw)':-2`];\n}\n\n/** `2M` → `4M`. A cap needs a buffer, and twice the rate is the usual choice. */\nfunction bufsize(maxBitrate: string): string {\n const m = /^(\\d+(?:\\.\\d+)?)([kKmM]?)$/.exec(maxBitrate.trim());\n if (!m)\n throw new Error(`media: maxBitrate \"${maxBitrate}\" is not a bitrate (try \"2M\" or \"800k\").`);\n return `${Number(m[1]) * 2}${m[2] ?? ''}`;\n}\n\nconst rateArgs = (maxBitrate: string | undefined): string[] =>\n maxBitrate ? ['-maxrate', maxBitrate, '-bufsize', bufsize(maxBitrate)] : [];\n\nconst audioArgs = (config: ResolvedMediaConfig, codec: string): string[] =>\n config.audio ? ['-c:a', codec, '-b:a', '128k'] : ['-an'];\n\nconst BASE = ['-y', '-nostdin', '-loglevel', 'error'];\n\n/**\n * VP9/WebM, two passes.\n *\n * The pass log goes into a fresh temp directory rather than the working directory.\n * ffmpeg's default (`ffmpeg2pass-0.log`, in cwd) makes cleanup cwd-dependent and\n * makes two concurrent encodes silently share one log — the second encode's rate\n * control then reads the first clip's statistics.\n */\nexport async function encodeWebm(\n input: string,\n output: string,\n config: ResolvedMediaConfig,\n): Promise<void> {\n const dir = mkdtempSync(join(tmpdir(), 'vitops-media-'));\n const passlog = join(dir, 'pass');\n const video = [\n ...scaleArgs(config.maxWidth),\n '-c:v',\n 'libvpx-vp9',\n // -b:v 0 with -crf is VP9's constant-quality mode; with -maxrate it becomes\n // constrained quality. A fixed bitrate would be right at one resolution only.\n '-b:v',\n '0',\n '-crf',\n String(config.crf),\n '-row-mt',\n '1',\n ...rateArgs(config.maxBitrate),\n ];\n try {\n // Pass 1 only gathers statistics, so it never encodes audio and writes no file.\n await run([\n ...BASE,\n '-i',\n input,\n ...video,\n '-an',\n '-pass',\n '1',\n '-passlogfile',\n passlog,\n '-f',\n 'null',\n '-',\n ]);\n await run([\n ...BASE,\n '-i',\n input,\n ...video,\n ...audioArgs(config, 'libopus'),\n '-pass',\n '2',\n '-passlogfile',\n passlog,\n output,\n ]);\n } finally {\n rmSync(dir, { recursive: true, force: true });\n }\n assertWritten(output, 'webm encode');\n}\n\n/**\n * H.264/MP4 — the fallback that makes `<video>` work on older iOS and in the\n * in-app webviews that still don't decode VP9.\n *\n * `+faststart` moves the moov atom to the front. Without it the browser has to\n * download the whole file before it can show frame one, which on a hero video\n * looks exactly like the optimisation didn't work.\n */\nexport async function encodeMp4(\n input: string,\n output: string,\n config: ResolvedMediaConfig,\n): Promise<void> {\n await run([\n ...BASE,\n '-i',\n input,\n ...scaleArgs(config.maxWidth),\n '-c:v',\n 'libx264',\n '-preset',\n 'slow',\n '-crf',\n String(Math.round(config.crf * X264_CRF_RATIO)),\n // Required by Safari and by every hardware decoder worth naming.\n '-pix_fmt',\n 'yuv420p',\n '-movflags',\n '+faststart',\n ...rateArgs(config.maxBitrate),\n ...audioArgs(config, 'aac'),\n output,\n ]);\n assertWritten(output, 'mp4 encode');\n}\n\n/** The poster frame. `-ss` before `-i` so the seek is a fast one. */\nexport async function extractPoster(\n input: string,\n output: string,\n config: ResolvedMediaConfig,\n): Promise<void> {\n await run([\n ...BASE,\n ...(config.posterTime > 0 ? ['-ss', String(config.posterTime)] : []),\n '-i',\n input,\n ...scaleArgs(config.maxWidth),\n '-frames:v',\n '1',\n '-q:v',\n '2',\n output,\n ]);\n assertWritten(output, 'poster extraction');\n}\n","/**\n * The cache — what makes a second run free.\n *\n * A committed artifact, unlike `indexing/`'s snapshot: the recommended layout\n * checks in both the processed outputs and this file, so CI never needs ffmpeg\n * and the outputs only change when someone deliberately re-encodes. That is why\n * `writeManifest` sorts and carries no timestamp — an unchanged run has to produce\n * a byte-identical file, or every build shows up as a diff.\n */\nimport type { MediaManifest, MediaManifestEntry } from './types.ts';\nimport { readVersioned, writeVersioned } from '../json-store.ts';\n\n/** Default location, relative to the consumer's project root. */\nexport const MEDIA_MANIFEST_PATH = '.vitops/media-manifest.json';\n\n/**\n * Read the manifest, or `undefined` when there isn't a usable one.\n *\n * A corrupt or wrong-version file reads as absent, which re-encodes everything.\n * The opposite reading — treating it as an empty but valid manifest — is the same\n * shape of bug `readSnapshot` avoids, except here it would be silent: nothing\n * would encode, and the build would reference outputs that were never written.\n */\nexport function readManifest(path: string): MediaManifest | undefined {\n return readVersioned<MediaManifest, 'entries'>(path, 'entries');\n}\n\n/** Build a manifest with its keys and output lists in a stable order. */\nexport function toManifest(entries: Record<string, MediaManifestEntry>): MediaManifest {\n const sorted: Record<string, MediaManifestEntry> = {};\n for (const key of Object.keys(entries).sort()) {\n const entry = entries[key] as MediaManifestEntry;\n sorted[key] = { ...entry, outputs: [...entry.outputs].sort() };\n }\n return { version: 1, entries: sorted };\n}\n\n/**\n * Write the manifest, creating its directory.\n *\n * Record a job only once its outputs are on disk. Writing eagerly would mark a\n * source as encoded that isn't, and because the next run trusts this file, the\n * result is a missing video that no rebuild ever fixes.\n */\nexport function writeManifest(path: string, manifest: MediaManifest): void {\n writeVersioned(path, manifest);\n}\n","/**\n * The media module's own option surface.\n *\n * Everything here describes *how to encode*, never *what the site is* — which is\n * why there is no `Config` block behind it. `legal`, `icons` and `indexing`\n * are anchored to a site config because what they emit describes the site; an\n * encoder setting describes a build step.\n */\n\n/** What a source video is turned into. */\nexport type OutputKind = 'webm' | 'mp4' | 'poster';\n\n/** The default set: a modern codec, a universal fallback, and a poster frame. */\nexport const DEFAULT_OUTPUTS: OutputKind[] = ['webm', 'mp4', 'poster'];\n\n/** File extension per output kind. */\nexport const OUTPUT_EXT: Record<OutputKind, string> = {\n webm: '.webm',\n mp4: '.mp4',\n poster: '.jpg',\n};\n\n/**\n * Bumped when the ffmpeg arguments in `ffmpeg.ts` change in a way that alters the\n * output bytes. It is part of the settings hash, so bumping it re-encodes every\n * source on the next run.\n *\n * Bump it deliberately and rarely. Consumers commit their processed outputs (see\n * the README), so every bump is a large binary diff in someone's repository —\n * that cost is worth paying for a real encoder fix and not for a tidy-up.\n */\nexport const ENCODER_VERSION = 1;\n\n/** How to encode. Every field here feeds the settings hash. */\nexport interface MediaConfig {\n /**\n * Cap the output width in pixels, preserving aspect ratio. Default 1920.\n *\n * A source larger than its display size is the usual reason a hero video is\n * enormous, so this is on by default rather than opt-in. `0` disables scaling.\n */\n maxWidth?: number | undefined;\n /**\n * Constant-quality level, on **VP9's CRF scale (0–63, lower is better)**.\n * Default 32, which is the range VP9's own guidance recommends for 1080p.\n *\n * H.264 uses a different scale (0–51), so the MP4 fallback is encoded at\n * `round(crf * 0.72)` — 23 at the default, x264's own default. One knob, because\n * two would drift apart and nobody would know which one mattered.\n */\n crf?: number | undefined;\n /**\n * Optional ceiling, e.g. `'2M'`. Absent by default: a bitrate cap makes the\n * encode constrained-quality rather than constant-quality, which is right when\n * you are protecting a data budget and wrong as a default, because the correct\n * cap depends on the resolution you are capping.\n */\n maxBitrate?: string | undefined;\n /**\n * Keep the audio track. Default false — the common case is a muted autoplay\n * loop, where the audio stream is bytes nobody will ever hear.\n */\n audio?: boolean | undefined;\n /**\n * Timestamp (seconds) the poster frame is taken from. Default 0.\n *\n * Frame 0 is often black on a clip that fades in, which is worth knowing before\n * concluding the poster is broken.\n */\n posterTime?: number | undefined;\n /** Which outputs to produce. Default `DEFAULT_OUTPUTS`. */\n outputs?: OutputKind[] | undefined;\n}\n\n/** A discovered source video. */\nexport interface MediaSource {\n path: string;\n /**\n * Path relative to the raw directory, POSIX-separated — the manifest key.\n * Relative so a manifest stays valid when the project moves or is cloned\n * somewhere else, which is the whole point of committing it.\n */\n id: string;\n /** sha256 of the file contents, truncated. */\n hash: string;\n}\n\n/** One file an encode will write. */\nexport interface MediaOutput {\n kind: OutputKind;\n /** Path relative to the output directory, POSIX-separated. */\n file: string;\n}\n\n/** One source and everything decided about it. */\nexport interface MediaJob {\n source: MediaSource;\n outputs: MediaOutput[];\n /** Hash of the settings these outputs were produced under. */\n settings: string;\n /** Why this job needs no work. Present iff it is being skipped. */\n skip?: string | undefined;\n}\n\n/** The complete account of a run, decided before any of it happens. */\nexport interface MediaPlan {\n jobs: MediaJob[];\n /** Things the operator needs to read. Printed by `--dry` and by a real run. */\n notes: string[];\n}\n\n/** One manifest record: what these outputs were made from, and under what settings. */\nexport interface MediaManifestEntry {\n /** `MediaSource.hash` at the time of encoding. */\n source: string;\n settings: string;\n /** Output paths relative to the output directory. */\n outputs: string[];\n}\n\n/**\n * The cache. A committed artifact, so it carries no timestamp and its keys are\n * sorted — a rebuild of unchanged sources must produce a byte-identical file, or\n * it shows up as noise in every diff.\n */\nexport interface MediaManifest {\n version: 1;\n entries: Record<string, MediaManifestEntry>;\n}\n","/**\n * Every decision `vitops media` makes, as one pure function.\n *\n * Pure in the load-bearing sense: no `fs`, no clock, no subprocess. Whether a\n * source needs re-encoding — the only thing here that can be wrong in a way that\n * costs minutes or ships a stale video — is decided from data passed in, and\n * asserted in `plan.test.ts` on a machine with no ffmpeg installed.\n *\n * That split is the same one `indexing/plan.ts` makes against its I/O modules, and\n * for the same reason: `--dry` should be a complete account of a run rather than\n * an approximation of one.\n */\nimport { createHash } from 'node:crypto';\nimport {\n DEFAULT_OUTPUTS,\n ENCODER_VERSION,\n type MediaConfig,\n type MediaJob,\n type MediaManifest,\n type MediaOutput,\n type MediaPlan,\n type MediaSource,\n OUTPUT_EXT,\n type OutputKind,\n} from './types.ts';\n\n/** Defaults, resolved once so the settings hash is computed over real values. */\nexport interface ResolvedMediaConfig {\n maxWidth: number;\n crf: number;\n maxBitrate: string | undefined;\n audio: boolean;\n posterTime: number;\n outputs: OutputKind[];\n}\n\n/**\n * Injected existence probe — `plan.ts` touches no filesystem itself.\n *\n * Takes a path relative to the output directory. The same injection shape as\n * `collectEntries(source, read)` in `indexing/sitemap.ts`.\n */\nexport type OutputProbe = (relativePath: string) => boolean;\n\nexport interface MediaPlanInput {\n sources: MediaSource[];\n config?: MediaConfig | undefined;\n /** The previous run's manifest, if there is a usable one. */\n previous?: MediaManifest | undefined;\n /** Whether an output file is already on disk. Defaults to \"nothing exists\". */\n probe?: OutputProbe | undefined;\n /** `--force`: re-encode regardless of the cache. */\n force?: boolean | undefined;\n}\n\nconst sha = (s: string) => createHash('sha256').update(s).digest('hex').slice(0, 16);\n\n/** Fill in defaults. Exported because the encoder and the hash must agree on them. */\nexport function resolveConfig(config: MediaConfig = {}): ResolvedMediaConfig {\n return {\n maxWidth: config.maxWidth ?? 1920,\n crf: config.crf ?? 32,\n maxBitrate: config.maxBitrate,\n audio: config.audio ?? false,\n posterTime: config.posterTime ?? 0,\n outputs: config.outputs ?? DEFAULT_OUTPUTS,\n };\n}\n\n/**\n * Hash the settings that decide the output bytes.\n *\n * Order-independent in the one place it could bite (`outputs` is sorted), so\n * re-ordering the list in a config doesn't re-encode a whole library.\n */\nexport function settingsHash(config: ResolvedMediaConfig): string {\n return sha(\n JSON.stringify({\n v: ENCODER_VERSION,\n maxWidth: config.maxWidth,\n crf: config.crf,\n maxBitrate: config.maxBitrate ?? null,\n audio: config.audio,\n posterTime: config.posterTime,\n outputs: [...config.outputs].sort(),\n }),\n );\n}\n\n/** `a/b/clip.mp4` → `a/b/clip` */\nfunction stem(id: string): string {\n const dot = id.lastIndexOf('.');\n const slash = id.lastIndexOf('/');\n return dot > slash ? id.slice(0, dot) : id;\n}\n\nfunction outputsFor(source: MediaSource, config: ResolvedMediaConfig): MediaOutput[] {\n const base = stem(source.id);\n return config.outputs.map((kind) => ({ kind, file: `${base}${OUTPUT_EXT[kind]}` }));\n}\n\n/**\n * Decide what to encode.\n *\n * A job is skipped only when all three hold: the source contents hash the same as\n * last time, the settings hash the same, and every declared output is still on\n * disk. Anything else re-encodes — including an absent or unreadable manifest,\n * which reads as \"encode everything\" and never as \"everything is done\".\n *\n * Throws on an output collision (`hero.mp4` and `hero.mov` in one directory both\n * want `hero.webm`). That is a layout mistake rather than a runtime condition, and\n * the failure it would otherwise cause — two sources overwriting each other every\n * run, each invalidating the other's cache entry — is invisible from the outside.\n */\nexport function planMedia(input: MediaPlanInput): MediaPlan {\n const config = resolveConfig(input.config);\n const settings = settingsHash(config);\n const probe = input.probe ?? (() => false);\n const notes: string[] = [];\n\n const jobs: MediaJob[] = [];\n const claimed = new Map<string, string>();\n\n for (const source of input.sources) {\n const outputs = outputsFor(source, config);\n for (const out of outputs) {\n const owner = claimed.get(out.file);\n if (owner !== undefined && owner !== source.id)\n throw new Error(\n `media: \"${source.id}\" and \"${owner}\" both produce \"${out.file}\". ` +\n 'Two sources with the same name in one directory overwrite each other — rename one.',\n );\n claimed.set(out.file, source.id);\n }\n\n const prev = input.previous?.entries[source.id];\n const cached =\n !input.force &&\n prev !== undefined &&\n prev.source === source.hash &&\n prev.settings === settings &&\n outputs.every((o) => probe(o.file));\n\n jobs.push({ source, outputs, settings, ...(cached ? { skip: 'unchanged' } : {}) });\n }\n\n const pending = jobs.filter((j) => !j.skip).length;\n if (input.sources.length === 0) notes.push('no video files found');\n else if (input.force && pending > 0) notes.push(`--force: re-encoding all ${pending}`);\n else if (!input.previous && pending > 0)\n notes.push('no manifest — encoding everything (a first run, or a cache that was not restored)');\n\n return { jobs, notes };\n}\n\n/** The jobs that will actually run. */\nexport const pendingJobs = (plan: MediaPlan): MediaJob[] => plan.jobs.filter((j) => !j.skip);\n\n/** Human-readable account of a plan, for `--dry` and for the run summary. */\nexport function formatPlan(plan: MediaPlan): string {\n const lines: string[] = [];\n const pending = pendingJobs(plan);\n const skipped = plan.jobs.length - pending.length;\n\n lines.push(`${plan.jobs.length} source${plan.jobs.length === 1 ? '' : 's'}`);\n lines.push(` encode: ${pending.length}`);\n lines.push(` skipped: ${skipped} (unchanged)`);\n\n for (const job of pending)\n lines.push(` • ${job.source.id} → ${job.outputs.map((o) => o.file).join(', ')}`);\n\n for (const note of plan.notes) lines.push(` ! ${note}`);\n return lines.join('\\n');\n}\n","/**\n * `vitops media` — turning a directory of raw video into web-ready assets.\n *\n * A separate subpath (`@getvitops/utils/media`) rather than part of the package\n * index, matching `./favicon` and `./indexing`: this module shells out to an\n * external encoder, and a consumer importing the content helpers shouldn't carry\n * it.\n *\n * The layering is the point, and it is the same one `indexing/` makes. `plan.ts`\n * decides everything and touches nothing; `ffmpeg.ts` executes and decides\n * nothing; `discover.ts` and `manifest.ts` are the filesystem either side. That is\n * what lets `--dry` be a complete account of a run, and what lets the cache — the\n * part with real consequences, since getting it wrong means either minutes of\n * needless encoding or a stale video shipped — be tested with no ffmpeg installed.\n */\nimport { existsSync, mkdirSync, statSync } from 'node:fs';\nimport { dirname, join, resolve } from 'node:path';\nimport { discoverVideos } from './discover.ts';\nimport { encodeMp4, encodeWebm, extractPoster, requireFfmpeg } from './ffmpeg.ts';\nimport { MEDIA_MANIFEST_PATH, readManifest, toManifest, writeManifest } from './manifest.ts';\nimport { pendingJobs, planMedia, resolveConfig } from './plan.ts';\nimport type { MediaConfig, MediaManifestEntry, MediaPlan } from './types.ts';\n\nexport interface ProcessMediaOptions {\n /** Directory of unprocessed sources. Walked recursively. */\n raw: string;\n /** Where the encoded outputs go. Structure under `raw` is preserved. */\n out: string;\n config?: MediaConfig | undefined;\n /** Cache location. Default `.vitops/media-manifest.json`. */\n manifest?: string | undefined;\n /** Re-encode regardless of the cache. */\n force?: boolean | undefined;\n /** Decide everything, run nothing. */\n dry?: boolean | undefined;\n /** Called before each source is encoded — these take minutes, so say so. */\n onProgress?: ((message: string) => void) | undefined;\n}\n\nexport interface ProcessMediaResult {\n plan: MediaPlan;\n /** Outputs written this run, relative to `out`. */\n written: string[];\n /** Sources that needed no work. */\n skipped: number;\n}\n\n/**\n * Encode everything under `raw` that has changed since the last run.\n *\n * Callers get the cache for free because it lives here rather than in either of\n * them: `vitops media` and the Astro integration are equally incremental, since\n * they call this same function.\n */\nexport async function processMedia(options: ProcessMediaOptions): Promise<ProcessMediaResult> {\n const rawDir = resolve(options.raw);\n const outDir = resolve(options.out);\n const manifestPath = resolve(options.manifest ?? MEDIA_MANIFEST_PATH);\n\n if (!existsSync(rawDir) || !statSync(rawDir).isDirectory())\n throw new Error(`media: raw directory not found: ${rawDir}`);\n // Sharing one directory would make each run's outputs the next run's inputs —\n // re-encoding a re-encode, forever, losing quality every time.\n if (rawDir === outDir)\n throw new Error(`media: --raw and --out are the same directory (${rawDir}). They must differ.`);\n\n const config = resolveConfig(options.config);\n const sources = await discoverVideos(rawDir);\n const previous = readManifest(manifestPath);\n const plan = planMedia({\n sources,\n ...(options.config ? { config: options.config } : {}),\n ...(previous ? { previous } : {}),\n ...(options.force ? { force: options.force } : {}),\n probe: (file) => existsSync(join(outDir, file)),\n });\n\n const pending = pendingJobs(plan);\n const skipped = plan.jobs.length - pending.length;\n if (options.dry) return { plan, written: [], skipped };\n\n // Carry forward what is still valid, so the manifest on disk is a truthful\n // account at every moment — including after a crash halfway through.\n const entries: Record<string, MediaManifestEntry> = {};\n for (const job of plan.jobs) {\n const prev = previous?.entries[job.source.id];\n if (job.skip && prev) entries[job.source.id] = prev;\n }\n\n if (pending.length > 0) await requireFfmpeg();\n\n const written: string[] = [];\n for (const job of pending) {\n options.onProgress?.(`encoding ${job.source.id}`);\n for (const out of job.outputs) {\n const target = join(outDir, out.file);\n mkdirSync(dirname(target), { recursive: true });\n if (out.kind === 'webm') await encodeWebm(job.source.path, target, config);\n else if (out.kind === 'mp4') await encodeMp4(job.source.path, target, config);\n else await extractPoster(job.source.path, target, config);\n written.push(out.file);\n }\n entries[job.source.id] = {\n source: job.source.hash,\n settings: job.settings,\n outputs: job.outputs.map((o) => o.file),\n };\n writeManifest(manifestPath, toManifest(entries));\n }\n\n // Also write when nothing encoded, which is what prunes entries for sources\n // that have since been deleted. Skipped when there is nothing to record either\n // way, so an empty raw directory doesn't create a manifest.\n if (pending.length === 0 && (previous || Object.keys(entries).length > 0))\n writeManifest(manifestPath, toManifest(entries));\n\n return { plan, written, skipped };\n}\n\nexport { DISCOVER_SKIP, VIDEO_EXT, discoverVideos, hashFile } from './discover.ts';\nexport { encodeMp4, encodeWebm, extractPoster, requireFfmpeg } from './ffmpeg.ts';\nexport { MEDIA_MANIFEST_PATH, readManifest, toManifest, writeManifest } from './manifest.ts';\nexport { formatPlan, pendingJobs, planMedia, resolveConfig, settingsHash } from './plan.ts';\nexport type { MediaPlanInput, OutputProbe, ResolvedMediaConfig } from './plan.ts';\nexport { DEFAULT_OUTPUTS, ENCODER_VERSION, OUTPUT_EXT } from './types.ts';\nexport type {\n MediaConfig,\n MediaJob,\n MediaManifest,\n MediaManifestEntry,\n MediaOutput,\n MediaPlan,\n MediaSource,\n OutputKind,\n} from './types.ts';\n"],"mappings":"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"}
|