@rathnasgala2/template 2.0.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/LICENSE +201 -0
- package/README.md +515 -0
- package/contracts/render-policy.jcs +1 -0
- package/contracts/theme-styling-contract.jcs +1 -0
- package/package.json +96 -0
- package/src/core/errors.js +136 -0
- package/src/core/index.js +758 -0
- package/src/core/internal/appearance/bootstrap-script.js +159 -0
- package/src/core/internal/appearance/contract.js +193 -0
- package/src/core/internal/appearance/controller-markup.js +80 -0
- package/src/core/internal/appearance/styling-contract.js +509 -0
- package/src/core/internal/canonical-jcs.js +0 -0
- package/src/core/internal/content-security.js +0 -0
- package/src/core/internal/eleventy-render.js +220 -0
- package/src/core/internal/feeds.js +161 -0
- package/src/core/internal/fs-walk.js +38 -0
- package/src/core/internal/media/avif-probe.js +169 -0
- package/src/core/internal/media/font-policy.js +56 -0
- package/src/core/internal/media/jpeg-codec.js +0 -0
- package/src/core/internal/media/limits.js +129 -0
- package/src/core/internal/media/pipeline.js +353 -0
- package/src/core/internal/media/png-codec.js +406 -0
- package/src/core/internal/media/resize.js +147 -0
- package/src/core/internal/media/sniff.js +109 -0
- package/src/core/internal/media/webp-probe.js +139 -0
- package/src/core/internal/messages.js +78 -0
- package/src/core/internal/page-kinds.js +709 -0
- package/src/core/internal/provenance.js +49 -0
- package/src/core/internal/render-policy-content.js +295 -0
- package/src/core/internal/route-labels.js +68 -0
- package/src/core/internal/route.js +167 -0
- package/src/core/internal/search-index.js +119 -0
- package/src/core/internal/seo.js +114 -0
- package/src/core/internal/sitemap.js +77 -0
- package/src/core/internal/skeleton.js +413 -0
- package/src/core/internal/source-inventory.js +145 -0
- package/src/core/internal/text-direction.js +59 -0
- package/src/core/internal/theme-assets.js +387 -0
- package/src/core/internal/uuid.js +51 -0
- package/src/core/manifest.js +0 -0
- package/types/eleventy-plugin-syntaxhighlight.d.ts +26 -0
- package/types/eleventy.d.ts +38 -0
- package/types/index.d.ts +347 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { MediaPipelineError } from '../../errors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Media pipeline resource ceilings (brief S2 section 3, S2-T05: "bounded
|
|
5
|
+
* decoding worker", "decompression and resource limits").
|
|
6
|
+
*
|
|
7
|
+
* The raster-specific numbers below are DEC-097's own closed
|
|
8
|
+
* `gala-theme-binary-assets-v2` bounds (decision document section titled
|
|
9
|
+
* "Binary theme assets"): non-animated raster, width/height each `1..8192`,
|
|
10
|
+
* at most 16,777,216 pixels, at most 67,108,864 decoded RGBA8 buffer bytes,
|
|
11
|
+
* source raster at most 16,777,216 bytes, source font at most 4,194,304
|
|
12
|
+
* bytes. DEC-097 states those bounds for the closed theme *passive-asset*
|
|
13
|
+
* decoder profile; the brief's author-media pipeline (S2-T05) gives no
|
|
14
|
+
* separate numeric profile of its own, so this renderer reuses the same
|
|
15
|
+
* numbers for the author-media decode-bound worker rather than inventing an
|
|
16
|
+
* unrelated set — the physical decode-bomb risk (integer overflow, unbounded
|
|
17
|
+
* allocation) is identical for both call sites. This is an explicit,
|
|
18
|
+
* documented implementer decision, not a value taken from author-media text.
|
|
19
|
+
*
|
|
20
|
+
* DEC-097's separate "document-22 operational release" MVP launch gate also
|
|
21
|
+
* fixes a smaller general per-source-file ceiling (10,485,760 bytes) that
|
|
22
|
+
* applies to *every* repository source file regardless of kind, with "the
|
|
23
|
+
* smaller declared per-file/package budget still wins." Applied here: the
|
|
24
|
+
* effective per-image source-byte ceiling is `min(16,777,216, 10,485,760)`.
|
|
25
|
+
* The font ceiling (4,194,304) is already smaller than the general cap, so
|
|
26
|
+
* it is unaffected.
|
|
27
|
+
*
|
|
28
|
+
* `MAX_IMAGES_PER_PUBLICATION` and `MAX_TOTAL_MEDIA_BYTES_PER_PUBLICATION`
|
|
29
|
+
* are not fixed anywhere in the brief or DEC-097; there is no cited
|
|
30
|
+
* per-publication *media-specific* aggregate. These two values are this
|
|
31
|
+
* renderer's own conservative, documented ceilings (well under DEC-097's
|
|
32
|
+
* unrelated 50,000-file / 1,073,741,824-byte whole-artifact ceiling), so a
|
|
33
|
+
* publication cannot exhaust the media pipeline's bounded decode budget by
|
|
34
|
+
* sheer reference count even though each individual file is itself bounded.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/** Maximum admitted raster width or height, in pixels, inclusive. */
|
|
38
|
+
export const MAX_IMAGE_DIMENSION = 8192;
|
|
39
|
+
|
|
40
|
+
/** Maximum admitted decoded pixel count (`width * height`). */
|
|
41
|
+
export const MAX_IMAGE_PIXELS = 16_777_216;
|
|
42
|
+
|
|
43
|
+
/** Maximum admitted decoded RGBA8 output buffer size, in bytes. */
|
|
44
|
+
export const MAX_DECODED_RGBA_BYTES = 67_108_864;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Maximum admitted raster source byte length: the smaller of DEC-097's
|
|
48
|
+
* raster-specific cap (16,777,216) and its general per-source-file cap
|
|
49
|
+
* (10,485,760).
|
|
50
|
+
*/
|
|
51
|
+
export const MAX_IMAGE_SOURCE_BYTES = 10_485_760;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Maximum admitted font source byte length (DEC-097's binary font cap,
|
|
55
|
+
* already smaller than the general per-source-file cap).
|
|
56
|
+
*/
|
|
57
|
+
export const MAX_FONT_SOURCE_BYTES = 4_194_304;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Maximum distinct image references a single publication's media pipeline
|
|
61
|
+
* will process (implementer-chosen; see module documentation).
|
|
62
|
+
*/
|
|
63
|
+
export const MAX_IMAGES_PER_PUBLICATION = 2048;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Maximum distinct font references a single publication's media pipeline
|
|
67
|
+
* will process. `appearance.fontAssets` is already schema-bounded to at
|
|
68
|
+
* most 8 entries (`resolvedFile[0..8]`); this constant documents that same
|
|
69
|
+
* number as the media pipeline's own font-count ceiling rather than
|
|
70
|
+
* re-deriving it from the schema at runtime.
|
|
71
|
+
*/
|
|
72
|
+
export const MAX_FONTS_PER_PUBLICATION = 8;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Maximum aggregate decoded-plus-derivative byte budget the media pipeline
|
|
76
|
+
* will spend for one publication (implementer-chosen; see module
|
|
77
|
+
* documentation). Comfortably under DEC-097's unrelated 1,073,741,824-byte
|
|
78
|
+
* whole-artifact ceiling, which this constant does not replace or relax.
|
|
79
|
+
*/
|
|
80
|
+
export const MAX_TOTAL_MEDIA_BYTES_PER_PUBLICATION = 268_435_456;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Assert that a decoded or declared raster's dimensions are within the
|
|
84
|
+
* shared decode ceilings, shared by every format-specific decoder/probe in
|
|
85
|
+
* this pipeline so the same check (and the same jscpd-flagged duplication
|
|
86
|
+
* it would otherwise create across `png-codec.js`, `webp-probe.js` and
|
|
87
|
+
* `avif-probe.js`) exists exactly once.
|
|
88
|
+
*
|
|
89
|
+
* @param {number} width declared or decoded width, in pixels
|
|
90
|
+
* @param {number} height declared or decoded height, in pixels
|
|
91
|
+
* @param {string} referencePath the source path, for error messages
|
|
92
|
+
* @param {string} formatLabel a short format name for the error message
|
|
93
|
+
* (for example `"PNG"`, `"WebP"`, `"AVIF"`)
|
|
94
|
+
* @returns {void}
|
|
95
|
+
*/
|
|
96
|
+
export function assertDimensionsWithinCeiling(
|
|
97
|
+
width,
|
|
98
|
+
height,
|
|
99
|
+
referencePath,
|
|
100
|
+
formatLabel,
|
|
101
|
+
) {
|
|
102
|
+
if (
|
|
103
|
+
width < 1 ||
|
|
104
|
+
height < 1 ||
|
|
105
|
+
width > MAX_IMAGE_DIMENSION ||
|
|
106
|
+
height > MAX_IMAGE_DIMENSION ||
|
|
107
|
+
width * height > MAX_IMAGE_PIXELS
|
|
108
|
+
) {
|
|
109
|
+
throw new MediaPipelineError(
|
|
110
|
+
'MEDIA_RESOURCE_EXCEEDED',
|
|
111
|
+
`${formatLabel} dimensions ${width}x${height} exceed the admitted decode ceiling`,
|
|
112
|
+
referencePath,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Non-resetting wall-clock budget for one file's bounded decode, in
|
|
119
|
+
* milliseconds. DEC-097's own closed binary-asset decoder uses a ten-second
|
|
120
|
+
* non-resetting deadline; this renderer reuses that figure for the same
|
|
121
|
+
* documented reason as the size/pixel ceilings above.
|
|
122
|
+
*/
|
|
123
|
+
export const DECODE_DEADLINE_MS = 10_000;
|
|
124
|
+
|
|
125
|
+
/** Deterministic responsive derivative widths, in pixels, ascending. */
|
|
126
|
+
export const DERIVATIVE_WIDTHS = Object.freeze([320, 640, 960, 1280, 1920]);
|
|
127
|
+
|
|
128
|
+
/** Fixed JPEG derivative encode quality (jpeg-js `quality` option, 0..100). */
|
|
129
|
+
export const DERIVATIVE_JPEG_QUALITY = 82;
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media pipeline orchestration (brief S2 section 3, S2-T05).
|
|
3
|
+
*
|
|
4
|
+
* `build-input:2.0.0` never carries raw asset bytes for a `resolvedFile`
|
|
5
|
+
* reference — only its repository-relative `path` and a `sourceDigest`
|
|
6
|
+
* (DEC-097 section 6: "`build` exposes this decoded set as a read-only
|
|
7
|
+
* source mount"). This pipeline therefore reads actual bytes from
|
|
8
|
+
* `options.sourceDirectory`, the caller-mounted read-only source tree, and
|
|
9
|
+
* fails closed (`MEDIA_SOURCE_DIGEST_MISMATCH`) if what it reads there does
|
|
10
|
+
* not hash to the digest `build-input` declared — the same fail-closed
|
|
11
|
+
* posture as every other adapter boundary in this renderer.
|
|
12
|
+
*
|
|
13
|
+
* Every distinct image/font reference in the publication (publication
|
|
14
|
+
* default image, each author's avatar, each content record's hero and
|
|
15
|
+
* social image, the appearance package's brand mark, wordmark and font
|
|
16
|
+
* assets) is collected once, deduplicated by path, processed independently,
|
|
17
|
+
* and turned into one or more content-addressed output files under
|
|
18
|
+
* `assets/media/` in the candidate output directory plus one
|
|
19
|
+
* `manifestAsset`-shaped row per output file. `manifestAsset` has no field
|
|
20
|
+
* for a separate source/transform/output digest triple or for width/height
|
|
21
|
+
* (`grep`-verified against the published schema); this pipeline expresses
|
|
22
|
+
* that provenance the only way the closed shape allows: the source digest a
|
|
23
|
+
* derivative was produced from is the same path's `manifestIncludedSource`
|
|
24
|
+
* row (already emitted by `collectIncludedSources`), the output digest is
|
|
25
|
+
* the asset row's own `sha256`, and the width/format transform is encoded
|
|
26
|
+
* in the content-addressed path and `mediaType` themselves.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { createHash } from 'node:crypto';
|
|
30
|
+
import { readFile } from 'node:fs/promises';
|
|
31
|
+
import path from 'node:path';
|
|
32
|
+
|
|
33
|
+
import { MediaPipelineError } from '../../errors.js';
|
|
34
|
+
import { decodeJpeg, encodeJpeg } from './jpeg-codec.js';
|
|
35
|
+
import {
|
|
36
|
+
MAX_FONTS_PER_PUBLICATION,
|
|
37
|
+
MAX_IMAGES_PER_PUBLICATION,
|
|
38
|
+
MAX_IMAGE_SOURCE_BYTES,
|
|
39
|
+
MAX_TOTAL_MEDIA_BYTES_PER_PUBLICATION,
|
|
40
|
+
DERIVATIVE_WIDTHS,
|
|
41
|
+
} from './limits.js';
|
|
42
|
+
import { assertFontPolicy } from './font-policy.js';
|
|
43
|
+
import { decodePng, encodePng } from './png-codec.js';
|
|
44
|
+
import { applyExifOrientation, resizeRaster } from './resize.js';
|
|
45
|
+
import { MEDIA_TYPE_BY_FORMAT, sniffMediaFormat } from './sniff.js';
|
|
46
|
+
import { probeAvif } from './avif-probe.js';
|
|
47
|
+
import { probeWebp } from './webp-probe.js';
|
|
48
|
+
|
|
49
|
+
/** @type {Readonly<Record<string, string>>} media type -> file extension */
|
|
50
|
+
const EXTENSION_BY_MEDIA_TYPE = Object.freeze({
|
|
51
|
+
'image/png': 'png',
|
|
52
|
+
'image/jpeg': 'jpg',
|
|
53
|
+
'image/webp': 'webp',
|
|
54
|
+
'image/avif': 'avif',
|
|
55
|
+
'font/woff2': 'woff2',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @typedef {{path: string, sourceDigest: string}} ImageReference
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Collect every distinct image reference in `buildInput`, deduplicated by
|
|
64
|
+
* path (rejecting a path reused with a conflicting digest).
|
|
65
|
+
*
|
|
66
|
+
* @param {import('../../../../types/index.d.ts').NormalizedBuildInput} buildInput
|
|
67
|
+
* the validated build input
|
|
68
|
+
* @returns {ImageReference[]} the distinct image references, in
|
|
69
|
+
* first-seen order
|
|
70
|
+
*/
|
|
71
|
+
function collectImageReferences(buildInput) {
|
|
72
|
+
/** @type {Map<string, ImageReference>} */
|
|
73
|
+
const byPath = new Map();
|
|
74
|
+
/** @type {(ref: ImageReference | undefined) => void} */
|
|
75
|
+
const add = (ref) => {
|
|
76
|
+
if (!ref) return;
|
|
77
|
+
const existing = byPath.get(ref.path);
|
|
78
|
+
if (existing && existing.sourceDigest !== ref.sourceDigest) {
|
|
79
|
+
throw new MediaPipelineError(
|
|
80
|
+
'MEDIA_SOURCE_DIGEST_MISMATCH',
|
|
81
|
+
'the same path is referenced with two different declared digests',
|
|
82
|
+
ref.path,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
byPath.set(ref.path, ref);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
add(buildInput.publication.defaultImage);
|
|
89
|
+
for (const author of buildInput.authors) add(author.avatar);
|
|
90
|
+
for (const record of buildInput.content) {
|
|
91
|
+
if (record.frontmatter.hero) add(record.frontmatter.hero.file);
|
|
92
|
+
add(record.frontmatter.socialImage);
|
|
93
|
+
}
|
|
94
|
+
add(buildInput.appearance.brandMark);
|
|
95
|
+
add(buildInput.appearance.wordmark);
|
|
96
|
+
|
|
97
|
+
return [...byPath.values()];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Collect every distinct font reference in `buildInput`, deduplicated by
|
|
102
|
+
* path.
|
|
103
|
+
*
|
|
104
|
+
* @param {import('../../../../types/index.d.ts').NormalizedBuildInput} buildInput
|
|
105
|
+
* the validated build input
|
|
106
|
+
* @returns {ImageReference[]} the distinct font references
|
|
107
|
+
*/
|
|
108
|
+
function collectFontReferences(buildInput) {
|
|
109
|
+
/** @type {Map<string, ImageReference>} */
|
|
110
|
+
const byPath = new Map();
|
|
111
|
+
for (const font of buildInput.appearance.fontAssets ?? []) {
|
|
112
|
+
byPath.set(font.path, font);
|
|
113
|
+
}
|
|
114
|
+
return [...byPath.values()];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Read one referenced source file and verify its bytes match the declared
|
|
119
|
+
* digest, failing closed on any mismatch, traversal attempt, or oversize
|
|
120
|
+
* file.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} sourceDirectory the caller-mounted read-only source root
|
|
123
|
+
* @param {ImageReference} reference the reference to read and verify
|
|
124
|
+
* @returns {Promise<Buffer>} the verified file bytes
|
|
125
|
+
*/
|
|
126
|
+
async function readAndVerify(sourceDirectory, reference) {
|
|
127
|
+
const resolved = path.resolve(sourceDirectory, reference.path);
|
|
128
|
+
const root = path.resolve(sourceDirectory) + path.sep;
|
|
129
|
+
if (!resolved.startsWith(root)) {
|
|
130
|
+
throw new MediaPipelineError(
|
|
131
|
+
'MEDIA_FORMAT_INVALID',
|
|
132
|
+
'reference path escapes the source directory',
|
|
133
|
+
reference.path,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
let bytes;
|
|
137
|
+
try {
|
|
138
|
+
bytes = await readFile(resolved);
|
|
139
|
+
} catch {
|
|
140
|
+
throw new MediaPipelineError(
|
|
141
|
+
'MEDIA_FORMAT_INVALID',
|
|
142
|
+
'referenced source file is missing or unreadable',
|
|
143
|
+
reference.path,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (bytes.length > MAX_IMAGE_SOURCE_BYTES) {
|
|
147
|
+
throw new MediaPipelineError(
|
|
148
|
+
'MEDIA_RESOURCE_EXCEEDED',
|
|
149
|
+
`source is ${bytes.length} bytes, exceeding the ${MAX_IMAGE_SOURCE_BYTES}-byte ceiling`,
|
|
150
|
+
reference.path,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const actualDigest = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
154
|
+
if (actualDigest !== reference.sourceDigest) {
|
|
155
|
+
throw new MediaPipelineError(
|
|
156
|
+
'MEDIA_SOURCE_DIGEST_MISMATCH',
|
|
157
|
+
`source bytes hash to ${actualDigest} but build-input declared ${reference.sourceDigest}`,
|
|
158
|
+
reference.path,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return bytes;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {Buffer} bytes derivative file bytes
|
|
166
|
+
* @returns {string} `sha256:` plus 64 lowercase hexadecimal characters
|
|
167
|
+
*/
|
|
168
|
+
function digest(bytes) {
|
|
169
|
+
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Build the content-addressed output path for one media variant.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} sourceDigest the reference's declared `sourceDigest`
|
|
176
|
+
* @param {string} variant `'original'` or `'<width>w'`
|
|
177
|
+
* @param {string} extension the file extension (no leading dot)
|
|
178
|
+
* @returns {string} a `repoRelativePath`-shaped output path
|
|
179
|
+
*/
|
|
180
|
+
function derivativePath(sourceDigest, variant, extension) {
|
|
181
|
+
const hex = sourceDigest.replace(/^sha256:/, '');
|
|
182
|
+
return `assets/media/${hex}/${variant}.${extension}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @typedef {{path: string, bytes: Buffer}} PipelineOutputFile
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Process one image reference into its canonical original plus every
|
|
191
|
+
* admitted responsive derivative.
|
|
192
|
+
*
|
|
193
|
+
* @param {Buffer} bytes verified source bytes
|
|
194
|
+
* @param {ImageReference} reference the reference these bytes back
|
|
195
|
+
* @returns {PipelineOutputFile[]} the output files this reference produces
|
|
196
|
+
*/
|
|
197
|
+
function processImage(bytes, reference) {
|
|
198
|
+
const format = sniffMediaFormat(bytes);
|
|
199
|
+
if (format === 'svg') {
|
|
200
|
+
throw new MediaPipelineError(
|
|
201
|
+
'MEDIA_SVG_REJECTED',
|
|
202
|
+
'SVG is never processed as author media',
|
|
203
|
+
reference.path,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (format === 'unknown') {
|
|
207
|
+
throw new MediaPipelineError(
|
|
208
|
+
'MEDIA_FORMAT_INVALID',
|
|
209
|
+
'unrecognized image format (checked from decoded bytes, not extension)',
|
|
210
|
+
reference.path,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (format === 'webp' || format === 'avif') {
|
|
215
|
+
// Bounded container/dimension validation only; see webp-probe.js and
|
|
216
|
+
// avif-probe.js module documentation for the documented scope decision.
|
|
217
|
+
if (format === 'webp') {
|
|
218
|
+
probeWebp(bytes, reference.path);
|
|
219
|
+
} else {
|
|
220
|
+
probeAvif(bytes, reference.path);
|
|
221
|
+
}
|
|
222
|
+
const extension = EXTENSION_BY_MEDIA_TYPE[MEDIA_TYPE_BY_FORMAT[format]];
|
|
223
|
+
return [
|
|
224
|
+
{
|
|
225
|
+
path: derivativePath(reference.sourceDigest, 'original', extension),
|
|
226
|
+
bytes,
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** @type {{width: number, height: number, rgba: Buffer}} */
|
|
232
|
+
let oriented;
|
|
233
|
+
/** @type {(raster: {width: number, height: number, rgba: Buffer}) => Buffer} */
|
|
234
|
+
let encode;
|
|
235
|
+
/** @type {'png' | 'jpg'} */
|
|
236
|
+
let extension;
|
|
237
|
+
if (format === 'png') {
|
|
238
|
+
oriented = decodePng(bytes, reference.path);
|
|
239
|
+
encode = encodePng;
|
|
240
|
+
extension = 'png';
|
|
241
|
+
} else {
|
|
242
|
+
const decodedJpeg = decodeJpeg(bytes, reference.path);
|
|
243
|
+
oriented = applyExifOrientation(decodedJpeg, decodedJpeg.orientation);
|
|
244
|
+
encode = encodeJpeg;
|
|
245
|
+
extension = 'jpg';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** @type {PipelineOutputFile[]} */
|
|
249
|
+
const outputs = [
|
|
250
|
+
{
|
|
251
|
+
path: derivativePath(reference.sourceDigest, 'original', extension),
|
|
252
|
+
bytes: encode(oriented),
|
|
253
|
+
},
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
const widths = DERIVATIVE_WIDTHS.filter((w) => w < oriented.width);
|
|
257
|
+
for (const width of widths) {
|
|
258
|
+
const resized = resizeRaster(oriented, width, reference.path);
|
|
259
|
+
outputs.push({
|
|
260
|
+
path: derivativePath(reference.sourceDigest, `${width}w`, extension),
|
|
261
|
+
bytes: encode(resized),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return outputs;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Process every image and font reference in `buildInput` into content-
|
|
269
|
+
* addressed output files plus their `manifestAsset` rows.
|
|
270
|
+
*
|
|
271
|
+
* @param {import('../../../../types/index.d.ts').NormalizedBuildInput} buildInput
|
|
272
|
+
* the validated build input
|
|
273
|
+
* @param {{sourceDirectory: string}} options the media pipeline's own
|
|
274
|
+
* options
|
|
275
|
+
* @returns {Promise<{assets: import('../../../../types/index.d.ts').ManifestAssetEntry[], files: PipelineOutputFile[]}>}
|
|
276
|
+
* the asset manifest rows and the output files backing them
|
|
277
|
+
*/
|
|
278
|
+
export async function processMedia(buildInput, { sourceDirectory }) {
|
|
279
|
+
const imageReferences = collectImageReferences(buildInput);
|
|
280
|
+
if (imageReferences.length > MAX_IMAGES_PER_PUBLICATION) {
|
|
281
|
+
throw new MediaPipelineError(
|
|
282
|
+
'MEDIA_RESOURCE_EXCEEDED',
|
|
283
|
+
`publication references ${imageReferences.length} distinct images, exceeding the ${MAX_IMAGES_PER_PUBLICATION} ceiling`,
|
|
284
|
+
'publication',
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
const fontReferences = collectFontReferences(buildInput);
|
|
288
|
+
if (fontReferences.length > MAX_FONTS_PER_PUBLICATION) {
|
|
289
|
+
throw new MediaPipelineError(
|
|
290
|
+
'MEDIA_RESOURCE_EXCEEDED',
|
|
291
|
+
`publication references ${fontReferences.length} distinct fonts, exceeding the ${MAX_FONTS_PER_PUBLICATION} ceiling`,
|
|
292
|
+
'appearance',
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** @type {PipelineOutputFile[]} */
|
|
297
|
+
const files = [];
|
|
298
|
+
let totalBytes = 0;
|
|
299
|
+
|
|
300
|
+
for (const reference of imageReferences) {
|
|
301
|
+
const bytes = await readAndVerify(sourceDirectory, reference);
|
|
302
|
+
for (const output of processImage(bytes, reference)) {
|
|
303
|
+
totalBytes += output.bytes.byteLength;
|
|
304
|
+
files.push(output);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const reference of fontReferences) {
|
|
309
|
+
const bytes = await readAndVerify(sourceDirectory, reference);
|
|
310
|
+
assertFontPolicy(bytes, reference.path);
|
|
311
|
+
totalBytes += bytes.byteLength;
|
|
312
|
+
files.push({
|
|
313
|
+
path: derivativePath(reference.sourceDigest, 'original', 'woff2'),
|
|
314
|
+
bytes,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (totalBytes > MAX_TOTAL_MEDIA_BYTES_PER_PUBLICATION) {
|
|
319
|
+
throw new MediaPipelineError(
|
|
320
|
+
'MEDIA_RESOURCE_EXCEEDED',
|
|
321
|
+
`media pipeline output is ${totalBytes} bytes, exceeding the ${MAX_TOTAL_MEDIA_BYTES_PER_PUBLICATION}-byte publication ceiling`,
|
|
322
|
+
'publication',
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Deterministic path order: derivativePath is a pure function of
|
|
327
|
+
// (sourceDigest, variant, extension), so sorting the finished file list by
|
|
328
|
+
// its own output path — rather than by reference-discovery order — is
|
|
329
|
+
// what makes this pipeline's manifest.assets ordering independent of
|
|
330
|
+
// `build-input`'s own field ordering.
|
|
331
|
+
files.sort((a, b) =>
|
|
332
|
+
Buffer.compare(Buffer.from(a.path, 'utf8'), Buffer.from(b.path, 'utf8')),
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const assets = files.map((file) => {
|
|
336
|
+
const extension = file.path.slice(file.path.lastIndexOf('.') + 1);
|
|
337
|
+
const mediaType =
|
|
338
|
+
extension === 'woff2'
|
|
339
|
+
? 'font/woff2'
|
|
340
|
+
: extension === 'jpg'
|
|
341
|
+
? 'image/jpeg'
|
|
342
|
+
: `image/${extension}`;
|
|
343
|
+
return /** @type {import('../../../../types/index.d.ts').ManifestAssetEntry} */ ({
|
|
344
|
+
path: file.path,
|
|
345
|
+
mediaType,
|
|
346
|
+
byteLength: String(file.bytes.byteLength),
|
|
347
|
+
sha256: digest(file.bytes),
|
|
348
|
+
immutable: true,
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return { assets, files };
|
|
353
|
+
}
|