Skip to content

Commit 206c4a5

Browse files
authored
Merge pull request #39 from PureStackStudio/perf-cache-for-plugins
Reuse highlighted code across builds through a shared persistent cache.
2 parents ee733bb + 531087d commit 206c4a5

45 files changed

Lines changed: 1948 additions & 138 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CONTRIBUTING.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,25 @@ enforce branch protection. See GitHub's [ruleset documentation](https://docs.git
5353
Dependabot opens weekly pull requests for GitHub Actions and dependencies. Review
5454
them through the same CI and pull request process as other changes.
5555

56+
To publish one package manually, log in to npm once and run the command from the
57+
repository root:
58+
59+
```sh
60+
npm login --registry=https://registry.npmjs.org
61+
yarn publish-package ts-images
62+
```
63+
64+
Both short names (`ts-images`) and full names (`@purestack/ts-images`, `purestack`)
65+
are accepted. The command rebuilds and verifies all distributable bundles, packs
66+
only the selected workspace with Yarn to resolve its `workspace:` dependencies,
67+
then publishes its current version publicly to npm with the `latest` tag. npm's
68+
authentication prompts remain interactive. It does not bump versions or publish
69+
dependencies; the package's dependency versions must already be available on npm.
70+
71+
Use `yarn publish-package ts-images --dry-run` to build, pack, and inspect npm's
72+
publish preview without uploading anything. An existing name/version cannot be
73+
published again; update the version through the normal release process first.
74+
5675
The existing **Publish new npm packages** workflow remains a manual operation.
5776
It only publishes packages missing from npm; it is not a version-release workflow.
5877
Use its default dry run first and review the registry results before publishing.

‎frontend/purestack.studio/guides/extending/plugins.mdx‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ This keeps the layouts and components while omitting automatic social metadata,
185185

186186
An explicitly listed standard plugin replaces the automatic instance by name and keeps its initialization position. You can also use `createDefaultPlugins()` to obtain the complete set. In programmatic builds, use `options.defaultPlugins` alongside `options.plugins`.
187187

188-
The highlighting plugin selects the configured backend. Configure native backend factories through `highlightPlugin({ createShiki, createHighlightJs })`, or supply `markdown.createHighlighter(config)` for a different engine. The generic provider returns `{ codeToHtml(code, lang?) }`. Ordinary providers override the default without constructing it; per-language `markdown.resolveHighlighter` callbacks retain precedence.
188+
The highlighting plugin selects the configured backend. Configure native backend factories through `highlightPlugin({ createShiki, createHighlightJs })`, or supply `markdown.createHighlighter({ mdx, cache })` for a different engine. The generic provider returns `{ codeToHtml(code, lang?) }`. Ordinary providers override the default without constructing it; per-language `markdown.resolveHighlighter` callbacks retain precedence.
189189

190190
The Pagefind plugin supplies a `search` provider. To use another engine, replace `purestack:pagefind` or disable the default set and install your own provider. The builder supplies excluded output paths and the rendered-page list for lazy requests, and manages invalidation on edits and deletions. Sitemap and robots generation remain built in.
191191

