@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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import antfu from '@antfu/eslint-config';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_TEST_FILE_PATTERN = ["**/*.{test,spec}.{ts,tsx}", "**/*.stories.{ts,tsx}"];
|
|
4
|
+
function isNonEmpty(value) {
|
|
5
|
+
return value.length > 0;
|
|
6
|
+
}
|
|
7
|
+
function resolveConsole(value) {
|
|
8
|
+
if (value === void 0 || value === "error" || !isNonEmpty(value)) {
|
|
9
|
+
return "error";
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function isTypeAware() {
|
|
14
|
+
return process.env.LINT_TYPEAWARE === "true";
|
|
15
|
+
}
|
|
16
|
+
function resolveEslintOptions(options = {}) {
|
|
17
|
+
return {
|
|
18
|
+
zod: options.zod ?? "none",
|
|
19
|
+
console: resolveConsole(options.console),
|
|
20
|
+
trpcGuard: options.trpcGuard ?? false,
|
|
21
|
+
prismaImportGuard: options.prismaImportGuard ?? false,
|
|
22
|
+
typeDefStyle: options.typeDefStyle ?? "interface",
|
|
23
|
+
autoImportPaths: options.autoImportPaths ?? ["utils/", "composables/", "~~/shared/types/"],
|
|
24
|
+
autoImportTypeOnly: options.autoImportTypeOnly ?? [],
|
|
25
|
+
// Both config locations stay listed: streamctl >= 0.2.0 scaffolds
|
|
26
|
+
// `streamctl.config.ts` in the repo root, but `.streamctl/config.ts` remains a
|
|
27
|
+
// supported fallback, so a repo can be on either while the fleet migrates.
|
|
28
|
+
ignoresTypeAware: options.ignoresTypeAware ?? ["prisma.config.ts", "eslint.config.ts", "streamctl.config.ts", ".streamctl/**/*.ts"],
|
|
29
|
+
testFilePattern: options.testFilePattern ?? DEFAULT_TEST_FILE_PATTERN,
|
|
30
|
+
typeAware: isTypeAware()
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TRPC_ROUTERS_GLOB = "**/server/trpc/routers/**/*.{ts,js}";
|
|
35
|
+
const ZOD_DEPRECATED_METHODS = ["extend", "merge", "passthrough"];
|
|
36
|
+
const APP_FILES = ["**/*.{ts,vue}"];
|
|
37
|
+
const APP_IGNORES = ["**/*.d.ts"];
|
|
38
|
+
const PROCESS_ENV_PROPERTY = {
|
|
39
|
+
object: "process",
|
|
40
|
+
property: "env",
|
|
41
|
+
message: "Do not use process.env directly. Define and import env via publicEnv.ts (app/server) or serverEnv.ts (server-only)."
|
|
42
|
+
};
|
|
43
|
+
const PUBLIC_PROCEDURE_SELECTOR = {
|
|
44
|
+
selector: "Identifier[name='publicProcedure']",
|
|
45
|
+
message: "publicProcedure is banned in tRPC routers. Use an authenticated procedure."
|
|
46
|
+
};
|
|
47
|
+
function zodMethodBans(options) {
|
|
48
|
+
if (options.zod !== "full") {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return ZOD_DEPRECATED_METHODS.map((method) => ({
|
|
52
|
+
selector: `CallExpression[callee.property.name='${method}']`,
|
|
53
|
+
message: `Zod's .${method}() is banned. Prefer the supported schema-composition alternative.`
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
const ZOD_SOURCE = "/^zod(\\/.*)?$/";
|
|
57
|
+
const ZOD_CORE_SOURCE = "/^zod(\\/v4|\\/mini|\\/v4-mini)?$/";
|
|
58
|
+
const ZOD_IMPORT_STYLE_MESSAGE = "Use `import * as z from 'zod'` instead of a named `z` import.";
|
|
59
|
+
function zodImportStyleBans(options) {
|
|
60
|
+
if (options.zod !== "full" && options.zod !== "import-style") {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
return [
|
|
64
|
+
// `import { z } from "zod"` / `import { z as zed } from "zod"`: match on the
|
|
65
|
+
// imported (source) name, so an alias doesn't dodge the ban. Value imports only.
|
|
66
|
+
{ selector: `ImportDeclaration[source.value=${ZOD_SOURCE}][importKind!='type'] ImportSpecifier[imported.name='z'][importKind!='type']`, message: ZOD_IMPORT_STYLE_MESSAGE },
|
|
67
|
+
// `import z from "zod"`: default import, core entry points only (locale/util
|
|
68
|
+
// default imports like `zod/v4/locales/de` are legitimate and must pass).
|
|
69
|
+
{ selector: `ImportDeclaration[source.value=${ZOD_CORE_SOURCE}][importKind!='type'] ImportDefaultSpecifier`, message: ZOD_IMPORT_STYLE_MESSAGE },
|
|
70
|
+
// `export { z } from "zod"` / `export { z as ... } from "zod"`: re-export of z.
|
|
71
|
+
{ selector: `ExportNamedDeclaration[source.value=${ZOD_SOURCE}][exportKind!='type'] ExportSpecifier[local.name='z'][exportKind!='type']`, message: ZOD_IMPORT_STYLE_MESSAGE }
|
|
72
|
+
];
|
|
73
|
+
}
|
|
74
|
+
function anchoredGlobs(rawPath) {
|
|
75
|
+
const path = rawPath.replace(/\/+$/, "");
|
|
76
|
+
if (/^(?:[~@#/]|\.{1,2}\/)/.test(path)) {
|
|
77
|
+
return [`${path}/**`];
|
|
78
|
+
}
|
|
79
|
+
return [`~/${path}/**`, `~~/${path}/**`, `@/${path}/**`];
|
|
80
|
+
}
|
|
81
|
+
function importRestrictions(options) {
|
|
82
|
+
const paths = [
|
|
83
|
+
{ name: "node:process", importNames: ["env", "default"], message: PROCESS_ENV_PROPERTY.message, allowTypeImports: true },
|
|
84
|
+
{ name: "process", importNames: ["env", "default"], message: PROCESS_ENV_PROPERTY.message, allowTypeImports: true }
|
|
85
|
+
];
|
|
86
|
+
const typeOnly = new Set(options.autoImportTypeOnly);
|
|
87
|
+
const patterns = options.autoImportPaths.map((path) => ({
|
|
88
|
+
group: anchoredGlobs(path),
|
|
89
|
+
message: `Do not import from "${path.replace(/\/+$/, "")}" directly. It is provided by Nuxt auto-imports.`,
|
|
90
|
+
...typeOnly.has(path) ? { allowTypeImports: true } : {}
|
|
91
|
+
}));
|
|
92
|
+
return { paths, patterns };
|
|
93
|
+
}
|
|
94
|
+
function buildEslintLayers(options) {
|
|
95
|
+
const configs = [];
|
|
96
|
+
configs.push({
|
|
97
|
+
name: "sidebase/defaults",
|
|
98
|
+
rules: {
|
|
99
|
+
"vue/return-in-computed-property": "off",
|
|
100
|
+
"curly": ["error", "all"],
|
|
101
|
+
"style/brace-style": ["error", "1tbs"],
|
|
102
|
+
"comma-dangle": ["error", {
|
|
103
|
+
arrays: "always-multiline",
|
|
104
|
+
objects: "always-multiline",
|
|
105
|
+
functions: "always-multiline",
|
|
106
|
+
exports: "always-multiline",
|
|
107
|
+
imports: "always-multiline"
|
|
108
|
+
}],
|
|
109
|
+
"space-infix-ops": ["error"],
|
|
110
|
+
// `node/prefer-global/process` is NOT set here: antfu 7.4.3 registers
|
|
111
|
+
// eslint-plugin-n (`node`) only inside its own files-scoped object, so setting
|
|
112
|
+
// the rule in this plugin-less layer makes ESLint 10 ABORT. It is instead
|
|
113
|
+
// neutralized to `"off"` via `.override("antfu/node/rules")` in
|
|
114
|
+
// `createSidebaseEslint`, inside antfu's node-scoped object where the plugin
|
|
115
|
+
// is registered (see the comment there). The `no-restricted-properties`
|
|
116
|
+
// process.env ban below carries the real guard.
|
|
117
|
+
"no-unused-expressions": ["error"],
|
|
118
|
+
"no-param-reassign": ["error"],
|
|
119
|
+
"no-fallthrough": ["error"],
|
|
120
|
+
"require-await": ["error"],
|
|
121
|
+
"ts/no-non-null-assertion": ["error"],
|
|
122
|
+
// antfu leaves `ts/no-explicit-any` off; this baseline enforces it, matching the
|
|
123
|
+
// shipped `AGENTS.md` ban on `any`. Breaking for consumers (Q5, Option A).
|
|
124
|
+
"ts/no-explicit-any": "error",
|
|
125
|
+
// Type-aware rules antfu enables that this baseline turns off.
|
|
126
|
+
"ts/no-misused-promises": "off",
|
|
127
|
+
"ts/no-unsafe-call": "off",
|
|
128
|
+
"ts/strict-boolean-expressions": "off",
|
|
129
|
+
"ts/promise-function-async": "off",
|
|
130
|
+
// Newer antfu rules outside the established house baseline. `prefer-static-regex`
|
|
131
|
+
// is a perf nudge the consuming apps never adopted; `custom-event-name-casing`
|
|
132
|
+
// is actively wrong for naive-ui's kebab `update:*` v-model events. Off by
|
|
133
|
+
// default; a repo can opt back in via `.append()`.
|
|
134
|
+
"e18e/prefer-static-regex": "off",
|
|
135
|
+
"vue/custom-event-name-casing": "off"
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
configs.push({
|
|
139
|
+
name: "sidebase/type-definitions",
|
|
140
|
+
rules: { "ts/consistent-type-definitions": ["error", options.typeDefStyle] }
|
|
141
|
+
});
|
|
142
|
+
configs.push({
|
|
143
|
+
name: "sidebase/console",
|
|
144
|
+
// Source files only: antfu's markdown processor lints fenced code
|
|
145
|
+
// blocks as virtual `*.md/*` files, so an un-scoped `no-console` fires inside
|
|
146
|
+
// README/doc examples. This carve-out leaves real `.ts`/`.vue`/... enforcement intact.
|
|
147
|
+
ignores: ["**/*.md/**"],
|
|
148
|
+
rules: {
|
|
149
|
+
// Always pass an explicit options object: a severity-only override would
|
|
150
|
+
// retain antfu's `{ allow: ["warn", "error"] }`. `"error"` (hard-ban)
|
|
151
|
+
// emits `{}` (no-console's schema rejects an empty `allow` array), which
|
|
152
|
+
// replaces those options so every `console.*` is banned. An empty
|
|
153
|
+
// allow-list was normalized to `"error"` in resolveEslintOptions, so the
|
|
154
|
+
// array branch here is always a non-empty allow-list (no cast needed).
|
|
155
|
+
"no-console": options.console === "error" ? ["error", {}] : ["error", { allow: options.console }]
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
const { paths, patterns } = importRestrictions(options);
|
|
159
|
+
const appRules = {
|
|
160
|
+
"no-restricted-properties": ["error", PROCESS_ENV_PROPERTY]
|
|
161
|
+
};
|
|
162
|
+
if (paths.length > 0 || patterns.length > 0) {
|
|
163
|
+
appRules["ts/no-restricted-imports"] = ["error", { paths, patterns }];
|
|
164
|
+
}
|
|
165
|
+
configs.push({
|
|
166
|
+
name: "sidebase/restricted-app",
|
|
167
|
+
files: APP_FILES,
|
|
168
|
+
ignores: APP_IGNORES,
|
|
169
|
+
rules: appRules
|
|
170
|
+
});
|
|
171
|
+
const zodSyntax = [...zodImportStyleBans(options), ...zodMethodBans(options)];
|
|
172
|
+
if (zodSyntax.length > 0) {
|
|
173
|
+
configs.push({
|
|
174
|
+
name: "sidebase/restricted-syntax",
|
|
175
|
+
// Source files only: antfu's markdown processor lints fenced code blocks as
|
|
176
|
+
// virtual `*.md/*` files, so an un-scoped syntax ban fires inside a doc example
|
|
177
|
+
// using a banned zod method. Mirrors the sidebase/console carve-out.
|
|
178
|
+
ignores: ["**/*.md/**"],
|
|
179
|
+
rules: { "no-restricted-syntax": ["error", ...zodSyntax] }
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
if (options.trpcGuard) {
|
|
183
|
+
configs.push({
|
|
184
|
+
name: "sidebase/trpc-guard",
|
|
185
|
+
files: [TRPC_ROUTERS_GLOB],
|
|
186
|
+
rules: { "no-restricted-syntax": ["error", ...zodSyntax, PUBLIC_PROCEDURE_SELECTOR] }
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (options.prismaImportGuard) {
|
|
190
|
+
const prismaPattern = {
|
|
191
|
+
group: ["~~/prisma/*", "~~/prisma/**"],
|
|
192
|
+
message: "Do not import the Prisma client on the app side. Use a server-only module."
|
|
193
|
+
};
|
|
194
|
+
configs.push({
|
|
195
|
+
name: "sidebase/prisma-import-guard",
|
|
196
|
+
files: APP_FILES,
|
|
197
|
+
ignores: [...APP_IGNORES, "**/server/**"],
|
|
198
|
+
rules: { "ts/no-restricted-imports": ["error", { paths, patterns: [...patterns, prismaPattern] }] }
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
if (options.testFilePattern.length > 0) {
|
|
202
|
+
configs.push({
|
|
203
|
+
name: "sidebase/test-overrides",
|
|
204
|
+
files: options.testFilePattern,
|
|
205
|
+
rules: {
|
|
206
|
+
"no-restricted-properties": "off",
|
|
207
|
+
"no-restricted-imports": "off",
|
|
208
|
+
"ts/no-restricted-imports": "off"
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return configs;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const ESLINT_OPTION_KEYS = Object.keys({
|
|
216
|
+
zod: 0,
|
|
217
|
+
console: 0,
|
|
218
|
+
trpcGuard: 0,
|
|
219
|
+
prismaImportGuard: 0,
|
|
220
|
+
typeDefStyle: 0,
|
|
221
|
+
autoImportPaths: 0,
|
|
222
|
+
autoImportTypeOnly: 0,
|
|
223
|
+
ignoresTypeAware: 0,
|
|
224
|
+
testFilePattern: 0
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
function createSidebaseEslint(options = {}) {
|
|
228
|
+
const resolved = resolveEslintOptions(options);
|
|
229
|
+
return antfu(
|
|
230
|
+
{
|
|
231
|
+
type: "app",
|
|
232
|
+
// `local.*` files and directories are personal, git-ignored scratch space
|
|
233
|
+
// and are never linted.
|
|
234
|
+
ignores: ["**/local.*", "**/local.*/**"],
|
|
235
|
+
// Org house style: 2-space indent, single quotes, Vue on,
|
|
236
|
+
// JSON/YAML linting off (handled elsewhere).
|
|
237
|
+
stylistic: { indent: 2, quotes: "single" },
|
|
238
|
+
vue: true,
|
|
239
|
+
jsonc: false,
|
|
240
|
+
yaml: false,
|
|
241
|
+
// Type-aware is gated behind `LINT_TYPEAWARE`; when on, point at the repo
|
|
242
|
+
// tsconfig and exclude the configured paths from the type-aware program.
|
|
243
|
+
typescript: resolved.typeAware ? { tsconfigPath: "tsconfig.json", ignoresTypeAware: resolved.ignoresTypeAware } : true
|
|
244
|
+
},
|
|
245
|
+
...buildEslintLayers(resolved)
|
|
246
|
+
).override("antfu/node/rules", {
|
|
247
|
+
// antfu 7.4.3's node layer defaults `node/prefer-global/process` to
|
|
248
|
+
// `["error", "never"]` (demands `require("process")`), which flags EVERY legit
|
|
249
|
+
// global `process` use in an ESM Nuxt app (env modules, server plugins, loggers).
|
|
250
|
+
// Leaving the rule unset is not enough: antfu's `never` then wins in consumer
|
|
251
|
+
// resolution. Neutralize it HERE, inside antfu's own node-scoped config where the
|
|
252
|
+
// `node` plugin IS registered; setting it in a plugin-less object makes ESLint 10
|
|
253
|
+
// abort. `"off"`, not `["error", "always"]`, because `"always"` would wrongly flag
|
|
254
|
+
// the generated Prisma client's `import * as process from 'node:process'`.
|
|
255
|
+
//
|
|
256
|
+
// Note what this does NOT mean. prefer-global is not merely redundant here:
|
|
257
|
+
// `["error", "always"]` would also close a real hole, because
|
|
258
|
+
// `no-restricted-properties` keys on the `process.env` MEMBER EXPRESSION, and any
|
|
259
|
+
// import that renames the binding erases the shape it matches. Measured against
|
|
260
|
+
// this factory, the property ban alone catches `process.env.X`,
|
|
261
|
+
// `const { env } = process`, and a default import still named `process`, but NOT
|
|
262
|
+
// `import { env }`, `import { env as e }`, `import * as proc`, or
|
|
263
|
+
// `import proc from "node:process"`. Those are closed at the import site instead,
|
|
264
|
+
// by the `node:process` / `process` entries in `paths` (see `importRestrictions` in
|
|
265
|
+
// rules.ts): narrower than flipping prefer-global, and free of the Prisma-client
|
|
266
|
+
// false positive.
|
|
267
|
+
//
|
|
268
|
+
// With `importNames: ["env", "default"]` that is now the COMPLETE set, not a sample.
|
|
269
|
+
// Every way to reach `process.env` is covered by one rule or the other: the member
|
|
270
|
+
// expression and an un-renamed default import at the USAGE site, and the named,
|
|
271
|
+
// renamed-named, namespace and renamed-default imports at the IMPORT site. The
|
|
272
|
+
// un-renamed default being caught at the usage site rather than by the import ban is
|
|
273
|
+
// the asymmetry that hid the renamed-default hole for two rounds of review.
|
|
274
|
+
rules: { "node/prefer-global/process": "off" }
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export { ESLINT_OPTION_KEYS, buildEslintLayers, createSidebaseEslint, isTypeAware, resolveEslintOptions };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { PrismaConfig } from 'prisma/config';
|
|
2
|
+
|
|
3
|
+
/** The schema-engine datasource override (Prisma 6.19 classic engine). */
|
|
4
|
+
interface SchemaEngineDatasource {
|
|
5
|
+
url: string;
|
|
6
|
+
shadowDatabaseUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Seed the three Prisma connection env vars with localhost dev defaults when UNSET.
|
|
10
|
+
* This is a deliberate SIDE EFFECT on `env` (defaults to `process.env`). Necessary because
|
|
11
|
+
* once a `prisma.config.ts` exists Prisma stops auto-loading `.env`, so a schema that
|
|
12
|
+
* reads `env("DATABASE_URL")` / `env("DIRECT_DATABASE_URL")` would fail at rest.
|
|
13
|
+
*
|
|
14
|
+
* Call it (after `import "dotenv/config"`) in the config file so the Prisma CLI runs
|
|
15
|
+
* with ZERO env; a real `.env`/shell value always wins. `buildPrismaConfig` then reads
|
|
16
|
+
* the seeded env and derives its direct-connection + shadow datasource.
|
|
17
|
+
*
|
|
18
|
+
* TWO paths, not one:
|
|
19
|
+
* - **Bare, or an explicit `DIRECT_DATABASE_URL`.** Seeds all three. Defaults:
|
|
20
|
+
* direct = local postgres, pooled = direct + `pgbouncer=1`, shadow = direct with a
|
|
21
|
+
* `/prisma-shadow` database.
|
|
22
|
+
* - **A real `DATABASE_URL` and no direct URL.** Seeds ONLY `DIRECT_DATABASE_URL`,
|
|
23
|
+
* derived from the pooled URL, and returns early. No shadow is seeded on purpose:
|
|
24
|
+
* deriving one would name a `/prisma-shadow` database on the production host, which
|
|
25
|
+
* Prisma CREATEs and DROPs.
|
|
26
|
+
*
|
|
27
|
+
* An empty-string value counts as unset throughout; `docker-compose` produces `""` for an
|
|
28
|
+
* undefined variable, and treating it as present is how the localhost default used to
|
|
29
|
+
* reach production.
|
|
30
|
+
*/
|
|
31
|
+
declare function applyPrismaDevEnv(env?: Record<string, string | undefined>): void;
|
|
32
|
+
/**
|
|
33
|
+
* Strip pooler-only query params from a connection string so the schema engine
|
|
34
|
+
* gets a usable direct connection. Other params (sslmode, schema, ...) are kept.
|
|
35
|
+
*/
|
|
36
|
+
declare function stripPgbouncerParams(url: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the schema engine's datasource (Pattern B):
|
|
39
|
+
* - direct URL = `DIRECT_DATABASE_URL`, else `DATABASE_URL` with pgbouncer params stripped;
|
|
40
|
+
* - shadow DB = `SHADOW_DATABASE_URL` when present.
|
|
41
|
+
*
|
|
42
|
+
* Returns `undefined` when neither a direct nor a pooled URL is set, so the
|
|
43
|
+
* config falls back to the schema's own `datasource` block (e.g. during
|
|
44
|
+
* `prisma generate` without a database).
|
|
45
|
+
*/
|
|
46
|
+
declare function resolveSchemaEngineDatasource(env: Record<string, string | undefined>): SchemaEngineDatasource | undefined;
|
|
47
|
+
|
|
48
|
+
/** Alias for Prisma 6.19's `defineConfig` input. */
|
|
49
|
+
type PrismaConfigInput = PrismaConfig;
|
|
50
|
+
interface BuildPrismaConfigOptions {
|
|
51
|
+
/** Enable the `views` config feature (view SQL under `prisma/views`). */
|
|
52
|
+
views?: boolean;
|
|
53
|
+
/** Enable the `typedSql` preview feature (queries under `prisma/sql`). */
|
|
54
|
+
typedSql?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build the shared `@sidebase` Prisma config (Prisma 6.19 "Pattern B").
|
|
58
|
+
* Wrap the result in `defineConfig`:
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* // prisma.config.ts
|
|
62
|
+
* import { defineConfig } from "prisma/config";
|
|
63
|
+
* import { buildPrismaConfig } from "@sidebase/base-config/prisma";
|
|
64
|
+
* export default defineConfig(buildPrismaConfig({ views: true, typedSql: true }));
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* Reads `DATABASE_URL` / `DIRECT_DATABASE_URL` / `SHADOW_DATABASE_URL` (the
|
|
68
|
+
* schema engine gets a pgbouncer-free direct connection). Prisma 7's changed
|
|
69
|
+
* config shape is out of scope for v1.
|
|
70
|
+
*/
|
|
71
|
+
declare function buildPrismaConfig(options?: BuildPrismaConfigOptions, env?: Record<string, string | undefined>): PrismaConfig;
|
|
72
|
+
|
|
73
|
+
export { applyPrismaDevEnv, buildPrismaConfig, resolveSchemaEngineDatasource, stripPgbouncerParams };
|
|
74
|
+
export type { BuildPrismaConfigOptions, PrismaConfigInput, SchemaEngineDatasource };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { PrismaConfig } from 'prisma/config';
|
|
2
|
+
|
|
3
|
+
/** The schema-engine datasource override (Prisma 6.19 classic engine). */
|
|
4
|
+
interface SchemaEngineDatasource {
|
|
5
|
+
url: string;
|
|
6
|
+
shadowDatabaseUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Seed the three Prisma connection env vars with localhost dev defaults when UNSET.
|
|
10
|
+
* This is a deliberate SIDE EFFECT on `env` (defaults to `process.env`). Necessary because
|
|
11
|
+
* once a `prisma.config.ts` exists Prisma stops auto-loading `.env`, so a schema that
|
|
12
|
+
* reads `env("DATABASE_URL")` / `env("DIRECT_DATABASE_URL")` would fail at rest.
|
|
13
|
+
*
|
|
14
|
+
* Call it (after `import "dotenv/config"`) in the config file so the Prisma CLI runs
|
|
15
|
+
* with ZERO env; a real `.env`/shell value always wins. `buildPrismaConfig` then reads
|
|
16
|
+
* the seeded env and derives its direct-connection + shadow datasource.
|
|
17
|
+
*
|
|
18
|
+
* TWO paths, not one:
|
|
19
|
+
* - **Bare, or an explicit `DIRECT_DATABASE_URL`.** Seeds all three. Defaults:
|
|
20
|
+
* direct = local postgres, pooled = direct + `pgbouncer=1`, shadow = direct with a
|
|
21
|
+
* `/prisma-shadow` database.
|
|
22
|
+
* - **A real `DATABASE_URL` and no direct URL.** Seeds ONLY `DIRECT_DATABASE_URL`,
|
|
23
|
+
* derived from the pooled URL, and returns early. No shadow is seeded on purpose:
|
|
24
|
+
* deriving one would name a `/prisma-shadow` database on the production host, which
|
|
25
|
+
* Prisma CREATEs and DROPs.
|
|
26
|
+
*
|
|
27
|
+
* An empty-string value counts as unset throughout; `docker-compose` produces `""` for an
|
|
28
|
+
* undefined variable, and treating it as present is how the localhost default used to
|
|
29
|
+
* reach production.
|
|
30
|
+
*/
|
|
31
|
+
declare function applyPrismaDevEnv(env?: Record<string, string | undefined>): void;
|
|
32
|
+
/**
|
|
33
|
+
* Strip pooler-only query params from a connection string so the schema engine
|
|
34
|
+
* gets a usable direct connection. Other params (sslmode, schema, ...) are kept.
|
|
35
|
+
*/
|
|
36
|
+
declare function stripPgbouncerParams(url: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the schema engine's datasource (Pattern B):
|
|
39
|
+
* - direct URL = `DIRECT_DATABASE_URL`, else `DATABASE_URL` with pgbouncer params stripped;
|
|
40
|
+
* - shadow DB = `SHADOW_DATABASE_URL` when present.
|
|
41
|
+
*
|
|
42
|
+
* Returns `undefined` when neither a direct nor a pooled URL is set, so the
|
|
43
|
+
* config falls back to the schema's own `datasource` block (e.g. during
|
|
44
|
+
* `prisma generate` without a database).
|
|
45
|
+
*/
|
|
46
|
+
declare function resolveSchemaEngineDatasource(env: Record<string, string | undefined>): SchemaEngineDatasource | undefined;
|
|
47
|
+
|
|
48
|
+
/** Alias for Prisma 6.19's `defineConfig` input. */
|
|
49
|
+
type PrismaConfigInput = PrismaConfig;
|
|
50
|
+
interface BuildPrismaConfigOptions {
|
|
51
|
+
/** Enable the `views` config feature (view SQL under `prisma/views`). */
|
|
52
|
+
views?: boolean;
|
|
53
|
+
/** Enable the `typedSql` preview feature (queries under `prisma/sql`). */
|
|
54
|
+
typedSql?: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build the shared `@sidebase` Prisma config (Prisma 6.19 "Pattern B").
|
|
58
|
+
* Wrap the result in `defineConfig`:
|
|
59
|
+
*
|
|
60
|
+
* ```ts
|
|
61
|
+
* // prisma.config.ts
|
|
62
|
+
* import { defineConfig } from "prisma/config";
|
|
63
|
+
* import { buildPrismaConfig } from "@sidebase/base-config/prisma";
|
|
64
|
+
* export default defineConfig(buildPrismaConfig({ views: true, typedSql: true }));
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* Reads `DATABASE_URL` / `DIRECT_DATABASE_URL` / `SHADOW_DATABASE_URL` (the
|
|
68
|
+
* schema engine gets a pgbouncer-free direct connection). Prisma 7's changed
|
|
69
|
+
* config shape is out of scope for v1.
|
|
70
|
+
*/
|
|
71
|
+
declare function buildPrismaConfig(options?: BuildPrismaConfigOptions, env?: Record<string, string | undefined>): PrismaConfig;
|
|
72
|
+
|
|
73
|
+
export { applyPrismaDevEnv, buildPrismaConfig, resolveSchemaEngineDatasource, stripPgbouncerParams };
|
|
74
|
+
export type { BuildPrismaConfigOptions, PrismaConfigInput, SchemaEngineDatasource };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { withQuery, parseURL, parseQuery, stringifyQuery, stringifyParsedURL } from 'ufo';
|
|
2
|
+
|
|
3
|
+
const PGBOUNCER_PARAMS = ["pgbouncer", "connection_limit", "pool_timeout", "statement_cache_size"];
|
|
4
|
+
const DEV_DIRECT_URL = "postgres://postgres@localhost:5432/postgres?connection_limit=1";
|
|
5
|
+
function deriveShadowUrl(direct) {
|
|
6
|
+
return stringifyParsedURL({ ...parseURL(direct), pathname: "/prisma-shadow" });
|
|
7
|
+
}
|
|
8
|
+
function blankToUndefined(value) {
|
|
9
|
+
return value === void 0 || value.trim() === "" ? void 0 : value;
|
|
10
|
+
}
|
|
11
|
+
function applyPrismaDevEnv(env = process.env) {
|
|
12
|
+
const declaredDirect = blankToUndefined(env.DIRECT_DATABASE_URL);
|
|
13
|
+
const declaredPooled = blankToUndefined(env.DATABASE_URL);
|
|
14
|
+
const declaredShadow = blankToUndefined(env.SHADOW_DATABASE_URL);
|
|
15
|
+
if (declaredDirect === void 0 && declaredPooled !== void 0) {
|
|
16
|
+
env.DIRECT_DATABASE_URL = stripPgbouncerParams(declaredPooled);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const direct = declaredDirect ?? DEV_DIRECT_URL;
|
|
20
|
+
env.DIRECT_DATABASE_URL = direct;
|
|
21
|
+
env.DATABASE_URL = declaredPooled ?? withQuery(direct, { pgbouncer: "1" });
|
|
22
|
+
env.SHADOW_DATABASE_URL = declaredShadow ?? deriveShadowUrl(direct);
|
|
23
|
+
}
|
|
24
|
+
function stripPgbouncerParams(url) {
|
|
25
|
+
const parsed = parseURL(url);
|
|
26
|
+
if (!parsed.search) {
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
const query = parseQuery(parsed.search);
|
|
30
|
+
for (const key of PGBOUNCER_PARAMS) {
|
|
31
|
+
delete query[key];
|
|
32
|
+
}
|
|
33
|
+
const search = stringifyQuery(query);
|
|
34
|
+
parsed.search = search ? `?${search}` : "";
|
|
35
|
+
return stringifyParsedURL(parsed);
|
|
36
|
+
}
|
|
37
|
+
function resolveSchemaEngineDatasource(env) {
|
|
38
|
+
const declaredDirect = blankToUndefined(env.DIRECT_DATABASE_URL);
|
|
39
|
+
const declaredPooled = blankToUndefined(env.DATABASE_URL);
|
|
40
|
+
const direct = declaredDirect ?? (declaredPooled ? stripPgbouncerParams(declaredPooled) : void 0);
|
|
41
|
+
if (!direct) {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
const shadowDatabaseUrl = blankToUndefined(env.SHADOW_DATABASE_URL);
|
|
45
|
+
return shadowDatabaseUrl ? { url: direct, shadowDatabaseUrl } : { url: direct };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildPrismaConfig(options = {}, env = process.env) {
|
|
49
|
+
const { views = false, typedSql = false } = options;
|
|
50
|
+
const base = {
|
|
51
|
+
...views ? { views: { path: "prisma/views" } } : {},
|
|
52
|
+
...typedSql ? { typedSql: { path: "prisma/sql" } } : {}
|
|
53
|
+
};
|
|
54
|
+
const datasource = resolveSchemaEngineDatasource(env);
|
|
55
|
+
if (datasource) {
|
|
56
|
+
return { ...base, engine: "classic", datasource };
|
|
57
|
+
}
|
|
58
|
+
return base;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { applyPrismaDevEnv, buildPrismaConfig, resolveSchemaEngineDatasource, stripPgbouncerParams };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for the published ESLint factory (`@sidebase/base-config/eslint`).
|
|
3
|
+
* Covers the per-repo variance found across the consuming repos.
|
|
4
|
+
*/
|
|
5
|
+
interface CreateSidebaseEslintOptions {
|
|
6
|
+
/** Zod `.extend()/.merge()/.passthrough()` bans and/or `import * as z` enforcement. Default `"none"` (opt-in). */
|
|
7
|
+
zod?: "full" | "import-style" | "none";
|
|
8
|
+
/** Allow-list of permitted console methods, or a hard error. Default `"error"`. */
|
|
9
|
+
console?: string[] | "error";
|
|
10
|
+
/** Ban `publicProcedure` in `server/trpc/routers/**`. Default `false`. */
|
|
11
|
+
trpcGuard?: boolean;
|
|
12
|
+
/** Restrict `~~/prisma/` client imports on the app side. Default `false`. */
|
|
13
|
+
prismaImportGuard?: boolean;
|
|
14
|
+
/** `ts/consistent-type-definitions`. Default `"interface"`. */
|
|
15
|
+
typeDefStyle?: "interface" | "type";
|
|
16
|
+
/** Paths banned from direct import (Nuxt auto-import). Default `["utils/", "composables/", "~~/shared/types/"]`. */
|
|
17
|
+
autoImportPaths?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Subset of {@link autoImportPaths} where `import type { ... }` is still allowed
|
|
20
|
+
* (value imports stay banned), for repos that import a path like `composables/`
|
|
21
|
+
* only for its types. Default `[]` (opt-in).
|
|
22
|
+
*/
|
|
23
|
+
autoImportTypeOnly?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Files excluded from antfu's type-aware program (only relevant when
|
|
26
|
+
* type-aware). Default excludes `prisma.config.ts`, `eslint.config.ts`, the
|
|
27
|
+
* root `streamctl.config.ts`, and the legacy `.streamctl` TS files (see
|
|
28
|
+
* `resolveEslintOptions`).
|
|
29
|
+
*/
|
|
30
|
+
ignoresTypeAware?: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Globs for test / spec / story files where the `process.env` + auto-import
|
|
33
|
+
* bans are relaxed. Defaults to the standard `.test.`/`.spec.`/
|
|
34
|
+
* `.stories.` `ts`/`tsx` globs (see `resolveEslintOptions`).
|
|
35
|
+
*/
|
|
36
|
+
testFilePattern?: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runtime list of every {@link CreateSidebaseEslintOptions} key, for config
|
|
40
|
+
* validation/iteration. The `satisfies Record<keyof ..., 0>` map makes this
|
|
41
|
+
* exhaustive: adding a future option to the interface fails to compile until
|
|
42
|
+
* the key is listed here too, so the validator allow-list can never drift.
|
|
43
|
+
*/
|
|
44
|
+
declare const ESLINT_OPTION_KEYS: string[];
|
|
45
|
+
|
|
46
|
+
export { ESLINT_OPTION_KEYS as E };
|
|
47
|
+
export type { CreateSidebaseEslintOptions as C };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for the published ESLint factory (`@sidebase/base-config/eslint`).
|
|
3
|
+
* Covers the per-repo variance found across the consuming repos.
|
|
4
|
+
*/
|
|
5
|
+
interface CreateSidebaseEslintOptions {
|
|
6
|
+
/** Zod `.extend()/.merge()/.passthrough()` bans and/or `import * as z` enforcement. Default `"none"` (opt-in). */
|
|
7
|
+
zod?: "full" | "import-style" | "none";
|
|
8
|
+
/** Allow-list of permitted console methods, or a hard error. Default `"error"`. */
|
|
9
|
+
console?: string[] | "error";
|
|
10
|
+
/** Ban `publicProcedure` in `server/trpc/routers/**`. Default `false`. */
|
|
11
|
+
trpcGuard?: boolean;
|
|
12
|
+
/** Restrict `~~/prisma/` client imports on the app side. Default `false`. */
|
|
13
|
+
prismaImportGuard?: boolean;
|
|
14
|
+
/** `ts/consistent-type-definitions`. Default `"interface"`. */
|
|
15
|
+
typeDefStyle?: "interface" | "type";
|
|
16
|
+
/** Paths banned from direct import (Nuxt auto-import). Default `["utils/", "composables/", "~~/shared/types/"]`. */
|
|
17
|
+
autoImportPaths?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* Subset of {@link autoImportPaths} where `import type { ... }` is still allowed
|
|
20
|
+
* (value imports stay banned), for repos that import a path like `composables/`
|
|
21
|
+
* only for its types. Default `[]` (opt-in).
|
|
22
|
+
*/
|
|
23
|
+
autoImportTypeOnly?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Files excluded from antfu's type-aware program (only relevant when
|
|
26
|
+
* type-aware). Default excludes `prisma.config.ts`, `eslint.config.ts`, the
|
|
27
|
+
* root `streamctl.config.ts`, and the legacy `.streamctl` TS files (see
|
|
28
|
+
* `resolveEslintOptions`).
|
|
29
|
+
*/
|
|
30
|
+
ignoresTypeAware?: string[];
|
|
31
|
+
/**
|
|
32
|
+
* Globs for test / spec / story files where the `process.env` + auto-import
|
|
33
|
+
* bans are relaxed. Defaults to the standard `.test.`/`.spec.`/
|
|
34
|
+
* `.stories.` `ts`/`tsx` globs (see `resolveEslintOptions`).
|
|
35
|
+
*/
|
|
36
|
+
testFilePattern?: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runtime list of every {@link CreateSidebaseEslintOptions} key, for config
|
|
40
|
+
* validation/iteration. The `satisfies Record<keyof ..., 0>` map makes this
|
|
41
|
+
* exhaustive: adding a future option to the interface fails to compile until
|
|
42
|
+
* the key is listed here too, so the validator allow-list can never drift.
|
|
43
|
+
*/
|
|
44
|
+
declare const ESLINT_OPTION_KEYS: string[];
|
|
45
|
+
|
|
46
|
+
export { ESLINT_OPTION_KEYS as E };
|
|
47
|
+
export type { CreateSidebaseEslintOptions as C };
|