@brett_lamy/docstream 1.2.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/README.md +76 -14
  2. package/dist/assets.d.ts +3 -0
  3. package/dist/assets.d.ts.map +1 -0
  4. package/dist/assets.js +20 -0
  5. package/dist/assets.js.map +1 -0
  6. package/dist/demo/DemoViewer.d.ts +64 -0
  7. package/dist/demo/DemoViewer.d.ts.map +1 -0
  8. package/dist/demo/DemoViewer.js +426 -0
  9. package/dist/demo/DemoViewer.js.map +1 -0
  10. package/dist/demo/context.d.ts +14 -0
  11. package/dist/demo/context.d.ts.map +1 -0
  12. package/dist/demo/context.js +25 -0
  13. package/dist/demo/context.js.map +1 -0
  14. package/dist/demo/glob.d.ts +60 -0
  15. package/dist/demo/glob.d.ts.map +1 -0
  16. package/dist/demo/glob.js +158 -0
  17. package/dist/demo/glob.js.map +1 -0
  18. package/dist/demo/index.d.ts +10 -0
  19. package/dist/demo/index.d.ts.map +1 -0
  20. package/dist/demo/index.js +6 -0
  21. package/dist/demo/index.js.map +1 -0
  22. package/dist/demo/markdown.d.ts +60 -0
  23. package/dist/demo/markdown.d.ts.map +1 -0
  24. package/dist/demo/markdown.js +181 -0
  25. package/dist/demo/markdown.js.map +1 -0
  26. package/dist/demo/surface.d.ts +11 -0
  27. package/dist/demo/surface.d.ts.map +1 -0
  28. package/dist/demo/surface.js +69 -0
  29. package/dist/demo/surface.js.map +1 -0
  30. package/dist/demo/types.d.ts +91 -0
  31. package/dist/demo/types.d.ts.map +1 -0
  32. package/dist/demo/types.js +2 -0
  33. package/dist/demo/types.js.map +1 -0
  34. package/dist/docs/CollapsibleCode.d.ts +14 -0
  35. package/dist/docs/CollapsibleCode.d.ts.map +1 -0
  36. package/dist/docs/CollapsibleCode.js +19 -0
  37. package/dist/docs/CollapsibleCode.js.map +1 -0
  38. package/dist/docs/DocsRenderer.d.ts +49 -0
  39. package/dist/docs/DocsRenderer.d.ts.map +1 -0
  40. package/dist/docs/DocsRenderer.js +277 -0
  41. package/dist/docs/DocsRenderer.js.map +1 -0
  42. package/dist/docs/HighlightedCode.d.ts +6 -0
  43. package/dist/docs/HighlightedCode.d.ts.map +1 -0
  44. package/dist/docs/HighlightedCode.js +40 -0
  45. package/dist/docs/HighlightedCode.js.map +1 -0
  46. package/dist/docs/Mermaid.d.ts +7 -0
  47. package/dist/docs/Mermaid.d.ts.map +1 -0
  48. package/dist/docs/Mermaid.js +39 -0
  49. package/dist/docs/Mermaid.js.map +1 -0
  50. package/dist/docs/PageActions.d.ts +33 -0
  51. package/dist/docs/PageActions.d.ts.map +1 -0
  52. package/dist/docs/PageActions.js +115 -0
  53. package/dist/docs/PageActions.js.map +1 -0
  54. package/dist/docs/controls.d.ts +29 -0
  55. package/dist/docs/controls.d.ts.map +1 -0
  56. package/dist/docs/controls.js +46 -0
  57. package/dist/docs/controls.js.map +1 -0
  58. package/dist/docs/copy.d.ts +24 -0
  59. package/dist/docs/copy.d.ts.map +1 -0
  60. package/dist/docs/copy.js +62 -0
  61. package/dist/docs/copy.js.map +1 -0
  62. package/dist/docs/math.d.ts +3 -0
  63. package/dist/docs/math.d.ts.map +1 -0
  64. package/dist/docs/math.js +12 -0
  65. package/dist/docs/math.js.map +1 -0
  66. package/dist/docs/reference.d.ts +27 -0
  67. package/dist/docs/reference.d.ts.map +1 -0
  68. package/dist/docs/reference.js +73 -0
  69. package/dist/docs/reference.js.map +1 -0
  70. package/dist/docs/tabs-sync.d.ts +5 -0
  71. package/dist/docs/tabs-sync.d.ts.map +1 -0
  72. package/dist/docs/tabs-sync.js +58 -0
  73. package/dist/docs/tabs-sync.js.map +1 -0
  74. package/dist/gitbook/ast.d.ts +506 -0
  75. package/dist/gitbook/ast.d.ts.map +1 -0
  76. package/dist/gitbook/ast.js +8 -0
  77. package/dist/gitbook/ast.js.map +1 -0
  78. package/dist/gitbook/attrs.d.ts +14 -0
  79. package/dist/gitbook/attrs.d.ts.map +1 -0
  80. package/dist/gitbook/attrs.js +36 -0
  81. package/dist/gitbook/attrs.js.map +1 -0
  82. package/dist/gitbook/flatten.d.ts +14 -0
  83. package/dist/gitbook/flatten.d.ts.map +1 -0
  84. package/dist/gitbook/flatten.js +45 -0
  85. package/dist/gitbook/flatten.js.map +1 -0
  86. package/dist/gitbook/index.d.ts +10 -0
  87. package/dist/gitbook/index.d.ts.map +1 -0
  88. package/dist/gitbook/index.js +8 -0
  89. package/dist/gitbook/index.js.map +1 -0
  90. package/dist/gitbook/inline.d.ts +28 -0
  91. package/dist/gitbook/inline.d.ts.map +1 -0
  92. package/dist/gitbook/inline.js +956 -0
  93. package/dist/gitbook/inline.js.map +1 -0
  94. package/dist/gitbook/outline.d.ts +16 -0
  95. package/dist/gitbook/outline.d.ts.map +1 -0
  96. package/dist/gitbook/outline.js +44 -0
  97. package/dist/gitbook/outline.js.map +1 -0
  98. package/dist/gitbook/package-managers.d.ts +12 -0
  99. package/dist/gitbook/package-managers.d.ts.map +1 -0
  100. package/dist/gitbook/package-managers.js +93 -0
  101. package/dist/gitbook/package-managers.js.map +1 -0
  102. package/dist/gitbook/parse.d.ts +44 -0
  103. package/dist/gitbook/parse.d.ts.map +1 -0
  104. package/dist/gitbook/parse.js +1113 -0
  105. package/dist/gitbook/parse.js.map +1 -0
  106. package/dist/gitbook/serialize.d.ts +23 -0
  107. package/dist/gitbook/serialize.d.ts.map +1 -0
  108. package/dist/gitbook/serialize.js +498 -0
  109. package/dist/gitbook/serialize.js.map +1 -0
  110. package/dist/index.d.ts +34 -0
  111. package/dist/index.d.ts.map +1 -0
  112. package/dist/index.js +24 -0
  113. package/dist/index.js.map +1 -0
  114. package/dist/openapi/OpenApiOperation.d.ts +6 -0
  115. package/dist/openapi/OpenApiOperation.d.ts.map +1 -0
  116. package/dist/openapi/OpenApiOperation.js +112 -0
  117. package/dist/openapi/OpenApiOperation.js.map +1 -0
  118. package/dist/playground/InlineDemoPreview.d.ts +24 -0
  119. package/dist/playground/InlineDemoPreview.d.ts.map +1 -0
  120. package/dist/playground/InlineDemoPreview.js +48 -0
  121. package/dist/playground/InlineDemoPreview.js.map +1 -0
  122. package/dist/playground/PlaygroundStreamdown.d.ts +16 -0
  123. package/dist/playground/PlaygroundStreamdown.d.ts.map +1 -0
  124. package/dist/playground/PlaygroundStreamdown.js +22 -0
  125. package/dist/playground/PlaygroundStreamdown.js.map +1 -0
  126. package/dist/playground/ReactCodePreview.d.ts +64 -0
  127. package/dist/playground/ReactCodePreview.d.ts.map +1 -0
  128. package/dist/playground/ReactCodePreview.js +108 -0
  129. package/dist/playground/ReactCodePreview.js.map +1 -0
  130. package/dist/playground/filesystem.d.ts +27 -0
  131. package/dist/playground/filesystem.d.ts.map +1 -0
  132. package/dist/playground/filesystem.js +65 -0
  133. package/dist/playground/filesystem.js.map +1 -0
  134. package/dist/playground/index.d.ts +9 -0
  135. package/dist/playground/index.d.ts.map +1 -0
  136. package/dist/playground/index.js +7 -0
  137. package/dist/playground/index.js.map +1 -0
  138. package/dist/playground/project.d.ts +57 -0
  139. package/dist/playground/project.d.ts.map +1 -0
  140. package/dist/playground/project.js +137 -0
  141. package/dist/playground/project.js.map +1 -0
  142. package/dist/replay/NativeReplay.d.ts +9 -0
  143. package/dist/replay/NativeReplay.d.ts.map +1 -0
  144. package/dist/replay/NativeReplay.js +173 -0
  145. package/dist/replay/NativeReplay.js.map +1 -0
  146. package/dist/replay/ReplayPreview.d.ts +33 -0
  147. package/dist/replay/ReplayPreview.d.ts.map +1 -0
  148. package/dist/replay/ReplayPreview.js +91 -0
  149. package/dist/replay/ReplayPreview.js.map +1 -0
  150. package/dist/replay/index.d.ts +5 -0
  151. package/dist/replay/index.d.ts.map +1 -0
  152. package/dist/replay/index.js +4 -0
  153. package/dist/replay/index.js.map +1 -0
  154. package/dist/replay/sanitize.d.ts +4 -0
  155. package/dist/replay/sanitize.d.ts.map +1 -0
  156. package/dist/replay/sanitize.js +86 -0
  157. package/dist/replay/sanitize.js.map +1 -0
  158. package/dist/replay/url.d.ts +8 -0
  159. package/dist/replay/url.d.ts.map +1 -0
  160. package/dist/replay/url.js +41 -0
  161. package/dist/replay/url.js.map +1 -0
  162. package/dist/source/SourcePreview.d.ts +13 -0
  163. package/dist/source/SourcePreview.d.ts.map +1 -0
  164. package/dist/source/SourcePreview.js +47 -0
  165. package/dist/source/SourcePreview.js.map +1 -0
  166. package/dist/source/client.d.ts +7 -0
  167. package/dist/source/client.d.ts.map +1 -0
  168. package/dist/source/client.js +52 -0
  169. package/dist/source/client.js.map +1 -0
  170. package/dist/source/index.d.ts +6 -0
  171. package/dist/source/index.d.ts.map +1 -0
  172. package/dist/source/index.js +3 -0
  173. package/dist/source/index.js.map +1 -0
  174. package/dist/source/types.d.ts +33 -0
  175. package/dist/source/types.d.ts.map +1 -0
  176. package/dist/source/types.js +2 -0
  177. package/dist/source/types.js.map +1 -0
  178. package/dist/streamdown.d.ts +19 -0
  179. package/dist/streamdown.d.ts.map +1 -0
  180. package/dist/streamdown.js +15 -0
  181. package/dist/streamdown.js.map +1 -0
  182. package/dist/styles.css +2441 -0
  183. package/dist/video/VideoEmbed.d.ts +17 -0
  184. package/dist/video/VideoEmbed.d.ts.map +1 -0
  185. package/dist/video/VideoEmbed.js +12 -0
  186. package/dist/video/VideoEmbed.js.map +1 -0
  187. package/dist/video/index.d.ts +3 -0
  188. package/dist/video/index.d.ts.map +1 -0
  189. package/dist/video/index.js +2 -0
  190. package/dist/video/index.js.map +1 -0
  191. package/dist/vite/index.d.ts +21 -0
  192. package/dist/vite/index.d.ts.map +1 -0
  193. package/dist/vite/index.js +137 -0
  194. package/dist/vite/index.js.map +1 -0
  195. package/dist/viz/VizEmbed.d.ts +18 -0
  196. package/dist/viz/VizEmbed.d.ts.map +1 -0
  197. package/dist/viz/VizEmbed.js +12 -0
  198. package/dist/viz/VizEmbed.js.map +1 -0
  199. package/dist/viz/index.d.ts +4 -0
  200. package/dist/viz/index.d.ts.map +1 -0
  201. package/dist/viz/index.js +3 -0
  202. package/dist/viz/index.js.map +1 -0
  203. package/package.json +70 -34
  204. package/src/demo/types.ts +2 -2
  205. package/src/docs/DocsRenderer.tsx +31 -7
  206. package/src/docs/Mermaid.tsx +5 -3
  207. package/src/docs/math.ts +3 -0
  208. package/src/index.ts +10 -28
  209. package/src/playground/PlaygroundStreamdown.tsx +28 -61
  210. package/src/playground/filesystem.ts +10 -190
  211. package/src/playground/index.ts +4 -0
  212. package/src/playground/project.ts +194 -0
  213. package/src/replay/NativeReplay.tsx +236 -0
  214. package/src/replay/ReplayPreview.tsx +7 -226
  215. package/src/styles.css +91 -1
  216. package/src/replay/styles.css +0 -87
