poveste 0.1.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 (251) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +11 -0
  3. package/bin.mjs +4 -0
  4. package/client.d.ts +19 -0
  5. package/client.js +15 -0
  6. package/dist/controls.d.ts +2 -0
  7. package/dist/controls.d.ts.map +1 -0
  8. package/dist/controls.js +1 -0
  9. package/dist/node/alias.d.ts +3 -0
  10. package/dist/node/alias.d.ts.map +1 -0
  11. package/dist/node/alias.js +5 -0
  12. package/dist/node/bin.d.ts +2 -0
  13. package/dist/node/bin.d.ts.map +1 -0
  14. package/dist/node/bin.js +35 -0
  15. package/dist/node/build-serialize.d.ts +22 -0
  16. package/dist/node/build-serialize.d.ts.map +1 -0
  17. package/dist/node/build-serialize.js +36 -0
  18. package/dist/node/build.d.ts +3 -0
  19. package/dist/node/build.d.ts.map +1 -0
  20. package/dist/node/build.js +228 -0
  21. package/dist/node/builtin-plugins/pinceau-tokens.d.ts +7 -0
  22. package/dist/node/builtin-plugins/pinceau-tokens.d.ts.map +1 -0
  23. package/dist/node/builtin-plugins/pinceau-tokens.js +505 -0
  24. package/dist/node/builtin-plugins/tailwind-tokens.d.ts +6 -0
  25. package/dist/node/builtin-plugins/tailwind-tokens.d.ts.map +1 -0
  26. package/dist/node/builtin-plugins/tailwind-tokens.js +523 -0
  27. package/dist/node/builtin-plugins/vanilla-support/MountStory.d.ts +15 -0
  28. package/dist/node/builtin-plugins/vanilla-support/MountStory.d.ts.map +1 -0
  29. package/dist/node/builtin-plugins/vanilla-support/MountStory.js +39 -0
  30. package/dist/node/builtin-plugins/vanilla-support/RenderStory.d.ts +39 -0
  31. package/dist/node/builtin-plugins/vanilla-support/RenderStory.d.ts.map +1 -0
  32. package/dist/node/builtin-plugins/vanilla-support/RenderStory.js +129 -0
  33. package/dist/node/builtin-plugins/vanilla-support/client.d.ts +4 -0
  34. package/dist/node/builtin-plugins/vanilla-support/client.d.ts.map +1 -0
  35. package/dist/node/builtin-plugins/vanilla-support/client.js +5 -0
  36. package/dist/node/builtin-plugins/vanilla-support/collect.d.ts +3 -0
  37. package/dist/node/builtin-plugins/vanilla-support/collect.d.ts.map +1 -0
  38. package/dist/node/builtin-plugins/vanilla-support/collect.js +33 -0
  39. package/dist/node/builtin-plugins/vanilla-support/plugin.d.ts +3 -0
  40. package/dist/node/builtin-plugins/vanilla-support/plugin.d.ts.map +1 -0
  41. package/dist/node/builtin-plugins/vanilla-support/plugin.js +29 -0
  42. package/dist/node/builtin-plugins/vanilla-support/types.d.ts +36 -0
  43. package/dist/node/builtin-plugins/vanilla-support/types.d.ts.map +1 -0
  44. package/dist/node/builtin-plugins/vanilla-support/types.js +3 -0
  45. package/dist/node/builtin-plugins/vanilla-support/vanilla.story.d.ts +4 -0
  46. package/dist/node/builtin-plugins/vanilla-support/vanilla.story.d.ts.map +1 -0
  47. package/dist/node/builtin-plugins/vanilla-support/vanilla.story.js +55 -0
  48. package/dist/node/collect/index.d.ts +14 -0
  49. package/dist/node/collect/index.d.ts.map +1 -0
  50. package/dist/node/collect/index.js +129 -0
  51. package/dist/node/collect/run.d.ts +3 -0
  52. package/dist/node/collect/run.d.ts.map +1 -0
  53. package/dist/node/collect/run.js +7 -0
  54. package/dist/node/collect/worker.d.ts +14 -0
  55. package/dist/node/collect/worker.d.ts.map +1 -0
  56. package/dist/node/collect/worker.js +64 -0
  57. package/dist/node/colors.d.ts +287 -0
  58. package/dist/node/colors.d.ts.map +1 -0
  59. package/dist/node/colors.js +324 -0
  60. package/dist/node/commands/build.d.ts +5 -0
  61. package/dist/node/commands/build.d.ts.map +1 -0
  62. package/dist/node/commands/build.js +9 -0
  63. package/dist/node/commands/dev.d.ts +8 -0
  64. package/dist/node/commands/dev.d.ts.map +1 -0
  65. package/dist/node/commands/dev.js +52 -0
  66. package/dist/node/commands/preview.d.ts +5 -0
  67. package/dist/node/commands/preview.d.ts.map +1 -0
  68. package/dist/node/commands/preview.js +15 -0
  69. package/dist/node/config.d.ts +40 -0
  70. package/dist/node/config.d.ts.map +1 -0
  71. package/dist/node/config.js +330 -0
  72. package/dist/node/context.d.ts +18 -0
  73. package/dist/node/context.d.ts.map +1 -0
  74. package/dist/node/context.js +28 -0
  75. package/dist/node/dom/dom-keys.d.ts +5 -0
  76. package/dist/node/dom/dom-keys.d.ts.map +1 -0
  77. package/dist/node/dom/dom-keys.js +233 -0
  78. package/dist/node/dom/env.d.ts +5 -0
  79. package/dist/node/dom/env.d.ts.map +1 -0
  80. package/dist/node/dom/env.js +47 -0
  81. package/dist/node/dom/util.d.ts +11 -0
  82. package/dist/node/dom/util.d.ts.map +1 -0
  83. package/dist/node/dom/util.js +65 -0
  84. package/dist/node/index.d.ts +12 -0
  85. package/dist/node/index.d.ts.map +1 -0
  86. package/dist/node/index.js +4 -0
  87. package/dist/node/load.d.ts +9 -0
  88. package/dist/node/load.d.ts.map +1 -0
  89. package/dist/node/load.js +42 -0
  90. package/dist/node/markdown.d.ts +11 -0
  91. package/dist/node/markdown.d.ts.map +1 -0
  92. package/dist/node/markdown.js +228 -0
  93. package/dist/node/plugin.d.ts +42 -0
  94. package/dist/node/plugin.d.ts.map +1 -0
  95. package/dist/node/plugin.js +67 -0
  96. package/dist/node/preview.d.ts +8 -0
  97. package/dist/node/preview.d.ts.map +1 -0
  98. package/dist/node/preview.js +41 -0
  99. package/dist/node/search.d.ts +17 -0
  100. package/dist/node/search.d.ts.map +1 -0
  101. package/dist/node/search.js +72 -0
  102. package/dist/node/server.d.ts +12 -0
  103. package/dist/node/server.d.ts.map +1 -0
  104. package/dist/node/server.js +189 -0
  105. package/dist/node/stories.d.ts +22 -0
  106. package/dist/node/stories.d.ts.map +1 -0
  107. package/dist/node/stories.js +125 -0
  108. package/dist/node/tree.d.ts +4 -0
  109. package/dist/node/tree.d.ts.map +1 -0
  110. package/dist/node/tree.js +117 -0
  111. package/dist/node/util/find-up.d.ts +2 -0
  112. package/dist/node/util/find-up.d.ts.map +1 -0
  113. package/dist/node/util/find-up.js +20 -0
  114. package/dist/node/util/fs.d.ts +2 -0
  115. package/dist/node/util/fs.d.ts.map +1 -0
  116. package/dist/node/util/fs.js +1 -0
  117. package/dist/node/util/log.d.ts +2 -0
  118. package/dist/node/util/log.d.ts.map +1 -0
  119. package/dist/node/util/log.js +9 -0
  120. package/dist/node/util/slugify.d.ts +2 -0
  121. package/dist/node/util/slugify.d.ts.map +1 -0
  122. package/dist/node/util/slugify.js +22 -0
  123. package/dist/node/util/vendors.d.ts +3 -0
  124. package/dist/node/util/vendors.d.ts.map +1 -0
  125. package/dist/node/util/vendors.js +22 -0
  126. package/dist/node/vendors/controls.d.ts +2 -0
  127. package/dist/node/vendors/controls.d.ts.map +1 -0
  128. package/dist/node/vendors/controls.js +1 -0
  129. package/dist/node/vendors/vue.d.ts +2 -0
  130. package/dist/node/vendors/vue.d.ts.map +1 -0
  131. package/dist/node/vendors/vue.js +1 -0
  132. package/dist/node/virtual/index.d.ts +38 -0
  133. package/dist/node/virtual/index.d.ts.map +1 -0
  134. package/dist/node/virtual/index.js +49 -0
  135. package/dist/node/virtual/markdown.d.ts +3 -0
  136. package/dist/node/virtual/markdown.d.ts.map +1 -0
  137. package/dist/node/virtual/markdown.js +19 -0
  138. package/dist/node/virtual/noop.d.ts +2 -0
  139. package/dist/node/virtual/noop.d.ts.map +1 -0
  140. package/dist/node/virtual/noop.js +1 -0
  141. package/dist/node/virtual/resolved-commands.d.ts +3 -0
  142. package/dist/node/virtual/resolved-commands.d.ts.map +1 -0
  143. package/dist/node/virtual/resolved-commands.js +32 -0
  144. package/dist/node/virtual/resolved-config.d.ts +3 -0
  145. package/dist/node/virtual/resolved-config.d.ts.map +1 -0
  146. package/dist/node/virtual/resolved-config.js +10 -0
  147. package/dist/node/virtual/resolved-generated-global-setup.d.ts +3 -0
  148. package/dist/node/virtual/resolved-generated-global-setup.d.ts.map +1 -0
  149. package/dist/node/virtual/resolved-generated-global-setup.js +23 -0
  150. package/dist/node/virtual/resolved-generated-setup-code.d.ts +3 -0
  151. package/dist/node/virtual/resolved-generated-setup-code.d.ts.map +1 -0
  152. package/dist/node/virtual/resolved-generated-setup-code.js +5 -0
  153. package/dist/node/virtual/resolved-markdown-files.d.ts +3 -0
  154. package/dist/node/virtual/resolved-markdown-files.d.ts.map +1 -0
  155. package/dist/node/virtual/resolved-markdown-files.js +17 -0
  156. package/dist/node/virtual/resolved-stories.d.ts +3 -0
  157. package/dist/node/virtual/resolved-stories.d.ts.map +1 -0
  158. package/dist/node/virtual/resolved-stories.js +42 -0
  159. package/dist/node/virtual/resolved-support-plugins-client.d.ts +3 -0
  160. package/dist/node/virtual/resolved-support-plugins-client.d.ts.map +1 -0
  161. package/dist/node/virtual/resolved-support-plugins-client.js +11 -0
  162. package/dist/node/virtual/resolved-support-plugins-collect.d.ts +3 -0
  163. package/dist/node/virtual/resolved-support-plugins-collect.d.ts.map +1 -0
  164. package/dist/node/virtual/resolved-support-plugins-collect.js +11 -0
  165. package/dist/node/virtual/resolved-theme.d.ts +3 -0
  166. package/dist/node/virtual/resolved-theme.d.ts.map +1 -0
  167. package/dist/node/virtual/resolved-theme.js +12 -0
  168. package/dist/node/virtual/story-source.d.ts +3 -0
  169. package/dist/node/virtual/story-source.d.ts.map +1 -0
  170. package/dist/node/virtual/story-source.js +16 -0
  171. package/dist/node/virtual/story.d.ts +3 -0
  172. package/dist/node/virtual/story.d.ts.map +1 -0
  173. package/dist/node/virtual/story.js +7 -0
  174. package/dist/node/virtual/util.d.ts +3 -0
  175. package/dist/node/virtual/util.d.ts.map +1 -0
  176. package/dist/node/virtual/util.js +4 -0
  177. package/dist/node/virtual/vite-plugin.d.ts +4 -0
  178. package/dist/node/virtual/vite-plugin.d.ts.map +1 -0
  179. package/dist/node/virtual/vite-plugin.js +126 -0
  180. package/dist/node/vite.d.ts +9 -0
  181. package/dist/node/vite.d.ts.map +1 -0
  182. package/dist/node/vite.js +337 -0
  183. package/index.d.ts +1 -0
  184. package/package.json +89 -0
  185. package/plugin.d.ts +3 -0
  186. package/plugin.js +11 -0
  187. package/src/controls.ts +1 -0
  188. package/src/node/__tests__/markdown/test1.story.md +7 -0
  189. package/src/node/__tests__/markdown/test2.story.md +5 -0
  190. package/src/node/__tests__/markdown.spec.ts +61 -0
  191. package/src/node/__tests__/tree.spec.ts +109 -0
  192. package/src/node/alias.ts +8 -0
  193. package/src/node/bin.ts +43 -0
  194. package/src/node/build-serialize.ts +62 -0
  195. package/src/node/build.ts +283 -0
  196. package/src/node/builtin-plugins/pinceau-tokens.ts +521 -0
  197. package/src/node/builtin-plugins/tailwind-tokens.ts +536 -0
  198. package/src/node/builtin-plugins/vanilla-support/MountStory.ts +49 -0
  199. package/src/node/builtin-plugins/vanilla-support/RenderStory.ts +164 -0
  200. package/src/node/builtin-plugins/vanilla-support/client.ts +6 -0
  201. package/src/node/builtin-plugins/vanilla-support/collect.ts +43 -0
  202. package/src/node/builtin-plugins/vanilla-support/plugin.ts +35 -0
  203. package/src/node/builtin-plugins/vanilla-support/types.ts +40 -0
  204. package/src/node/builtin-plugins/vanilla-support/vanilla.story.ts +63 -0
  205. package/src/node/collect/index.ts +160 -0
  206. package/src/node/collect/run.ts +9 -0
  207. package/src/node/collect/worker.ts +97 -0
  208. package/src/node/colors.ts +340 -0
  209. package/src/node/commands/build.ts +14 -0
  210. package/src/node/commands/dev.ts +67 -0
  211. package/src/node/commands/preview.ts +22 -0
  212. package/src/node/config.ts +385 -0
  213. package/src/node/context.ts +60 -0
  214. package/src/node/dom/dom-keys.ts +238 -0
  215. package/src/node/dom/env.ts +60 -0
  216. package/src/node/dom/util.ts +83 -0
  217. package/src/node/index.ts +12 -0
  218. package/src/node/load.ts +59 -0
  219. package/src/node/markdown.ts +267 -0
  220. package/src/node/plugin.ts +95 -0
  221. package/src/node/preview.ts +57 -0
  222. package/src/node/search.ts +84 -0
  223. package/src/node/server.ts +235 -0
  224. package/src/node/stories.ts +154 -0
  225. package/src/node/tree.ts +158 -0
  226. package/src/node/util/find-up.ts +24 -0
  227. package/src/node/util/fs.ts +1 -0
  228. package/src/node/util/log.ts +10 -0
  229. package/src/node/util/slugify.ts +26 -0
  230. package/src/node/util/vendors.ts +27 -0
  231. package/src/node/vendors/controls.ts +1 -0
  232. package/src/node/vendors/vue.ts +1 -0
  233. package/src/node/virtual/index.ts +50 -0
  234. package/src/node/virtual/markdown.ts +21 -0
  235. package/src/node/virtual/noop.ts +1 -0
  236. package/src/node/virtual/resolved-commands.ts +42 -0
  237. package/src/node/virtual/resolved-config.ts +12 -0
  238. package/src/node/virtual/resolved-generated-global-setup.ts +25 -0
  239. package/src/node/virtual/resolved-generated-setup-code.ts +7 -0
  240. package/src/node/virtual/resolved-markdown-files.ts +19 -0
  241. package/src/node/virtual/resolved-stories.ts +44 -0
  242. package/src/node/virtual/resolved-support-plugins-client.ts +14 -0
  243. package/src/node/virtual/resolved-support-plugins-collect.ts +14 -0
  244. package/src/node/virtual/resolved-theme.ts +14 -0
  245. package/src/node/virtual/story-source.ts +18 -0
  246. package/src/node/virtual/story.ts +9 -0
  247. package/src/node/virtual/util.ts +5 -0
  248. package/src/node/virtual/vite-plugin.ts +146 -0
  249. package/src/node/vite.ts +379 -0
  250. package/tsconfig.json +44 -0
  251. package/vitest.config.ts +10 -0
