Scripts
What the scripts/ folder is for and how to use it.
The scripts/ folder sits at the project root and runs outside the Next.js
context. Scripts are language-agnostic, use whatever fits the task best. The convention
in this repo is TypeScript (run via pnpm exec tsx) or Bash
Two kinds of scripts
Helper scripts
Permanent utilities that support the development workflow. They live in
scripts/ indefinitely and are typically wired to a package.json command:
| Script | Command | What it does |
|---|---|---|
seed.ts | pnpm db:seed | Clears and re-populates example_items with deterministic test data |
db-smoke-check.ts | pnpm db:check | Verifies the DB connection and migration state |
copy-swagger-ui.ts | pnpm docs:assets (pre-build) | Vendors Swagger UI assets into public/ |
lint-probe.sh | pnpm lint:probe | Regression-tests eslint.config.ts - probes each rule to confirm it fires (and doesn't) on synthetic snippets |
These are committed, documented, and expected to stay working.
Exploration scripts
Throwaway files for trying something out before committing to it. Common uses:
- Calling a third-party API and inspecting the raw response shape
- Testing a data transformation or mapping before wiring it into a service
- Running a service class method directly against the live database
- Verifying a Drizzle query returns what you expect
// scripts/explore-hubspot-contacts.ts
import { config } from 'dotenv';
config({ path: '.env.local' });
import { hubspotService } from '@/classes/services/hubspot';
async function main() {
const contacts = await hubspotService.getContacts({ limit: 5 });
console.log(JSON.stringify(contacts[0], null, 2));
}
main().catch(console.error);NODE_OPTIONS='--conditions=react-server' pnpm exec tsx scripts/explore-hubspot-contacts.tsExploration scripts are temporary, use them to validate your understanding, then either promote the logic into a service class or delete the file.
Exploration scripts must not be wired to package.json commands or imported
from src/. They exist only to be run manually during development and should
not accumulate in the repo.
Importing server-only modules
Services in src/classes/services/ import server-only at the top of the
file. This is a compile-time guard that prevents them from being bundled into
the client - but it also means plain tsx throws an error if you try to import
them directly:
# ❌ fails - server-only guard throws
pnpm exec tsx scripts/explore-example.ts
# Error: This module cannot be imported from a Client Component module.The fix is the --conditions=react-server Node flag. It tells the module
resolver to use the react-server export condition, which is how Next.js
signals "we are running in a server context" - server-only sees this condition
and allows the import:
# ✅ works - server context satisfied
NODE_OPTIONS='--conditions=react-server' pnpm exec tsx scripts/explore-example.tsFor scripts that import from src/classes/services/, wire this into
package.json so you don't have to remember the flag:
{
"scripts": {
"explore:example": "NODE_OPTIONS='--conditions=react-server' tsx scripts/explore-example.ts"
}
}Exploration script shape
import { config } from 'dotenv';
/*
* Load .env.local before any src/ imports - env vars must be present
* before service constructors run (DB connection strings, API keys etc.).
*/
config({ path: '.env.local' });
import { exampleItemService } from '@/classes/services/example-item';
async function main() {
const items = await exampleItemService.getAll();
console.log('items:', JSON.stringify(items, null, 2));
}
main().catch(console.error);Two rules:
config()must be called before anysrc/import - Service constructors read env vars at instantiation time.- Wrap everything in an
async function main()- Top-level await is not supported in CJS output.
Running a script
# Standard script (no server-only imports)
pnpm exec tsx scripts/my-script.ts
# Script that imports from src/classes/services/
NODE_OPTIONS='--conditions=react-server' pnpm exec tsx scripts/my-script.ts