package/src/index.ts CHANGED
@@ -1,5 +1,7 @@
1
- export { PlaygroundStreamdown as GitbookStreamdown } from "./playground/PlaygroundStreamdown"
2
- export type { PlaygroundStreamdownProps as GitbookStreamdownProps } from "./playground/PlaygroundStreamdown"
1
+ // The root entry never references `@agent-wasm/core` (almost-node). Live code fences and
2
+ // inline-file demos run only when the host passes a runtime — see `@brett_lamy/docstream/playground`.
3
+ export { GitbookStreamdown } from "./streamdown"
4
+ export type { GitbookStreamdownProps } from "./streamdown"
3
5
  export { DocsRenderer, MarkdownContent } from "./docs/DocsRenderer"
4
6
  export type { DocRenderOptions, LivePreviewProps, LivePreviewRenderer, SourceReferenceRenderer } from "./docs/DocsRenderer"
5
7
  export { DocPageActions } from "./docs/PageActions"
@@ -47,45 +49,27 @@ export type {
47
49
  } from "./demo"
48
50
  export { CitationSources, DocstreamRefContext, InlineReference } from "./docs/reference"
49
51
  export type { CitationPreview, CitationPreviewResolver, DocstreamRefContextValue } from "./docs/reference"
50
- export { ReplayEmbed, ReplayPreview } from "./replay"
52
+ export { ReplayEmbed, ReplayPreview } from "./replay/ReplayPreview"
51
53
  export type {
52
54
  ReplayEventsSource,
53
55
  ReplayEventsUrlSource,
54
56
  ReplayPageSource,
55
57
  ReplayPreviewProps,
56
58
  ReplaySource,
57
- } from "./replay"
58
- export { isReplayQaUrl, normalizeReplayEmbedUrl } from "./replay"
59
+ } from "./replay/ReplayPreview"
60
+ export { isReplayQaUrl, normalizeReplayEmbedUrl } from "./replay/url"
59
61
  export { VideoEmbed } from "./video"
