EEPNext.js Template
Conventions

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:

ScriptCommandWhat it does
seed.tspnpm db:seedClears and re-populates example_items with deterministic test data
db-smoke-check.tspnpm db:checkVerifies the DB connection and migration state
copy-swagger-ui.tspnpm docs:assets (pre-build)Vendors Swagger UI assets into public/
lint-probe.shpnpm lint:probeRegression-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.ts

Exploration 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.ts

For 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:

  1. config() must be called before any src/ import - Service constructors read env vars at instantiation time.
  2. 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

On this page