pellicule 0.0.4 → 0.1.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/bin/cli.js +109 -13
- package/package.json +21 -3
- package/src/bundler/entry.js +126 -0
- package/src/bundler/rsbuild.js +177 -0
- package/src/bundler/vite.js +88 -91
- package/src/config/detect.js +225 -0
- package/src/macros/define-video-config.js +96 -34
- package/src/nuxt/module.js +108 -0
- package/src/nuxt/runtime/render-page.vue +89 -0
- package/src/quasar/index.js +134 -0
- package/src/renderer/encode.js +4 -0
- package/src/renderer/render.js +73 -10
package/bin/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { parseArgs } from 'node:util'
|
|
4
|
-
import { resolve, extname, basename, dirname } from 'node:path'
|
|
4
|
+
import { resolve, join, extname, basename, dirname } from 'node:path'
|
|
5
5
|
import { existsSync, readFileSync } from 'node:fs'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
7
|
import { renderToMp4 } from '../src/render.js'
|
|
8
8
|
import { extractVideoConfig, resolveVideoConfig } from '../src/macros/define-video-config.js'
|
|
9
|
+
import { detectProject, readPelliculeConfig, resolveInputFile } from '../src/config/detect.js'
|
|
9
10
|
|
|
10
11
|
// Read version from package.json
|
|
11
12
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
@@ -60,6 +61,13 @@ ${c.bold('OPTIONS')}
|
|
|
60
61
|
${c.info('--help')} Show this help message
|
|
61
62
|
${c.info('--version')} Show version number
|
|
62
63
|
|
|
64
|
+
${c.bold('INTEGRATION OPTIONS')}
|
|
65
|
+
${c.info('--server-url')} <url> Use a running dev server (BYOS mode)
|
|
66
|
+
${c.info('--bundler')} <name> Force a bundler: vite or rsbuild
|
|
67
|
+
${c.info('--config')} <file> Use a specific config file
|
|
68
|
+
${c.info('--videos-dir')} <path> Custom directory for video components
|
|
69
|
+
${c.info('--out-dir')} <path> Directory for rendered video output
|
|
70
|
+
|
|
63
71
|
${c.bold('COMPONENT CONFIG')}
|
|
64
72
|
Use ${c.highlight('defineVideoConfig')} in your component to set defaults:
|
|
65
73
|
|
|
@@ -68,6 +76,22 @@ ${c.bold('COMPONENT CONFIG')}
|
|
|
68
76
|
No import needed - it's a compiler macro like Vue's defineProps.
|
|
69
77
|
Then just run: ${c.highlight('pellicule')} ${c.dim('(no flags needed!)')}
|
|
70
78
|
|
|
79
|
+
${c.bold('PROJECT CONFIG')}
|
|
80
|
+
Set options once in package.json instead of passing CLI flags:
|
|
81
|
+
|
|
82
|
+
${c.dim('{ "pellicule": { "serverUrl": "http://localhost:3000" } }')}
|
|
83
|
+
|
|
84
|
+
Supported keys: ${c.info('serverUrl')}, ${c.info('videosDir')}, ${c.info('outDir')}, ${c.info('bundler')}
|
|
85
|
+
Resolution: CLI flags > package.json > auto-detected > defaults
|
|
86
|
+
|
|
87
|
+
${c.bold('AUTO-DETECTION')}
|
|
88
|
+
Pellicule reads your existing config files automatically:
|
|
89
|
+
${c.dim('vite.config.js')} → Vite adapter
|
|
90
|
+
${c.dim('rsbuild.config.js')} → Rsbuild adapter
|
|
91
|
+
${c.dim('config/shipwright.js')} → Rsbuild adapter (boring stack)
|
|
92
|
+
${c.dim('nuxt.config.ts')} → BYOS mode (defaults to localhost:3000)
|
|
93
|
+
${c.dim('No config')} → Built-in Vite (zero config)
|
|
94
|
+
|
|
71
95
|
${c.bold('EXAMPLES')}
|
|
72
96
|
${c.dim('# Zero-config (uses defineVideoConfig from component)')}
|
|
73
97
|
${c.highlight('pellicule')}
|
|
@@ -84,6 +108,12 @@ ${c.bold('EXAMPLES')}
|
|
|
84
108
|
${c.dim('# Render only frames 100-200 (for faster iteration)')}
|
|
85
109
|
${c.highlight('pellicule')} Video.vue -r 100:200
|
|
86
110
|
|
|
111
|
+
${c.dim('# Use with Nuxt (auto-detects, connects to localhost:3000)')}
|
|
112
|
+
${c.highlight('pellicule')} InvoiceDemo
|
|
113
|
+
|
|
114
|
+
${c.dim('# Force Rsbuild bundler')}
|
|
115
|
+
${c.highlight('pellicule')} Video.vue --bundler rsbuild
|
|
116
|
+
|
|
87
117
|
${c.bold('DURATION HELPER')}
|
|
88
118
|
frames = seconds * fps
|
|
89
119
|
${c.dim('3 seconds at 30fps = 90 frames')}
|
|
@@ -125,6 +155,11 @@ async function main() {
|
|
|
125
155
|
height: { type: 'string', short: 'h' },
|
|
126
156
|
range: { type: 'string', short: 'r' },
|
|
127
157
|
audio: { type: 'string', short: 'a' },
|
|
158
|
+
'server-url': { type: 'string' },
|
|
159
|
+
bundler: { type: 'string' },
|
|
160
|
+
config: { type: 'string' },
|
|
161
|
+
'videos-dir': { type: 'string' },
|
|
162
|
+
'out-dir': { type: 'string' },
|
|
128
163
|
help: { type: 'boolean' },
|
|
129
164
|
version: { type: 'boolean' }
|
|
130
165
|
}
|
|
@@ -141,21 +176,37 @@ async function main() {
|
|
|
141
176
|
process.exit(0)
|
|
142
177
|
}
|
|
143
178
|
|
|
144
|
-
//
|
|
145
|
-
const
|
|
179
|
+
// ── Auto-detection ────────────────────────────────────────────────
|
|
180
|
+
const detected = detectProject()
|
|
181
|
+
const pkgConfig = readPelliculeConfig()
|
|
182
|
+
|
|
183
|
+
// Resolution: CLI flags > package.json "pellicule" key > auto-detected
|
|
184
|
+
const bundler = values.bundler || pkgConfig.bundler || detected.bundler
|
|
185
|
+
const configFile = values.config ? resolve(values.config) : detected.configFile
|
|
186
|
+
const videosDir = values['videos-dir'] ? resolve(values['videos-dir']) : pkgConfig.videosDir || detected.videosDir
|
|
187
|
+
const outDir = values['out-dir'] ? resolve(values['out-dir']) : pkgConfig.outDir || null
|
|
188
|
+
const serverUrl = values['server-url'] || pkgConfig.serverUrl || detected.defaultServerUrl || null
|
|
189
|
+
const projectType = detected.projectType
|
|
190
|
+
|
|
191
|
+
// Validate bundler flag
|
|
192
|
+
if (values.bundler && !['vite', 'rsbuild'].includes(values.bundler)) {
|
|
193
|
+
fail(`Unknown bundler: ${values.bundler}`, 'Supported bundlers: vite, rsbuild')
|
|
194
|
+
}
|
|
146
195
|
|
|
147
|
-
//
|
|
148
|
-
|
|
196
|
+
// ── Input file resolution ─────────────────────────────────────────
|
|
197
|
+
const input = positionals[0] || 'Video.vue'
|
|
198
|
+
const result = resolveInputFile(input, videosDir)
|
|
149
199
|
|
|
150
|
-
if (
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
inputPath = withVue
|
|
154
|
-
}
|
|
200
|
+
if (result.error) {
|
|
201
|
+
const searchedPaths = result.searched.map(p => ` - ${p}`).join('\n')
|
|
202
|
+
fail(result.error, `Looked in:\n${searchedPaths}`)
|
|
155
203
|
}
|
|
156
204
|
|
|
157
|
-
|
|
158
|
-
|
|
205
|
+
const inputPath = result.resolved
|
|
206
|
+
|
|
207
|
+
if (extname(inputPath) !== '.vue') {
|
|
208
|
+
fail(`Input must be a .vue file, got: ${extname(inputPath) || '(no extension)'}`)
|
|
209
|
+
}
|
|
159
210
|
|
|
160
211
|
// Extract config from component (if defineVideoConfig is used)
|
|
161
212
|
const componentConfig = extractVideoConfig(inputPath)
|
|
@@ -185,7 +236,16 @@ async function main() {
|
|
|
185
236
|
const durationInFrames = resolvedConfig.durationInFrames
|
|
186
237
|
const width = resolvedConfig.width
|
|
187
238
|
const height = resolvedConfig.height
|
|
188
|
-
|
|
239
|
+
let output
|
|
240
|
+
if (values.output) {
|
|
241
|
+
output = values.output
|
|
242
|
+
} else if (outDir) {
|
|
243
|
+
// Use component name as filename when outDir is configured
|
|
244
|
+
const componentName = basename(inputPath, '.vue')
|
|
245
|
+
output = join(outDir, `${componentName}.mp4`)
|
|
246
|
+
} else {
|
|
247
|
+
output = './output.mp4'
|
|
248
|
+
}
|
|
189
249
|
const outputPath = resolve(output)
|
|
190
250
|
|
|
191
251
|
// Parse optional range (start:end format)
|
|
@@ -222,6 +282,23 @@ async function main() {
|
|
|
222
282
|
if (componentConfig) {
|
|
223
283
|
console.log(` ${c.bold('Config')} ${c.highlight('defineVideoConfig')} ${c.dim('detected ✓')}`)
|
|
224
284
|
}
|
|
285
|
+
|
|
286
|
+
// Show detected project info
|
|
287
|
+
if (projectType !== 'standalone') {
|
|
288
|
+
const projectLabels = {
|
|
289
|
+
laravel: 'Laravel',
|
|
290
|
+
vite: 'Vite',
|
|
291
|
+
rsbuild: 'Rsbuild',
|
|
292
|
+
shipwright: 'Boring Stack (Shipwright)',
|
|
293
|
+
nuxt: 'Nuxt',
|
|
294
|
+
quasar: 'Quasar'
|
|
295
|
+
}
|
|
296
|
+
console.log(` ${c.bold('Project')} ${c.highlight(projectLabels[projectType] || projectType)} ${c.dim('detected ✓')}`)
|
|
297
|
+
}
|
|
298
|
+
if (serverUrl) {
|
|
299
|
+
console.log(` ${c.bold('Server')} ${c.info(serverUrl)} ${c.dim('(BYOS)')}`)
|
|
300
|
+
}
|
|
301
|
+
|
|
225
302
|
console.log(` ${c.bold('Output')} ${c.info(basename(outputPath))}`)
|
|
226
303
|
console.log(` ${c.bold('Resolution')} ${width}x${height}`)
|
|
227
304
|
console.log(` ${c.bold('Duration')} ${durationInFrames} frames @ ${fps}fps ${c.dim(`(${durationSeconds}s)`)}`)
|
|
@@ -239,6 +316,15 @@ async function main() {
|
|
|
239
316
|
const clearLine = '\x1b[2K' // Clear entire line
|
|
240
317
|
const cursorToStart = '\r' // Move cursor to start of line
|
|
241
318
|
|
|
319
|
+
// For Nuxt and Quasar projects, construct the /pellicule render page URL.
|
|
320
|
+
// - Nuxt: pellicule/nuxt module injects the page
|
|
321
|
+
// - Quasar: pellicule/quasar Vite plugin serves the page
|
|
322
|
+
let finalServerUrl = serverUrl
|
|
323
|
+
if ((projectType === 'nuxt' || projectType === 'quasar') && serverUrl) {
|
|
324
|
+
const componentName = basename(inputPath, '.vue')
|
|
325
|
+
finalServerUrl = `${serverUrl.replace(/\/$/, '')}/pellicule?component=${encodeURIComponent(componentName)}`
|
|
326
|
+
}
|
|
327
|
+
|
|
242
328
|
try {
|
|
243
329
|
// Render with progress callback
|
|
244
330
|
await renderToMp4({
|
|
@@ -252,6 +338,10 @@ async function main() {
|
|
|
252
338
|
output: outputPath,
|
|
253
339
|
audio: audioPath,
|
|
254
340
|
silent: true,
|
|
341
|
+
serverUrl: finalServerUrl,
|
|
342
|
+
bundler,
|
|
343
|
+
configFile,
|
|
344
|
+
projectType,
|
|
255
345
|
onProgress: ({ frame, total, fps: currentFps }) => {
|
|
256
346
|
// Clear line and print progress (stays on same line)
|
|
257
347
|
process.stdout.write(clearLine + cursorToStart + formatProgress(frame, total, currentFps || fps))
|
|
@@ -280,6 +370,12 @@ async function main() {
|
|
|
280
370
|
console.error()
|
|
281
371
|
}
|
|
282
372
|
|
|
373
|
+
if (error.message.includes('Rsbuild')) {
|
|
374
|
+
console.error(c.warn(' Hint: Install @rsbuild/core and @rsbuild/plugin-vue'))
|
|
375
|
+
console.error(c.dim(' npm install -D @rsbuild/core @rsbuild/plugin-vue'))
|
|
376
|
+
console.error()
|
|
377
|
+
}
|
|
378
|
+
|
|
283
379
|
process.exit(1)
|
|
284
380
|
}
|
|
285
381
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pellicule",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Deterministic video rendering with Vue",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./src/index.js",
|
|
12
|
-
"./render": "./src/render.js"
|
|
12
|
+
"./render": "./src/render.js",
|
|
13
|
+
"./nuxt": "./src/nuxt/module.js",
|
|
14
|
+
"./quasar": "./src/quasar/index.js"
|
|
13
15
|
},
|
|
14
16
|
"keywords": [
|
|
15
17
|
"vue",
|
|
@@ -27,12 +29,28 @@
|
|
|
27
29
|
"url": "https://github.com/sailscastshq/pellicule"
|
|
28
30
|
},
|
|
29
31
|
"dependencies": {
|
|
30
|
-
"@vue/compiler-sfc": "^3.0.0",
|
|
31
32
|
"@vitejs/plugin-vue": "^5.0.0",
|
|
32
33
|
"playwright": "^1.40.0",
|
|
33
34
|
"vite": "^5.0.0"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
37
|
+
"@nuxt/kit": "^3.0.0",
|
|
38
|
+
"@rsbuild/core": "^1.0.0",
|
|
39
|
+
"@rsbuild/plugin-vue": "^1.0.0",
|
|
36
40
|
"vue": "^3.0.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"@rsbuild/core": {
|
|
44
|
+
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"@rsbuild/plugin-vue": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"@nuxt/kit": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@nuxt/kit": "^4.3.0"
|
|
37
55
|
}
|
|
38
56
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundler-agnostic entry file generation.
|
|
3
|
+
*
|
|
4
|
+
* Both the Vite adapter and the Rsbuild adapter produce the same
|
|
5
|
+
* index.html + entry.js scaffold to mount the user's Vue component.
|
|
6
|
+
* This module owns that template so changes propagate everywhere.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { writeFile, mkdir, rm } from 'fs/promises'
|
|
10
|
+
import { join, basename } from 'path'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate the entry HTML that wraps the video at the given dimensions.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} options
|
|
16
|
+
* @param {number} options.width
|
|
17
|
+
* @param {number} options.height
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function generateHtml({ width = 1920, height = 1080 }) {
|
|
21
|
+
return `<!DOCTYPE html>
|
|
22
|
+
<html>
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="UTF-8">
|
|
25
|
+
<style>
|
|
26
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
27
|
+
html, body { width: ${width}px; height: ${height}px; overflow: hidden; }
|
|
28
|
+
#app { width: 100%; height: 100%; }
|
|
29
|
+
</style>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<div id="app"></div>
|
|
33
|
+
<script type="module" src="./entry.js"></script>
|
|
34
|
+
</body>
|
|
35
|
+
</html>`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Generate the entry JS that mounts the Vue component with
|
|
40
|
+
* Pellicule's frame/config injection.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} options
|
|
43
|
+
* @param {string} options.componentPath - Relative path from the temp dir to the .vue file
|
|
44
|
+
* @param {number} options.width
|
|
45
|
+
* @param {number} options.height
|
|
46
|
+
* @returns {string}
|
|
47
|
+
*/
|
|
48
|
+
export function generateEntryJs({ componentPath, width = 1920, height = 1080 }) {
|
|
49
|
+
return `
|
|
50
|
+
import { createApp, ref, provide, h, nextTick } from 'vue'
|
|
51
|
+
import VideoComponent from '${componentPath}'
|
|
52
|
+
|
|
53
|
+
// Pellicule injection keys (must match composables.js)
|
|
54
|
+
const FRAME_KEY = Symbol.for('pellicule-frame')
|
|
55
|
+
const CONFIG_KEY = Symbol.for('pellicule-config')
|
|
56
|
+
|
|
57
|
+
// Get initial config from URL
|
|
58
|
+
const params = new URLSearchParams(window.location.search)
|
|
59
|
+
const fps = parseInt(params.get('fps') || '30', 10)
|
|
60
|
+
const durationInFrames = parseInt(params.get('duration') || '90', 10)
|
|
61
|
+
const width = parseInt(params.get('width') || '${width}', 10)
|
|
62
|
+
const height = parseInt(params.get('height') || '${height}', 10)
|
|
63
|
+
|
|
64
|
+
const config = { fps, durationInFrames, width, height }
|
|
65
|
+
|
|
66
|
+
// Frame ref - reactive, will trigger re-render when changed
|
|
67
|
+
const frameRef = ref(0)
|
|
68
|
+
|
|
69
|
+
// Expose setFrame function for the renderer to call
|
|
70
|
+
window.__PELLICULE_SET_FRAME__ = async (frame) => {
|
|
71
|
+
frameRef.value = frame
|
|
72
|
+
await nextTick() // Wait for Vue to re-render
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Create app with frame context
|
|
77
|
+
const app = createApp({
|
|
78
|
+
setup() {
|
|
79
|
+
provide(FRAME_KEY, frameRef)
|
|
80
|
+
provide(CONFIG_KEY, config)
|
|
81
|
+
return () => h(VideoComponent)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
app.mount('#app')
|
|
86
|
+
window.__PELLICULE_READY__ = true
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error('Pellicule render error:', error)
|
|
89
|
+
window.__PELLICULE_READY__ = true
|
|
90
|
+
window.__PELLICULE_ERROR__ = error.message
|
|
91
|
+
}
|
|
92
|
+
`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Write the temp scaffold (.pellicule/ folder with index.html + entry.js).
|
|
97
|
+
*
|
|
98
|
+
* @param {object} options
|
|
99
|
+
* @param {string} options.inputPath - Absolute path to the .vue file
|
|
100
|
+
* @param {number} options.width
|
|
101
|
+
* @param {number} options.height
|
|
102
|
+
* @returns {Promise<{ tempDir: string, cleanup: () => Promise<void> }>}
|
|
103
|
+
*/
|
|
104
|
+
export async function writeTempEntry({ inputPath, width = 1920, height = 1080 }) {
|
|
105
|
+
const inputDir = join(inputPath, '..')
|
|
106
|
+
const inputFile = basename(inputPath)
|
|
107
|
+
const tempDir = join(inputDir, '.pellicule')
|
|
108
|
+
|
|
109
|
+
await mkdir(tempDir, { recursive: true })
|
|
110
|
+
|
|
111
|
+
const html = generateHtml({ width, height })
|
|
112
|
+
const js = generateEntryJs({
|
|
113
|
+
componentPath: `../${inputFile}`,
|
|
114
|
+
width,
|
|
115
|
+
height
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await writeFile(join(tempDir, 'index.html'), html)
|
|
119
|
+
await writeFile(join(tempDir, 'entry.js'), js)
|
|
120
|
+
|
|
121
|
+
const cleanup = async () => {
|
|
122
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { tempDir, cleanup }
|
|
126
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rsbuild bundler adapter for Pellicule.
|
|
3
|
+
*
|
|
4
|
+
* Handles two scenarios:
|
|
5
|
+
* 1. Standalone Rsbuild projects (rsbuild.config.js / rsbuild.config.ts)
|
|
6
|
+
* 2. Boring Stack apps (config/shipwright.js → reads the `build` key)
|
|
7
|
+
*
|
|
8
|
+
* This module is lazy-loaded. @rsbuild/core is an optional peer dependency.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { resolve, dirname } from 'path'
|
|
12
|
+
import { fileURLToPath } from 'url'
|
|
13
|
+
import { createRequire } from 'module'
|
|
14
|
+
import { pelliculeMacroRsbuildPlugin } from '../macros/define-video-config.js'
|
|
15
|
+
import { writeTempEntry } from './entry.js'
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
18
|
+
const pelliculeSrc = resolve(__dirname, '..')
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load the user's Rsbuild config from a config file.
|
|
22
|
+
*
|
|
23
|
+
* For Shipwright configs, reads config/shipwright.js and
|
|
24
|
+
* extracts the `build` key (standard Rsbuild config).
|
|
25
|
+
*
|
|
26
|
+
* @param {string} configFile - Absolute path to the config file
|
|
27
|
+
* @param {'rsbuild'|'shipwright'} projectType
|
|
28
|
+
* @returns {Promise<object>} Rsbuild config object
|
|
29
|
+
*/
|
|
30
|
+
async function loadUserConfig(configFile, projectType) {
|
|
31
|
+
if (projectType === 'shipwright') {
|
|
32
|
+
// Shipwright configs are CommonJS: module.exports.shipwright = { build: { ... } }
|
|
33
|
+
const require = createRequire(import.meta.url)
|
|
34
|
+
const mod = require(configFile)
|
|
35
|
+
return mod?.shipwright?.build || {}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Standalone rsbuild.config.js — use Rsbuild's own config loader
|
|
39
|
+
const { loadConfig } = await import('@rsbuild/core')
|
|
40
|
+
const { content } = await loadConfig({ cwd: dirname(configFile) })
|
|
41
|
+
return content || {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Creates an Rsbuild dev server for rendering a video component.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} options
|
|
48
|
+
* @param {string} options.input - Absolute path to the .vue file
|
|
49
|
+
* @param {number} options.width - Video width
|
|
50
|
+
* @param {number} options.height - Video height
|
|
51
|
+
* @param {string|null} [options.configFile] - Path to the user's config file
|
|
52
|
+
* @param {'rsbuild'|'shipwright'} [options.projectType] - Which config format to read
|
|
53
|
+
* @returns {Promise<{ server: object, url: string, cleanup: function, tempDir: string }>}
|
|
54
|
+
*/
|
|
55
|
+
export async function createVideoServer(options) {
|
|
56
|
+
const {
|
|
57
|
+
input,
|
|
58
|
+
width = 1920,
|
|
59
|
+
height = 1080,
|
|
60
|
+
configFile = null,
|
|
61
|
+
projectType = 'rsbuild'
|
|
62
|
+
} = options
|
|
63
|
+
|
|
64
|
+
// Resolve Rsbuild from the user's project (not from pellicule's location).
|
|
65
|
+
// This is necessary because pellicule may be symlinked, and ESM import()
|
|
66
|
+
// resolves relative to the file's physical location, not the project root.
|
|
67
|
+
const projectRequire = createRequire(resolve(process.cwd(), 'package.json'))
|
|
68
|
+
|
|
69
|
+
let createRsbuild, mergeRsbuildConfig
|
|
70
|
+
try {
|
|
71
|
+
const rsbuildCorePath = projectRequire.resolve('@rsbuild/core')
|
|
72
|
+
const rsbuildCore = await import(rsbuildCorePath)
|
|
73
|
+
createRsbuild = rsbuildCore.createRsbuild
|
|
74
|
+
mergeRsbuildConfig = rsbuildCore.mergeRsbuildConfig
|
|
75
|
+
} catch {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'Rsbuild is required but not installed.\n' +
|
|
78
|
+
'Install it with: npm install -D @rsbuild/core @rsbuild/plugin-vue\n' +
|
|
79
|
+
'Or use --bundler vite to use the Vite adapter instead.'
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const inputPath = resolve(input)
|
|
84
|
+
|
|
85
|
+
// Write the shared entry scaffold (.pellicule/index.html + entry.js)
|
|
86
|
+
const { tempDir, cleanup: cleanupTemp } = await writeTempEntry({
|
|
87
|
+
inputPath,
|
|
88
|
+
width,
|
|
89
|
+
height
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// Load @rsbuild/plugin-vue (also resolved from user's project)
|
|
93
|
+
let pluginVue
|
|
94
|
+
try {
|
|
95
|
+
const pluginVuePath = projectRequire.resolve('@rsbuild/plugin-vue')
|
|
96
|
+
const mod = await import(pluginVuePath)
|
|
97
|
+
pluginVue = mod.pluginVue
|
|
98
|
+
} catch {
|
|
99
|
+
throw new Error(
|
|
100
|
+
'@rsbuild/plugin-vue is required for Rsbuild projects.\n' +
|
|
101
|
+
'Install it with: npm install -D @rsbuild/plugin-vue'
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Build resolve.alias — always alias 'pellicule' to the local source.
|
|
106
|
+
// Also alias 'vue' to the project's Vue to avoid duplicate Vue runtimes.
|
|
107
|
+
// Without this, pellicule's source files (physically located in the pellicule
|
|
108
|
+
// repo) would resolve 'vue' from their own node_modules, while the project's
|
|
109
|
+
// entry code resolves 'vue' from the project's node_modules — two different
|
|
110
|
+
// Vue instances means provide/inject breaks silently.
|
|
111
|
+
// For Shipwright projects, also add the conventional ~ and @ aliases
|
|
112
|
+
// that sails-hook-shipwright normally injects at runtime.
|
|
113
|
+
const aliases = {
|
|
114
|
+
'pellicule': pelliculeSrc,
|
|
115
|
+
'vue': projectRequire.resolve('vue')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (projectType === 'shipwright') {
|
|
119
|
+
const cwd = process.cwd()
|
|
120
|
+
aliases['@'] = resolve(cwd, 'assets', 'js')
|
|
121
|
+
aliases['~'] = resolve(cwd, 'assets')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Pellicule's required Rsbuild config
|
|
125
|
+
// logLevel: 'error' suppresses Rsbuild's "start build started..." and
|
|
126
|
+
// "ready built in X s" messages. Pellicule has its own progress display.
|
|
127
|
+
// Note: rsbuild.logger is NOT exposed on the createRsbuild() return object,
|
|
128
|
+
// so logger.override() doesn't work. logLevel is the config-level equivalent.
|
|
129
|
+
const pelliculeConfig = {
|
|
130
|
+
source: {
|
|
131
|
+
entry: {
|
|
132
|
+
index: resolve(tempDir, 'entry.js')
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
resolve: {
|
|
136
|
+
alias: aliases
|
|
137
|
+
},
|
|
138
|
+
html: {
|
|
139
|
+
template: resolve(tempDir, 'index.html')
|
|
140
|
+
},
|
|
141
|
+
plugins: [pelliculeMacroRsbuildPlugin(), pluginVue()],
|
|
142
|
+
server: {
|
|
143
|
+
strictPort: false,
|
|
144
|
+
printUrls: false
|
|
145
|
+
},
|
|
146
|
+
dev: {
|
|
147
|
+
writeToDisk: false
|
|
148
|
+
},
|
|
149
|
+
logLevel: 'error'
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let finalConfig = pelliculeConfig
|
|
153
|
+
|
|
154
|
+
// If the user has a config file, load and merge it
|
|
155
|
+
if (configFile) {
|
|
156
|
+
const userConfig = await loadUserConfig(configFile, projectType)
|
|
157
|
+
|
|
158
|
+
if (userConfig && Object.keys(userConfig).length > 0) {
|
|
159
|
+
// User config is the base, Pellicule config merges on top
|
|
160
|
+
finalConfig = mergeRsbuildConfig(userConfig, pelliculeConfig)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const rsbuild = await createRsbuild({ rsbuildConfig: finalConfig })
|
|
165
|
+
|
|
166
|
+
const devServer = await rsbuild.createDevServer()
|
|
167
|
+
const { port } = await devServer.listen()
|
|
168
|
+
|
|
169
|
+
const url = `http://localhost:${port}`
|
|
170
|
+
|
|
171
|
+
const cleanup = async () => {
|
|
172
|
+
await devServer.close()
|
|
173
|
+
await cleanupTemp()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { server: devServer, url, cleanup, tempDir }
|
|
177
|
+
}
|