60
62
  export type { VideoEmbedProps } from "./video"
61
- export { VizEmbed } from "./viz"
62
- export type { VizEmbedProps } from "./viz"
63
63
  export { OpenApiOperation } from "./openapi/OpenApiOperation"
64
- export {
65
- InlineDemoPreview,
66
- ReactCodePreview,
67
- ReactDemo,
68
- almostNodeDemoRuntime,
69
- createAlmostNodeDemoRuntime,
70
- createInlineDemoProject,
71
- createReactDemoFiles,
72
- createAlmostNodeFilesystem,
73
- createAlmostNodeWorkspace,
74
- } from "./playground"
64
+ export { createInlineDemoProject, createReactDemoFiles } from "./playground/project"
75
65
  export type {
76
66
  AlmostNodeFile,
77
67
  AlmostNodeFileContent,
78
68
  AlmostNodeFiles,
79
- AlmostNodeFilesystemOptions,
80
- AlmostNodeWorkspace,
81
- AlmostNodeWorkspaceOptions,
82
- AlmostNodeDemoRuntimeOptions,
83
- InlineDemoPreviewProps,
84
69
  InlineDemoProject,
85
70
  InlineDemoProjectOptions,
86
- ReactCodePreviewProps,
87
- ReactDemoProps,
88
- } from "./playground"
71
+ ReactDemoFilesOptions,
72
+ } from "./playground/project"
89
73
  export { resolveAsset, setAssetBase } from "./assets"
