diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..1ef9173 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,62 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + filter: blob:none + - uses: actions/setup-node@v6 + with: + node-version: "24" + - uses: nubjs/setup-nub@v0 + with: + nub-version: ^0.9.0 + cache: true + cache-dependency-path: | + nub.lock + package.json + - uses: moonrepo/setup-toolchain@v0 + with: + moon-version: "2.5.5" + - run: nub install --frozen-lockfile + - run: nubx playwright install --with-deps chromium + - name: Check affected tasks + id: ci + run: moon ci + - uses: moonrepo/run-report-action@v1 + if: "!cancelled() && steps.ci.outcome != 'skipped'" + with: + access-token: ${{ secrets.GITHUB_TOKEN }} + skip-comment: true + - uses: actions/upload-artifact@v7 + if: always() + with: + name: moon-reports-${{ github.run_attempt }} + path: | + .moon/cache/ciReport.json + .moon/cache/runReport.json + include-hidden-files: true + if-no-files-found: ignore + retention-days: 14 + - uses: actions/upload-artifact@v7 + if: always() + with: + name: test-results-${{ github.run_attempt }} + path: test-results/ + if-no-files-found: ignore + retention-days: 1 diff --git a/.gitignore b/.gitignore index 7b516de..918bb69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -.task/ +.moon/cache/ node_modules/ dist/ .npmrc diff --git a/.moon/tasks/all.yml b/.moon/tasks/all.yml new file mode 100644 index 0000000..fc0627b --- /dev/null +++ b/.moon/tasks/all.yml @@ -0,0 +1,6 @@ +implicitInputs: + - moon.yml + - package.json + - nub.lock + - /.moon/**/*.yml + - /.github/workflows/ci.yaml diff --git a/.moon/workspace.yml b/.moon/workspace.yml new file mode 100644 index 0000000..132e9ee --- /dev/null +++ b/.moon/workspace.yml @@ -0,0 +1,11 @@ +defaultProject: solid-static +projects: + solid-static: . +versionConstraint: ">=2.5.4 <3.0.0" +vcs: + client: git + defaultBranch: master +pipeline: + installDependencies: false + syncProjects: false + syncWorkspace: false diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..d4831a8 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,3 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json" +} diff --git a/AGENTS.md b/AGENTS.md index edaae81..dfc3470 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,8 +9,8 @@ ## Verification -- after significant changes run `task check` +- after significant changes run `moon run solid-static:check` ## Releases -- agent can do `task publish` if human asks for it explicitly. Verbs: "publish"/"release" +- agent can do `moon run solid-static:publish` if human asks for it explicitly. Verbs: "publish"/"release" diff --git a/README.md b/README.md index ba0e528..2632878 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,29 @@ staticSite({ Add `.tsx`, `.md`, or `.mdx` pages under `src/pages`. The directory structure determines each page's route. Markdown pages must declare a SolidJS layout in their frontmatter. +### Markdown headings and tables of contents + +`createHtmlMarkdownProcessor()` assigns stable, GitHub-style IDs to headings. +Repeated headings receive unique suffixes, with numbering scoped to each document. +Markdown and MDX pages using `solidMarkdown()` also receive heading IDs. + +Rendered content collection entries expose `rendered.html` and +`rendered.headings`. Each heading contains `depth` (1–6), `slug` (the HTML ID), +and plain `text`. Build a table of contents from this metadata instead of parsing +the HTML a second time: + +```tsx + + +``` + +Custom Markdown processors can provide the same metadata through their result's +`data.headings`. Results without heading metadata produce an empty headings list. + ### Client islands Import a self-mounting browser entry with the `?island` query, then reference the returned URL from a module script. The page remains static HTML; only the named entry and its imports are compiled for the browser. @@ -212,6 +235,33 @@ export default function Page() { When `src` contains image metadata, specifying only `width` or `height` infers the other dimension while preserving the aspect ratio. The returned promise resolves to a `GetImageResult` containing the generated `src`, inferred `attributes`, normalized `options`, original `rawOptions`, and an Astro-compatible `srcSet` object. Generated URLs work in both the Vite development server and production builds. `getImage()` throws if called in the browser. +## Development and CI + +Install Node.js 24, Nub 0.9, and Moon 2.5.5, then run: + +```sh +nub install --frozen-lockfile +nubx playwright install chromium +moon run solid-static:check +``` + +`moon.yml` owns the build, lint, typecheck, unit, and browser tasks. Tests depend +on the package build because Vite fixtures resolve the package's exported runtime +from `dist`. A clean checkout does not need prebuilt artifacts. + +The standalone GitHub workflow follows [Moon's CI guide](https://moonrepo.dev/docs/guides/ci): +full Git history, dependency installation, then `moon ci` to select affected +tasks and run their dependencies. It uploads native reports and keeps +publication out of CI. There is no separate CI-only task graph or persisted +Moon workspace cache. + +The same project tasks can be registered as `solid-static` in a parent Moon +workspace. Consumers should depend on `solid-static:build` and use a workspace +package dependency. The parent owns dependency and browser installation; the +submodule's standalone workflow does not run inside the parent's workflow. + +To publish an explicitly approved release, use `moon run solid-static:publish`. + ## Documentation Dedicated documentation is not available yet. For the concepts and intended behavior, see the corresponding Astro guides: diff --git a/Taskfile.yaml b/Taskfile.yaml deleted file mode 100644 index e75cb57..0000000 --- a/Taskfile.yaml +++ /dev/null @@ -1,91 +0,0 @@ -version: "3" - -tasks: - lint: - run: once - cmd: nubx oxlint src e2e playwright.config.ts - sources: - - nub.lock - - package.json - - playwright.config.ts - - tsconfig.json - - e2e/**/* - - src/**/* - - typecheck: - run: once - cmd: nubx tsc - sources: - - nub.lock - - package.json - - tsconfig.json - - src/**/* - - typecheck:playwright: - run: once - cmd: nubx tsc -p tsconfig.playwright.json - sources: - - e2e/**/* - - nub.lock - - package.json - - playwright.config.ts - - tsconfig.json - - tsconfig.playwright.json - - test:unit: - run: once - cmd: nubx vitest run - sources: - - nub.lock - - package.json - - tsconfig.json - - vitest.config.ts - - src/**/* - - playwright:install: - run: once - cmd: nubx playwright install chromium - sources: - - nub.lock - - test:playwright: - run: once - deps: - - playwright:install - cmd: nubx playwright test - sources: - - e2e/**/* - - nub.lock - - package.json - - playwright.config.ts - - src/**/* - - test: - deps: - - test:unit - - test:playwright - - build: - run: once - cmd: nubx tsc -p tsconfig.build.json - sources: - - nub.lock - - package.json - - tsconfig.json - - tsconfig.build.json - - src/**/* - generates: - - dist/**/* - - check: - deps: - - lint - - typecheck - - typecheck:playwright - - test - - publish: - deps: - - check - - build - cmd: nub publish --access public diff --git a/moon.yml b/moon.yml new file mode 100644 index 0000000..14f6730 --- /dev/null +++ b/moon.yml @@ -0,0 +1,79 @@ +toolchains: + default: system +tasks: + lint: + command: nubx oxlint --config .oxlintrc.json src e2e playwright.config.ts + inputs: + - .oxlintrc.json + - src/**/* + - e2e/**/* + - playwright.config.ts + - package.json + - /nub.lock + build: + command: nubx tsc -p tsconfig.build.json + inputs: + - src/**/* + - package.json + - tsconfig.json + - tsconfig.build.json + - /nub.lock + outputs: + - dist/**/* + typecheck: + command: nubx tsc + inputs: + - src/**/* + - package.json + - tsconfig.json + - /nub.lock + typecheck-playwright: + command: nubx tsc -p tsconfig.playwright.json + inputs: + - src/**/* + - e2e/**/* + - package.json + - playwright.config.ts + - tsconfig.json + - tsconfig.playwright.json + - /nub.lock + test-unit: + command: nubx vitest run + deps: + - ~:build + inputs: + - src/**/* + - package.json + - tsconfig.json + - vitest.config.ts + - /nub.lock + test-e2e: + command: nubx playwright test + deps: + - ~:build + inputs: + - src/**/* + - e2e/**/* + - playwright.config.ts + - tsconfig.playwright.json + - package.json + - /nub.lock + check: + deps: + - ~:lint + - ~:typecheck + - ~:typecheck-playwright + - ~:test-unit + - ~:test-e2e + options: + runInCI: false + inputs: [] + publish: + command: nub publish --access public + deps: + - ~:check + - ~:build + options: + cache: false + runInCI: false + inputs: [] diff --git a/nub.lock b/nub.lock index 5f2d34f..031b7c2 100644 --- a/nub.lock +++ b/nub.lock @@ -11,6 +11,12 @@ importers: '@mdx-js/rollup': specifier: ^3.1.1 version: 3.1.1(rollup@4.62.4) + github-slugger: + specifier: ^2.0.0 + version: 2.0.0 + hast-util-to-string: + specifier: ^3.0.1 + version: 3.0.1 js-yaml: specifier: ^5.2.3 version: 5.3.0 @@ -44,6 +50,12 @@ importers: unified: specifier: 11.0.5 version: 11.0.5 + unist-util-visit: + specifier: ^5.1.0 + version: 5.1.0 + vfile: + specifier: ^6.0.3 + version: 6.0.3 vite-plugin-solid: specifier: ^2.11.14 version: 2.11.14(solid-js@1.9.15)(vite@8.2.1(@types/node@26.2.0)(yaml@2.9.0)) @@ -51,6 +63,9 @@ importers: '@playwright/test': specifier: ^1.62.1 version: 1.62.1 + '@types/hast': + specifier: ^3.0.5 + version: 3.0.5 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -1130,6 +1145,9 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + hast-util-to-estree@3.1.3: resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} @@ -1139,6 +1157,9 @@ packages: hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} @@ -1753,7 +1774,6 @@ packages: optional: true jsdom: optional: true - vite: {} why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} @@ -2574,6 +2594,8 @@ snapshots: gensync@1.0.0-beta.2: {} + github-slugger@2.0.0: {} + hast-util-to-estree@3.1.3: dependencies: '@types/estree': 1.0.9 @@ -2629,6 +2651,10 @@ snapshots: transitivePeerDependencies: - supports-color + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.5 + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.5 diff --git a/package.json b/package.json index fee6e21..0c99ac8 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,8 @@ }, "dependencies": { "@mdx-js/rollup": "^3.1.1", + "github-slugger": "^2.0.0", + "hast-util-to-string": "^3.0.1", "js-yaml": "^5.2.3", "mdast-util-from-markdown": "^2.0.3", "rehype-stringify": "^10.0.1", @@ -52,10 +54,13 @@ "turndown": "^7.2.4", "turndown-plugin-gfm": "^1.0.2", "unified": "11.0.5", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3", "vite-plugin-solid": "^2.11.14" }, "devDependencies": { "@playwright/test": "^1.62.1", + "@types/hast": "^3.0.5", "@types/js-yaml": "^4.0.9", "@types/mdast": "^4.0.4", "@types/node": "^26.2.0", diff --git a/src/content.test.ts b/src/content.test.ts index 8dac06f..0367350 100644 --- a/src/content.test.ts +++ b/src/content.test.ts @@ -3,8 +3,29 @@ import { tmpdir } from "node:os" import { join } from "node:path" import { describe, expect, test } from "vitest" import { loadCollections } from "./content.ts" +import { createHtmlMarkdownProcessor } from "./markdown.ts" describe("content collections", () => { + test("carries rendered heading anchors into collection entries", async () => { + const directory = await mkdtemp(join(tmpdir(), "solid-static-headings-")) + try { + await writeFile(join(directory, "guide.md"), "---\ntitle: Guide\n---\n## Listen\n### Share\n") + const collections = await loadCollections( + { guides: { directory, pattern: /\.md$/u } }, + createHtmlMarkdownProcessor(), + ) + expect(collections.guides?.[0]?.rendered).toEqual({ + html: '