promptslide 0.3.3 → 0.3.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/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/commands/add.mjs +1 -2
- package/src/commands/create.mjs +1 -2
- package/src/commands/publish.mjs +200 -184
- package/src/utils/export.mjs +68 -0
- package/src/utils/registry.mjs +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.4](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.3...promptslide-v0.3.4) (2026-03-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* unify deckPrefix and deckSlug into single deck slug concept ([#66](https://github.com/prompticeu/promptslide/issues/66)) ([f091c7d](https://github.com/prompticeu/promptslide/commit/f091c7dcd93738670fde4b48ff1020a358bce128))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Performance Improvements
|
|
12
|
+
|
|
13
|
+
* optimize deck publish with parallel phases and reusable capture session ([#63](https://github.com/prompticeu/promptslide/issues/63)) ([62fb985](https://github.com/prompticeu/promptslide/commit/62fb9853f7d220be8bf5ba07e5cc55744b658229))
|
|
14
|
+
|
|
3
15
|
## [0.3.3](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.2...promptslide-v0.3.3) (2026-03-07)
|
|
4
16
|
|
|
5
17
|
|
package/package.json
CHANGED
package/src/commands/add.mjs
CHANGED
|
@@ -149,8 +149,7 @@ export async function add(args) {
|
|
|
149
149
|
|
|
150
150
|
// Persist deck slug for future pull/publish if this is a deck
|
|
151
151
|
if (item.type === "deck") {
|
|
152
|
-
|
|
153
|
-
updateLockfilePublishConfig(cwd, { deckSlug: item.name, deckPrefix: prefix })
|
|
152
|
+
updateLockfilePublishConfig(cwd, { deckSlug: item.name })
|
|
154
153
|
}
|
|
155
154
|
|
|
156
155
|
// Auto-update deck-config.ts
|
package/src/commands/create.mjs
CHANGED
|
@@ -236,8 +236,7 @@ export async function create(args) {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
// Persist deck slug for future pull/publish in the new project
|
|
239
|
-
|
|
240
|
-
updateLockfilePublishConfig(targetDir, { deckSlug: fromSlug, deckPrefix: prefix })
|
|
239
|
+
updateLockfilePublishConfig(targetDir, { deckSlug: fromSlug })
|
|
241
240
|
|
|
242
241
|
console.log()
|
|
243
242
|
}
|
package/src/commands/publish.mjs
CHANGED
|
@@ -4,41 +4,43 @@ import { fileURLToPath } from "node:url"
|
|
|
4
4
|
|
|
5
5
|
import { bold, green, cyan, red, dim } from "../utils/ansi.mjs"
|
|
6
6
|
import { requireAuth } from "../utils/auth.mjs"
|
|
7
|
-
import { captureSlideAsDataUri, isPlaywrightAvailable } from "../utils/export.mjs"
|
|
8
|
-
import { publishToRegistry, registryItemExists, searchRegistry, updateLockfileItem, updateLockfilePublishConfig, readLockfile, hashContent, detectPackageManager, requestUploadTokens, uploadBinaryToBlob, assetFileToSlug, detectAssetDepsInContent } from "../utils/registry.mjs"
|
|
7
|
+
import { captureSlideAsDataUri, isPlaywrightAvailable, createCaptureSession } from "../utils/export.mjs"
|
|
8
|
+
import { publishToRegistry, registryItemExists, searchRegistry, updateLockfileItem, updateLockfilePublishConfig, readLockfile, writeLockfile, hashContent, detectPackageManager, requestUploadTokens, uploadBinaryToBlob, assetFileToSlug, detectAssetDepsInContent } from "../utils/registry.mjs"
|
|
9
9
|
import { prompt, confirm, select, closePrompts } from "../utils/prompts.mjs"
|
|
10
10
|
import { parseDeckConfig } from "../utils/deck-config.mjs"
|
|
11
11
|
|
|
12
12
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
13
13
|
const CLI_VERSION = JSON.parse(readFileSync(join(__dirname, "..", "..", "package.json"), "utf-8")).version
|
|
14
14
|
|
|
15
|
-
function
|
|
16
|
-
// Prefer stored prefix from lockfile (user's previous choice)
|
|
15
|
+
function readDeckSlug(cwd) {
|
|
17
16
|
const lock = readLockfile(cwd)
|
|
17
|
+
// Prefer stored deck slug — migrate old two-part format (e.g. "my-deck/name" → "my-deck")
|
|
18
|
+
if (lock.deckSlug) return lock.deckSlug.split("/")[0]
|
|
19
|
+
// Migrate legacy deckPrefix (old lockfiles stored prefix separately)
|
|
18
20
|
if (lock.deckPrefix) return lock.deckPrefix
|
|
21
|
+
return ""
|
|
22
|
+
}
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return (pkg.name || "").toLowerCase()
|
|
24
|
-
} catch {
|
|
25
|
-
return ""
|
|
26
|
-
}
|
|
24
|
+
function defaultDeckSlug(cwd) {
|
|
25
|
+
const dirName = basename(cwd)
|
|
26
|
+
return dirName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
async function
|
|
30
|
-
const
|
|
29
|
+
async function promptDeckSlug(cwd, interactive) {
|
|
30
|
+
const stored = readDeckSlug(cwd)
|
|
31
|
+
const fallback = defaultDeckSlug(cwd)
|
|
32
|
+
const defaultValue = stored || fallback
|
|
31
33
|
if (!interactive) {
|
|
32
|
-
if (!
|
|
33
|
-
return
|
|
34
|
+
if (!defaultValue) throw new Error("Deck slug is required. Publish interactively to set it.")
|
|
35
|
+
return defaultValue
|
|
34
36
|
}
|
|
35
|
-
let
|
|
37
|
+
let slug
|
|
36
38
|
while (true) {
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
console.log(` ${red("Error:")} Deck
|
|
39
|
+
slug = await prompt("Deck slug:", defaultValue)
|
|
40
|
+
if (slug && /^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(slug)) break
|
|
41
|
+
console.log(` ${red("Error:")} Deck slug is required (lowercase alphanumeric with hyphens, min 2 chars)`)
|
|
40
42
|
}
|
|
41
|
-
return
|
|
43
|
+
return slug
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
function titleCase(slug) {
|
|
@@ -228,10 +230,10 @@ function scanForFiles(cwd) {
|
|
|
228
230
|
|
|
229
231
|
/**
|
|
230
232
|
* Publish a single item to the registry.
|
|
231
|
-
* @param {{ filePath: string, cwd: string, auth: object, typeOverride?: string, interactive?: boolean,
|
|
233
|
+
* @param {{ filePath: string, cwd: string, auth: object, typeOverride?: string, interactive?: boolean, deckSlug?: string }} opts
|
|
232
234
|
* @returns {Promise<{ slug: string, status: string }>}
|
|
233
235
|
*/
|
|
234
|
-
async function publishItem({ filePath, cwd, auth, typeOverride, interactive = true,
|
|
236
|
+
async function publishItem({ filePath, cwd, auth, typeOverride, interactive = true, deckSlug: deckSlugOverride }) {
|
|
235
237
|
const fullPath = join(cwd, filePath)
|
|
236
238
|
if (!existsSync(fullPath)) {
|
|
237
239
|
throw new Error(`File not found: ${filePath}`)
|
|
@@ -240,8 +242,8 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
|
|
|
240
242
|
const content = readFileSync(fullPath, "utf-8")
|
|
241
243
|
const fileName = basename(fullPath)
|
|
242
244
|
const baseSlug = fileName.replace(/\.tsx?$/, "")
|
|
243
|
-
const
|
|
244
|
-
const slug = `${
|
|
245
|
+
const deck = deckSlugOverride || await promptDeckSlug(cwd, interactive)
|
|
246
|
+
const slug = `${deck}/${baseSlug}`
|
|
245
247
|
|
|
246
248
|
const type = typeOverride || detectType(filePath) || "slide"
|
|
247
249
|
const steps = detectSteps(content)
|
|
@@ -283,7 +285,7 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
|
|
|
283
285
|
previewImage = await captureSlideAsDataUri({ cwd, slidePath: filePath }).catch(() => null)
|
|
284
286
|
}
|
|
285
287
|
|
|
286
|
-
const prefixedDeps = registryDeps.map(d => `${
|
|
288
|
+
const prefixedDeps = registryDeps.map(d => `${deck}/${d}`)
|
|
287
289
|
const payload = {
|
|
288
290
|
type,
|
|
289
291
|
slug,
|
|
@@ -378,33 +380,11 @@ export async function publish(args) {
|
|
|
378
380
|
process.exit(1)
|
|
379
381
|
}
|
|
380
382
|
|
|
381
|
-
//
|
|
382
|
-
const
|
|
383
|
-
const existingSlug = existingLock.deckSlug
|
|
384
|
-
let deckPrefix, deckSlug
|
|
383
|
+
// Resolve deck slug (= deck identity, used as namespace for all items)
|
|
384
|
+
const deckSlug = await promptDeckSlug(cwd, true)
|
|
385
385
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
if (existingSlug) {
|
|
390
|
-
console.log(` ${dim("Previously published as")} ${cyan(existingSlug)}`)
|
|
391
|
-
console.log()
|
|
392
|
-
const reuse = await confirm(` Publish to ${bold(existingSlug)}?`)
|
|
393
|
-
|
|
394
|
-
if (reuse) {
|
|
395
|
-
deckSlug = existingSlug
|
|
396
|
-
deckPrefix = existingSlug.split("/")[0]
|
|
397
|
-
} else {
|
|
398
|
-
deckPrefix = await promptDeckPrefix(cwd, true)
|
|
399
|
-
deckSlug = `${deckPrefix}/${deckBaseSlug}`
|
|
400
|
-
}
|
|
401
|
-
} else {
|
|
402
|
-
deckPrefix = await promptDeckPrefix(cwd, true)
|
|
403
|
-
deckSlug = `${deckPrefix}/${deckBaseSlug}`
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Persist prefix for next time (slug is saved after successful publish)
|
|
407
|
-
updateLockfilePublishConfig(cwd, { deckPrefix })
|
|
386
|
+
// Persist slug early so it's available even if publish fails partway
|
|
387
|
+
updateLockfilePublishConfig(cwd, { deckSlug })
|
|
408
388
|
|
|
409
389
|
// Walk public/ to collect assets and build reference set
|
|
410
390
|
const publicDir = join(cwd, "public")
|
|
@@ -458,7 +438,7 @@ export async function publish(args) {
|
|
|
458
438
|
console.log()
|
|
459
439
|
|
|
460
440
|
// Collect deck metadata
|
|
461
|
-
const title = await prompt("Title:", titleCase(
|
|
441
|
+
const title = await prompt("Title:", titleCase(deckSlug))
|
|
462
442
|
const description = await prompt("Description:", "")
|
|
463
443
|
const tagsInput = await prompt("Tags (comma-separated):", "")
|
|
464
444
|
const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
|
|
@@ -500,10 +480,10 @@ export async function publish(args) {
|
|
|
500
480
|
|
|
501
481
|
// Read lockfile for skip-if-unchanged
|
|
502
482
|
const lock = readLockfile(cwd)
|
|
503
|
-
let itemIndex = 0
|
|
504
483
|
let published = 0
|
|
505
484
|
let skipped = 0
|
|
506
485
|
let failed = 0
|
|
486
|
+
const lockfileUpdates = []
|
|
507
487
|
|
|
508
488
|
function isUnchanged(slug, fileHashes) {
|
|
509
489
|
const entry = lock.items[slug]
|
|
@@ -514,51 +494,66 @@ export async function publish(args) {
|
|
|
514
494
|
return currentKeys.every(k => entry.files[k] === fileHashes[k])
|
|
515
495
|
}
|
|
516
496
|
|
|
517
|
-
|
|
518
|
-
for (const asset of publicAssets) {
|
|
519
|
-
itemIndex++
|
|
520
|
-
const assetSlug = assetFileToSlug(deckPrefix, asset.relativePath)
|
|
521
|
-
const fileName = basename(asset.fullPath)
|
|
522
|
-
const dirPart = asset.relativePath.includes("/")
|
|
523
|
-
? "public/" + asset.relativePath.substring(0, asset.relativePath.lastIndexOf("/") + 1)
|
|
524
|
-
: "public/"
|
|
525
|
-
|
|
526
|
-
const fileData = readFileForRegistry(asset.fullPath)
|
|
527
|
-
const fileHash = fileData.binary
|
|
528
|
-
? hashContent(fileData.buffer.toString("base64"))
|
|
529
|
-
: hashContent(fileData.content)
|
|
530
|
-
const fileHashes = { [dirPart + fileName]: fileHash }
|
|
531
|
-
|
|
532
|
-
if (isUnchanged(assetSlug, fileHashes)) {
|
|
533
|
-
console.log(` [${itemIndex}/${totalItems}] ${dim("—")} asset ${dim(assetSlug)} (unchanged)`)
|
|
534
|
-
skipped++
|
|
535
|
-
continue
|
|
536
|
-
}
|
|
497
|
+
const CONCURRENCY = 6
|
|
537
498
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
} else {
|
|
544
|
-
files = [{ path: fileName, target: dirPart, content: fileData.content }]
|
|
499
|
+
async function runParallel(tasks) {
|
|
500
|
+
const results = []
|
|
501
|
+
for (let i = 0; i < tasks.length; i += CONCURRENCY) {
|
|
502
|
+
const batch = tasks.slice(i, i + CONCURRENCY)
|
|
503
|
+
results.push(...await Promise.allSettled(batch.map(fn => fn())))
|
|
545
504
|
}
|
|
505
|
+
return results
|
|
506
|
+
}
|
|
546
507
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
508
|
+
// ── Phase 1: Assets ──
|
|
509
|
+
let itemIndex = 0
|
|
510
|
+
const assetTasks = publicAssets.map((asset) => {
|
|
511
|
+
const myIndex = ++itemIndex
|
|
512
|
+
return async () => {
|
|
513
|
+
const assetSlug = assetFileToSlug(deckSlug, asset.relativePath)
|
|
514
|
+
const fileName = basename(asset.fullPath)
|
|
515
|
+
const dirPart = asset.relativePath.includes("/")
|
|
516
|
+
? "public/" + asset.relativePath.substring(0, asset.relativePath.lastIndexOf("/") + 1)
|
|
517
|
+
: "public/"
|
|
518
|
+
|
|
519
|
+
const fileData = readFileForRegistry(asset.fullPath)
|
|
520
|
+
const fileHash = fileData.binary
|
|
521
|
+
? hashContent(fileData.buffer.toString("base64"))
|
|
522
|
+
: hashContent(fileData.content)
|
|
523
|
+
const fileHashes = { [dirPart + fileName]: fileHash }
|
|
524
|
+
|
|
525
|
+
if (isUnchanged(assetSlug, fileHashes)) {
|
|
526
|
+
console.log(` [${myIndex}/${totalItems}] ${dim("—")} asset ${dim(assetSlug)} (unchanged)`)
|
|
527
|
+
skipped++
|
|
528
|
+
return
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
let files
|
|
532
|
+
if (fileData.binary) {
|
|
533
|
+
files = await uploadBinaryFiles(assetSlug, [
|
|
534
|
+
{ path: fileName, target: dirPart, binary: true, buffer: fileData.buffer, contentType: fileData.contentType, size: fileData.size }
|
|
535
|
+
], auth)
|
|
536
|
+
} else {
|
|
537
|
+
files = [{ path: fileName, target: dirPart, content: fileData.content }]
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
const result = await publishToRegistry({ type: "asset", slug: assetSlug, title: fileName, files }, auth)
|
|
542
|
+
console.log(` [${myIndex}/${totalItems}] ${green("✓")} asset ${cyan(assetSlug)} ${dim(`v${result.version}`)}`)
|
|
543
|
+
lockfileUpdates.push({ slug: assetSlug, version: result.version ?? 0, fileHashes })
|
|
544
|
+
published++
|
|
545
|
+
} catch (err) {
|
|
546
|
+
console.log(` [${myIndex}/${totalItems}] ${red("✗")} asset ${dim(assetSlug)}: ${err.message}`)
|
|
547
|
+
failed++
|
|
548
|
+
}
|
|
555
549
|
}
|
|
556
|
-
}
|
|
550
|
+
})
|
|
551
|
+
await runParallel(assetTasks)
|
|
557
552
|
|
|
558
553
|
// ── Phase 2: Theme ──
|
|
559
554
|
if (hasTheme) {
|
|
560
555
|
itemIndex++
|
|
561
|
-
const themeSlug = `${
|
|
556
|
+
const themeSlug = `${deckSlug}/theme`
|
|
562
557
|
const themePayloadFiles = []
|
|
563
558
|
const themeFileHashes = {}
|
|
564
559
|
let themeContent = ""
|
|
@@ -574,7 +569,7 @@ export async function publish(args) {
|
|
|
574
569
|
console.log(` [${itemIndex}/${totalItems}] ${dim("—")} theme ${dim(themeSlug)} (unchanged)`)
|
|
575
570
|
skipped++
|
|
576
571
|
} else {
|
|
577
|
-
const assetDeps = detectAssetDepsInContent(themeContent,
|
|
572
|
+
const assetDeps = detectAssetDepsInContent(themeContent, deckSlug, publicFileSet)
|
|
578
573
|
const npmDeps = detectNpmDeps(themeContent)
|
|
579
574
|
|
|
580
575
|
try {
|
|
@@ -588,7 +583,7 @@ export async function publish(args) {
|
|
|
588
583
|
promptslideVersion: CLI_VERSION
|
|
589
584
|
}, auth)
|
|
590
585
|
console.log(` [${itemIndex}/${totalItems}] ${green("✓")} theme ${cyan(themeSlug)} ${dim(`v${result.version}`)}`)
|
|
591
|
-
|
|
586
|
+
lockfileUpdates.push({ slug: themeSlug, version: result.version ?? 0, fileHashes: themeFileHashes })
|
|
592
587
|
published++
|
|
593
588
|
} catch (err) {
|
|
594
589
|
console.log(` [${itemIndex}/${totalItems}] ${red("✗")} theme ${dim(themeSlug)}: ${err.message}`)
|
|
@@ -597,100 +592,110 @@ export async function publish(args) {
|
|
|
597
592
|
}
|
|
598
593
|
}
|
|
599
594
|
|
|
600
|
-
// ── Phase 3: Layouts ──
|
|
601
|
-
|
|
602
|
-
itemIndex
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
595
|
+
// ── Phase 3: Layouts (parallel) ──
|
|
596
|
+
const layoutTasks = layoutEntries.map((layoutFile) => {
|
|
597
|
+
const myIndex = ++itemIndex
|
|
598
|
+
return async () => {
|
|
599
|
+
const layoutName = layoutFile.replace(/\.tsx?$/, "")
|
|
600
|
+
const layoutSlug = `${deckSlug}/${layoutName}`
|
|
601
|
+
const content = readFileSync(join(cwd, "src", "layouts", layoutFile), "utf-8")
|
|
602
|
+
const fileHashes = { [`src/layouts/${layoutFile}`]: hashContent(content) }
|
|
603
|
+
|
|
604
|
+
if (isUnchanged(layoutSlug, fileHashes)) {
|
|
605
|
+
console.log(` [${myIndex}/${totalItems}] ${dim("—")} layout ${dim(layoutSlug)} (unchanged)`)
|
|
606
|
+
skipped++
|
|
607
|
+
return
|
|
608
|
+
}
|
|
607
609
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
610
|
+
const assetDeps = detectAssetDepsInContent(content, deckSlug, publicFileSet)
|
|
611
|
+
const npmDeps = detectNpmDeps(content)
|
|
612
|
+
const regDeps = hasTheme ? [`${deckSlug}/theme`] : []
|
|
613
|
+
regDeps.push(...assetDeps)
|
|
613
614
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
published++
|
|
632
|
-
} catch (err) {
|
|
633
|
-
console.log(` [${itemIndex}/${totalItems}] ${red("✗")} layout ${dim(layoutSlug)}: ${err.message}`)
|
|
634
|
-
failed++
|
|
615
|
+
try {
|
|
616
|
+
const result = await publishToRegistry({
|
|
617
|
+
type: "layout",
|
|
618
|
+
slug: layoutSlug,
|
|
619
|
+
title: titleCase(layoutName),
|
|
620
|
+
files: [{ path: layoutFile, target: "src/layouts/", content }],
|
|
621
|
+
registryDependencies: regDeps.length ? regDeps : undefined,
|
|
622
|
+
npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined,
|
|
623
|
+
promptslideVersion: CLI_VERSION
|
|
624
|
+
}, auth)
|
|
625
|
+
console.log(` [${myIndex}/${totalItems}] ${green("✓")} layout ${cyan(layoutSlug)} ${dim(`v${result.version}`)}`)
|
|
626
|
+
lockfileUpdates.push({ slug: layoutSlug, version: result.version ?? 0, fileHashes })
|
|
627
|
+
published++
|
|
628
|
+
} catch (err) {
|
|
629
|
+
console.log(` [${myIndex}/${totalItems}] ${red("✗")} layout ${dim(layoutSlug)}: ${err.message}`)
|
|
630
|
+
failed++
|
|
631
|
+
}
|
|
635
632
|
}
|
|
636
|
-
}
|
|
633
|
+
})
|
|
634
|
+
await runParallel(layoutTasks)
|
|
635
|
+
|
|
636
|
+
// ── Phase 4: Slides (parallel, reuse single capture session) ──
|
|
637
|
+
const captureSession = canCapture ? await createCaptureSession({ cwd }) : null
|
|
638
|
+
|
|
639
|
+
const slideTasks = slideEntries.map((slideFile) => {
|
|
640
|
+
const myIndex = ++itemIndex
|
|
641
|
+
return async () => {
|
|
642
|
+
const slideName = slideFile.replace(/\.tsx?$/, "")
|
|
643
|
+
const slideSlug = `${deckSlug}/${slideName}`
|
|
644
|
+
const content = readFileSync(join(cwd, "src", "slides", slideFile), "utf-8")
|
|
645
|
+
const fileHashes = { [`src/slides/${slideFile}`]: hashContent(content) }
|
|
646
|
+
|
|
647
|
+
if (isUnchanged(slideSlug, fileHashes)) {
|
|
648
|
+
console.log(` [${myIndex}/${totalItems}] ${dim("—")} slide ${dim(slideSlug)} (unchanged)`)
|
|
649
|
+
skipped++
|
|
650
|
+
return
|
|
651
|
+
}
|
|
637
652
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const fileHashes = { [`src/slides/${slideFile}`]: hashContent(content) }
|
|
653
|
+
const assetDeps = detectAssetDepsInContent(content, deckSlug, publicFileSet)
|
|
654
|
+
const layoutDeps = detectRegistryDeps(content).map(d => `${deckSlug}/${d}`)
|
|
655
|
+
const npmDeps = detectNpmDeps(content)
|
|
656
|
+
const steps = detectSteps(content)
|
|
657
|
+
const slideConfig = deckConfig.slides.find(s => s.slug === slideName)
|
|
658
|
+
const section = slideConfig?.section || undefined
|
|
645
659
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
skipped++
|
|
649
|
-
continue
|
|
650
|
-
}
|
|
660
|
+
const regDeps = hasTheme ? [`${deckSlug}/theme`] : []
|
|
661
|
+
regDeps.push(...layoutDeps, ...assetDeps)
|
|
651
662
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
const section = slideConfig?.section || undefined
|
|
658
|
-
|
|
659
|
-
const regDeps = hasTheme ? [`${deckPrefix}/theme`] : []
|
|
660
|
-
regDeps.push(...layoutDeps, ...assetDeps)
|
|
663
|
+
// Generate preview image reusing shared session
|
|
664
|
+
let slidePreview = null
|
|
665
|
+
if (captureSession) {
|
|
666
|
+
slidePreview = await captureSession.capture(`src/slides/${slideFile}`).catch(() => null)
|
|
667
|
+
}
|
|
661
668
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
669
|
+
try {
|
|
670
|
+
const result = await publishToRegistry({
|
|
671
|
+
type: "slide",
|
|
672
|
+
slug: slideSlug,
|
|
673
|
+
title: titleCase(slideName),
|
|
674
|
+
files: [{ path: slideFile, target: "src/slides/", content }],
|
|
675
|
+
steps,
|
|
676
|
+
section,
|
|
677
|
+
registryDependencies: regDeps.length ? regDeps : undefined,
|
|
678
|
+
npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined,
|
|
679
|
+
previewImage: slidePreview || undefined,
|
|
680
|
+
promptslideVersion: CLI_VERSION
|
|
681
|
+
}, auth)
|
|
682
|
+
console.log(` [${myIndex}/${totalItems}] ${green("✓")} slide ${cyan(slideSlug)} ${dim(`v${result.version}`)}`)
|
|
683
|
+
lockfileUpdates.push({ slug: slideSlug, version: result.version ?? 0, fileHashes })
|
|
684
|
+
published++
|
|
685
|
+
} catch (err) {
|
|
686
|
+
console.log(` [${myIndex}/${totalItems}] ${red("✗")} slide ${dim(slideSlug)}: ${err.message}`)
|
|
687
|
+
failed++
|
|
688
|
+
}
|
|
666
689
|
}
|
|
690
|
+
})
|
|
691
|
+
await runParallel(slideTasks)
|
|
667
692
|
|
|
668
|
-
|
|
669
|
-
const result = await publishToRegistry({
|
|
670
|
-
type: "slide",
|
|
671
|
-
slug: slideSlug,
|
|
672
|
-
title: titleCase(slideName),
|
|
673
|
-
files: [{ path: slideFile, target: "src/slides/", content }],
|
|
674
|
-
steps,
|
|
675
|
-
section,
|
|
676
|
-
registryDependencies: regDeps.length ? regDeps : undefined,
|
|
677
|
-
npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined,
|
|
678
|
-
previewImage: slidePreview || undefined,
|
|
679
|
-
promptslideVersion: CLI_VERSION
|
|
680
|
-
}, auth)
|
|
681
|
-
console.log(` [${itemIndex}/${totalItems}] ${green("✓")} slide ${cyan(slideSlug)} ${dim(`v${result.version}`)}`)
|
|
682
|
-
updateLockfileItem(cwd, slideSlug, result.version ?? 0, fileHashes)
|
|
683
|
-
published++
|
|
684
|
-
} catch (err) {
|
|
685
|
-
console.log(` [${itemIndex}/${totalItems}] ${red("✗")} slide ${dim(slideSlug)}: ${err.message}`)
|
|
686
|
-
failed++
|
|
687
|
-
}
|
|
688
|
-
}
|
|
693
|
+
if (captureSession) await captureSession.close()
|
|
689
694
|
|
|
690
695
|
// ── Phase 5: Deck manifest ──
|
|
691
696
|
itemIndex++
|
|
692
|
-
const slideSlugs = slideEntries.map(f => `${
|
|
693
|
-
const assetSlugs = publicAssets.map(a => assetFileToSlug(
|
|
697
|
+
const slideSlugs = slideEntries.map(f => `${deckSlug}/${f.replace(/\.tsx?$/, "")}`)
|
|
698
|
+
const assetSlugs = publicAssets.map(a => assetFileToSlug(deckSlug, a.relativePath))
|
|
694
699
|
const allDeckDeps = [...slideSlugs, ...assetSlugs]
|
|
695
700
|
|
|
696
701
|
let deckItemId = null
|
|
@@ -709,8 +714,7 @@ export async function publish(args) {
|
|
|
709
714
|
promptslideVersion: CLI_VERSION
|
|
710
715
|
}, auth)
|
|
711
716
|
console.log(` [${itemIndex}/${totalItems}] ${green("✓")} deck ${cyan(deckSlug)} ${dim(`v${result.version}`)}`)
|
|
712
|
-
|
|
713
|
-
updateLockfilePublishConfig(cwd, { deckSlug })
|
|
717
|
+
lockfileUpdates.push({ slug: deckSlug, version: result.version ?? 0, fileHashes: {} })
|
|
714
718
|
deckItemId = result.id
|
|
715
719
|
published++
|
|
716
720
|
} catch (err) {
|
|
@@ -718,6 +722,18 @@ export async function publish(args) {
|
|
|
718
722
|
failed++
|
|
719
723
|
}
|
|
720
724
|
|
|
725
|
+
// Batch-write all lockfile updates at once
|
|
726
|
+
const finalLock = readLockfile(cwd)
|
|
727
|
+
for (const update of lockfileUpdates) {
|
|
728
|
+
finalLock.items[update.slug] = {
|
|
729
|
+
version: update.version,
|
|
730
|
+
installedAt: new Date().toISOString().split("T")[0],
|
|
731
|
+
files: update.fileHashes
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (deckItemId) finalLock.deckSlug = deckSlug
|
|
735
|
+
writeLockfile(cwd, finalLock)
|
|
736
|
+
|
|
721
737
|
console.log()
|
|
722
738
|
console.log(` ${bold("Done:")} ${green(`${published} published`)}${skipped ? `, ${skipped} unchanged` : ""}${failed ? `, ${red(`${failed} failed`)}` : ""}`)
|
|
723
739
|
if (auth.organizationSlug && deckItemId) {
|
|
@@ -758,12 +774,12 @@ export async function publish(args) {
|
|
|
758
774
|
}
|
|
759
775
|
console.log()
|
|
760
776
|
|
|
761
|
-
// Prompt for deck
|
|
762
|
-
const
|
|
763
|
-
const slug = `${
|
|
777
|
+
// Prompt for deck slug (required — items are namespaced under the deck)
|
|
778
|
+
const deck = await promptDeckSlug(cwd, true)
|
|
779
|
+
const slug = `${deck}/${baseSlug}`
|
|
764
780
|
|
|
765
|
-
// Persist
|
|
766
|
-
updateLockfilePublishConfig(cwd, {
|
|
781
|
+
// Persist deck slug for next time
|
|
782
|
+
updateLockfilePublishConfig(cwd, { deckSlug: deck })
|
|
767
783
|
|
|
768
784
|
console.log(` Slug: ${cyan(slug)}`)
|
|
769
785
|
console.log()
|
|
@@ -788,7 +804,7 @@ export async function publish(args) {
|
|
|
788
804
|
const missing = []
|
|
789
805
|
|
|
790
806
|
for (const depSlug of registryDeps) {
|
|
791
|
-
const prefixedDepSlug = `${
|
|
807
|
+
const prefixedDepSlug = `${deck}/${depSlug}`
|
|
792
808
|
const exists = await registryItemExists(prefixedDepSlug, auth)
|
|
793
809
|
if (!exists) {
|
|
794
810
|
missing.push(depSlug)
|
|
@@ -826,7 +842,7 @@ export async function publish(args) {
|
|
|
826
842
|
auth,
|
|
827
843
|
typeOverride: depType,
|
|
828
844
|
interactive: true,
|
|
829
|
-
|
|
845
|
+
deckSlug: deck
|
|
830
846
|
})
|
|
831
847
|
console.log()
|
|
832
848
|
const depVer = result.version ? ` ${dim(`v${result.version}`)}` : ""
|
|
@@ -881,7 +897,7 @@ export async function publish(args) {
|
|
|
881
897
|
console.log()
|
|
882
898
|
|
|
883
899
|
// Publish main item
|
|
884
|
-
const prefixedRegistryDeps = registryDeps.map(d => `${
|
|
900
|
+
const prefixedRegistryDeps = registryDeps.map(d => `${deck}/${d}`)
|
|
885
901
|
const payload = {
|
|
886
902
|
type,
|
|
887
903
|
slug,
|
package/src/utils/export.mjs
CHANGED
|
@@ -97,6 +97,74 @@ export async function captureSlideScreenshot({ cwd, slidePath, width = 1280, hei
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Create a reusable capture session that shares a single Vite server and browser
|
|
102
|
+
* instance across multiple screenshot captures.
|
|
103
|
+
*
|
|
104
|
+
* @param {{ cwd: string, width?: number, height?: number }} opts
|
|
105
|
+
* @returns {Promise<{ capture: (slidePath: string) => Promise<string | null>, close: () => Promise<void> } | null>}
|
|
106
|
+
* null if Playwright is not available
|
|
107
|
+
*/
|
|
108
|
+
export async function createCaptureSession({ cwd, width = 1280, height = 720 }) {
|
|
109
|
+
let chromium
|
|
110
|
+
try {
|
|
111
|
+
const pw = await import("playwright")
|
|
112
|
+
chromium = pw.chromium
|
|
113
|
+
} catch {
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await ensureChromium(chromium)
|
|
118
|
+
ensureTsConfig(cwd)
|
|
119
|
+
|
|
120
|
+
const config = createViteConfig({ cwd, mode: "development" })
|
|
121
|
+
const server = await createServer({
|
|
122
|
+
...config,
|
|
123
|
+
server: { port: 0, strictPort: false },
|
|
124
|
+
logLevel: "silent"
|
|
125
|
+
})
|
|
126
|
+
await server.listen()
|
|
127
|
+
|
|
128
|
+
const address = server.httpServer.address()
|
|
129
|
+
const port = typeof address === "object" ? address.port : 0
|
|
130
|
+
|
|
131
|
+
const browser = await chromium.launch({ headless: true })
|
|
132
|
+
|
|
133
|
+
async function capture(slidePath) {
|
|
134
|
+
const url = `http://localhost:${port}/?export=true&slidePath=${encodeURIComponent(slidePath)}`
|
|
135
|
+
const page = await browser.newPage({ viewport: { width, height } })
|
|
136
|
+
try {
|
|
137
|
+
const errors = []
|
|
138
|
+
page.on("pageerror", err => errors.push(err.message))
|
|
139
|
+
|
|
140
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 })
|
|
141
|
+
await page.waitForSelector("[data-export-ready='true']", { timeout: 15000 }).catch(err => {
|
|
142
|
+
if (errors.length) {
|
|
143
|
+
throw new Error(`${err.message}\n Browser errors:\n ${errors.join("\n ")}`)
|
|
144
|
+
}
|
|
145
|
+
throw err
|
|
146
|
+
})
|
|
147
|
+
await page.waitForTimeout(200)
|
|
148
|
+
|
|
149
|
+
const element = await page.$("[data-export-ready='true']")
|
|
150
|
+
const screenshot = await element.screenshot({ type: "png" })
|
|
151
|
+
return `data:image/png;base64,${screenshot.toString("base64")}`
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error(` Screenshot error: ${err.message}`)
|
|
154
|
+
return null
|
|
155
|
+
} finally {
|
|
156
|
+
await page.close().catch(() => {})
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function close() {
|
|
161
|
+
await browser.close().catch(() => {})
|
|
162
|
+
await server.close()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { capture, close }
|
|
166
|
+
}
|
|
167
|
+
|
|
100
168
|
/**
|
|
101
169
|
* Capture a slide and return as base64 data URI.
|
|
102
170
|
* Returns null if Playwright is not available or capture fails.
|
package/src/utils/registry.mjs
CHANGED
|
@@ -80,14 +80,15 @@ export function updateLockfileItem(cwd, slug, version, files) {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
|
-
* Store publish configuration (
|
|
83
|
+
* Store publish configuration (deckSlug) in the lockfile.
|
|
84
|
+
* Removes the legacy deckPrefix key if present.
|
|
84
85
|
* @param {string} cwd
|
|
85
|
-
* @param {{
|
|
86
|
+
* @param {{ deckSlug?: string }} config
|
|
86
87
|
*/
|
|
87
88
|
export function updateLockfilePublishConfig(cwd, config) {
|
|
88
89
|
const lock = readLockfile(cwd)
|
|
89
|
-
if (config.deckPrefix !== undefined) lock.deckPrefix = config.deckPrefix
|
|
90
90
|
if (config.deckSlug !== undefined) lock.deckSlug = config.deckSlug
|
|
91
|
+
delete lock.deckPrefix
|
|
91
92
|
writeLockfile(cwd, lock)
|
|
92
93
|
}
|
|
93
94
|
|