promptslide 0.3.3 → 0.3.5
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 +20 -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 +317 -198
- package/src/utils/deck-config.mjs +47 -17
- package/src/utils/export.mjs +68 -0
- package/src/utils/registry.mjs +51 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.5](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.4...promptslide-v0.3.5) (2026-03-08)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* bundle shared sources with deck publish and fix deck-config parsing ([5f7fd1d](https://github.com/prompticeu/promptslide/commit/5f7fd1de6e5fc58a0568bd87830094dd40c9eca7))
|
|
9
|
+
* persist deck and item metadata to lockfile for publish defaults ([#68](https://github.com/prompticeu/promptslide/issues/68)) ([0dc8ca3](https://github.com/prompticeu/promptslide/commit/0dc8ca3f52b86214eb42dacd9e1ead4d05928242))
|
|
10
|
+
|
|
11
|
+
## [0.3.4](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.3...promptslide-v0.3.4) (2026-03-07)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* 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))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Performance Improvements
|
|
20
|
+
|
|
21
|
+
* 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))
|
|
22
|
+
|
|
3
23
|
## [0.3.3](https://github.com/prompticeu/promptslide/compare/promptslide-v0.3.2...promptslide-v0.3.3) (2026-03-07)
|
|
4
24
|
|
|
5
25
|
|
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, readDeckMeta, updateDeckMeta, readItemMeta, updateItemMeta } 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) {
|
|
@@ -89,6 +91,57 @@ function detectRegistryDeps(content) {
|
|
|
89
91
|
return deps
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Discover shared source files under src/ that aren't slides, layouts, or theme.
|
|
96
|
+
* These are files in directories like src/components/, src/lib/, src/hooks/, etc.
|
|
97
|
+
* Returns an array of { relativePath, fullPath, target } objects.
|
|
98
|
+
*/
|
|
99
|
+
function discoverSharedSources(cwd) {
|
|
100
|
+
const srcDir = join(cwd, "src")
|
|
101
|
+
if (!existsSync(srcDir)) return []
|
|
102
|
+
|
|
103
|
+
// Top-level dirs whose top-level files are published as individual items
|
|
104
|
+
// (slides/*.tsx → slide items, layouts/*.tsx → layout items).
|
|
105
|
+
// Subdirectories within these ARE included as shared sources (e.g. slides/commercial/slide1.tsx).
|
|
106
|
+
const ITEM_DIRS = new Set(["slides", "layouts"])
|
|
107
|
+
// Top-level files already handled (theme.ts, globals.css, deck-config.ts, App.tsx)
|
|
108
|
+
const SKIP_FILES = new Set(["theme.ts", "globals.css", "deck-config.ts", "App.tsx", "vite-env.d.ts"])
|
|
109
|
+
const SOURCE_EXTS = new Set([".tsx", ".ts", ".jsx", ".js"])
|
|
110
|
+
|
|
111
|
+
const results = []
|
|
112
|
+
|
|
113
|
+
function walk(dir, relativeDir, skipTopLevelFiles) {
|
|
114
|
+
if (!existsSync(dir)) return
|
|
115
|
+
for (const entry of readdirSync(dir)) {
|
|
116
|
+
if (entry.startsWith(".")) continue
|
|
117
|
+
const full = join(dir, entry)
|
|
118
|
+
const s = statSync(full)
|
|
119
|
+
if (s.isDirectory()) {
|
|
120
|
+
// Enter slides/ and layouts/ but mark that top-level files are handled elsewhere
|
|
121
|
+
if (!relativeDir && ITEM_DIRS.has(entry)) {
|
|
122
|
+
walk(full, entry, true)
|
|
123
|
+
} else {
|
|
124
|
+
walk(full, relativeDir ? `${relativeDir}/${entry}` : entry, false)
|
|
125
|
+
}
|
|
126
|
+
} else if (s.isFile()) {
|
|
127
|
+
// Skip known top-level files in src/
|
|
128
|
+
if (!relativeDir && SKIP_FILES.has(entry)) continue
|
|
129
|
+
// Skip top-level files in slides/ and layouts/ (published as individual items)
|
|
130
|
+
if (skipTopLevelFiles) continue
|
|
131
|
+
const ext = extname(entry).toLowerCase()
|
|
132
|
+
if (!SOURCE_EXTS.has(ext)) continue
|
|
133
|
+
const relativePath = relativeDir ? `${relativeDir}/${entry}` : entry
|
|
134
|
+
// target mirrors the src/ directory structure for @/ import resolution
|
|
135
|
+
const target = relativeDir ? `src/${relativeDir}/` : "src/"
|
|
136
|
+
results.push({ relativePath, fullPath: full, fileName: entry, target })
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
walk(srcDir, "", false)
|
|
142
|
+
return results
|
|
143
|
+
}
|
|
144
|
+
|
|
92
145
|
const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".webp"])
|
|
93
146
|
const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 2MB
|
|
94
147
|
|
|
@@ -228,10 +281,10 @@ function scanForFiles(cwd) {
|
|
|
228
281
|
|
|
229
282
|
/**
|
|
230
283
|
* Publish a single item to the registry.
|
|
231
|
-
* @param {{ filePath: string, cwd: string, auth: object, typeOverride?: string, interactive?: boolean,
|
|
284
|
+
* @param {{ filePath: string, cwd: string, auth: object, typeOverride?: string, interactive?: boolean, deckSlug?: string }} opts
|
|
232
285
|
* @returns {Promise<{ slug: string, status: string }>}
|
|
233
286
|
*/
|
|
234
|
-
async function publishItem({ filePath, cwd, auth, typeOverride, interactive = true,
|
|
287
|
+
async function publishItem({ filePath, cwd, auth, typeOverride, interactive = true, deckSlug: deckSlugOverride }) {
|
|
235
288
|
const fullPath = join(cwd, filePath)
|
|
236
289
|
if (!existsSync(fullPath)) {
|
|
237
290
|
throw new Error(`File not found: ${filePath}`)
|
|
@@ -240,8 +293,8 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
|
|
|
240
293
|
const content = readFileSync(fullPath, "utf-8")
|
|
241
294
|
const fileName = basename(fullPath)
|
|
242
295
|
const baseSlug = fileName.replace(/\.tsx?$/, "")
|
|
243
|
-
const
|
|
244
|
-
const slug = `${
|
|
296
|
+
const deck = deckSlugOverride || await promptDeckSlug(cwd, interactive)
|
|
297
|
+
const slug = `${deck}/${baseSlug}`
|
|
245
298
|
|
|
246
299
|
const type = typeOverride || detectType(filePath) || "slide"
|
|
247
300
|
const steps = detectSteps(content)
|
|
@@ -252,11 +305,13 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
|
|
|
252
305
|
let title, description, tags, section, releaseNotes, previewImage
|
|
253
306
|
|
|
254
307
|
if (interactive) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
308
|
+
const storedMeta = readItemMeta(cwd, slug)
|
|
309
|
+
title = await prompt("Title:", storedMeta.title || titleCase(baseSlug))
|
|
310
|
+
description = await prompt("Description:", storedMeta.description || "")
|
|
311
|
+
const storedTagsDefault = storedMeta.tags?.length ? storedMeta.tags.join(", ") : ""
|
|
312
|
+
const tagsInput = await prompt("Tags (comma-separated):", storedTagsDefault)
|
|
258
313
|
tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
|
|
259
|
-
section = await prompt("Section:", "")
|
|
314
|
+
section = await prompt("Section:", storedMeta.section || "")
|
|
260
315
|
releaseNotes = await prompt("Release notes:", "")
|
|
261
316
|
const imagePath = await prompt("Preview image path (leave empty to auto-generate):", "")
|
|
262
317
|
if (imagePath) {
|
|
@@ -283,7 +338,7 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
|
|
|
283
338
|
previewImage = await captureSlideAsDataUri({ cwd, slidePath: filePath }).catch(() => null)
|
|
284
339
|
}
|
|
285
340
|
|
|
286
|
-
const prefixedDeps = registryDeps.map(d => `${
|
|
341
|
+
const prefixedDeps = registryDeps.map(d => `${deck}/${d}`)
|
|
287
342
|
const payload = {
|
|
288
343
|
type,
|
|
289
344
|
slug,
|
|
@@ -305,6 +360,13 @@ async function publishItem({ filePath, cwd, auth, typeOverride, interactive = tr
|
|
|
305
360
|
// Track in lockfile
|
|
306
361
|
const fileHashes = { [target + fileName]: hashContent(content) }
|
|
307
362
|
updateLockfileItem(cwd, slug, result.version ?? 0, fileHashes)
|
|
363
|
+
// Persist item metadata for next publish
|
|
364
|
+
updateItemMeta(cwd, slug, {
|
|
365
|
+
title,
|
|
366
|
+
description: description || undefined,
|
|
367
|
+
tags,
|
|
368
|
+
section: section || undefined
|
|
369
|
+
})
|
|
308
370
|
|
|
309
371
|
return { slug, status: result.status || "published", version: result.version }
|
|
310
372
|
}
|
|
@@ -378,33 +440,11 @@ export async function publish(args) {
|
|
|
378
440
|
process.exit(1)
|
|
379
441
|
}
|
|
380
442
|
|
|
381
|
-
//
|
|
382
|
-
const
|
|
383
|
-
const existingSlug = existingLock.deckSlug
|
|
384
|
-
let deckPrefix, deckSlug
|
|
385
|
-
|
|
386
|
-
const dirName = basename(cwd)
|
|
387
|
-
const deckBaseSlug = dirName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")
|
|
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
|
-
}
|
|
443
|
+
// Resolve deck slug (= deck identity, used as namespace for all items)
|
|
444
|
+
const deckSlug = await promptDeckSlug(cwd, true)
|
|
405
445
|
|
|
406
|
-
// Persist
|
|
407
|
-
updateLockfilePublishConfig(cwd, {
|
|
446
|
+
// Persist slug early so it's available even if publish fails partway
|
|
447
|
+
updateLockfilePublishConfig(cwd, { deckSlug })
|
|
408
448
|
|
|
409
449
|
// Walk public/ to collect assets and build reference set
|
|
410
450
|
const publicDir = join(cwd, "public")
|
|
@@ -443,6 +483,9 @@ export async function publish(args) {
|
|
|
443
483
|
? readdirSync(slidesDir).filter(f => f.endsWith(".tsx") || f.endsWith(".ts"))
|
|
444
484
|
: []
|
|
445
485
|
|
|
486
|
+
// Discover shared source files (src/components/, src/lib/, etc.)
|
|
487
|
+
const sharedSources = discoverSharedSources(cwd)
|
|
488
|
+
|
|
446
489
|
const totalItems = publicAssets.length + (hasTheme ? 1 : 0) + layoutEntries.length + slideEntries.length + 1
|
|
447
490
|
|
|
448
491
|
// Display summary
|
|
@@ -451,16 +494,26 @@ export async function publish(args) {
|
|
|
451
494
|
if (hasTheme) console.log(` Theme: 1`)
|
|
452
495
|
if (layoutEntries.length) console.log(` Layouts: ${layoutEntries.length}`)
|
|
453
496
|
console.log(` Slides: ${slideEntries.length}`)
|
|
497
|
+
if (sharedSources.length) console.log(` Shared: ${sharedSources.length} ${dim("(bundled with deck)")}`)
|
|
454
498
|
console.log(` Total: ${totalItems} items`)
|
|
499
|
+
// Info if there are slide files on disk not referenced in deck-config.ts
|
|
500
|
+
const deckConfigSlideCount = deckConfig.slides.length
|
|
501
|
+
if (deckConfigSlideCount !== slideEntries.length) {
|
|
502
|
+
console.log()
|
|
503
|
+
console.log(` ${dim("ℹ")} deck-config.ts has ${deckConfigSlideCount} slides, ${slideEntries.length} slide files on disk`)
|
|
504
|
+
console.log(` ${dim("All slides are published. Only deck-config slides appear in the deck preview.")}`)
|
|
505
|
+
}
|
|
455
506
|
if (deckConfig.transition) {
|
|
456
507
|
console.log(` Transition: ${deckConfig.transition}${deckConfig.directionalTransition ? " (directional)" : ""}`)
|
|
457
508
|
}
|
|
458
509
|
console.log()
|
|
459
510
|
|
|
460
|
-
// Collect deck metadata
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
const
|
|
511
|
+
// Collect deck metadata (use stored values as defaults)
|
|
512
|
+
const storedDeckMeta = readDeckMeta(cwd)
|
|
513
|
+
const title = await prompt("Title:", storedDeckMeta.title || titleCase(deckSlug))
|
|
514
|
+
const description = await prompt("Description:", storedDeckMeta.description || "")
|
|
515
|
+
const storedTagsDefault = storedDeckMeta.tags?.length ? storedDeckMeta.tags.join(", ") : ""
|
|
516
|
+
const tagsInput = await prompt("Tags (comma-separated):", storedTagsDefault)
|
|
464
517
|
const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
|
|
465
518
|
const releaseNotes = await prompt("Release notes:", "")
|
|
466
519
|
|
|
@@ -500,10 +553,10 @@ export async function publish(args) {
|
|
|
500
553
|
|
|
501
554
|
// Read lockfile for skip-if-unchanged
|
|
502
555
|
const lock = readLockfile(cwd)
|
|
503
|
-
let itemIndex = 0
|
|
504
556
|
let published = 0
|
|
505
557
|
let skipped = 0
|
|
506
558
|
let failed = 0
|
|
559
|
+
const lockfileUpdates = []
|
|
507
560
|
|
|
508
561
|
function isUnchanged(slug, fileHashes) {
|
|
509
562
|
const entry = lock.items[slug]
|
|
@@ -514,51 +567,66 @@ export async function publish(args) {
|
|
|
514
567
|
return currentKeys.every(k => entry.files[k] === fileHashes[k])
|
|
515
568
|
}
|
|
516
569
|
|
|
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
|
-
}
|
|
570
|
+
const CONCURRENCY = 6
|
|
537
571
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
} else {
|
|
544
|
-
files = [{ path: fileName, target: dirPart, content: fileData.content }]
|
|
572
|
+
async function runParallel(tasks) {
|
|
573
|
+
const results = []
|
|
574
|
+
for (let i = 0; i < tasks.length; i += CONCURRENCY) {
|
|
575
|
+
const batch = tasks.slice(i, i + CONCURRENCY)
|
|
576
|
+
results.push(...await Promise.allSettled(batch.map(fn => fn())))
|
|
545
577
|
}
|
|
578
|
+
return results
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ── Phase 1: Assets ──
|
|
582
|
+
let itemIndex = 0
|
|
583
|
+
const assetTasks = publicAssets.map((asset) => {
|
|
584
|
+
const myIndex = ++itemIndex
|
|
585
|
+
return async () => {
|
|
586
|
+
const assetSlug = assetFileToSlug(deckSlug, asset.relativePath)
|
|
587
|
+
const fileName = basename(asset.fullPath)
|
|
588
|
+
const dirPart = asset.relativePath.includes("/")
|
|
589
|
+
? "public/" + asset.relativePath.substring(0, asset.relativePath.lastIndexOf("/") + 1)
|
|
590
|
+
: "public/"
|
|
591
|
+
|
|
592
|
+
const fileData = readFileForRegistry(asset.fullPath)
|
|
593
|
+
const fileHash = fileData.binary
|
|
594
|
+
? hashContent(fileData.buffer.toString("base64"))
|
|
595
|
+
: hashContent(fileData.content)
|
|
596
|
+
const fileHashes = { [dirPart + fileName]: fileHash }
|
|
597
|
+
|
|
598
|
+
if (isUnchanged(assetSlug, fileHashes)) {
|
|
599
|
+
console.log(` [${myIndex}/${totalItems}] ${dim("—")} asset ${dim(assetSlug)} (unchanged)`)
|
|
600
|
+
skipped++
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
let files
|
|
605
|
+
if (fileData.binary) {
|
|
606
|
+
files = await uploadBinaryFiles(assetSlug, [
|
|
607
|
+
{ path: fileName, target: dirPart, binary: true, buffer: fileData.buffer, contentType: fileData.contentType, size: fileData.size }
|
|
608
|
+
], auth)
|
|
609
|
+
} else {
|
|
610
|
+
files = [{ path: fileName, target: dirPart, content: fileData.content }]
|
|
611
|
+
}
|
|
546
612
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
613
|
+
try {
|
|
614
|
+
const result = await publishToRegistry({ type: "asset", slug: assetSlug, title: fileName, files }, auth)
|
|
615
|
+
console.log(` [${myIndex}/${totalItems}] ${green("✓")} asset ${cyan(assetSlug)} ${dim(`v${result.version}`)}`)
|
|
616
|
+
lockfileUpdates.push({ slug: assetSlug, version: result.version ?? 0, fileHashes })
|
|
617
|
+
published++
|
|
618
|
+
} catch (err) {
|
|
619
|
+
console.log(` [${myIndex}/${totalItems}] ${red("✗")} asset ${dim(assetSlug)}: ${err.message}`)
|
|
620
|
+
failed++
|
|
621
|
+
}
|
|
555
622
|
}
|
|
556
|
-
}
|
|
623
|
+
})
|
|
624
|
+
await runParallel(assetTasks)
|
|
557
625
|
|
|
558
626
|
// ── Phase 2: Theme ──
|
|
559
627
|
if (hasTheme) {
|
|
560
628
|
itemIndex++
|
|
561
|
-
const themeSlug = `${
|
|
629
|
+
const themeSlug = `${deckSlug}/theme`
|
|
562
630
|
const themePayloadFiles = []
|
|
563
631
|
const themeFileHashes = {}
|
|
564
632
|
let themeContent = ""
|
|
@@ -574,7 +642,7 @@ export async function publish(args) {
|
|
|
574
642
|
console.log(` [${itemIndex}/${totalItems}] ${dim("—")} theme ${dim(themeSlug)} (unchanged)`)
|
|
575
643
|
skipped++
|
|
576
644
|
} else {
|
|
577
|
-
const assetDeps = detectAssetDepsInContent(themeContent,
|
|
645
|
+
const assetDeps = detectAssetDepsInContent(themeContent, deckSlug, publicFileSet)
|
|
578
646
|
const npmDeps = detectNpmDeps(themeContent)
|
|
579
647
|
|
|
580
648
|
try {
|
|
@@ -588,7 +656,7 @@ export async function publish(args) {
|
|
|
588
656
|
promptslideVersion: CLI_VERSION
|
|
589
657
|
}, auth)
|
|
590
658
|
console.log(` [${itemIndex}/${totalItems}] ${green("✓")} theme ${cyan(themeSlug)} ${dim(`v${result.version}`)}`)
|
|
591
|
-
|
|
659
|
+
lockfileUpdates.push({ slug: themeSlug, version: result.version ?? 0, fileHashes: themeFileHashes })
|
|
592
660
|
published++
|
|
593
661
|
} catch (err) {
|
|
594
662
|
console.log(` [${itemIndex}/${totalItems}] ${red("✗")} theme ${dim(themeSlug)}: ${err.message}`)
|
|
@@ -597,102 +665,128 @@ export async function publish(args) {
|
|
|
597
665
|
}
|
|
598
666
|
}
|
|
599
667
|
|
|
600
|
-
// ── Phase 3: Layouts ──
|
|
601
|
-
|
|
602
|
-
itemIndex
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
668
|
+
// ── Phase 3: Layouts (parallel) ──
|
|
669
|
+
const layoutTasks = layoutEntries.map((layoutFile) => {
|
|
670
|
+
const myIndex = ++itemIndex
|
|
671
|
+
return async () => {
|
|
672
|
+
const layoutName = layoutFile.replace(/\.tsx?$/, "")
|
|
673
|
+
const layoutSlug = `${deckSlug}/${layoutName}`
|
|
674
|
+
const content = readFileSync(join(cwd, "src", "layouts", layoutFile), "utf-8")
|
|
675
|
+
const fileHashes = { [`src/layouts/${layoutFile}`]: hashContent(content) }
|
|
676
|
+
|
|
677
|
+
if (isUnchanged(layoutSlug, fileHashes)) {
|
|
678
|
+
console.log(` [${myIndex}/${totalItems}] ${dim("—")} layout ${dim(layoutSlug)} (unchanged)`)
|
|
679
|
+
skipped++
|
|
680
|
+
return
|
|
681
|
+
}
|
|
607
682
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
683
|
+
const assetDeps = detectAssetDepsInContent(content, deckSlug, publicFileSet)
|
|
684
|
+
const npmDeps = detectNpmDeps(content)
|
|
685
|
+
const regDeps = hasTheme ? [`${deckSlug}/theme`] : []
|
|
686
|
+
regDeps.push(...assetDeps)
|
|
613
687
|
|
|
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++
|
|
688
|
+
try {
|
|
689
|
+
const result = await publishToRegistry({
|
|
690
|
+
type: "layout",
|
|
691
|
+
slug: layoutSlug,
|
|
692
|
+
title: titleCase(layoutName),
|
|
693
|
+
files: [{ path: layoutFile, target: "src/layouts/", content }],
|
|
694
|
+
registryDependencies: regDeps.length ? regDeps : undefined,
|
|
695
|
+
npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined,
|
|
696
|
+
promptslideVersion: CLI_VERSION
|
|
697
|
+
}, auth)
|
|
698
|
+
console.log(` [${myIndex}/${totalItems}] ${green("✓")} layout ${cyan(layoutSlug)} ${dim(`v${result.version}`)}`)
|
|
699
|
+
lockfileUpdates.push({ slug: layoutSlug, version: result.version ?? 0, fileHashes })
|
|
700
|
+
published++
|
|
701
|
+
} catch (err) {
|
|
702
|
+
console.log(` [${myIndex}/${totalItems}] ${red("✗")} layout ${dim(layoutSlug)}: ${err.message}`)
|
|
703
|
+
failed++
|
|
704
|
+
}
|
|
635
705
|
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
const
|
|
644
|
-
|
|
706
|
+
})
|
|
707
|
+
await runParallel(layoutTasks)
|
|
708
|
+
|
|
709
|
+
// ── Phase 4: Slides (parallel, reuse single capture session) ──
|
|
710
|
+
const captureSession = canCapture ? await createCaptureSession({ cwd }) : null
|
|
711
|
+
|
|
712
|
+
const slideTasks = slideEntries.map((slideFile) => {
|
|
713
|
+
const myIndex = ++itemIndex
|
|
714
|
+
return async () => {
|
|
715
|
+
const slideName = slideFile.replace(/\.tsx?$/, "")
|
|
716
|
+
const slideSlug = `${deckSlug}/${slideName}`
|
|
717
|
+
const content = readFileSync(join(cwd, "src", "slides", slideFile), "utf-8")
|
|
718
|
+
const fileHashes = { [`src/slides/${slideFile}`]: hashContent(content) }
|
|
719
|
+
|
|
720
|
+
if (isUnchanged(slideSlug, fileHashes)) {
|
|
721
|
+
console.log(` [${myIndex}/${totalItems}] ${dim("—")} slide ${dim(slideSlug)} (unchanged)`)
|
|
722
|
+
skipped++
|
|
723
|
+
return
|
|
724
|
+
}
|
|
645
725
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
726
|
+
const assetDeps = detectAssetDepsInContent(content, deckSlug, publicFileSet)
|
|
727
|
+
const layoutDeps = detectRegistryDeps(content).map(d => `${deckSlug}/${d}`)
|
|
728
|
+
const npmDeps = detectNpmDeps(content)
|
|
729
|
+
const steps = detectSteps(content)
|
|
730
|
+
const slideConfig = deckConfig.slides.find(s => s.slug === slideName)
|
|
731
|
+
const section = slideConfig?.section || undefined
|
|
651
732
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
const npmDeps = detectNpmDeps(content)
|
|
655
|
-
const steps = detectSteps(content)
|
|
656
|
-
const slideConfig = deckConfig.slides.find(s => s.slug === slideName)
|
|
657
|
-
const section = slideConfig?.section || undefined
|
|
733
|
+
const regDeps = hasTheme ? [`${deckSlug}/theme`] : []
|
|
734
|
+
regDeps.push(...layoutDeps, ...assetDeps)
|
|
658
735
|
|
|
659
|
-
|
|
660
|
-
|
|
736
|
+
// Generate preview image reusing shared session
|
|
737
|
+
let slidePreview = null
|
|
738
|
+
if (captureSession) {
|
|
739
|
+
slidePreview = await captureSession.capture(`src/slides/${slideFile}`).catch(() => null)
|
|
740
|
+
}
|
|
661
741
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
742
|
+
try {
|
|
743
|
+
const result = await publishToRegistry({
|
|
744
|
+
type: "slide",
|
|
745
|
+
slug: slideSlug,
|
|
746
|
+
title: titleCase(slideName),
|
|
747
|
+
files: [{ path: slideFile, target: "src/slides/", content }],
|
|
748
|
+
steps,
|
|
749
|
+
section,
|
|
750
|
+
registryDependencies: regDeps.length ? regDeps : undefined,
|
|
751
|
+
npmDependencies: Object.keys(npmDeps).length ? npmDeps : undefined,
|
|
752
|
+
previewImage: slidePreview || undefined,
|
|
753
|
+
promptslideVersion: CLI_VERSION
|
|
754
|
+
}, auth)
|
|
755
|
+
console.log(` [${myIndex}/${totalItems}] ${green("✓")} slide ${cyan(slideSlug)} ${dim(`v${result.version}`)}`)
|
|
756
|
+
lockfileUpdates.push({ slug: slideSlug, version: result.version ?? 0, fileHashes })
|
|
757
|
+
published++
|
|
758
|
+
} catch (err) {
|
|
759
|
+
console.log(` [${myIndex}/${totalItems}] ${red("✗")} slide ${dim(slideSlug)}: ${err.message}`)
|
|
760
|
+
failed++
|
|
761
|
+
}
|
|
666
762
|
}
|
|
763
|
+
})
|
|
764
|
+
await runParallel(slideTasks)
|
|
667
765
|
|
|
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
|
-
}
|
|
766
|
+
if (captureSession) await captureSession.close()
|
|
689
767
|
|
|
690
|
-
// ── Phase 5: Deck manifest ──
|
|
768
|
+
// ── Phase 5: Deck manifest (includes shared source modules) ──
|
|
691
769
|
itemIndex++
|
|
692
|
-
const slideSlugs = slideEntries.map(f => `${
|
|
693
|
-
const assetSlugs = publicAssets.map(a => assetFileToSlug(
|
|
770
|
+
const slideSlugs = slideEntries.map(f => `${deckSlug}/${f.replace(/\.tsx?$/, "")}`)
|
|
771
|
+
const assetSlugs = publicAssets.map(a => assetFileToSlug(deckSlug, a.relativePath))
|
|
694
772
|
const allDeckDeps = [...slideSlugs, ...assetSlugs]
|
|
695
773
|
|
|
774
|
+
// Bundle shared source files with the deck item so preview/install can resolve @/ imports
|
|
775
|
+
const sharedFiles = sharedSources.map(s => ({
|
|
776
|
+
path: s.fileName,
|
|
777
|
+
target: s.target,
|
|
778
|
+
content: readFileSync(s.fullPath, "utf-8")
|
|
779
|
+
}))
|
|
780
|
+
if (sharedFiles.length) {
|
|
781
|
+
console.log(` ${dim(`Bundling ${sharedFiles.length} shared source file(s) with deck`)}`)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Collect npm deps from shared source files
|
|
785
|
+
const sharedNpmDeps = {}
|
|
786
|
+
for (const f of sharedFiles) {
|
|
787
|
+
Object.assign(sharedNpmDeps, detectNpmDeps(f.content))
|
|
788
|
+
}
|
|
789
|
+
|
|
696
790
|
let deckItemId = null
|
|
697
791
|
try {
|
|
698
792
|
const result = await publishToRegistry({
|
|
@@ -702,22 +796,38 @@ export async function publish(args) {
|
|
|
702
796
|
description: description || undefined,
|
|
703
797
|
tags,
|
|
704
798
|
deckConfig,
|
|
705
|
-
files:
|
|
799
|
+
files: sharedFiles,
|
|
800
|
+
npmDependencies: Object.keys(sharedNpmDeps).length ? sharedNpmDeps : undefined,
|
|
706
801
|
registryDependencies: allDeckDeps.length ? allDeckDeps : undefined,
|
|
707
802
|
releaseNotes: releaseNotes || undefined,
|
|
708
803
|
previewImage: previewImage || undefined,
|
|
709
804
|
promptslideVersion: CLI_VERSION
|
|
710
805
|
}, auth)
|
|
711
806
|
console.log(` [${itemIndex}/${totalItems}] ${green("✓")} deck ${cyan(deckSlug)} ${dim(`v${result.version}`)}`)
|
|
712
|
-
|
|
713
|
-
updateLockfilePublishConfig(cwd, { deckSlug })
|
|
807
|
+
lockfileUpdates.push({ slug: deckSlug, version: result.version ?? 0, fileHashes: {} })
|
|
714
808
|
deckItemId = result.id
|
|
715
809
|
published++
|
|
810
|
+
// Persist deck metadata for next publish
|
|
811
|
+
updateDeckMeta(cwd, { title, description: description || undefined, tags })
|
|
716
812
|
} catch (err) {
|
|
717
813
|
console.log(` [${itemIndex}/${totalItems}] ${red("✗")} deck ${dim(deckSlug)}: ${err.message}`)
|
|
718
814
|
failed++
|
|
719
815
|
}
|
|
720
816
|
|
|
817
|
+
// Batch-write all lockfile updates at once
|
|
818
|
+
const finalLock = readLockfile(cwd)
|
|
819
|
+
for (const update of lockfileUpdates) {
|
|
820
|
+
const existing = finalLock.items[update.slug] || {}
|
|
821
|
+
finalLock.items[update.slug] = {
|
|
822
|
+
...existing,
|
|
823
|
+
version: update.version,
|
|
824
|
+
installedAt: new Date().toISOString().split("T")[0],
|
|
825
|
+
files: update.fileHashes
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (deckItemId) finalLock.deckSlug = deckSlug
|
|
829
|
+
writeLockfile(cwd, finalLock)
|
|
830
|
+
|
|
721
831
|
console.log()
|
|
722
832
|
console.log(` ${bold("Done:")} ${green(`${published} published`)}${skipped ? `, ${skipped} unchanged` : ""}${failed ? `, ${red(`${failed} failed`)}` : ""}`)
|
|
723
833
|
if (auth.organizationSlug && deckItemId) {
|
|
@@ -758,12 +868,12 @@ export async function publish(args) {
|
|
|
758
868
|
}
|
|
759
869
|
console.log()
|
|
760
870
|
|
|
761
|
-
// Prompt for deck
|
|
762
|
-
const
|
|
763
|
-
const slug = `${
|
|
871
|
+
// Prompt for deck slug (required — items are namespaced under the deck)
|
|
872
|
+
const deck = await promptDeckSlug(cwd, true)
|
|
873
|
+
const slug = `${deck}/${baseSlug}`
|
|
764
874
|
|
|
765
|
-
// Persist
|
|
766
|
-
updateLockfilePublishConfig(cwd, {
|
|
875
|
+
// Persist deck slug for next time
|
|
876
|
+
updateLockfilePublishConfig(cwd, { deckSlug: deck })
|
|
767
877
|
|
|
768
878
|
console.log(` Slug: ${cyan(slug)}`)
|
|
769
879
|
console.log()
|
|
@@ -788,7 +898,7 @@ export async function publish(args) {
|
|
|
788
898
|
const missing = []
|
|
789
899
|
|
|
790
900
|
for (const depSlug of registryDeps) {
|
|
791
|
-
const prefixedDepSlug = `${
|
|
901
|
+
const prefixedDepSlug = `${deck}/${depSlug}`
|
|
792
902
|
const exists = await registryItemExists(prefixedDepSlug, auth)
|
|
793
903
|
if (!exists) {
|
|
794
904
|
missing.push(depSlug)
|
|
@@ -826,7 +936,7 @@ export async function publish(args) {
|
|
|
826
936
|
auth,
|
|
827
937
|
typeOverride: depType,
|
|
828
938
|
interactive: true,
|
|
829
|
-
|
|
939
|
+
deckSlug: deck
|
|
830
940
|
})
|
|
831
941
|
console.log()
|
|
832
942
|
const depVer = result.version ? ` ${dim(`v${result.version}`)}` : ""
|
|
@@ -848,12 +958,14 @@ export async function publish(args) {
|
|
|
848
958
|
}
|
|
849
959
|
}
|
|
850
960
|
|
|
851
|
-
// Collect metadata for main item
|
|
852
|
-
const
|
|
853
|
-
const
|
|
854
|
-
const
|
|
961
|
+
// Collect metadata for main item (use stored values as defaults)
|
|
962
|
+
const storedMeta = readItemMeta(cwd, slug)
|
|
963
|
+
const title = await prompt("Title:", storedMeta.title || titleCase(baseSlug))
|
|
964
|
+
const description = await prompt("Description:", storedMeta.description || "")
|
|
965
|
+
const storedTagsDefault = storedMeta.tags?.length ? storedMeta.tags.join(", ") : ""
|
|
966
|
+
const tagsInput = await prompt("Tags (comma-separated):", storedTagsDefault)
|
|
855
967
|
const tags = tagsInput ? tagsInput.split(",").map(t => t.trim()).filter(Boolean) : []
|
|
856
|
-
const section = await prompt("Section:", "")
|
|
968
|
+
const section = await prompt("Section:", storedMeta.section || "")
|
|
857
969
|
const releaseNotes = await prompt("Release notes:", "")
|
|
858
970
|
const previewImagePath = await prompt("Preview image path (leave empty to auto-generate):", "")
|
|
859
971
|
let previewImage = null
|
|
@@ -881,7 +993,7 @@ export async function publish(args) {
|
|
|
881
993
|
console.log()
|
|
882
994
|
|
|
883
995
|
// Publish main item
|
|
884
|
-
const prefixedRegistryDeps = registryDeps.map(d => `${
|
|
996
|
+
const prefixedRegistryDeps = registryDeps.map(d => `${deck}/${d}`)
|
|
885
997
|
const payload = {
|
|
886
998
|
type,
|
|
887
999
|
slug,
|
|
@@ -911,6 +1023,13 @@ export async function publish(args) {
|
|
|
911
1023
|
// Track in lockfile
|
|
912
1024
|
const fileHashes = { [target + fileName]: hashContent(content) }
|
|
913
1025
|
updateLockfileItem(cwd, slug, result.version ?? 0, fileHashes)
|
|
1026
|
+
// Persist item metadata for next publish
|
|
1027
|
+
updateItemMeta(cwd, slug, {
|
|
1028
|
+
title,
|
|
1029
|
+
description: description || undefined,
|
|
1030
|
+
tags,
|
|
1031
|
+
section: section || undefined
|
|
1032
|
+
})
|
|
914
1033
|
} catch (err) {
|
|
915
1034
|
console.error(` ${red("Error:")} ${err.message}`)
|
|
916
1035
|
process.exit(1)
|
|
@@ -6,13 +6,16 @@ const DECK_CONFIG_PATH = "src/deck-config.ts"
|
|
|
6
6
|
/**
|
|
7
7
|
* Convert a kebab-case filename to PascalCase component name.
|
|
8
8
|
* e.g. "slide-hero-gradient" → "SlideHeroGradient"
|
|
9
|
+
* e.g. "01-title" → "Slide01Title" (prefixed to ensure valid JS identifier)
|
|
9
10
|
*/
|
|
10
11
|
export function toPascalCase(kebab) {
|
|
11
|
-
|
|
12
|
+
const raw = kebab
|
|
12
13
|
.replace(/\.tsx?$/, "")
|
|
13
14
|
.split("-")
|
|
14
15
|
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
15
16
|
.join("")
|
|
17
|
+
// JS identifiers cannot start with a digit — prefix with "Slide"
|
|
18
|
+
return /^\d/.test(raw) ? `Slide${raw}` : raw
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/**
|
|
@@ -222,11 +225,20 @@ export function parseDeckConfig(cwd) {
|
|
|
222
225
|
const content = readFileSync(configPath, "utf-8")
|
|
223
226
|
|
|
224
227
|
// 1. Parse imports to build componentName -> slug map
|
|
225
|
-
//
|
|
228
|
+
// Handles: import { SlideTitle } from "@/slides/slide-title"
|
|
229
|
+
// Handles: import { default as SlideTitle } from "@/slides/slide-title"
|
|
230
|
+
// Handles: import { SlideTitle as Alias } from "@/slides/slide-title"
|
|
226
231
|
const componentToSlug = {}
|
|
227
|
-
const importRegex = /import\s*\{
|
|
232
|
+
const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']@\/slides\/([^"']+)["']/g
|
|
228
233
|
for (const match of content.matchAll(importRegex)) {
|
|
229
|
-
|
|
234
|
+
const importClause = match[1].trim()
|
|
235
|
+
const slug = match[2].replace(/\.tsx?$/, "")
|
|
236
|
+
// Handle "X", "X as Y" (use Y), "default as Y" (use Y)
|
|
237
|
+
const asMatch = importClause.match(/(?:\w+\s+)?as\s+(\w+)/)
|
|
238
|
+
const componentName = asMatch ? asMatch[1] : importClause.match(/(\w+)/)?.[1]
|
|
239
|
+
if (componentName) {
|
|
240
|
+
componentToSlug[componentName] = slug
|
|
241
|
+
}
|
|
230
242
|
}
|
|
231
243
|
|
|
232
244
|
// 2. Parse transition exports
|
|
@@ -238,20 +250,38 @@ export function parseDeckConfig(cwd) {
|
|
|
238
250
|
const dirMatch = content.match(/export\s+const\s+directionalTransition\s*=\s*(true|false)/)
|
|
239
251
|
if (dirMatch) directionalTransition = dirMatch[1] === "true"
|
|
240
252
|
|
|
241
|
-
// 3.
|
|
253
|
+
// 3. Extract slides array content, then parse entries within it
|
|
242
254
|
const slides = []
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
+
const slidesArrayMatch = content.match(/export\s+const\s+slides\s*:\s*SlideConfig\[\]\s*=\s*\[/)
|
|
256
|
+
if (slidesArrayMatch) {
|
|
257
|
+
const arrayStartIdx = content.indexOf(slidesArrayMatch[0]) + slidesArrayMatch[0].length
|
|
258
|
+
// Find matching closing bracket
|
|
259
|
+
let bracketDepth = 1
|
|
260
|
+
let arrayEndIdx = arrayStartIdx
|
|
261
|
+
for (let i = arrayStartIdx; i < content.length; i++) {
|
|
262
|
+
if (content[i] === "[") bracketDepth++
|
|
263
|
+
if (content[i] === "]") bracketDepth--
|
|
264
|
+
if (bracketDepth === 0) {
|
|
265
|
+
arrayEndIdx = i
|
|
266
|
+
break
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const arrayContent = content.slice(arrayStartIdx, arrayEndIdx)
|
|
270
|
+
|
|
271
|
+
// Match entries within the slides array only
|
|
272
|
+
const entryRegex = /\{([^}]+)\}/g
|
|
273
|
+
for (const match of arrayContent.matchAll(entryRegex)) {
|
|
274
|
+
const body = match[1]
|
|
275
|
+
const componentMatch = body.match(/component:\s*(\w+)/)
|
|
276
|
+
if (!componentMatch) continue
|
|
277
|
+
const slug = componentToSlug[componentMatch[1]]
|
|
278
|
+
if (!slug) continue // layout or unknown import — skip
|
|
279
|
+
const stepsMatch = body.match(/steps:\s*(\d+)/)
|
|
280
|
+
const entry = { slug, steps: stepsMatch ? parseInt(stepsMatch[1], 10) : 0 }
|
|
281
|
+
const sectionMatch = body.match(/section:\s*["']([^"']+)["']/)
|
|
282
|
+
if (sectionMatch) entry.section = sectionMatch[1]
|
|
283
|
+
slides.push(entry)
|
|
284
|
+
}
|
|
255
285
|
}
|
|
256
286
|
|
|
257
287
|
if (slides.length === 0) return null
|
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
|
@@ -71,7 +71,9 @@ export function isFileDirty(cwd, relativePath, storedHash) {
|
|
|
71
71
|
*/
|
|
72
72
|
export function updateLockfileItem(cwd, slug, version, files) {
|
|
73
73
|
const lock = readLockfile(cwd)
|
|
74
|
+
const existing = lock.items[slug] || {}
|
|
74
75
|
lock.items[slug] = {
|
|
76
|
+
...existing,
|
|
75
77
|
version,
|
|
76
78
|
installedAt: new Date().toISOString().split("T")[0],
|
|
77
79
|
files
|
|
@@ -80,14 +82,60 @@ export function updateLockfileItem(cwd, slug, version, files) {
|
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
/**
|
|
83
|
-
* Store publish configuration (
|
|
85
|
+
* Store publish configuration (deckSlug) in the lockfile.
|
|
86
|
+
* Removes the legacy deckPrefix key if present.
|
|
84
87
|
* @param {string} cwd
|
|
85
|
-
* @param {{
|
|
88
|
+
* @param {{ deckSlug?: string }} config
|
|
86
89
|
*/
|
|
87
90
|
export function updateLockfilePublishConfig(cwd, config) {
|
|
88
91
|
const lock = readLockfile(cwd)
|
|
89
|
-
if (config.deckPrefix !== undefined) lock.deckPrefix = config.deckPrefix
|
|
90
92
|
if (config.deckSlug !== undefined) lock.deckSlug = config.deckSlug
|
|
93
|
+
delete lock.deckPrefix
|
|
94
|
+
writeLockfile(cwd, lock)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read stored deck-level publish metadata (title, description, tags).
|
|
99
|
+
* @param {string} cwd
|
|
100
|
+
* @returns {{ title?: string, description?: string, tags?: string[] }}
|
|
101
|
+
*/
|
|
102
|
+
export function readDeckMeta(cwd) {
|
|
103
|
+
const lock = readLockfile(cwd)
|
|
104
|
+
return lock.deckMeta || {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Persist deck-level publish metadata so it can be reused as defaults.
|
|
109
|
+
* @param {string} cwd
|
|
110
|
+
* @param {{ title?: string, description?: string, tags?: string[] }} meta
|
|
111
|
+
*/
|
|
112
|
+
export function updateDeckMeta(cwd, meta) {
|
|
113
|
+
const lock = readLockfile(cwd)
|
|
114
|
+
lock.deckMeta = { ...lock.deckMeta, ...meta }
|
|
115
|
+
writeLockfile(cwd, lock)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read stored per-item publish metadata (title, description, tags, section).
|
|
120
|
+
* @param {string} cwd
|
|
121
|
+
* @param {string} slug
|
|
122
|
+
* @returns {{ title?: string, description?: string, tags?: string[], section?: string }}
|
|
123
|
+
*/
|
|
124
|
+
export function readItemMeta(cwd, slug) {
|
|
125
|
+
const lock = readLockfile(cwd)
|
|
126
|
+
return lock.items[slug]?.meta || {}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Persist per-item publish metadata alongside the version/files entry.
|
|
131
|
+
* @param {string} cwd
|
|
132
|
+
* @param {string} slug
|
|
133
|
+
* @param {{ title?: string, description?: string, tags?: string[], section?: string }} meta
|
|
134
|
+
*/
|
|
135
|
+
export function updateItemMeta(cwd, slug, meta) {
|
|
136
|
+
const lock = readLockfile(cwd)
|
|
137
|
+
if (!lock.items[slug]) lock.items[slug] = {}
|
|
138
|
+
lock.items[slug].meta = meta
|
|
91
139
|
writeLockfile(cwd, lock)
|
|
92
140
|
}
|
|
93
141
|
|