diff --git a/.github/actions/deploy-docs/action.yml b/.github/actions/deploy-docs/action.yml new file mode 100644 index 0000000000..e03892e6b7 --- /dev/null +++ b/.github/actions/deploy-docs/action.yml @@ -0,0 +1,87 @@ +name: 'Build and deploy docs' +description: >- + Build the VitePress docs site (which bundles install.sh / install.ps1 into + its public assets) and deploy it to a void.app project. Run on a Linux + runner after checkout. See rfcs/deploy-docs-on-release.md for which + workflow deploys to which project. + +inputs: + void-project: + description: 'void.app project to deploy to (e.g. viteplus, viteplus-main).' + required: true + void-token: + description: 'void.app deploy token (pass secrets.VOID_TOKEN).' + required: true + cache-ref: + description: >- + Ref-scoped segment of the Vite Task cache key: main, or pr- for + PR previews. Non-main refs fall back to the main cache on restore. + required: false + default: 'main' + cache-sha: + description: 'Commit sha that scopes the primary cache key.' + required: false + default: ${{ github.sha }} + site-origin: + description: >- + Origin of this deploy when it is not production viteplus.dev + (e.g. https://main.viteplus.dev). When set, the docs build rewrites the + https://vite.plus installer URLs to this origin's install scripts. + Leave empty for production. + required: false + default: '' + +runs: + using: 'composite' + steps: + - uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # main + with: + cache: true + working-directory: docs + cache-dependency-path: docs/pnpm-lock.yaml + + - name: Compute Vite Task cache keys + id: cache-keys + shell: bash + env: + CACHE_REF: ${{ inputs.cache-ref }} + CACHE_SHA: ${{ inputs.cache-sha }} + run: | + prefix="vite-task-docs-${RUNNER_OS}-${RUNNER_ARCH}" + { + echo "key=${prefix}-${CACHE_REF}-${CACHE_SHA}" + echo 'restore-keys<> "$GITHUB_OUTPUT" + + - name: Restore docs Vite Task cache + id: vite-task-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: docs/node_modules/.vite/task-cache + key: ${{ steps.cache-keys.outputs.key }} + # Prefer this ref's newest cache; Vite Task fingerprints decide reuse. + restore-keys: ${{ steps.cache-keys.outputs.restore-keys }} + + - run: vp run build + shell: bash + working-directory: docs + env: + DOCS_SITE_ORIGIN: ${{ inputs.site-origin }} + + - name: Save docs Vite Task cache + if: success() && steps.vite-task-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: docs/node_modules/.vite/task-cache + key: ${{ steps.vite-task-cache.outputs.cache-primary-key }} + + - run: vpx void deploy --dir docs/.vitepress/dist + shell: bash + env: + VOID_PROJECT: ${{ inputs.void-project }} + VOID_TOKEN: ${{ inputs.void-token }} diff --git a/.github/workflows/deploy-docs-main.yml b/.github/workflows/deploy-docs-main.yml new file mode 100644 index 0000000000..9b7a309d28 --- /dev/null +++ b/.github/workflows/deploy-docs-main.yml @@ -0,0 +1,40 @@ +name: Deploy Docs Main Preview + +permissions: {} + +# Deploys the docs at the head of main to main.viteplus.dev (the viteplus-main +# void.app project) so developers can preview unreleased docs. Production +# viteplus.dev deploys from release.yml instead. +# See rfcs/deploy-docs-on-release.md. +on: + push: + branches: [main] + paths: + - 'docs/**' + - 'packages/cli/install.sh' + - 'packages/cli/install.ps1' + - '.github/workflows/deploy-docs-main.yml' + - '.github/actions/deploy-docs/**' + +concurrency: + group: deploy-docs-main + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + deploy: + if: github.repository == 'voidzero-dev/vite-plus' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 + + - uses: ./.github/actions/deploy-docs + with: + void-project: viteplus-main + void-token: ${{ secrets.VOID_TOKEN }} + site-origin: https://main.viteplus.dev diff --git a/.github/workflows/deploy-docs-preview.yml b/.github/workflows/deploy-docs-preview.yml index 2267f3192b..2579e08698 100644 --- a/.github/workflows/deploy-docs-preview.yml +++ b/.github/workflows/deploy-docs-preview.yml @@ -9,6 +9,7 @@ on: - 'packages/cli/install.sh' - 'packages/cli/install.ps1' - '.github/workflows/deploy-docs-preview.yml' + - '.github/actions/deploy-docs/**' concurrency: group: deploy-docs-preview-${{ github.event.pull_request.number }} @@ -28,41 +29,17 @@ jobs: contents: read pull-requests: write env: - VOID_PROJECT: viteplus-staging PREVIEW_URL: https://viteplus-staging.void.app/ steps: - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 - - uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # main + - uses: ./.github/actions/deploy-docs with: - cache: true - working-directory: docs - cache-dependency-path: docs/pnpm-lock.yaml - - - name: Restore docs Vite Task cache - id: vite-task-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: docs/node_modules/.vite/task-cache - key: vite-task-docs-${{ runner.os }}-${{ runner.arch }}-pr-${{ github.event.pull_request.number }}-${{ github.event.pull_request.head.sha }} - # Prefer this PR's newest cache, then fall back to main for new PRs. - restore-keys: | - vite-task-docs-${{ runner.os }}-${{ runner.arch }}-pr-${{ github.event.pull_request.number }}- - vite-task-docs-${{ runner.os }}-${{ runner.arch }}-main- - - - run: vp run build - working-directory: docs - - - name: Save docs Vite Task cache - if: success() && steps.vite-task-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: docs/node_modules/.vite/task-cache - key: ${{ steps.vite-task-cache.outputs.cache-primary-key }} - - - run: vpx void deploy --dir docs/.vitepress/dist - env: - VOID_TOKEN: ${{ secrets.VOID_TOKEN }} + void-project: viteplus-staging + void-token: ${{ secrets.VOID_TOKEN }} + cache-ref: pr-${{ github.event.pull_request.number }} + cache-sha: ${{ github.event.pull_request.head.sha }} + site-origin: https://viteplus-staging.void.app - name: Comment on PR uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 6d365252cf..c205ab9e60 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -2,16 +2,18 @@ name: Deploy Docs permissions: {} +# Deploys production viteplus.dev, which also serves install.sh / install.ps1 +# behind https://vite.plus. Runs from release.yml after a stable release is +# published, or manually here for urgent updates. Pushes to main deploy to +# main.viteplus.dev via deploy-docs-main.yml instead. +# See rfcs/deploy-docs-on-release.md. on: - push: - branches: [main] - paths: - - 'docs/**' - - 'packages/cli/install.sh' - - 'packages/cli/install.ps1' - - '.github/workflows/deploy-docs.yml' workflow_dispatch: +# Shared with the deploy-docs job in release.yml so production deploys +# serialize across both entry paths. GitHub keeps only the newest pending run +# in the group (a queued deploy replaces a pending one), so production +# converges to the newest queued content; the running deploy always completes. concurrency: group: deploy-docs cancel-in-progress: false @@ -26,37 +28,10 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - env: - VOID_PROJECT: viteplus steps: - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 - - uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # main + - uses: ./.github/actions/deploy-docs with: - cache: true - working-directory: docs - cache-dependency-path: docs/pnpm-lock.yaml - - - name: Restore docs Vite Task cache - id: vite-task-cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: docs/node_modules/.vite/task-cache - key: vite-task-docs-${{ runner.os }}-${{ runner.arch }}-main-${{ github.sha }} - # Restore the latest main cache; Vite Task fingerprints decide reuse. - restore-keys: | - vite-task-docs-${{ runner.os }}-${{ runner.arch }}-main- - - - run: vp run build - working-directory: docs - - - name: Save docs Vite Task cache - if: success() && steps.vite-task-cache.outputs.cache-hit != 'true' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: docs/node_modules/.vite/task-cache - key: ${{ steps.vite-task-cache.outputs.cache-primary-key }} - - - run: vpx void deploy --dir docs/.vitepress/dist - env: - VOID_TOKEN: ${{ secrets.VOID_TOKEN }} + void-project: viteplus + void-token: ${{ secrets.VOID_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 62581f316c..207d308550 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -213,6 +213,34 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release edit "v${VERSION}" --draft=false --repo "${{ github.repository }}" + # Deploy viteplus.dev from the release commit after the npm packages and the + # GitHub release are published, so the site and the install scripts always + # match the released vp. Skipped for prereleases: production docs must keep + # describing the latest stable release. See rfcs/deploy-docs-on-release.md. + deploy-docs: + name: Deploy docs + runs-on: ubuntu-latest + needs: [check, Release] + if: >- + needs.check.outputs.version_changed == 'true' && + !contains(needs.check.outputs.version, '-') + # Shared with deploy-docs.yml so production deploys serialize across both + # entry paths. Only the newest pending run survives in the group; if this + # job's pending run is replaced by a manual dispatch, the canceled job + # holds back discord-notify and the deploy must be re-run. + concurrency: + group: deploy-docs + cancel-in-progress: false + permissions: + contents: read + steps: + - uses: taiki-e/checkout-action@7d1e50e93dc4fb3bba58f85018fadf77898aee8b # v1.4.2 + + - uses: ./.github/actions/deploy-docs + with: + void-project: viteplus + void-token: ${{ secrets.VOID_TOKEN }} + # Build and push the official toolchain Docker image to GHCR after the npm # release is published (the image installs vp from npm, so the version must # exist first). See docker/Dockerfile and docs/guide/docker.md. @@ -266,15 +294,22 @@ jobs: VP_VERSION=${{ env.VERSION }} provenance: false - # Announce the release on Discord last, after the Docker images are published, - # so the message can include the GHCR image. Runs after the npm release - # (Release) and the images (publish-docker). + # Announce the release on Discord last, after the Docker images and the docs + # are published, so the message never points at a missing image or a site + # that still shows the previous release. discord-notify: name: Notify Discord runs-on: ubuntu-latest # publish-docker already needs Release, so depending on it orders this last. - needs: [check, publish-docker] - if: needs.check.outputs.version_changed == 'true' + # deploy-docs is skipped for prereleases; gate on its result explicitly so + # prerelease announcements still go out, while a failed docs deploy on a + # stable release holds the announcement back. + needs: [check, publish-docker, deploy-docs] + if: >- + !cancelled() && + needs.check.outputs.version_changed == 'true' && + needs.publish-docker.result == 'success' && + (needs.deploy-docs.result == 'success' || needs.deploy-docs.result == 'skipped') env: VERSION: ${{ needs.check.outputs.version }} IMAGE: ghcr.io/voidzero-dev/vite-plus diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 413fc85b26..293673f43f 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -7,6 +7,26 @@ import { groupIconMdPlugin, groupIconVitePlugin } from 'vitepress-plugin-group-i import llmstxt from 'vitepress-plugin-llms'; import { withMermaid } from 'vitepress-plugin-mermaid'; +// Non-production deploys (the main preview, PR staging) serve their own +// copies of the install scripts and llms dumps, so the https://vite.plus +// installer shortcuts and absolute site URLs must point at the deploy's +// origin instead of production. The deploy workflows set DOCS_SITE_ORIGIN via +// the deploy-docs composite action; markdown content is rewritten through +// markdown-it below, and Vue components read the __DOCS_*__ define constants. +const siteOrigin = process.env.DOCS_SITE_ORIGIN; +const docsOrigin = siteOrigin || 'https://viteplus.dev'; +const installShUrl = siteOrigin ? `${siteOrigin}/install.sh` : 'https://vite.plus'; +const installPs1Url = siteOrigin ? `${siteOrigin}/install.ps1` : 'https://vite.plus/ps1'; + +function rewriteInstallUrls(text: string): string { + if (!siteOrigin) { + return text; + } + return text + .replaceAll('https://vite.plus/ps1', installPs1Url) + .replaceAll('https://vite.plus', installShUrl); +} + const taskRunnerGuideItems = [ { text: 'Run', @@ -113,6 +133,11 @@ export default extendConfig( ['meta', { name: 'twitter:site', content: '@voidzerodev' }], ], vite: { + define: { + __DOCS_ORIGIN__: JSON.stringify(docsOrigin), + __DOCS_INSTALL_SH_URL__: JSON.stringify(installShUrl), + __DOCS_INSTALL_PS1_URL__: JSON.stringify(installPs1Url), + }, optimizeDeps: { include: ['mermaid > @braintree/sanitize-url'], }, @@ -258,6 +283,31 @@ export default extendConfig( markdown: { config(md) { md.use(groupIconMdPlugin); + if (siteOrigin) { + md.core.ruler.push('rewrite-install-urls', (state) => { + const walk = (tokens: typeof state.tokens) => { + for (const token of tokens) { + if ( + token.type === 'fence' || + token.type === 'code_inline' || + token.type === 'text' + ) { + token.content = rewriteInstallUrls(token.content); + } + if (token.type === 'link_open') { + const href = token.attrGet('href'); + if (href) { + token.attrSet('href', rewriteInstallUrls(href)); + } + } + if (token.children) { + walk(token.children); + } + } + }; + walk(state.tokens); + }); + } }, }, }), diff --git a/docs/.vitepress/env.d.ts b/docs/.vitepress/env.d.ts index adcc6abf4e..23ce8f7581 100644 --- a/docs/.vitepress/env.d.ts +++ b/docs/.vitepress/env.d.ts @@ -1,3 +1,13 @@ +// Build-time constants injected via vite.define in config.mts. They point at +// the current deploy's origin (production, main preview, or PR staging). The +// dunder names follow the Vite convention for compile-time replaced globals. +// oxlint-disable-next-line no-underscore-dangle +declare const __DOCS_ORIGIN__: string; +// oxlint-disable-next-line no-underscore-dangle +declare const __DOCS_INSTALL_SH_URL__: string; +// oxlint-disable-next-line no-underscore-dangle +declare const __DOCS_INSTALL_PS1_URL__: string; + // Vue SFC module declaration declare module '*.vue' { import type { DefineComponent } from 'vue'; diff --git a/docs/.vitepress/theme/components/CopyPrompt.vue b/docs/.vitepress/theme/components/CopyPrompt.vue index 1520b16379..1b9de196d7 100644 --- a/docs/.vitepress/theme/components/CopyPrompt.vue +++ b/docs/.vitepress/theme/components/CopyPrompt.vue @@ -7,11 +7,11 @@ import { computed, onBeforeUnmount, ref, useId } from 'vue'; // live llms-full.txt docs dump. const DEFAULT_PROMPT = `I want to use Vite+ in my project. Vite+ is the unified toolchain for the web behind the \`vp\` CLI — one tool combining Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task, plus runtime and package-manager management. -First, read https://viteplus.dev/llms-full.txt to learn Vite+'s commands and configuration. +First, read ${__DOCS_ORIGIN__}/llms-full.txt to learn Vite+'s commands and configuration. Install the \`vp\` CLI if it's not already on the system: -- macOS / Linux: curl -fsSL https://vite.plus | bash -- Windows (PowerShell): irm https://vite.plus/ps1 | iex +- macOS / Linux: curl -fsSL ${__DOCS_INSTALL_SH_URL__} | bash +- Windows (PowerShell): irm ${__DOCS_INSTALL_PS1_URL__} | iex Then open a new terminal and run \`vp help\`. To scaffold a new project run \`vp create\`; to move an existing Vite project onto Vite+ run \`vp migrate\`. @@ -19,17 +19,22 @@ Day-to-day commands: \`vp install\` (dependencies), \`vp dev\` (dev server), \`v Help me get set up and explain anything I should know.`; +// DEFAULT_PROMPT interpolates the __DOCS_*__ define constants, so it is not a +// static literal and cannot be a withDefaults() default (defineProps is +// hoisted out of setup). Resolve the fallback in promptText instead. const props = withDefaults( defineProps<{ prompt?: string; label?: string; }>(), { - prompt: DEFAULT_PROMPT, + prompt: '', label: 'View Prompt', }, ); +const promptText = computed(() => props.prompt || DEFAULT_PROMPT); + const titleId = useId(); const dialogEl = ref(null); const state = ref<'idle' | 'copied' | 'error'>('idle'); @@ -71,7 +76,7 @@ const blurPointerTarget = (event: MouseEvent) => { const copyPrompt = async (event: MouseEvent) => { blurPointerTarget(event); try { - await navigator.clipboard.writeText(props.prompt); + await navigator.clipboard.writeText(promptText.value); flash('copied'); } catch { flash('error'); @@ -132,7 +137,7 @@ onBeforeUnmount(() => {
{{ prompt }}
+ >{{ promptText }}