@decocms/start 3.0.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/deco-to-tanstack-migration/SKILL.md +1 -1
- package/.agents/skills/deco-to-tanstack-migration/references/worker-cloudflare.md +32 -135
- package/.agents/skills/deco-to-tanstack-migration/templates/package-json.md +2 -5
- package/.cursor/rules/migration-tooling-policy.mdc +32 -48
- package/CODEOWNERS +4 -12
- package/MIGRATION_TOOLING_PLAN.md +9 -13
- package/package.json +2 -3
- package/scripts/migrate/phase-scaffold.ts +9 -12
- package/scripts/migrate/phase-verify.ts +3 -5
- package/scripts/migrate/templates/package-json.ts +6 -9
- package/src/sdk/logger.test.ts +53 -0
- package/src/sdk/logger.ts +58 -3
- package/src/vite/plugin.js +32 -18
- package/src/vite/plugin.test.js +54 -0
- package/.github/workflows/deploy.yml +0 -141
- package/.github/workflows/preview.yml +0 -200
- package/.github/workflows/sync-secrets.yml +0 -171
- package/deploy/README.md +0 -121
- package/deploy/wrangler-template.jsonc +0 -46
- package/scripts/deploy/build-wrangler-config.mjs +0 -47
- package/scripts/deploy/jsonc.mjs +0 -76
- package/scripts/deploy/site-registry.mjs +0 -95
- package/scripts/deploy/wrangler-wrapper.mjs +0 -118
- package/scripts/migrate/templates/github-workflows.ts +0 -159
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
name: sync-secrets (central)
|
|
2
|
-
|
|
3
|
-
# Reusable workflow. Reconciles the per-storefront `SECRET_*` values stored in
|
|
4
|
-
# this repo's `<site_name>-secrets` GitHub Environment with the Cloudflare
|
|
5
|
-
# Worker's runtime secrets (with the `SECRET_` prefix stripped).
|
|
6
|
-
#
|
|
7
|
-
# Examples:
|
|
8
|
-
# SECRET_STRIPE_KEY in deco-start env -> STRIPE_KEY on the worker
|
|
9
|
-
#
|
|
10
|
-
# v3 architecture (D6.2 / S1): SECRET_* values live in deco-start environments
|
|
11
|
-
# named `<site_name>-secrets` (e.g. `baggagio-tanstack-secrets`). Site teams
|
|
12
|
-
# get per-environment edit/approve permissions via GitHub Environment
|
|
13
|
-
# protection rules, enforced by GitHub itself. CF credentials never leave
|
|
14
|
-
# decocms/deco-start, AND site secrets never leave decocms/deco-start either.
|
|
15
|
-
# The storefront repo holds zero credentials.
|
|
16
|
-
#
|
|
17
|
-
# Defaults to dry-run; the caller must pass `mode=apply` to actually write.
|
|
18
|
-
# Orphans on the worker (present on worker but not in the env as SECRET_*) are
|
|
19
|
-
# warned about, never deleted.
|
|
20
|
-
#
|
|
21
|
-
# Caller usage (in the storefront repo, `.github/workflows/sync-secrets.yml`):
|
|
22
|
-
#
|
|
23
|
-
# on:
|
|
24
|
-
# workflow_dispatch:
|
|
25
|
-
# inputs:
|
|
26
|
-
# mode:
|
|
27
|
-
# type: choice
|
|
28
|
-
# options: [dry-run, apply]
|
|
29
|
-
# default: dry-run
|
|
30
|
-
# permissions:
|
|
31
|
-
# contents: read
|
|
32
|
-
# jobs:
|
|
33
|
-
# trigger:
|
|
34
|
-
# runs-on: ubuntu-latest
|
|
35
|
-
# steps:
|
|
36
|
-
# - uses: actions/create-github-app-token@v1
|
|
37
|
-
# id: app-token
|
|
38
|
-
# with:
|
|
39
|
-
# app-id: ${{ secrets.DECOCMS_DEPLOYER_APP_ID }}
|
|
40
|
-
# private-key: ${{ secrets.DECOCMS_DEPLOYER_APP_PRIVATE_KEY }}
|
|
41
|
-
# owner: decocms
|
|
42
|
-
# repositories: deco-start
|
|
43
|
-
# - env:
|
|
44
|
-
# GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
45
|
-
# run: |
|
|
46
|
-
# gh workflow run sync-secrets.yml \
|
|
47
|
-
# --repo decocms/deco-start \
|
|
48
|
-
# --ref v3 \
|
|
49
|
-
# -f site_name=${GITHUB_REPOSITORY##*/} \
|
|
50
|
-
# -f mode=${{ inputs.mode }}
|
|
51
|
-
|
|
52
|
-
on:
|
|
53
|
-
workflow_dispatch:
|
|
54
|
-
inputs:
|
|
55
|
-
site_name:
|
|
56
|
-
description: "Storefront repo basename. Becomes the Cloudflare worker name AND the environment name (`<site_name>-secrets`)."
|
|
57
|
-
type: string
|
|
58
|
-
required: true
|
|
59
|
-
mode:
|
|
60
|
-
description: "dry-run = print diff only | apply = set secrets on worker"
|
|
61
|
-
type: string
|
|
62
|
-
required: false
|
|
63
|
-
default: "dry-run"
|
|
64
|
-
|
|
65
|
-
permissions:
|
|
66
|
-
contents: read
|
|
67
|
-
|
|
68
|
-
concurrency:
|
|
69
|
-
group: sync-secrets-${{ inputs.site_name }}
|
|
70
|
-
cancel-in-progress: false
|
|
71
|
-
|
|
72
|
-
jobs:
|
|
73
|
-
sync:
|
|
74
|
-
runs-on: ubuntu-latest
|
|
75
|
-
# Dynamic per-storefront environment. Site teams get edit/approve
|
|
76
|
-
# permissions on their own environment via GitHub Environment protection
|
|
77
|
-
# rules (set up in this repo's Settings -> Environments).
|
|
78
|
-
environment: ${{ inputs.site_name }}-secrets
|
|
79
|
-
steps:
|
|
80
|
-
- name: Checkout deco-start (template + scripts)
|
|
81
|
-
uses: actions/checkout@v4
|
|
82
|
-
|
|
83
|
-
- name: Generate wrangler.jsonc (so wrangler knows which worker to target)
|
|
84
|
-
env:
|
|
85
|
-
DECO_START_PATH: "."
|
|
86
|
-
WORKER_NAME: ${{ inputs.site_name }}
|
|
87
|
-
OUTPUT_PATH: "./wrangler.jsonc"
|
|
88
|
-
run: node scripts/deploy/build-wrangler-config.mjs
|
|
89
|
-
|
|
90
|
-
- uses: actions/setup-node@v4
|
|
91
|
-
with:
|
|
92
|
-
node-version: 22
|
|
93
|
-
|
|
94
|
-
- name: Install wrangler
|
|
95
|
-
run: npm install --no-save wrangler@4
|
|
96
|
-
|
|
97
|
-
- name: Plan and (optionally) apply
|
|
98
|
-
env:
|
|
99
|
-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
|
100
|
-
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
|
101
|
-
# ALL_SECRETS includes both repo secrets (CLOUDFLARE_*) AND
|
|
102
|
-
# environment secrets (SECRET_*) because we're bound to the env.
|
|
103
|
-
ALL_SECRETS: ${{ toJSON(secrets) }}
|
|
104
|
-
MODE: ${{ inputs.mode }}
|
|
105
|
-
run: |
|
|
106
|
-
set -euo pipefail
|
|
107
|
-
|
|
108
|
-
desired_json=$(printf '%s' "$ALL_SECRETS" | jq '
|
|
109
|
-
to_entries
|
|
110
|
-
| map(select(.key | startswith("SECRET_")))
|
|
111
|
-
| map({ key: (.key | sub("^SECRET_"; "")), value: .value })
|
|
112
|
-
| from_entries
|
|
113
|
-
')
|
|
114
|
-
|
|
115
|
-
desired_names=$(printf '%s' "$desired_json" | jq -r 'keys[]' | sort)
|
|
116
|
-
|
|
117
|
-
while IFS= read -r name; do
|
|
118
|
-
[ -z "$name" ] && continue
|
|
119
|
-
if ! [[ "$name" =~ ^[A-Z][A-Z0-9_]{0,63}$ ]]; then
|
|
120
|
-
echo "::error::Invalid worker secret name derived from SECRET_${name}: must match ^[A-Z][A-Z0-9_]{0,63}$"
|
|
121
|
-
exit 1
|
|
122
|
-
fi
|
|
123
|
-
done <<< "$desired_names"
|
|
124
|
-
|
|
125
|
-
existing=$(npx wrangler secret list --format=json | jq -r '.[].name' | sort)
|
|
126
|
-
|
|
127
|
-
to_set="$desired_names"
|
|
128
|
-
orphans=$(comm -23 <(printf '%s\n' "$existing") <(printf '%s\n' "$desired_names") || true)
|
|
129
|
-
|
|
130
|
-
{
|
|
131
|
-
echo "## Diff for ${{ inputs.site_name }}"
|
|
132
|
-
echo ""
|
|
133
|
-
echo "### Will set (from SECRET_* in \`${{ inputs.site_name }}-secrets\` environment):"
|
|
134
|
-
if [ -z "$to_set" ]; then
|
|
135
|
-
echo " (none -- environment has no SECRET_* values)"
|
|
136
|
-
else
|
|
137
|
-
echo "$to_set" | sed 's/^/ + /'
|
|
138
|
-
fi
|
|
139
|
-
echo ""
|
|
140
|
-
echo "### Orphans (on worker, not in env as SECRET_*):"
|
|
141
|
-
if [ -z "$orphans" ]; then
|
|
142
|
-
echo " (none)"
|
|
143
|
-
else
|
|
144
|
-
echo "$orphans" | sed 's/^/ ! /'
|
|
145
|
-
echo ""
|
|
146
|
-
echo " These will NOT be deleted automatically. To remove manually:"
|
|
147
|
-
echo "$orphans" | sed 's|^| npx wrangler secret delete "|; s|$|" --force|'
|
|
148
|
-
fi
|
|
149
|
-
} | tee -a "$GITHUB_STEP_SUMMARY"
|
|
150
|
-
|
|
151
|
-
if [ -n "$orphans" ]; then
|
|
152
|
-
echo "::warning::${orphans//$'\n'/, } exist on the worker but not in env as SECRET_*. Not deleting."
|
|
153
|
-
fi
|
|
154
|
-
|
|
155
|
-
if [ "$MODE" = "dry-run" ]; then
|
|
156
|
-
echo "::notice::dry-run mode -- no changes applied"
|
|
157
|
-
exit 0
|
|
158
|
-
fi
|
|
159
|
-
|
|
160
|
-
if [ -z "$to_set" ]; then
|
|
161
|
-
echo "Nothing to apply."
|
|
162
|
-
exit 0
|
|
163
|
-
fi
|
|
164
|
-
|
|
165
|
-
printf '%s' "$desired_json" | jq -r 'to_entries[] | "\(.key)\t\(.value)"' \
|
|
166
|
-
| while IFS=$'\t' read -r name value; do
|
|
167
|
-
echo "Setting $name..."
|
|
168
|
-
printf '%s' "$value" | npx wrangler secret put "$name"
|
|
169
|
-
done
|
|
170
|
-
|
|
171
|
-
echo "::notice::Applied $(echo "$to_set" | wc -l | tr -d ' ') secrets."
|
package/deploy/README.md
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
# `deploy/` — central wrangler template
|
|
2
|
-
|
|
3
|
-
This directory holds **`wrangler-template.jsonc`** — the canonical wrangler config
|
|
4
|
-
that every storefront on the platform inherits. It is consumed by the reusable
|
|
5
|
-
GitHub workflows under [`.github/workflows/`](../.github/workflows/)
|
|
6
|
-
(`deploy.yml`, `preview.yml`, `sync-secrets.yml`) and by the local
|
|
7
|
-
`deco-wrangler` CLI.
|
|
8
|
-
|
|
9
|
-
There is **no per-site registry**. Worker name is the storefront repo basename
|
|
10
|
-
by convention (`deco-sites/baggagio-tanstack` → worker `baggagio-tanstack`).
|
|
11
|
-
Anything that must vary deterministically per worker (like the Analytics Engine
|
|
12
|
-
dataset name) is encoded as a substitution token in the template — see
|
|
13
|
-
[Substitution tokens](#substitution-tokens) below.
|
|
14
|
-
|
|
15
|
-
## Trust model
|
|
16
|
-
|
|
17
|
-
The deploy is gated by the `decocms-deployer` **GitHub App** being installed on
|
|
18
|
-
the target storefront repo:
|
|
19
|
-
|
|
20
|
-
1. The storefront's caller workflow mints a short-lived App-installation token.
|
|
21
|
-
2. It calls `gh workflow run deploy.yml --repo decocms/deco-start -f site_owner=… -f site_name=…`.
|
|
22
|
-
3. The central deploy workflow runs **in this repo's context** and itself mints
|
|
23
|
-
another short-lived App-installation token to check out the storefront. If
|
|
24
|
-
the App isn't installed on `<site_owner>/<site_name>`, the mint fails and
|
|
25
|
-
the deploy never starts.
|
|
26
|
-
4. The central workflow then runs build + `wrangler deploy` using
|
|
27
|
-
`CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID` from this repo's plain
|
|
28
|
-
repo secrets.
|
|
29
|
-
|
|
30
|
-
Properties this gives:
|
|
31
|
-
|
|
32
|
-
- **CF credentials never leave decocms/deco-start.** The storefront repo holds
|
|
33
|
-
zero Cloudflare credentials — it only has the GitHub App credentials, which
|
|
34
|
-
can be used solely to trigger workflows on this repo.
|
|
35
|
-
- **Worker naming is convention-based and not customer-controlled.** A
|
|
36
|
-
customer with push access to their own storefront cannot rename the worker
|
|
37
|
-
their deploy lands on (the central workflow always uses
|
|
38
|
-
`inputs.site_name` as the worker name; modifying the caller stub to pass a
|
|
39
|
-
different `site_name` would also require the App to be installed on that
|
|
40
|
-
other repo).
|
|
41
|
-
- **Force-rollback is impossible for production.** The central deploy
|
|
42
|
-
workflow ignores any caller-supplied sha and always resolves the
|
|
43
|
-
storefront's current default-branch HEAD itself. The worst a compromised
|
|
44
|
-
storefront can do across tenants is trigger a no-op redeploy of another
|
|
45
|
-
storefront's current main.
|
|
46
|
-
|
|
47
|
-
`deploy/` and `scripts/deploy/` and the central workflow files are
|
|
48
|
-
CODEOWNERS-protected — only the platform team approves changes.
|
|
49
|
-
|
|
50
|
-
### Where Cloudflare credentials live
|
|
51
|
-
|
|
52
|
-
| Secret class | Lives in | How it reaches the worker |
|
|
53
|
-
|---|---|---|
|
|
54
|
-
| `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID` | this repo's **repo secrets** | central workflow runs in this repo's context, env-var resolves natively |
|
|
55
|
-
| `DECOCMS_DEPLOYER_APP_ID` / `DECOCMS_DEPLOYER_APP_PRIVATE_KEY` | this repo's repo secrets AND deco-sites org-level secrets | mints short-lived installation tokens for both directions of the dispatch flow |
|
|
56
|
-
| `SECRET_*` runtime secrets (per site) | this repo's `<site_name>-secrets` GitHub Environment | `sync-secrets.yml` binds to that environment, reads `SECRET_*` from `${{ secrets }}`, runs `wrangler secret put` |
|
|
57
|
-
|
|
58
|
-
To rotate Cloudflare credentials, edit them in this repo only. To rotate a
|
|
59
|
-
runtime secret for one storefront, edit the corresponding environment in this
|
|
60
|
-
repo only. No storefront PR needed for either.
|
|
61
|
-
|
|
62
|
-
## How `wrangler.jsonc` is generated
|
|
63
|
-
|
|
64
|
-
At deploy time, the central workflow runs
|
|
65
|
-
[`scripts/deploy/build-wrangler-config.mjs`](../scripts/deploy/build-wrangler-config.mjs),
|
|
66
|
-
which:
|
|
67
|
-
|
|
68
|
-
1. Loads `deploy/wrangler-template.jsonc`.
|
|
69
|
-
2. Substitutes `$WORKER_*` tokens (see below) using the worker name passed by
|
|
70
|
-
the central workflow (= storefront repo basename).
|
|
71
|
-
3. Writes the result to `./wrangler.jsonc` in the storefront checkout, with
|
|
72
|
-
`name` injected as the first key.
|
|
73
|
-
|
|
74
|
-
`account_id` is never written to JSON — wrangler reads it from
|
|
75
|
-
`CLOUDFLARE_ACCOUNT_ID` (env var in CI; `wrangler login` locally). This way a
|
|
76
|
-
typo cannot misroute a deploy to a different Cloudflare account.
|
|
77
|
-
|
|
78
|
-
### Substitution tokens
|
|
79
|
-
|
|
80
|
-
Any string in the template containing one of these literals is replaced at
|
|
81
|
-
build time:
|
|
82
|
-
|
|
83
|
-
| Token | Replacement | Example use |
|
|
84
|
-
|---|---|---|
|
|
85
|
-
| `$WORKER_NAME` | worker name verbatim | rare; mostly available for parity |
|
|
86
|
-
| `$WORKER_UNDERSCORE` | worker name with `-` → `_` | `analytics_engine_datasets[].dataset` (must be a valid Postgres-style identifier) |
|
|
87
|
-
|
|
88
|
-
To add a new derived field, add the token wherever it makes sense in
|
|
89
|
-
`wrangler-template.jsonc`. Anything not in the substitution table appears
|
|
90
|
-
verbatim in the generated config.
|
|
91
|
-
|
|
92
|
-
## Adding a new site
|
|
93
|
-
|
|
94
|
-
1. Install the `decocms-deployer` GitHub App on the new storefront repo
|
|
95
|
-
(Settings → Integrations → GitHub Apps in the deco-sites org).
|
|
96
|
-
2. Add the four caller workflow stubs to the new repo (copy from any existing
|
|
97
|
-
storefront's `.github/workflows/{deploy,preview,sync-secrets,regen-blocks}.yml`).
|
|
98
|
-
3. Add `wrangler.jsonc` to the new repo's `.gitignore` and add the
|
|
99
|
-
`gen:wrangler` / `predev` / `prebuild` / `types` scripts to `package.json`
|
|
100
|
-
so local dev still works (use any existing storefront as a template).
|
|
101
|
-
4. If the site needs runtime secrets, create a new environment in this repo
|
|
102
|
-
named `<repo-basename>-secrets` and add the `SECRET_*` values there. Set
|
|
103
|
-
environment protection rules to grant the site team self-service access to
|
|
104
|
-
their own environment.
|
|
105
|
-
5. Push to `main` and verify the deploy lands on a worker named after the repo.
|
|
106
|
-
|
|
107
|
-
## Migrating an existing site whose worker name doesn't match its repo
|
|
108
|
-
|
|
109
|
-
Two cases to be aware of:
|
|
110
|
-
|
|
111
|
-
- **Worker rename.** The worker created by the first deploy will use the repo
|
|
112
|
-
basename. If an old worker exists with a different name (e.g.
|
|
113
|
-
`miess-01-tanstack` repo whose old worker was `miess-tanstack`), you'll need
|
|
114
|
-
a manual cutover: deploy the new worker, re-attach custom domain routes via
|
|
115
|
-
the Cloudflare dashboard, copy any wrangler secrets, then delete the old
|
|
116
|
-
worker. There is intentionally no per-site override for this — these cases
|
|
117
|
-
are rare and best resolved at the CF layer.
|
|
118
|
-
- **AE dataset rename.** The dataset name is derived from worker name, so a
|
|
119
|
-
worker rename also changes the AE dataset. Old data remains queryable under
|
|
120
|
-
the old dataset name; new data goes to the new name. Update Grafana panels
|
|
121
|
-
and saved queries accordingly.
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// Canonical wrangler.jsonc template for every storefront on the platform.
|
|
2
|
-
//
|
|
3
|
-
// There is no per-site registry. Worker name == storefront repo basename by
|
|
4
|
-
// convention; the central deploy workflows pass `WORKER_NAME` to the build
|
|
5
|
-
// script, which substitutes `$WORKER_*` tokens in this template and writes
|
|
6
|
-
// the result to `wrangler.jsonc` in the caller checkout.
|
|
7
|
-
//
|
|
8
|
-
// Substitution tokens (see scripts/deploy/site-registry.mjs):
|
|
9
|
-
// $WORKER_NAME -> worker name verbatim (e.g. "als-tanstack")
|
|
10
|
-
// $WORKER_UNDERSCORE -> worker name, `-` -> `_` (e.g. "als_tanstack")
|
|
11
|
-
//
|
|
12
|
-
// To upgrade compatibility flags, observability, KV bindings, or any field
|
|
13
|
-
// that should change for every site at once, change this file and tag a new
|
|
14
|
-
// deco-start release.
|
|
15
|
-
//
|
|
16
|
-
// Notes on what is INTENTIONALLY missing here:
|
|
17
|
-
// - "name" -- always derived from the per-site `worker_name`.
|
|
18
|
-
// - "account_id" -- never lives in the JSON. Wrangler reads it from the
|
|
19
|
-
// CLOUDFLARE_ACCOUNT_ID env var in CI and from local config otherwise.
|
|
20
|
-
// This way, a typo in JSON cannot misroute a deploy.
|
|
21
|
-
// - "routes" -- production custom domains are managed in the Cloudflare
|
|
22
|
-
// dashboard, not in JSON. Avoids drift between JSON and CF reality and
|
|
23
|
-
// means a site going live doesn't need a deco-start PR.
|
|
24
|
-
//
|
|
25
|
-
// To upgrade compatibility flags, observability, or worker-entry path across
|
|
26
|
-
// every site at once, change this file and tag a new deco-start release.
|
|
27
|
-
{
|
|
28
|
-
"compatibility_date": "2026-02-14",
|
|
29
|
-
"compatibility_flags": ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
|
|
30
|
-
"main": "./src/worker-entry.ts",
|
|
31
|
-
"workers_dev": true,
|
|
32
|
-
"preview_urls": true,
|
|
33
|
-
"kv_namespaces": [
|
|
34
|
-
{ "binding": "SITES_KV", "id": "ad0b74fc4d9341c9af9149c4ab85132f" }
|
|
35
|
-
],
|
|
36
|
-
"version_metadata": { "binding": "CF_VERSION_METADATA" },
|
|
37
|
-
"analytics_engine_datasets": [
|
|
38
|
-
{ "binding": "DECO_METRICS", "dataset": "deco_metrics_$WORKER_UNDERSCORE" }
|
|
39
|
-
],
|
|
40
|
-
"observability": {
|
|
41
|
-
"logs": {
|
|
42
|
-
"enabled": true,
|
|
43
|
-
"invocation_logs": true
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// build-wrangler-config.mjs
|
|
3
|
-
//
|
|
4
|
-
// Materializes a `wrangler.jsonc` from the canonical template, with
|
|
5
|
-
// $WORKER_* tokens substituted. The output file is what `wrangler deploy` /
|
|
6
|
-
// `wrangler versions upload` / `wrangler secret put` will read.
|
|
7
|
-
//
|
|
8
|
-
// Required env:
|
|
9
|
-
// DECO_START_PATH - path to a checked-out deco-start (e.g. ".deco-start")
|
|
10
|
-
// WORKER_NAME - the Cloudflare worker name (= storefront repo basename
|
|
11
|
-
// by convention; passed by the central CI workflows)
|
|
12
|
-
// OUTPUT_PATH - where to write the merged wrangler.jsonc
|
|
13
|
-
// (e.g. "./wrangler.jsonc" in the caller checkout)
|
|
14
|
-
|
|
15
|
-
import { writeFileSync } from "node:fs";
|
|
16
|
-
import { resolve } from "node:path";
|
|
17
|
-
import { applyWorkerName, loadTemplate } from "./site-registry.mjs";
|
|
18
|
-
|
|
19
|
-
function fail(message) {
|
|
20
|
-
console.error(`::error::${message}`);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const decoStartPath = process.env.DECO_START_PATH;
|
|
25
|
-
const workerName = process.env.WORKER_NAME;
|
|
26
|
-
const outputPath = process.env.OUTPUT_PATH;
|
|
27
|
-
|
|
28
|
-
if (!decoStartPath) fail("DECO_START_PATH env var is required");
|
|
29
|
-
if (!workerName) fail("WORKER_NAME env var is required");
|
|
30
|
-
if (!outputPath) fail("OUTPUT_PATH env var is required");
|
|
31
|
-
|
|
32
|
-
let merged;
|
|
33
|
-
try {
|
|
34
|
-
merged = applyWorkerName(loadTemplate(decoStartPath), workerName);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const header = `// AUTOGENERATED by @decocms/start at deploy time.
|
|
40
|
-
// Do not edit -- changes will be overwritten on the next deploy.
|
|
41
|
-
// Source: decocms/deco-start deploy/wrangler-template.jsonc + worker name "${workerName}"
|
|
42
|
-
`;
|
|
43
|
-
|
|
44
|
-
const body = JSON.stringify(merged, null, 2);
|
|
45
|
-
writeFileSync(resolve(outputPath), `${header}${body}\n`);
|
|
46
|
-
|
|
47
|
-
console.log(`Wrote ${outputPath} (worker "${workerName}")`);
|
package/scripts/deploy/jsonc.mjs
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
// Minimal JSONC -> JSON parser used by the deploy scripts.
|
|
2
|
-
//
|
|
3
|
-
// Strips // line comments and /* block comments */ outside of string literals
|
|
4
|
-
// and tolerates trailing commas in objects/arrays. Dependency-free so the
|
|
5
|
-
// deploy scripts can run with vanilla `node` in CI.
|
|
6
|
-
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @param {string} input
|
|
11
|
-
* @returns {string}
|
|
12
|
-
*/
|
|
13
|
-
function stripComments(input) {
|
|
14
|
-
let out = "";
|
|
15
|
-
let i = 0;
|
|
16
|
-
let inStr = false;
|
|
17
|
-
let strChar = "";
|
|
18
|
-
while (i < input.length) {
|
|
19
|
-
const c = input[i];
|
|
20
|
-
const n = input[i + 1];
|
|
21
|
-
if (inStr) {
|
|
22
|
-
out += c;
|
|
23
|
-
if (c === "\\" && i + 1 < input.length) {
|
|
24
|
-
out += input[i + 1];
|
|
25
|
-
i += 2;
|
|
26
|
-
continue;
|
|
27
|
-
}
|
|
28
|
-
if (c === strChar) inStr = false;
|
|
29
|
-
i++;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (c === '"') {
|
|
33
|
-
inStr = true;
|
|
34
|
-
strChar = c;
|
|
35
|
-
out += c;
|
|
36
|
-
i++;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (c === "/" && n === "/") {
|
|
40
|
-
while (i < input.length && input[i] !== "\n") i++;
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (c === "/" && n === "*") {
|
|
44
|
-
i += 2;
|
|
45
|
-
while (i < input.length && !(input[i] === "*" && input[i + 1] === "/")) i++;
|
|
46
|
-
i += 2;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
out += c;
|
|
50
|
-
i++;
|
|
51
|
-
}
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* @param {string} text
|
|
57
|
-
* @returns {unknown}
|
|
58
|
-
*/
|
|
59
|
-
export function parseJsonc(text) {
|
|
60
|
-
const stripped = stripComments(text).replace(/,\s*([}\]])/g, "$1");
|
|
61
|
-
return JSON.parse(stripped);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* @param {string} path
|
|
66
|
-
* @returns {unknown}
|
|
67
|
-
*/
|
|
68
|
-
export function readJsoncFile(path) {
|
|
69
|
-
const raw = readFileSync(path, "utf8");
|
|
70
|
-
try {
|
|
71
|
-
return parseJsonc(raw);
|
|
72
|
-
} catch (err) {
|
|
73
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
74
|
-
throw new Error(`Failed to parse JSONC at ${path}: ${message}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
// Template loader + token substitution for the canonical wrangler config.
|
|
2
|
-
//
|
|
3
|
-
// There is no per-site "registry" anymore: every site's wrangler config is
|
|
4
|
-
// produced from `deploy/wrangler-template.jsonc` plus the worker name (which
|
|
5
|
-
// equals the storefront repo basename by convention). To accommodate fields
|
|
6
|
-
// that must vary deterministically per worker, the template can use these
|
|
7
|
-
// substitution tokens, which are replaced at config-build time:
|
|
8
|
-
//
|
|
9
|
-
// $WORKER_NAME -> worker name verbatim (e.g. "als-tanstack")
|
|
10
|
-
// $WORKER_UNDERSCORE -> worker name, `-` -> `_` (e.g. "als_tanstack")
|
|
11
|
-
//
|
|
12
|
-
// Trust model: callers cannot pass a fabricated worker name to the central
|
|
13
|
-
// CI workflows -- the deploy is gated by the `decocms-deployer` GitHub App
|
|
14
|
-
// being installed on the target storefront repo. If the App isn't installed
|
|
15
|
-
// there, the App-token mint fails and the deploy never starts.
|
|
16
|
-
|
|
17
|
-
import { existsSync } from "node:fs";
|
|
18
|
-
import { join } from "node:path";
|
|
19
|
-
import { readJsoncFile } from "./jsonc.mjs";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @param {string} decoStartPath
|
|
23
|
-
* @returns {string}
|
|
24
|
-
*/
|
|
25
|
-
export function templatePath(decoStartPath) {
|
|
26
|
-
return join(decoStartPath, "deploy", "wrangler-template.jsonc");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* @param {string} decoStartPath
|
|
31
|
-
* @returns {Record<string, unknown>}
|
|
32
|
-
*/
|
|
33
|
-
export function loadTemplate(decoStartPath) {
|
|
34
|
-
const path = templatePath(decoStartPath);
|
|
35
|
-
if (!existsSync(path)) {
|
|
36
|
-
throw new Error(`wrangler-template.jsonc not found at ${path}.`);
|
|
37
|
-
}
|
|
38
|
-
const raw = readJsoncFile(path);
|
|
39
|
-
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
40
|
-
throw new Error(`Template at ${path} must be a JSON object.`);
|
|
41
|
-
}
|
|
42
|
-
return /** @type {Record<string, unknown>} */ (raw);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Recursively replace `$WORKER_*` tokens in any string value of an
|
|
47
|
-
* object/array tree. Returns a new tree.
|
|
48
|
-
*
|
|
49
|
-
* @param {unknown} value
|
|
50
|
-
* @param {Record<string, string>} replacements
|
|
51
|
-
* @returns {unknown}
|
|
52
|
-
*/
|
|
53
|
-
function substituteTokens(value, replacements) {
|
|
54
|
-
if (typeof value === "string") {
|
|
55
|
-
let out = value;
|
|
56
|
-
for (const [token, repl] of Object.entries(replacements)) {
|
|
57
|
-
out = out.split(token).join(repl);
|
|
58
|
-
}
|
|
59
|
-
return out;
|
|
60
|
-
}
|
|
61
|
-
if (Array.isArray(value)) {
|
|
62
|
-
return value.map((v) => substituteTokens(v, replacements));
|
|
63
|
-
}
|
|
64
|
-
if (value && typeof value === "object") {
|
|
65
|
-
const out = /** @type {Record<string, unknown>} */ ({});
|
|
66
|
-
for (const [k, v] of Object.entries(value)) {
|
|
67
|
-
out[k] = substituteTokens(v, replacements);
|
|
68
|
-
}
|
|
69
|
-
return out;
|
|
70
|
-
}
|
|
71
|
-
return value;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Produce the wrangler config object by substituting `$WORKER_*` tokens in
|
|
76
|
-
* the template and prepending `name`.
|
|
77
|
-
*
|
|
78
|
-
* @param {Record<string, unknown>} template
|
|
79
|
-
* @param {string} workerName
|
|
80
|
-
* @returns {Record<string, unknown>}
|
|
81
|
-
*/
|
|
82
|
-
export function applyWorkerName(template, workerName) {
|
|
83
|
-
if (typeof workerName !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(workerName)) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
`Invalid worker name: ${JSON.stringify(workerName)}. Use lowercase, hyphen-separated.`,
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
const substituted = /** @type {Record<string, unknown>} */ (
|
|
89
|
-
substituteTokens(template, {
|
|
90
|
-
$WORKER_UNDERSCORE: workerName.replace(/-/g, "_"),
|
|
91
|
-
$WORKER_NAME: workerName,
|
|
92
|
-
})
|
|
93
|
-
);
|
|
94
|
-
return { name: workerName, ...substituted };
|
|
95
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// deco-wrangler
|
|
3
|
-
//
|
|
4
|
-
// Local-dev wrapper around `wrangler` for storefront repos that no longer
|
|
5
|
-
// commit a `wrangler.jsonc`. Generates the canonical config from
|
|
6
|
-
// `@decocms/start`'s template into `./wrangler.jsonc` (gitignored), then
|
|
7
|
-
// optionally execs the real `wrangler` with that config in cwd.
|
|
8
|
-
//
|
|
9
|
-
// Usage from a customer repo (after `npm i -D @decocms/start@latest`):
|
|
10
|
-
//
|
|
11
|
-
// npx deco-wrangler gen # generate ./wrangler.jsonc and exit
|
|
12
|
-
// # (used by predev/prebuild/prepare hooks)
|
|
13
|
-
// npx deco-wrangler tail # tail prod logs (worker name resolved automatically)
|
|
14
|
-
// npx deco-wrangler types # regenerate worker-configuration.d.ts
|
|
15
|
-
// npx deco-wrangler deploy # discouraged in dev; deploys go via CI
|
|
16
|
-
// npx deco-wrangler --help # passes through to wrangler
|
|
17
|
-
//
|
|
18
|
-
// All non-`gen` invocations also (re)generate ./wrangler.jsonc first so the
|
|
19
|
-
// file is always in sync with the template before wrangler runs. The
|
|
20
|
-
// `@cloudflare/vite-plugin` and the `wrangler` CLI both auto-discover
|
|
21
|
-
// ./wrangler.jsonc in cwd, so no extra config plumbing is required.
|
|
22
|
-
//
|
|
23
|
-
// Worker name resolves in this order:
|
|
24
|
-
//
|
|
25
|
-
// 1. DECO_WORKER_NAME env var (explicit override)
|
|
26
|
-
// 2. git remote `origin` URL parsed for the GitHub repo basename
|
|
27
|
-
// 3. package.json `name` field
|
|
28
|
-
//
|
|
29
|
-
// By convention worker name == storefront repo basename. The CI deploys use
|
|
30
|
-
// the same convention. Local dev never deploys -- this wrapper is purely for
|
|
31
|
-
// generating a config that matches what the central workflow will produce.
|
|
32
|
-
|
|
33
|
-
import { execSync, spawnSync } from "node:child_process";
|
|
34
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
35
|
-
import { dirname, resolve } from "node:path";
|
|
36
|
-
import { fileURLToPath } from "node:url";
|
|
37
|
-
import { applyWorkerName, loadTemplate } from "./site-registry.mjs";
|
|
38
|
-
|
|
39
|
-
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
40
|
-
// scripts/deploy/wrangler-wrapper.mjs -> deco-start root
|
|
41
|
-
const DECO_START_PATH = resolve(SCRIPT_DIR, "..", "..");
|
|
42
|
-
|
|
43
|
-
function eprintln(msg) {
|
|
44
|
-
process.stderr.write(`[deco-wrangler] ${msg}\n`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function tryGitRemoteWorkerName() {
|
|
48
|
-
try {
|
|
49
|
-
const url = execSync("git remote get-url origin", {
|
|
50
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
51
|
-
})
|
|
52
|
-
.toString()
|
|
53
|
-
.trim();
|
|
54
|
-
// matches both git@github.com:org/repo(.git) and https://github.com/org/repo(.git)
|
|
55
|
-
const m = url.match(/github\.com[:/][^/]+\/([^/]+?)(?:\.git)?$/);
|
|
56
|
-
return m?.[1];
|
|
57
|
-
} catch {
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function tryPackageJsonName() {
|
|
63
|
-
try {
|
|
64
|
-
const pkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf8"));
|
|
65
|
-
return typeof pkg.name === "string" ? pkg.name : undefined;
|
|
66
|
-
} catch {
|
|
67
|
-
return undefined;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function resolveWorkerName() {
|
|
72
|
-
const envName = process.env.DECO_WORKER_NAME?.trim();
|
|
73
|
-
if (envName) return { name: envName, source: "DECO_WORKER_NAME env var" };
|
|
74
|
-
const gitName = tryGitRemoteWorkerName();
|
|
75
|
-
if (gitName) return { name: gitName, source: "git remote origin" };
|
|
76
|
-
const pkgName = tryPackageJsonName();
|
|
77
|
-
if (pkgName) return { name: pkgName, source: "package.json name" };
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const resolved = resolveWorkerName();
|
|
82
|
-
if (!resolved) {
|
|
83
|
-
eprintln(
|
|
84
|
-
"Could not determine worker name. Set DECO_WORKER_NAME or run from a repo with a github.com 'origin' remote.",
|
|
85
|
-
);
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
let merged;
|
|
90
|
-
try {
|
|
91
|
-
merged = applyWorkerName(loadTemplate(DECO_START_PATH), resolved.name);
|
|
92
|
-
} catch (err) {
|
|
93
|
-
eprintln(err instanceof Error ? err.message : String(err));
|
|
94
|
-
eprintln(`Worker name "${resolved.name}" was inferred from ${resolved.source}.`);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const outputPath = resolve(process.cwd(), "wrangler.jsonc");
|
|
99
|
-
const header = `// AUTOGENERATED by deco-wrangler -- DO NOT COMMIT.
|
|
100
|
-
// Add wrangler.jsonc to .gitignore. Regenerated on every \`deco-wrangler\` run.
|
|
101
|
-
// Source: @decocms/start deploy/wrangler-template.jsonc + worker name "${resolved.name}"
|
|
102
|
-
`;
|
|
103
|
-
writeFileSync(outputPath, `${header}${JSON.stringify(merged, null, 2)}\n`);
|
|
104
|
-
|
|
105
|
-
eprintln(`Resolved worker name "${resolved.name}" (via ${resolved.source})`);
|
|
106
|
-
eprintln(`Generated ${outputPath}`);
|
|
107
|
-
|
|
108
|
-
const argv = process.argv.slice(2);
|
|
109
|
-
// `gen` mode: just write the config and exit. Used by package.json
|
|
110
|
-
// predev/prebuild/prepare hooks to keep wrangler.jsonc in sync with the
|
|
111
|
-
// template without invoking wrangler itself.
|
|
112
|
-
if (argv[0] === "gen" || argv[0] === "generate") {
|
|
113
|
-
process.exit(0);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const wranglerArgs = ["wrangler", ...argv];
|
|
117
|
-
const result = spawnSync("npx", wranglerArgs, { stdio: "inherit" });
|
|
118
|
-
process.exit(result.status ?? 1);
|