@turbo-player/build-variant-list 1.1578.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 glomex GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @turbo-player/build-variant-list
2
+
3
+ A command-line tool (CLI) to build and generate variant manifests/lists for the turbo player integration.
4
+
5
+ This tool uses Puppeteer under the hood to crawl or render the player configurations and export the built variant profiles.
6
+
7
+ ## Installation
8
+
9
+ You can install this package from NPM like this:
10
+
11
+ ```bash
12
+ # Using npm
13
+ npm install @turbo-player/build-variant-list
14
+
15
+ # Using pnpm
16
+ pnpm add @turbo-player/build-variant-list
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ This package exposes a CLI executable named `build-variant-list`.
22
+
23
+ ### Command Line
24
+
25
+ ```bash
26
+ build-variant-list
27
+ ```
28
+
29
+ ### Environment Variables
30
+
31
+ The CLI configures itself dynamically using the following environment variables:
32
+
33
+ - `VARIANTS_MANIFEST`: Path to the local compiled variants manifest file (e.g., `./build/variants-manifest.js`).
34
+ - `INTEGRATION_TAG`: The integration tag identifier (e.g., `my-integration`).
35
+ - `JS_FILE`: The entry JavaScript filename (e.g., `integration.js`).
36
+ - `OUTPUT_FILE`: Path where the generated JSON list should be written (e.g., `./build/variant/list.json`).
37
+
38
+ ### Example Integration Script
39
+
40
+ Typically, this tool is run as part of a post-build lifecycle hook in a monorepo setup:
41
+
42
+ ```json
43
+ "scripts": {
44
+ "build:variant-list": "VARIANTS_MANIFEST=./build/variants-manifest.js INTEGRATION_TAG=my-integration JS_FILE=integration.js OUTPUT_FILE=./build/variant/list.json pnpm exec build-variant-list"
45
+ }
46
+ ```
47
+
48
+ ## License
49
+
50
+ This project is licensed under the [MIT License](./LICENSE.md).
@@ -0,0 +1,5 @@
1
+ declare global {
2
+ interface Window {
3
+ }
4
+ }
5
+ export {};
@@ -0,0 +1,334 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { join, relative, resolve } from 'node:path';
3
+ import { serve } from '@hono/node-server';
4
+ import { serveStatic } from '@hono/node-server/serve-static';
5
+ import { Hono } from 'hono';
6
+ import puppeteer from 'puppeteer';
7
+ const INTEGRATION_NAME_PLACEHOLDER = '{{INTEGRATION_NAME}}';
8
+ const { PORT = '8787', WAIT_TIMEOUT = '30000', VARIANTS_MANIFEST, INTEGRATION_TAG, JS_FILE, OUTPUT_FILE } = process.env;
9
+ if (!OUTPUT_FILE) {
10
+ throw new Error('OUTPUT_FILE env variable needs to be defined.');
11
+ }
12
+ if (!VARIANTS_MANIFEST) {
13
+ throw new Error('VARIANTS_MANIFEST env variable needs to be defined.');
14
+ }
15
+ if (!INTEGRATION_TAG) {
16
+ throw new Error('INTEGRATION_TAG env variable needs to be defined.');
17
+ }
18
+ if (!JS_FILE) {
19
+ throw new Error('JS_FILE env variable needs to be defined.');
20
+ }
21
+ const manifestPath = resolve(VARIANTS_MANIFEST);
22
+ const { default: variantsManifest } = (await import(manifestPath));
23
+ const INTEGRATION_TAG_NAME = INTEGRATION_TAG;
24
+ const variantNames = Object.keys(variantsManifest);
25
+ const waitTimeout = Number(WAIT_TIMEOUT);
26
+ if (Number.isNaN(waitTimeout)) {
27
+ throw new Error('WAIT_TIMEOUT env variable needs to be a number.');
28
+ }
29
+ const outputFile = resolve(OUTPUT_FILE);
30
+ const resolvedBasePath = join(outputFile, '..', '..');
31
+ // Ensure parent directory of output file exists before writing
32
+ await mkdir(join(outputFile, '..'), { recursive: true });
33
+ // ---------------------------------------------------------------------------
34
+ /**
35
+ * Returns a minimal HTML page that loads the integration JS and mounts the
36
+ * integration element. This avoids a hard dependency on the pre-built
37
+ * integration.html; all we need is the compiled JS file and the tag name.
38
+ */
39
+ function buildMinimalHtml(integrationTag, jsFile) {
40
+ return `<!doctype html>
41
+ <html lang="en">
42
+ <head>
43
+ <meta charset="utf-8" />
44
+ <style>
45
+ html {
46
+ font-family: system-ui, sans-serif;
47
+ width: 100%;
48
+ height: 100%;
49
+ }
50
+ body {
51
+ margin: 0;
52
+ width: 100%;
53
+ height: 100%;
54
+ }
55
+ ${integrationTag} {
56
+ height: 100%;
57
+ }
58
+ ::-webkit-scrollbar {
59
+ display: none;
60
+ }
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <script type="module">
65
+ import { addIntegrationToIframe } from './${jsFile}';
66
+ addIntegrationToIframe(window, '${integrationTag}');
67
+ </script>
68
+ </body>
69
+ </html>`;
70
+ }
71
+ // Served over HTTP because blob:nodedata:... URLs are not accessible to the browser.
72
+ // Regenerate: ffmpeg -f lavfi -i "color=black:size=1920x1080:duration=1:rate=1" -f lavfi -i "anullsrc=r=44100:cl=stereo" -t 1 -c:v libx264 -profile:v baseline -level 3.1 -pix_fmt yuv420p -c:a aac -movflags +faststart bin/probe_1080p.mp4
73
+ const VIDEO_BYTES = await readFile(join(import.meta.dirname, 'video_1080p.mp4'));
74
+ const POSTER_IMAGE_BYTES = await readFile(join(import.meta.dirname, 'poster_1080p.jpg'));
75
+ // serve static files via http server so that the browser can load the integration
76
+ const app = new Hono();
77
+ app.get('/integration-bootstrap', (c) => {
78
+ return c.html(buildMinimalHtml(INTEGRATION_TAG_NAME, JS_FILE));
79
+ });
80
+ app.get('/probe.mp4', (c) => {
81
+ return c.body(new Uint8Array(VIDEO_BYTES), 200, { 'Content-Type': 'video/mp4' });
82
+ });
83
+ app.get('/poster.jpg', (c) => {
84
+ return c.body(new Uint8Array(POSTER_IMAGE_BYTES), 200, { 'Content-Type': 'image/jpeg' });
85
+ });
86
+ app.use('/*', serveStatic({ root: relative(process.cwd(), resolvedBasePath) }));
87
+ const server = serve({
88
+ fetch: app.fetch,
89
+ port: Number(PORT)
90
+ });
91
+ async function getCssPartsWithVariables(page, integrationName, variantName) {
92
+ const posterImage = `http://localhost:${PORT}/poster.jpg`;
93
+ const mediaItems = [
94
+ {
95
+ id: `my-video`,
96
+ title: `my video`,
97
+ poster: posterImage,
98
+ sources: [
99
+ {
100
+ src: `http://localhost:${PORT}/probe.mp4`
101
+ }
102
+ ]
103
+ }
104
+ ];
105
+ await page
106
+ .goto(`http://localhost:${PORT}/integration-bootstrap?integrationId=40599x1il0m8akog&mediaItems=${encodeURIComponent(JSON.stringify(mediaItems))}&variant=${variantName}`, {
107
+ waitUntil: 'domcontentloaded'
108
+ })
109
+ .catch((error) => {
110
+ throw new Error(`Goto Failed: ${error}`);
111
+ });
112
+ await page.waitForSelector(integrationName, { timeout: waitTimeout }).catch((error) => {
113
+ throw new Error(`Wait for selector failed: ${error}`);
114
+ });
115
+ const result = await page
116
+ .evaluate(evaluateCssPartsWithVariables, integrationName, INTEGRATION_NAME_PLACEHOLDER)
117
+ .catch((error) => {
118
+ throw new Error(`eval failed: ${error}`);
119
+ });
120
+ return result;
121
+ }
122
+ const evaluateCssPartsWithVariables = async (integrationName, integrationNamePlaceholder) => {
123
+ const integration = window.document.querySelector(integrationName);
124
+ if (!integration) {
125
+ throw new Error(`Integration root "${integrationName}" not found in DOM.`);
126
+ }
127
+ await window.customElements.whenDefined(integrationName);
128
+ await integration.ready;
129
+ // Give Lit a moment to flush re-renders triggered by playlist data arriving
130
+ // after ready, so all child custom elements are upgraded and their shadow
131
+ // roots are populated before we traverse them.
132
+ await new Promise((resolve) => setTimeout(resolve, 500));
133
+ function convertExportparts(exportparts) {
134
+ return exportparts
135
+ .toLowerCase()
136
+ .replace(/ /g, '')
137
+ .split(',')
138
+ .map((part) => {
139
+ const splitPart = part.split(':');
140
+ if (splitPart.length > 1) {
141
+ return {
142
+ [splitPart[1]]: splitPart[0]
143
+ };
144
+ }
145
+ return {
146
+ [splitPart[0]]: splitPart[0]
147
+ };
148
+ })
149
+ .reduce((acc, currentValue) => ({
150
+ // biome-ignore lint/performance/noAccumulatingSpread: it is fine here
151
+ ...acc,
152
+ ...currentValue
153
+ }));
154
+ }
155
+ const getPart = (partName, currentNode) => {
156
+ const { shadowRoot } = currentNode;
157
+ if (shadowRoot) {
158
+ const partNode = shadowRoot.querySelector(`[part~=${partName}]`);
159
+ if (partNode?.getAttribute('part')) {
160
+ return partNode;
161
+ }
162
+ const exportpartsNode = shadowRoot.querySelector(`[exportparts*=${partName}]`);
163
+ const exportParts = exportpartsNode?.getAttribute('exportparts');
164
+ if (exportParts && exportpartsNode) {
165
+ const exportparts = convertExportparts(exportParts);
166
+ return getPart(exportparts[partName], exportpartsNode);
167
+ }
168
+ }
169
+ };
170
+ const getPartNodes = (givenNode, partNodes, integrationNamePlaceholder) => {
171
+ if (!givenNode)
172
+ return partNodes;
173
+ if (!partNodes[integrationNamePlaceholder]) {
174
+ partNodes[integrationNamePlaceholder] = [];
175
+ }
176
+ partNodes[integrationNamePlaceholder].push(givenNode);
177
+ Array.prototype.slice.call(givenNode.shadowRoot?.querySelectorAll('*')).forEach((node) => {
178
+ if (node.getAttribute('part')) {
179
+ if (partNodes[`${integrationNamePlaceholder}::part(${node.getAttribute('part')})`]) {
180
+ partNodes[`${integrationNamePlaceholder}::part(${node.getAttribute('part')})`].push(node);
181
+ }
182
+ else {
183
+ partNodes[`${integrationNamePlaceholder}::part(${node.getAttribute('part')})`] = [node];
184
+ }
185
+ }
186
+ else if (!node.assignedSlot) {
187
+ partNodes[integrationNamePlaceholder].push(node);
188
+ }
189
+ if (node.getAttribute('exportparts')) {
190
+ const exportparts = convertExportparts(node.getAttribute('exportparts'));
191
+ Object.keys(exportparts).forEach((part) => {
192
+ const partRef = getPart(exportparts[part], node);
193
+ if (!partRef)
194
+ return;
195
+ if (partNodes[`${integrationNamePlaceholder}::part(${part})`]) {
196
+ partNodes[`${integrationNamePlaceholder}::part(${part})`].push(partRef);
197
+ }
198
+ else {
199
+ partNodes[`${integrationNamePlaceholder}::part(${part})`] = [partRef];
200
+ }
201
+ });
202
+ }
203
+ });
204
+ return partNodes;
205
+ };
206
+ const partNodes = {};
207
+ getPartNodes(integration, partNodes, integrationNamePlaceholder);
208
+ // collect CSS variables of selected parts
209
+ const selectorToVariables = {};
210
+ Object.keys(partNodes).forEach((selector) => {
211
+ const matchSet = new Set();
212
+ partNodes[selector].forEach((node) => {
213
+ if (node.shadowRoot) {
214
+ for (const stylesheet of [
215
+ ...node.shadowRoot.styleSheets,
216
+ ...node.shadowRoot.adoptedStyleSheets
217
+ ]) {
218
+ let cssRules;
219
+ try {
220
+ // external stylesheets can't be read
221
+ cssRules = stylesheet.cssRules;
222
+ }
223
+ catch (_e) {
224
+ // ignore
225
+ }
226
+ if (!cssRules)
227
+ continue;
228
+ for (const rule of cssRules) {
229
+ // TODO: maybe collecting the right-hand-side of: var(--variable, value)
230
+ const matches = rule.cssText.match(/var\(--([^),]*)/gi);
231
+ if (matches) {
232
+ matches.forEach((match) => {
233
+ if (!node?.shadowRoot)
234
+ return;
235
+ const cssVariable = match.replace(/var\(/i, '');
236
+ const cssValue = window
237
+ .getComputedStyle(node.shadowRoot.host)
238
+ .getPropertyValue(cssVariable);
239
+ matchSet.add(`${cssVariable}: ${cssValue ? cssValue.trim() : 'initial'};`);
240
+ });
241
+ }
242
+ }
243
+ }
244
+ }
245
+ selectorToVariables[selector] = [...matchSet];
246
+ });
247
+ });
248
+ // generate example CSS
249
+ // with all exposed parts and available CSS variables
250
+ // make sure to escape any closing CSS comments
251
+ let defaultCss = '';
252
+ Object.keys(selectorToVariables).forEach((selector) => {
253
+ defaultCss += `${selector}${selectorToVariables[selector].length > 0
254
+ ? ` {
255
+ ${selectorToVariables[selector].map((cssVar) => ` /* ${cssVar.replace(/\*\//g, '*\\/')} */`).join('\n')}
256
+ }
257
+ `
258
+ : ' {}'}
259
+ `;
260
+ });
261
+ return defaultCss;
262
+ };
263
+ try {
264
+ (async () => {
265
+ const browser = await puppeteer.launch({
266
+ headless: true,
267
+ args: ['--no-sandbox', '--disable-setuid-sandbox']
268
+ // in case you want to execute it locally (requiring real video tag)
269
+ // executablePath: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
270
+ });
271
+ console.log(`Evaluate templates: ${variantNames.join(', ')}`);
272
+ const templateList = [];
273
+ for (const variantName of variantNames) {
274
+ console.log(`Process template: ${variantName}`);
275
+ const manifestVariant = variantsManifest[variantName];
276
+ if (!manifestVariant) {
277
+ console.warn(`Variant ${variantName} not found in variants-manifest.ts`);
278
+ continue;
279
+ }
280
+ const defaultSettings = manifestVariant.defaultSettings;
281
+ let templateCss = manifestVariant.defaultCss;
282
+ // Replace variant selector with the placeholder (e.g., [variant="adunit-billboard"] -> {{INTEGRATION_NAME}})
283
+ templateCss = templateCss.replace(new RegExp(`\\[variant=["']${variantName}["']\\]`, 'g'), INTEGRATION_NAME_PLACEHOLDER);
284
+ let page;
285
+ try {
286
+ const context = await browser.createBrowserContext();
287
+ page = await context.newPage();
288
+ }
289
+ catch (error) {
290
+ throw new Error(`Failed to create page: ${error}`);
291
+ }
292
+ // because of this issue: https://github.com/evanw/esbuild/issues/2605#issuecomment-2050808084
293
+ // we need to set the __name function
294
+ await page
295
+ .evaluateOnNewDocument(() => {
296
+ window.__name = (func) => func;
297
+ })
298
+ .catch((error) => {
299
+ throw new Error(`New Document Evaluation Failed: ${error}`);
300
+ });
301
+ page.on('error', (error) => {
302
+ console.log('error occurred: ', error);
303
+ });
304
+ page.on('pageerror', (error) => {
305
+ console.trace('pageerror occurred: ', error);
306
+ process.exit(1);
307
+ });
308
+ page.on('console', (consoleObj) => {
309
+ console.log('browser log', consoleObj.text());
310
+ });
311
+ const cssPartsWithVariables = await getCssPartsWithVariables(page, INTEGRATION_TAG_NAME, variantName);
312
+ await page.browserContext().close();
313
+ const finalCss = `${templateCss}\n${cssPartsWithVariables}`;
314
+ templateList.push({
315
+ defaultCss: finalCss,
316
+ defaultSettings,
317
+ variantName
318
+ });
319
+ }
320
+ await writeFile(outputFile, JSON.stringify(templateList, null, 2));
321
+ await browser.close();
322
+ server.close();
323
+ })();
324
+ }
325
+ catch (error) {
326
+ console.error(error);
327
+ server.close();
328
+ process.exit(1);
329
+ }
330
+ process.on('unhandledRejection', (error) => {
331
+ console.error('unhandledRejection', error);
332
+ server.close();
333
+ process.exit(1);
334
+ });
Binary file
Binary file
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import './bin/build-variant-list.js';
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@turbo-player/build-variant-list",
3
+ "version": "1.1578.4",
4
+ "bin": {
5
+ "build-variant-list": "./build-variant-list.js"
6
+ },
7
+ "description": "CLI tool to build variant lists for turbo player",
8
+ "type": "module",
9
+ "files": [
10
+ "LICENSE.md",
11
+ "build-variant-list.js",
12
+ "bin"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "lint": "biome ci",
17
+ "prepack": "npm run build",
18
+ "postpublish": "pnpm publish --registry=https://npm.pkg.github.com --no-git-checks --ignore-scripts"
19
+ },
20
+ "license": "MIT",
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "dependencies": {
25
+ "@hono/node-server": "^1.19.5",
26
+ "hono": "^4.10.2",
27
+ "puppeteer": "^24.33.0"
28
+ },
29
+ "devDependencies": {
30
+ "@biomejs/biome": "catalog:",
31
+ "@types/node": "^22.18.10",
32
+ "typescript": "catalog:"
33
+ }
34
+ }