livemark 0.28.3 → 0.28.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +2 -2
  2. package/build/actions/changelog/build.d.ts +1 -1
  3. package/build/actions/changelog/build.js +5 -5
  4. package/build/actions/changelog/build.unit.js +6 -6
  5. package/build/actions/target/build.d.ts +9 -5
  6. package/build/actions/target/build.js +14 -9
  7. package/build/actions/target/build.unit.js +17 -2
  8. package/build/actions/target/clear.d.ts +2 -2
  9. package/build/actions/target/clear.js +11 -9
  10. package/build/actions/target/clear.unit.js +10 -3
  11. package/build/commands/clear.d.ts +1 -1
  12. package/build/commands/clear.js +6 -5
  13. package/build/helpers/tailwind-sources.unit.js +4 -4
  14. package/build/helpers/target-dir.d.ts +2 -0
  15. package/build/helpers/target-dir.js +11 -0
  16. package/build/helpers/target-export.unit.js +2 -2
  17. package/build/helpers/task-queue.d.ts +7 -0
  18. package/build/helpers/task-queue.js +28 -0
  19. package/build/helpers/task-queue.unit.d.ts +1 -0
  20. package/build/helpers/task-queue.unit.js +56 -0
  21. package/build/plugins/vite-content.d.ts +9 -0
  22. package/build/plugins/vite-content.js +60 -0
  23. package/build/plugins/vite-livemark.d.ts +2 -1
  24. package/build/plugins/vite-livemark.js +20 -9
  25. package/build/vite.config.js +3 -3
  26. package/package.json +2 -2
  27. package/skills/livemark-configuration/SKILL.md +1 -1
  28. package/skills/livemark-customization/SKILL.md +1 -1
  29. package/skills/livemark-deployment/SKILL.md +1 -1
  30. package/source/actions/changelog/build.ts +4 -5
  31. package/source/actions/changelog/build.unit.ts +5 -5
  32. package/source/actions/target/build.ts +29 -9
  33. package/source/actions/target/build.unit.ts +24 -1
  34. package/source/actions/target/clear.ts +9 -8
  35. package/source/actions/target/clear.unit.ts +9 -2
  36. package/source/commands/clear.ts +5 -4
  37. package/source/helpers/tailwind-sources.unit.ts +3 -3
  38. package/source/helpers/target-dir.ts +12 -0
  39. package/source/helpers/target-export.unit.ts +1 -1
  40. package/source/helpers/task-queue.ts +28 -0
  41. package/source/helpers/task-queue.unit.ts +60 -0
  42. package/source/plugins/vite-content.ts +72 -0
  43. package/source/plugins/vite-livemark.ts +19 -9
  44. package/source/vite.config.ts +2 -2
@@ -1,4 +1,3 @@
1
- import contentCollections from "@content-collections/vite";
2
1
  import tailwind from "@tailwindcss/vite";
3
2
  import { devtools } from "@tanstack/devtools-vite";
4
3
  import { tanstackStart } from "@tanstack/react-start/plugin/vite";
@@ -7,6 +6,7 @@ import { defineConfig } from "vite";
7
6
  import svgr from "vite-plugin-svgr";
8
7
  import { loadConfig } from "./actions/config/load.js";
9
8
  import { agents } from "./plugins/vite-agents.js";
9
+ import { content } from "./plugins/vite-content.js";
10
10
  import { livemark } from "./plugins/vite-livemark.js";
11
11
  import { robots } from "./plugins/vite-robots.js";
12
12
  const config = await loadConfig();
