Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,37 @@ import { chmod, readFile, rename, stat, unlink, writeFile } from 'fs/promises';
import esbuild from 'esbuild';
import { sentryEsbuildPlugin } from '@sentry/esbuild-plugin';

const plugins = [];
// jsonc-parser ships a UMD entry (its `main`) whose body does runtime
// `require("./impl/format")` etc. esbuild can't follow those dynamic sibling
// requires when bundling, so the built `dist/craft` fails at startup with
// "Cannot find module './impl/format'". Redirect the package to its ESM entry
// (`module`), which uses static imports esbuild can bundle. Pulled in
// transitively via @vercel/client → @vercel/microfrontends.
const jsoncParserEsmPlugin = {
name: 'jsonc-parser-esm',
setup(build) {
build.onResolve({ filter: /^jsonc-parser$/ }, async args => {
// Avoid recursing into our own resolve call below.
if (args.pluginData?.resolved) {
return;
}
const result = await build.resolve('jsonc-parser', {
importer: args.importer,
kind: args.kind,
resolveDir: args.resolveDir,
pluginData: { resolved: true },
});
if (result.errors.length > 0) {
return result;
}
return {
path: result.path.replace(/([\\/])lib[\\/]umd[\\/]/, '$1lib/esm/'),
};
});
},
};

const plugins = [jsoncParserEsmPlugin];

// Only add Sentry plugin if auth token is available (production builds on master)
if (process.env.SENTRY_AUTH_TOKEN) {
Expand Down
1 change: 1 addition & 0 deletions docs/src/content/docs/targets/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Targets define where Craft publishes your release artifacts. Configure them in `
| [GCS](./gcs/) | Upload to Google Cloud Storage |
| [GitHub Pages](./gh-pages/) | Deploy static sites |
| [Cloudflare](./cloudflare/) | Deploy static sites or Workers to Cloudflare |
| [Vercel](./vercel/) | Deploy a prebuilt static site to Vercel |
| [CocoaPods](./cocoapods/) | Publish iOS/macOS pods |
| [Ruby Gems](./gem/) | Publish Ruby gems |
| [Maven](./maven/) | Publish to Maven Central |
Expand Down
60 changes: 60 additions & 0 deletions docs/src/content/docs/targets/vercel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: Vercel
description: Deploy a prebuilt static site to Vercel
---

Deploys a release artifact to [Vercel](https://vercel.com/) as a production deployment.

The target extracts a ZIP artifact and deploys it via the Vercel deploy API (using [`@vercel/client`](https://www.npmjs.com/package/@vercel/client)) to promote it to production. It does not use or require the `vercel` CLI.

## Configuration

| Option | Description |
|--------|-------------|
| `prebuilt` | Whether the artifact contains a prebuilt `.vercel/output` (the result of `vercel build`). When `true` (default), the artifact's prebuilt `.vercel/output` is uploaded and the remote build step is skipped. Set to `false` to have Vercel build from source. |
| `workingDir` | Subdirectory within the extracted artifact to deploy from. |

## Environment Variables

| Name | Required | Description |
|------|----------|-------------|
| `VERCEL_TOKEN` | Yes | Vercel access token. Passed to the deploy API, never on the command line. |
| `VERCEL_ORG_ID` | No | Vercel organization/team ID. An identifier, not a secret. Forwarded to the deploy API as the team ID so the deployment links non-interactively. |
| `VERCEL_PROJECT_ID` | No | Vercel project ID. An identifier, not a secret. Forwarded to the deploy API as the project identifier so the deployment links to the intended project non-interactively. |

:::note
For non-interactive CI deployments, set both `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` so the deploy API knows which project to deploy to.
:::

## Default Behavior

By default, this target:

1. Looks for a single artifact matching `vercel.zip` (or `*-vercel.zip`). Override with `includeNames`.
2. Extracts its contents (preserving the archive layout, e.g. a top-level `.vercel/output`).
3. Deploys to production via the Vercel deploy API.

The version being released is attached to the deployment as metadata (`release=<version>`) for traceability.

## Example

```yaml
targets:
- name: vercel
# prebuilt defaults to true: the docs site is built in CI and this
# target only promotes the prebuilt output to production.
```

Deploying from a subdirectory of the artifact:

```yaml
targets:
- name: vercel
workingDir: docs
```

## Workflow

1. Build the site in CI (e.g. `vercel build`) and create a `vercel.zip` artifact containing the prebuilt `.vercel/output` (or the source when `prebuilt: false`).
2. Configure the target in `.craft.yml`.
3. Set `VERCEL_TOKEN` in your environment, plus `VERCEL_ORG_ID` and `VERCEL_PROJECT_ID` so the deploy targets the right project.
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"pnpm": "10.27.0"
},
"dependencies": {
"@vercel/client": "^18.2.5",
"fastest-levenshtein": "^1.0.16",
"ignore": "^7.0.5",
"marked": "^17.0.1",
Expand All @@ -115,7 +116,10 @@
"form-data@>=4": "^4.0.6",
"form-data@<3": "^2.5.6",
"vite": "^7.3.5",
"@babel/core": "^7.29.6"
"@babel/core": "^7.29.6",
"js-yaml": "^4.3.0",
"path-to-regexp@<6.3.0": "^6.3.0",
"tar-fs@<1.16.4": "1.16.6"
}
}
}
Loading
Loading