@@ -0,0 +1,84 @@
1
+ import type { Context } from './context.js'
2
+ import { noCase } from 'change-case'
3
+
4
+ export async function generateTitleSearchData(ctx: Context) {
5
+ const searchIndex = []
6
+ const { idMap, addToIdMap } = createIdMap()
7
+
8
+ for (const storyFile of ctx.storyFiles) {
9
+ if (storyFile.story) {
10
+ searchIndex.push({
11
+ id: addToIdMap(storyFile.story.id, 'story'),
12
+ text: convertTitleToSentence(storyFile.story.title),
13
+ })
14
+ for (const variant of storyFile.story.variants) {
15
+ searchIndex.push({
16
+ id: addToIdMap(`${storyFile.story.id}:${variant.id}`, 'variant'),
17
+ text: convertTitleToSentence(`${storyFile.story.title} ${variant.title}`),
18
+ })
19
+ }
20
+ }
21
+ }
22
+
23
+ return {
24
+ index: searchIndex,
25
+ idMap,
26
+ }
27
+ }
28
+
29
+ export async function generateDocSearchData(ctx: Context) {
30
+ const searchIndex = []
31
+ const { idMap, addToIdMap } = createIdMap()
32
+
33
+ for (const storyFile of ctx.storyFiles) {
34
+ if (storyFile.story && storyFile.story.docsText) {
35
+ searchIndex.push({
36
+ id: addToIdMap(storyFile.story.id, 'story'),
37
+ text: storyFile.story.docsText,
38
+ })
39
+ }
40
+ }
41
+
42
+ return {
43
+ index: searchIndex,
44
+ idMap,
45
+ }
46
+ }
47
+
48
+ function createIdMap() {
49
+ let uid = 0
50
+ const idMap: Record<number, { id: string, kind: string }> = {}
51
+
52
+ function addToIdMap(id: string, kind: string) {
53
+ const n = uid++
54
+ idMap[n] = { id, kind }
55
+ return n
56
+ }
57
+
58
+ return {
59
+ idMap,
60
+ addToIdMap,
61
+ }
62
+ }
63
+
64
+ function convertTitleToSentence(text: string) {
65
+ return text.split(' ').map(str => noCase(str)).join(' ')
66
+ }
67
+
68
+ // @TODO clear handlers when SearchPane unmounts
69
+ export function getSearchDataJS(data: any) {
70
+ return `export let searchData = ${JSON.stringify(data)}
71
+ const handlers = []
72
+ export function onUpdate (cb) {
73
+ handlers.push(cb)
74
+ }
75
+ if (import.meta.hot) {
76
+ import.meta.hot.accept(newModule => {
77
+ searchData = newModule.searchData
78
+ handlers.forEach(h => {
79
+ h(newModule.searchData)
80
+ newModule.onUpdate(h)
81
+ })
82
+ })
83
+ }`
84
+ }
@@ -0,0 +1,235 @@
1
+ import type { ServerStoryFile } from '@poveste/shared'
2
+ import type { Context } from './context.js'
3
+ import { performance } from 'node:perf_hooks'
4
+ import pc from 'picocolors'
5
+ import { createServer as createViteServer, mergeConfig as mergeViteConfig } from 'vite'
6
+ import { useCollectStories } from './collect/index.js'
7
+ import { useModuleLoader } from './load.js'
8
+ import { createMarkdownFilesWatcher, onMarkdownListChange } from './markdown.js'
9
+ import { DevEventPluginApi, DevPluginApi } from './plugin.js'
10
+ import { onStoryChange, onStoryListChange, watchStories } from './stories.js'
11
+ import { wrapLogError } from './util/log.js'
12
+ import * as VirtualFiles from './virtual/index.js'
13
+ import { getViteConfigWithPlugins } from './vite.js'
14
+
15
+ export interface CreateServerOptions {
16
+ port?: number
17
+ open?: boolean
18
+ host?: string | boolean
19
+ }
20
+
21
+ export async function createServer(ctx: Context, options: CreateServerOptions = {}) {
22
+ const getViteServer = async (collecting: boolean) => {
23
+ const { viteConfig, viteConfigFile } = await getViteConfigWithPlugins(collecting, ctx)
24
+
25
+ if (!collecting) {
26
+ if (options.open) {
27
+ viteConfig.server.open = true
28
+ }
29
+
30
+ if (options.host) {
31
+ viteConfig.server.host = options.host
32
+ }
33
+ }
34
+
35
+ const server = await createViteServer(
36
+ mergeViteConfig(viteConfig, {
37
+ optimizeDeps: { include: viteConfig.optimizeDeps?.include ?? [], noDiscovery: collecting },
38
+ }),
39
+ )
40
+ await server.pluginContainer.buildStart({})
41
+ return {
42
+ server,
43
+ viteConfigFile,
44
+ }
45
+ }
46
+
47
+ // Should be run sequentially to get a fresh vite.config.js each time
48
+ const { server: nodeServer } = await getViteServer(true) // Run before normal vite to prevent breaking HMR in Nuxt
49
+ const { server, viteConfigFile } = await getViteServer(false)
50
+ await watchStories(ctx)
51
+ const { stop: stopMdFileWatcher } = await createMarkdownFilesWatcher(ctx)
52
+
53
+ const moduleLoader = useModuleLoader({
54
+ server: nodeServer,
55
+ })
56
+
57
+ const pluginOnCleanups: (() => void | Promise<void>)[] = []
58
+ for (const plugin of ctx.config.plugins) {
59
+ if (plugin.onDev) {
60
+ const api = new DevPluginApi(ctx, plugin, moduleLoader)
61
+ const onCleanup = (cb: () => void | Promise<void>) => {
62
+ pluginOnCleanups.push(cb)
63
+ }
64
+ await plugin.onDev(api, onCleanup)
65
+ }
66
+ }
67
+
68
+ // Custom dev events
69
+ server.ws.on(`histoire:dev-event`, async ({ event, payload }) => {
70
+ for (const plugin of ctx.config.plugins) {
71
+ if (plugin.onDevEvent) {
72
+ const api = new DevEventPluginApi(ctx, plugin, moduleLoader, event, payload)
73
+ const result = await plugin.onDevEvent(api)
74
+ if (!event.startsWith('on') && result !== undefined) {
75
+ server.ws.send(`histoire:dev-event-result`, { event, result })
76
+ break
77
+ }
78
+ }
79
+ }
80
+ })
81
+
82
+ // Wait for pre-bundling (in `listen()`)
83
+ await server.listen(options.port ?? server.config.server?.port)
84
+
85
+ const {
86
+ clearCache,
87
+ executeStoryFile,
88
+ destroy: destroyCollectStories,
89
+ } = useCollectStories({
90
+ server: nodeServer,
91
+ mainServer: server,
92
+ }, ctx)
93
+
94
+ // onStoryChange debouncing
95
+ let queued = false
96
+ let queuedFiles: ServerStoryFile[] = []
97
+ let currentFiles: ServerStoryFile[] = []
98
+ let queueTimer
99
+ let collecting = false
100
+ let didAllStoriesYet = false
101
+
102
+ // Invalidate modules
103
+ const invalidateModule = (id: string) => {
104
+ const mod = server.moduleGraph.getModuleById(id)
105
+ if (!mod) {
106
+ return
107
+ }
108
+ server.moduleGraph.invalidateModule(mod)
109
+
110
+ // Send HMR update
111
+ const timestamp = Date.now()
112
+ mod.lastHMRTimestamp = timestamp
113
+ server.ws.send({
114
+ type: 'update',
115
+ updates: [
116
+ {
117
+ type: 'js-update',
118
+ acceptedPath: mod.url,
119
+ path: mod.url,
120
+ timestamp,
121
+ },
122
+ ],
123
+ })
124
+ }
125
+
126
+ onStoryChange(async (changedFile) => {
127
+ if (changedFile && !didAllStoriesYet) {
128
+ return
129
+ }
130
+
131
+ if (changedFile) {
132
+ if (!queuedFiles.includes(changedFile)) {
133
+ queuedFiles.push(changedFile)
134
+ }
135
+ }
136
+ else {
137
+ queuedFiles = []
138
+ }
139
+
140
+ if (!queued) {
141
+ queued = true
142
+ if (!collecting) {
143
+ clearTimeout(queueTimer)
144
+ // Debounce
145
+ queueTimer = setTimeout(collect, 100)
146
+ }
147
+ else if (!changedFile && !currentFiles.length) {
148
+ // Full collect in progress
149
+ queued = false
150
+ }
151
+ }
152
+ })
153
+
154
+ async function collect() {
155
+ collecting = true
156
+
157
+ clearCache()
158
+
159
+ currentFiles = queuedFiles.slice()
160
+ queuedFiles = []
161
+ queued = false
162
+
163
+ console.log('Collect stories start', currentFiles.length ? currentFiles.map(f => f.fileName).join(', ') : 'all')
164
+ const time = performance.now()
165
+ if (currentFiles.length) {
166
+ await Promise.all(currentFiles.map(async (storyFile) => {
167
+ await executeStoryFile(storyFile)
168
+ if (storyFile.story) {
169
+ await invalidateModule(`/__resolved__virtual:story-source:${storyFile.story.id}`)
170
+ }
171
+ }))
172
+ }
173
+ else {
174
+ // Full update
175
+
176
+ // Progress tracking
177
+ const fileCount = ctx.storyFiles.length
178
+ let loadedFilesCount = 0
179
+ const sendProgress = () => {
180
+ server.ws.send('histoire:stories-loading-progress', {
181
+ loadedFileCount: loadedFilesCount,
182
+ totalFileCount: fileCount,
183
+ })
184
+ }
185
+
186
+ sendProgress()
187
+
188
+ await Promise.all(ctx.storyFiles.map(async (storyFile) => {
189
+ await executeStoryFile(storyFile)
190
+ loadedFilesCount++
191
+ sendProgress()
192
+ }))
193
+
194
+ didAllStoriesYet = true
195
+ server.ws.send('histoire:all-stories-loaded', {})
196
+ }
197
+ console.log(`Collect stories end ${pc.bold(pc.blue(Math.round(performance.now() - time)))}ms`)
198
+
199
+ invalidateModule(VirtualFiles.RESOLVED_STORIES_ID)
200
+ invalidateModule(VirtualFiles.RESOLVED_SEARCH_TITLE_DATA_ID)
201
+
202
+ collecting = false
203
+
204
+ if (queued) {
205
+ await collect()
206
+ }
207
+ }
208
+
209
+ onStoryListChange(() => {
210
+ invalidateModule(VirtualFiles.RESOLVED_STORIES_ID)
211
+ invalidateModule(VirtualFiles.RESOLVED_SEARCH_TITLE_DATA_ID)
212
+ })
213
+
214
+ onMarkdownListChange(() => {
215
+ invalidateModule(VirtualFiles.RESOLVED_MARKDOWN_FILES)
216
+ })
217
+
218
+ async function close() {
219
+ for (const cb of pluginOnCleanups) {
220
+ await wrapLogError('plugin.onDev.onCleanup', () => cb())
221
+ }
222
+ await wrapLogError('server.close', () => server.close())
223
+ await wrapLogError('nodeServer', () => nodeServer.close())
224
+ await wrapLogError('destroyCollectStories', () => destroyCollectStories())
225
+ await wrapLogError('stopMdFileWatcher', () => stopMdFileWatcher())
226
+ }
227
+
228
+ collect()
229
+
230
+ return {
231
+ server,
232
+ viteConfigFile,
233
+ close,
234
+ }
235
+ }
@@ -0,0 +1,154 @@
1
+ import type { ServerStoryFile } from '@poveste/shared'
2
+ import type { Context } from './context.js'
3
+ import { kebabCase } from 'change-case'
4
+ import chokidar from 'chokidar'
5
+ import { globby } from 'globby'
6
+ import micromatch from 'micromatch'
7
+ import { basename, resolve } from 'pathe'
8
+
9
+ type StoryChangeHandler = (file?: ServerStoryFile) => unknown
10
+ const storyChangeHandlers: StoryChangeHandler[] = []
11
+
12
+ /**
13
+ * Called when a new story is added or modified. Collecting should be done.
14
+ * @param handler
15
+ */
16
+ export function onStoryChange(handler: StoryChangeHandler) {
17
+ storyChangeHandlers.push(handler)
18
+ }
19
+
20
+ export function notifyStoryChange(file?: ServerStoryFile) {
21
+ for (const handler of storyChangeHandlers) {
22
+ handler(file)
23
+ }
24
+ }
25
+
26
+ type StoryListChangeHandler = () => unknown
27
+ const storyListChangeHandlers: StoryListChangeHandler[] = []
28
+
29
+ /**
30
+ * Called when the story list has changed (ex: removed a story). No collecting should be needed.
31
+ * @param handler
32
+ */
33
+ export function onStoryListChange(handler: StoryListChangeHandler) {
34
+ storyListChangeHandlers.push(handler)
35
+ }
36
+
37
+ export function notifyStoryListChange() {
38
+ for (const handler of storyListChangeHandlers) {
39
+ handler()
40
+ }
41
+ }
42
+
43
+ let context: Context
44
+
45
+ export async function watchStories(newContext: Context) {
46
+ context = newContext
47
+
48
+ const baseWatchPaths = Array.from(new Set(context.config.storyMatch.map((pattern) => {
49
+ const segments = pattern.split('/')
50
+ return segments.slice(0, segments.findIndex(segment => segment.includes('*'))).join('/') || '.'
51
+ })))
52
+
53
+ const resolvedStoryIgnored = context.config.storyIgnored.map((pattern) => {
54
+ return resolve(context.root, pattern)
55
+ })
56
+
57
+ const resolveStoryMatch = context.config.storyMatch.map((pattern) => {
58
+ return resolve(context.root, pattern)
59
+ })
60
+
61
+ const watcher = chokidar.watch(baseWatchPaths, {
62
+ cwd: context.root,
63
+ ignored: (path, stats) => {
64
+ if (resolvedStoryIgnored.some(pattern => micromatch.isMatch(path, pattern))) {
65
+ return true
66
+ }
67
+ if (resolveStoryMatch.some(pattern => micromatch.isMatch(path, pattern))) {
68
+ return false
69
+ }
70
+
71
+ // Allow directories to be traversed, only ignore files that don't match
72
+ return stats?.isFile() ?? false
73
+ },
74
+ })
75
+
76
+ watcher
77
+ .on('add', (file) => {
78
+ const storyFile = addStory(file)
79
+ setTimeout(() => notifyStoryChange(storyFile), 100) // Delay in case file renaming fired Add event before Unlink event
80
+ })
81
+ .on('unlink', (file) => {
82
+ removeStory(file)
83
+ notifyStoryListChange()
84
+ })
85
+
86
+ return watcher
87
+ }
88
+
89
+ function getAbsoluteFilePath(relativeFilePath: string) {
90
+ return resolve(context.root, relativeFilePath)
91
+ }
92
+
93
+ export function addStory(relativeFilePath: string, virtualModuleCode?: string) {
94
+ const absoluteFilePath = getAbsoluteFilePath(relativeFilePath)
95
+
96
+ for (const file of context.storyFiles) {
97
+ if (file.path === absoluteFilePath) {
98
+ return file
99
+ }
100
+ }
101
+
102
+ const fileId = kebabCase(relativeFilePath.toLowerCase())
103
+ let fileName = basename(relativeFilePath)
104
+ if (fileName.includes('.')) {
105
+ fileName = fileName.substring(0, fileName.indexOf('.'))
106
+ }
107
+
108
+ let supportPluginId: string
109
+
110
+ for (const p of context.config.supportMatch) {
111
+ if (micromatch.isMatch(absoluteFilePath, p.patterns, {
112
+ dot: true,
113
+ })) {
114
+ supportPluginId = p.pluginIds[0]
115
+ break
116
+ }
117
+ }
118
+
119
+ if (!supportPluginId) {
120
+ throw new Error(`No support plugin found for file ${absoluteFilePath}`)
121
+ }
122
+
123
+ const file: ServerStoryFile = {
124
+ id: fileId, // The file id will be changed by the story id after it is collected
125
+ path: absoluteFilePath,
126
+ relativePath: relativeFilePath,
127
+ fileName,
128
+ moduleId: virtualModuleCode ? `virtual:story:${absoluteFilePath}` : absoluteFilePath,
129
+ supportPluginId,
130
+ virtual: !!virtualModuleCode,
131
+ moduleCode: virtualModuleCode,
132
+ }
133
+ context.storyFiles.push(file)
134
+ return file
135
+ }
136
+
137
+ export function removeStory(relativeFilePath: string) {
138
+ const absoluteFilePath = getAbsoluteFilePath(relativeFilePath)
139
+ const index = context.storyFiles.findIndex(file => file.path === absoluteFilePath)
140
+ if (index !== -1) context.storyFiles.splice(index, 1)
141
+ }
142
+
143
+ export async function findAllStories(newContext: Context) {
144
+ context = newContext
145
+
146
+ const files = await globby(context.config.storyMatch, {
147
+ cwd: context.root,
148
+ ignore: context.config.storyIgnored,
149
+ })
150
+ context.storyFiles.length = 0
151
+ for (const file of files) {
152
+ addStory(file)
153
+ }
154
+ }
@@ -0,0 +1,158 @@
1
+ import type {
2
+ PovesteConfig,
3
+ ServerStoryFile,
4
+ ServerTree,
5
+ ServerTreeFile,
6
+ TreeGroupConfig,
7
+ } from '@poveste/shared'
8
+ import pc from 'picocolors'
9
+
10
+ export function createPath(config: PovesteConfig, file: ServerTreeFile) {
11
+ if (config.tree.file === 'title') {
12
+ return file.title.split('/')
13
+ }
14
+
15
+ if (config.tree.file === 'path') {
16
+ const paths = file.path.split('/').slice(0, -1)
17
+
18
+ // check if tree file path is a plugin
19
+ const index = paths.findIndex(p => p.includes('.histoire'))
20
+
21
+ if (index !== -1) {
22
+ return ['plugins', file.title]
23
+ }
24
+
25
+ return [...paths, file.title]
26
+ }
27
+
28
+ return config.tree.file(file)
29
+ }
30
+
31
+ export function makeTree(config: PovesteConfig, files: ServerStoryFile[]) {
32
+ interface ITreeObject {
33
+ [index: string]: number | ITreeObject
34
+ }
35
+
36
+ interface ITreeGroup {
37
+ groupConfig?: TreeGroupConfig
38
+ treeObject: ITreeObject
39
+ }
40
+
41
+ const groups: ITreeGroup[] = config.tree?.groups?.map(g => ({
42
+ groupConfig: g,
43
+ treeObject: {},
44
+ })) || []
45
+ const defaultGroup = {
46
+ treeObject: {},
47
+ }
48
+ groups.push(defaultGroup)
49
+
50
+ files.forEach((file, index) => {
51
+ const group = getGroup(file)
52
+ setPath(file.treePath, index, group.treeObject)
53
+ })
54
+
55
+ let sortingFunction = (a: string, b: string) => a.localeCompare(b)
56
+
57
+ if (config.tree.order !== 'asc') {
58
+ sortingFunction = config.tree.order
59
+ }
60
+
61
+ const result: ServerTree = []
62
+
63
+ for (const group of groups) {
64
+ if (group === defaultGroup) {
65
+ result.push(...buildTree(group.treeObject))
66
+ }
67
+ else {
68
+ result.push({
69
+ group: true,
70
+ id: group.groupConfig.id,
71
+ title: group.groupConfig.title,
72
+ children: buildTree(group.treeObject),
73
+ })
74
+ }
75
+ }
76
+
77
+ return result
78
+
79
+ function getGroup(file: ServerStoryFile): ITreeGroup {
80
+ if (file.story?.group) {
81
+ const group = groups.find(g => g.groupConfig?.id === file.story.group)
82
+ if (group) {
83
+ return group
84
+ }
85
+ else {
86
+ console.error(pc.red(`Group ${file.story.group} not found for story ${file.path}`))
87
+ }
88
+ }
89
+ for (const group of groups) {
90
+ if (group.groupConfig?.include && group.groupConfig.include(file.treeFile)) {
91
+ return group
92
+ }
93
+ }
94
+ return defaultGroup
95
+ }
96
+
97
+ function setPath(path: string[], value: unknown, tree: unknown) {
98
+ path.reduce((subtree, key, i) => {
99
+ if (i === path.length - 1) {
100
+ setKey(subtree, key, value)
101
+ }
102
+ else if (isLeaf(subtree[key])) {
103
+ setKey(subtree, key, subtree[key])
104
+ subtree[key] = {}
105
+ }
106
+ else if (!subtree[key]) {
107
+ subtree[key] = {}
108
+ }
109
+ return subtree[key]
110
+ }, tree)
111
+
112
+ function isLeaf(element) {
113
+ return !Number.isNaN(Number(element))
114
+ }
115
+ }
116
+
117
+ function setKey(tree, key, value) {
118
+ if (isUndefined(tree[key])) {
119
+ tree[key] = value
120
+ return
121
+ }
122
+
123
+ let copyNumber = 1
124
+
125
+ while (!isUndefined(tree[`${key}-${copyNumber}`])) {
126
+ copyNumber++
127
+ }
128
+
129
+ tree[`${key}-${copyNumber}`] = value
130
+
131
+ function isUndefined(element) {
132
+ return element === undefined
133
+ }
134
+ }
135
+
136
+ function buildTree(treeObject: ITreeObject): ServerTree {
137
+ const tree: ServerTree = []
138
+
139
+ for (const [key, element] of Object.entries(treeObject)) {
140
+ if (Number.isInteger(element)) {
141
+ tree.push({
142
+ title: key,
143
+ index: element as number,
144
+ })
145
+ }
146
+ else {
147
+ tree.push({
148
+ title: key,
149
+ children: buildTree(element as ITreeObject),
150
+ })
151
+ }
152
+ }
153
+
154
+ tree.sort((a, b) => sortingFunction(a.title, b.title))
155
+
156
+ return tree
157
+ }
158
+ }
@@ -0,0 +1,24 @@
1
+ import fs from 'node:fs'
2
+ import path from 'pathe'
3
+
4
+ export function findUp(cwd: string = process.cwd(), fileNames: string[]): string {
5
+ let { root } = path.parse(cwd)
6
+ let dir = cwd
7
+
8
+ // On Windows, it will for example return `C:`, we need to add the trailing `/`
9
+ if (root[1] === ':' && root[2] === undefined) {
10
+ root += '/'
11
+ }
12
+
13
+ while (dir !== root) {
14
+ for (const fileName of fileNames) {
15
+ const searchPath = path.join(dir, fileName)
16
+ if (fs.existsSync(searchPath)) {
17
+ return searchPath
18
+ }
19
+ }
20
+ dir = path.dirname(dir)
21
+ }
22
+
23
+ return null
24
+ }
@@ -0,0 +1 @@
1
+ export const slash = (str: string) => str.replace(/\\/g, '/')
@@ -0,0 +1,10 @@
1
+ import pc from 'picocolors'
2
+
3
+ export async function wrapLogError(id: string, cb: () => unknown) {
4
+ try {
5
+ await cb()
6
+ }
7
+ catch (e) {
8
+ console.error(pc.red(`[Error] ${id}: ${e}`))
9
+ }
10
+ }
@@ -0,0 +1,26 @@
1
+ // From vitepress
2
+
3
+ // string.js slugify drops non ascii chars so we have to
4
+ // use a custom implementation here
5
+ import { remove as removeDiacritics } from 'diacritics'
6
+ // eslint-disable-next-line no-control-regex
7
+ const rControl = /[\u0000-\u001F]/g
8
+ const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'<>,.?/]+/g
9
+
10
+ export function slugify(str: string): string {
11
+ return (
12
+ removeDiacritics(str)
13
+ // Remove control characters
14
+ .replace(rControl, '')
15
+ // Replace special characters
16
+ .replace(rSpecial, '-')
17
+ // Remove continuous separators
18
+ .replace(/-{2,}/g, '-')
19
+ // Remove prefixing and trailing separators
20
+ .replace(/^-+|-+$/g, '')
21
+ // ensure it doesn't start with a number (#121)
22
+ .replace(/^(\d)/, '_$1')
23
+ // lowercase
24
+ .toLowerCase()
25
+ )
26
+ }