@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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/dist/config.d.mts +189 -0
  4. package/dist/config.d.ts +189 -0
  5. package/dist/config.mjs +46 -0
  6. package/dist/eslint/index.d.mts +55 -0
  7. package/dist/eslint/index.d.ts +55 -0
  8. package/dist/eslint/index.mjs +278 -0
  9. package/dist/prisma/index.d.mts +74 -0
  10. package/dist/prisma/index.d.ts +74 -0
  11. package/dist/prisma/index.mjs +61 -0
  12. package/dist/shared/base-config.CuUhyvQo.d.mts +47 -0
  13. package/dist/shared/base-config.CuUhyvQo.d.ts +47 -0
  14. package/docs/migration.md +764 -0
  15. package/package.json +94 -0
  16. package/presets/base/AGENTS.md +31 -0
  17. package/presets/base/CLAUDE.md +3 -0
  18. package/presets/base/dockerignore +18 -0
  19. package/presets/base/editorconfig +12 -0
  20. package/presets/base/github/workflows/streamctl-upgrade.yml +130 -0
  21. package/presets/base/gitignore +19 -0
  22. package/presets/base/oxlintrc.json +31 -0
  23. package/presets/base/pnpm-workspace.yaml +27 -0
  24. package/presets/base/preset.json +38 -0
  25. package/presets/base/templates/pnpm/only-built-dependency.yml +1 -0
  26. package/presets/base/tsconfig.json +3 -0
  27. package/presets/base/vscode/extensions.json +8 -0
  28. package/presets/base/vscode/settings.json +43 -0
  29. package/presets/config.template.ts +18 -0
  30. package/presets/manifest.json +17 -0
  31. package/presets/nuxt-app/Dockerfile +63 -0
  32. package/presets/nuxt-app/eslint.config.ts +5 -0
  33. package/presets/nuxt-app/github/workflows/ci.yml +66 -0
  34. package/presets/nuxt-app/github/workflows/pr-preview-cleanup.yml +41 -0
  35. package/presets/nuxt-app/preset.json +67 -0
  36. package/presets/nuxt-app/prisma.config.ts +6 -0
  37. package/presets/nuxt-app/templates/ci/e2e-job.yml +39 -0
  38. package/presets/nuxt-app/templates/ci/test-job.yml +13 -0
  39. package/presets/nuxt-app/tsconfig.json +6 -0
  40. 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 };