Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"enabledManagers": ["github-actions", "npm"],
"dependencyDashboard": true,
"timezone": "America/Los_Angeles",
"schedule": ["* * * * 1"],
"vulnerabilityAlerts": {
"description": [
"Security fixes must not wait for the Monday window, so this overrides the schedule only.",
"`minimumReleaseAge` is set here EXPLICITLY. Renovate force-applies `minimumReleaseAge: null` to this block, so omitting it drops the cooldown instead of inheriting it from packageRules. The cooldown guards against a compromised release that gets yanked within a day."
],
"enabled": true,
"schedule": [],
"minimumReleaseAge": "1 day",
"labels": ["security", "dependencies"]
},
"packageRules": [
{
"description": "Tooling and examples track new releases in the lockfile without rewriting their ranges",
"matchManagers": ["npm"],
"rangeStrategy": "update-lockfile"
},
{
"description": "A published package's private dependency range starts at the version CI tested, so re-vendoring or publishing carries that floor into every app (PACKAGES.md -> Dependencies)",
"matchManagers": ["npm"],
"matchFileNames": ["packages/*/package.json"],
"matchDepTypes": ["dependencies"],
"rangeStrategy": "bump"
},
{
"description": "Apps own their peer versions. An in-range release only moves the devDependency copy that CI tests; an out-of-range release widens the peer so apps can upgrade on their own schedule (PACKAGES.md -> Dependencies)",
"matchManagers": ["npm"],
"matchDepTypes": ["peerDependencies"],
"rangeStrategy": "widen"
},
{
"matchManagers": ["github-actions"],
"groupName": "github-actions",
"groupSlug": "github-actions",
"separateMajorMinor": false
},
{
"description": "Require explicit approval for npm majors so API and runtime compatibility get a human review before a branch is created",
"matchManagers": ["npm"],
"matchUpdateTypes": ["major"],
"dependencyDashboardApproval": true
},
{
"description": "kysely-codegen peers on kysely; generated declarations and the query builder move together",
"matchManagers": ["npm"],
"matchPackageNames": ["kysely", "kysely-codegen"],
"groupName": "kysely",
"groupSlug": "kysely",
"separateMajorMinor": false
},
{
"description": "The @hono adapters peer on hono",
"matchManagers": ["npm"],
"matchPackageNames": ["hono", "@hono/**"],
"groupName": "hono",
"groupSlug": "hono",
"separateMajorMinor": false
},
{
"matchManagers": ["npm"],
"matchUpdateTypes": ["patch"],
"minimumReleaseAge": "1 day"
},
{
"matchManagers": ["npm"],
"matchUpdateTypes": ["minor"],
"minimumReleaseAge": "3 days"
},
{
"matchManagers": ["npm"],
"matchUpdateTypes": ["major"],
"minimumReleaseAge": "14 days"
},
{
"description": "@types/node must not expose APIs newer than the Node major in .nvmrc and CI",
"matchManagers": ["npm"],
"matchPackageNames": ["@types/node"],
"allowedVersions": ">=24 <25"
},
{
"description": "Node is governed by engines, .nvmrc and the CI setup-node version, and is bumped by hand",
"matchPackageNames": ["node"],
"enabled": false
}
]
}
10 changes: 8 additions & 2 deletions PACKAGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,15 @@ Three packages share version 0.1.0: `pgstencil` (infrastructure and test primiti

Run `pnpm packages:pack` to produce the three archives in `dist/packages`. They contain ESM JavaScript, TypeScript declarations, the MIT license, and required SQL/Compose assets. Workspace development uses source exports; packing switches the exports to compiled files. Nothing runs migrations during install.

Copy the tarballs to a consumer's `vendor/` directory, depend on them using `file:` paths, and override all three package names to those same paths in the consumer's pnpm configuration. The override for `pgstencil` ensures auth and billing's transitive dependency also resolves locally. Commit the archives and lockfile together for a reproducible temporary distribution. Once public npm is configured, replace these paths with exact registry versions and remove the overrides.
Copy the tarballs to a consumer's `vendor/` directory, depend on them using `file:` paths, and override all three package names to those same paths in the consumer's pnpm configuration. The override for `pgstencil` ensures auth and billing's peer also resolves locally. Pack a committed revision in a clean checkout, because an untracked file under `migrations` would otherwise ship in the archive. Commit the archives and lockfile together for a reproducible temporary distribution. Once public npm is configured, replace these paths with exact registry versions and remove the overrides.

`pnpm packages:verify` builds, packs, installs into an unrelated temporary pnpm project, checks TypeScript declarations, and runs an email login and required-card trial against a real cloned database. It shares this repository's Docker service state, but loads all code and SQL from installed archives. The consumer directory is printed for inspection.
`pnpm packages:verify` builds, packs, installs into an unrelated temporary pnpm project, checks TypeScript declarations, and runs an email login and required-card trial against a real cloned database. The project declares each peer at the version this workspace tests, and fails on an unmet peer. It shares this repository's Docker service state, but loads all code and SQL from installed archives. The consumer directory is printed for inspection.

## Dependencies

Applications declare pgstencil's peer dependencies themselves: `kysely` for every package, `hono` for `@pgstencil/auth`, and `stripe` for `@pgstencil/stripe`. Auth and billing also peer on the `pgstencil` released with them. A library is a peer when the application and pgstencil must share one copy. Either objects cross the boundary, or the library holds module-level state. Kysely and Hono classes have private fields, so two copies are incompatible types. `pgstencil/diagnostics` keeps its request scope in `AsyncLocalStorage`. The application's Renovate updates each shared library once, and pgstencil uses that copy. pnpm reports a release outside a peer range; pgstencil must widen the range first.

Every other dependency is private: pgstencil owns its version and applications do not import it. Better Auth is deliberately private. pgstencil imports its internal subpaths and tests login and linking rules against specific releases, so its range admits patches only. A new login provider or Better Auth plugin belongs in `@pgstencil/auth`, not in an application. Private ranges start at the version pgstencil's CI tested. [`.github/renovate.json`](.github/renovate.json) raises that floor, and re-vendoring carries it into each application.

## Application composition

Expand Down
2 changes: 1 addition & 1 deletion examples/better-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"@hono/node-server": "1.19.17",
"better-auth": "1.7.3",
"hono": "^4.13.7",
"kysely": "0.29.5",
"kysely": "^0.29.5",
"pg": "^8.16.0",
"pgstencil": "workspace:*",
"@pgstencil/auth": "workspace:*"
Expand Down
6 changes: 4 additions & 2 deletions examples/login/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
"type": "module",
"dependencies": {
"@pgstencil/stripe": "workspace:*",
"kysely": "0.29.5",
"kysely": "^0.29.5",
"pg": "^8.16.0",
"pgstencil": "workspace:*",
"@pgstencil/auth": "workspace:*"
"@pgstencil/auth": "workspace:*",
"hono": "^4.13.7",
"stripe": "^22.6.1"
}
}
5 changes: 4 additions & 1 deletion examples/workers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"private": true,
"type": "module",
"dependencies": {
"@pgstencil/auth": "workspace:*"
"@pgstencil/auth": "workspace:*",
"hono": "^4.13.7",
"kysely": "^0.29.5",
"pgstencil": "workspace:*"
},
"scripts": {
"build": "wrangler deploy --dry-run --outdir dist"
Expand Down
15 changes: 11 additions & 4 deletions packages/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,18 @@
"./better-auth-workers": "./src/better-auth-workers.ts"
},
"dependencies": {
"better-auth": "1.7.3",
"better-auth": "~1.7.3",
"jose": "^6.2.12",
"openid-client": "^6.8.8"
},
"peerDependencies": {
"hono": "^4.13.7",
"kysely": "^0.29.5",
"pgstencil": "workspace:^"
},
"devDependencies": {
"hono": "^4.13.7",
"jose": "6.2.12",
"kysely": "0.29.5",
"openid-client": "6.8.8",
"kysely": "^0.29.5",
"pgstencil": "workspace:*"
},
"engines": {
Expand Down
11 changes: 8 additions & 3 deletions packages/pgstencil/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@
},
"dependencies": {
"pg": "^8.16.0",
"kysely": "0.29.5",
"node-pg-migrate": "9.0.0",
"testcontainers": "12.1.0",
"node-pg-migrate": "^9.0.0",
"testcontainers": "^12.1.0",
"cheerio": "^1.1.0",
"turndown": "^7.2.0"
},
"peerDependencies": {
"kysely": "^0.29.5"
},
"devDependencies": {
"kysely": "^0.29.5"
},
"engines": {
"node": ">=24"
},
Expand Down
11 changes: 8 additions & 3 deletions packages/stripe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@
"./testing": "./src/testing.ts",
"./migrations": "./src/migrations.ts"
},
"dependencies": {
"peerDependencies": {
"kysely": "^0.29.5",
"pgstencil": "workspace:^",
"stripe": "^22.6.1"
},
"devDependencies": {
"kysely": "^0.29.5",
"pgstencil": "workspace:*",
"kysely": "0.29.5",
"stripe": "22.6.1"
"stripe": "^22.6.1"
},
"engines": {
"node": ">=24"
Expand Down
53 changes: 35 additions & 18 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ packages:
onlyBuiltDependencies:
- esbuild
- workerd

# Ignore package versions published less than 1 day (1440 min) ago, so a
# freshly published (possibly compromised or broken) release cannot enter an
# install before it has had time to be caught. Mirrors the Renovate
# minimumReleaseAge gating in .github/renovate.json.
minimumReleaseAge: 1440
29 changes: 27 additions & 2 deletions scripts/verify-packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,39 @@ import { projectRoot } from '../packages/pgstencil/src/paths.ts';
const directory = await mkdtemp(join(tmpdir(), 'pgstencil-packed-'));
await mkdir(join(directory, 'vendor'));
const dependencies: Record<string, string> = {};
// The consumer owns every shared library, at the version this workspace tests.
const peers: Record<string, string> = {};
for (const name of ['pgstencil', '@pgstencil/auth', '@pgstencil/stripe']) {
const packageDirectory = name === 'pgstencil' ? name : name.split('/')[1]!;
const manifest = JSON.parse(
await readFile(
join(projectRoot, 'packages', packageDirectory, 'package.json'),
'utf8',
),
) as { version: string };
) as { version: string; peerDependencies?: Record<string, string> };
const file = `${name.replace('@', '').replace('/', '-')}-${manifest.version}.tgz`;
await cp(
join(projectRoot, 'dist/packages', file),
join(directory, 'vendor', file),
);
dependencies[name] = `file:./vendor/${file}`;
for (const peer of Object.keys(manifest.peerDependencies ?? {})) {
if (peer === 'pgstencil') continue;
const installed = JSON.parse(
await readFile(
join(
projectRoot,
'packages',
packageDirectory,
'node_modules',
peer,
'package.json',
),
'utf8',
),
) as { version: string };
peers[peer] = installed.version;
}
}
await writeFile(
join(directory, 'package.json'),
Expand All @@ -30,7 +49,7 @@ await writeFile(
private: true,
type: 'module',
packageManager: 'pnpm@10.30.1',
dependencies,
dependencies: { ...dependencies, ...peers },
pnpm: { overrides: dependencies },
devDependencies: { '@types/node': '24.13.3', typescript: '5.9.3' },
},
Expand Down Expand Up @@ -114,6 +133,12 @@ await writeFile(
include: ['verify.ts'],
}),
);
// A shared library outside pgstencil's range, or one nothing provides, fails
// the install instead of only warning.
await writeFile(
join(directory, '.npmrc'),
'auto-install-peers=false\nstrict-peer-dependencies=true\n',
);
execFileSync('pnpm', ['install', '--ignore-scripts'], {
cwd: directory,
stdio: 'inherit',
Expand Down
Loading