@@ -31,7 +31,7 @@ export default defineConfig({
31
31
  // any browser error back and forth forever.
32
32
  devtools({ consolePiping: { enabled: false } }),
33
33
  tailwind(),
34
- contentCollections({ configPath: "content-collections.ts" }),
34
+ content({ configPath: "content-collections.ts", contentDir: config.root }),
35
35
  tanstackStart({
36
36
  srcDirectory: ".",
37
37
  // Seed path must match what the crawler produces (router-emitted
@@ -57,4 +57,4 @@ export default defineConfig({
57
57
  svgr(),
58
58
  ],
59
59
  });
60
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidml0ZS5jb25maWcuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zb3VyY2Uvdml0ZS5jb25maWcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxrQkFBa0IsTUFBTSwyQkFBMkIsQ0FBQTtBQUMxRCxPQUFPLFFBQVEsTUFBTSxtQkFBbUIsQ0FBQTtBQUN4QyxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0seUJBQXlCLENBQUE7QUFDbEQsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLG1DQUFtQyxDQUFBO0FBQ2pFLE9BQU8sS0FBSyxNQUFNLHNCQUFzQixDQUFBO0FBQ3hDLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxNQUFNLENBQUE7QUFDbkMsT0FBTyxJQUFJLE1BQU0sa0JBQWtCLENBQUE7QUFDbkMsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLDBCQUEwQixDQUFBO0FBQ3JELE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQTtBQUNqRCxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sNEJBQTRCLENBQUE7QUFDckQsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLDBCQUEwQixDQUFBO0FBRWpELE1BQU0sTUFBTSxHQUFHLE1BQU0sVUFBVSxFQUFFLENBQUE7QUFFakMsdUVBQXVFO0FBQ3ZFLHdFQUF3RTtBQUN4RSx5RUFBeUU7QUFDekUsbURBQW1EO0FBQ25ELE1BQU0sYUFBYSxHQUFHLE1BQU0sQ0FBQyxJQUFJO0lBQy9CLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxRQUFRLElBQUksRUFBRSxDQUFDO1NBQ3BCLE1BQU0sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxJQUFJLEtBQUssTUFBTSxDQUFDO1NBQzlCLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUM7UUFDVCxJQUFJLEVBQUUsR0FBRyxNQUFNLENBQUMsSUFBSSxJQUFJLEVBQUUsR0FBRyxDQUFDLENBQUMsTUFBTSxTQUFTO1FBQzlDLE9BQU8sRUFBRSxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUU7S0FDM0IsQ0FBQyxDQUFDO0lBQ1AsQ0FBQyxDQUFDLEVBQUUsQ0FBQTtBQUVOLGVBQWUsWUFBWSxDQUFDO0lBQzFCLE9BQU8sRUFBRTtRQUNQLFFBQVEsQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDO1FBQ3BCLE1BQU0sQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDO1FBQ2xCLE1BQU0sQ0FBQyxFQUFFLE1BQU0sRUFBRSxDQUFDO1FBQ2xCLG9GQUFvRjtRQUNwRiw0Q0FBNEM7UUFDNUMsUUFBUSxDQUFDLEVBQUUsYUFBYSxFQUFFLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxFQUFFLENBQUM7UUFDL0MsUUFBUSxFQUFFO1FBQ1Ysa0JBQWtCLENBQUMsRUFBRSxVQUFVLEVBQUUsd0JBQXdCLEVBQUUsQ0FBQztRQUM1RCxhQUFhLENBQUM7WUFDWixZQUFZLEVBQUUsR0FBRztZQUNqQixpRUFBaUU7WUFDakUsZ0VBQWdFO1lBQ2hFLHlEQUF5RDtZQUN6RCxLQUFLLEVBQUUsQ0FBQyxFQUFFLElBQUksRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxJQUFJLEdBQUcsQ0FBQyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUUsR0FBRyxhQUFhLENBQUM7WUFDMUUsZ0VBQWdFO1lBQ2hFLG9FQUFvRTtZQUNwRSxrRUFBa0U7WUFDbEUsNkJBQTZCO1lBQzdCLFNBQVMsRUFBRTtnQkFDVCxPQUFPLEVBQUUsSUFBSTtnQkFDYixVQUFVLEVBQUUsSUFBSTtnQkFDaEIsd0JBQXdCLEVBQUUsS0FBSzthQUNoQztZQUNELE9BQU8sRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLE1BQU0sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFO1lBQ2hGLE1BQU0sRUFBRTtnQkFDTixrQkFBa0IsRUFBRSxrQkFBa0I7Z0JBQ3RDLFFBQVEsRUFBRSxNQUFNLENBQUMsSUFBSTthQUN0QjtTQUNGLENBQUM7UUFDRixLQUFLLEVBQUU7UUFDUCxJQUFJLEVBQUU7S0FDUDtDQUNGLENBQUMsQ0FBQSIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBjb250ZW50Q29sbGVjdGlvbnMgZnJvbSBcIkBjb250ZW50LWNvbGxlY3Rpb25zL3ZpdGVcIlxuaW1wb3J0IHRhaWx3aW5kIGZyb20gXCJAdGFpbHdpbmRjc3Mvdml0ZVwiXG5pbXBvcnQgeyBkZXZ0b29scyB9IGZyb20gXCJAdGFuc3RhY2svZGV2dG9vbHMtdml0ZVwiXG5pbXBvcnQgeyB0YW5zdGFja1N0YXJ0IH0gZnJvbSBcIkB0YW5zdGFjay9yZWFjdC1zdGFydC9wbHVnaW4vdml0ZVwiXG5pbXBvcnQgcmVhY3QgZnJvbSBcIkB2aXRlanMvcGx1Z2luLXJlYWN0XCJcbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gXCJ2aXRlXCJcbmltcG9ydCBzdmdyIGZyb20gXCJ2aXRlLXBsdWdpbi1zdmdyXCJcbmltcG9ydCB7IGxvYWRDb25maWcgfSBmcm9tIFwiLi9hY3Rpb25zL2NvbmZpZy9sb2FkLnRzXCJcbmltcG9ydCB7IGFnZW50cyB9IGZyb20gXCIuL3BsdWdpbnMvdml0ZS1hZ2VudHMudHNcIlxuaW1wb3J0IHsgbGl2ZW1hcmsgfSBmcm9tIFwiLi9wbHVnaW5zL3ZpdGUtbGl2ZW1hcmsudHNcIlxuaW1wb3J0IHsgcm9ib3RzIH0gZnJvbSBcIi4vcGx1Z2lucy92aXRlLXJvYm90cy50c1wiXG5cbmNvbnN0IGNvbmZpZyA9IGF3YWl0IGxvYWRDb25maWcoKVxuXG4vLyBQZXItYmxvZy1zZWN0aW9uIFJTUyBmZWVkIHBhdGhzIHRvIHByZXJlbmRlci4gR2F0ZWQgb24gYGNvbmZpZy5zaXRlYFxuLy8gYmVjYXVzZSBmZWVkIFVSTHMgYXJlIGFic29sdXRlIChtYXRjaGluZyB0aGUgc2l0ZW1hcC9yb2JvdHMudHh0IGdhdGUpXG4vLyBhbmQgdGhlIHJvdXRlIHJldHVybnMgNDA0IHdpdGhvdXQgaXQgYW55d2F5LiBFeGNsdWRlZCBmcm9tIHRoZSBzaXRlbWFwXG4vLyBzaW5jZSBgLnhtbGAgcGF5bG9hZHMgZG9uJ3QgYmVsb25nIGluIGEgc2l0ZW1hcC5cbmNvbnN0IGJsb2dGZWVkUGFnZXMgPSBjb25maWcuc2l0ZVxuICA/IChjb25maWcuc2VjdGlvbnMgPz8gW10pXG4gICAgICAuZmlsdGVyKHMgPT4gcy50eXBlID09PSBcImJsb2dcIilcbiAgICAgIC5tYXAocyA9PiAoe1xuICAgICAgICBwYXRoOiBgJHtjb25maWcuYmFzZSA/PyBcIlwifSR7cy5wcmVmaXh9cnNzLnhtbGAsXG4gICAgICAgIHNpdGVtYXA6IHsgZXhjbHVkZTogdHJ1ZSB9LFxuICAgICAgfSkpXG4gIDogW11cblxuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcbiAgcGx1Z2luczogW1xuICAgIGxpdmVtYXJrKHsgY29uZmlnIH0pLFxuICAgIHJvYm90cyh7IGNvbmZpZyB9KSxcbiAgICBhZ2VudHMoeyBjb25maWcgfSksXG4gICAgLy8gSXRzIHNlcnZlci10by1icm93c2VyIGNvbnNvbGUgcGlwaW5nIGFuZCBWaXRlJ3MgYnJvd3Nlci10by1zZXJ2ZXIgZm9yd2FyZGluZyBlY2hvXG4gICAgLy8gYW55IGJyb3dzZXIgZXJyb3IgYmFjayBhbmQgZm9ydGggZm9yZXZlci5cbiAgICBkZXZ0b29scyh7IGNvbnNvbGVQaXBpbmc6IHsgZW5hYmxlZDogZmFsc2UgfSB9KSxcbiAgICB0YWlsd2luZCgpLFxuICAgIGNvbnRlbnRDb2xsZWN0aW9ucyh7IGNvbmZpZ1BhdGg6IFwiY29udGVudC1jb2xsZWN0aW9ucy50c1wiIH0pLFxuICAgIHRhbnN0YWNrU3RhcnQoe1xuICAgICAgc3JjRGlyZWN0b3J5OiBcIi5cIixcbiAgICAgIC8vIFNlZWQgcGF0aCBtdXN0IG1hdGNoIHdoYXQgdGhlIGNyYXdsZXIgcHJvZHVjZXMgKHJvdXRlci1lbWl0dGVkXG4gICAgICAvLyBocmVmcyBpbmNsdWRlIHRoZSBiYXNlcGF0aCksIG90aGVyd2lzZSBwcmVyZW5kZXIgcmVjb3JkcyBib3RoXG4gICAgICAvLyBzaGFwZXMgYW5kIHRoZSBzaXRlbWFwIGVuZHMgdXAgd2l0aCBkdXBsaWNhdGUgZW50cmllcy5cbiAgICAgIHBhZ2VzOiBbeyBwYXRoOiBjb25maWcuYmFzZSA/IGAke2NvbmZpZy5iYXNlfS9gIDogXCIvXCIgfSwgLi4uYmxvZ0ZlZWRQYWdlc10sXG4gICAgICAvLyBEaXNhYmxlIHJvdXRlLXRyZWUgYXV0by1kaXNjb3Zlcnk6IGl0IHByb2R1Y2VzIGJhc2VwYXRoLW5haXZlXG4gICAgICAvLyBwYXRocyAoZS5nLiBgL2ApIHRoYXQgZHVwbGljYXRlIHRoZSBjcmF3bC1kZXJpdmVkIHByZWZpeGVkIHBhdGhzLFxuICAgICAgLy8gYmxvYXRpbmcgcGFnZXMuanNvbiBhbmQgc2l0ZW1hcC4gQ3Jhd2xpbmcgZnJvbSB0aGUgc2VlZCBhbHJlYWR5XG4gICAgICAvLyByZWFjaGVzIGV2ZXJ5IGxpbmtlZCBwYWdlLlxuICAgICAgcHJlcmVuZGVyOiB7XG4gICAgICAgIGVuYWJsZWQ6IHRydWUsXG4gICAgICAgIGNyYXdsTGlua3M6IHRydWUsXG4gICAgICAgIGF1dG9TdGF0aWNQYXRoc0Rpc2NvdmVyeTogZmFsc2UsXG4gICAgICB9LFxuICAgICAgc2l0ZW1hcDogY29uZmlnLnNpdGUgPyB7IGVuYWJsZWQ6IHRydWUsIGhvc3Q6IGNvbmZpZy5zaXRlIH0gOiB7IGVuYWJsZWQ6IGZhbHNlIH0sXG4gICAgICByb3V0ZXI6IHtcbiAgICAgICAgZ2VuZXJhdGVkUm91dGVUcmVlOiBcInJvdXRlVHJlZS5nZW4udHNcIixcbiAgICAgICAgYmFzZXBhdGg6IGNvbmZpZy5iYXNlLFxuICAgICAgfSxcbiAgICB9KSxcbiAgICByZWFjdCgpLFxuICAgIHN2Z3IoKSxcbiAgXSxcbn0pXG4iXX0=
60
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidml0ZS5jb25maWcuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zb3VyY2Uvdml0ZS5jb25maWcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxRQUFRLE1BQU0sbUJBQW1CLENBQUE7QUFDeEMsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLHlCQUF5QixDQUFBO0FBQ2xELE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxtQ0FBbUMsQ0FBQTtBQUNqRSxPQUFPLEtBQUssTUFBTSxzQkFBc0IsQ0FBQTtBQUN4QyxPQUFPLEVBQUUsWUFBWSxFQUFFLE1BQU0sTUFBTSxDQUFBO0FBQ25DLE9BQU8sSUFBSSxNQUFNLGtCQUFrQixDQUFBO0FBQ25DLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSwwQkFBMEIsQ0FBQTtBQUNyRCxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sMEJBQTBCLENBQUE7QUFDakQsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLDJCQUEyQixDQUFBO0FBQ25ELE9BQU8sRUFBRSxRQUFRLEVBQUUsTUFBTSw0QkFBNEIsQ0FBQTtBQUNyRCxPQUFPLEVBQUUsTUFBTSxFQUFFLE1BQU0sMEJBQTBCLENBQUE7QUFFakQsTUFBTSxNQUFNLEdBQUcsTUFBTSxVQUFVLEVBQUUsQ0FBQTtBQUVqQyx1RUFBdUU7QUFDdkUsd0VBQXdFO0FBQ3hFLHlFQUF5RTtBQUN6RSxtREFBbUQ7QUFDbkQsTUFBTSxhQUFhLEdBQUcsTUFBTSxDQUFDLElBQUk7SUFDL0IsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLFFBQVEsSUFBSSxFQUFFLENBQUM7U0FDcEIsTUFBTSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLElBQUksS0FBSyxNQUFNLENBQUM7U0FDOUIsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQztRQUNULElBQUksRUFBRSxHQUFHLE1BQU0sQ0FBQyxJQUFJLElBQUksRUFBRSxHQUFHLENBQUMsQ0FBQyxNQUFNLFNBQVM7UUFDOUMsT0FBTyxFQUFFLEVBQUUsT0FBTyxFQUFFLElBQUksRUFBRTtLQUMzQixDQUFDLENBQUM7SUFDUCxDQUFDLENBQUMsRUFBRSxDQUFBO0FBRU4sZUFBZSxZQUFZLENBQUM7SUFDMUIsT0FBTyxFQUFFO1FBQ1AsUUFBUSxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7UUFDcEIsTUFBTSxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7UUFDbEIsTUFBTSxDQUFDLEVBQUUsTUFBTSxFQUFFLENBQUM7UUFDbEIsb0ZBQW9GO1FBQ3BGLDRDQUE0QztRQUM1QyxRQUFRLENBQUMsRUFBRSxhQUFhLEVBQUUsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFLEVBQUUsQ0FBQztRQUMvQyxRQUFRLEVBQUU7UUFDVixPQUFPLENBQUMsRUFBRSxVQUFVLEVBQUUsd0JBQXdCLEVBQUUsVUFBVSxFQUFFLE1BQU0sQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUMxRSxhQUFhLENBQUM7WUFDWixZQUFZLEVBQUUsR0FBRztZQUNqQixpRUFBaUU7WUFDakUsZ0VBQWdFO1lBQ2hFLHlEQUF5RDtZQUN6RCxLQUFLLEVBQUUsQ0FBQyxFQUFFLElBQUksRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxJQUFJLEdBQUcsQ0FBQyxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUUsR0FBRyxhQUFhLENBQUM7WUFDMUUsZ0VBQWdFO1lBQ2hFLG9FQUFvRTtZQUNwRSxrRUFBa0U7WUFDbEUsNkJBQTZCO1lBQzdCLFNBQVMsRUFBRTtnQkFDVCxPQUFPLEVBQUUsSUFBSTtnQkFDYixVQUFVLEVBQUUsSUFBSTtnQkFDaEIsd0JBQXdCLEVBQUUsS0FBSzthQUNoQztZQUNELE9BQU8sRUFBRSxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLE9BQU8sRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLE1BQU0sQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUMsRUFBRSxPQUFPLEVBQUUsS0FBSyxFQUFFO1lBQ2hGLE1BQU0sRUFBRTtnQkFDTixrQkFBa0IsRUFBRSxrQkFBa0I7Z0JBQ3RDLFFBQVEsRUFBRSxNQUFNLENBQUMsSUFBSTthQUN0QjtTQUNGLENBQUM7UUFDRixLQUFLLEVBQUU7UUFDUCxJQUFJLEVBQUU7S0FDUDtDQUNGLENBQUMsQ0FBQSIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB0YWlsd2luZCBmcm9tIFwiQHRhaWx3aW5kY3NzL3ZpdGVcIlxuaW1wb3J0IHsgZGV2dG9vbHMgfSBmcm9tIFwiQHRhbnN0YWNrL2RldnRvb2xzLXZpdGVcIlxuaW1wb3J0IHsgdGFuc3RhY2tTdGFydCB9IGZyb20gXCJAdGFuc3RhY2svcmVhY3Qtc3RhcnQvcGx1Z2luL3ZpdGVcIlxuaW1wb3J0IHJlYWN0IGZyb20gXCJAdml0ZWpzL3BsdWdpbi1yZWFjdFwiXG5pbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tIFwidml0ZVwiXG5pbXBvcnQgc3ZnciBmcm9tIFwidml0ZS1wbHVnaW4tc3ZnclwiXG5pbXBvcnQgeyBsb2FkQ29uZmlnIH0gZnJvbSBcIi4vYWN0aW9ucy9jb25maWcvbG9hZC50c1wiXG5pbXBvcnQgeyBhZ2VudHMgfSBmcm9tIFwiLi9wbHVnaW5zL3ZpdGUtYWdlbnRzLnRzXCJcbmltcG9ydCB7IGNvbnRlbnQgfSBmcm9tIFwiLi9wbHVnaW5zL3ZpdGUtY29udGVudC50c1wiXG5pbXBvcnQgeyBsaXZlbWFyayB9IGZyb20gXCIuL3BsdWdpbnMvdml0ZS1saXZlbWFyay50c1wiXG5pbXBvcnQgeyByb2JvdHMgfSBmcm9tIFwiLi9wbHVnaW5zL3ZpdGUtcm9ib3RzLnRzXCJcblxuY29uc3QgY29uZmlnID0gYXdhaXQgbG9hZENvbmZpZygpXG5cbi8vIFBlci1ibG9nLXNlY3Rpb24gUlNTIGZlZWQgcGF0aHMgdG8gcHJlcmVuZGVyLiBHYXRlZCBvbiBgY29uZmlnLnNpdGVgXG4vLyBiZWNhdXNlIGZlZWQgVVJMcyBhcmUgYWJzb2x1dGUgKG1hdGNoaW5nIHRoZSBzaXRlbWFwL3JvYm90cy50eHQgZ2F0ZSlcbi8vIGFuZCB0aGUgcm91dGUgcmV0dXJucyA0MDQgd2l0aG91dCBpdCBhbnl3YXkuIEV4Y2x1ZGVkIGZyb20gdGhlIHNpdGVtYXBcbi8vIHNpbmNlIGAueG1sYCBwYXlsb2FkcyBkb24ndCBiZWxvbmcgaW4gYSBzaXRlbWFwLlxuY29uc3QgYmxvZ0ZlZWRQYWdlcyA9IGNvbmZpZy5zaXRlXG4gID8gKGNvbmZpZy5zZWN0aW9ucyA/PyBbXSlcbiAgICAgIC5maWx0ZXIocyA9PiBzLnR5cGUgPT09IFwiYmxvZ1wiKVxuICAgICAgLm1hcChzID0+ICh7XG4gICAgICAgIHBhdGg6IGAke2NvbmZpZy5iYXNlID8/IFwiXCJ9JHtzLnByZWZpeH1yc3MueG1sYCxcbiAgICAgICAgc2l0ZW1hcDogeyBleGNsdWRlOiB0cnVlIH0sXG4gICAgICB9KSlcbiAgOiBbXVxuXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBwbHVnaW5zOiBbXG4gICAgbGl2ZW1hcmsoeyBjb25maWcgfSksXG4gICAgcm9ib3RzKHsgY29uZmlnIH0pLFxuICAgIGFnZW50cyh7IGNvbmZpZyB9KSxcbiAgICAvLyBJdHMgc2VydmVyLXRvLWJyb3dzZXIgY29uc29sZSBwaXBpbmcgYW5kIFZpdGUncyBicm93c2VyLXRvLXNlcnZlciBmb3J3YXJkaW5nIGVjaG9cbiAgICAvLyBhbnkgYnJvd3NlciBlcnJvciBiYWNrIGFuZCBmb3J0aCBmb3JldmVyLlxuICAgIGRldnRvb2xzKHsgY29uc29sZVBpcGluZzogeyBlbmFibGVkOiBmYWxzZSB9IH0pLFxuICAgIHRhaWx3aW5kKCksXG4gICAgY29udGVudCh7IGNvbmZpZ1BhdGg6IFwiY29udGVudC1jb2xsZWN0aW9ucy50c1wiLCBjb250ZW50RGlyOiBjb25maWcucm9vdCB9KSxcbiAgICB0YW5zdGFja1N0YXJ0KHtcbiAgICAgIHNyY0RpcmVjdG9yeTogXCIuXCIsXG4gICAgICAvLyBTZWVkIHBhdGggbXVzdCBtYXRjaCB3aGF0IHRoZSBjcmF3bGVyIHByb2R1Y2VzIChyb3V0ZXItZW1pdHRlZFxuICAgICAgLy8gaHJlZnMgaW5jbHVkZSB0aGUgYmFzZXBhdGgpLCBvdGhlcndpc2UgcHJlcmVuZGVyIHJlY29yZHMgYm90aFxuICAgICAgLy8gc2hhcGVzIGFuZCB0aGUgc2l0ZW1hcCBlbmRzIHVwIHdpdGggZHVwbGljYXRlIGVudHJpZXMuXG4gICAgICBwYWdlczogW3sgcGF0aDogY29uZmlnLmJhc2UgPyBgJHtjb25maWcuYmFzZX0vYCA6IFwiL1wiIH0sIC4uLmJsb2dGZWVkUGFnZXNdLFxuICAgICAgLy8gRGlzYWJsZSByb3V0ZS10cmVlIGF1dG8tZGlzY292ZXJ5OiBpdCBwcm9kdWNlcyBiYXNlcGF0aC1uYWl2ZVxuICAgICAgLy8gcGF0aHMgKGUuZy4gYC9gKSB0aGF0IGR1cGxpY2F0ZSB0aGUgY3Jhd2wtZGVyaXZlZCBwcmVmaXhlZCBwYXRocyxcbiAgICAgIC8vIGJsb2F0aW5nIHBhZ2VzLmpzb24gYW5kIHNpdGVtYXAuIENyYXdsaW5nIGZyb20gdGhlIHNlZWQgYWxyZWFkeVxuICAgICAgLy8gcmVhY2hlcyBldmVyeSBsaW5rZWQgcGFnZS5cbiAgICAgIHByZXJlbmRlcjoge1xuICAgICAgICBlbmFibGVkOiB0cnVlLFxuICAgICAgICBjcmF3bExpbmtzOiB0cnVlLFxuICAgICAgICBhdXRvU3RhdGljUGF0aHNEaXNjb3Zlcnk6IGZhbHNlLFxuICAgICAgfSxcbiAgICAgIHNpdGVtYXA6IGNvbmZpZy5zaXRlID8geyBlbmFibGVkOiB0cnVlLCBob3N0OiBjb25maWcuc2l0ZSB9IDogeyBlbmFibGVkOiBmYWxzZSB9LFxuICAgICAgcm91dGVyOiB7XG4gICAgICAgIGdlbmVyYXRlZFJvdXRlVHJlZTogXCJyb3V0ZVRyZWUuZ2VuLnRzXCIsXG4gICAgICAgIGJhc2VwYXRoOiBjb25maWcuYmFzZSxcbiAgICAgIH0sXG4gICAgfSksXG4gICAgcmVhY3QoKSxcbiAgICBzdmdyKCksXG4gIF0sXG59KVxuIl19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livemark",
3
- "version": "0.28.3",
3
+ "version": "0.28.5",
4
4
  "description": "Livemark description",
5
5
  "license": "MIT",
6
6
  "author": "Evgeny Karev",
@@ -30,8 +30,8 @@
30
30
  "@base-ui/react": "1.9.0",
31
31
  "@clack/prompts": "1.8.1",
32
32
  "@content-collections/core": "0.15.3",
33
+ "@content-collections/integrations": "0.5.0",
33
34
  "@content-collections/mdx": "0.2.2",
34
- "@content-collections/vite": "0.3.1",
35
35
  "@orama/orama": "^3.1.18",
36
36
  "@shikijs/rehype": "4.5.0",
37
37
  "@shikijs/twoslash": "^4.5.0",
@@ -80,7 +80,7 @@ export default defineConfig({
80
80
 
81
81
  - **Article** (default when `type` is omitted): flat docs with a tree sidebar.
82
82
  - **Blog** (`type: "blog"`): auto-generated index at `prefix`, sorts by `date`, exposes `/prefix/tags/<tag>/` pages.
83
- - **Changelog** (`type: "changelog"`): reads `source` file; with `version: true`, splits on semver headings into per-version articles. Several changelog sections may coexist as long as their prefixes differ — each is cached in its own `cache/changelog/<slug-of-prefix>-<hash-of-prefix>/` directory (the hash keeps prefixes that slugify alike, like `/python/changelog/` and `/python-changelog/`, apart), so a version shared by two changelogs never collides. The `(version)` suffix only renders for `position: "header"` sections.
83
+ - **Changelog** (`type: "changelog"`): reads `source` file; with `version: true`, splits on semver headings into per-version articles. Several changelog sections may coexist as long as their prefixes differ — each is cached in its own `.livemark/.cache/changelog/<slug-of-prefix>-<hash-of-prefix>/` directory (the hash keeps prefixes that slugify alike, like `/python/changelog/` and `/python-changelog/`, apart), so a version shared by two changelogs never collides. The `(version)` suffix only renders for `position: "header"` sections.
84
84
 
85
85
  Shared fields on every section: `title`, `prefix` (keep trailing-slashed), `type`, `icon` (lucide), `position` (`"header"` default, `"sidebar"` also available). Header sections and links never overflow: past the bar's width Search turns icon-only, then the rest fold into a "More" menu.
86
86
 
@@ -118,7 +118,7 @@ Custom routes win over the Markdown catch-all at exact URLs. Don't create a rout
118
118
  3. **Override files are full replacements**, not diffs. Copy the default first (via `escape`), then edit.
119
119
  4. **Import Livemark internals via `livemark/*`**, not via relative paths — the plugin resolves those to the project's copy, so chained overrides (e.g., your Layout using your Header) compose correctly.
120
120
  5. **Static files belong in `.livemark/public/`**, which Vite exposes at the site root.
121
- 6. **Never edit `.livemark/.target/`** — it is the generated mount Vite runs (Livemark's source with your overrides copied in), rebuilt on every run, so stack traces point there but edits are lost. Gitignore it alongside `.livemark/build/`.
121
+ 6. **Never edit `.livemark/.serve/` or `.livemark/.build/`** — they are the generated mounts Vite runs for the dev server and the production build (Livemark's source with your overrides copied in), rebuilt on every run, so stack traces point there but edits are lost. Each carries its own `.gitignore`.
122
122
  7. **Keyboard shortcuts live in the component they drive** — bare keys through `@tanstack/react-hotkeys`, ignored while typing: `L` sidebar (`Sidebar`), `S` search (`Search`), `T` theme (`Theme`), `J`/`K` scroll (`Root`), `B` back to top (`BackToTop`). Overriding one of these drops its key unless the copy keeps the `useHotkey` call; register a key in a component that mounts once, or one press fires twice.
123
123
  8. **Search highlights live in `Article`** — picking a result opens the page with `?highlight=<query>`, and `Article` passes it to `useSearchHighlight` with a ref on its `.prose` wrapper. An overridden `components/Article.tsx` must keep that call and ref, or matches stop being marked. Theme the mark through the `--highlight` and `--highlight-foreground` tokens; code blocks are never marked.
124
124
  9. **Edited Markdown reaches the page through `router.tsx`** — articles are route loader data, and route HMR keeps loader data, so `getRouter` re-runs loaders on Vite's `vite:afterUpdate`. An overridden `router.tsx` must keep that hook, or Markdown edits show only after a reload.
@@ -23,7 +23,7 @@ npx livemark build # prerender every route into .livemark/build/client/
23
23
  npx livemark preview # serve that build locally on port 8000
24
24
  ```
25
25
 
26
- `.livemark/build/client/` is the **only** directory to publish: an `index.html` per route plus assets, `sitemap.xml`, `robots.txt`, RSS feeds, Markdown twins, `llms.txt` and a `_headers` file. Not `dist/`, and not `.livemark/build/` itself — its `server/` sibling is the prerender bundle, not a site. Gitignore `.livemark/build/` and `.livemark/.target/`; both are regenerated on every build.
26
+ `.livemark/build/client/` is the **only** directory to publish: an `index.html` per route plus assets, `sitemap.xml`, `robots.txt`, RSS feeds, Markdown twins, `llms.txt` and a `_headers` file. Not `dist/`, and not `.livemark/build/` itself — its `server/` sibling is the prerender bundle, not a site. Gitignore `.livemark/build/`; it is regenerated on every build.
27
27
 
28
28
  ## Config that affects deployment
29
29
 
@@ -11,7 +11,7 @@ import { visit } from "unist-util-visit"
11
11
  import type { Config } from "../../models/config.ts"
12
12
  import type { ChangelogSection } from "../../models/section.ts"
13
13
  import { isGitHubUrl, parseGitHubRepo } from "../../helpers/changelog-source.ts"
14
- import { targetDirFor } from "../target/build.ts"
14
+ import { cacheDirFor } from "../../helpers/target-dir.ts"
15
15
 
16
16
  interface Release {
17
17
  tag_name: string
@@ -34,8 +34,6 @@ interface ChangelogEntry {
34
34
  sourceUrl?: string
35
35
  }
36
36
 
37
- const CACHE_SUBDIR = "cache/changelog"
38
-
39
37
  const parser = unified().use(remarkParse)
40
38
  const stringifier = unified().use(remarkStringify, {
41
39
  bullet: "-",
@@ -52,7 +50,7 @@ export function cacheIncludeGlob(section: ChangelogSection, config: Config) {
52
50
 
53
51
  /** Directory holding one changelog section's cache state: the rendered
54
52
  * `.md` entries plus a `meta.json` holding the etag for conditional
55
- * GETs. Each section gets its own subdirectory of `cache/changelog/`,
53
+ * GETs. Each section gets its own subdirectory of `.livemark/.cache/changelog/`,
56
54
  * keyed by its prefix's slug plus a hash of the raw prefix
57
55
  * (`/python/changelog/` → `python-changelog-<hash>`), so several
58
56
  * changelogs never overwrite each other's entries even when their
@@ -60,7 +58,7 @@ export function cacheIncludeGlob(section: ChangelogSection, config: Config) {
60
58
  export function changelogCacheDir(section: ChangelogSection, config: Config) {
61
59
  const slug = slugify(section.prefix) || "index"
62
60
  const hash = createHash("sha256").update(section.prefix).digest("hex").slice(0, 8)
63
- return join(targetDirFor(config.configPath), CACHE_SUBDIR, `${slug}-${hash}`)
61
+ return join(cacheDirFor(config.configPath), "changelog", `${slug}-${hash}`)
64
62
  }
65
63
 
66
64
  /** Build the per-version changelog cache files of one changelog section
@@ -77,6 +75,7 @@ export async function buildChangelog(section: ChangelogSection, config: Config)
77
75
  if (entries === undefined) return
78
76
 
79
77
  await mkdir(cacheDir, { recursive: true })
78
+ await writeFile(join(cacheDirFor(config.configPath), ".gitignore"), "*\n")
80
79
  for (const f of await readdir(cacheDir).catch(() => [])) {
81
80
  if (f.endsWith(".md")) await rm(join(cacheDir, f), { force: true })
82
81
  }
@@ -5,7 +5,7 @@ import { describe, expect, it } from "vite-plus/test"
5
5
  import { partitionBySection } from "../../helpers/section.ts"
6
6
  import { Config } from "../../models/config.ts"
7
7
  import type { ChangelogSection } from "../../models/section.ts"
8
- import { targetDirFor } from "../target/build.ts"
8
+ import { cacheDirFor } from "../../helpers/target-dir.ts"
9
9
  import {
10
10
  buildChangelog,
11
11
  cacheIncludeGlob,
@@ -49,7 +49,7 @@ describe("buildChangelog", () => {
49
49
 
50
50
  const standardDir = changelogCacheDir(standard, config)
51
51
  const pythonDir = changelogCacheDir(python, config)
52
- const cacheRoot = join(targetDirFor(configPath), "cache/changelog")
52
+ const cacheRoot = join(cacheDirFor(configPath), "changelog")
53
53
  expect(dirname(standardDir)).toBe(cacheRoot)
54
54
  expect(dirname(pythonDir)).toBe(cacheRoot)
55
55
  expect(basename(standardDir)).toMatch(/^changelog-[0-9a-f]{8}$/)
@@ -79,7 +79,7 @@ describe("buildChangelog", () => {
79
79
  "/python/changelog/0-2-0/",
80
80
  ])
81
81
  } finally {
82
- await rm(targetDirFor(configPath), { recursive: true, force: true })
82
+ await rm(cacheDirFor(configPath), { recursive: true, force: true })
83
83
  }
84
84
  }))
85
85
 
@@ -114,7 +114,7 @@ describe("buildChangelog", () => {
114
114
  expect(await readdir(changelogCacheDir(standard, config))).toEqual(["0-4-0.md"])
115
115
  expect(await readdir(changelogCacheDir(python, config))).toEqual(["0-3-0.md"])
116
116
  } finally {
117
- await rm(targetDirFor(configPath), { recursive: true, force: true })
117
+ await rm(cacheDirFor(configPath), { recursive: true, force: true })
118
118
  }
119
119
  }))
120
120
 
@@ -150,7 +150,7 @@ describe("buildChangelog", () => {
150
150
  expect(await readdir(nestedDir)).toEqual(["0-1-0.md"])
151
151
  expect(await readdir(flatDir)).toEqual(["0-2-0.md"])
152
152
  } finally {
153
- await rm(targetDirFor(configPath), { recursive: true, force: true })
153
+ await rm(cacheDirFor(configPath), { recursive: true, force: true })
154
154
  }
155
155
  }))
156
156
  })
@@ -1,18 +1,35 @@
1
- import { cpSync, existsSync, mkdirSync, readlinkSync, rmSync, symlinkSync } from "node:fs"
1
+ import {
2
+ cpSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readlinkSync,
6
+ rmSync,
7
+ symlinkSync,
8
+ writeFileSync,
9
+ } from "node:fs"
2
10
  import { basename, dirname, join, relative, resolve } from "node:path"
3
11
  import { resolveDependencyDir } from "../../helpers/dependency-dir.ts"
12
+ import { removeTargetDir } from "../../helpers/target-dir.ts"
4
13
  import { ESCAPABLE_FILES, ESCAPABLE_FOLDERS } from "../../settings.ts"
5
14
 
6
15
  /**
7
- * Synthesize the project's Vite mount at `.livemark/.target/`: livemark's `source/` with
8
- * the project's `.livemark/` escapes copied over it, plus a `node_modules` link to
9
- * livemark's dependencies. Escapable folders are rebuilt on every run, so a removed escape
10
- * never keeps rendering. Returns the target dir.
16
+ * Synthesize the project's Vite mount — `.livemark/.serve/` for the dev server,
17
+ * `.livemark/.build/` for a production build or preview: livemark's `source/` with the
18
+ * project's `.livemark/` escapes copied over it, plus a `node_modules` link to livemark's
19
+ * dependencies. Escapable folders are rebuilt on every run, so a removed escape never
20
+ * keeps rendering. Returns the target dir.
11
21
  */
12
- export function buildTarget(props: { configPath: string; sourceDir: string }) {
13
- const targetDir = targetDirFor(props.configPath)
22
+ export function buildTarget(props: {
23
+ configPath: string
24
+ sourceDir: string
25
+ command?: TargetCommand
26
+ }) {
27
+ const targetDir = targetDirFor(props.configPath, props.command)
14
28
  const overridesRoot = dirname(targetDir)
29
+ // `.target/` was the mount before `.serve/` and `.build/`; an upgrade must not keep it.
30
+ removeTargetDir(join(overridesRoot, ".target"))
15
31
  mkdirSync(targetDir, { recursive: true })
32
+ writeFileSync(join(targetDir, ".gitignore"), "*\n")
16
33
 
17
34
  cpSync(props.sourceDir, targetDir, {
18
35
  recursive: true,
@@ -36,9 +53,12 @@ export function buildTarget(props: { configPath: string; sourceDir: string }) {
36
53
  return targetDir
37
54
  }
38
55
 
56
+ /** Which mount a run uses: the dev server's, or the production build's. */
57
+ export type TargetCommand = "serve" | "build"
58
+
39
59
  /** Locate the project's target dir without building it. */
40
- export function targetDirFor(configPath: string) {
41
- return join(dirname(configPath), ".livemark", ".target")
60
+ export function targetDirFor(configPath: string, command: TargetCommand = "serve") {
61
+ return join(dirname(configPath), ".livemark", `.${command}`)
42
62
  }
43
63
 
44
64
  function isPackageFile(src: string) {
@@ -14,7 +14,7 @@ describe("buildTarget", () => {
14
14
 
15
15
  const targetDir = buildTarget({ configPath, sourceDir })
16
16
 
17
- expect(targetDir).toBe(join(root, "project/.livemark/.target"))
17
+ expect(targetDir).toBe(join(root, "project/.livemark/.serve"))
18
18
  expect(read(targetDir, "components/Footer.tsx")).toBe("escaped")
19
19
  expect(read(targetDir, "components/Header.tsx")).toBe("default")
20
20
  expect(read(targetDir, "head.ts")).toBe("escaped head")
@@ -22,6 +22,29 @@ describe("buildTarget", () => {
22
22
  expect(existsSync(join(targetDir, "helpers/util.unit.ts"))).toBe(false)
23
23
  }))
24
24
 
25
+ it("mounts a production build apart from the dev server's target", () =>
26
+ temporaryDirectoryTask(async root => {
27
+ const { configPath, sourceDir } = await createFixture(root)
28
+
29
+ const targetDir = buildTarget({ configPath, sourceDir, command: "build" })
30
+
31
+ expect(targetDir).toBe(join(root, "project/.livemark/.build"))
32
+ expect(read(targetDir, "routes/index.tsx")).toBe("index")
33
+ expect(existsSync(targetDirFor(configPath))).toBe(false)
34
+ }))
35
+
36
+ it("ignores itself in git and removes the legacy .target mount", () =>
37
+ temporaryDirectoryTask(async root => {
38
+ const { configPath, sourceDir } = await createFixture(root)
39
+ const legacyDir = join(root, "project/.livemark/.target")
40
+ await put(join(legacyDir, "routes/index.tsx"), "stale")
41
+
42
+ const targetDir = buildTarget({ configPath, sourceDir })
43
+
44
+ expect(read(targetDir, ".gitignore")).toBe("*\n")
45
+ expect(existsSync(legacyDir)).toBe(false)
46
+ }))
47
+
25
48
  it("drops escapes removed since the last run", () =>
26
49
  temporaryDirectoryTask(async root => {
27
50
  const { configPath, sourceDir } = await createFixture(root)
@@ -1,14 +1,15 @@
1
- import { rmSync } from "node:fs"
2
- import { join } from "node:path"
1
+ import { cacheDirFor, removeTargetDir } from "../../helpers/target-dir.ts"
3
2
  import { targetDirFor } from "./build.ts"
4
3
 
5
4
  /**
6
- * Wipe the project's target dir so the next run rebuilds it from scratch.
5
+ * Wipe the project's target dirs and cache so the next run rebuilds them from scratch.
7
6
  */
8
7
  export function clearTarget(configPath: string) {
9
- const targetDir = targetDirFor(configPath)
10
- // Unlink the dependency link first so the recursive remove cannot reach the store.
11
- rmSync(join(targetDir, "node_modules"), { force: true })
12
- rmSync(targetDir, { recursive: true, force: true })
13
- return targetDir
8
+ const targetDirs = [
9
+ targetDirFor(configPath, "serve"),
10
+ targetDirFor(configPath, "build"),
11
+ cacheDirFor(configPath),
12
+ ]
13
+ for (const targetDir of targetDirs) removeTargetDir(targetDir)
14
+ return targetDirs
14
15
  }
@@ -3,22 +3,29 @@ import { mkdir, symlink, writeFile } from "node:fs/promises"
3
3
  import { join } from "node:path"
4
4
  import { temporaryDirectoryTask } from "tempy"
5
5
  import { describe, expect, it } from "vite-plus/test"
6
+ import { cacheDirFor } from "../../helpers/target-dir.ts"
6
7
  import { targetDirFor } from "./build.ts"
7
8
  import { clearTarget } from "./clear.ts"
8
9
 
9
10
  describe("clearTarget", () => {
10
- it("removes the target dir without following its dependency link", () =>
11
+ it("removes the target dirs without following their dependency link", () =>
11
12
  temporaryDirectoryTask(async root => {
12
13
  const configPath = join(root, "livemark.config.ts")
13
14
  const targetDir = targetDirFor(configPath)
15
+ const buildDir = targetDirFor(configPath, "build")
16
+ const cacheDir = cacheDirFor(configPath)
17
+ await mkdir(buildDir, { recursive: true })
18
+ await mkdir(cacheDir, { recursive: true })
14
19
  await mkdir(join(targetDir, "cache"), { recursive: true })
15
20
  await writeFile(join(targetDir, "cache", "entry.md"), "cached")
16
21
  await mkdir(join(root, "deps"))
17
22
  await writeFile(join(root, "deps", "kept.js"), "")
18
23
  await symlink(join(root, "deps"), join(targetDir, "node_modules"), "junction")
19
24
 
20
- expect(clearTarget(configPath)).toBe(targetDir)
25
+ expect(clearTarget(configPath)).toEqual([targetDir, buildDir, cacheDir])
21
26
  expect(existsSync(targetDir)).toBe(false)
27
+ expect(existsSync(buildDir)).toBe(false)
28
+ expect(existsSync(cacheDir)).toBe(false)
22
29
  expect(existsSync(join(root, "deps", "kept.js"))).toBe(true)
23
30
  expect(() => clearTarget(configPath)).not.toThrow()
24
31
  }))
@@ -4,12 +4,13 @@ import { loadConfig } from "../actions/config/load.ts"
4
4
  import { clearTarget } from "../actions/target/clear.ts"
5
5
 
6
6
  /**
7
- * Wipe the generated `.livemark/.target/` mount.
7
+ * Wipe the generated `.livemark/.serve/` and `.livemark/.build/` mounts and `.livemark/.cache/`.
8
8
  */
9
9
  export const clear = new Command("clear")
10
- .description("Wipe the generated .livemark/.target/ mount")
10
+ .description("Wipe the generated .livemark/.serve/, .build/ and .cache/")
11
11
  .action(async () => {
12
12
  const config = await loadConfig()
13
- const targetDir = clearTarget(config.configPath)
14
- console.log(`Cleared ${pc.dim(targetDir)}`)
13
+ for (const targetDir of clearTarget(config.configPath)) {
14
+ console.log(`Cleared ${pc.dim(targetDir)}`)
15
+ }
15
16
  })
@@ -1,14 +1,14 @@
1
1
  import { describe, expect, it } from "vite-plus/test"
2
2
  import { excludeTailwindSources } from "./tailwind-sources.ts"
3
3
 
4
- const targetDir = "/project/.livemark/.target"
4
+ const targetDir = "/project/.livemark/.serve"
5
5
 
6
6
  describe("excludeTailwindSources", () => {
7
7
  it("excludes the dependency link and the content cache from a Tailwind entry", () => {
8
8
  const css = `@import "tailwindcss";\n@import "./shape.css";`
9
9
  expect(excludeTailwindSources({ css, targetDir })).toBe(
10
- `${css}\n@source not "/project/.livemark/.target/node_modules";\n` +
11
- `@source not "/project/.livemark/.target/.content-collections/cache";\n`,
10
+ `${css}\n@source not "/project/.livemark/.serve/node_modules";\n` +
11
+ `@source not "/project/.livemark/.serve/.content-collections/cache";\n`,
12
12
  )
13
13
  })
14
14
 
@@ -0,0 +1,12 @@
1
+ import { rmSync } from "node:fs"
2
+ import { dirname, join } from "node:path"
3
+
4
+ export function removeTargetDir(targetDir: string) {
5
+ // Unlink the dependency link first so the recursive remove cannot reach the store.
6
+ rmSync(join(targetDir, "node_modules"), { force: true })
7
+ rmSync(targetDir, { recursive: true, force: true })
8
+ }
9
+
10
+ export function cacheDirFor(configPath: string) {
11
+ return join(dirname(configPath), ".livemark", ".cache")
12
+ }
@@ -3,7 +3,7 @@ import { describe, expect, it } from "vite-plus/test"
3
3
  import packageJson from "../../package.json" with { type: "json" }
4
4
  import { resolveTargetExport } from "./target-export.ts"
5
5
 
6
- const targetDir = "/project/.livemark/.target"
6
+ const targetDir = "/project/.livemark/.serve"
7
7
 
8
8
  function resolveId(id: string) {
9
9
  return resolveTargetExport({ id, targetDir, exports: packageJson.exports })
@@ -0,0 +1,28 @@
1
+ /** Run keyed async tasks one at a time; a key enqueued again before its task
2
+ * starts replaces it, so a burst of events for one file costs one run. */
3
+ export function createTaskQueue(props: { onError: (error: unknown) => void }) {
4
+ const pending = new Map<string, () => Promise<unknown>>()
5
+ let draining: Promise<void> | undefined
6
+
7
+ return {
8
+ enqueue(key: string, task: () => Promise<unknown>) {
9
+ pending.set(key, task)
10
+ draining ??= drain()
11
+ return draining
12
+ },
13
+ }
14
+
15
+ async function drain() {
16
+ // A Map iterator also visits entries set during iteration, which is what
17
+ // picks up keys enqueued while a task runs.
18
+ for (const [key, task] of pending) {
19
+ pending.delete(key)
20
+ try {
21
+ await task()
22
+ } catch (error) {
23
+ props.onError(error)
24
+ }
25
+ }
26
+ draining = undefined
27
+ }
28
+ }
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from "vite-plus/test"
2
+ import { createTaskQueue } from "./task-queue.ts"
3
+
4
+ describe("createTaskQueue", () => {
5
+ it("runs one task at a time", async () => {
6
+ const queue = createTaskQueue({ onError: () => {} })
7
+ const log: string[] = []
8
+ const task = (name: string, ms: number) => async () => {
9
+ log.push(`start ${name}`)
10
+ await new Promise(resolve => setTimeout(resolve, ms))
11
+ log.push(`end ${name}`)
12
+ }
13
+ queue.enqueue("a", task("a", 30))
14
+ await queue.enqueue("b", task("b", 1))
15
+ expect(log).toEqual(["start a", "end a", "start b", "end b"])
16
+ })
17
+
18
+ it("collapses a key enqueued again before it starts", async () => {
19
+ const queue = createTaskQueue({ onError: () => {} })
20
+ const runs: string[] = []
21
+ queue.enqueue("first", async () => runs.push("first"))
22
+ queue.enqueue("file", async () => runs.push("stale"))
23
+ await queue.enqueue("file", async () => runs.push("latest"))
24
+ expect(runs).toEqual(["first", "latest"])
25
+ })
26
+
27
+ it("reruns a key enqueued while it is running", async () => {
28
+ const queue = createTaskQueue({ onError: () => {} })
29
+ const runs: number[] = []
30
+ let release = () => {}
31
+ queue.enqueue("file", async () => {
32
+ runs.push(1)
33
+ await new Promise<void>(resolve => (release = resolve))
34
+ })
35
+ const done = queue.enqueue("file", async () => runs.push(2))
36
+ release()
37
+ await done
38
+ expect(runs).toEqual([1, 2])
39
+ })
40
+
41
+ it("reports a failed task and keeps draining", async () => {
42
+ const errors: unknown[] = []
43
+ const queue = createTaskQueue({ onError: error => errors.push(error) })
44
+ const runs: string[] = []
45
+ queue.enqueue("a", async () => {
46
+ throw new Error("boom")
47
+ })
48
+ await queue.enqueue("b", async () => runs.push("b"))
49
+ expect(errors).toHaveLength(1)
50
+ expect(runs).toEqual(["b"])
51
+ })
52
+
53
+ it("starts a fresh drain after the previous one finished", async () => {
54
+ const queue = createTaskQueue({ onError: () => {} })
55
+ const runs: string[] = []
56
+ await queue.enqueue("a", async () => runs.push("a"))
57
+ await queue.enqueue("a", async () => runs.push("again"))
58
+ expect(runs).toEqual(["a", "again"])
59
+ })
60
+ })
@@ -0,0 +1,72 @@
1
+ import { existsSync, watch, type FSWatcher } from "node:fs"
2
+ import { dirname, join, resolve } from "node:path"
3
+ import { createBuilder } from "@content-collections/core"
4
+ import { configureLogging } from "@content-collections/integrations"
5
+ import type { Plugin } from "vite"
6
+ import { createTaskQueue } from "../helpers/task-queue.ts"
7
+
8
+ export interface ContentOptions {
9
+ configPath: string
10
+ contentDir: string
11
+ }
12
+
13
+ type Builder = Awaited<ReturnType<typeof createBuilder>>
14
+
15
+ const IGNORED_PATHS = /(^|[/\\])(node_modules|\.[^/\\]+)([/\\]|$)/
16
+
17
+ /** Build the content collections and rebuild them on edits, one build at a time:
18
+ * `@content-collections/vite` builds per file event, a save fires two, and their
19
+ * overlapping writes tore `allArticles.js` so every request failed. */
20
+ export function content(opts: ContentOptions): Plugin {
21
+ const queue = createTaskQueue({ onError: error => console.error(error) })
22
+ let builder: Builder | undefined
23
+ let initialBuild: Promise<void> | undefined
24
+ let watcher: FSWatcher | undefined
25
+ return {
26
+ name: "livemark:content",
27
+ config(config) {
28
+ const configPath = resolve(config.root ?? process.cwd(), opts.configPath)
29
+ const generated = join(dirname(configPath), ".content-collections", "generated")
30
+ return {
31
+ optimizeDeps: { exclude: ["content-collections"] },
32
+ resolve: { alias: { "content-collections": generated } },
33
+ ...((config.server?.fs?.allow?.length ?? 0) > 0 && {
34
+ server: { fs: { allow: [generated] } },
35
+ }),
36
+ }
37
+ },
38
+ async configResolved(config) {
39
+ builder = await createBuilder(resolve(config.root, opts.configPath))
40
+ configureLogging(builder)
41
+ },
42
+ buildStart() {
43
+ const current = builder
44
+ if (!current) return
45
+ // Once, not per environment, and through the queue so an edit landing
46
+ // before it cannot overlap it.
47
+ initialBuild ??= new Promise((resolve, reject) => {
48
+ queue.enqueue("build", () => current.build().then(resolve, reject))
49
+ })
50
+ return initialBuild
51
+ },
52
+ configureServer() {
53
+ const current = builder
54
+ if (!current) return
55
+ watcher = watch(
56
+ opts.contentDir,
57
+ { recursive: true, ignore: IGNORED_PATHS },
58
+ (_, filename) => {
59
+ if (!filename) return
60
+ const file = join(opts.contentDir, filename)
61
+ queue.enqueue(file, () =>
62
+ current.sync(existsSync(file) ? "update" : "delete", file),
63
+ )
64
+ },
65
+ )
66
+ },
67
+ closeBundle() {
68
+ watcher?.close()
69
+ watcher = undefined
70
+ },
71
+ }
72
+ }
@@ -42,7 +42,8 @@ const LOCKFILES = [
42
42
  const VIRTUAL_ID = "livemark:virtual"
43
43
  const VIRTUAL_RESOLVED_ID = `\0${VIRTUAL_ID}`
44
44
 
45
- /** Owns the project's `.livemark/.target/` mount: synthesizes it via
45
+ /** Owns the project's `.livemark/.serve/` mount (`.build/` when building, so a build
46
+ * never rewrites a running dev server's files): synthesizes it via
46
47
  * `buildTarget()`, returns the Vite config that anchors there, and
47
48
  * copies `.livemark/` edits (plus livemark's `source/` in its own dev)
48
49
  * into the target. Vite watches the target natively, so its own HMR
@@ -58,6 +59,7 @@ export function livemark(opts: LivemarkOptions): Plugin {
58
59
  targetDir = buildTarget({
59
60
  configPath: opts.config.configPath,
60
61
  sourceDir: SOURCE_DIR,
62
+ command: env.command === "build" || env.isPreview ? "build" : "serve",
61
63
  })
62
64
  const base: ViteUserConfig = {
63
65
  root: targetDir,
@@ -137,14 +139,22 @@ export function livemark(opts: LivemarkOptions): Plugin {
137
139
  return
138
140
  }
139
141
  if (isWithin(file, targetDir) || isWithin(file, outDir)) return
140
- const overlayHit = matchPath(file, overridesRoot)
141
- if (overlayHit) {
142
- syncOverlay(overlayHit, kind, targetDir, overridesRoot)
143
- return
144
- }
145
- const sourceHit = matchPath(file, SOURCE_DIR)
146
- if (sourceHit && !sourceHit.rel.includes("routeTree.gen.ts")) {
147
- syncSource(sourceHit, kind, targetDir, overridesRoot)
142
+ // A watcher callback that throws kills the server, and a copy racing another
143
+ // writer of the target (an editor's atomic save, `livemark clear`) fails.
144
+ try {
145
+ const overlayHit = matchPath(file, overridesRoot)
146
+ if (overlayHit) {
147
+ syncOverlay(overlayHit, kind, targetDir, overridesRoot)
148
+ return
149
+ }
150
+ const sourceHit = matchPath(file, SOURCE_DIR)
151
+ if (sourceHit && !sourceHit.rel.includes("routeTree.gen.ts")) {
152
+ syncSource(sourceHit, kind, targetDir, overridesRoot)
153
+ }
154
+ } catch (error) {
155
+ server.config.logger.error(`[livemark] failed to sync ${file}: ${error}`, {
156
+ error: error instanceof Error ? error : undefined,
157
+ })
148
158
  }
149
159
  }
150
160
  },