promptslide 0.3.17 → 0.3.19

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.
@@ -1,6 +1,8 @@
1
1
  import { readFileSync, writeFileSync, existsSync } from "node:fs"
2
2
  import { join } from "node:path"
3
3
 
4
+ import { DEFAULT_LOOP_CONFIG, readLoopConfig, writeLoopConfig } from "./loop-config.mjs"
5
+
4
6
  const DECK_CONFIG_PATH = "src/deck-config.ts"
5
7
 
6
8
  /**
@@ -224,6 +226,8 @@ export function replaceDeckConfig(cwd, slides, opts = {}) {
224
226
  ].join("\n")
225
227
 
226
228
  writeFileSync(configPath, content, "utf-8")
229
+ if (opts.loopConfig !== undefined) writeLoopConfig(cwd, opts.loopConfig)
230
+ else if (!existsSync(join(cwd, "loop-config.json"))) writeLoopConfig(cwd, DEFAULT_LOOP_CONFIG)
227
231
  return true
228
232
  }
229
233
 
@@ -310,6 +314,8 @@ export function parseDeckConfig(cwd) {
310
314
  if (slides.length === 0) return null
311
315
 
312
316
  const result = { slides }
317
+ const loopConfig = readLoopConfig(cwd)
318
+ if (loopConfig) result.loopConfig = loopConfig
313
319
  if (transition) result.transition = transition
314
320
  if (directionalTransition !== undefined) result.directionalTransition = directionalTransition
315
321
  return result
@@ -0,0 +1,70 @@
1
+ import { randomUUID } from "node:crypto"
2
+ import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync, lstatSync } from "node:fs"
3
+ import { join } from "node:path"
4
+
5
+ import { DEFAULT_LOOP_CONFIG, validateLoopConfig } from "../core/loop/config.mjs"
6
+
7
+ export function readLoopConfig(root) {
8
+ const path = join(root, "loop-config.json")
9
+ if (!existsSync(path)) return undefined
10
+ if (lstatSync(path).isSymbolicLink()) throw new Error("loop-config.json must be a regular file")
11
+ return validateLoopConfig(JSON.parse(readFileSync(path, "utf8")))
12
+ }
13
+
14
+ export function writeLoopConfig(root, config) {
15
+ const value = validateLoopConfig(config)
16
+ const path = join(root, "loop-config.json")
17
+ if (existsSync(path) && lstatSync(path).isSymbolicLink())
18
+ throw new Error("loop-config.json must be a regular file")
19
+ const temporary = join(root, `.loop-config-${randomUUID()}.tmp`)
20
+ try {
21
+ writeFileSync(temporary, JSON.stringify(value, null, 2) + "\n", { flag: "wx" })
22
+ renameSync(temporary, path)
23
+ } finally {
24
+ if (existsSync(temporary)) unlinkSync(temporary)
25
+ }
26
+ }
27
+
28
+ export function loopConfigMiddleware(root, onSaved = () => {}) {
29
+ return (req, res, next) => {
30
+ if (req.url !== "/__promptslide_loop") return next()
31
+ if (req.method !== "POST") {
32
+ res.statusCode = 405
33
+ return res.end()
34
+ }
35
+ const host = req.headers.host || ""
36
+ const remote = req.socket.remoteAddress
37
+ if (
38
+ !/^(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/.test(host) ||
39
+ !["127.0.0.1", "::1", "::ffff:127.0.0.1"].includes(remote) ||
40
+ req.headers.origin !== `http://${host}` ||
41
+ !req.headers["content-type"]?.startsWith("application/json")
42
+ ) {
43
+ res.statusCode = 403
44
+ return res.end("Loop settings can only be saved from local Studio.")
45
+ }
46
+ let body = ""
47
+ req.on("data", chunk => {
48
+ body += chunk
49
+ if (Buffer.byteLength(body) > 65536) {
50
+ res.statusCode = 413
51
+ res.end()
52
+ req.destroy()
53
+ }
54
+ })
55
+ req.on("end", () => {
56
+ if (res.writableEnded) return
57
+ try {
58
+ writeLoopConfig(root, JSON.parse(body))
59
+ onSaved()
60
+ res.statusCode = 204
61
+ res.end()
62
+ } catch {
63
+ res.statusCode = 400
64
+ res.end("Invalid loop settings or project file could not be written.")
65
+ }
66
+ })
67
+ }
68
+ }
69
+
70
+ export { DEFAULT_LOOP_CONFIG }
@@ -5,6 +5,7 @@ import { fileURLToPath } from "node:url"
5
5
  import tailwindcss from "@tailwindcss/postcss"
6
6
  import react from "@vitejs/plugin-react"
7
7
 
8
+ import { editorPlugin } from "../editor/plugin.mjs"
8
9
  import { promptslidePlugin } from "./plugin.mjs"
9
10
 
10
11
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -17,13 +18,20 @@ const tailwindPath = resolve(dirname(require.resolve("tailwindcss")), "..")
17
18
  // Resolve promptslide core from source so Vite uses TS directly without a build step
18
19
  const promptslidePath = resolve(__dirname, "../core/index.ts")
19
20
 
20
- export function createViteConfig({ cwd, mode = "development" }) {
21
+ export function createViteConfig({ cwd, mode = "development", editor = true }) {
21
22
  return {
22
23
  configFile: false,
23
24
  root: cwd,
24
25
  mode,
25
- plugins: [react(), promptslidePlugin({ root: cwd })],
26
+ plugins: [
27
+ ...(mode === "development" && editor ? [editorPlugin({ root: cwd })] : []),
28
+ react(),
29
+ promptslidePlugin({ root: cwd })
30
+ ],
26
31
  resolve: {
32
+ // The CLI's core/editor and the deck must share one React dispatcher,
33
+ // including when the CLI is installed or linked outside the deck root.
34
+ dedupe: ["react", "react-dom"],
27
35
  alias: {
28
36
  "@": resolve(cwd, "src"),
29
37
  promptslide: promptslidePath,
@@ -31,6 +39,19 @@ export function createViteConfig({ cwd, mode = "development" }) {
31
39
  tailwindcss: tailwindPath
32
40
  }
33
41
  },
42
+ // These imports live in virtual entry modules, which dependency scanning
43
+ // cannot discover before the first page loads. Optimize them together so
44
+ // the viewer and editor never start with different dependency generations.
45
+ optimizeDeps: {
46
+ include: [
47
+ "react",
48
+ "react-dom/client",
49
+ "lucide-react",
50
+ "framer-motion",
51
+ "clsx",
52
+ "tailwind-merge"
53
+ ]
54
+ },
34
55
  css: {
35
56
  postcss: {
36
57
  plugins: [tailwindcss()]
@@ -2,6 +2,7 @@ import { readFileSync, writeFileSync, existsSync } from "node:fs"
2
2
  import { join, resolve, sep, extname } from "node:path"
3
3
 
4
4
  import { bold, dim } from "../utils/ansi.mjs"
5
+ import { readLoopConfig, DEFAULT_LOOP_CONFIG, loopConfigMiddleware } from "../utils/loop-config.mjs"
5
6
  import { projectIdentity } from "../utils/studio-discovery.mjs"
6
7
 
7
8
  /** Normalize a path to forward slashes so it can be used in ES module import strings on Windows. */
