@sidebase/base-config 0.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/LICENSE +21 -0
- package/README.md +554 -0
- package/dist/config.d.mts +189 -0
- package/dist/config.d.ts +189 -0
- package/dist/config.mjs +46 -0
- package/dist/eslint/index.d.mts +55 -0
- package/dist/eslint/index.d.ts +55 -0
- package/dist/eslint/index.mjs +278 -0
- package/dist/prisma/index.d.mts +74 -0
- package/dist/prisma/index.d.ts +74 -0
- package/dist/prisma/index.mjs +61 -0
- package/dist/shared/base-config.CuUhyvQo.d.mts +47 -0
- package/dist/shared/base-config.CuUhyvQo.d.ts +47 -0
- package/docs/migration.md +764 -0
- package/package.json +94 -0
- package/presets/base/AGENTS.md +31 -0
- package/presets/base/CLAUDE.md +3 -0
- package/presets/base/dockerignore +18 -0
- package/presets/base/editorconfig +12 -0
- package/presets/base/github/workflows/streamctl-upgrade.yml +130 -0
- package/presets/base/gitignore +19 -0
- package/presets/base/oxlintrc.json +31 -0
- package/presets/base/pnpm-workspace.yaml +27 -0
- package/presets/base/preset.json +38 -0
- package/presets/base/templates/pnpm/only-built-dependency.yml +1 -0
- package/presets/base/tsconfig.json +3 -0
- package/presets/base/vscode/extensions.json +8 -0
- package/presets/base/vscode/settings.json +43 -0
- package/presets/config.template.ts +18 -0
- package/presets/manifest.json +17 -0
- package/presets/nuxt-app/Dockerfile +63 -0
- package/presets/nuxt-app/eslint.config.ts +5 -0
- package/presets/nuxt-app/github/workflows/ci.yml +66 -0
- package/presets/nuxt-app/github/workflows/pr-preview-cleanup.yml +41 -0
- package/presets/nuxt-app/preset.json +67 -0
- package/presets/nuxt-app/prisma.config.ts +6 -0
- package/presets/nuxt-app/templates/ci/e2e-job.yml +39 -0
- package/presets/nuxt-app/templates/ci/test-job.yml +13 -0
- package/presets/nuxt-app/tsconfig.json +6 -0
- package/tsconfig.base.json +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 sidebase
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
# @sidebase/base-config
|
|
2
|
+
|
|
3
|
+
Shared `@sidebase` Nuxt configuration, delivered over two channels in one package:
|
|
4
|
+
|
|
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.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
For the npm channel, install the package plus what the subpaths you use require:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
pnpm add -D @sidebase/base-config eslint jiti
|
|
16
|
+
```
|
|
17
|
+
|
|
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, but
|
|
24
|
+
since nothing declares it, **pnpm emits no missing-peer warning if you leave it out**.
|
|
25
|
+
This README is 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.
|
|
64
|
+
|
|
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.
|
|
69
|
+
|
|
70
|
+
#### Reconciled `scripts.lint`
|
|
71
|
+
|
|
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.
|
|
75
|
+
|
|
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:
|
|
81
|
+
|
|
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:
|
|
96
|
+
|
|
97
|
+
```ini
|
|
98
|
+
# BEGIN streamctl MANAGED BLOCK editorconfig
|
|
99
|
+
[*]
|
|
100
|
+
indent_size = 2
|
|
101
|
+
# END streamctl MANAGED BLOCK editorconfig
|
|
102
|
+
|
|
103
|
+
# your override wins because it comes after the managed block
|
|
104
|
+
[*]
|
|
105
|
+
indent_size = 4
|
|
106
|
+
```
|
|
107
|
+
|
|
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.)
|
|
111
|
+
|
|
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.
|
|
118
|
+
|
|
119
|
+
If you instead want to stop managing a file entirely, opt it out per-path:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
files: { ".editorconfig": "off" },
|
|
123
|
+
```
|
|
124
|
+
|
|
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
|
|
131
|
+
|
|
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.
|
|
136
|
+
|
|
137
|
+
The root `streamctl.config.ts` this payload documents needs **streamctl >= 0.2.0**, so the
|
|
138
|
+
pin and the `pnpm-workspace.yaml` c12 override both move when that release lands. See
|
|
139
|
+
"Config location" below.
|
|
140
|
+
|
|
141
|
+
## Typed config
|
|
142
|
+
|
|
143
|
+
The root export `@sidebase/base-config` and the subpath `@sidebase/base-config/config`
|
|
144
|
+
are the same module. Both provide `defineNuxtBaseConfig` and the `NuxtBaseConfig` type,
|
|
145
|
+
which is what makes a `streamctl.config.ts` typed. Every consuming repo has this file,
|
|
146
|
+
and `streamctl init` scaffolds it importing from the root export:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
// streamctl.config.ts
|
|
150
|
+
import { defineNuxtBaseConfig } from "@sidebase/base-config";
|
|
151
|
+
|
|
152
|
+
export default defineNuxtBaseConfig({
|
|
153
|
+
package: "@sidebase/base-config",
|
|
154
|
+
base: "nuxt-app",
|
|
155
|
+
version: "0.1.0",
|
|
156
|
+
profile: "nuxt-4",
|
|
157
|
+
ci: { unitTests: true },
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Config location
|
|
162
|
+
|
|
163
|
+
streamctl >= 0.2.0 reads `streamctl.config.ts` from the repo root, and `init` scaffolds it
|
|
164
|
+
there. The older `.streamctl/config.ts` is still resolved, so an existing repo keeps working
|
|
165
|
+
untouched and can move the file whenever it likes.
|
|
166
|
+
|
|
167
|
+
Move it and nothing else changes: the contents are identical, and the CLI warns rather
|
|
168
|
+
than guesses if both exist, naming the root file as the one it read. Keeping the legacy
|
|
169
|
+
path is fine too. The one thing not to do is leave both in place, since only the root file
|
|
170
|
+
is read and edits to the other will look like they do nothing.
|
|
171
|
+
|
|
172
|
+
If you set `ignoresTypeAware` yourself, list whichever path your repo uses. The default
|
|
173
|
+
covers both.
|
|
174
|
+
|
|
175
|
+
`defineNuxtBaseConfig` returns the config unchanged. Its job is typed editor inference
|
|
176
|
+
for this payload's knobs: completion and type checking on `ci`, `versions`, `docker`,
|
|
177
|
+
`automation`, `security`, `pnpm`, and the whole `eslint` option surface, plus `profile`
|
|
178
|
+
narrowed to the profile names that actually exist. Without it the object is an untyped
|
|
179
|
+
literal, so a typo such as `ci: { unitTest: true }` stays silent in your editor.
|
|
180
|
+
|
|
181
|
+
The CLI shape-checks the config against the manifest when it loads it, so a sync still
|
|
182
|
+
works without the helper. The helper is what moves that feedback into your editor.
|
|
183
|
+
|
|
184
|
+
Import `NuxtBaseConfig` directly when you need the type on its own:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import type { NuxtBaseConfig } from "@sidebase/base-config/config";
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Shared tsconfig
|
|
191
|
+
|
|
192
|
+
`@sidebase/base-config/tsconfig.base` is the shared strict TypeScript base, exported as a
|
|
193
|
+
JSON file so a `tsconfig.json` can extend it by package name. The `nuxt-app` preset ships
|
|
194
|
+
a managed `tsconfig.json` that chains it with Nuxt's generated config:
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"extends": [
|
|
199
|
+
"@sidebase/base-config/tsconfig.base",
|
|
200
|
+
"./.nuxt/tsconfig.json"
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Order matters: the shared base comes first, so Nuxt's generated config applies over it.
|
|
206
|
+
|
|
207
|
+
## Naming convention
|
|
208
|
+
|
|
209
|
+
Two rules cover every exported name, so you can predict them:
|
|
210
|
+
|
|
211
|
+
- Types describing **this payload's config shape** are `NuxtBase*`: `NuxtBaseConfig`,
|
|
212
|
+
`NuxtBaseCiConfig`, `defineNuxtBaseConfig`, and the `NUXT_BASE_*_KEYS` lists. The prefix
|
|
213
|
+
is doing real work here, because these types describe the config of this specific
|
|
214
|
+
payload rather than anything generic.
|
|
215
|
+
- **Everything else is domain-named with no package prefix**: `buildPrismaConfig`,
|
|
216
|
+
`applyPrismaDevEnv`, `createSidebaseEslint`, `resolveEslintOptions`. The import path
|
|
217
|
+
already states where a symbol comes from, so repeating it in the identifier adds
|
|
218
|
+
nothing.
|
|
219
|
+
|
|
220
|
+
`streamctl` is reserved for the CLI (`@sidebase/streamctl`) and the files it owns:
|
|
221
|
+
`streamctl.config.ts` in the repo root, and the legacy `.streamctl/` directory it still
|
|
222
|
+
reads. Nothing exported from this package carries that name, because nothing
|
|
223
|
+
exported from this package comes from the CLI. The ESLint layers this package builds are
|
|
224
|
+
named `sidebase/*` (`sidebase/defaults`, `sidebase/console`, and so on), which is what you
|
|
225
|
+
see in lint output and what you target if you `.override()` a layer by name. They are
|
|
226
|
+
built at runtime by this package; the CLI never sees them.
|
|
227
|
+
|
|
228
|
+
## ESLint factory
|
|
229
|
+
|
|
230
|
+
`@sidebase/base-config/eslint` exports `createSidebaseEslint(options?)`, which wraps
|
|
231
|
+
[`@antfu/eslint-config`](https://github.com/antfu/eslint-config) (a dependency of this
|
|
232
|
+
package, so **do not add `@antfu/eslint-config` directly**) and returns its
|
|
233
|
+
`FlatConfigComposer`, so you chain `.append()` / `.override()` for local rules:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
// eslint.config.ts
|
|
237
|
+
import { createSidebaseEslint } from "@sidebase/base-config/eslint";
|
|
238
|
+
|
|
239
|
+
export default createSidebaseEslint({ zod: "full", trpcGuard: true })
|
|
240
|
+
.append({ rules: { "vue/multi-word-component-names": "off" } });
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Options
|
|
244
|
+
|
|
245
|
+
| Option | Default | Effect |
|
|
246
|
+
| ------ | ------- | ------ |
|
|
247
|
+
| `zod` | `"none"` | `"full"` = ban `.extend()/.merge()/.passthrough()` + enforce `import * as z`; `"import-style"` = import style only; `"none"` = off |
|
|
248
|
+
| `console` | `"error"` | `"error"` bans all `console.*`; an array is the allow-list (e.g. `["warn", "error"]`) |
|
|
249
|
+
| `trpcGuard` | `false` | Ban `publicProcedure` in `server/trpc/routers/**` |
|
|
250
|
+
| `prismaImportGuard` | `false` | Ban `~~/prisma/` client imports on the app side |
|
|
251
|
+
| `typeDefStyle` | `"interface"` | `ts/consistent-type-definitions` |
|
|
252
|
+
| `autoImportPaths` | `["utils/", "composables/", "~~/shared/types/"]` | Paths banned from direct import (Nuxt auto-imports) |
|
|
253
|
+
| `autoImportTypeOnly` | `[]` | Subset of `autoImportPaths` where `import type { ... }` stays allowed (value imports still banned) |
|
|
254
|
+
| `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 |
|
|
255
|
+
| `testFilePattern` | `["**/*.{test,spec}.{ts,tsx}", "**/*.stories.{ts,tsx}"]` | Test/spec/story globs where the `process.env` + auto-import bans are relaxed |
|
|
256
|
+
|
|
257
|
+
Rare per-repo rules belong in your local `.append(...)`, not in the option surface.
|
|
258
|
+
|
|
259
|
+
### Rule IDs & inline suppression
|
|
260
|
+
|
|
261
|
+
The custom bans map to these ESLint rule IDs. Use the exact ID in an
|
|
262
|
+
`// eslint-disable-next-line ...` directive to suppress one line:
|
|
263
|
+
|
|
264
|
+
| Ban | Rule ID |
|
|
265
|
+
| --- | ------- |
|
|
266
|
+
| Direct `process.env` access, as a member expression (`process.env.X`, `const { env } = process`) | `no-restricted-properties` |
|
|
267
|
+
| 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` |
|
|
268
|
+
| Auto-import path imports (`autoImportPaths`) | `ts/no-restricted-imports` |
|
|
269
|
+
| App-side Prisma client import (`prismaImportGuard`) | `ts/no-restricted-imports` |
|
|
270
|
+
| Zod named-`z` import style (`zod: "import-style"`/`"full"`) | `no-restricted-syntax` |
|
|
271
|
+
| Zod `.extend()/.merge()/.passthrough()` (`zod: "full"`) | `no-restricted-syntax` |
|
|
272
|
+
| `publicProcedure` in routers (`trpcGuard`) | `no-restricted-syntax` |
|
|
273
|
+
|
|
274
|
+
"Re-export" is not a figure of speech: a barrel file doing `export * from "node:process"`
|
|
275
|
+
reports, as do `export { env } from "node:process"` and `export { default as p } from
|
|
276
|
+
"node:process"`. The one shape that does **not** report is a bare side-effect import with
|
|
277
|
+
no bindings, `import "node:process"` -- it cannot reach `env`, so there is nothing to ban.
|
|
278
|
+
|
|
279
|
+
The `process.env` ban is enforced by **two** rules, and which one reports depends on the
|
|
280
|
+
shape, so suppressing the wrong one fails twice over: the directive does not suppress
|
|
281
|
+
anything, and ESLint additionally warns that it was unused.
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
// eslint-disable-next-line no-restricted-properties
|
|
285
|
+
import { env } from 'node:process'
|
|
286
|
+
|
|
287
|
+
1:1 warning Unused eslint-disable directive (no problems were reported from 'no-restricted-properties')
|
|
288
|
+
2:10 error 'env' import from 'node:process' is restricted... ts/no-restricted-imports
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### Requirements
|
|
292
|
+
|
|
293
|
+
`eslint` is a peer dependency (`^10.5.0`), so consumers of `/eslint` must supply ESLint
|
|
294
|
+
themselves. The peer range matches the version baseline, so `streamctl` version-sync
|
|
295
|
+
keeps a repo's ESLint in step with what this package is tested against.
|
|
296
|
+
|
|
297
|
+
Flat config only. To load a TypeScript `eslint.config.ts` on Node, consumers add
|
|
298
|
+
`jiti` (`>= 2.2.0`) as a devDependency: ESLint has no native TS loader, and ESLint 10
|
|
299
|
+
rejects jiti below 2.2.0. Writing `eslint.config.mjs` instead drops the jiti requirement
|
|
300
|
+
entirely, as does running on Deno or Bun, which import TypeScript directly.
|
|
301
|
+
|
|
302
|
+
#### Type-aware linting prerequisite
|
|
303
|
+
|
|
304
|
+
Type-aware linting is gated behind `LINT_TYPEAWARE=true` (off by default), so this only
|
|
305
|
+
matters on the opt-in / CI path. When type-aware lint is on, the Prisma client and Nuxt types must be
|
|
306
|
+
generated **before** `lint` runs, otherwise type-aware rules fail on missing generated types. This is
|
|
307
|
+
satisfied by ordering, not a preflight check:
|
|
308
|
+
|
|
309
|
+
- the managed `ci.yml` runs `prisma generate` (and `nuxi prepare`) before `pnpm lint`, and
|
|
310
|
+
- `scripts.postinstall: "nuxt prepare"` regenerates Nuxt types on install.
|
|
311
|
+
|
|
312
|
+
So a standard `pnpm install` + the managed CI ordering covers the prerequisite; no separate check is run.
|
|
313
|
+
|
|
314
|
+
## Prisma factory
|
|
315
|
+
|
|
316
|
+
`@sidebase/base-config/prisma` exports `buildPrismaConfig({ views?, typedSql? })`,
|
|
317
|
+
which returns a Prisma 6.19 config object you wrap in `defineConfig`:
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
// prisma.config.ts
|
|
321
|
+
import { defineConfig } from "prisma/config";
|
|
322
|
+
import { buildPrismaConfig } from "@sidebase/base-config/prisma";
|
|
323
|
+
|
|
324
|
+
export default defineConfig(buildPrismaConfig({ views: true, typedSql: true }));
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
It reads `DATABASE_URL` / `DIRECT_DATABASE_URL` / `SHADOW_DATABASE_URL` from the
|
|
328
|
+
environment (Pattern B): the schema engine gets a pgbouncer-free direct
|
|
329
|
+
connection: `DIRECT_DATABASE_URL` if set, otherwise `DATABASE_URL` with
|
|
330
|
+
pooler-only query params (`pgbouncer`, `connection_limit`, and so on) stripped via `ufo`.
|
|
331
|
+
`SHADOW_DATABASE_URL` is wired only when present; with no database URL the engine
|
|
332
|
+
block is omitted so the schema's own `datasource` applies.
|
|
333
|
+
|
|
334
|
+
| Option | Default | Effect |
|
|
335
|
+
| ------ | ------- | ------ |
|
|
336
|
+
| `views` | `false` | Configure the `views` feature (`prisma/views`) |
|
|
337
|
+
| `typedSql` | `false` | Configure the `typedSql` preview feature (`prisma/sql`) |
|
|
338
|
+
|
|
339
|
+
### `applyPrismaDevEnv`
|
|
340
|
+
|
|
341
|
+
The same subpath exports `applyPrismaDevEnv(env?)`, the companion for local development.
|
|
342
|
+
It seeds `DIRECT_DATABASE_URL`, `DATABASE_URL`, and `SHADOW_DATABASE_URL` with localhost
|
|
343
|
+
defaults, but only when they are unset, so a real `.env` or shell value always wins. It
|
|
344
|
+
writes to the environment record you pass and defaults to `process.env`, which is a
|
|
345
|
+
deliberate side effect.
|
|
346
|
+
|
|
347
|
+
You need it because once a `prisma.config.ts` exists, Prisma stops auto-loading `.env`.
|
|
348
|
+
A schema that reads `env("DATABASE_URL")` then fails with no environment at all. Call it
|
|
349
|
+
in the config file, after loading dotenv, and the Prisma CLI runs against local postgres
|
|
350
|
+
with zero setup:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
// prisma.config.ts
|
|
354
|
+
import "dotenv/config";
|
|
355
|
+
import { defineConfig } from "prisma/config";
|
|
356
|
+
import { applyPrismaDevEnv, buildPrismaConfig } from "@sidebase/base-config/prisma";
|
|
357
|
+
|
|
358
|
+
applyPrismaDevEnv();
|
|
359
|
+
|
|
360
|
+
export default defineConfig(buildPrismaConfig());
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
`buildPrismaConfig` alone is enough wherever the connection variables are already set,
|
|
364
|
+
which is every deployed environment and CI. Add `applyPrismaDevEnv` when you want
|
|
365
|
+
`prisma migrate dev` and friends to work on a fresh checkout without a `.env` file. The
|
|
366
|
+
defaults are direct = local postgres, pooled = the direct URL plus `pgbouncer=1`, and
|
|
367
|
+
shadow = the direct URL on a `/prisma-shadow` database.
|
|
368
|
+
|
|
369
|
+
The URL helpers never log a connection string and never put one in an error message, so a
|
|
370
|
+
password cannot reach your terminal or CI output through them. Keep that property if you
|
|
371
|
+
wrap them.
|
|
372
|
+
|
|
373
|
+
`prisma` / `@prisma/client` are peer dependencies (`^6.19`).
|
|
374
|
+
|
|
375
|
+
> Prisma 7 is out of scope for now. v7 changes the config shape (`env()` helper,
|
|
376
|
+
> `directUrl` becomes `url`, no `package.json` prisma block); a `buildPrismaConfig` v7
|
|
377
|
+
> variant is a deliberate later bump.
|
|
378
|
+
|
|
379
|
+
## Supply-chain cooldown (`pnpm-workspace.yaml`)
|
|
380
|
+
|
|
381
|
+
The base preset fully owns `pnpm-workspace.yaml`, pnpm's settings file. It ships two
|
|
382
|
+
supply-chain controls, both on by default for every repo:
|
|
383
|
+
|
|
384
|
+
```yaml
|
|
385
|
+
packages: []
|
|
386
|
+
minimumReleaseAge: 10080 # 7 days, in minutes
|
|
387
|
+
minimumReleaseAgeExclude:
|
|
388
|
+
- "@sidebase/*"
|
|
389
|
+
onlyBuiltDependencies: # the postinstall-script allowlist
|
|
390
|
+
- "@prisma/client"
|
|
391
|
+
- "esbuild"
|
|
392
|
+
- "prisma"
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
`minimumReleaseAge` refuses to resolve a version until it has been published for
|
|
396
|
+
7 days, so a compromised release has time to be caught and yanked before the fleet
|
|
397
|
+
installs it. It needs pnpm 10.16 or newer and gates *fresh resolution only*, so
|
|
398
|
+
`pnpm install --frozen-lockfile` replays the lockfile untouched and CI is unaffected.
|
|
399
|
+
|
|
400
|
+
The pnpm baseline shipped to consumers is 10.28.1 (`presets/*/preset.json`). This repo's
|
|
401
|
+
own `packageManager` pin tracks separately and is usually newer; the two are independent
|
|
402
|
+
by design, not drift.
|
|
403
|
+
|
|
404
|
+
Exclusions match **package names, not dependency trees**: an excluded package's own
|
|
405
|
+
dependencies still face the cooldown, and pnpm resolves the newest *eligible* older
|
|
406
|
+
version in range. Only when nothing in range is old enough does resolution fail, with
|
|
407
|
+
a misleading `ERR_PNPM_NO_MATCHING_VERSION` ([pnpm#9998](https://github.com/pnpm/pnpm/issues/9998)).
|
|
408
|
+
|
|
409
|
+
`@sidebase/*` is exempt: the first-party scope covering both this payload and the
|
|
410
|
+
`streamctl` CLI, published by the org itself, so a payload release reaches the fleet
|
|
411
|
+
the same day instead of waiting out its own cooldown.
|
|
412
|
+
|
|
413
|
+
**`packages: []` is load-bearing.** pnpm defaults it to `**` whenever a
|
|
414
|
+
`pnpm-workspace.yaml` exists, which would silently promote every nested `package.json`
|
|
415
|
+
(test fixtures, examples) to a workspace project and break `--frozen-lockfile`.
|
|
416
|
+
|
|
417
|
+
`onlyBuiltDependencies` is the install-script allowlist. Everything else installs
|
|
418
|
+
with its lifecycle scripts blocked. Add extras through the config, **not** through
|
|
419
|
+
`pnpm approve-builds` (which writes to the managed file and is reverted on the next
|
|
420
|
+
sync):
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
export default {
|
|
424
|
+
// ...
|
|
425
|
+
security: { minimumReleaseAge: "10080" }, // minutes; "0" disables the cooldown
|
|
426
|
+
pnpm: { onlyBuiltDependencies: ["sharp"] }, // on top of the baked-in baseline
|
|
427
|
+
};
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
| Option | Default | Effect |
|
|
431
|
+
| ------ | ------- | ------ |
|
|
432
|
+
| `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. |
|
|
433
|
+
| `pnpm.onlyBuiltDependencies` | `[]` | Extra packages allowed to run install scripts, appended below the baked-in baseline |
|
|
434
|
+
|
|
435
|
+
Because the file is `full`-owned, a repo that already has a `pnpm-workspace.yaml`
|
|
436
|
+
raises a one-time adoption conflict on the first sync; reconcile it with
|
|
437
|
+
`streamctl sync --interactive`.
|
|
438
|
+
|
|
439
|
+
**Adopting it discards your existing keys.** The rendered file is the payload's, so an
|
|
440
|
+
existing `onlyBuiltDependencies` allowlist and `ignoredBuiltDependencies` are both replaced
|
|
441
|
+
by the baked-in baseline, and **nothing warns about it**. Most affected packages ship a
|
|
442
|
+
prebuilt native binary and keep working either way, so the practical breakage is limited to
|
|
443
|
+
architectures with no prebuild and to postinstalls doing essential non-native work. Capture
|
|
444
|
+
the old list before you adopt and re-add it via `pnpm.onlyBuiltDependencies` above. A repo that genuinely needs its own `packages:` list
|
|
445
|
+
(a real monorepo) or its own exclude list should opt the file out entirely instead:
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
files: { "pnpm-workspace.yaml": "off" },
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Dockerfile knobs
|
|
452
|
+
|
|
453
|
+
The `nuxt-app` preset ships a managed `Dockerfile`. Beyond `docker.aptPackages`
|
|
454
|
+
(an `openssl`-plus allowlist), six `docker.*` knobs inject raw text at fixed
|
|
455
|
+
positions so every fleet repo can express its own build without opting the file
|
|
456
|
+
out:
|
|
457
|
+
|
|
458
|
+
| Knob | Position | Default |
|
|
459
|
+
| ---- | -------- | ------- |
|
|
460
|
+
| `docker.preInstall` | build stage, before `pnpm install` | `""` |
|
|
461
|
+
| `docker.buildArgs` | build stage, before `COPY . .` | `""` |
|
|
462
|
+
| `docker.buildSteps` | the build-command block | `nuxi prepare` / `prisma generate` / `run build` |
|
|
463
|
+
| `docker.finalStage` | final stage, before `CMD` | `""` |
|
|
464
|
+
| `docker.prismaRuntime` | final stage | copy the schema + install the `migrate deploy` CLI |
|
|
465
|
+
| `docker.startCommand` | inside `CMD` | `prisma migrate deploy` then the node server |
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
export default {
|
|
469
|
+
// ...
|
|
470
|
+
docker: {
|
|
471
|
+
preInstall: "COPY ./vendor ./vendor", // vendored dep needed at install time
|
|
472
|
+
buildSteps: "RUN pnpm nuxi prepare\nRUN pnpm run build", // a repo with no Prisma
|
|
473
|
+
prismaRuntime: "", // drop Prisma entirely
|
|
474
|
+
startCommand: "node .output/server/index.mjs",
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
> **These six values land verbatim.** They are `string`, not `string[]`, so order
|
|
480
|
+
> is preserved (a sorted array would scramble ordered `RUN` steps), and no
|
|
481
|
+
> shell-metacharacter check runs on them. The trust level is exactly that of
|
|
482
|
+
> `files: { "Dockerfile": "off" }`, which any consumer can already set: whoever
|
|
483
|
+
> can edit `streamctl.config.ts` can already replace the whole file. The
|
|
484
|
+
> `USER node` switch and the `.output` ownership stay FIXED outside every knob,
|
|
485
|
+
> so an override cannot re-root the container or drop the runtime user.
|
|
486
|
+
|
|
487
|
+
Setting a knob replaces its default outright, it does not append. So a `docker.buildSteps`
|
|
488
|
+
override must restate every build command you still want, and `docker.prismaRuntime: ""`
|
|
489
|
+
removes the Prisma runtime block entirely. The three knobs that default to real content
|
|
490
|
+
(`buildSteps`, `prismaRuntime`, `startCommand`) are the ones where this matters; the other
|
|
491
|
+
three default to `""`.
|
|
492
|
+
|
|
493
|
+
**`prismaRuntime` and `startCommand` are coupled -- override one and you must override the
|
|
494
|
+
other.** `prismaRuntime` is what installs the Prisma CLI into the final stage, and the
|
|
495
|
+
DEFAULT `startCommand` is `npm exec prisma migrate deploy && node ...`. Setting only
|
|
496
|
+
`prismaRuntime: ""` therefore produces an image whose `CMD` invokes a CLI that is no longer
|
|
497
|
+
installed, and the container fails to start. The example above overrides both, which is why
|
|
498
|
+
it is safe to copy; a single-knob change is not. Drop the `migrate deploy` half of
|
|
499
|
+
`startCommand` at the same time.
|
|
500
|
+
|
|
501
|
+
Global installs in `docker.preInstall` must use `npm i -g <tool>`, not
|
|
502
|
+
`pnpm add -g`: the image sets no `PNPM_HOME`, so pnpm's global bin dir is
|
|
503
|
+
undefined.
|
|
504
|
+
|
|
505
|
+
## Upgrade-PR workflow
|
|
506
|
+
|
|
507
|
+
The base preset ships `.github/workflows/streamctl-upgrade.yml`, a scheduled workflow
|
|
508
|
+
that opens a PR when a payload update is available. It is an `enabledBy`-gated managed
|
|
509
|
+
file, off by default: it only lands once a repo opts in, and it is inert until the
|
|
510
|
+
required token exists.
|
|
511
|
+
|
|
512
|
+
Enable it in `streamctl.config.ts`, then `streamctl sync`:
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
export default {
|
|
516
|
+
// ...
|
|
517
|
+
automation: { upgradePr: true },
|
|
518
|
+
// optional: version pins, default to the payload's baseline. `node` feeds
|
|
519
|
+
// ci.yml + the Dockerfile; `pnpm` feeds the Dockerfile only (CI pins pnpm
|
|
520
|
+
// via package.json#packageManager):
|
|
521
|
+
// versions: { node: "24.13.0", pnpm: "10.28.1" },
|
|
522
|
+
};
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Required secrets:
|
|
526
|
+
|
|
527
|
+
| Secret | Purpose |
|
|
528
|
+
| ------ | ------- |
|
|
529
|
+
| `STREAMCTL_PR_TOKEN` | A GitHub App installation token or machine-account PAT used to open the PR. **Not** the default `GITHUB_TOKEN`: a PR opened with `GITHUB_TOKEN` does not trigger `pull_request` workflows, so the repo's own `check` gate would never run on the bot PR. |
|
|
530
|
+
|
|
531
|
+
That is the only one, because the payload is public npm, so the install step needs no registry
|
|
532
|
+
token. A repo whose *other* dependencies are private must opt this full-owned workflow
|
|
533
|
+
out and wire its own auth.
|
|
534
|
+
|
|
535
|
+
The workflow branches on the CLI exit codes: `check` exit 4 opens a PR; exit 0 stops;
|
|
536
|
+
exit 3 (pre-existing drift) opens a drift issue instead. A clean `upgrade` (exit 0)
|
|
537
|
+
produces a reconcile PR; a conflicted `upgrade` (exit 2, rolled back) produces a
|
|
538
|
+
plan-only PR labelled `needs-interactive-upgrade` for a human to finish with
|
|
539
|
+
`streamctl sync --interactive`.
|
|
540
|
+
|
|
541
|
+
> The org bot identity is still being decided. Until the runbook lands, leave the
|
|
542
|
+
> workflow disabled. Design only; nothing is enabled anywhere.
|
|
543
|
+
|
|
544
|
+
### Action pinning policy
|
|
545
|
+
|
|
546
|
+
Every action in every managed workflow and CI job fragment is pinned by full commit
|
|
547
|
+
SHA (with the version tag in a trailing comment), since moving tags can be re-pointed, so
|
|
548
|
+
tags are never trusted, secrets or not. `test/workflow-pins.test.ts` sweeps all preset
|
|
549
|
+
templates and fails on any `uses:` that is not a 40-hex SHA.
|
|
550
|
+
|
|
551
|
+
The shipped pins track `actions/checkout` v6, `actions/setup-node` v6, and
|
|
552
|
+
`pnpm/action-setup` v6. These majors run on the node24 action runtime, which needs a
|
|
553
|
+
GitHub Actions runner `>= v2.327.1`. GitHub-hosted `ubuntu-latest` is far past that; only
|
|
554
|
+
a repo that adds its own **self-hosted** runner needs to keep it above that floor.
|