├── .changeset ├── README.md ├── changelog-config.cjs └── config.json ├── .github ├── FUNDING.yml ├── actions │ └── setup │ │ └── action.yml └── workflows │ ├── ci.yml │ ├── fix.yml │ ├── release-dev.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── eslint.config.js ├── examples ├── custom-head │ ├── README.md │ ├── astro.config.ts │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── CustomFont.html │ │ │ ├── CustomHead.astro │ │ │ ├── Typography.astro │ │ │ ├── Typography.stories.ts │ │ │ └── global.css │ │ └── env.d.ts │ └── tsconfig.json ├── mixed │ ├── README.md │ ├── astro.config.ts │ ├── package.json │ ├── src │ │ ├── components │ │ │ └── Card.astro │ │ ├── env.d.ts │ │ ├── layouts │ │ │ └── Layout.astro │ │ ├── pages │ │ │ └── index.astro │ │ ├── stories │ │ │ ├── PreactCounter.stories.ts │ │ │ └── PreactCounter.tsx │ │ └── styles │ │ │ └── global.css │ └── tsconfig.json ├── playground │ ├── .npmrc │ ├── README.md │ ├── astro.config.ts │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── astro │ │ │ │ ├── AstroCounter.astro │ │ │ │ └── AstroCounter.stories.ts │ │ │ ├── lit │ │ │ │ ├── LitCounter.astro │ │ │ │ ├── LitCounter.stories.ts │ │ │ │ └── LitCounter.ts │ │ │ ├── preact │ │ │ │ ├── PreactCounter.stories.ts │ │ │ │ └── PreactCounter.tsx │ │ │ ├── react │ │ │ │ ├── ReactCounter.stories.ts │ │ │ │ └── ReactCounter.tsx │ │ │ ├── solid │ │ │ │ ├── SolidCounter.stories.ts │ │ │ │ └── SolidCounter.tsx │ │ │ ├── svelte │ │ │ │ ├── SvelteCounter.stories.ts │ │ │ │ └── SvelteCounter.svelte │ │ │ └── vue │ │ │ │ ├── VueCounter.stories.ts │ │ │ │ └── VueCounter.vue │ │ ├── env.d.ts │ │ └── styles │ │ │ └── global.css │ └── tsconfig.json ├── tailwindcss │ ├── README.md │ ├── astro.config.ts │ ├── package.json │ ├── src │ │ ├── env.d.ts │ │ ├── stories │ │ │ ├── PreactCounter.stories.ts │ │ │ └── PreactCounter.tsx │ │ └── styles │ │ │ └── global.css │ └── tsconfig.json └── unocss │ ├── README.md │ ├── astro.config.ts │ ├── package.json │ ├── src │ ├── env.d.ts │ └── stories │ │ ├── PreactCounter.stories.ts │ │ └── PreactCounter.tsx │ ├── tsconfig.json │ └── uno.config.ts ├── package.json ├── packages ├── astrobook │ ├── CHANGELOG.md │ ├── README.md │ ├── lib │ │ ├── components │ │ │ └── head.astro │ │ └── pages │ │ │ ├── app.astro │ │ │ └── story.astro │ ├── package.json │ ├── src │ │ ├── client.ts │ │ └── index.ts │ ├── tsconfig.build.json │ └── tsconfig.json ├── core │ ├── package.json │ ├── src │ │ ├── astro-integration.ts │ │ ├── client.ts │ │ ├── index.ts │ │ ├── utils │ │ │ ├── base.spec.ts │ │ │ ├── base.ts │ │ │ ├── get-exports.spec.ts │ │ │ ├── get-exports.ts │ │ │ ├── invariant.ts │ │ │ └── path.ts │ │ └── virtual-module │ │ │ ├── get-story-modules.spec.ts │ │ │ ├── get-story-modules.ts │ │ │ ├── story-modules.ts │ │ │ ├── virtual-module-ids.ts │ │ │ ├── virtual-routes.ts │ │ │ └── vite-plugin.ts │ ├── tsconfig.json │ ├── tsdown.config.ts │ └── vitest.config.ts ├── types │ ├── lib │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── types.d.ts │ │ └── virtual.d.ts │ ├── package.json │ └── tsconfig.json └── ui │ ├── copy.ts │ ├── package.json │ ├── src │ ├── components │ │ ├── app.astro │ │ ├── layout.astro │ │ ├── layout.css │ │ ├── sidebar-button-fullscreen.astro │ │ ├── sidebar-button-theme.astro │ │ ├── sidebar-button.astro │ │ ├── sidebar-component-list.astro │ │ ├── sidebar-title.astro │ │ ├── sidebar.astro │ │ └── theme-message.ts │ ├── env.d.ts │ ├── index.ts │ └── style.css │ ├── tsconfig.json │ └── uno.config.ts ├── playwright.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tests ├── example-urls.js ├── mixed │ └── index.test.ts ├── playground │ ├── counter.test.ts │ └── home.test.ts ├── tailwindcss │ └── index.test.ts └── unocss │ └── index.test.ts ├── tsconfig.json ├── turbo.json └── vitest.workspace.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/changelog-config.cjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** @type import('@changesets/types').GetReleaseLine */ 4 | async function getReleaseLine(changeset) { 5 | let returnVal = `- ` + formatCommit(changeset.commit) 6 | 7 | const lines = changeset.summary.split('\n').map((line) => line.trimEnd()) 8 | 9 | for (const [index, line] of lines.entries()) { 10 | if (index === 0) { 11 | returnVal += line 12 | } else { 13 | returnVal += `\n ` + line 14 | } 15 | } 16 | 17 | return returnVal + '\n' 18 | } 19 | 20 | /** 21 | * @param {string | null | undefined} commit 22 | */ 23 | function formatCommit(commit) { 24 | if (!commit || typeof commit !== 'string' || commit.length < 7) { 25 | return '' 26 | } 27 | 28 | const shortCommit = commit.slice(0, 7) 29 | 30 | return `[\`${shortCommit}\`](https://github.com/ocavue/astrobook/commit/${commit}) ` 31 | } 32 | 33 | /** @type import('@changesets/types').GetDependencyReleaseLine */ 34 | async function getDependencyReleaseLine() { 35 | return '' 36 | } 37 | 38 | /** @type import('@changesets/types').ChangelogFunctions */ 39 | const functions = { 40 | getReleaseLine, 41 | getDependencyReleaseLine, 42 | } 43 | 44 | module.exports = functions 45 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json", 3 | "changelog": "./changelog-config.cjs", 4 | "commit": false, 5 | "fixed": [["@astrobook/*", "astrobook"]], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "master", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [ 11 | "example-playground", 12 | "example-mixed", 13 | "example-custom-head", 14 | "example-tailwindcss", 15 | "example-unocss" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ocavue] 2 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Setup the environment 3 | 4 | inputs: 5 | node-version: 6 | description: The version of node.js 7 | required: false 8 | default: '20' 9 | 10 | runs: 11 | using: composite 12 | steps: 13 | - name: Install pnpm 14 | uses: pnpm/action-setup@v4 15 | 16 | - name: Setup node 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ inputs.node-version }} 20 | cache: pnpm 21 | registry-url: 'https://registry.npmjs.org' 22 | 23 | - name: Install 24 | run: pnpm install 25 | shell: bash 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | os: [ubuntu-latest, windows-latest, macos-latest] 18 | 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - uses: ./.github/actions/setup 25 | 26 | - name: Build 27 | run: pnpm run build 28 | 29 | lint: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - uses: ./.github/actions/setup 35 | 36 | - name: Lint 37 | run: pnpm run lint 38 | 39 | - name: Typecheck 40 | run: pnpm run typecheck 41 | 42 | test-unit: 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | os: [ubuntu-latest, windows-latest, macos-latest] 47 | 48 | runs-on: ${{ matrix.os }} 49 | steps: 50 | - uses: actions/checkout@v4 51 | 52 | - uses: ./.github/actions/setup 53 | 54 | - name: Test 55 | run: pnpm run test 56 | 57 | test-e2e: 58 | strategy: 59 | fail-fast: false 60 | matrix: 61 | os: [ubuntu-latest, windows-latest, macos-latest] 62 | mode: [dev, preview] 63 | 64 | runs-on: ${{ matrix.os }} 65 | env: 66 | ASTROBOOK_TEST_MODE: ${{ matrix.mode }} 67 | 68 | steps: 69 | - uses: actions/checkout@v4 70 | 71 | - uses: ./.github/actions/setup 72 | 73 | - name: Install Playwright 74 | run: pnpm run test:install 75 | 76 | - name: Build 77 | if: ${{ matrix.mode == 'preview' }} 78 | run: pnpm run build 79 | 80 | - name: Test 81 | run: pnpm run test:e2e 82 | -------------------------------------------------------------------------------- /.github/workflows/fix.yml: -------------------------------------------------------------------------------- 1 | # https://autofix.ci/setup 2 | 3 | name: autofix.ci 4 | 5 | on: 6 | pull_request: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | fix: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: ./.github/actions/setup 18 | 19 | - name: Fix 20 | run: pnpm run fix 21 | 22 | - uses: autofix-ci/action@551dded8c6cc8a1054039c8bc0b8b48c51dfc6ef 23 | -------------------------------------------------------------------------------- /.github/workflows/release-dev.yml: -------------------------------------------------------------------------------- 1 | name: Release (dev) 2 | on: [pull_request] 3 | 4 | jobs: 5 | release: 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - uses: actions/checkout@v4 10 | 11 | - uses: ./.github/actions/setup 12 | 13 | - run: pnpm run ci:publish-dev 14 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | prepare: 12 | name: Prepare 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - uses: ./.github/actions/setup 18 | 19 | - name: Create Release Pull Request 20 | id: changesets 21 | uses: changesets/action@v1 22 | with: 23 | version: pnpm ci:version 24 | commit: 'chore: version packages' 25 | title: 'chore: version packages' 26 | createGithubReleases: false 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | publish: 31 | name: Publish 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | 36 | - uses: ./.github/actions/setup 37 | 38 | - name: Build packages 39 | run: pnpm build:package 40 | 41 | - name: Publish to NPM 42 | env: 43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 44 | run: pnpm ci:publish 45 | 46 | - name: Get package version 47 | run: | 48 | MAIN_VERSION=$(jq -r ".version" packages/astrobook/package.json) 49 | echo "MAIN_RELEASE_TAG=v${MAIN_VERSION}" >> "$GITHUB_ENV" 50 | 51 | - name: Create GitHub release 52 | uses: ncipollo/release-action@v1 53 | with: 54 | commit: master 55 | tag: '${{ env.MAIN_RELEASE_TAG }}' 56 | body: 'Please refer to [CHANGELOG.md](https://github.com/ocavue/astrobook/blob/${{ env.MAIN_RELEASE_TAG }}/packages/astrobook/CHANGELOG.md) for details.' 57 | token: ${{ secrets.GITHUB_TOKEN }} 58 | continue-on-error: true 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | !packages/astrobook/CHANGELOG.md 3 | .cache 4 | .DS_Store 5 | .idea 6 | *.log 7 | *.tgz 8 | coverage 9 | dist 10 | lib-cov 11 | logs 12 | node_modules 13 | temp 14 | .astro 15 | .tsup 16 | .turbo 17 | node_modules 18 | test-results 19 | playwright-report 20 | blob-report 21 | **/playwright/.cache 22 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | pnpm-lock.yaml 3 | package-lock.json 4 | CHANGELOG.md 5 | 6 | .next 7 | .cache 8 | .DS_Store 9 | .idea 10 | *.log 11 | *.tgz 12 | coverage 13 | dist 14 | lib-cov 15 | logs 16 | node_modules 17 | temp 18 | dist-types 19 | *.tsbuildinfo 20 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"], 5 | "overrides": [ 6 | { 7 | "files": "*.astro", 8 | "options": { 9 | "parser": "astro" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "unocss.root": ["packages/ui"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ocavue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./packages/astrobook/README.md -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import { defineESLintConfig } from '@ocavue/eslint-config' 2 | 3 | export default defineESLintConfig() 4 | -------------------------------------------------------------------------------- /examples/custom-head/README.md: -------------------------------------------------------------------------------- 1 | An example of using custom `` tags with Astrobook. 2 | 3 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/ocavue/astrobook/tree/master/examples/custom-head) 4 | -------------------------------------------------------------------------------- /examples/custom-head/astro.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config' 2 | import astrobook from 'astrobook' 3 | 4 | // https://astro.build/config 5 | export default defineConfig({ 6 | server: { 7 | port: 4305, 8 | }, 9 | 10 | // Enable many frameworks to support all different kinds of components. 11 | integrations: [ 12 | astrobook({ 13 | directory: 'src/components', 14 | head: './src/components/CustomHead.astro', 15 | title: 'Custom Title', 16 | }), 17 | ], 18 | }) 19 | -------------------------------------------------------------------------------- /examples/custom-head/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-custom-head", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "description": "Astrobook example", 7 | "scripts": { 8 | "dev": "astro dev", 9 | "start": "astro dev", 10 | "build": "astro build", 11 | "preview": "astro preview", 12 | "astro": "astro" 13 | }, 14 | "dependencies": { 15 | "astro": "^5.8.1", 16 | "astrobook": "*" 17 | }, 18 | "stackblitz": { 19 | "installDependencies": false, 20 | "startCommand": "pnpm install && pnpm run dev" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/custom-head/src/components/CustomFont.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 28 | -------------------------------------------------------------------------------- /examples/custom-head/src/components/CustomHead.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import './global.css' 3 | import CustomFont from './CustomFont.html' 4 | --- 5 | 6 | 7 | -------------------------------------------------------------------------------- /examples/custom-head/src/components/Typography.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | fontName: 'freckle-face' | 'lobster' | 'press-start-2p' 4 | } 5 | 6 | const { fontName } = Astro.props 7 | --- 8 | 9 |
10 |
11 |