@@ -84,9 +85,11 @@ import { StrictMode, createElement } from "react"
84
85
  import { createRoot } from "react-dom/client"
85
86
  import "${r}/src/globals.css"
86
87
  import App from "${r}/src/App"
88
+ import { LoopConfigProvider } from "promptslide"
89
+ const loopConfig = ${JSON.stringify(readLoopConfig(root) ?? DEFAULT_LOOP_CONFIG)}
87
90
 
88
91
  createRoot(document.getElementById("root")).render(
89
- createElement(StrictMode, null, createElement(App))
92
+ createElement(StrictMode, null, createElement(LoopConfigProvider, { config: loopConfig }, createElement(App)))
90
93
  )
91
94
  `
92
95
  }
@@ -256,7 +259,7 @@ export function promptslidePlugin({ root: initialRoot } = {}) {
256
259
  return {
257
260
  server: {
258
261
  watch: {
259
- ignored: ["**/annotations.json"]
262
+ ignored: ["**/annotations.json", "**/.loop-config-*.tmp"]
260
263
  }
261
264
  }
262
265
  }
@@ -284,6 +287,19 @@ export function promptslidePlugin({ root: initialRoot } = {}) {
284
287
  },
285
288
 
286
289
  configureServer(server) {
290
+ const invalidateLoopConfig = () => {
291
+ const entry = server.moduleGraph.getModuleById(RESOLVED_VIRTUAL_ENTRY_ID)
292
+ if (entry) server.moduleGraph.invalidateModule(entry)
293
+ }
294
+ const loopFileChanged = path => {
295
+ if (resolve(path) === resolve(root, "loop-config.json")) invalidateLoopConfig()
296
+ }
297
+ // New page loads see the saved file without interrupting the current presentation.
298
+ server.watcher
299
+ .on("change", loopFileChanged)
300
+ .on("add", loopFileChanged)
301
+ .on("unlink", loopFileChanged)
302
+ server.middlewares.use(loopConfigMiddleware(root, invalidateLoopConfig))
287
303
  server.middlewares.use(async (req, res, next) => {
288
304
  if (req.method !== "GET" || req.url !== "/__promptslide_pdf") return next()
289
305
  try {
@@ -384,7 +400,7 @@ export function promptslidePlugin({ root: initialRoot } = {}) {
384
400
  server.middlewares.use(async (req, res, next) => {
385
401
  const url = new URL(req.url, "http://localhost")
386
402
  if (url.searchParams.get("pdf") === "true") {
387
- const html = await server.transformIndexHtml("/index.html", getPdfHtmlTemplate())
403
+ const html = await server.transformIndexHtml("/index.html", getPdfHtmlTemplate(), req.url)
388
404
  res.setHeader("Content-Type", "text/html")
389
405
  res.statusCode = 200
390
406
  res.end(html)
@@ -402,7 +418,8 @@ export function promptslidePlugin({ root: initialRoot } = {}) {
402
418
  }
403
419
  const html = await server.transformIndexHtml(
404
420
  "/index.html",
405
- getExportHtmlTemplate(slidePath)
421
+ getExportHtmlTemplate(slidePath),
422
+ req.url
406
423
  )
407
424
  res.setHeader("Content-Type", "text/html")
408
425
  res.statusCode = 200
@@ -3,6 +3,6 @@ import { SlideTitle } from "@/slides/slide-title";
3
3
  import { SlideExample } from "@/slides/slide-example";
4
4
 
5
5
  export const slides: SlideConfig[] = [
6
- { id: "title", component: SlideTitle, steps: 0 },
7
- { id: "example", component: SlideExample, steps: 2 },
6
+ { id: "title", title: "Introduction", component: SlideTitle, steps: 0 },
7
+ { id: "example", title: "Example", component: SlideExample, steps: 2 },
8
8
  ];