@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 +9 -0
- package/README.md +50 -0
- package/bin/build-variant-list.d.ts +5 -0
- package/bin/build-variant-list.js +334 -0
- package/bin/poster_1080p.jpg +0 -0
- package/bin/video_1080p.mp4 +0 -0
- package/build-variant-list.js +2 -0
- package/package.json +34 -0
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,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
|
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
|
+
}
|