@sidebase/base-config 0.2.2 → 0.3.1

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/README.md CHANGED
@@ -1,237 +1,161 @@
1
1
  # @sidebase/base-config
2
2
 
3
- Shared `@sidebase` Nuxt configuration, delivered over two channels in one package:
3
+ Shared setup for sidebase Nuxt 4 apps. [`@sidebase/streamctl`](https://github.com/sidebase/streamctl)
4
+ syncs this package's config files (CI, Dockerfile, tsconfig, lint config, editor settings, ...) into your
5
+ repo and keeps them up to date. The package also exports ESLint, Prisma and tsconfig helpers.
4
6
 
5
- - npm channel: the root export `@sidebase/base-config` plus the subpath exports
6
- `@sidebase/base-config/config`, `@sidebase/base-config/eslint`,
7
- `@sidebase/base-config/prisma`, and `@sidebase/base-config/tsconfig.base`.
8
- - file-sync channel: the bundled `presets/` payload read by the [`@sidebase/streamctl`](https://github.com/sidebase/streamctl) CLI.
7
+ ## Quick start
9
8
 
10
- ## Install
11
-
12
- For the npm channel, install the package plus what the subpaths you use require:
9
+ Requires Nuxt 4, Node >= 24.21 and pnpm.
13
10
 
14
11
  ```sh
15
- pnpm add -D @sidebase/base-config eslint jiti
12
+ pnpm add -D @sidebase/streamctl
13
+ pnpm streamctl init --package @sidebase/base-config
16
14
  ```
17
15
 
18
- `eslint` (`^10.5.0`) and `jiti` (`>= 2.2.0`) are what `/eslint` needs; jiti is only
19
- required because the config file is TypeScript. `prisma` and `@prisma/client` (`^6.19`)
20
- are optional peers, install them only if you use `/prisma`.
21
-
22
- **`jiti` is not a declared peer.** `peerDependencies` is `eslint`, `prisma` and
23
- `@prisma/client`. It is in the command above because `/eslint` genuinely needs it. Since
24
- nothing declares it, pnpm emits no missing-peer warning if you leave it out. This README is
25
- the only thing that will tell you. See Requirements below.
26
-
27
- For the file-sync channel, see Adopting below. The CLI wires the dependency itself.
28
-
29
- ## Adopting
30
-
31
- The payload *is* the install: [`@sidebase/streamctl`](https://github.com/sidebase/streamctl)
32
- reads `presets/` straight from this package in a consuming repo's `node_modules`.
33
-
34
- ```sh
35
- pnpm streamctl init --package @sidebase/base-config # scaffold + first sync
36
- pnpm streamctl check # CI drift gate
37
- pnpm streamctl upgrade --to <version> # move the pin forward
38
- ```
39
-
40
- Neither package needs a registry token or private-registry setup: both target the public
41
- `@sidebase` scope on npm.
42
-
43
- Coming from `@sidestream-tech/nuxt-config`? That rename is a manual migration, not an
44
- upgrade: `streamctl upgrade` moves the version pin and never rewrites `package:`. Follow
45
- [`docs/migration.md`](docs/migration.md), and note that the step order is load-bearing.
46
-
47
- ### Presets and profiles
48
-
49
- Two separate axes in `streamctl.config.ts`:
50
-
51
- - `base` is the preset, the set of managed files you get. `nuxt-app` names the repo
52
- *type*: it ships a Dockerfile, a postgres/prisma CI pipeline, and PR-preview cleanup.
53
- - `profile` is the version baseline, the dependency pins reconciled into your
54
- `package.json`. It names the framework and its major: `nuxt-4`.
55
-
56
- The major is in the profile name on purpose. Each profile carries its own
57
- `detect.majorIs`, so `init` picks the right one from your `package.json` and a future
58
- `nuxt-5` profile can sit alongside `nuxt-4` while repos migrate one at a time.
59
-
60
- `nuxt-4` is the only profile shipped today, so this payload is Nuxt 4 only. There is no
61
- `nuxt-3` profile and none is planned. Running `init` in a Nuxt 3 repo fails with
62
- "No version profile could be detected", because no profile's `detect.majorIs` matches the
63
- `nuxt` version in your `package.json`. Move the repo to Nuxt 4 before adopting.
16
+ `init` adds `@sidebase/base-config`, writes `streamctl.config.ts`, and runs the first sync.
17
+ Review the changes and commit them.
64
18
 
65
- A profile name must match a `versionProfiles` key in the preset chain. If it does not, the
66
- CLI fails with `CONFIG_INVALID`: `profile "..." is not declared in presets/manifest.json
67
- profiles[]`. `pnpm validate:presets` catches the same mismatch at build time, so a broken
68
- profile name cannot ship in the first place.
19
+ Coming from `@sidestream-tech/nuxt-config`? Follow [`docs/migration.md`](docs/migration.md)
20
+ instead.
69
21
 
70
- #### Reconciled `scripts.lint`
22
+ ## What you get
71
23
 
72
- The `nuxt-4` profile reconciles your `package.json` `scripts.lint` to
73
- `oxlint -c .oxlintrc.json --deny-warnings && eslint --max-warnings 0 .`, so the fleet
74
- runs `oxlint` (already pinned, with the shipped `.oxlintrc.json`) before ESLint.
24
+ | File | Managed as | What it is |
25
+ | ---- | ---------- | ---------- |
26
+ | `.github/workflows/ci.yml` | owned | Lint, typecheck and build, plus optional test and e2e jobs |
27
+ | `.github/workflows/pr-preview-cleanup.yml` | owned | Removes the `preview-deployed` label when `preview` is removed |
28
+ | `.github/workflows/streamctl-upgrade.yml` | owned, opt-in | Weekly upgrade PRs (see [Upgrades](#upgrades)) |
29
+ | `Dockerfile` | owned | Production image, runs `prisma migrate deploy` and then the server |
30
+ | `tsconfig.json` | owned | Extends the shared strict base and Nuxt's generated config |
31
+ | `pnpm-workspace.yaml` | owned | 7-day release cooldown and the install-script allowlist |
32
+ | `.oxlintrc.json` | owned | oxlint rules, run before ESLint |
33
+ | `AGENTS.md` | owned | Conventions for coding agents |
34
+ | `.editorconfig`, `.gitignore`, `.dockerignore` | block | A marked block; the rest of the file is yours |
35
+ | `.vscode/settings.json`, `.vscode/extensions.json` | merged | Only the keys the payload sets; your other keys stay |
36
+ | `eslint.config.ts`, `prisma.config.ts` | scaffolded | Written once, then yours |
75
37
 
76
- Unlike a version pin, a script has no forward-version floor to protect it, because a
77
- script is not a semver, so this is a plain **overwrite**: every `sync` replaces whatever
78
- your `scripts.lint` currently is. A repo that wants its own (for example
79
- `... && LINT_TYPEAWARE=true eslint --max-warnings 0 .` to opt into type-aware linting)
80
- keeps it by excluding the key:
38
+ - **owned:** streamctl writes the whole file. Local edits show up as drift in `check`.
39
+ - **block:** only the region between the `BEGIN`/`END streamctl MANAGED BLOCK` markers is
40
+ managed.
81
41
 
82
- ```ts
83
- export default {
84
- // ...
85
- versionSyncExclude: ["scripts.lint"],
86
- };
87
- ```
88
-
89
- #### Overriding a managed file
90
-
91
- `.editorconfig` is **block**-managed: the payload owns a marked region, delimited by
92
- `# BEGIN streamctl MANAGED BLOCK editorconfig` / `# END ...`, and preserves everything
93
- outside it byte-for-byte. On first sync the managed block is appended to the end of your
94
- file. Because EditorConfig applies later matching sections over earlier ones, you keep a
95
- project override by placing it **below** the managed block:
42
+ `sync` also updates `package.json`:
43
+ - `engines`, `packageManager`, and the version ranges of nuxt, typescript, eslint, oxlint,
44
+ prisma, `@prisma/client`, jiti, json-sort-cli and vitest. A range is only raised, never
45
+ lowered.
46
+ - `scripts.postinstall` (`nuxt prepare`) and `scripts.lint` (oxlint, then ESLint).
96
47
 
97
- ```ini
98
- # BEGIN streamctl MANAGED BLOCK editorconfig
99
- [*]
100
- indent_size = 2
101
- # END streamctl MANAGED BLOCK editorconfig
48
+ ## Everyday use
102
49
 
103
- # your override wins because it comes after the managed block
104
- [*]
105
- indent_size = 4
50
+ ```sh
51
+ pnpm streamctl sync # apply the payload after changing the config
52
+ pnpm streamctl check # CI gate: fails when managed files drifted
53
+ pnpm streamctl upgrade # move to the latest payload version and re-sync
106
54
  ```
107
55
 
108
- That way the payload keeps updating its region on every sync while your `indent_size = 4`
109
- survives. (A section placed *above* the block would be overridden by the block's
110
- `indent_size = 2`, so keep overrides below the `END` marker.)
56
+ `sync` stops on conflicts, e.g. a file you edited by hand. Resolve them with
57
+ `pnpm streamctl sync --interactive`.
111
58
 
112
- **First-sync hazard.** If your repo already has an `.editorconfig`, the block writer appends
113
- without looking at what is there, so all of your existing content ends up *above* the block
114
- and therefore loses. `streamctl check` exits 0 either way, because the block strategy only
115
- inspects its own markers. This applies to a fresh `streamctl init` just as much as to a
116
- migration, and you have to fix it by hand once. Step 8 of
117
- [`docs/migration.md`](docs/migration.md) has the detector and the two-case fix.
59
+ ### Upgrades
118
60
 
119
- If you instead want to stop managing a file entirely, opt it out per-path:
61
+ Upgrade by hand with `pnpm streamctl upgrade`, or let a weekly workflow open the PRs:
120
62
 
121
63
  ```ts
122
- files: { ".editorconfig": "off" },
64
+ automation: { upgradePr: true },
123
65
  ```
124
66
 
125
- The cost of `off` is total: streamctl no longer touches that file, so **every future
126
- payload fix to it stops reaching your repo**, including new rules, security bumps, and
127
- format corrections. Prefer the block override above when you only need to change part of
128
- a file.
129
-
130
- ## This repo's CLI dependency
67
+ The workflow runs `streamctl check`, lint, typecheck, test and build before it opens the PR.
68
+ Files that need manual work end up in a draft PR with instructions.
131
69
 
132
- The payload is validated and dogfooded against the CLI, so `@sidebase/streamctl` is a
133
- `devDependency` here, pinned to the published semver range.
134
- `validate:presets` imports the CLI's exported manifest schema and the e2e dry-run drives the
135
- real CLI binary, so both resolve `@sidebase/streamctl` from the registry.
70
+ By default it uses `GITHUB_TOKEN`, which may not change `.github/workflows/**`, so
71
+ workflow changes are left out of the PR. To include them, set
72
+ `automation.allowWorkflowUpdates: true` and add a `STREAMCTL_PR_TOKEN` secret (a
73
+ fine-grained PAT or GitHub App token with Contents, Pull requests and Workflows write).
74
+ That token also makes the PR trigger your normal CI.
136
75
 
137
- This payload needs **streamctl >= 0.3.0**: its manifest declares `schemaVersion` 3 (the
138
- `fromDependency` placeholder behind the derived Prisma ARG), which a 0.2.x CLI rejects with
139
- `SCHEMA_UNSUPPORTED`. The root `streamctl.config.ts` location it documents arrived in
140
- 0.2.0. See "Config location" below.
76
+ ## Configuration
141
77
 
142
- ## Typed config
143
-
144
- The root export `@sidebase/base-config` and the subpath `@sidebase/base-config/config`
145
- are the same module. Both provide `defineNuxtBaseConfig` and the `NuxtBaseConfig` type,
146
- which is what makes a `streamctl.config.ts` typed. Every consuming repo has this file,
147
- and `streamctl init` scaffolds it importing from the root export:
78
+ Everything is configured in `streamctl.config.ts`. `defineNuxtBaseConfig` gives you
79
+ completion and type checking for every option:
148
80
 
149
81
  ```ts
150
- // streamctl.config.ts
151
82
  import { defineNuxtBaseConfig } from "@sidebase/base-config";
152
83
 
153
84
  export default defineNuxtBaseConfig({
154
85
  package: "@sidebase/base-config",
155
86
  base: "nuxt-app",
156
- version: "0.1.0",
87
+ version: "0.3.0",
157
88
  profile: "nuxt-4",
158
- ci: { unitTests: true },
89
+
90
+ ci: { unitTests: true, e2e: true },
91
+ docker: { aptPackages: ["ffmpeg"] },
92
+ automation: { upgradePr: true },
93
+ editor: { i18nSourceLanguage: "en" },
159
94
  });
160
95
  ```
161
96
 
162
- ### Config location
163
-
164
- streamctl >= 0.2.0 reads `streamctl.config.ts` from the repo root, and `init` scaffolds it
165
- there. The older `.streamctl/config.ts` is still resolved, so an existing repo keeps working
166
- untouched and can move the file whenever it likes.
167
-
168
- Move it and nothing else changes: the contents are identical, and the CLI warns rather
169
- than guesses if both exist, naming the root file as the one it read. Keeping the legacy
170
- path is fine too. The one thing not to do is leave both in place, since only the root file
171
- is read and edits to the other will look like they do nothing.
172
-
173
- If you set `ignoresTypeAware` yourself, list whichever path your repo uses. The default
174
- covers both.
175
-
176
- `defineNuxtBaseConfig` returns the config unchanged. Its job is typed editor inference
177
- for this payload's knobs: completion and type checking on `ci`, `versions`, `docker`,
178
- `automation`, `security`, `pnpm`, `editor`, and the whole `eslint` option surface, plus `profile`
179
- narrowed to the profile names that actually exist. Without it the object is an untyped
180
- literal, so a typo such as `ci: { unitTest: true }` stays silent in your editor.
181
-
182
- The CLI shape-checks the config against the manifest when it loads it, so a sync still
183
- works without the helper. The helper is what moves that feedback into your editor.
97
+ ### Options
184
98
 
185
- Import `NuxtBaseConfig` directly when you need the type on its own:
99
+ | Option | Default | Effect |
100
+ | ------ | ------- | ------ |
101
+ | `ci.unitTests` | `false` | Add a `test` job (`pnpm test`) |
102
+ | `ci.e2e` | `false` | Add an `e2e` job with a postgres service |
103
+ | `ci.aptPackages` | `[]` | apt packages for the `test`/`e2e` jobs and upgrade validation |
104
+ | `docker.aptPackages` | `[]` | apt packages for the image, on top of `openssl` |
105
+ | `docker.preInstall` | `""` | Dockerfile lines before `pnpm install` |
106
+ | `docker.buildArgs` | `""` | Dockerfile lines before `COPY . .` |
107
+ | `docker.buildSteps` | `nuxi prepare`, `prisma generate`, `build` | The build commands |
108
+ | `docker.finalStage` | `""` | Dockerfile lines in the final stage, before `CMD` |
109
+ | `docker.prismaRuntime` | copy schema, install Prisma CLI | Prisma setup in the final stage |
110
+ | `docker.prismaVersion` | your `prisma` version | Prisma CLI version in the image |
111
+ | `docker.startCommand` | `npm exec prisma migrate deploy && exec node ...` | The `CMD`, one line |
112
+ | `versions.node` | `24.21.0` | Node in CI, the Dockerfile and the upgrade workflow |
113
+ | `versions.pnpm` | `10.34.5` | pnpm in the Dockerfile (CI uses `packageManager`) |
114
+ | `automation.upgradePr` | `false` | Add the weekly upgrade workflow |
115
+ | `automation.allowWorkflowUpdates` | `false` | Include workflow changes in upgrade PRs (needs a token) |
116
+ | `automation.prTokenSecret` | `"STREAMCTL_PR_TOKEN"` | Secret name for that token |
117
+ | `automation.prepare` | `pnpm prisma generate` if Prisma is installed | Command run before validating an upgrade; `""` skips |
118
+ | `security.minimumReleaseAge` | `"10080"` | Minutes a release must be public before pnpm installs it; `"0"` disables |
119
+ | `pnpm.onlyBuiltDependencies` | `[]` | Extra packages allowed to run install scripts |
120
+ | `editor.i18nSourceLanguage` | `"de"` | i18n-ally source language |
121
+
122
+ Things to know:
123
+
124
+ - **A `docker.*` text option replaces the default.** It does not append, so a `buildSteps`
125
+ override must list every build command you still need.
126
+ - **`docker.prismaRuntime` and `docker.startCommand` go together.** Without Prisma, set
127
+ `prismaRuntime: ""` and a `startCommand` without `migrate deploy`, e.g.
128
+ `exec node .output/server/index.mjs`. Keep the `exec`, so node gets SIGTERM.
129
+ - **Repos with English source strings must set `editor.i18nSourceLanguage: "en"`.**
130
+ Otherwise every sync writes `"de"` back.
131
+ - **Adopting `pnpm-workspace.yaml` replaces your existing file**, including any
132
+ `onlyBuiltDependencies`. Move those entries to `pnpm.onlyBuiltDependencies` instead of
133
+ running `pnpm approve-builds`, which the next sync reverts.
134
+
135
+ ### Customizing managed files
136
+
137
+ **Block files:** put your own settings below the `END` marker. For `.editorconfig`,
138
+ later sections win, so an override below the block takes precedence. On the first sync,
139
+ the block is appended *after* your existing content, so move that content below the
140
+ block once.
141
+
142
+ **Opt a file out** when the options are not enough:
186
143
 
187
144
  ```ts
188
- import type { NuxtBaseConfig } from "@sidebase/base-config/config";
145
+ files: { "Dockerfile": "off" },
189
146
  ```
190
147
 
191
- ## Shared tsconfig
148
+ streamctl then leaves the file alone, including all future fixes to it.
192
149
 
193
- `@sidebase/base-config/tsconfig.base` is the shared strict TypeScript base, exported as a
194
- JSON file so a `tsconfig.json` can extend it by package name. The `nuxt-app` preset ships
195
- a managed `tsconfig.json` that chains it with Nuxt's generated config:
150
+ **Keep your own `scripts.lint`** (or any other synced `package.json` field):
196
151
 
197
- ```json
198
- {
199
- "extends": [
200
- "@sidebase/base-config/tsconfig.base",
201
- "./.nuxt/tsconfig.json"
202
- ]
203
- }
152
+ ```ts
153
+ versionSyncExclude: ["scripts.lint"],
204
154
  ```
205
155
 
206
- Order matters: the shared base comes first, so Nuxt's generated config applies over it.
207
-
208
- ## Naming convention
209
-
210
- Two rules cover every exported name, so you can predict them:
211
-
212
- - Types describing **this payload's config shape** are `NuxtBase*`: `NuxtBaseConfig`,
213
- `NuxtBaseCiConfig`, `defineNuxtBaseConfig`, and the `NUXT_BASE_*_KEYS` lists. The prefix
214
- is doing real work here, because these types describe the config of this specific
215
- payload rather than anything generic.
216
- - **Everything else is domain-named with no package prefix**: `buildPrismaConfig`,
217
- `applyPrismaDevEnv`, `createSidebaseEslint`, `resolveEslintOptions`. The import path
218
- already states where a symbol comes from, so repeating it in the identifier adds
219
- nothing.
220
-
221
- `streamctl` is reserved for the CLI (`@sidebase/streamctl`) and the files it owns:
222
- `streamctl.config.ts` in the repo root, and the legacy `.streamctl/` directory it still
223
- reads. Nothing exported from this package carries that name, because nothing
224
- exported from this package comes from the CLI. The ESLint layers this package builds are
225
- named `sidebase/*` (`sidebase/defaults`, `sidebase/console`, and so on), which is what you
226
- see in lint output and what you target if you `.override()` a layer by name. They are
227
- built at runtime by this package; the CLI never sees them.
228
-
229
- ## ESLint factory
156
+ ## Helpers
230
157
 
231
- `@sidebase/base-config/eslint` exports `createSidebaseEslint(options?)`, which wraps
232
- [`@antfu/eslint-config`](https://github.com/antfu/eslint-config) (a dependency of this
233
- package, so **do not add `@antfu/eslint-config` directly**) and returns its
234
- `FlatConfigComposer`, so you chain `.append()` / `.override()` for local rules:
158
+ ### ESLint
235
159
 
236
160
  ```ts
237
161
  // eslint.config.ts
@@ -241,427 +165,82 @@ export default createSidebaseEslint({ zod: "full", trpcGuard: true })
241
165
  .append({ rules: { "vue/multi-word-component-names": "off" } });
242
166
  ```
243
167
 
244
- ### Options
168
+ Wraps [`@antfu/eslint-config`](https://github.com/antfu/eslint-config), which is a
169
+ dependency of this package; don't install it yourself. Needs `eslint` and `jiti` (for the
170
+ TypeScript config file) in your devDependencies; `sync` adds both.
245
171
 
246
172
  | Option | Default | Effect |
247
173
  | ------ | ------- | ------ |
248
- | `zod` | `"none"` | `"full"` = ban `.extend()/.merge()/.passthrough()` + enforce `import * as z`; `"import-style"` = import style only; `"none"` = off |
249
- | `console` | `"error"` | `"error"` bans all `console.*`; an array is the allow-list (e.g. `["warn", "error"]`) |
174
+ | `zod` | `"none"` | `"import-style"`: enforce `import * as z`. `"full"`: also ban `.extend()`, `.merge()`, `.passthrough()` |
175
+ | `console` | `"error"` | Ban all `console.*`, or pass allowed methods, e.g. `["warn", "error"]` |
250
176
  | `trpcGuard` | `false` | Ban `publicProcedure` in `server/trpc/routers/**` |
251
- | `prismaImportGuard` | `false` | Ban `~~/prisma/` client imports on the app side |
252
- | `typeDefStyle` | `"interface"` | `ts/consistent-type-definitions` |
253
- | `autoImportPaths` | `["utils/", "composables/", "~~/shared/types/"]` | Paths banned from direct import (Nuxt auto-imports) |
254
- | `autoImportTypeOnly` | `[]` | Subset of `autoImportPaths` where `import type { ... }` stays allowed (value imports still banned) |
255
- | `ignoresTypeAware` | `["prisma.config.ts", "eslint.config.ts", "streamctl.config.ts", ".streamctl/**/*.ts"]` | Files excluded from antfu's type-aware program (only relevant when type-aware). Covers both config locations, since `.streamctl/config.ts` stays supported after the root default lands |
256
- | `testFilePattern` | `["**/*.{test,spec}.{ts,tsx}", "**/*.stories.{ts,tsx}"]` | Test/spec/story globs where the `process.env` + auto-import bans are relaxed |
257
-
258
- Rare per-repo rules belong in your local `.append(...)`, not in the option surface.
177
+ | `prismaImportGuard` | `false` | Ban `~~/prisma/` client imports outside server code |
178
+ | `typeDefStyle` | `"interface"` | Prefer `interface` or `type` |
179
+ | `autoImportPaths` | `["utils/", "composables/", "~~/shared/types/"]` | Paths Nuxt auto-imports, so direct imports are banned |
180
+ | `autoImportTypeOnly` | `[]` | Of those, paths where `import type` stays allowed |
181
+ | `ignoresTypeAware` | config files | Files excluded from type-aware linting |
182
+ | `testFilePattern` | `*.test`, `*.spec`, `*.stories` | Files where the `process.env` and auto-import bans are off |
259
183
 
260
- ### Rule IDs & inline suppression
184
+ Type-aware linting is off by default; run with `LINT_TYPEAWARE=true` to enable it.
261
185
 
262
- The custom bans map to these ESLint rule IDs. Use the exact ID in an
263
- `// eslint-disable-next-line ...` directive to suppress one line:
186
+ To silence a single line, use the rule ID:
264
187
 
265
188
  | Ban | Rule ID |
266
189
  | --- | ------- |
267
- | Direct `process.env` access, as a member expression (`process.env.X`, `const { env } = process`) | `no-restricted-properties` |
268
- | ANY import **or re-export** of `node:process` or `process` that can reach `env` (`import { env }`, `import { env as e }`, `import * as proc`, `import proc from "node:process"`, `export { env } from "node:process"`, `export * from "node:process"`) | `ts/no-restricted-imports` |
269
- | Auto-import path imports (`autoImportPaths`) | `ts/no-restricted-imports` |
270
- | App-side Prisma client import (`prismaImportGuard`) | `ts/no-restricted-imports` |
271
- | Zod named-`z` import style (`zod: "import-style"`/`"full"`) | `no-restricted-syntax` |
272
- | Zod `.extend()/.merge()/.passthrough()` (`zod: "full"`) | `no-restricted-syntax` |
273
- | `publicProcedure` in routers (`trpcGuard`) | `no-restricted-syntax` |
274
-
275
- "Re-export" is not a figure of speech: a barrel file doing `export * from "node:process"`
276
- reports, as do `export { env } from "node:process"` and `export { default as p } from
277
- "node:process"`. The one shape that does **not** report is a bare side-effect import with
278
- no bindings, `import "node:process"`. It cannot reach `env`, so there is nothing to ban.
279
-
280
- The `process.env` ban is enforced by **two** rules, and which one reports depends on the
281
- shape, so suppressing the wrong one fails twice over: the directive does not suppress
282
- anything, and ESLint additionally warns that it was unused.
283
-
284
- ```
285
- // eslint-disable-next-line no-restricted-properties
286
- import { env } from 'node:process'
287
-
288
- 1:1 warning Unused eslint-disable directive (no problems were reported from 'no-restricted-properties')
289
- 2:10 error 'env' import from 'node:process' is restricted... ts/no-restricted-imports
290
- ```
291
-
292
- ### Requirements
293
-
294
- `eslint` is a peer dependency (`^10.5.0`), so consumers of `/eslint` must supply ESLint
295
- themselves. The peer range matches the version baseline, so `streamctl` version-sync
296
- keeps a repo's ESLint in step with what this package is tested against.
297
-
298
- Flat config only. To load a TypeScript `eslint.config.ts` on Node, consumers add
299
- `jiti` (`>= 2.2.0`) as a devDependency: ESLint has no native TS loader, and ESLint 10
300
- rejects jiti below 2.2.0. Writing `eslint.config.mjs` instead drops the jiti requirement
301
- entirely, as does running on Deno or Bun, which import TypeScript directly.
302
-
303
- #### Type-aware linting prerequisite
304
-
305
- Type-aware linting is gated behind `LINT_TYPEAWARE=true` (off by default), so this only
306
- matters on the opt-in and CI path. When type-aware lint is on, the Prisma client and Nuxt
307
- types must be generated **before** `lint` runs, otherwise type-aware rules fail on missing
308
- generated types. Ordering satisfies this, not a preflight check:
309
-
310
- - the managed `ci.yml` runs `prisma generate` (and `nuxi prepare`) before `pnpm lint`, and
311
- - `scripts.postinstall: "nuxt prepare"` regenerates Nuxt types on install.
312
-
313
- A standard `pnpm install` plus the managed CI ordering covers the prerequisite. No separate
314
- check runs.
315
-
316
- ## Prisma factory
317
-
318
- `@sidebase/base-config/prisma` exports `buildPrismaConfig({ views?, typedSql? })`,
319
- which returns a Prisma 6.19 config object you wrap in `defineConfig`:
320
-
321
- ```ts
322
- // prisma.config.ts
323
- import { defineConfig } from "prisma/config";
324
- import { buildPrismaConfig } from "@sidebase/base-config/prisma";
325
-
326
- export default defineConfig(buildPrismaConfig({ views: true, typedSql: true }));
327
- ```
328
-
329
- It reads `DATABASE_URL` / `DIRECT_DATABASE_URL` / `SHADOW_DATABASE_URL` from the
330
- environment (Pattern B): the schema engine gets a pgbouncer-free direct
331
- connection: `DIRECT_DATABASE_URL` if set, otherwise `DATABASE_URL` with
332
- pooler-only query params (`pgbouncer`, `connection_limit`, and so on) stripped via `ufo`.
333
- `SHADOW_DATABASE_URL` is wired only when present; with no database URL the engine
334
- block is omitted so the schema's own `datasource` applies.
335
-
336
- | Option | Default | Effect |
337
- | ------ | ------- | ------ |
338
- | `views` | `false` | Configure the `views` feature (`prisma/views`) |
339
- | `typedSql` | `false` | Configure the `typedSql` preview feature (`prisma/sql`) |
340
-
341
- ### `applyPrismaDevEnv`
190
+ | `process.env.X` | `no-restricted-properties` |
191
+ | `import { env } from "node:process"` and similar | `ts/no-restricted-imports` |
192
+ | Auto-import paths, app-side Prisma client | `ts/no-restricted-imports` |
193
+ | Zod import style and methods, `publicProcedure` | `no-restricted-syntax` |
342
194
 
343
- The same subpath exports `applyPrismaDevEnv(env?)`, the companion for local development.
344
- It seeds `DIRECT_DATABASE_URL`, `DATABASE_URL`, and `SHADOW_DATABASE_URL` with localhost
345
- defaults, but only when they are unset, so a real `.env` or shell value always wins. It
346
- writes to the environment record you pass and defaults to `process.env`, which is a
347
- deliberate side effect.
348
-
349
- You need it because once a `prisma.config.ts` exists, Prisma stops auto-loading `.env`.
350
- A schema that reads `env("DATABASE_URL")` then fails with no environment at all. Call it
351
- in the config file, after loading dotenv, and the Prisma CLI runs against local postgres
352
- with zero setup:
195
+ ### Prisma
353
196
 
354
197
  ```ts
355
198
  // prisma.config.ts
356
199
  import "dotenv/config";
357
- import { defineConfig } from "prisma/config";
358
200
  import { applyPrismaDevEnv, buildPrismaConfig } from "@sidebase/base-config/prisma";
201
+ import { defineConfig } from "prisma/config";
359
202
 
360
- applyPrismaDevEnv();
361
-
362
- export default defineConfig(buildPrismaConfig());
363
- ```
364
-
365
- `buildPrismaConfig` alone is enough wherever the connection variables are already set,
366
- which is every deployed environment and CI. Add `applyPrismaDevEnv` when you want
367
- `prisma migrate dev` and friends to work on a fresh checkout without a `.env` file. The
368
- defaults are direct = local postgres, pooled = the direct URL plus `pgbouncer=1`, and
369
- shadow = the direct URL on a `/prisma-shadow` database.
370
-
371
- The URL helpers never log a connection string and never put one in an error message, so a
372
- password cannot reach your terminal or CI output through them. Keep that property if you
373
- wrap them.
374
-
375
- `prisma` / `@prisma/client` are peer dependencies (`^6.19`).
376
-
377
- > Prisma 7 is out of scope for now. v7 changes the config shape (`env()` helper,
378
- > `directUrl` becomes `url`, no `package.json` prisma block); a `buildPrismaConfig` v7
379
- > variant is a deliberate later bump.
380
-
381
- ## Supply-chain cooldown (`pnpm-workspace.yaml`)
382
-
383
- The base preset fully owns `pnpm-workspace.yaml`, pnpm's settings file. It ships two
384
- supply-chain controls, both on by default for every repo:
385
-
386
- ```yaml
387
- packages: []
388
- minimumReleaseAge: 10080 # 7 days, in minutes
389
- minimumReleaseAgeExclude:
390
- - "@sidebase/*"
391
- onlyBuiltDependencies: # the postinstall-script allowlist
392
- - "@prisma/client"
393
- - "esbuild"
394
- - "prisma"
395
- ```
396
-
397
- `minimumReleaseAge` refuses to resolve a version until it has been published for
398
- 7 days, so a compromised release has time to be caught and yanked before the fleet
399
- installs it. It needs pnpm 10.16 or newer and gates *fresh resolution only*, so
400
- `pnpm install --frozen-lockfile` replays the lockfile untouched and CI is unaffected.
401
-
402
- The pnpm baseline shipped to consumers is 10.28.1 (`presets/*/preset.json`). This repo's
403
- own `packageManager` pin tracks separately and is usually newer; the two are independent
404
- by design, not drift.
405
-
406
- Exclusions match **package names, not dependency trees**: an excluded package's own
407
- dependencies still face the cooldown, and pnpm resolves the newest *eligible* older
408
- version in range. Only when nothing in range is old enough does resolution fail, with
409
- a misleading `ERR_PNPM_NO_MATCHING_VERSION` ([pnpm#9998](https://github.com/pnpm/pnpm/issues/9998)).
410
-
411
- `@sidebase/*` is exempt: the first-party scope covering both this payload and the
412
- `streamctl` CLI, published by the org itself, so a payload release reaches the fleet
413
- the same day instead of waiting out its own cooldown.
414
-
415
- **`packages: []` is load-bearing.** pnpm defaults it to `**` whenever a
416
- `pnpm-workspace.yaml` exists, which would silently promote every nested `package.json`
417
- (test fixtures, examples) to a workspace project and break `--frozen-lockfile`.
418
-
419
- `onlyBuiltDependencies` is the install-script allowlist. Everything else installs
420
- with its lifecycle scripts blocked. Add extras through the config, **not** through
421
- `pnpm approve-builds` (which writes to the managed file and is reverted on the next
422
- sync):
423
-
424
- ```ts
425
- export default {
426
- // ...
427
- security: { minimumReleaseAge: "10080" }, // minutes; "0" disables the cooldown
428
- pnpm: { onlyBuiltDependencies: ["sharp"] }, // on top of the baked-in baseline
429
- };
430
- ```
431
-
432
- | Option | Default | Effect |
433
- | ------ | ------- | ------ |
434
- | `security.minimumReleaseAge` | `"10080"` | Minutes a release must age before pnpm installs it. A string, because the manifest's `configKeys` has no number type, same as the `versions.*` pins. |
435
- | `pnpm.onlyBuiltDependencies` | `[]` | Extra packages allowed to run install scripts, appended below the baked-in baseline |
436
-
437
- Because the file is `full`-owned, a repo that already has a `pnpm-workspace.yaml`
438
- raises a one-time adoption conflict on the first sync; reconcile it with
439
- `streamctl sync --interactive`.
440
-
441
- **Adopting it discards your existing keys.** The rendered file is the payload's, so an
442
- existing `onlyBuiltDependencies` allowlist and `ignoredBuiltDependencies` are both replaced
443
- by the baked-in baseline, and **nothing warns about it**. Most affected packages ship a
444
- prebuilt native binary and keep working either way, so the practical breakage is limited to
445
- architectures with no prebuild and to postinstalls doing essential non-native work. Capture
446
- the old list before you adopt and re-add it via `pnpm.onlyBuiltDependencies` above. A repo
447
- that genuinely needs its own `packages:` list (a real monorepo) or its own exclude list
448
- should opt the file out entirely instead:
449
-
450
- ```ts
451
- files: { "pnpm-workspace.yaml": "off" },
452
- ```
453
-
454
- ## CI knobs
455
-
456
- The `nuxt-app` preset ships a managed `.github/workflows/ci.yml`. Its `lint-typecheck` and
457
- `build` jobs always run; the rest is knobs:
458
-
459
- | Knob | Default | Effect |
460
- | ---- | ------- | ------ |
461
- | `ci.unitTests` | `false` | Add the `test` job (`pnpm test`) |
462
- | `ci.e2e` | `false` | Add the `e2e` job, with a health-checked postgres service |
463
- | `ci.aptPackages` | `[]` | System packages `apt-get`-installed in the `test` and `e2e` jobs |
464
-
465
- `ci.aptPackages` is the CI counterpart to `docker.aptPackages` below: extra system packages
466
- on top of what the runner image already carries, for a binary the suite needs and
467
- `ubuntu-latest` does not ship. The motivating case is `libxml2-utils`, for `xmllint`.
468
-
469
- It applies to the `test` and `e2e` jobs only, never `lint-typecheck` or `build`. The step
470
- installs directly after `checkout`, before `pnpm install`, so a package needed by an install
471
- lifecycle script is covered as well as one needed by the tests. Leave it unset and the step
472
- is skipped entirely: it renders with a guard on the value being non-empty, so the default
473
- costs a no-op step rather than an `apt-get` call.
474
-
475
- ```ts
476
- export default {
477
- // ...
478
- ci: { unitTests: true, e2e: true, aptPackages: ["libxml2-utils"] },
479
- };
480
- ```
481
-
482
- Package names only. Every element is rejected if it contains a shell metacharacter, and the
483
- joined string is then re-validated against the Debian package-name character set (lowercase,
484
- digits, `+`, `-`, `.`). Both checks exist because the rendered command deliberately leaves
485
- the value unquoted, which is how several packages word-split into one `apt-get` call.
486
-
487
- ## Dockerfile knobs
488
-
489
- The `nuxt-app` preset ships a managed `Dockerfile`. Beyond `docker.aptPackages`
490
- (an `openssl`-plus allowlist), six `docker.*` knobs inject raw text at fixed
491
- positions so every fleet repo can express its own build without opting the file
492
- out:
493
-
494
- | Knob | Position | Default |
495
- | ---- | -------- | ------- |
496
- | `docker.preInstall` | build stage, before `pnpm install` | `""` |
497
- | `docker.buildArgs` | build stage, before `COPY . .` | `""` |
498
- | `docker.buildSteps` | the build-command block | `nuxi prepare` / `prisma generate` / `run build` |
499
- | `docker.finalStage` | final stage, before `CMD` | `""` |
500
- | `docker.prismaRuntime` | final stage | copy the schema + install the `migrate deploy` CLI |
501
- | `docker.startCommand` | inside `CMD` | `prisma migrate deploy` then the node server |
502
-
503
- A seventh knob is different in kind: a validated version, not raw text.
504
-
505
- | Knob | Position | Default |
506
- | ---- | -------- | ------- |
507
- | `docker.prismaVersion` | the `ARG PRISMA_VERSION=` default inside `docker.prismaRuntime` | derived from the repo's `prisma` pin (its range floor); the baseline only when no full `x.y.z` pin exists |
508
-
509
- It is pattern-checked (`x.y.z`, optional prerelease), so unlike the six raw-text
510
- knobs it cannot inject arbitrary Dockerfile content. `--build-arg PRISMA_VERSION=`
511
- still overrides at build time.
512
-
513
- ```ts
514
- export default {
515
- // ...
516
- docker: {
517
- preInstall: "COPY ./vendor ./vendor", // vendored dep needed at install time
518
- buildSteps: "RUN pnpm nuxi prepare\nRUN pnpm run build", // a repo with no Prisma
519
- prismaRuntime: "", // drop Prisma entirely
520
- startCommand: "node .output/server/index.mjs",
521
- },
522
- };
203
+ applyPrismaDevEnv(); // optional: localhost defaults for unset DB URLs
204
+ export default defineConfig(buildPrismaConfig({ views: true, typedSql: true }));
523
205
  ```
524
206
 
525
- > **These six values land verbatim.** They are `string`, not `string[]`, so order
526
- > is preserved (a sorted array would scramble ordered `RUN` steps), and no
527
- > shell-metacharacter check runs on them. The trust level is exactly that of
528
- > `files: { "Dockerfile": "off" }`, which any consumer can already set: whoever
529
- > can edit `streamctl.config.ts` can already replace the whole file. The
530
- > `.output` ownership stays FIXED outside every knob. The container itself runs
531
- > as **root**: a fixed `USER node` broke every deployment that overrides the image
532
- > CMD with a root-requiring command, so a repo that wants an unprivileged runtime
533
- > opts in with `docker.finalStage: "USER node"`.
534
-
535
- Setting a knob replaces its default outright, it does not append. So a `docker.buildSteps`
536
- override must restate every build command you still want, and `docker.prismaRuntime: ""`
537
- removes the Prisma runtime block entirely. The three knobs that default to real content
538
- (`buildSteps`, `prismaRuntime`, `startCommand`) are the ones where this matters; the other
539
- three default to `""`.
540
-
541
- **`prismaRuntime` and `startCommand` are coupled.** Override one and you must override the
542
- other. `prismaRuntime` is what installs the Prisma CLI into the final stage, and the
543
- DEFAULT `startCommand` is `npm exec prisma migrate deploy && node ...`. Setting only
544
- `prismaRuntime: ""` therefore produces an image whose `CMD` invokes a CLI that is no longer
545
- installed, and the container fails to start. The example above overrides both, which is why
546
- it is safe to copy; a single-knob change is not. Drop the `migrate deploy` half of
547
- `startCommand` at the same time.
548
-
549
- Global installs in `docker.preInstall` must use `npm i -g <tool>`, not
550
- `pnpm add -g`: the image sets no `PNPM_HOME`, so pnpm's global bin dir is
551
- undefined.
552
-
553
- ## Editor settings (`.vscode/settings.json`)
554
-
555
- The base preset owns its own keys in `.vscode/settings.json` and leaves the rest of the file
556
- to the project. Among them is i18n-ally, configured for the fleet's standard setup:
557
- `@nuxtjs/i18n` with flat dotted keys under `i18n/locales`.
558
-
559
- Everything the payload does not write is yours and survives a sync untouched: your
560
- `editor.fontSize`, your `files.exclude`, any unrelated key at all. Six of those are named in
561
- the manifest as project-owned on top of that (`editor.fontSize`, `editor.rulers`,
562
- `editor.formatOnSave`, `files.exclude`, `search.exclude`, `files.watcherExclude`), which
563
- keeps them yours even if a later payload version starts setting them. Only the keys the
564
- payload writes are reverted, and they are the ones below.
565
-
566
- | Knob | Default | Effect |
567
- | ---- | ------- | ------ |
568
- | `editor.i18nSourceLanguage` | `"de"` | The locale i18n-ally translates FROM (`i18n-ally.sourceLanguage`). Payload-enforced; see below |
207
+ - `buildPrismaConfig` reads `DATABASE_URL`, `DIRECT_DATABASE_URL` and `SHADOW_DATABASE_URL`.
208
+ Migrations use `DIRECT_DATABASE_URL`, or `DATABASE_URL` with the pooler parameters removed.
209
+ `views` and `typedSql` enable those Prisma features.
210
+ - `applyPrismaDevEnv` sets localhost defaults for the URLs that are unset, so
211
+ `prisma migrate dev` works on a fresh checkout without a `.env`.
569
212
 
570
- ```ts
571
- export default {
572
- // ...
573
- editor: { i18nSourceLanguage: "en" },
574
- };
575
- ```
213
+ Prisma 6.19 only; `prisma` and `@prisma/client` are optional peer dependencies.
576
214
 
577
- **If your repo authors its source strings in English, set this.** The source language is
578
- payload-owned, so leaving it unset does not mean "keep what I have". Every sync writes
579
- `"de"` into the file, over the top of an `"en"` you put there by hand. Nothing warns, and
580
- `streamctl check` stays green afterwards, because the value it finds is the value the payload
581
- intends. Setting the knob is the only thing that makes `"en"` survive a sync.
215
+ ### tsconfig
582
216
 
583
- `i18n-ally.localesPaths` and `i18n-ally.keystyle` are deliberately NOT configurable: the
584
- locales path and the flat key style are fleet-wide conventions, so they are enforced rather
585
- than offered. Editing either one in your own `.vscode/settings.json` is reverted on the next
586
- sync. A repo that genuinely needs a different layout should opt the file out
587
- (`files: { ".vscode/settings.json": "off" }`) rather than fight the merge.
217
+ The managed `tsconfig.json` extends `@sidebase/base-config/tsconfig.base`, the shared
218
+ strict config, before Nuxt's generated one. You can extend it directly as well.
588
219
 
589
- ## Upgrade-PR workflow
220
+ ## Troubleshooting
590
221
 
591
- The base preset ships `.github/workflows/streamctl-upgrade.yml`, a scheduled workflow
592
- that opens a PR when a payload update is available. It is an `enabledBy`-gated managed
593
- file, off by default. It only lands once a repo opts in.
222
+ - **`No version profile could be detected`:** the repo is not on Nuxt 4. Nuxt 3 is not
223
+ supported.
224
+ - **`pnpm approve-builds` changes disappear:** use `pnpm.onlyBuiltDependencies`.
225
+ - **`.editorconfig` settings ignored after the first sync:** your old content sits above the
226
+ managed block. Move it below.
227
+ - **`eslint.config.ts` fails to load:** add `jiti` to devDependencies.
228
+ - **Repo with private npm dependencies:** the upgrade workflow installs without registry
229
+ credentials. Opt it out and use your own.
594
230
 
595
- Enable it in `streamctl.config.ts`, then `streamctl sync`:
231
+ ## Developing this package
596
232
 
597
- ```ts
598
- export default {
599
- // ...
600
- automation: {
601
- upgradePr: true,
602
- // allowWorkflowUpdates: true,
603
- // prTokenSecret: "STREAMCTL_PR_TOKEN",
604
- },
605
- // optional: version pins, default to the payload's baseline. `node` feeds
606
- // ci.yml + the Dockerfile; `pnpm` feeds the Dockerfile only (CI pins pnpm
607
- // via package.json#packageManager):
608
- // versions: { node: "24.13.0", pnpm: "10.28.1" },
609
- };
610
- ```
611
-
612
- By default, the workflow uses `GITHUB_TOKEN` and omits changes under
613
- `.github/workflows/**`. If an update contains workflow changes, it opens a draft PR
614
- with every other safe change and lists the omitted files and completion commands.
615
-
616
- Set `automation.allowWorkflowUpdates` to `true` to include workflow changes. Add a
617
- `STREAMCTL_PR_TOKEN` repository secret with `Contents`, `Pull requests`, and
618
- `Workflows` write access. Change `automation.prTokenSecret` when the secret has another
619
- name. A fine-grained PAT or GitHub App token works. The built-in `GITHUB_TOKEN` cannot
620
- receive `Workflows` permission.
621
-
622
- | Knob | Default | Effect |
623
- | ---- | ------- | ------ |
624
- | `automation.upgradePr` | `false` | Sync the scheduled upgrade workflow |
625
- | `automation.allowWorkflowUpdates` | `false` | Include `.github/workflows/**` changes in upgrade PRs |
626
- | `automation.prTokenSecret` | `"STREAMCTL_PR_TOKEN"` | Secret used when workflow updates are enabled |
627
-
628
- The upgrade job runs `streamctl check` and every available lint, typecheck, test, and
629
- build script before opening a ready PR. PRs created with `GITHUB_TOKEN` do not trigger
630
- their own `pull_request` workflows, so these inline checks are required. The external
631
- token allows the new PR to trigger them normally.
632
-
633
- The workflow handles CLI results as follows:
634
-
635
- - `check` exit 0: stop without a PR.
636
- - `check` exit 3: open or update one drift issue.
637
- - `check` exit 4: attempt the upgrade.
638
- - `upgrade` exit 0: validate and open a ready PR, or a draft when workflow files were omitted.
639
- - `upgrade` exit 2: inspect conflicts using the pre-upgrade status.
640
- - Any other exit: fail the workflow.
641
-
642
- A conflict is safe to apply when the same path was previously `full` managed and
643
- `in-sync`. The workflow retries those generated replacements with `--force`. Any new,
644
- modified, malformed, or structurally conflicting path produces a draft PR instead.
645
- Workflow changes also produce a draft unless explicitly allowed. Drafts contain every
646
- safe change, keep conflicting and omitted workflow files at their current content, list
647
- what remains, and give the commands needed to finish the upgrade. When a malformed or
648
- structurally invalid file blocks `--force`, the draft only updates the payload version
649
- and lockfile.
650
-
651
- Diagnostic JSON stays under the runner's temporary directory. Upgrade branches include
652
- the target version, so a later release does not overwrite an older open PR.
653
-
654
- The payload is public npm. A repo with other private dependencies must opt this workflow
655
- out and provide its own install authentication.
656
-
657
- ### Action pinning policy
658
-
659
- Every action in every managed workflow and CI job fragment is pinned by full commit SHA,
660
- with the version tag in a trailing comment. Moving tags can be re-pointed, so tags are
661
- never trusted, secrets or not. `test/workflow-pins.test.ts` sweeps all preset
662
- templates and fails on any `uses:` that is not a 40-hex SHA.
663
-
664
- The shipped pins track `actions/checkout` v6, `actions/setup-node` v6, and
665
- `pnpm/action-setup` v6. These majors run on the node24 action runtime, which needs a
666
- GitHub Actions runner `>= v2.327.1`. GitHub-hosted `ubuntu-latest` is far past that; only
667
- a repo that adds its own **self-hosted** runner needs to keep it above that floor.
233
+ ```sh
234
+ pnpm lint && pnpm typecheck && pnpm test
235
+ pnpm validate:presets # manifests and cross-file invariants
236
+ pnpm e2e:dry-run # pack, install into a scratch consumer, sync and check
237
+ pnpm e2e:docker # build and start the rendered Dockerfile (needs Docker)
238
+ ```
239
+
240
+ - The payload lives in `presets/` (`base`, and `nuxt-app` on top of it). The manifests in
241
+ `preset.json` define files, options and version baselines.
242
+ - Tests check contracts derived from the manifests, so changing a preset file rarely needs
243
+ a test change.
244
+ - Actions in managed workflows are pinned by commit SHA; the tests enforce it.
245
+ - Requires streamctl >= 0.3.0 (manifest schema version 3).
246
+ - Releases: [`docs/release.md`](docs/release.md). Agent conventions: [`AGENTS.md`](AGENTS.md).