90
74
  export { createBundledSourceClient, createViteSourceClient, SourcePreview } from "./source"
91
75
  export type {
@@ -108,8 +92,6 @@ export {
108
92
  serializeInline,
109
93
  serializeReference,
110
94
  } from "./gitbook/inline"
111
- export { Streamdown } from "streamdown"
112
- export type { StreamdownProps } from "streamdown"
113
95
  export type * from "./gitbook/ast"
114
96
  export { PACKAGE_MANAGERS, packageManagerCommands } from "./gitbook/package-managers"
115
97
  export { flattenBlocks, flattenForPlainMarkdown } from "./gitbook/flatten"
@@ -1,73 +1,40 @@
1
- import { useContext, useMemo } from "react"
1
+ import { useContext } from "react"
2
2
 
3
- import { DocsRenderer, type DocRenderOptions, type LivePreviewRenderer, type SourceReferenceRenderer } from "../docs/DocsRenderer"
4
- import { parseMarkdown, trimPartialInlineToken } from "../gitbook/parse"
3
+ import type { LivePreviewRenderer } from "../docs/DocsRenderer"
5
4
  import { DocstreamDemoRuntimeContext } from "../demo/context"
5
+ import { GitbookStreamdown, type GitbookStreamdownProps } from "../streamdown"
6
6
  import { almostNodeDemoRuntime } from "./InlineDemoPreview"
7
7
  import { ReactCodePreview } from "./ReactCodePreview"
8
8
 
9
- export interface PlaygroundStreamdownProps extends DocRenderOptions {
10
- children?: string
11
- sourceRenderer?: SourceReferenceRenderer
12
- className?: string
13
- isAnimating?: boolean
14
- isStreaming?: boolean
15
- markdown?: string
16
- }
9
+ export type PlaygroundStreamdownProps = GitbookStreamdownProps
17
10
 
18
11
  /**
19
- * The full renderer variant. It keeps the optional almost-node playground
20
- * behind an explicit import boundary; markdown-only consumers should use the
21
- * `@brett_lamy/docstream/streamdown` entry instead. Live code fences and
22
- * `{% demo %}` blocks with inline files (and no resolver) run in almost-node.
12
+ * A `LivePreviewRenderer` that runs `live` React/JSX/TSX code fences in almost-node.
13
+ * Pass it as `liveRenderer` to the root `GitbookStreamdown` / `DocsRenderer` (or the editor).
23
14
  */
24
- export function PlaygroundStreamdown({
25
- children,
26
- className,
27
- isAnimating,
28
- isStreaming = false,
29
- markdown,
30
- sourceRenderer,
31
- demoResolver,
32
- demoRuntime,
33
- demoDependencies,
34
- pageActions,
35
- }: PlaygroundStreamdownProps) {
36
- const content = markdown ?? children ?? ""
37
- // Inline-file demos run in almost-node unless the host (or an outer renderer) chose a runtime.
38
- const inheritedRuntime = useContext(DocstreamDemoRuntimeContext).runtime
39
- const runtime = demoRuntime ?? inheritedRuntime ?? almostNodeDemoRuntime
40
- const doc = useMemo(
41
- () => parseMarkdown(isStreaming ? trimPartialInlineToken(content) : content),
42
- [content, isStreaming]
43
- )
44
- const liveRenderer: LivePreviewRenderer = ({ files, entry, title, collapsedCodeLines, expandedCodeLines }) => (
45
- <ReactCodePreview
46
- files={files}
47
- entry={entry}
48
- title={title}
49
- collapsedCodeLines={collapsedCodeLines}
50
- expandedCodeLines={expandedCodeLines}
51
- />
52
- )
15
+ export const almostNodeLiveRenderer: LivePreviewRenderer = ({ files, entry, title, collapsedCodeLines, expandedCodeLines }) => (
16
+ <ReactCodePreview
17
+ files={files}
18
+ entry={entry}
19
+ title={title}
20
+ collapsedCodeLines={collapsedCodeLines}
21
+ expandedCodeLines={expandedCodeLines}
22
+ />
23
+ )
53
24
 
25
+ /**
26
+ * `GitbookStreamdown` with the almost-node playground wired in: live code fences and
27
+ * `{% demo %}` blocks with inline files (and no resolver) run in almost-node
28
+ * (`@agent-wasm/core`, an optional peer dependency — install it to use this entry).
29
+ * Explicit `liveRenderer` / `demoRuntime` props (or an outer runtime context) still win.
30
+ */
31
+ export function PlaygroundStreamdown({ liveRenderer, demoRuntime, ...props }: PlaygroundStreamdownProps) {
32
+ const inheritedRuntime = useContext(DocstreamDemoRuntimeContext).runtime
54
33
  return (
55
- <div
56
- aria-busy={isAnimating ?? isStreaming}
57
- className={className}
58
- data-docstream=""
59
- data-streaming={isStreaming ? "" : undefined}
60
- >
61
- <DocsRenderer
62
- doc={doc}
63
- markdown={content}
64
- liveRenderer={liveRenderer}
65
- sourceRenderer={sourceRenderer}
66
- demoResolver={demoResolver}
67
- demoRuntime={runtime}
68
- demoDependencies={demoDependencies}
69
- pageActions={isStreaming ? false : pageActions}
70
- />
71
- </div>
34
+ <GitbookStreamdown
35
+ {...props}
36
+ liveRenderer={liveRenderer ?? almostNodeLiveRenderer}
37
+ demoRuntime={demoRuntime ?? inheritedRuntime ?? almostNodeDemoRuntime}
38
+ />
72
39
  )
73
40
  }
@@ -1,20 +1,13 @@
1
1
  import type { ContainerInstance, ContainerOptions, VirtualFS } from "@agent-wasm/core"
2
+ import {
3
+ normalizeAlmostNodeFiles,
4
+ normalizeAlmostNodePath,
5
+ type AlmostNodeFileContent,
6
+ type AlmostNodeFiles,
7
+ type AlmostNodeFilesystemOptions,
8
+ } from "./project"
2
9
 
3
- export type AlmostNodeFileContent = string | Uint8Array
4
-
5
- export interface AlmostNodeFile {
6
- path: string
7
- content: AlmostNodeFileContent
8
- }
9
-
10
- export type AlmostNodeFiles =
11
- | Readonly<Record<string, AlmostNodeFileContent>>
12
- | readonly AlmostNodeFile[]
13
-
14
- export interface AlmostNodeFilesystemOptions {
15
- /** Prefix for relative file paths. Defaults to the virtual root. */
16
- root?: string
17
- }
10
+ export * from "./project"
18
11
 
19
12
  export interface AlmostNodeWorkspaceOptions extends Omit<ContainerOptions, "cwd"> {
20
13
  /** Project root used as the container cwd and Vite root. */
@@ -44,49 +37,6 @@ async function loadAlmostNode(): Promise<AlmostNodeModule> {
44
37
  }
45
38
  }
46
39
 
47
- export function normalizeAlmostNodePath(path: string, root = "/"): string {
48
- const normalizedRoot = normalizeRoot(root)
49
- const source = path.replaceAll("\\", "/")
50
- const absolute = source.startsWith("/") ? source : `${normalizedRoot}/${source}`
51
- const result = normalizeAbsolutePath(absolute)
52
-
53
- if (normalizedRoot !== "/" && result !== normalizedRoot && !result.startsWith(`${normalizedRoot}/`)) {
54
- throw new Error(`File path escapes the almost-node root: ${path}`)
55
- }
56
- return result
57
- }
58
-
59
- function normalizeAbsolutePath(path: string): string {
60
- const parts: string[] = []
61
-
62
- for (const part of path.split("/")) {
63
- if (!part || part === ".") continue
64
- if (part === "..") {
65
- if (parts.length > 0) parts.pop()
66
- continue
67
- }
68
- if (part.includes("\0")) throw new Error("almost-node file paths cannot contain null bytes")
69
- parts.push(part)
70
- }
71
-
72
- const result = `/${parts.join("/")}`
73
- return result === "" ? "/" : result
74
- }
75
-
76
- export function normalizeAlmostNodeFiles(files: AlmostNodeFiles): AlmostNodeFile[] {
77
- if (Array.isArray(files)) return files.map((file) => ({ ...file }))
78
- return Object.entries(files).map(([path, content]) => ({ path, content }))
79
- }
80
-
81
- export function toAlmostNodeFileMap(files: AlmostNodeFiles): Record<string, AlmostNodeFileContent> {
82
- return Object.fromEntries(normalizeAlmostNodeFiles(files).map(({ path, content }) => [path, content]))
83
- }
84
-
85
- function normalizeRoot(root: string): string {
86
- const normalized = normalizeAbsolutePath(root.replaceAll("\\", "/"))
87
- return normalized === "/" ? normalized : normalized.replace(/\/$/, "")
88
- }
89
-
90
40
  function parentDirectory(path: string): string {
91
41
  const index = path.lastIndexOf("/")
92
42
  return index <= 0 ? "/" : path.slice(0, index)
@@ -110,7 +60,7 @@ export async function createAlmostNodeFilesystem(
110
60
  options: AlmostNodeFilesystemOptions = {},
111
61
  ): Promise<VirtualFS> {
112
62
  const { VirtualFS } = await loadAlmostNode()
113
- const root = normalizeRoot(options.root ?? "/")
63
+ const root = normalizeAlmostNodePath(options.root ?? "/")
114
64
  const vfs = new VirtualFS()
115
65
  writeFilesToVfs(vfs, files, root)
116
66
  return vfs
@@ -125,7 +75,7 @@ export async function createAlmostNodeWorkspace(
125
75
  options: AlmostNodeWorkspaceOptions = {},
126
76
  ): Promise<AlmostNodeWorkspace> {
127
77
  const { root: requestedRoot, ...containerOptions } = options
128
- const root = normalizeRoot(requestedRoot ?? "/")
78
+ const root = normalizeAlmostNodePath(requestedRoot ?? "/")
129
79
  const { createContainer } = await loadAlmostNode()
130
80
  const container = createContainer({ ...containerOptions, cwd: root })
131
81
 
@@ -152,133 +102,3 @@ export async function createAlmostNodeWorkspace(
152
102
  }
153
103
  }
154
104
 
155
- export interface ReactDemoFilesOptions {
156
- entry?: string
157
- /** Extra npm dependencies for the generated package.json (ignored when the files bring one). */
158
- dependencies?: Record<string, string>
159
- }
160
-
161
- /**
162
- * Add the tiny Vite/React shell needed by a file map that only contains source
163
- * files. Existing package.json and index.html files are always respected.
164
- */
165
- export function createReactDemoFiles(
166
- files: AlmostNodeFiles,
167
- options: ReactDemoFilesOptions = {},
168
- ): Record<string, AlmostNodeFileContent> {
169
- const map = toAlmostNodeFileMap(files)
170
- const entry = normalizeAlmostNodePath(options.entry ?? "/src/main.jsx")
171
- const has = (candidate: string) => Object.keys(map).some((path) => normalizeAlmostNodePath(path) === candidate)
172
-
173
- if (!has("/package.json")) {
174
- map["/package.json"] = JSON.stringify(
175
- {
176
- name: "docstream-react-demo",
177
- private: true,
178
- type: "module",
179
- dependencies: { react: "^19.0.0", "react-dom": "^19.0.0", ...options.dependencies },
180
- },
181
- null,
182
- 2,
183
- )
184
- }
185
-
186
- if (!has("/index.html")) {
187
- const entryUrl = `.${entry}`
188
- map["/index.html"] = `<!doctype html>
189
- <html lang="en">
190
- <head>
191
- <meta charset="UTF-8" />
192
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
193
- <title>Docstream React demo</title>
194
- </head>
195
- <body>
196
- <div id="root"></div>
197
- <script type="module" src="${entryUrl}"></script>
198
- </body>
199
- </html>
200
- `
201
- }
202
-
203
- return map
204
- }
205
-
206
- export interface InlineDemoProjectOptions {
207
- /** Entry file among `files` (relative path). Defaults to the first file. */
208
- entry?: string
209
- /** Extra npm dependencies (merged over a `package.json` among the files, if any). */
210
- dependencies?: Record<string, string>
211
- }
212
-
213
- export interface InlineDemoProject {
214
- /** Complete almost-node project: the demo under `/src/demo/`, a wrapper entry, package.json, index.html. */
215
- files: Record<string, AlmostNodeFileContent>
216
- /** The Vite entry to hand to `createReactDemoFiles` / `ReactDemo`. */
217
- entry: string
218
- /** Whether the entry is wrapped (so `?variant=` reaches the component as its `variant` prop). */
219
- wrapped: boolean
220
- }
221
-
222
- /** Folder the inline demo's files are written to inside the almost-node project. */
223
- export const INLINE_DEMO_DIR = "/src/demo"
224
- const WRAPPER_ENTRY = "/src/__docstream_demo_entry.js"
225
-
226
- function selfMounting(source: string): boolean {
227
- return /\bcreateRoot\s*\(|\bReactDOM\.render\s*\(|\bhydrateRoot\s*\(/.test(source)
228
- }
229
-
230
- /**
231
- * Turn a `{% demo %}` block's inline files into a runnable almost-node project.
232
- *
233
- * The files go under `/src/demo/` (relative imports between them keep working). A tiny
234
- * wrapper entry imports the entry file, takes its default export (or the first exported
235
- * function), and renders it with `variant` read from the page's `?variant=` query — so
236
- * switching variants just reloads the iframe, no new Vite server. An entry that mounts
237
- * itself (`createRoot(…)`) is used directly instead, and receives no `variant`.
238
- *
239
- * A `package.json` among the files contributes its `dependencies`; `options.dependencies`
240
- * are merged over them. Bare imports resolve through almost-node (installed packages, else
241
- * esm.sh at the major version declared here), with `react`/`react-dom` provided.
242
- */
243
- export function createInlineDemoProject(
244
- files: readonly { path: string; content: string }[],
245
- options: InlineDemoProjectOptions = {},
246
- ): InlineDemoProject {
247
- const map: Record<string, AlmostNodeFileContent> = {}
248
- let packageDeps: Record<string, string> = {}
249
- for (const file of files) {
250
- const relative = file.path.replace(/^\.?\/+/, "")
251
- if (relative === "package.json") {
252
- try {
253
- const pkg = JSON.parse(file.content) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
254
- packageDeps = { ...pkg.devDependencies, ...pkg.dependencies }
255
- } catch {
256
- // Not JSON: keep it as a plain file.
257
- }
258
- }
259
- map[normalizeAlmostNodePath(relative, INLINE_DEMO_DIR)] = file.content
260
- }
261
- const entryFile = files.find((f) => f.path === options.entry) ?? files[0]
262
- if (!entryFile) throw new Error("An inline demo needs at least one file")
263
- const entryPath = normalizeAlmostNodePath(entryFile.path.replace(/^\.?\/+/, ""), INLINE_DEMO_DIR)
264
- const dependencies = { ...packageDeps, ...options.dependencies }
265
- const wrapped = !selfMounting(entryFile.content)
266
- if (wrapped) {
267
- const specifier = `.${entryPath.slice("/src".length)}`
268
- map[WRAPPER_ENTRY] = `import { createElement } from "react"
269
- import { createRoot } from "react-dom/client"
270
- import * as demo from ${JSON.stringify(specifier)}
271
-
272
- const Component = demo.default ?? Object.values(demo).find((value) => typeof value === "function")
273
- const variant = new URLSearchParams(location.search).get("variant")
274
- const props = variant === null ? {} : { variant }
275
- createRoot(document.getElementById("root")).render(
276
- Component
277
- ? createElement(Component, props)
278
- : createElement("pre", null, ${JSON.stringify(`${entryFile.path} has no default export to render.`)}),
279
- )
280
- `
281
- }
282
- const entry = wrapped ? WRAPPER_ENTRY : entryPath
283
- return { files: createReactDemoFiles(map, { entry, dependencies }), entry, wrapped }
284
- }
@@ -1,3 +1,7 @@
1
+ /* `@brett_lamy/docstream/playground` — the only entry that touches almost-node
2
+ (`@agent-wasm/core`, an optional peer dependency). Install it to use anything here. */
3
+ export { PlaygroundStreamdown, PlaygroundStreamdown as GitbookStreamdown, almostNodeLiveRenderer } from "./PlaygroundStreamdown"
4
+ export type { PlaygroundStreamdownProps, PlaygroundStreamdownProps as GitbookStreamdownProps } from "./PlaygroundStreamdown"
1
5
  export { ReactCodePreview, ReactDemo, useAlmostNodeServer } from "./ReactCodePreview"
2
6
  export type { AlmostNodeServerOptions, AlmostNodeServerState, PreviewState, ReactCodePreviewProps, ReactDemoProps } from "./ReactCodePreview"
3
7
  export { InlineDemoPreview, almostNodeDemoRuntime, createAlmostNodeDemoRuntime } from "./InlineDemoPreview"
@@ -0,0 +1,194 @@
1
+ /* Pure, runtime-free helpers for describing an almost-node project as a file map.
2
+ Nothing here imports `@agent-wasm/core`, so these are safe to use from the package
3
+ root; the runtime itself lives in ./filesystem (the `/playground` entry). */
4
+
5
+ export type AlmostNodeFileContent = string | Uint8Array
6
+
7
+ export interface AlmostNodeFile {
8
+ path: string
9
+ content: AlmostNodeFileContent
10
+ }
11
+
12
+ export type AlmostNodeFiles =
13
+ | Readonly<Record<string, AlmostNodeFileContent>>
14
+ | readonly AlmostNodeFile[]
15
+
16
+ export interface AlmostNodeFilesystemOptions {
17
+ /** Prefix for relative file paths. Defaults to the virtual root. */
18
+ root?: string
19
+ }
20
+
21
+ export function normalizeAlmostNodePath(path: string, root = "/"): string {
22
+ const normalizedRoot = normalizeRoot(root)
23
+ const source = path.replaceAll("\\", "/")
24
+ const absolute = source.startsWith("/") ? source : `${normalizedRoot}/${source}`
25
+ const result = normalizeAbsolutePath(absolute)
26
+
27
+ if (normalizedRoot !== "/" && result !== normalizedRoot && !result.startsWith(`${normalizedRoot}/`)) {
28
+ throw new Error(`File path escapes the almost-node root: ${path}`)
29
+ }
30
+ return result
31
+ }
32
+
33
+ function normalizeAbsolutePath(path: string): string {
34
+ const parts: string[] = []
35
+
36
+ for (const part of path.split("/")) {
37
+ if (!part || part === ".") continue
38
+ if (part === "..") {
39
+ if (parts.length > 0) parts.pop()
40
+ continue
41
+ }
42
+ if (part.includes("\0")) throw new Error("almost-node file paths cannot contain null bytes")
43
+ parts.push(part)
44
+ }
45
+
46
+ const result = `/${parts.join("/")}`
47
+ return result === "" ? "/" : result
48
+ }
49
+
50
+ export function normalizeAlmostNodeFiles(files: AlmostNodeFiles): AlmostNodeFile[] {
51
+ if (Array.isArray(files)) return files.map((file) => ({ ...file }))
52
+ return Object.entries(files).map(([path, content]) => ({ path, content }))
53
+ }
54
+
55
+ export function toAlmostNodeFileMap(files: AlmostNodeFiles): Record<string, AlmostNodeFileContent> {
56
+ return Object.fromEntries(normalizeAlmostNodeFiles(files).map(({ path, content }) => [path, content]))
57
+ }
58
+
59
+ function normalizeRoot(root: string): string {
60
+ const normalized = normalizeAbsolutePath(root.replaceAll("\\", "/"))
61
+ return normalized === "/" ? normalized : normalized.replace(/\/$/, "")
62
+ }
63
+
64
+
65
+ export interface ReactDemoFilesOptions {
66
+ entry?: string
67
+ /** Extra npm dependencies for the generated package.json (ignored when the files bring one). */
68
+ dependencies?: Record<string, string>
69
+ }
70
+
71
+ /**
72
+ * Add the tiny Vite/React shell needed by a file map that only contains source
73
+ * files. Existing package.json and index.html files are always respected.
74
+ */
75
+ export function createReactDemoFiles(
76
+ files: AlmostNodeFiles,
77
+ options: ReactDemoFilesOptions = {},
78
+ ): Record<string, AlmostNodeFileContent> {
79
+ const map = toAlmostNodeFileMap(files)
80
+ const entry = normalizeAlmostNodePath(options.entry ?? "/src/main.jsx")
81
+ const has = (candidate: string) => Object.keys(map).some((path) => normalizeAlmostNodePath(path) === candidate)
82
+
83
+ if (!has("/package.json")) {
84
+ map["/package.json"] = JSON.stringify(
85
+ {
86
+ name: "docstream-react-demo",
87
+ private: true,
88
+ type: "module",
89
+ dependencies: { react: "^19.0.0", "react-dom": "^19.0.0", ...options.dependencies },
90
+ },
91
+ null,
92
+ 2,
93
+ )
94
+ }
95
+
96
+ if (!has("/index.html")) {
97
+ const entryUrl = `.${entry}`
98
+ map["/index.html"] = `<!doctype html>
99
+ <html lang="en">
100
+ <head>
101
+ <meta charset="UTF-8" />
102
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
103
+ <title>Docstream React demo</title>
104
+ </head>
105
+ <body>
106
+ <div id="root"></div>
107
+ <script type="module" src="${entryUrl}"></script>
108
+ </body>
109
+ </html>
110
+ `
111
+ }
112
+
113
+ return map
114
+ }
115
+
116
+ export interface InlineDemoProjectOptions {
117
+ /** Entry file among `files` (relative path). Defaults to the first file. */
118
+ entry?: string
119
+ /** Extra npm dependencies (merged over a `package.json` among the files, if any). */
120
+ dependencies?: Record<string, string>
121
+ }
122
+
123
+ export interface InlineDemoProject {
124
+ /** Complete almost-node project: the demo under `/src/demo/`, a wrapper entry, package.json, index.html. */
125
+ files: Record<string, AlmostNodeFileContent>
126
+ /** The Vite entry to hand to `createReactDemoFiles` / `ReactDemo`. */
127
+ entry: string
128
+ /** Whether the entry is wrapped (so `?variant=` reaches the component as its `variant` prop). */
129
+ wrapped: boolean
130
+ }
131
+
132
+ /** Folder the inline demo's files are written to inside the almost-node project. */
133
+ export const INLINE_DEMO_DIR = "/src/demo"
134
+ const WRAPPER_ENTRY = "/src/__docstream_demo_entry.js"
135
+
136
+ function selfMounting(source: string): boolean {
137
+ return /\bcreateRoot\s*\(|\bReactDOM\.render\s*\(|\bhydrateRoot\s*\(/.test(source)
138
+ }
139
+
140
+ /**
141
+ * Turn a `{% demo %}` block's inline files into a runnable almost-node project.
142
+ *
143
+ * The files go under `/src/demo/` (relative imports between them keep working). A tiny
144
+ * wrapper entry imports the entry file, takes its default export (or the first exported
145
+ * function), and renders it with `variant` read from the page's `?variant=` query — so
146
+ * switching variants just reloads the iframe, no new Vite server. An entry that mounts
147
+ * itself (`createRoot(…)`) is used directly instead, and receives no `variant`.
148
+ *
149
+ * A `package.json` among the files contributes its `dependencies`; `options.dependencies`
150
+ * are merged over them. Bare imports resolve through almost-node (installed packages, else
151
+ * esm.sh at the major version declared here), with `react`/`react-dom` provided.
152
+ */
153
+ export function createInlineDemoProject(
154
+ files: readonly { path: string; content: string }[],
155
+ options: InlineDemoProjectOptions = {},
156
+ ): InlineDemoProject {
157
+ const map: Record<string, AlmostNodeFileContent> = {}
158
+ let packageDeps: Record<string, string> = {}
159
+ for (const file of files) {
160
+ const relative = file.path.replace(/^\.?\/+/, "")
161
+ if (relative === "package.json") {
162
+ try {
163
+ const pkg = JSON.parse(file.content) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
164
+ packageDeps = { ...pkg.devDependencies, ...pkg.dependencies }
165
+ } catch {
166
+ // Not JSON: keep it as a plain file.
167
+ }
168
+ }
169
+ map[normalizeAlmostNodePath(relative, INLINE_DEMO_DIR)] = file.content
170
+ }
171
+ const entryFile = files.find((f) => f.path === options.entry) ?? files[0]
172
+ if (!entryFile) throw new Error("An inline demo needs at least one file")
173
+ const entryPath = normalizeAlmostNodePath(entryFile.path.replace(/^\.?\/+/, ""), INLINE_DEMO_DIR)
174
+ const dependencies = { ...packageDeps, ...options.dependencies }
175
+ const wrapped = !selfMounting(entryFile.content)
176
+ if (wrapped) {
177
+ const specifier = `.${entryPath.slice("/src".length)}`
178
+ map[WRAPPER_ENTRY] = `import { createElement } from "react"
179
+ import { createRoot } from "react-dom/client"
180
+ import * as demo from ${JSON.stringify(specifier)}
181
+
182
+ const Component = demo.default ?? Object.values(demo).find((value) => typeof value === "function")
183
+ const variant = new URLSearchParams(location.search).get("variant")
184
+ const props = variant === null ? {} : { variant }
185
+ createRoot(document.getElementById("root")).render(
186
+ Component
187
+ ? createElement(Component, props)
188
+ : createElement("pre", null, ${JSON.stringify(`${entryFile.path} has no default export to render.`)}),
189
+ )
190
+ `
191
+ }
192
+ const entry = wrapped ? WRAPPER_ENTRY : entryPath
193
+ return { files: createReactDemoFiles(map, { entry, dependencies }), entry, wrapped }
194
+ }