promptslide 0.2.0 → 0.2.2
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/dist/index.js +40 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/commands/add.mjs +182 -0
- package/src/commands/build.mjs +1 -1
- package/src/commands/create.mjs +44 -28
- package/src/commands/info.mjs +79 -0
- package/src/commands/list.mjs +37 -0
- package/src/commands/login.mjs +176 -0
- package/src/commands/logout.mjs +17 -0
- package/src/commands/org.mjs +68 -0
- package/src/commands/publish.mjs +384 -0
- package/src/commands/remove.mjs +111 -0
- package/src/commands/search.mjs +57 -0
- package/src/commands/to-image.mjs +52 -0
- package/src/commands/update.mjs +218 -0
- package/src/core/slide-deck.tsx +50 -0
- package/src/index.mjs +70 -0
- package/src/utils/ansi.mjs +1 -0
- package/src/utils/auth.mjs +66 -0
- package/src/utils/colors.mjs +0 -7
- package/src/utils/deck-config.mjs +208 -0
- package/src/utils/export.mjs +115 -0
- package/src/utils/registry.mjs +319 -0
- package/src/vite/config.mjs +1 -1
- package/src/vite/plugin.mjs +92 -11
- package/templates/default/AGENTS.md +42 -408
- package/templates/default/src/globals.css +5 -43
- package/templates/default/src/slides/slide-title.tsx +0 -5
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execSync } from "node:child_process"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
|
|
5
|
+
import { createServer } from "vite"
|
|
6
|
+
|
|
7
|
+
import { ensureTsConfig } from "./tsconfig.mjs"
|
|
8
|
+
import { createViteConfig } from "../vite/config.mjs"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if Playwright is available.
|
|
12
|
+
* @returns {Promise<boolean>}
|
|
13
|
+
*/
|
|
14
|
+
export async function isPlaywrightAvailable() {
|
|
15
|
+
try {
|
|
16
|
+
await import("playwright")
|
|
17
|
+
return true
|
|
18
|
+
} catch {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ensure the Chromium browser binary is installed.
|
|
25
|
+
* Attempts a launch and auto-installs if the binary is missing.
|
|
26
|
+
* @param {import("playwright").BrowserType} chromium
|
|
27
|
+
*/
|
|
28
|
+
async function ensureChromium(chromium) {
|
|
29
|
+
try {
|
|
30
|
+
const browser = await chromium.launch({ headless: true })
|
|
31
|
+
await browser.close()
|
|
32
|
+
} catch (err) {
|
|
33
|
+
if (err.message && err.message.includes("Executable doesn't exist")) {
|
|
34
|
+
const pwIndex = fileURLToPath(import.meta.resolve("playwright"))
|
|
35
|
+
const cliPath = join(dirname(pwIndex), "cli.js")
|
|
36
|
+
execSync(`node "${cliPath}" install chromium`, { stdio: "inherit" })
|
|
37
|
+
} else {
|
|
38
|
+
throw err
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Capture a screenshot of a specific slide.
|
|
45
|
+
* @param {{ cwd: string, slidePath: string, width?: number, height?: number }} opts
|
|
46
|
+
* @returns {Promise<Buffer | null>} PNG buffer, or null if Playwright is not installed
|
|
47
|
+
*/
|
|
48
|
+
export async function captureSlideScreenshot({ cwd, slidePath, width = 1280, height = 720 }) {
|
|
49
|
+
let chromium
|
|
50
|
+
try {
|
|
51
|
+
const pw = await import("playwright")
|
|
52
|
+
chromium = pw.chromium
|
|
53
|
+
} catch {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await ensureChromium(chromium)
|
|
58
|
+
|
|
59
|
+
ensureTsConfig(cwd)
|
|
60
|
+
|
|
61
|
+
const config = createViteConfig({ cwd, mode: "development" })
|
|
62
|
+
const server = await createServer({
|
|
63
|
+
...config,
|
|
64
|
+
server: { port: 0, strictPort: false },
|
|
65
|
+
logLevel: "silent"
|
|
66
|
+
})
|
|
67
|
+
await server.listen()
|
|
68
|
+
|
|
69
|
+
const address = server.httpServer.address()
|
|
70
|
+
const port = typeof address === "object" ? address.port : 0
|
|
71
|
+
const url = `http://localhost:${port}/?export=true&slidePath=${encodeURIComponent(slidePath)}`
|
|
72
|
+
|
|
73
|
+
let browser
|
|
74
|
+
try {
|
|
75
|
+
browser = await chromium.launch({ headless: true })
|
|
76
|
+
const page = await browser.newPage({ viewport: { width, height } })
|
|
77
|
+
|
|
78
|
+
const errors = []
|
|
79
|
+
page.on("pageerror", err => errors.push(err.message))
|
|
80
|
+
|
|
81
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 })
|
|
82
|
+
await page.waitForSelector("[data-export-ready='true']", { timeout: 15000 }).catch(err => {
|
|
83
|
+
if (errors.length) {
|
|
84
|
+
throw new Error(`${err.message}\n Browser errors:\n ${errors.join("\n ")}`)
|
|
85
|
+
}
|
|
86
|
+
throw err
|
|
87
|
+
})
|
|
88
|
+
await page.waitForTimeout(200)
|
|
89
|
+
|
|
90
|
+
const element = await page.$("[data-export-ready='true']")
|
|
91
|
+
const screenshot = await element.screenshot({ type: "png" })
|
|
92
|
+
|
|
93
|
+
return screenshot
|
|
94
|
+
} finally {
|
|
95
|
+
if (browser) await browser.close().catch(() => {})
|
|
96
|
+
await server.close()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Capture a slide and return as base64 data URI.
|
|
102
|
+
* Returns null if Playwright is not available or capture fails.
|
|
103
|
+
* @param {{ cwd: string, slidePath: string }} opts
|
|
104
|
+
* @returns {Promise<string | null>}
|
|
105
|
+
*/
|
|
106
|
+
export async function captureSlideAsDataUri({ cwd, slidePath }) {
|
|
107
|
+
try {
|
|
108
|
+
const buffer = await captureSlideScreenshot({ cwd, slidePath })
|
|
109
|
+
if (!buffer) return null
|
|
110
|
+
return `data:image/png;base64,${buffer.toString("base64")}`
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error(` Screenshot error: ${err.message}`)
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
const LOCKFILE = ".promptslide-lock.json"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Read the lockfile from a project directory.
|
|
9
|
+
* @param {string} cwd
|
|
10
|
+
* @returns {{ items: Record<string, { version: number, installedAt: string }> }}
|
|
11
|
+
*/
|
|
12
|
+
export function readLockfile(cwd) {
|
|
13
|
+
const path = join(cwd, LOCKFILE)
|
|
14
|
+
if (!existsSync(path)) return { items: {} }
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(path, "utf-8"))
|
|
17
|
+
} catch {
|
|
18
|
+
console.warn(` Warning: ${LOCKFILE} is corrupt or unreadable. Starting fresh.`)
|
|
19
|
+
return { items: {} }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Write the lockfile to a project directory.
|
|
25
|
+
* @param {string} cwd
|
|
26
|
+
* @param {{ items: Record<string, { version: number, installedAt: string }> }} data
|
|
27
|
+
*/
|
|
28
|
+
export function writeLockfile(cwd, data) {
|
|
29
|
+
writeFileSync(join(cwd, LOCKFILE), JSON.stringify(data, null, 2) + "\n", "utf-8")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Hash a string with SHA-256 and return the hex digest.
|
|
34
|
+
* @param {string} content
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function hashContent(content) {
|
|
38
|
+
return createHash("sha256").update(content, "utf-8").digest("hex")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Hash a file on disk. Returns null if the file doesn't exist.
|
|
43
|
+
* @param {string} filePath - Absolute path
|
|
44
|
+
* @returns {string | null}
|
|
45
|
+
*/
|
|
46
|
+
export function hashFile(filePath) {
|
|
47
|
+
if (!existsSync(filePath)) return null
|
|
48
|
+
return hashContent(readFileSync(filePath, "utf-8"))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if a file on disk differs from its stored hash.
|
|
53
|
+
* Returns true if the file has been modified or doesn't exist.
|
|
54
|
+
* @param {string} cwd
|
|
55
|
+
* @param {string} relativePath - e.g. "src/slides/hero.tsx"
|
|
56
|
+
* @param {string} storedHash
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
export function isFileDirty(cwd, relativePath, storedHash) {
|
|
60
|
+
const currentHash = hashFile(join(cwd, relativePath))
|
|
61
|
+
if (!currentHash) return true
|
|
62
|
+
return currentHash !== storedHash
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Add or update a single item in the lockfile.
|
|
67
|
+
* @param {string} cwd
|
|
68
|
+
* @param {string} slug
|
|
69
|
+
* @param {number} version
|
|
70
|
+
* @param {Record<string, string>} files - Map of relative paths to content hashes
|
|
71
|
+
*/
|
|
72
|
+
export function updateLockfileItem(cwd, slug, version, files) {
|
|
73
|
+
const lock = readLockfile(cwd)
|
|
74
|
+
lock.items[slug] = {
|
|
75
|
+
version,
|
|
76
|
+
installedAt: new Date().toISOString().split("T")[0],
|
|
77
|
+
files
|
|
78
|
+
}
|
|
79
|
+
writeLockfile(cwd, lock)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Remove a single item from the lockfile.
|
|
84
|
+
* @param {string} cwd
|
|
85
|
+
* @param {string} slug
|
|
86
|
+
*/
|
|
87
|
+
export function removeLockfileItem(cwd, slug) {
|
|
88
|
+
const lock = readLockfile(cwd)
|
|
89
|
+
delete lock.items[slug]
|
|
90
|
+
writeLockfile(cwd, lock)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build common auth headers for registry API requests.
|
|
95
|
+
* @param {{ token: string, organizationId?: string }} auth
|
|
96
|
+
* @returns {Record<string, string>}
|
|
97
|
+
*/
|
|
98
|
+
function authHeaders(auth) {
|
|
99
|
+
const headers = { Authorization: `Bearer ${auth.token}` }
|
|
100
|
+
if (auth.organizationId) {
|
|
101
|
+
headers["X-Organization-Id"] = auth.organizationId
|
|
102
|
+
}
|
|
103
|
+
return headers
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Fetch the user's organizations from the registry.
|
|
108
|
+
* @param {{ registry: string, token: string }} auth
|
|
109
|
+
* @returns {Promise<{ id: string, name: string, slug: string, role: string }[]>}
|
|
110
|
+
*/
|
|
111
|
+
export async function fetchOrganizations(auth) {
|
|
112
|
+
const res = await fetch(`${auth.registry}/api/organizations`, {
|
|
113
|
+
headers: authHeaders(auth)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
if (res.status === 401) {
|
|
117
|
+
throw new Error("Authentication failed. Run `promptslide login` to re-authenticate.")
|
|
118
|
+
}
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
throw new Error(`Failed to fetch organizations (${res.status}): ${await res.text()}`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const data = await res.json()
|
|
124
|
+
return data.organizations || []
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fetch a registry item JSON from the registry API.
|
|
129
|
+
* @param {string} nameOrUrl - Item name (e.g. "slide-hero-gradient") or full URL
|
|
130
|
+
* @param {{ registry: string, apiKey: string }} auth - Auth credentials
|
|
131
|
+
* @returns {Promise<object>} Registry item JSON
|
|
132
|
+
*/
|
|
133
|
+
export async function fetchRegistryItem(nameOrUrl, auth) {
|
|
134
|
+
const url = nameOrUrl.startsWith("http")
|
|
135
|
+
? nameOrUrl
|
|
136
|
+
: `${auth.registry}/api/r/${nameOrUrl}.json`
|
|
137
|
+
|
|
138
|
+
const res = await fetch(url, {
|
|
139
|
+
headers: authHeaders(auth)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
if (res.status === 401) {
|
|
143
|
+
throw new Error("Authentication failed. Run `promptslide login` to re-authenticate.")
|
|
144
|
+
}
|
|
145
|
+
if (res.status === 403) {
|
|
146
|
+
const body = await res.json().catch(() => ({}))
|
|
147
|
+
if (body.status === "pending_review") {
|
|
148
|
+
throw new Error(`Item "${nameOrUrl}" is pending review. An admin must approve it first.`)
|
|
149
|
+
}
|
|
150
|
+
if (body.status === "rejected") {
|
|
151
|
+
throw new Error(`Item "${nameOrUrl}" was rejected by an admin.`)
|
|
152
|
+
}
|
|
153
|
+
throw new Error("Access denied. This item belongs to a different organization.")
|
|
154
|
+
}
|
|
155
|
+
if (res.status === 404) {
|
|
156
|
+
throw new Error(`Item not found: ${nameOrUrl}`)
|
|
157
|
+
}
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
throw new Error(`Registry error (${res.status}): ${await res.text()}`)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return res.json()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolve all registry dependencies recursively.
|
|
167
|
+
* Returns a flat list of all items to install (including the root item).
|
|
168
|
+
* Skips items whose target files already exist locally.
|
|
169
|
+
*
|
|
170
|
+
* @param {object} item - Root registry item JSON
|
|
171
|
+
* @param {{ registry: string, apiKey: string }} auth
|
|
172
|
+
* @param {string} cwd - Project root directory
|
|
173
|
+
* @returns {Promise<{ items: object[], npmDeps: Record<string, string> }>}
|
|
174
|
+
*/
|
|
175
|
+
export async function resolveRegistryDependencies(item, auth, cwd) {
|
|
176
|
+
const seen = new Set()
|
|
177
|
+
const items = []
|
|
178
|
+
const npmDeps = {}
|
|
179
|
+
|
|
180
|
+
async function resolve(current) {
|
|
181
|
+
if (seen.has(current.name)) return
|
|
182
|
+
seen.add(current.name)
|
|
183
|
+
|
|
184
|
+
// Collect npm dependencies
|
|
185
|
+
if (current.dependencies) {
|
|
186
|
+
Object.assign(npmDeps, current.dependencies)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check if any files need writing (skip if all exist)
|
|
190
|
+
const needsInstall = current.files?.some(f => {
|
|
191
|
+
const targetPath = join(cwd, f.target, f.path)
|
|
192
|
+
return !existsSync(targetPath)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
if (needsInstall || current === item) {
|
|
196
|
+
items.push(current)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Recurse into registry dependencies
|
|
200
|
+
if (current.registryDependencies?.length) {
|
|
201
|
+
for (const depName of current.registryDependencies) {
|
|
202
|
+
try {
|
|
203
|
+
const depItem = await fetchRegistryItem(depName, auth)
|
|
204
|
+
await resolve(depItem)
|
|
205
|
+
} catch (err) {
|
|
206
|
+
console.warn(` Warning: Could not resolve dependency "${depName}": ${err.message}`)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await resolve(item)
|
|
213
|
+
return { items, npmDeps }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if a registry item exists (by slug).
|
|
218
|
+
* @param {string} name - Item slug
|
|
219
|
+
* @param {{ registry: string, token: string }} auth
|
|
220
|
+
* @returns {Promise<boolean>}
|
|
221
|
+
*/
|
|
222
|
+
export async function registryItemExists(name, auth) {
|
|
223
|
+
try {
|
|
224
|
+
await fetchRegistryItem(name, auth)
|
|
225
|
+
return true
|
|
226
|
+
} catch {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Search/list registry items.
|
|
233
|
+
* @param {{ search?: string, type?: string }} params
|
|
234
|
+
* @param {{ registry: string, apiKey: string }} auth
|
|
235
|
+
* @returns {Promise<object[]>}
|
|
236
|
+
*/
|
|
237
|
+
export async function searchRegistry(params, auth) {
|
|
238
|
+
const url = new URL(`${auth.registry}/api/r`)
|
|
239
|
+
if (params.search) url.searchParams.set("search", params.search)
|
|
240
|
+
if (params.type) url.searchParams.set("type", params.type)
|
|
241
|
+
|
|
242
|
+
const res = await fetch(url.toString(), {
|
|
243
|
+
headers: authHeaders(auth)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
if (res.status === 401) {
|
|
247
|
+
throw new Error("Authentication failed. Run `promptslide login` to re-authenticate.")
|
|
248
|
+
}
|
|
249
|
+
if (!res.ok) {
|
|
250
|
+
throw new Error(`Registry error (${res.status}): ${await res.text()}`)
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return res.json()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Publish an item to the registry.
|
|
258
|
+
* @param {object} payload - Publish payload
|
|
259
|
+
* @param {{ registry: string, apiKey: string }} auth
|
|
260
|
+
* @returns {Promise<object>} Server response
|
|
261
|
+
*/
|
|
262
|
+
export async function publishToRegistry(payload, auth) {
|
|
263
|
+
const res = await fetch(`${auth.registry}/api/publish`, {
|
|
264
|
+
method: "POST",
|
|
265
|
+
headers: {
|
|
266
|
+
...authHeaders(auth),
|
|
267
|
+
"Content-Type": "application/json"
|
|
268
|
+
},
|
|
269
|
+
body: JSON.stringify(payload)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
if (res.status === 401) {
|
|
273
|
+
throw new Error("Authentication failed. Run `promptslide login` to re-authenticate.")
|
|
274
|
+
}
|
|
275
|
+
if (!res.ok) {
|
|
276
|
+
const body = await res.text()
|
|
277
|
+
throw new Error(`Publish failed (${res.status}): ${body}`)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return res.json()
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Detect the package manager in use.
|
|
285
|
+
* @param {string} cwd
|
|
286
|
+
* @returns {"bun" | "yarn" | "pnpm" | "npm"}
|
|
287
|
+
*/
|
|
288
|
+
export function detectPackageManager(cwd) {
|
|
289
|
+
if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb"))) return "bun"
|
|
290
|
+
if (existsSync(join(cwd, "yarn.lock"))) return "yarn"
|
|
291
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm"
|
|
292
|
+
return "npm"
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Get the install command and args for adding dependencies.
|
|
297
|
+
* Returns { cmd, args, display } for safe use with execFileSync.
|
|
298
|
+
* @param {"bun" | "yarn" | "pnpm" | "npm"} pm
|
|
299
|
+
* @param {string[]} packages
|
|
300
|
+
* @returns {{ cmd: string, args: string[], display: string }}
|
|
301
|
+
*/
|
|
302
|
+
export function getInstallCommand(pm, packages) {
|
|
303
|
+
// Validate package names to prevent injection
|
|
304
|
+
for (const pkg of packages) {
|
|
305
|
+
if (!/^@?[a-zA-Z0-9][-a-zA-Z0-9/._]*@[^\s]+$/.test(pkg)) {
|
|
306
|
+
throw new Error(`Invalid package specifier: ${pkg}`)
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
switch (pm) {
|
|
310
|
+
case "bun":
|
|
311
|
+
return { cmd: "bun", args: ["add", ...packages], display: `bun add ${packages.join(" ")}` }
|
|
312
|
+
case "yarn":
|
|
313
|
+
return { cmd: "yarn", args: ["add", ...packages], display: `yarn add ${packages.join(" ")}` }
|
|
314
|
+
case "pnpm":
|
|
315
|
+
return { cmd: "pnpm", args: ["add", ...packages], display: `pnpm add ${packages.join(" ")}` }
|
|
316
|
+
default:
|
|
317
|
+
return { cmd: "npm", args: ["install", ...packages], display: `npm install ${packages.join(" ")}` }
|
|
318
|
+
}
|
|
319
|
+
}
|
package/src/vite/config.mjs
CHANGED
|
@@ -22,7 +22,7 @@ export function createViteConfig({ cwd, mode = "development" }) {
|
|
|
22
22
|
configFile: false,
|
|
23
23
|
root: cwd,
|
|
24
24
|
mode,
|
|
25
|
-
plugins: [react(), promptslidePlugin()],
|
|
25
|
+
plugins: [react(), promptslidePlugin({ root: cwd })],
|
|
26
26
|
resolve: {
|
|
27
27
|
alias: {
|
|
28
28
|
"@": resolve(cwd, "src"),
|
package/src/vite/plugin.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
const VIRTUAL_ENTRY_ID = "virtual:promptslide-entry"
|
|
2
2
|
const RESOLVED_VIRTUAL_ENTRY_ID = "\0" + VIRTUAL_ENTRY_ID
|
|
3
|
+
const VIRTUAL_EXPORT_ID = "virtual:promptslide-export"
|
|
4
|
+
const RESOLVED_VIRTUAL_EXPORT_ID = "\0" + VIRTUAL_EXPORT_ID
|
|
3
5
|
|
|
4
6
|
function getHtmlTemplate() {
|
|
5
7
|
return `<!doctype html>
|
|
6
|
-
<html lang="en"
|
|
8
|
+
<html lang="en">
|
|
7
9
|
<head>
|
|
8
10
|
<meta charset="UTF-8" />
|
|
9
11
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
@@ -16,12 +18,27 @@ function getHtmlTemplate() {
|
|
|
16
18
|
</html>`
|
|
17
19
|
}
|
|
18
20
|
|
|
19
|
-
function
|
|
21
|
+
function getExportHtmlTemplate() {
|
|
22
|
+
return `<!doctype html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="UTF-8" />
|
|
26
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
27
|
+
<title>PromptSlide Export</title>
|
|
28
|
+
</head>
|
|
29
|
+
<body>
|
|
30
|
+
<div id="root"></div>
|
|
31
|
+
<script type="module" src="/@id/${VIRTUAL_EXPORT_ID}"></script>
|
|
32
|
+
</body>
|
|
33
|
+
</html>`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getEntryModule(root) {
|
|
20
37
|
return `
|
|
21
38
|
import { StrictMode, createElement } from "react"
|
|
22
39
|
import { createRoot } from "react-dom/client"
|
|
23
|
-
import "
|
|
24
|
-
import App from "
|
|
40
|
+
import "${root}/src/globals.css"
|
|
41
|
+
import App from "${root}/src/App"
|
|
25
42
|
|
|
26
43
|
createRoot(document.getElementById("root")).render(
|
|
27
44
|
createElement(StrictMode, null, createElement(App))
|
|
@@ -29,24 +46,88 @@ createRoot(document.getElementById("root")).render(
|
|
|
29
46
|
`
|
|
30
47
|
}
|
|
31
48
|
|
|
32
|
-
|
|
49
|
+
function getExportEntryModule(root, slidePath) {
|
|
50
|
+
return `
|
|
51
|
+
import { StrictMode, createElement, useState, useEffect } from "react"
|
|
52
|
+
import { createRoot } from "react-dom/client"
|
|
53
|
+
import { AnimationProvider, SlideErrorBoundary, SlideThemeProvider } from "promptslide"
|
|
54
|
+
import "${root}/src/globals.css"
|
|
55
|
+
import * as slideMod from "${root}/${slidePath}"
|
|
56
|
+
|
|
57
|
+
let theme = {}
|
|
58
|
+
try {
|
|
59
|
+
const themeMod = await import("${root}/src/theme")
|
|
60
|
+
theme = themeMod.theme || themeMod.default || {}
|
|
61
|
+
} catch {}
|
|
62
|
+
|
|
63
|
+
const SlideComponent = slideMod.default || Object.values(slideMod).find(v => typeof v === "function")
|
|
64
|
+
|
|
65
|
+
function ExportView() {
|
|
66
|
+
const [ready, setReady] = useState(false)
|
|
67
|
+
useEffect(() => { setReady(true) }, [])
|
|
68
|
+
return createElement("div", {
|
|
69
|
+
"data-export-ready": ready ? "true" : undefined,
|
|
70
|
+
style: { width: 1280, height: 720, overflow: "hidden", position: "relative", background: "black" }
|
|
71
|
+
},
|
|
72
|
+
createElement(AnimationProvider, { currentStep: 0, totalSteps: 0, showAllAnimations: true },
|
|
73
|
+
createElement(SlideErrorBoundary, { slideIndex: 0 },
|
|
74
|
+
SlideComponent
|
|
75
|
+
? createElement(SlideComponent, { slideNumber: 1, totalSlides: 1 })
|
|
76
|
+
: null
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
createRoot(document.getElementById("root")).render(
|
|
83
|
+
createElement(StrictMode, null,
|
|
84
|
+
createElement(SlideThemeProvider, { theme },
|
|
85
|
+
createElement(ExportView)
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function promptslidePlugin({ root: initialRoot } = {}) {
|
|
93
|
+
let root = initialRoot
|
|
94
|
+
let exportSlidePath = null
|
|
95
|
+
|
|
33
96
|
return {
|
|
34
97
|
name: "promptslide",
|
|
35
98
|
enforce: "pre",
|
|
36
99
|
|
|
100
|
+
configResolved(config) {
|
|
101
|
+
if (!root) root = config.root
|
|
102
|
+
},
|
|
103
|
+
|
|
37
104
|
resolveId(id) {
|
|
38
|
-
if (id === VIRTUAL_ENTRY_ID)
|
|
39
|
-
|
|
40
|
-
}
|
|
105
|
+
if (id === VIRTUAL_ENTRY_ID) return RESOLVED_VIRTUAL_ENTRY_ID
|
|
106
|
+
if (id === VIRTUAL_EXPORT_ID) return RESOLVED_VIRTUAL_EXPORT_ID
|
|
41
107
|
},
|
|
42
108
|
|
|
43
109
|
load(id) {
|
|
44
|
-
if (id === RESOLVED_VIRTUAL_ENTRY_ID)
|
|
45
|
-
|
|
46
|
-
}
|
|
110
|
+
if (id === RESOLVED_VIRTUAL_ENTRY_ID) return getEntryModule(root)
|
|
111
|
+
if (id === RESOLVED_VIRTUAL_EXPORT_ID) return getExportEntryModule(root, exportSlidePath || "src/slides/slide-title.tsx")
|
|
47
112
|
},
|
|
48
113
|
|
|
49
114
|
configureServer(server) {
|
|
115
|
+
// Pre-middleware: intercept export URLs before Vite's SPA fallback rewrites them
|
|
116
|
+
server.middlewares.use(async (req, res, next) => {
|
|
117
|
+
const url = new URL(req.url, "http://localhost")
|
|
118
|
+
if (url.searchParams.get("export") !== "true") return next()
|
|
119
|
+
|
|
120
|
+
exportSlidePath = url.searchParams.get("slidePath") || "src/slides/slide-title.tsx"
|
|
121
|
+
// Invalidate cached export module so it regenerates with the new slidePath
|
|
122
|
+
const mod = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_EXPORT_ID)
|
|
123
|
+
if (mod) server.moduleGraph.invalidateModule(mod)
|
|
124
|
+
const html = await server.transformIndexHtml("/index.html", getExportHtmlTemplate())
|
|
125
|
+
res.setHeader("Content-Type", "text/html")
|
|
126
|
+
res.statusCode = 200
|
|
127
|
+
res.end(html)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Post-middleware: serve the regular app after Vite's built-in middleware
|
|
50
131
|
return () => {
|
|
51
132
|
server.middlewares.use(async (req, res, next) => {
|
|
52
133
|
if (req.url === "/" || req.url === "/index.html") {
|