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.
- package/README.md +2 -2
- package/build/actions/changelog/build.d.ts +1 -1
- package/build/actions/changelog/build.js +5 -5
- package/build/actions/changelog/build.unit.js +6 -6
- package/build/actions/target/build.d.ts +9 -5
- package/build/actions/target/build.js +14 -9
- package/build/actions/target/build.unit.js +17 -2
- package/build/actions/target/clear.d.ts +2 -2
- package/build/actions/target/clear.js +11 -9
- package/build/actions/target/clear.unit.js +10 -3
- package/build/commands/clear.d.ts +1 -1
- package/build/commands/clear.js +6 -5
- package/build/helpers/tailwind-sources.unit.js +4 -4
- package/build/helpers/target-dir.d.ts +2 -0
- package/build/helpers/target-dir.js +11 -0
- package/build/helpers/target-export.unit.js +2 -2
- package/build/helpers/task-queue.d.ts +7 -0
- package/build/helpers/task-queue.js +28 -0
- package/build/helpers/task-queue.unit.d.ts +1 -0
- package/build/helpers/task-queue.unit.js +56 -0
- package/build/plugins/vite-content.d.ts +9 -0
- package/build/plugins/vite-content.js +60 -0
- package/build/plugins/vite-livemark.d.ts +2 -1
- package/build/plugins/vite-livemark.js +20 -9
- package/build/vite.config.js +3 -3
- package/package.json +2 -2
- package/skills/livemark-configuration/SKILL.md +1 -1
- package/skills/livemark-customization/SKILL.md +1 -1
- package/skills/livemark-deployment/SKILL.md +1 -1
- package/source/actions/changelog/build.ts +4 -5
- package/source/actions/changelog/build.unit.ts +5 -5
- package/source/actions/target/build.ts +29 -9
- package/source/actions/target/build.unit.ts +24 -1
- package/source/actions/target/clear.ts +9 -8
- package/source/actions/target/clear.unit.ts +9 -2
- package/source/commands/clear.ts +5 -4
- package/source/helpers/tailwind-sources.unit.ts +3 -3
- package/source/helpers/target-dir.ts +12 -0
- package/source/helpers/target-export.unit.ts +1 -1
- package/source/helpers/task-queue.ts +28 -0
- package/source/helpers/task-queue.unit.ts +60 -0
- package/source/plugins/vite-content.ts +72 -0
- package/source/plugins/vite-livemark.ts +19 -9
- package/source/vite.config.ts +2 -2
package/build/vite.config.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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
|
+
"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
|
|
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/.
|
|
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
|
|
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 {
|
|
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
|
|
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(
|
|
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 {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
153
|
+
await rm(cacheDirFor(configPath), { recursive: true, force: true })
|
|
154
154
|
}
|
|
155
155
|
}))
|
|
156
156
|
})
|
|
@@ -1,18 +1,35 @@
|
|
|
1
|
-
import {
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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: {
|
|
13
|
-
|
|
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",
|
|
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/.
|
|
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 {
|
|
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
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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)).
|
|
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
|
}))
|
package/source/commands/clear.ts
CHANGED
|
@@ -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/.
|
|
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/.
|
|
10
|
+
.description("Wipe the generated .livemark/.serve/, .build/ and .cache/")
|
|
11
11
|
.action(async () => {
|
|
12
12
|
const config = await loadConfig()
|
|
13
|
-
const targetDir
|
|
14
|
-
|
|
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/.
|
|
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/.
|
|
11
|
-
`@source not "/project/.livemark/.
|
|
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/.
|
|
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/.
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
},
|