Lorem ipsum dolor sit amet

12 |
13 |
14 | -------------------------------------------------------------------------------- /examples/custom-head/src/components/Typography.stories.ts: -------------------------------------------------------------------------------- 1 | import Typography from './Typography.astro' 2 | 3 | export default { 4 | component: Typography, 5 | } 6 | 7 | export const Lobster = { 8 | args: { 9 | fontName: 'lobster', 10 | }, 11 | } 12 | 13 | export const FreckleFace = { 14 | args: { 15 | fontName: 'freckle-face', 16 | }, 17 | } 18 | 19 | export const PressStart2P = { 20 | args: { 21 | fontName: 'press-start-2p', 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /examples/custom-head/src/components/global.css: -------------------------------------------------------------------------------- 1 | p { 2 | padding: 1rem; 3 | } 4 | -------------------------------------------------------------------------------- /examples/custom-head/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare module '*.vue' { 5 | import type { DefineComponent } from 'vue' 6 | 7 | const component: DefineComponent 8 | export default component 9 | } 10 | 11 | declare module '*.astro' { 12 | import type { AstroComponentFactory } from 'astro/runtime/server/index.js' 13 | 14 | const content: AstroComponentFactory 15 | export default content 16 | } 17 | -------------------------------------------------------------------------------- /examples/custom-head/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "esnext", 5 | "lib": ["esnext"], 6 | "jsx": "preserve", 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "moduleResolution": "Bundler", 10 | "esModuleInterop": true, 11 | "outDir": "${configDir}/node_modules/.cache/tsc", 12 | "strict": true, 13 | "allowJs": true, 14 | "composite": true, 15 | "strictNullChecks": true, 16 | "verbatimModuleSyntax": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "skipDefaultLibCheck": true 21 | }, 22 | "include": ["./src", "./*.js", "./*.mjs", "./*.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/mixed/README.md: -------------------------------------------------------------------------------- 1 | An example that shows how to add Astrobook into an existing Astro project. 2 | 3 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/ocavue/astrobook/tree/master/examples/mixed) 4 | -------------------------------------------------------------------------------- /examples/mixed/astro.config.ts: -------------------------------------------------------------------------------- 1 | import preact from '@astrojs/preact' 2 | import { defineConfig } from 'astro/config' 3 | import astrobook from 'astrobook' 4 | 5 | // https://astro.build/config 6 | export default defineConfig({ 7 | server: { 8 | port: 4302, 9 | }, 10 | 11 | base: '/base', 12 | 13 | integrations: [ 14 | preact(), 15 | astrobook({ directory: 'src/stories', subpath: '/docs/components' }), 16 | ], 17 | }) 18 | -------------------------------------------------------------------------------- /examples/mixed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-mixed", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "description": "Astrobook example", 7 | "scripts": { 8 | "dev": "astro dev", 9 | "start": "astro dev", 10 | "build": "astro build", 11 | "preview": "astro preview", 12 | "astro": "astro" 13 | }, 14 | "dependencies": { 15 | "@astrojs/preact": "^4.1.0", 16 | "astro": "^5.8.1", 17 | "astrobook": "*", 18 | "preact": "^10.26.8" 19 | }, 20 | "stackblitz": { 21 | "installDependencies": false, 22 | "startCommand": "pnpm install && pnpm run dev" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/mixed/src/components/Card.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string 4 | body: string 5 | href: string 6 | } 7 | 8 | const { href, title, body } = Astro.props 9 | --- 10 | 11 | 22 | 62 | -------------------------------------------------------------------------------- /examples/mixed/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/mixed/src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | title: string 4 | } 5 | 6 | const { title } = Astro.props 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {title} 18 | 19 | 20 | 21 | 22 | 23 | 51 | -------------------------------------------------------------------------------- /examples/mixed/src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro' 3 | import Card from '../components/Card.astro' 4 | --- 5 | 6 | 7 |
8 | 36 |

Welcome to Astro

37 |

38 | This is an example of an Astro project that includes Astrobook under a 39 | subpath. 40 |

41 | 48 |
49 |
50 | 51 | 112 | -------------------------------------------------------------------------------- /examples/mixed/src/stories/PreactCounter.stories.ts: -------------------------------------------------------------------------------- 1 | import { PreactCounter } from './PreactCounter' 2 | 3 | export default { 4 | component: PreactCounter, 5 | } 6 | 7 | export const Default = { 8 | args: {}, 9 | } 10 | 11 | export const LargeStep = { 12 | args: { 13 | step: 5, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /examples/mixed/src/stories/PreactCounter.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource preact */ 2 | 3 | import '../styles/global.css' 4 | 5 | import type { ComponentChildren } from 'preact' 6 | import { useState } from 'preact/hooks' 7 | 8 | /** A counter written with Preact */ 9 | export function PreactCounter({ 10 | step = 1, 11 | children, 12 | }: { 13 | step?: number 14 | children?: ComponentChildren 15 | }) { 16 | const [count, setCount] = useState(0) 17 | const add = () => setCount((i) => i + step) 18 | const subtract = () => setCount((i) => i - step) 19 | 20 | return ( 21 | <> 22 |
23 | 24 |
{count}
25 | 26 |
27 |
{children}
28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /examples/mixed/src/styles/global.css: -------------------------------------------------------------------------------- 1 | .counter { 2 | display: grid; 3 | font-size: 2em; 4 | grid-template-columns: repeat(3, minmax(0, 1fr)); 5 | margin-top: 2em; 6 | place-items: center; 7 | } 8 | 9 | .counter-message { 10 | text-align: center; 11 | } 12 | -------------------------------------------------------------------------------- /examples/mixed/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "esnext", 5 | "lib": ["esnext"], 6 | "jsx": "react-jsx", 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "moduleResolution": "Bundler", 10 | "esModuleInterop": true, 11 | "outDir": "${configDir}/node_modules/.cache/tsc", 12 | "strict": true, 13 | "allowJs": true, 14 | "composite": true, 15 | "strictNullChecks": true, 16 | "verbatimModuleSyntax": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "skipDefaultLibCheck": true, 21 | "jsxImportSource": "preact" 22 | }, 23 | "include": ["./src", "./*.js", "./*.mjs", "./*.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/playground/.npmrc: -------------------------------------------------------------------------------- 1 | # Lit libraries are required to be hoisted due to dependency issues. 2 | public-hoist-pattern[]=*lit* 3 | -------------------------------------------------------------------------------- /examples/playground/README.md: -------------------------------------------------------------------------------- 1 | An example of using multiple UI rendering frameworks (React, Preact, Vue, Svelte, Solid, Lit, Astro) with Astrobook. 2 | 3 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/ocavue/astrobook/tree/master/examples/playground) 4 | -------------------------------------------------------------------------------- /examples/playground/astro.config.ts: -------------------------------------------------------------------------------- 1 | import preact from '@astrojs/preact' 2 | import react from '@astrojs/react' 3 | import solid from '@astrojs/solid-js' 4 | import svelte from '@astrojs/svelte' 5 | import vue from '@astrojs/vue' 6 | import { defineConfig } from 'astro/config' 7 | import astrobook from 'astrobook' 8 | 9 | // https://astro.build/config 10 | export default defineConfig({ 11 | server: { 12 | port: 4321, 13 | }, 14 | 15 | // Enable many frameworks to support all different kinds of components. 16 | integrations: [ 17 | react({ include: ['**/react/*'] }), 18 | preact({ include: ['**/preact/*'] }), 19 | solid({ include: ['**/solid/*'] }), 20 | svelte(), 21 | vue(), 22 | astrobook({ directory: 'src/components' }), 23 | ], 24 | }) 25 | -------------------------------------------------------------------------------- /examples/playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-playground", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "description": "Astrobook example", 7 | "scripts": { 8 | "dev": "astro dev", 9 | "start": "astro dev", 10 | "build": "astro build", 11 | "preview": "astro preview", 12 | "astro": "astro" 13 | }, 14 | "dependencies": { 15 | "@astrojs/preact": "^4.1.0", 16 | "@astrojs/react": "^4.3.0", 17 | "@astrojs/solid-js": "^5.1.0", 18 | "@astrojs/svelte": "^7.1.0", 19 | "@astrojs/vue": "^5.1.0", 20 | "@types/react": "^19.1.6", 21 | "@types/react-dom": "^19.1.5", 22 | "astro": "^5.8.1", 23 | "astrobook": "*", 24 | "lit": "^3.3.0", 25 | "preact": "^10.26.8", 26 | "react": "^19.1.0", 27 | "react-dom": "^19.1.0", 28 | "solid-js": "^1.9.7", 29 | "svelte": "^5.33.11", 30 | "vue": "^3.5.16" 31 | }, 32 | "stackblitz": { 33 | "installDependencies": false, 34 | "startCommand": "pnpm install && pnpm run dev" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /examples/playground/src/components/astro/AstroCounter.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '../../styles/global.css' 3 | 4 | interface Props { 5 | step?: number 6 | } 7 | const step = Astro.props.step || 1 8 | --- 9 | 10 | 11 |
12 | 13 |
0
14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 | 56 | -------------------------------------------------------------------------------- /examples/playground/src/components/astro/AstroCounter.stories.ts: -------------------------------------------------------------------------------- 1 | import AstroCounter from './AstroCounter.astro' 2 | 3 | export default { 4 | component: AstroCounter, 5 | } 6 | 7 | export const Default = { 8 | args: {}, 9 | } 10 | 11 | export const LargeStep = { 12 | args: { 13 | step: 5, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /examples/playground/src/components/lit/LitCounter.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '../../styles/global.css' 3 | 4 | interface Props { 5 | step?: number 6 | } 7 | const step = Astro.props.step || 1 8 | --- 9 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/playground/src/components/lit/LitCounter.stories.ts: -------------------------------------------------------------------------------- 1 | import LitCounter from './LitCounter.astro' 2 | 3 | export default { 4 | component: LitCounter, 5 | } 6 | 7 | export const Default = { 8 | args: {}, 9 | } 10 | 11 | export const LargeStep = { 12 | args: { 13 | step: 5, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /examples/playground/src/components/lit/LitCounter.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html, css } from 'lit' 2 | 3 | export class LitCounter extends LitElement { 4 | static properties = { 5 | count: { type: Number, state: true }, 6 | step: { type: Number }, 7 | } 8 | 9 | declare count: number 10 | declare step: number 11 | 12 | static styles = css` 13 | .counter { 14 | display: grid; 15 | font-size: 2em; 16 | grid-template-columns: repeat(3, minmax(0, 1fr)); 17 | margin-top: 2em; 18 | place-items: center; 19 | } 20 | 21 | .counter-message { 22 | text-align: center; 23 | } 24 | ` 25 | 26 | constructor() { 27 | super() 28 | this.count = 0 29 | this.step = 1 30 | } 31 | 32 | subtract = () => { 33 | this.count -= this.step 34 | } 35 | 36 | add = () => { 37 | this.count += this.step 38 | } 39 | 40 | render() { 41 | return html` 42 |
43 | 44 |
${this.count}
45 | 46 |
47 |
48 | 49 |
50 | ` 51 | } 52 | } 53 | 54 | customElements.define('lit-counter', LitCounter) 55 | -------------------------------------------------------------------------------- /examples/playground/src/components/preact/PreactCounter.stories.ts: -------------------------------------------------------------------------------- 1 | import { PreactCounter } from './PreactCounter' 2 | 3 | export default { 4 | component: PreactCounter, 5 | } 6 | 7 | export const Default = { 8 | args: {}, 9 | } 10 | 11 | export const LargeStep = { 12 | args: { 13 | step: 5, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /examples/playground/src/components/preact/PreactCounter.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource preact */ 2 | 3 | import '../../styles/global.css' 4 | 5 | import type { ComponentChildren } from 'preact' 6 | import { useState } from 'preact/hooks' 7 | 8 | /** A counter written with Preact */ 9 | export function PreactCounter({ 10 | step = 1, 11 | children, 12 | }: { 13 | step?: number 14 | children?: ComponentChildren 15 | }) { 16 | const [count, setCount] = useState(0) 17 | const add = () => setCount((i) => i + step) 18 | const subtract = () => setCount((i) => i - step) 19 | 20 | return ( 21 | <> 22 |
23 | 24 |
{count}
25 | 26 |
27 |
{children}
28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /examples/playground/src/components/react/ReactCounter.stories.ts: -------------------------------------------------------------------------------- 1 | import { ReactCounter } from './ReactCounter' 2 | 3 | export default { 4 | component: ReactCounter, 5 | } 6 | 7 | export const Default = { 8 | args: {}, 9 | } 10 | 11 | export const LargeStep = { 12 | args: { 13 | step: 5, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /examples/playground/src/components/react/ReactCounter.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource react */ 2 | 3 | import '../../styles/global.css' 4 | 5 | import { useState, type ReactNode } from 'react' 6 | 7 | /** A counter written with React */ 8 | export function ReactCounter({ 9 | step = 1, 10 | children, 11 | }: { 12 | step?: number 13 | children?: ReactNode 14 | }) { 15 | const [count, setCount] = useState(0) 16 | const add = () => setCount((i) => i + step) 17 | const subtract = () => setCount((i) => i - step) 18 | 19 | return ( 20 | <> 21 |
22 | 23 |
{count}
24 | 25 |
26 |
{children}
27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /examples/playground/src/components/solid/SolidCounter.stories.ts: -------------------------------------------------------------------------------- 1 | import SolidCounter from './SolidCounter' 2 | 3 | export default { 4 | component: SolidCounter, 5 | } 6 | 7 | export const Default = { 8 | args: {}, 9 | } 10 | 11 | export const LargeStep = { 12 | args: { 13 | step: 5, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /examples/playground/src/components/solid/SolidCounter.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource solid-js */ 2 | 3 | import '../../styles/global.css' 4 | 5 | import { createSignal, type JSX } from 'solid-js' 6 | 7 | /** A counter written with Solid */ 8 | export default function SolidCounter(props: { 9 | step?: number 10 | children?: JSX.Element 11 | }) { 12 | const [count, setCount] = createSignal(0) 13 | const add = () => setCount(count() + (props.step || 1)) 14 | const subtract = () => setCount(count() - (props.step || 1)) 15 | 16 | return ( 17 | <> 18 |
19 | 20 |
{count()}
21 | 22 |
23 |
{props.children}
24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /examples/playground/src/components/svelte/SvelteCounter.stories.ts: -------------------------------------------------------------------------------- 1 | import SvelteCounter from './SvelteCounter.svelte' 2 | 3 | export default { 4 | component: SvelteCounter, 5 | } 6 | 7 | export const Default = { 8 | args: {}, 9 | } 10 | 11 | export const LargeStep = { 12 | args: { 13 | step: 5, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /examples/playground/src/components/svelte/SvelteCounter.svelte: -------------------------------------------------------------------------------- 1 | 4 | 21 | 22 |
23 | 24 |
{$count}
25 | 26 |
27 |
28 | 29 |
30 | -------------------------------------------------------------------------------- /examples/playground/src/components/vue/VueCounter.stories.ts: -------------------------------------------------------------------------------- 1 | import VueCounter from './VueCounter.vue' 2 | 3 | export default { 4 | component: VueCounter, 5 | } 6 | 7 | export const Default = { 8 | args: {}, 9 | } 10 | 11 | export const LargeStep = { 12 | args: { 13 | step: 5, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /examples/playground/src/components/vue/VueCounter.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 38 | -------------------------------------------------------------------------------- /examples/playground/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare module '*.vue' { 5 | import type { DefineComponent } from 'vue' 6 | 7 | const component: DefineComponent 8 | export default component 9 | } 10 | 11 | declare module '*.astro' { 12 | import type { AstroComponentFactory } from 'astro/runtime/server/index.js' 13 | 14 | const content: AstroComponentFactory 15 | export default content 16 | } 17 | -------------------------------------------------------------------------------- /examples/playground/src/styles/global.css: -------------------------------------------------------------------------------- 1 | .counter { 2 | display: grid; 3 | font-size: 2em; 4 | grid-template-columns: repeat(3, minmax(0, 1fr)); 5 | margin-top: 2em; 6 | place-items: center; 7 | } 8 | 9 | .counter-message { 10 | text-align: center; 11 | } 12 | -------------------------------------------------------------------------------- /examples/playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "esnext", 5 | "lib": ["esnext"], 6 | "jsx": "preserve", 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "moduleResolution": "Bundler", 10 | "esModuleInterop": true, 11 | "outDir": "${configDir}/node_modules/.cache/tsc", 12 | "strict": true, 13 | "allowJs": true, 14 | "composite": true, 15 | "strictNullChecks": true, 16 | "verbatimModuleSyntax": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "skipDefaultLibCheck": true 21 | }, 22 | "include": ["./src", "./*.js", "./*.mjs", "./*.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /examples/tailwindcss/README.md: -------------------------------------------------------------------------------- 1 | An example that uses Astrobook and Tailwind CSS. 2 | 3 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/ocavue/astrobook/tree/master/examples/tailwindcss) 4 | -------------------------------------------------------------------------------- /examples/tailwindcss/astro.config.ts: -------------------------------------------------------------------------------- 1 | import preact from '@astrojs/preact' 2 | import tailwindcss from '@tailwindcss/vite' 3 | import { defineConfig } from 'astro/config' 4 | import astrobook from 'astrobook' 5 | 6 | // https://astro.build/config 7 | export default defineConfig({ 8 | server: { 9 | port: 4304, 10 | }, 11 | 12 | vite: { 13 | plugins: [tailwindcss()], 14 | }, 15 | 16 | integrations: [ 17 | preact(), 18 | astrobook({ 19 | css: ['./src/styles/global.css'], 20 | }), 21 | ], 22 | }) 23 | -------------------------------------------------------------------------------- /examples/tailwindcss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-tailwindcss", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "description": "Astrobook example", 7 | "scripts": { 8 | "dev": "astro dev", 9 | "start": "astro dev", 10 | "build": "astro build", 11 | "preview": "astro preview", 12 | "astro": "astro" 13 | }, 14 | "dependencies": { 15 | "@astrojs/preact": "^4.1.0", 16 | "@tailwindcss/vite": "^4.1.8", 17 | "astro": "^5.8.1", 18 | "astrobook": "*", 19 | "preact": "^10.26.8", 20 | "tailwindcss": "^4.1.8" 21 | }, 22 | "stackblitz": { 23 | "installDependencies": false, 24 | "startCommand": "pnpm install && pnpm run dev" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/tailwindcss/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/tailwindcss/src/stories/PreactCounter.stories.ts: -------------------------------------------------------------------------------- 1 | import { PreactCounter } from './PreactCounter' 2 | 3 | export default { 4 | component: PreactCounter, 5 | } 6 | 7 | export const Default = { 8 | args: {}, 9 | } 10 | 11 | export const LargeStep = { 12 | args: { 13 | step: 5, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /examples/tailwindcss/src/stories/PreactCounter.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource preact */ 2 | 3 | import type { ComponentChildren } from 'preact' 4 | import { useState } from 'preact/hooks' 5 | 6 | /** A counter written with Preact */ 7 | export function PreactCounter({ 8 | step = 1, 9 | children, 10 | }: { 11 | step?: number 12 | children?: ComponentChildren 13 | }) { 14 | const [count, setCount] = useState(0) 15 | const add = () => setCount((i) => i + step) 16 | const subtract = () => setCount((i) => i - step) 17 | 18 | return ( 19 | <> 20 |
21 | 27 |
{count}
28 | 34 |
35 |
{children}
36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /examples/tailwindcss/src/styles/global.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | -------------------------------------------------------------------------------- /examples/tailwindcss/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "esnext", 5 | "lib": ["esnext"], 6 | "jsx": "react-jsx", 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "moduleResolution": "Bundler", 10 | "esModuleInterop": true, 11 | "outDir": "${configDir}/node_modules/.cache/tsc", 12 | "strict": true, 13 | "allowJs": true, 14 | "composite": true, 15 | "strictNullChecks": true, 16 | "verbatimModuleSyntax": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "skipDefaultLibCheck": true, 21 | "jsxImportSource": "preact" 22 | }, 23 | "include": ["./src", "./*.js", "./*.mjs", "./*.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/unocss/README.md: -------------------------------------------------------------------------------- 1 | An example that uses Astrobook and UnoCSS. 2 | 3 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/ocavue/astrobook/tree/master/examples/unocss) 4 | -------------------------------------------------------------------------------- /examples/unocss/astro.config.ts: -------------------------------------------------------------------------------- 1 | import preact from '@astrojs/preact' 2 | import { defineConfig } from 'astro/config' 3 | import astrobook from 'astrobook' 4 | import unocss from 'unocss/astro' 5 | 6 | // https://astro.build/config 7 | export default defineConfig({ 8 | server: { 9 | port: 4303, 10 | }, 11 | 12 | integrations: [preact(), astrobook({}), unocss({ injectReset: true })], 13 | }) 14 | -------------------------------------------------------------------------------- /examples/unocss/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-unocss", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "description": "Astrobook example", 7 | "scripts": { 8 | "dev": "astro dev", 9 | "start": "astro dev", 10 | "build": "astro build", 11 | "preview": "astro preview", 12 | "astro": "astro" 13 | }, 14 | "dependencies": { 15 | "@astrojs/preact": "^4.1.0", 16 | "@unocss/reset": "^66.1.2", 17 | "astro": "^5.8.1", 18 | "astrobook": "*", 19 | "preact": "^10.26.8", 20 | "unocss": "^66.1.2" 21 | }, 22 | "stackblitz": { 23 | "installDependencies": false, 24 | "startCommand": "pnpm install && pnpm run dev" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/unocss/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/unocss/src/stories/PreactCounter.stories.ts: -------------------------------------------------------------------------------- 1 | import { PreactCounter } from './PreactCounter' 2 | 3 | export default { 4 | component: PreactCounter, 5 | } 6 | 7 | export const Default = { 8 | args: {}, 9 | } 10 | 11 | export const LargeStep = { 12 | args: { 13 | step: 5, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /examples/unocss/src/stories/PreactCounter.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource preact */ 2 | 3 | import type { ComponentChildren } from 'preact' 4 | import { useState } from 'preact/hooks' 5 | 6 | /** A counter written with Preact */ 7 | export function PreactCounter({ 8 | step = 1, 9 | children, 10 | }: { 11 | step?: number 12 | children?: ComponentChildren 13 | }) { 14 | const [count, setCount] = useState(0) 15 | const add = () => setCount((i) => i + step) 16 | const subtract = () => setCount((i) => i - step) 17 | 18 | return ( 19 | <> 20 |
21 | 27 |
{count}
28 | 34 |
35 |
{children}
36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /examples/unocss/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "esnext", 5 | "lib": ["esnext"], 6 | "jsx": "react-jsx", 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "moduleResolution": "Bundler", 10 | "esModuleInterop": true, 11 | "outDir": "${configDir}/node_modules/.cache/tsc", 12 | "strict": true, 13 | "allowJs": true, 14 | "composite": true, 15 | "strictNullChecks": true, 16 | "verbatimModuleSyntax": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "skipDefaultLibCheck": true, 21 | "jsxImportSource": "preact" 22 | }, 23 | "include": ["./src", "./*.js", "./*.mjs", "./*.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/unocss/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'unocss' 2 | import { presetUno } from 'unocss/preset-uno' 3 | 4 | export default defineConfig({ 5 | presets: [presetUno()], 6 | }) 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astrobook-monorepo", 3 | "type": "module", 4 | "version": "0.0.0", 5 | "private": true, 6 | "packageManager": "pnpm@10.11.0", 7 | "description": "Astrobook monorepo", 8 | "license": "MIT", 9 | "scripts": { 10 | "dev": "turbo --filter example-playground dev", 11 | "postinstall": "pnpm --filter=./packages/ui build", 12 | "build": "turbo run build", 13 | "build:package": "turbo build --filter='./packages/*' --concurrency=1", 14 | "lint": "eslint .", 15 | "fix": "manypkg fix && eslint --fix . && prettier --write .", 16 | "prepublishOnly": "pnpm run build", 17 | "test": "vitest", 18 | "test:install": "playwright install --with-deps", 19 | "test:e2e": "playwright test", 20 | "typecheck": "tsc --build .", 21 | "change": "changeset", 22 | "ci:version": "changeset version && pnpm install --no-frozen-lockfile", 23 | "ci:publish": "pnpm run build:package && pnpm publish --access public -r --no-git-checks --tag latest", 24 | "ci:publish-dev": "pnpm run build:package && pkg-pr-new publish './packages/*' --template ./examples/playground --pnpm" 25 | }, 26 | "devDependencies": { 27 | "@changesets/cli": "^2.29.4", 28 | "@changesets/types": "^6.1.0", 29 | "@manypkg/cli": "^0.24.0", 30 | "@ocavue/eslint-config": "^3.1.0", 31 | "@playwright/test": "^1.52.0", 32 | "@types/node": "^20.17.5", 33 | "eslint": "^9.27.0", 34 | "pkg-pr-new": "^0.0.51", 35 | "playwright": "^1.52.0", 36 | "prettier": "^3.5.3", 37 | "prettier-plugin-astro": "^0.14.1", 38 | "prettier-plugin-tailwindcss": "^0.6.12", 39 | "turbo": "^2.5.4", 40 | "typescript": "^5.8.3", 41 | "vite": "^6.3.5", 42 | "vitest": "^3.1.4" 43 | }, 44 | "renovate": { 45 | "extends": [ 46 | "github>ocavue/config-renovate" 47 | ] 48 | }, 49 | "pnpm": { 50 | "overrides": { 51 | "astrobook@*": "workspace:*" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/astrobook/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # astrobook 2 | 3 | ## 0.8.1 4 | 5 | ### Patch Changes 6 | 7 | - [`25b2d47`](https://github.com/ocavue/astrobook/commit/25b2d47dd438aa2af71f1a64b3615620809ae0d4) Update dependencies. 8 | 9 | ## 0.8.0 10 | 11 | ### Minor Changes 12 | 13 | - [`9c445c8`](https://github.com/ocavue/astrobook/commit/9c445c80f696f565355b71628bcbb1e0c6353c26) Add a new `css` option to adding custom styles to your Astrobook project. You 14 | can pass an array of CSS file paths to be imported into your Astrobook project. 15 | It's easier than adding a custom `head` component, especially when you have no 16 | experience with Astro components. 17 | 18 | ## 0.7.1 19 | 20 | ### Patch Changes 21 | 22 | - [`30729fb`](https://github.com/ocavue/astrobook/commit/30729fb316ba42ee22e2167e9226a3b812a5ad6b) Fix an issue where the theme toggle button was not working in some edge cases. 23 | 24 | ## 0.7.0 25 | 26 | ### Minor Changes 27 | 28 | - [`aebaa00`](https://github.com/ocavue/astrobook/commit/aebaa0090f9d8f73f1ed374eaa5cfd3e451641a0) Add a new `head` option. This allows you to configure the global styles and fonts. Please check the README.md for more information. 29 | 30 | Add a new `title` option to set the title for your Astrobook site. 31 | 32 | ## 0.6.0 33 | 34 | ### Minor Changes 35 | 36 | - [`74ee6e2`](https://github.com/ocavue/astrobook/commit/74ee6e2690146b6e39ddfa12a13d5115c4387fb9) Support Astro v5. 37 | 38 | ## 0.5.1 39 | 40 | ### Patch Changes 41 | 42 | - [`09c2d10`](https://github.com/ocavue/astrobook/commit/09c2d1029b94c2558fe9210b16059db7da7211cc) Group stories under the same directory in the sidebar. 43 | 44 | ## 0.5.0 45 | 46 | ### Minor Changes 47 | 48 | - [`4155bdf`](https://github.com/ocavue/astrobook/commit/4155bdf838ee0c1382407cefbc546c5250aab13f) Add a new `subpath` option so that Astrobook can be added to an existing Astro project. 49 | 50 | ## 0.4.4 51 | 52 | ### Patch Changes 53 | 54 | - [`b15252f`](https://github.com/ocavue/astrobook/commit/b15252fecd4965ae2a3f0f6fe0dea20ae346c58d) Fix an issue where the theme toggle button doesn't work. 55 | 56 | ## 0.4.3 57 | 58 | ### Patch Changes 59 | 60 | - [`dd343fb`](https://github.com/ocavue/astrobook/commit/dd343fba02ec12026192812f6c25940e9c360692) Update dependencies. 61 | 62 | ## 0.4.2 63 | 64 | ### Patch Changes 65 | 66 | - [`c1e8c88`](https://github.com/ocavue/astrobook/commit/c1e8c88671e2472c227495abaa7633ae082fea7f) Fix an issue where the theme toggle button was not focusable. 67 | 68 | ## 0.4.1 69 | 70 | ### Patch Changes 71 | 72 | - [`faa79f8`](https://github.com/ocavue/astrobook/commit/faa79f821ff63ee433f28aecd5fa261358d44c5a) Always show the theme button when scrolling down component list. 73 | 74 | ## 0.4.0 75 | 76 | ### Minor Changes 77 | 78 | - [`ea040d1`](https://github.com/ocavue/astrobook/commit/ea040d14585193f24ec50b89073bc18dc1837658) Add a button to toggle the color theme. 79 | 80 | ## 0.3.3 81 | 82 | ### Patch Changes 83 | 84 | - [`3e65d77`](https://github.com/ocavue/astrobook/commit/3e65d77a447ccffb9979d320ce97531a510807e9) Fix issues when running on Windows. 85 | 86 | ## 0.3.2 87 | 88 | ### Patch Changes 89 | 90 | - [`b55c9ca`](https://github.com/ocavue/astrobook/commit/b55c9caf38b5eb57572bc088f074c3d17d714b2e) Remove favicon. 91 | 92 | ## 0.3.1 93 | 94 | ### Patch Changes 95 | 96 | - [`8608ead`](https://github.com/ocavue/astrobook/commit/8608eadfe132d1470c2b592a7428ef9997de8c22) Refactor virtual routes to isolate each story in its own virtual component. This improves the performance and fixes style and script conflicts between stories. 97 | 98 | ## 0.3.0 99 | 100 | ### Minor Changes 101 | 102 | - [`b135226`](https://github.com/ocavue/astrobook/commit/b13522691dde443facf567c889ecf512dfb18ed4) Make the URL prettier by turning the story name into kebab-case. 103 | 104 | - [`399f35e`](https://github.com/ocavue/astrobook/commit/399f35e4301319053aaa983534509ea78d10f856) Support Astro `base` option. 105 | 106 | ### Patch Changes 107 | 108 | - [`e4b4b6e`](https://github.com/ocavue/astrobook/commit/e4b4b6e9151d3565574637e5dbf0d04227746adc) Enable scrolling in the main area. 109 | 110 | ## 0.2.0 111 | 112 | ### Minor Changes 113 | 114 | - [`a238792`](https://github.com/ocavue/astrobook/commit/a2387928b822f1ed8c0ec5cf5ba9d9ce61bbd3f1) Add `directory` option to `astrobook` integration to allow specifying the directory to scan for stories. 115 | 116 | - [`4a77ceb`](https://github.com/ocavue/astrobook/commit/4a77ceb75bc42f05c8474b9af47800da2c34b7b5) Add a fullscreen button. 117 | 118 | ## 0.1.1 119 | 120 | ### Patch Changes 121 | 122 | - [`de4ac26`](https://github.com/ocavue/astrobook/commit/de4ac26393aeaccfdd154ca47e7b828fdeedff6d) Fix incorrect `package.json` `files` field. 123 | 124 | ## 0.1.0 125 | 126 | ### Minor Changes 127 | 128 | - [`e1dcb95`](https://github.com/ocavue/astrobook/commit/e1dcb95c5e66c049c6cb94367d4ba09429635a30) Release first version. 129 | -------------------------------------------------------------------------------- /packages/astrobook/README.md: -------------------------------------------------------------------------------- 1 |
2 |

Astrobook

3 |

The minimal UI component playground

4 |
5 | 6 | ![astrobook](https://github.com/user-attachments/assets/02289aa9-df34-48f8-8aa5-42015c172443) 7 | 8 | Astrobook is a UI component playground that supports multiple frameworks including **React**, **Vue**, **Preact**, **Svelte**, **Solid**, **Lit**, and **Astro**. It offers a unified environment to develop, test, and showcase components. 9 | 10 | ## Try it Online 11 | 12 | - An example of using multiple UI rendering frameworks (React, Preact, Vue, Svelte, Solid, Lit, Astro) with Astrobook. 13 | 14 | Online demo: [astrobook.pages.dev](https://astrobook.pages.dev/) 15 | 16 | [![Open in StackBlitz][stackblitz_badge]][example_playground] 17 | 18 | - An example of using custom `` tags with Astrobook. 19 | 20 | [![Open in StackBlitz][stackblitz_badge]][example_custom_head] 21 | 22 | - An example that shows how to add Astrobook into an existing Astro project. 23 | 24 | [![Open in StackBlitz][stackblitz_badge]][example_mixed] 25 | 26 | - An example of using TailwindCSS with Astrobook. 27 | 28 | [![Open in StackBlitz][stackblitz_badge]][example_tailwindcss] 29 | 30 | - An example of using UnoCSS with Astrobook. 31 | 32 | [![Open in StackBlitz][stackblitz_badge]][example_unocss] 33 | 34 | ## Quick start 35 | 36 | > [!NOTE] 37 | > Astrobook supports various frameworks. We use React as an example here. Check the [Astro docs](https://docs.astro.build/en/guides/integrations-guide/#official-integrations) for other integrations. 38 | 39 | 1. Install the packages 40 | 41 | ```bash 42 | npm install astro @astrojs/react astrobook 43 | ``` 44 | 45 | 2. Create `astro.config.mjs` and add the `astrobook` integration 46 | 47 | ```js 48 | // astro.config.mjs 49 | import { defineConfig } from 'astro/config' 50 | import react from '@astrojs/react' 51 | import astrobook from 'astrobook' 52 | 53 | // https://astro.build/config 54 | export default defineConfig({ 55 | integrations: [react(), astrobook()], 56 | }) 57 | ``` 58 | 59 | 3. Add scripts to your `package.json` 60 | 61 | ```json 62 | "scripts": { 63 | "dev": "astro dev", 64 | "build": "astro build" 65 | } 66 | ``` 67 | 68 | 4. Write stories. Astrobook scans all `.stories.{ts,tsx,js,jsx,mts,mtsx,mjs,mjsx}` files. It's compatible with Storybook's [Component Story Format v3](https://storybook.js.org/docs/api/csf). 69 | 70 | ```ts 71 | // src/components/Button.stories.ts 72 | import { Button, type ButtonProps } from './Button.tsx' 73 | 74 | export default { 75 | component: Button, 76 | } 77 | 78 | export const PrimaryButton = { 79 | args: { 80 | variant: 'primary', 81 | } satisfies ButtonProps, 82 | } 83 | 84 | export const SecondaryButton = { 85 | args: { 86 | variant: 'secondary', 87 | } satisfies ButtonProps, 88 | } 89 | ``` 90 | 91 | 5. Run `npm run dev` and open `http://localhost:4321` to see your stories. 92 | 93 | ## Options 94 | 95 | ### `directory` 96 | 97 | You can use the `directory` option to specify the directory to scan for stories. The default directory is current working directory. 98 | 99 | ```js 100 | // astro.config.mjs 101 | import { defineConfig } from 'astro/config' 102 | import astrobook from 'astrobook' 103 | 104 | export default defineConfig({ 105 | integrations: [ 106 | astrobook({ 107 | directory: 'src/components', 108 | }), 109 | /* ...other integrations */ 110 | ], 111 | }) 112 | ``` 113 | 114 | ### `subpath` 115 | 116 | You can run Astrobook as a standalone app. You can also add it to your existing Astro project. In the latter case, you can use the `subpath` option to specify the subpath of Astrobook. 117 | 118 | ```js 119 | // astro.config.mjs 120 | import { defineConfig } from 'astro/config' 121 | import astrobook from 'astrobook' 122 | 123 | export default defineConfig({ 124 | integrations: [ 125 | astrobook({ 126 | subpath: '/docs/components', 127 | }), 128 | ], 129 | }) 130 | ``` 131 | 132 | In the example above, Astrobook will be available at `http://localhost:4321/docs/components`. 133 | 134 | Notice that the `subpath` option is relative to the [base URL](https://docs.astro.build/en/reference/configuration-reference/#base) of your Astro project. For example, if you configure both Astro's `base` and `astrobook`'s `subpath`, like so: 135 | 136 | ```js 137 | // astro.config.mjs 138 | import { defineConfig } from 'astro/config' 139 | import astrobook from 'astrobook' 140 | 141 | export default defineConfig({ 142 | base: '/base', 143 | integrations: [ 144 | astrobook({ 145 | subpath: '/docs/components', 146 | }), 147 | ], 148 | }) 149 | ``` 150 | 151 | You Astro project will be available at `http://localhost:4321/base` and Astrobook will be available at `http://localhost:4321/base/docs/components`. 152 | 153 | ### `css` 154 | 155 | You can customize the styles by using the `css` option to specify the CSS files to be imported into your Astrobook site. 156 | 157 | ```js 158 | // astro.config.mjs 159 | import { defineConfig } from 'astro/config' 160 | import astrobook from 'astrobook' 161 | 162 | export default defineConfig({ 163 | integrations: [ 164 | astrobook({ 165 | css: [ 166 | // Relative path to your custom CSS file 167 | './src/styles/custom.css', 168 | ], 169 | }), 170 | ], 171 | }) 172 | ``` 173 | 174 | ### `head` 175 | 176 | You can further customize your Astrobook project by providing a custom `head` options. It's a path to an Astro component that includes custom tags to the `` of your Astrobook site. It should only include [elements permitted inside ``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head#see_also), like ``, ` 191 | ``` 192 | 193 | ```js 194 | // astro.config.mjs 195 | import { defineConfig } from 'astro/config' 196 | import astrobook from 'astrobook' 197 | 198 | export default defineConfig({ 199 | integrations: [ 200 | astrobook({ 201 | // Relative path to your custom head component 202 | head: './src/components/CustomHead.astro', 203 | }), 204 | ], 205 | }) 206 | ``` 207 | 208 | ### `title` 209 | 210 | You can set the title for your website. 211 | 212 | ```js 213 | // astro.config.mjs 214 | import { defineConfig } from 'astro/config' 215 | import astrobook from 'astrobook' 216 | 217 | export default defineConfig({ 218 | integrations: [ 219 | astrobook({ 220 | title: 'My Components Playground', 221 | }), 222 | ], 223 | }) 224 | ``` 225 | 226 | ## Advanced 227 | 228 | ### Toggle theme via message 229 | 230 | If you're running Astrobook in an iframe, you can toggle the theme via a message. 231 | 232 | ```js 233 | const theme = 'light' // or "dark" 234 | iframe.contentWindow.postMessage({ type: 'astrobook:set-theme', theme }, '*') 235 | ``` 236 | 237 | ## Who's using Astrobook? 238 | 239 | - [ProseKit](https://prosekit.dev/astrobook) 240 | 241 | _[Add your project](https://github.com/ocavue/astrobook/edit/master/packages/astrobook/README.md)_ 242 | 243 | ## License 244 | 245 | MIT 246 | 247 | [stackblitz_badge]: https://developer.stackblitz.com/img/open_in_stackblitz.svg 248 | [example_playground]: https://stackblitz.com/github/ocavue/astrobook/tree/master/examples/playground 249 | [example_unocss]: https://stackblitz.com/github/ocavue/astrobook/tree/master/examples/unocss 250 | [example_tailwindcss]: https://stackblitz.com/github/ocavue/astrobook/tree/master/examples/tailwindcss 251 | [example_custom_head]: https://stackblitz.com/github/ocavue/astrobook/tree/master/examples/custom-head 252 | [example_mixed]: https://stackblitz.com/github/ocavue/astrobook/tree/master/examples/mixed 253 | -------------------------------------------------------------------------------- /packages/astrobook/lib/components/head.astro: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/astrobook/lib/pages/app.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import AppComponent from '@astrobook/ui/components/app.astro' 3 | import type { GetStaticPathsResult } from 'astro' 4 | 5 | export function getStaticPaths(): GetStaticPathsResult { 6 | return [ 7 | { 8 | params: { 9 | path: undefined, 10 | }, 11 | }, 12 | ] 13 | } 14 | --- 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/astrobook/lib/pages/story.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import LayoutComponent from '@astrobook/ui/components/layout.astro' 3 | import type { ComponentProps } from 'astro/types' 4 | 5 | type Props = ComponentProps 6 | --- 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/astrobook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astrobook", 3 | "type": "module", 4 | "version": "0.8.1", 5 | "description": "The minimal UI component playground", 6 | "author": "ocavue ", 7 | "license": "MIT", 8 | "funding": "https://github.com/sponsors/ocavue", 9 | "homepage": "https://github.com/ocavue/astrobook#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/ocavue/astrobook.git" 13 | }, 14 | "bugs": "https://github.com/ocavue/astrobook/issues", 15 | "keywords": [ 16 | "ui", 17 | "component", 18 | "playground", 19 | "storybook", 20 | "react", 21 | "vue", 22 | "preact", 23 | "svelte", 24 | "solid", 25 | "lit", 26 | "astro", 27 | "vite" 28 | ], 29 | "sideEffects": false, 30 | "main": "./src/index.ts", 31 | "module": "./src/index.ts", 32 | "types": "./src/index.ts", 33 | "exports": { 34 | ".": { 35 | "default": "./src/index.ts" 36 | }, 37 | "./client": { 38 | "default": "./src/client.ts" 39 | }, 40 | "./components/*": { 41 | "default": "./lib/components/*" 42 | }, 43 | "./pages/*": { 44 | "default": "./lib/pages/*" 45 | } 46 | }, 47 | "typesVersions": { 48 | "*": { 49 | ".": [ 50 | "./src/index.ts" 51 | ], 52 | "./client": [ 53 | "./src/client.ts" 54 | ], 55 | "./pages/*": [ 56 | "./lib/pages/*" 57 | ], 58 | "./components/*": [ 59 | "./lib/components/*" 60 | ] 61 | } 62 | }, 63 | "files": [ 64 | "dist", 65 | "lib" 66 | ], 67 | "scripts": { 68 | "build": "tsc --build tsconfig.build.json" 69 | }, 70 | "dependencies": { 71 | "@astrobook/core": "workspace:*", 72 | "@astrobook/types": "workspace:*", 73 | "@astrobook/ui": "workspace:*" 74 | }, 75 | "peerDependencies": { 76 | "astro": ">=4.0.0" 77 | }, 78 | "peerDependenciesMeta": { 79 | "astro": { 80 | "optional": true 81 | } 82 | }, 83 | "devDependencies": { 84 | "@types/node": "^20.17.5", 85 | "astro": "^5.8.1", 86 | "typescript": "^5.8.3", 87 | "vite": "^6.3.5", 88 | "vitest": "^3.1.4" 89 | }, 90 | "publishConfig": { 91 | "exports": { 92 | ".": { 93 | "types": "./dist/index.d.ts", 94 | "import": "./dist/index.js", 95 | "default": "./dist/index.js" 96 | }, 97 | "./client": { 98 | "types": "./dist/client.d.ts", 99 | "import": "./dist/client.js", 100 | "default": "./dist/client.js" 101 | }, 102 | "./components/*": { 103 | "default": "./lib/components/*" 104 | }, 105 | "./pages/*": { 106 | "default": "./lib/pages/*" 107 | } 108 | }, 109 | "main": "./dist/index.js", 110 | "module": "./dist/index.js", 111 | "types": "./dist/index.d.ts", 112 | "typesVersions": { 113 | "*": { 114 | ".": [ 115 | "./dist/index.d.ts" 116 | ], 117 | "./client": [ 118 | "./dist/client.d.ts" 119 | ], 120 | "./pages/*": [ 121 | "./lib/pages/*" 122 | ], 123 | "./components/*": [ 124 | "./lib/components/*" 125 | ] 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /packages/astrobook/src/client.ts: -------------------------------------------------------------------------------- 1 | export * from '@astrobook/core/client' 2 | -------------------------------------------------------------------------------- /packages/astrobook/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createAstrobookIntegration } from '@astrobook/core' 2 | 3 | export default createAstrobookIntegration 4 | -------------------------------------------------------------------------------- /packages/astrobook/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./dist/.tsbuildinfo", 5 | "emitDeclarationOnly": false, 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "composite": true 9 | }, 10 | "include": ["./**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/astrobook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true 5 | }, 6 | "include": ["src/**/*", "./*.js", "./*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astrobook/core", 3 | "type": "module", 4 | "version": "0.8.1", 5 | "description": "", 6 | "author": "ocavue ", 7 | "license": "MIT", 8 | "funding": "https://github.com/sponsors/ocavue", 9 | "homepage": "https://github.com/ocavue/astrobook#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/ocavue/astrobook.git" 13 | }, 14 | "bugs": "https://github.com/ocavue/astrobook/issues", 15 | "keywords": [], 16 | "sideEffects": false, 17 | "main": "./src/index.ts", 18 | "module": "./src/index.ts", 19 | "types": "./src/index.ts", 20 | "exports": { 21 | ".": { 22 | "default": "./src/index.ts" 23 | }, 24 | "./client": { 25 | "default": "./src/client.ts" 26 | } 27 | }, 28 | "files": [ 29 | "dist" 30 | ], 31 | "scripts": { 32 | "build": "tsdown" 33 | }, 34 | "dependencies": { 35 | "@astrobook/types": "workspace:*", 36 | "acorn": "^8.14.1", 37 | "acorn-jsx": "^5.3.2", 38 | "fdir": "^6.4.5", 39 | "picomatch": "^4.0.2", 40 | "slash": "^5.1.0" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^20.17.5", 44 | "@types/picomatch": "^4.0.0", 45 | "@types/react": "^19.1.6", 46 | "@types/react-dom": "^19.1.5", 47 | "astro": "^5.8.1", 48 | "just-kebab-case": "^4.2.0", 49 | "tsdown": "^0.12.5", 50 | "typescript": "^5.8.3", 51 | "vite": "^6.3.5", 52 | "vitest": "^3.1.4" 53 | }, 54 | "publishConfig": { 55 | "exports": { 56 | ".": { 57 | "types": "./dist/index.d.ts", 58 | "import": "./dist/index.js", 59 | "default": "./dist/index.js" 60 | }, 61 | "./client": { 62 | "types": "./dist/client.d.ts", 63 | "import": "./dist/client.js", 64 | "default": "./dist/client.js" 65 | } 66 | }, 67 | "main": "./dist/index.js", 68 | "module": "./dist/index.js", 69 | "types": "./dist/index.d.ts", 70 | "typesVersions": { 71 | "*": { 72 | ".": [ 73 | "./dist/index.d.ts" 74 | ], 75 | "./client": [ 76 | "./dist/client.d.ts" 77 | ] 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/core/src/astro-integration.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import path from 'node:path' 3 | import pathPosix from 'node:path/posix' 4 | import { fileURLToPath } from 'node:url' 5 | 6 | import type { IntegrationOptions } from '@astrobook/types' 7 | import type { AstroIntegration } from 'astro' 8 | 9 | import { 10 | createVirtualRouteComponent, 11 | getVirtualRoutes, 12 | } from './virtual-module/virtual-routes' 13 | import { createVirtualFilesPlugin } from './virtual-module/vite-plugin' 14 | 15 | export function createAstrobookIntegration( 16 | options?: IntegrationOptions, 17 | ): AstroIntegration { 18 | return { 19 | name: 'astrobook', 20 | hooks: { 21 | 'astro:config:setup': async ({ 22 | updateConfig, 23 | injectRoute, 24 | createCodegenDir, 25 | config, 26 | logger, 27 | }) => { 28 | const rootDir = path.resolve(options?.directory || '.') 29 | const astroBaseUrl = config.base || '/' 30 | const astrobookBaseUrl = options?.subpath || '' 31 | const baseUrl = pathPosix.join(astroBaseUrl, astrobookBaseUrl) 32 | 33 | logger.debug(`Creating dedicated folder`) 34 | let codegenDir: string 35 | if (createCodegenDir) { 36 | codegenDir = fileURLToPath(createCodegenDir()) 37 | } else { 38 | // Astro v4, where `createCodegenDir()` is not available 39 | codegenDir = path.resolve('.astro', 'integrations', 'astrobook') 40 | await fs.mkdir(codegenDir, { recursive: true }) 41 | } 42 | logger.debug(`Created dedicated folder at ${codegenDir}`) 43 | 44 | logger.debug(`Scanning for stories in ${rootDir}`) 45 | const routes = await getVirtualRoutes(rootDir, codegenDir, logger) 46 | 47 | logger.debug(`Writing files to ${codegenDir}`) 48 | await Promise.all( 49 | Array.from(routes.values()).map(async (route) => { 50 | const filePath = route.entrypoint 51 | const fileContent = createVirtualRouteComponent(route) 52 | await fs.mkdir(path.dirname(filePath), { recursive: true }) 53 | await fs.writeFile(filePath, fileContent, { encoding: 'utf-8' }) 54 | }), 55 | ) 56 | 57 | updateConfig({ 58 | vite: { 59 | plugins: [ 60 | createVirtualFilesPlugin( 61 | rootDir, 62 | { 63 | baseUrl, 64 | head: options?.head || 'astrobook/components/head.astro', 65 | css: options?.css || [], 66 | title: options?.title || 'Astrobook', 67 | }, 68 | config, 69 | ), 70 | ], 71 | }, 72 | }) 73 | 74 | logger.debug(`Injecting routes`) 75 | for (const route of routes.values()) { 76 | const pattern = pathPosix.join(astrobookBaseUrl, route.pattern) 77 | const entrypoint = path.normalize( 78 | path.relative('.', route.entrypoint), 79 | ) 80 | injectRoute({ 81 | pattern, 82 | entrypoint, 83 | prerender: true, 84 | }) 85 | } 86 | injectRoute({ 87 | pattern: astrobookBaseUrl, 88 | entrypoint: 'astrobook/pages/app.astro', 89 | prerender: true, 90 | }) 91 | }, 92 | }, 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/core/src/client.ts: -------------------------------------------------------------------------------- 1 | import type { AstroComponentFactory } from 'astro/runtime/server/index.js' 2 | 3 | export { getPathWithBase } from './utils/base' 4 | 5 | export function isAstroStory(module: { default?: { component?: unknown } }) { 6 | try { 7 | const component = module.default?.component 8 | if (!component) return false 9 | 10 | return isAstroComponentFactory(component) 11 | } catch { 12 | return false 13 | } 14 | } 15 | 16 | // Copy from https://github.com/withastro/astro/blob/astro@5.0.0/packages/astro/src/runtime/server/render/astro/factory.ts#L15 17 | export function isAstroComponentFactory( 18 | obj: unknown, 19 | ): obj is AstroComponentFactory { 20 | return obj == null 21 | ? false 22 | : (obj as Record).isAstroComponentFactory === true 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createAstrobookIntegration } from './astro-integration' 2 | 3 | export { createAstrobookIntegration } 4 | -------------------------------------------------------------------------------- /packages/core/src/utils/base.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | 3 | import { getPathWithBase } from './base' 4 | 5 | test('getPathWithBase', () => { 6 | expect(getPathWithBase('/path', '')).toMatchInlineSnapshot(`"/path"`) 7 | expect(getPathWithBase('/path/', '')).toMatchInlineSnapshot(`"/path"`) 8 | expect(getPathWithBase('/path/', '/base')).toMatchInlineSnapshot( 9 | `"/base/path"`, 10 | ) 11 | expect(getPathWithBase('/path', '/')).toMatchInlineSnapshot(`"/path"`) 12 | expect(getPathWithBase('/', '/')).toMatchInlineSnapshot(`"/"`) 13 | expect(getPathWithBase('/', 'base')).toMatchInlineSnapshot(`"/base"`) 14 | expect(getPathWithBase('/path', 'base/')).toMatchInlineSnapshot( 15 | `"/base/path"`, 16 | ) 17 | }) 18 | -------------------------------------------------------------------------------- /packages/core/src/utils/base.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { ensureLeadingSlash, stripSlashes } from './path' 4 | 5 | export function getPathWithBase(path: string, base: string): string { 6 | return ensureLeadingSlash( 7 | [base, path].map(stripSlashes).filter(Boolean).join('/'), 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/utils/get-exports.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | import { getExports } from './get-exports' 4 | 5 | describe('getExports', () => { 6 | it('can parse named exports', () => { 7 | expect(getExports('export const a = 1')).toEqual(['a']) 8 | expect(getExports('const a = 1; export { a }')).toEqual(['a']) 9 | expect(getExports('const a = 1; export { a as b }')).toEqual(['b']) 10 | expect(getExports('export function a () {}')).toEqual(['a']) 11 | expect(getExports('export class A {}')).toEqual(['A']) 12 | expect(getExports('export { a } from "./other"')).toEqual(['a']) 13 | }) 14 | 15 | it('can parse named exports', () => { 16 | expect(getExports('export default { name: "a" }')).toEqual(['default']) 17 | expect(getExports('const a = 1; export { a as default }')).toEqual([ 18 | 'default', 19 | ]) 20 | }) 21 | 22 | it('can parse JSX', () => { 23 | expect( 24 | getExports('export function a() { return
hello
}'), 25 | ).toEqual(['a']) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /packages/core/src/utils/get-exports.ts: -------------------------------------------------------------------------------- 1 | import { Parser } from 'acorn' 2 | import jsx from 'acorn-jsx' 3 | 4 | /** 5 | * Parses the content of the given file and returns all its exports 6 | */ 7 | export function getExports(code: string): string[] { 8 | // Parse the code into an AST 9 | const parser = Parser.extend(jsx()) 10 | const ast = parser.parse(code, { 11 | sourceType: 'module', 12 | ecmaVersion: 'latest', 13 | allowImportExportEverywhere: true, 14 | }) 15 | 16 | const exports = new Set() 17 | 18 | // Walk through the AST 19 | ast.body.forEach((node) => { 20 | if (node.type === 'ExportNamedDeclaration') { 21 | node.specifiers.forEach((specifier) => { 22 | const { exported } = specifier 23 | if (exported.type === 'Identifier') { 24 | exports.add(exported.name) 25 | } 26 | }) 27 | 28 | const { declaration } = node 29 | if (declaration?.type === 'VariableDeclaration') { 30 | const { declarations } = declaration 31 | declarations.forEach((declaration) => { 32 | const id = declaration.id 33 | if (id.type === 'Identifier') { 34 | exports.add(id.name) 35 | } 36 | }) 37 | } 38 | if (declaration?.type === 'FunctionDeclaration') { 39 | exports.add(declaration.id.name) 40 | } 41 | if (declaration?.type === 'ClassDeclaration') { 42 | exports.add(declaration.id.name) 43 | } 44 | } else if (node.type === 'ExportDefaultDeclaration') { 45 | exports.add('default') 46 | } 47 | }) 48 | 49 | return Array.from(exports).sort() 50 | } 51 | -------------------------------------------------------------------------------- /packages/core/src/utils/invariant.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert' 2 | 3 | export function invariant( 4 | condition: unknown, 5 | message = 'Unexpected invariant violation', 6 | ): asserts condition { 7 | if (!condition) { 8 | assert( 9 | condition, 10 | `[astrobook] Unexpected internal error. Please open an issue at https://github.com/ocavue/astrobook/issues. ${message}`, 11 | ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/utils/path.ts: -------------------------------------------------------------------------------- 1 | /** Ensure the passed path does not start and end with slashes. */ 2 | export function stripSlashes(path: string): string { 3 | return path.split('/').filter(Boolean).join('/') 4 | } 5 | 6 | /** Ensure the passed path starts with a leading slash. */ 7 | export function ensureLeadingSlash(path: string): string { 8 | if (path.startsWith('/')) { 9 | return path 10 | } 11 | return '/' + path 12 | } 13 | 14 | /** Ensure the passed path ends with a trailing slash. */ 15 | export function ensureTrailingSlash(path: string): string { 16 | if (path.endsWith('/')) { 17 | return path 18 | } 19 | return path + '/' 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/virtual-module/get-story-modules.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | 3 | import { convertStoryFileToModule } from './get-story-modules' 4 | 5 | test('convertStoryFileToModule', () => { 6 | expect( 7 | convertStoryFileToModule('/my-project', { 8 | filePath: '/my-project/path/to/Button.stories.js', 9 | defaultExport: true, 10 | namedExports: ['PrimaryButton', 'SecondaryButton'], 11 | }), 12 | ).toMatchInlineSnapshot(` 13 | { 14 | "directory": "path/to", 15 | "id": "path/to/button", 16 | "importPath": "/my-project/path/to/Button.stories.js", 17 | "name": "Button", 18 | "stories": [ 19 | { 20 | "id": "path/to/button/primary-button", 21 | "name": "PrimaryButton", 22 | }, 23 | { 24 | "id": "path/to/button/secondary-button", 25 | "name": "SecondaryButton", 26 | }, 27 | ], 28 | } 29 | `) 30 | }) 31 | -------------------------------------------------------------------------------- /packages/core/src/virtual-module/get-story-modules.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import path from 'node:path' 3 | 4 | import type { StoryModule } from '@astrobook/types' 5 | import { fdir } from 'fdir' 6 | import kebabCase from 'just-kebab-case' 7 | import slash from 'slash' 8 | 9 | import { getExports } from '../utils/get-exports' 10 | import { invariant } from '../utils/invariant' 11 | 12 | /** 13 | * List the absolute paths of all story files in the given directory. 14 | */ 15 | async function listStoryFiles(rootDir: string): Promise { 16 | invariant( 17 | path.isAbsolute(rootDir), 18 | `Root directory must be absolute, but got '${rootDir}'`, 19 | ) 20 | 21 | const filePaths = await new fdir() 22 | .withSymlinks() 23 | .withFullPaths() 24 | .normalize() 25 | .glob('./**/*.stories.{ts,tsx,js,jsx,mts,mtsx,mjs,mjsx}') 26 | .exclude((dirname) => dirname === 'node_modules') 27 | .crawl(rootDir) 28 | .withPromise() 29 | 30 | return filePaths.sort() 31 | } 32 | 33 | type ParsedStoryFile = { 34 | /** 35 | * The absolute path to the file. Unix-style slashes are used. 36 | */ 37 | filePath: string 38 | 39 | /** 40 | * Whether the file has a default export 41 | */ 42 | defaultExport: boolean 43 | 44 | /** 45 | * The named exports in the file 46 | */ 47 | namedExports: string[] 48 | } 49 | 50 | async function parseStoryFiles(filePath: string): Promise { 51 | const code = await fs.readFile(filePath, 'utf-8') 52 | const exports = getExports(code) 53 | const defaultExport = exports.includes('default') 54 | const namedExports = exports.filter((name) => name !== 'default') 55 | 56 | return { filePath, defaultExport, namedExports } 57 | } 58 | 59 | export function convertStoryFileToModule( 60 | rootDir: string, 61 | file: ParsedStoryFile, 62 | ): StoryModule | undefined { 63 | if (file.namedExports.length === 0) { 64 | console.warn(`[astrobook] File ${file.filePath} has no named exports`) 65 | return 66 | } 67 | 68 | if (!file.defaultExport) { 69 | console.warn(`[astrobook] File ${file.filePath} has no default export`) 70 | return 71 | } 72 | 73 | const relativePath = path.normalize(path.relative(rootDir, file.filePath)) 74 | const relativePathWithoutStories = relativePath.replace( 75 | /\.stories\.\w+$/i, 76 | '', 77 | ) 78 | const parts = relativePathWithoutStories.split(path.sep) 79 | 80 | const directory = parts.slice(0, -1).join('/') 81 | const name = parts.pop() 82 | invariant(name, `Unexpected file path: ${file.filePath}`) 83 | 84 | const moduleId = directory 85 | ? `${directory}/${kebabCase(name)}` 86 | : kebabCase(name) 87 | 88 | return { 89 | id: moduleId, 90 | name, 91 | directory, 92 | importPath: slash(file.filePath), 93 | stories: file.namedExports.map((name) => { 94 | return { id: `${moduleId}/${kebabCase(name)}`, name } 95 | }), 96 | } 97 | } 98 | 99 | export async function getStoryModules(rootDir: string): Promise { 100 | const storyFilePaths = await listStoryFiles(rootDir) 101 | const storyFiles = await Promise.all(storyFilePaths.map(parseStoryFiles)) 102 | 103 | return storyFiles 104 | .map((storyFile) => convertStoryFileToModule(rootDir, storyFile)) 105 | .filter((module) => !!module) 106 | } 107 | -------------------------------------------------------------------------------- /packages/core/src/virtual-module/story-modules.ts: -------------------------------------------------------------------------------- 1 | import { getStoryModules } from './get-story-modules' 2 | 3 | export async function loadStoryModules(rootDir: string): Promise { 4 | const modules = await getStoryModules(rootDir) 5 | return `export default ${JSON.stringify(modules)}` 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/virtual-module/virtual-module-ids.ts: -------------------------------------------------------------------------------- 1 | export const STORY_MODULES_ID = 'virtual:astrobook/story-modules.mjs' 2 | export const STORY_MODULES_RESOLVED_ID = 3 | '__virtual_astrobook_story_modules__.mjs' 4 | 5 | export const GLOBAL_CONFIG_ID = 'virtual:astrobook/global-config.mjs' 6 | export const GLOBAL_CONFIG_RESOLVED_ID = '__virtual_astrobook_user_config__.mjs' 7 | 8 | export const COMPONENT_HEAD_ID = 'virtual:astrobook/components/head.mjs' 9 | export const COMPONENT_HEAD_RESOLVED_ID = 10 | '__virtual_astrobook_COMPONENT_HEAD__.mjs' 11 | 12 | export const USER_CSS_ID = 'virtual:astrobook/user-css.mjs' 13 | export const USER_CSS_RESOLVED_ID = '__virtual_astrobook_user_css__.mjs' 14 | -------------------------------------------------------------------------------- /packages/core/src/virtual-module/virtual-routes.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | import type { Story, StoryModule } from '@astrobook/types' 4 | import type { AstroIntegrationLogger } from 'astro' 5 | import slash from 'slash' 6 | 7 | import { invariant } from '../utils/invariant' 8 | 9 | import { getStoryModules } from './get-story-modules' 10 | 11 | export interface VirtualRoute { 12 | pattern: string 13 | /** 14 | * The absolute path to the virtual entrypoint file. Posix slash format. 15 | * 16 | * It's important to use an absolute path here because we must ensure that we 17 | * only have one `id`, so that Astro's CSS plugin can find the correct CSS 18 | * content in the following line. 19 | * https://github.com/withastro/astro/blob/bc2796436dc3810e988c27b71b7a66fcb1ae8bda/packages/astro/src/core/build/plugins/plugin-css.ts#L144 20 | */ 21 | entrypoint: string 22 | storyModule: StoryModule 23 | story: Story 24 | props: { 25 | hasSidebar: boolean 26 | story: string 27 | } 28 | } 29 | 30 | export async function getVirtualRoutes( 31 | rootDir: string, 32 | codegenDir: string, 33 | logger: AstroIntegrationLogger, 34 | ): Promise> { 35 | const routes: VirtualRoute[] = [] 36 | const storyModules = await getStoryModules(rootDir) 37 | 38 | for (const storyModule of storyModules) { 39 | logger.debug( 40 | `Found ${storyModule.stories.length} stories in ${storyModule.importPath}`, 41 | ) 42 | for (const story of storyModule.stories) { 43 | invariant( 44 | !story.id.startsWith('..'), 45 | `Story ID cannot start with '..', but got '${story.id}'`, 46 | ) 47 | invariant( 48 | !story.id.startsWith('/'), 49 | `Story ID cannot start with '/', but got '${story.id}'`, 50 | ) 51 | routes.push( 52 | { 53 | pattern: '/dashboard/' + story.id, 54 | entrypoint: slash( 55 | path.resolve(codegenDir, 'dashboard', story.id + '.astro'), 56 | ), 57 | storyModule, 58 | story, 59 | props: { hasSidebar: true, story: story.id }, 60 | }, 61 | { 62 | pattern: '/stories/' + story.id, 63 | entrypoint: slash( 64 | path.resolve(codegenDir, 'stories', story.id + '.astro'), 65 | ), 66 | storyModule, 67 | story, 68 | props: { hasSidebar: false, story: story.id }, 69 | }, 70 | ) 71 | } 72 | } 73 | 74 | logger.info(`Found ${routes.length} stories in ${storyModules.length} files`) 75 | 76 | return new Map(routes.map((route) => [route.pattern, route])) 77 | } 78 | 79 | export function createVirtualRouteComponent(route: VirtualRoute): string { 80 | return ` 81 | --- 82 | import StoryPage from 'astrobook/pages/story.astro' 83 | import { isAstroStory } from 'astrobook/client' 84 | import * as m from '${route.storyModule.importPath}' 85 | 86 | const isAstro = isAstroStory(m) 87 | --- 88 | 89 | 90 | { 91 | isAstro 92 | ? () 93 | : () 94 | } 95 | 96 | `.trim() 97 | } 98 | -------------------------------------------------------------------------------- /packages/core/src/virtual-module/vite-plugin.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { fileURLToPath } from 'node:url' 3 | 4 | import type { GlobalConfig } from '@astrobook/types' 5 | import type { AstroConfig } from 'astro' 6 | import type { Plugin } from 'vite' 7 | 8 | import { loadStoryModules } from './story-modules' 9 | import { 10 | COMPONENT_HEAD_ID, 11 | COMPONENT_HEAD_RESOLVED_ID, 12 | STORY_MODULES_ID, 13 | STORY_MODULES_RESOLVED_ID, 14 | GLOBAL_CONFIG_ID, 15 | GLOBAL_CONFIG_RESOLVED_ID, 16 | USER_CSS_RESOLVED_ID, 17 | USER_CSS_ID, 18 | } from './virtual-module-ids' 19 | 20 | export function createVirtualFilesPlugin( 21 | rootDir: string, 22 | config: GlobalConfig, 23 | astroConfig: AstroConfig, 24 | ): Plugin { 25 | const root = astroConfig.root 26 | 27 | /** 28 | * Resolves module IDs to a usable format: 29 | * - Relative paths (e.g. `'./module.js'`) are resolved against `base` and formatted as an absolute path. 30 | * - Package identifiers (e.g. `'module'`) are returned unchanged. 31 | * 32 | * By default, `base` is the project root directory. 33 | */ 34 | function resolveId(id: string, base = root) { 35 | return JSON.stringify( 36 | id.startsWith('.') ? resolve(fileURLToPath(base), id) : id, 37 | ) 38 | } 39 | 40 | return { 41 | name: 'astrobook/virtual-files', 42 | resolveId(id) { 43 | switch (id) { 44 | case STORY_MODULES_ID: 45 | return STORY_MODULES_RESOLVED_ID 46 | case GLOBAL_CONFIG_ID: 47 | return GLOBAL_CONFIG_RESOLVED_ID 48 | case COMPONENT_HEAD_ID: 49 | return COMPONENT_HEAD_RESOLVED_ID 50 | case USER_CSS_ID: 51 | return USER_CSS_RESOLVED_ID 52 | } 53 | }, 54 | load(id) { 55 | switch (id) { 56 | case STORY_MODULES_RESOLVED_ID: 57 | return loadStoryModules(rootDir) 58 | case GLOBAL_CONFIG_RESOLVED_ID: 59 | return `const config = ${JSON.stringify(config)}; export default config;` 60 | case COMPONENT_HEAD_RESOLVED_ID: 61 | return `export { default } from ${resolveId(config.head)};` 62 | case USER_CSS_RESOLVED_ID: 63 | return config.css.map((id) => `import ${resolveId(id)};`).join('') 64 | } 65 | }, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true 5 | }, 6 | "include": ["./src/**/*.ts", "./src/**/*.d.ts", "./*.js", "./*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsdown' 2 | 3 | const config: unknown = defineConfig({ 4 | entry: { 5 | index: 'src/index.ts', 6 | client: 'src/client.ts', 7 | }, 8 | format: ['esm'], 9 | clean: true, 10 | dts: true, 11 | external: [/^virtual:/], 12 | }) 13 | 14 | export default config 15 | -------------------------------------------------------------------------------- /packages/core/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineProject } from 'vitest/config' 2 | 3 | export default defineProject({ 4 | test: {}, 5 | }) 6 | -------------------------------------------------------------------------------- /packages/types/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export * from './types' 4 | -------------------------------------------------------------------------------- /packages/types/lib/index.js: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /packages/types/lib/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface IntegrationOptions { 2 | /** 3 | * The directory to scan for stories. 4 | * 5 | * @default '.' 6 | * 7 | * @example 8 | * 9 | * ```js 10 | * astrobook({ 11 | * directory: './src/astrobook', 12 | * }) 13 | * ``` 14 | */ 15 | directory?: string 16 | 17 | /** 18 | * The subpath to deploy Astrobook to relative to the [base 19 | * URL](https://docs.astro.build/en/reference/configuration-reference/#base) 20 | * of your Astro project. This is useful if you want to use Astrobook in your 21 | * existing Astro project. 22 | * 23 | * @example 24 | * 25 | * ```js 26 | * astrobook({ 27 | * subpath: '/docs/components', 28 | * }) 29 | * ``` 30 | */ 31 | subpath?: string 32 | 33 | /** 34 | * Set the title for your website. Will be used in metadata and in the browser tab title. 35 | * 36 | * @default 'Astrobook' 37 | * 38 | * @example 39 | * 40 | * ```js 41 | * astrobook({ 42 | * title: 'My Components Playground', 43 | * }) 44 | * ``` 45 | */ 46 | title?: string 47 | 48 | /** 49 | * Provide CSS files to customize the look and feel of your Astrobook project. 50 | * 51 | * Supports local CSS files relative to the root of your project, 52 | * e.g. `'./src/custom.css'`, and CSS you installed as an npm 53 | * module, e.g. `'@fontsource/roboto'`. 54 | * 55 | * @example 56 | * 57 | * ```js 58 | * astrobook({ 59 | * css: ['./src/custom-styles.css', '@fontsource/roboto'], 60 | * }) 61 | * ``` 62 | */ 63 | css?: string[] 64 | 65 | /** 66 | * The path to an Astro component to provide custom tags in the ``. 67 | * 68 | * @example 69 | * 70 | * ```js 71 | * astrobook({ 72 | * head: './src/components/CustomHead.astro', 73 | * }) 74 | * ``` 75 | * 76 | * ```astro 77 | * astrobook({ 78 | * head: './src/components/CustomHead.html', 79 | * }) 80 | * ``` 81 | */ 82 | head?: string 83 | } 84 | 85 | export interface StoryModule { 86 | /** 87 | * The id of the story module. 88 | * 89 | * @example 'components/ui/button' 90 | */ 91 | id: string 92 | 93 | /** 94 | * The name of the story module. 95 | * 96 | * @example 'Button' 97 | */ 98 | name: string 99 | 100 | /** 101 | * The directory of the story module. It might be an empty string. 102 | * 103 | * @example 'components/ui' 104 | */ 105 | directory: string 106 | 107 | /** 108 | * The import path of the story module. 109 | * 110 | * @example '/Users/john/projects/my-project/components/ui/Button.stories.js' 111 | * @example '../ui/Button.stories.ts' 112 | */ 113 | importPath: string 114 | 115 | /** 116 | * All stories in the story module. 117 | */ 118 | stories: Story[] 119 | } 120 | 121 | export interface Story { 122 | /** 123 | * The id of the story. 124 | * 125 | * @example 'components/ui/button/primary-button' 126 | */ 127 | id: string 128 | 129 | /** 130 | * The name of the story. 131 | * 132 | * @example 'PrimaryButton' 133 | */ 134 | name: string 135 | } 136 | 137 | export interface GlobalConfig { 138 | baseUrl: string 139 | head: string 140 | css: string[] 141 | title: string 142 | } 143 | 144 | declare global { 145 | interface Window { 146 | astrobook?: { 147 | setTheme?: (theme: 'dark' | 'light') => void 148 | getTheme?: () => 'dark' | 'light' 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /packages/types/lib/virtual.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'virtual:astrobook/story-modules.mjs' { 2 | const entries: import('./types').StoryModule[] 3 | export default entries 4 | } 5 | 6 | declare module 'virtual:astrobook/global-config.mjs' { 7 | const config: import('./types').GlobalConfig 8 | export default config 9 | } 10 | 11 | declare module 'virtual:astrobook/components/head.mjs' { 12 | const Head: (props: unknown) => unknown 13 | export default Head 14 | } 15 | 16 | declare module 'virtual:astrobook/user-css.mjs' {} 17 | -------------------------------------------------------------------------------- /packages/types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astrobook/types", 3 | "type": "module", 4 | "version": "0.8.1", 5 | "description": "", 6 | "author": "ocavue ", 7 | "license": "MIT", 8 | "funding": "https://github.com/sponsors/ocavue", 9 | "homepage": "https://github.com/ocavue/astrobook#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/ocavue/astrobook.git" 13 | }, 14 | "bugs": "https://github.com/ocavue/astrobook/issues", 15 | "keywords": [], 16 | "sideEffects": false, 17 | "main": "./lib/index.js", 18 | "module": "./lib/index.js", 19 | "types": "./lib/index.d.ts", 20 | "exports": { 21 | ".": { 22 | "types": "./lib/index.d.ts", 23 | "default": "./lib/index.js" 24 | } 25 | }, 26 | "files": [ 27 | "lib" 28 | ], 29 | "devDependencies": { 30 | "typescript": "^5.8.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true 5 | }, 6 | "include": ["lib/"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/copy.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'node:crypto' 2 | import { promises as fs } from 'node:fs' 3 | import * as path from 'node:path' 4 | 5 | async function getFileHash(filePath: string): Promise { 6 | const fileBuffer = await fs.readFile(filePath) 7 | const hashSum = crypto.createHash('sha256') 8 | hashSum.update(fileBuffer) 9 | return hashSum.digest('hex') 10 | } 11 | 12 | async function copyFile(srcPath: string, destPath: string): Promise { 13 | // Check if destination file exists 14 | try { 15 | await fs.access(destPath) 16 | // If file exists, compare content 17 | const [srcHash, destHash] = await Promise.all([ 18 | getFileHash(srcPath), 19 | getFileHash(destPath), 20 | ]) 21 | 22 | if (srcHash === destHash) { 23 | return 24 | } 25 | } catch { 26 | // Destination file doesn't exist, we'll copy it 27 | } 28 | 29 | // Copy file 30 | await fs.copyFile(srcPath, destPath) 31 | console.log(`[copy.ts] ${srcPath} -> ${destPath}`) 32 | } 33 | 34 | async function copyDirectory(srcPath: string, destPath: string): Promise { 35 | // Create destination directory if it doesn't exist 36 | await fs.mkdir(destPath, { recursive: true }) 37 | 38 | // Read all files/directories in the source directory 39 | const entries = await fs.readdir(srcPath, { withFileTypes: true }) 40 | 41 | await Promise.all( 42 | entries.map(async (entry) => { 43 | const copy = entry.isDirectory() ? copyDirectory : copyFile 44 | await copy( 45 | path.join(srcPath, entry.name), 46 | path.join(destPath, entry.name), 47 | ) 48 | }), 49 | ) 50 | } 51 | 52 | async function main() { 53 | // Get command line arguments 54 | const args = process.argv.slice(2) 55 | 56 | if (args.length !== 2) { 57 | console.error('Usage: tsx copy.ts ') 58 | process.exit(1) 59 | } 60 | 61 | const [src, dest] = args 62 | await copyDirectory(src, dest) 63 | } 64 | 65 | await main() 66 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@astrobook/ui", 3 | "type": "module", 4 | "version": "0.8.1", 5 | "description": "", 6 | "author": "ocavue ", 7 | "license": "MIT", 8 | "funding": "https://github.com/sponsors/ocavue", 9 | "homepage": "https://github.com/ocavue/astrobook#readme", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/ocavue/astrobook.git" 13 | }, 14 | "bugs": "https://github.com/ocavue/astrobook/issues", 15 | "keywords": [], 16 | "sideEffects": false, 17 | "main": "./dist/index.ts", 18 | "module": "./dist/index.ts", 19 | "types": "./dist/index.ts", 20 | "exports": { 21 | ".": { 22 | "default": "./dist/index.ts" 23 | }, 24 | "./components/*": { 25 | "default": "./dist/components/*" 26 | } 27 | }, 28 | "typesVersions": { 29 | "*": { 30 | ".": [ 31 | "./dist/index.ts" 32 | ], 33 | "./components/*": [ 34 | "./dist/components/*" 35 | ] 36 | } 37 | }, 38 | "files": [ 39 | "dist" 40 | ], 41 | "scripts": { 42 | "build": "tsx copy.ts src temp && unocss --write-transformed && prettier -w temp/style.css && tsx copy.ts temp dist" 43 | }, 44 | "dependencies": { 45 | "@astrobook/core": "workspace:*", 46 | "@astrobook/types": "workspace:*", 47 | "astro-theme-toggle": "^0.6.0", 48 | "just-group-by": "^2.2.0" 49 | }, 50 | "peerDependencies": { 51 | "astro": ">=4.0.0" 52 | }, 53 | "peerDependenciesMeta": { 54 | "astro": { 55 | "optional": true 56 | } 57 | }, 58 | "devDependencies": { 59 | "@iconify-json/lucide": "^1.2.45", 60 | "@iconify-json/mdi": "^1.2.3", 61 | "@types/node": "^20.17.5", 62 | "@unocss/cli": "^66.1.2", 63 | "astro": "^5.8.1", 64 | "prettier": "^3.5.3", 65 | "tsx": "^4.19.4", 66 | "typescript": "^5.8.3", 67 | "unocss": "^66.1.2", 68 | "vite": "^6.3.5", 69 | "vitest": "^3.1.4" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/ui/src/components/app.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from './layout.astro' 3 | --- 4 | 5 | 6 |
9 |

Astrobook

10 | 39 |
40 |
41 | -------------------------------------------------------------------------------- /packages/ui/src/components/layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '../style.css' 3 | import './layout.css' 4 | import Sidebar from './sidebar.astro' 5 | import { ThemeScript } from 'astro-theme-toggle' 6 | import CustomHead from 'virtual:astrobook/components/head.mjs' 7 | import config from 'virtual:astrobook/global-config.mjs' 8 | 9 | // Important that this is the last import so it can override built-in styles. 10 | import 'virtual:astrobook/user-css.mjs' 11 | 12 | interface Props { 13 | story: string | undefined 14 | hasSidebar: boolean 15 | } 16 | 17 | const hasSidebar = Astro.props.hasSidebar ?? true 18 | --- 19 | 20 | 21 | 22 | 23 | 24 | 25 | {config.title} 26 | 27 | 28 | 29 | 30 | 33 | { 34 | hasSidebar && ( 35 |
36 | 37 |
38 | ) 39 | } 40 |
41 | 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /packages/ui/src/components/layout.css: -------------------------------------------------------------------------------- 1 | html { 2 | /* Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3) */ 3 | font-family: 4 | system-ui, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 5 | 'Apple Color Emoji', 'Segoe UI Emoji'; 6 | 7 | /* Correct the line height in all browsers. */ 8 | line-height: 1.15; 9 | 10 | /* Prevent adjustments of font size after orientation changes in iOS. */ 11 | -webkit-text-size-adjust: 100%; 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui/src/components/sidebar-button-fullscreen.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '@astrobook/types' 3 | import config from 'virtual:astrobook/global-config.mjs' 4 | 5 | import { getPathWithBase } from '@astrobook/core/client' 6 | import SidebarButton from './sidebar-button.astro' 7 | 8 | interface Props { 9 | story?: string 10 | } 11 | --- 12 | 13 | { 14 | Astro.props.story ? ( 15 | 19 | 20 | 21 | ) : null 22 | } 23 | -------------------------------------------------------------------------------- /packages/ui/src/components/sidebar-button-theme.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '@astrobook/types' 3 | import SidebarButton from './sidebar-button.astro' 4 | --- 5 | 6 | 7 | 9 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /packages/ui/src/components/sidebar-button.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | id?: string 4 | href?: string 5 | title: string 6 | } 7 | 8 | const Element = Astro.props.href ? 'a' : 'button' 9 | --- 10 | 11 | 21 | {Astro.props.title} 22 | 23 | 24 | -------------------------------------------------------------------------------- /packages/ui/src/components/sidebar-component-list.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '@astrobook/types' 3 | 4 | import { getPathWithBase } from '@astrobook/core/client' 5 | import groupBy from 'just-group-by' 6 | import config from 'virtual:astrobook/global-config.mjs' 7 | import modules from 'virtual:astrobook/story-modules.mjs' 8 | 9 | interface Props { 10 | story?: string 11 | } 12 | 13 | // Group modules by directory 14 | const groupedModules = groupBy(modules, (module) => module.directory) 15 | --- 16 | 17 |
21 | { 22 | Object.entries(groupedModules).map(([directory, directoryModules]) => ( 23 |
24 |
25 | {directory} 26 |
27 | {directoryModules.map((module) => ( 28 | 29 | 40 | 41 | {module.name} 42 | 43 | {module.stories.map((story) => ( 44 | 56 | 57 | {story.name} 58 | 59 | ))} 60 | 61 | ))} 62 |
63 | )) 64 | } 65 |
66 | 67 | 125 | -------------------------------------------------------------------------------- /packages/ui/src/components/sidebar-title.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '@astrobook/types' 3 | 4 | import { getPathWithBase } from '@astrobook/core/client' 5 | import config from 'virtual:astrobook/global-config.mjs' 6 | --- 7 | 8 | 15 | {config.title} 16 | 17 | -------------------------------------------------------------------------------- /packages/ui/src/components/sidebar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import SidebarButtonFullscreen from './sidebar-button-fullscreen.astro' 3 | import SidebarButtonTheme from './sidebar-button-theme.astro' 4 | import SidebarComponentList from './sidebar-component-list.astro' 5 | import SidebarTitle from './sidebar-title.astro' 6 | 7 | interface Props { 8 | story?: string 9 | } 10 | --- 11 | 12 | 23 | -------------------------------------------------------------------------------- /packages/ui/src/components/theme-message.ts: -------------------------------------------------------------------------------- 1 | import { setTheme } from 'astro-theme-toggle' 2 | 3 | if (typeof window !== 'undefined') { 4 | // Add message event listener to toggle the theme 5 | window.addEventListener('message', (event) => { 6 | try { 7 | const { type, theme } = event.data as { type: string; theme: string } 8 | if ( 9 | type === 'astrobook:set-theme' && 10 | (theme === 'dark' || theme === 'light') 11 | ) { 12 | setTheme(theme) 13 | } 14 | } catch { 15 | // ignore 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /packages/ui/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.astro' 4 | -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /packages/ui/src/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | src/style.css is just a placeholder file so that the import works. 3 | The actual styles will be placed in dist/style.css 4 | */ 5 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "composite": true 5 | }, 6 | "include": ["./*.js", "./*.ts", "./src/", "./dist/"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/uno.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | transformerCompileClass, 4 | presetIcons, 5 | presetWind3, 6 | } from 'unocss' 7 | 8 | const config: unknown = defineConfig({ 9 | cli: { 10 | entry: { 11 | patterns: ['temp/components/**/*.astro'], 12 | outFile: 'temp/style.css', 13 | }, 14 | }, 15 | presets: [ 16 | presetWind3({ 17 | variablePrefix: 'astrobook-', 18 | preflight: true, 19 | }), 20 | presetIcons(), 21 | ], 22 | transformers: [ 23 | transformerCompileClass({ 24 | classPrefix: 'astrobook-', 25 | }), 26 | ], 27 | shortcuts: { 28 | 'astrobook-focus-ring': 29 | 'outline-gray-900 dark:outline-gray-100 outline-0 focus-visible:outline-2 outline-offset-2 focus-visible:z-20', 30 | }, 31 | }) 32 | 33 | export default config 34 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | import { EXAMPLE_URLS } from './tests/example-urls' 4 | 5 | /** 6 | * Read environment variables from file. 7 | * https://github.com/motdotla/dotenv 8 | */ 9 | // import dotenv from 'dotenv'; 10 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 11 | 12 | /** 13 | * See https://playwright.dev/docs/test-configuration. 14 | */ 15 | export default defineConfig({ 16 | testDir: './tests', 17 | /* Run tests in files in parallel */ 18 | fullyParallel: true, 19 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 20 | forbidOnly: !!process.env.CI, 21 | /* Retry on CI only */ 22 | retries: process.env.CI ? 2 : 0, 23 | /* Opt out of parallel tests on CI. */ 24 | workers: process.env.CI ? 1 : undefined, 25 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 26 | reporter: 'html', 27 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 28 | use: { 29 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 30 | trace: 'on-first-retry', 31 | }, 32 | 33 | /* Configure projects for major browsers */ 34 | projects: [ 35 | { 36 | name: 'chromium', 37 | use: { ...devices['Desktop Chrome'] }, 38 | }, 39 | 40 | { 41 | name: 'firefox', 42 | use: { ...devices['Desktop Firefox'] }, 43 | }, 44 | 45 | { 46 | name: 'webkit', 47 | use: { ...devices['Desktop Safari'] }, 48 | }, 49 | 50 | /* Test against mobile viewports. */ 51 | // { 52 | // name: 'Mobile Chrome', 53 | // use: { ...devices['Pixel 5'] }, 54 | // }, 55 | // { 56 | // name: 'Mobile Safari', 57 | // use: { ...devices['iPhone 12'] }, 58 | // }, 59 | 60 | /* Test against branded browsers. */ 61 | // { 62 | // name: 'Microsoft Edge', 63 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 64 | // }, 65 | // { 66 | // name: 'Google Chrome', 67 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 68 | // }, 69 | ], 70 | 71 | /* Run your local dev server before starting the tests */ 72 | webServer: Object.entries(EXAMPLE_URLS).map(([name, url]) => ({ 73 | command: 74 | process.env.ASTROBOOK_TEST_MODE === 'preview' 75 | ? `pnpm run --filter ${name} preview` 76 | : `pnpm run --filter ${name} dev`, 77 | url: url, 78 | reuseExistingServer: !process.env.CI, 79 | })), 80 | }) 81 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - examples/* 4 | - packages/* 5 | -------------------------------------------------------------------------------- /tests/example-urls.js: -------------------------------------------------------------------------------- 1 | /** A map of examples' names to their corresponding ports */ 2 | export const EXAMPLE_URLS = /** @type {const} */ ({ 3 | 'example-playground': 'http://localhost:4321', 4 | 'example-mixed': 'http://localhost:4302/base', 5 | 'example-unocss': 'http://localhost:4303', 6 | 'example-tailwindcss': 'http://localhost:4304', 7 | }) 8 | -------------------------------------------------------------------------------- /tests/mixed/index.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | import { EXAMPLE_URLS } from '../example-urls' 4 | 5 | const BASE_URL = EXAMPLE_URLS['example-mixed'] 6 | 7 | test('mixed example', async ({ page }) => { 8 | await test.step('Check the home page', async () => { 9 | await page.goto(BASE_URL) 10 | await expect(page).toHaveTitle('Welcome to Astro.') 11 | }) 12 | 13 | await test.step('Open the astrobook', async () => { 14 | const button = page.locator('a', { hasText: 'Go to Astrobook' }) 15 | await button.click() 16 | 17 | await expect(page).toHaveURL(`${BASE_URL}/docs/components`) 18 | await expect(page).toHaveTitle('Astrobook') 19 | await expect(page.locator('h1')).toHaveText('Astrobook') 20 | }) 21 | 22 | await test.step('Select the story', async () => { 23 | const button = page.locator('a', { hasText: 'LargeStep' }) 24 | await expect(button).toBeVisible() 25 | await button.click() 26 | 27 | await page.waitForURL( 28 | `${BASE_URL}/docs/components/dashboard/preact-counter/large-step`, 29 | ) 30 | }) 31 | 32 | const counterNumber = page 33 | .locator('div.counter', { hasText: '+' }) 34 | .locator('pre') 35 | 36 | await test.step('Interact with the story', async () => { 37 | await expect(counterNumber).toHaveText('0') 38 | 39 | const button = page.locator('button', { hasText: '+' }) 40 | await button.click() 41 | 42 | await expect(counterNumber).toHaveText('5') 43 | }) 44 | 45 | await test.step('Go to full screen', async () => { 46 | await page.goto( 47 | `${BASE_URL}/docs/components/stories/preact-counter/large-step`, 48 | ) 49 | }) 50 | 51 | await test.step('Interact with the story', async () => { 52 | await expect(counterNumber).toHaveText('0') 53 | 54 | const button = page.locator('button', { hasText: '+' }) 55 | await button.click() 56 | 57 | await expect(counterNumber).toHaveText('5') 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /tests/playground/counter.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test' 2 | 3 | import { EXAMPLE_URLS } from '../example-urls' 4 | 5 | const BASE_URL = EXAMPLE_URLS['example-playground'] 6 | 7 | for (const dir of ['dashboard', 'stories']) { 8 | for (const framework of [ 9 | 'astro', 10 | 'lit', 11 | 'preact', 12 | 'react', 13 | 'solid', 14 | 'svelte', 15 | 'vue', 16 | ]) { 17 | for (const story of ['default', 'large-step']) { 18 | const url = `${BASE_URL}/${dir}/${framework}/${framework}-counter/${story}` 19 | test(url, async ({ page }) => { 20 | await page.goto(url) 21 | 22 | const counter = page.locator('.counter') 23 | await expect(counter).toBeVisible() 24 | 25 | const counterPre = counter.locator('pre') 26 | const counterAdd = counter.locator('button', { hasText: '+' }) 27 | const counterSub = counter.locator('button', { hasText: '-' }) 28 | 29 | await expect(counterPre).toHaveText('0') 30 | 31 | // Wait for the counter button to be clickable 32 | await expect(async () => { 33 | await counterAdd.click() 34 | await expect(counterPre).not.toHaveText('0') 35 | }).toPass() 36 | 37 | // Reset the counter 38 | await expect(async () => { 39 | await counterSub.click() 40 | await expect(counterPre).toHaveText('0') 41 | }).toPass() 42 | 43 | await expect(counterPre).toHaveText('0') 44 | 45 | await counterAdd.click() 46 | await expect(counterPre).toHaveText(story === 'default' ? '1' : '5') 47 | 48 | await counterSub.click() 49 | await expect(counterPre).toHaveText('0') 50 | }) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/playground/home.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | import { EXAMPLE_URLS } from '../example-urls' 4 | 5 | const BASE_URL = EXAMPLE_URLS['example-playground'] 6 | 7 | test('home page', async ({ page }) => { 8 | await page.goto(BASE_URL) 9 | await expect(page).toHaveTitle(/Astrobook/) 10 | }) 11 | -------------------------------------------------------------------------------- /tests/tailwindcss/index.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | import { EXAMPLE_URLS } from '../example-urls' 4 | 5 | const BASE_URL = EXAMPLE_URLS['example-tailwindcss'] 6 | 7 | test('tailwindcss example', async ({ page }) => { 8 | await test.step('Select the story', async () => { 9 | await page.goto(BASE_URL) 10 | const button = page.locator('a', { hasText: 'LargeStep' }) 11 | await expect(button).toBeVisible() 12 | await button.click() 13 | 14 | await page.waitForURL( 15 | `${BASE_URL}/dashboard/src/stories/preact-counter/large-step`, 16 | ) 17 | }) 18 | 19 | await test.step('Check the style', async () => { 20 | const button = page.locator('button', { hasText: '+' }) 21 | await expect(button).toHaveClass(/size-\[100px]/) 22 | await expect(button).toHaveCSS('width', '100px') 23 | await expect(button).toHaveCSS('height', '100px') 24 | const box = await button.boundingBox() 25 | expect(box).not.toBeNull() 26 | expect(Math.round(box?.width ?? 0)).toBe(100) 27 | expect(Math.round(box?.height ?? 0)).toBe(100) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tests/unocss/index.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | import { EXAMPLE_URLS } from '../example-urls' 4 | 5 | const BASE_URL = EXAMPLE_URLS['example-unocss'] 6 | 7 | test('unocss example', async ({ page }) => { 8 | await test.step('Select the story', async () => { 9 | await page.goto(BASE_URL) 10 | const button = page.locator('a', { hasText: 'LargeStep' }) 11 | await expect(button).toBeVisible() 12 | await button.click() 13 | 14 | await page.waitForURL( 15 | `${BASE_URL}/dashboard/src/stories/preact-counter/large-step`, 16 | ) 17 | }) 18 | 19 | await test.step('Check the style', async () => { 20 | const button = page.locator('button', { hasText: '+' }) 21 | await expect(button).toHaveClass(/size-\[100px]/) 22 | await expect(button).toHaveCSS('width', '100px') 23 | await expect(button).toHaveCSS('height', '100px') 24 | const box = await button.boundingBox() 25 | expect(box).not.toBeNull() 26 | expect(Math.round(box?.width ?? 0)).toBe(100) 27 | expect(Math.round(box?.height ?? 0)).toBe(100) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "esnext", 5 | "lib": ["esnext"], 6 | "jsx": "preserve", 7 | "declaration": true, 8 | "emitDeclarationOnly": true, 9 | "moduleResolution": "Bundler", 10 | "esModuleInterop": true, 11 | "outDir": "${configDir}/node_modules/.cache/tsc", 12 | "strict": true, 13 | "allowJs": true, 14 | "composite": true, 15 | "strictNullChecks": true, 16 | "verbatimModuleSyntax": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "resolveJsonModule": true, 19 | "skipLibCheck": true, 20 | "skipDefaultLibCheck": true 21 | }, 22 | "include": ["./*.js", "./*.ts", "./tests", "tests/example-urls.js"], 23 | "files": [], 24 | "references": [ 25 | { 26 | "path": "./examples/playground" 27 | }, 28 | { 29 | "path": "./examples/unocss" 30 | }, 31 | { 32 | "path": "./examples/mixed" 33 | }, 34 | { 35 | "path": "./packages/core" 36 | }, 37 | { 38 | "path": "./packages/types" 39 | }, 40 | { 41 | "path": "./packages/ui" 42 | }, 43 | { 44 | "path": "./packages/astrobook" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["tsconfig.json"], 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/"], 8 | "outputLogs": "new-only" 9 | }, 10 | "dev": { 11 | "dependsOn": ["^build"], 12 | "persistent": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | export default ['packages/*'] 2 | --------------------------------------------------------------------------------