@@ -236,6 +236,8 @@ export const brandPlugin = definePlugin({
236236

237237
`resolveSiteConfig(input, { plugins, defaultPlugins })` composes the supplied plugin instances with the standard plugins by default. This synchronous helper does not apply JSON plugin selection (`plugins` or `defaultPlugins`) or load `purestack.config.ts`. Plugin-owned sections (`style`, `docLayout`, `pageToc`, `logo`, `auth`, `consent`, `analytics`, `pagefind`, and `preview`) are optional in `SiteConfig`: an omitted plugin does not supply defaults or validate its unused input. A replacement plugin reads its settings from `input` and `file`, then contributes normalized config. Analytics also resolves explicitly configured consent when its browser runtime is omitted, so enabled consent is never bypassed. Configuration contributions are synchronous and may run whenever a project configuration is resolved; keep file writes and other side effects in lifecycle hooks.
238238

239+
`config.cache` is `false` when a run uses `--no-cache` or `options.cache: false`. A plugin that keeps a persistent cache outside the output directory should neither read nor write it then. Site settings cannot change this flag, and it never invalidates incremental output.
240+
239241
Builder style state is isolated across asynchronous operations, including concurrent sites. Its owned theme options remain linked to `context.config.style.theme`, so in-place config-hook changes and `themes.setOptions` update page colors, CSS, and generated favicons consistently. Head, metadata, template, Markdown, provider, and lifecycle errors identify their owning plugin and preserve the original cause. Markdown wrappers preserve unified presets, duplicate-plugin option merging, and callback transforms.
240242

241243
## Optimize images
@@ -617,6 +619,22 @@ When you publish the package:
617619
- **Ship JavaScript.** Packages load without bundling, so publish compiled JavaScript with type declarations, not TypeScript source.
618620
- **Share the site's PureStack.** List `purestack`, and any `@purestack/*` package the plugin imports, under `peerDependencies`, not `dependencies`. The plugin then uses the site's copy instead of installing its own.
619621

622+
## Cache build-time computations
623+
624+
Plugins and build-time components can use `PersistentCache` from
625+
`@purestack/ts-util-node` to reuse expensive results across builds. Create one
626+
cache per factory, use an owner namespace and implementation version, and include
627+
all output-affecting inputs in its keys. Pass `enabled: config.cache` to honor
628+
`--no-cache`.
629+
630+
Only cache computed values: dependency tracking, styles, and asset emission must
631+
still run when a value is reused. The cache batches small values into JSON shards
632+
and keeps bounded memory. Await `cache.flush()` after the computation batch;
633+
periodic development flushing does not keep Node alive. Highlighters can return
634+
an optional `flush()` method, which core awaits after each build or development
635+
render batch, including failures. See the
636+
[cache API and component example](https://github.com/PureStackStudio/PureStack/tree/main/packages/ts-util-node#persistent-build-cache).
637+
620638
## Build from a script
621639

622640
`buildSite` and `startDevServer` run the same build as the CLI, for a project that needs its own build script. They do not look for `purestack.config.ts`; pass plugins in `options.plugins`:

‎frontend/purestack.studio/guides/getting-started/purestack-cli.mdx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ yarn purestack build --content ./content --clean
115115

116116
`--clean` removes the configured `outDir` before rebuilding it. Use it when you need an output folder containing only files from the current build. Without `--clean`, PureStack keeps the directory and rewrites current outputs.
117117

118+
`--no-cache` bypasses reads and writes to persistent computation caches kept outside the output directory, such as highlighted code and optimized images. It works with `build`, `serve`, and `publish`. Use it to compare uncached rendering or investigate a stale cached result. Existing cache files stay in place; a cold build with caching enabled also spends time populating its cache.
119+
120+
During `serve`, pages still render on request and reuse their rendered output until invalidated. `--no-cache` affects computations when a page renders; it does not force rendering on every request. To rebuild into an empty output directory with persistent caches bypassed, combine `--clean` and `--no-cache` (`publish` already cleans its output directory).
121+
118122
### `serve`: preview and edit
119123

120124
```sh
@@ -136,6 +140,7 @@ These options belong to `serve`:
136140
| `--host <host>` | Listen on a particular interface, for example `127.0.0.1`. |
137141
| `--port <port>` | Listen on a different port. |
138142
| `--clean` | Remove `outDir` before setup and when config changes reset shared setup. |
143+
| `--no-cache` | Skip persistent caches, such as highlighted code and optimized images. |
139144
| `--full-render` | Build every page, asset, and the complete search index once at startup. Watched edits remain incremental. |
140145
| `--no-watch` | Keep the server running without watching the content directory. Pages still render on request, or all at startup with `--full-render`. |
141146
| `--no-reload` | Keep watching and updating the site, but do not inject the browser live reload script. |
@@ -150,7 +155,7 @@ yarn purestack publish --content ./content
150155

151156
`publish` uses `publishDir` instead of `outDir`. It cleans that directory before building, minifies generated HTML and browser scripts, disables pretty printed CSS, and treats asset build errors as failures. It creates the same site content and optional search and sitemap outputs as `build`, according to `siteConfig.json`.
152157

153-
Point your deployment process at `publishDir` after a successful run. `publish` prepares files locally; it does not upload them. It accepts `--content`, with no separate `--clean` flag because cleaning is built in.
158+
Point your deployment process at `publishDir` after a successful run. `publish` prepares files locally; it does not upload them. It accepts `--content` and `--no-cache`, with no separate `--clean` flag because cleaning is built in.
154159

155160
## Choose the public URL
156161

‎frontend/purestack.studio/guides/getting-started/site-config.mdx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,11 @@ Place this in `content/guides/_nav.json`. A custom item's `url` is relative to t
190190
}
191191
```
192192

193-
`mdx.highlighter` accepts `highlightjs` (the default and faster option) or `shiki`. `disableHighlighter` turns highlighting off. `compileMdAsMdx` defaults to `true`, so `.md` files use the MDX/Regor markup pipeline; `.mdx` files use that pipeline in either case. The [Regor guide](/guides/extending/regor/) explains the supported markup and components.
193+
`mdx.highlighter` accepts `highlightjs` (the default) or `shiki`. The standard Shiki backend caches highlighted HTML across builds in the OS temporary directory. Cache keys include the code, language, themes, loaded languages, and Shiki version. On a warm build, cached blocks reuse their HTML, and Shiki initializes only when a block needs highlighting. Output from a custom `createShiki` factory is not cached.
194+
195+
Use the CLI's [`--no-cache` flag](/guides/getting-started/purestack-cli/#build-generate-static-files) to bypass persistent caches for a run. This is a run option, not a `siteConfig.json` setting.
196+
197+
`disableHighlighter` turns highlighting off. `compileMdAsMdx` defaults to `true`, so `.md` files use the MDX/Regor markup pipeline; `.mdx` files use that pipeline in either case. The [Regor guide](/guides/extending/regor/) explains the supported markup and components.
194198

195199
## Search
196200

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"bump-version": "yarn tsx --no-warnings scripts/bump-version.ts",
5353
"bundle": "yarn tsx --no-warnings scripts/bundle.ts",
5454
"cleanup": "yarn tsx --no-warnings scripts/cleanup.ts",
55+
"publish-package": "yarn tsx --no-warnings scripts/publish-package.ts",
5556
"publish-new-packages": "yarn tsx --no-warnings scripts/publish-new-packages.ts",
5657
"test": "vitest",
5758
"test:ci": "vitest run",

‎packages/purestack/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ Run these commands from the project root:
103103

104104
`publish` prepares files for a static host; it does not upload them. Use
105105
`--clean` with `build` or `serve` to clear the output directory before a build.
106+
Use `--no-cache` with any command to skip persistent caches, such as
107+
highlighted code and optimized images, for a fully cold build.
106108
Use `--host` and `--port` with `serve` to choose its address. Every command
107109
needs `--content` pointing to a directory with `siteConfig.json`.
108110

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
/** Converts source code to highlighted pre/code HTML. */
22
export interface MdxCodeHighlighter {
33
codeToHtml: (code: string, lang?: string) => string | Promise<string>
4+
/** Persist buffered computation results after a render batch. Await producers before calling. */
5+
flush?: () => void | Promise<void>
46
}

‎packages/ts-common/src/site-config-types.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export interface CoreSiteConfig {
2222
sitemap: SitemapConfig
2323
i18n: I18nConfig
2424
mdx: SiteMdxConfig
25+
/**
26+
* Whether plugins may read and write persistent caches outside the output
27+
* directory. Set per run (`--no-cache`, `options.cache`), never by site settings.
28+
*/
29+
cache: boolean
2530
}
2631

2732
export interface SiteConfig extends CoreSiteConfig {
@@ -156,7 +161,9 @@ export type SiteStyleConfigInput = DeepPartial<
156161
theme?: ThemeOptionsInput
157162
}
158163

159-
export type SiteConfigInput = DeepPartial<Omit<SiteConfig, 'style'>> & {
164+
export type SiteConfigInput = DeepPartial<
165+
Omit<SiteConfig, 'style' | 'cache'>
166+
> & {
160167
style?: SiteStyleConfigInput
161168
}
162169

‎packages/ts-components/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"@purestack/ts-style": "workspace:^",
5757
"@purestack/ts-svg-icons": "workspace:^",
5858
"@purestack/ts-util": "workspace:^",
59-
"regor": "^1.7.5"
59+
"regor": "^1.7.6"
6060
},
6161
"regorComponents": [
6262
"AlertBox",

‎packages/ts-highlight/README.md‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,23 @@ unused factory is never called. The Shiki options contain the default
2626
languages and themes. For highlight.js, prefer a new instance with explicitly
2727
registered grammars to avoid changing the shared registry.
2828

29-
`createMdxHighlighter`, `createHljsHighlighter`, `DEFAULT_MDX_CODE_LANGS`, and
29+
The default Shiki backend caches highlighted HTML in the OS temporary
30+
directory (`purestack-cache/shiki`), keyed by Shiki's version, the themes, the loaded languages, and the
31+
code. Repeat builds reuse it, and Shiki loads only when a block is not cached.
32+
Builds run with `--no-cache` skip it. Output from a custom `createShiki`
33+
factory is never cached, because its grammars are unknown.
34+
35+
Storage uses `PersistentCache` from `@purestack/ts-util-node`: 32 lazy JSON shards,
36+
buffered writes merged under shard locks, bounded memory, and cleanup of entries
37+
unused for 30 days. Large entries stay in separate files. PureStack flushes after
38+
build and development render batches. Standalone users should await
39+
`highlighter.flush?.()` after rendering their batch; the periodic flush timer
40+
does not keep Node alive.
41+
Other build-time plugins and components can use the same utility with their own
42+
namespace, version, and input keys.
43+
44+
`createMdxHighlighter(themes?, langs?, { create?, cache? })`,
45+
`createHljsHighlighter`, `DEFAULT_MDX_CODE_LANGS`, and
3046
`DEFAULT_MDX_CODE_THEMES` are exported for standalone use. The shared
3147
`MdxCodeHighlighter` contract is exported by `ts-common` and `ts-ssg`.
3248

0 commit comments

Comments
 (0)