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
88 changes: 88 additions & 0 deletions .github/workflows/ci-src.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: CI - src (TypeScript)

on:
pull_request:
paths:
- '.github/workflows/ci-src.yml'
- 'src/**'
- 'tools/src/**'
push:
branches: [master, js-to-ts]

workflow_dispatch:

jobs:
src:
runs-on: ubuntu-latest
name: Typecheck and build

steps:
- uses: actions/checkout@v6

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: 22
package-manager-cache: false

- name: Cache dependencies
uses: actions/cache@v5
with:
path: ~/.npm
key: npm-linux-${{ hashFiles('package-lock.json') }}
restore-keys: npm-linux-

- name: Install npm dependencies
run: npm ci

- name: Typecheck
run: npm run src:typecheck

- name: Build
run: npm run src:build

smoke:
needs: src
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [18, 20, 22, 24]

name: Load dist on Node.js ${{ matrix.node-version }}

steps:
- uses: actions/checkout@v6

- name: Set up Node.js 22 to install and build
uses: actions/setup-node@v6
with:
node-version: 22
package-manager-cache: false

- name: Cache dependencies
uses: actions/cache@v5
with:
path: ~/.npm
key: npm-linux-${{ hashFiles('package-lock.json') }}
restore-keys: npm-linux-

- name: Install npm dependencies
run: npm ci

- name: Build
run: npm run src:build

- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
package-manager-cache: false

- name: Load the built entry points
run: |
if [ ! -f dist/index.js ]; then
echo "dist/index.js is not transcribed yet: nothing to load"
exit 0
fi
node -e "require('./dist/index.js'); require('./dist/promise.js'); console.log('loaded on', process.version)"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ test/circular/dist.mjs
/.lagune/skills/*
/**/lagune.*
/**/lagune/

# Temporary TypeScript transcription output (src/)
/dist/
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
}
},
{
"files": "test/**/*.mts",
"files": ["test/**/*.mts", "src/**/*.ts", "tools/src/**/*.mts"],
"options": {
"plugins": ["@ianvs/prettier-plugin-sort-imports"],
"importOrder": [
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ You are an expert Node.js developer contributing to MySQL2, a high-performance M
- Minimum compatibility: **Node 14**, whatever the `engines` field in `package.json` declares.
- Core: `/lib`, exposed through `index.js` (callback) and `promise.js` (promise-based).
- Types: `/typings` (`.d.ts` files), exposed through `index.d.ts` (callback) and `promise.d.ts` (promise-based).
- Off limits: `/src` is a separate effort (the TypeScript transcription) and out of scope for any task that does not explicitly ask for it. Never touch it, and never replicate a change made to `/lib`, `index.js` or `promise.js` there.
- Documentation: `website/docs/` (Docusaurus).
- Tests:
- Unit: `test/unit`, parallel.
Expand Down
12 changes: 11 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.7/schema.json",
"$schema": "https://biomejs.dev/schemas/2.5.10/schema.json",
"files": {
"includes": [
"**/*.js",
Expand Down Expand Up @@ -251,6 +251,16 @@
"javascript": {
"globals": ["arguments"]
}
},
{
"includes": ["src/**/*.ts"],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "error"
}
}
}
}
]
}
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
"test:coverage": "c8 npm test",
"test:build": "rollup -c",
"typecheck": "cd \"test/tsc-build\" && tsc -p \"tsconfig.json\" && cd .. && tsc -p \"tsconfig.json\" --noEmit",
"src:typecheck": "tsc -p src/tsconfig.json --noEmit && tsc -p tools/src/tsconfig.json",
"src:build": "tsx tools/src/clean.mts && tsc -p src/tsconfig.json",
"src:test": "npm run src:build && tsx tools/src/prepare-tests.mts && poku --config=tools/src/poku.config.mts",
"src:test:bun": "npm run src:build && tsx tools/src/prepare-tests.mts && bun poku --config=tools/src/poku.config.mts",
"src:test:deno": "npm run src:build && tsx tools/src/prepare-tests.mts && deno run -A npm:poku --config=tools/src/poku.config.mts",
"benchmark": "node ./benchmarks/benchmark.js",
"wait-port": "wait-on"
},
Expand Down
79 changes: 79 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# `src`: the TypeScript transcription of MySQL2

`src` is the TypeScript transcription of the shipped JavaScript (the two entry points and `lib`), developed in parallel while that JavaScript remains what users install. Nothing in `src` is published, covered by the regular CI, or loaded at runtime until the community declares the transcription complete.

Context: [#2803](https://github.com/sidorares/node-mysql2/issues/2803) and [#3695](https://github.com/sidorares/node-mysql2/issues/3695).

## Rules

1. **Runtime behavior never changes.** The built output must do exactly what the JavaScript does today: same exports, same lazy loading, same errors, same event order, same performance. Types describe the code, they do not redesign it.
2. **Mirror the layout by default.** Each shipped file gets a TypeScript twin at the same path under `src`. Deviate only when TypeScript or the build requires it, and say why in the pull request.
3. **Transcribe, do not refactor.** Keep names, structure and statement order. A bug found while transcribing is fixed in the shipped JavaScript first, through a regular pull request to `master`, then ported.
4. **No `any`, no double casts.** Use `unknown` and narrow it. Prefer `type` over `interface`, and named exports over default exports, so the emitted CommonJS keeps the flat `exports` shape users rely on.
5. **Imports carry the `.js` extension**, the way the built output resolves them. Biome enforces it.
6. **Ambient declarations for dependencies without typings live in `src/types`.** Keep them faithful to how the shipped code uses each package, nothing more.
7. **`typings` remains the public type contract until the switch.** When it disagrees with `src`, the mismatch is a finding to discuss in the pull request, not something to paper over.

## Build

The build emits to `dist`, git-ignored, mirroring the repository root: the two entry points, `lib`, and a declaration file next to each module. Class fields compile to constructor assignments, matching the JavaScript.

| Setting | Value | Why |
| -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `target` | `es2020` | Node.js 14 is the real runtime floor and ES2020 is the newest syntax it runs natively, so nothing the JavaScript already uses gets down-leveled. |
| `module` | `node16` | With the package declared as CommonJS, every file compiles to CommonJS, and Node.js, Bun and Deno keep loading the same module format. |
| `declaration` | `true` | The emitted declarations become the type contract at the switch. |
| `sourceMap` | `false` | Open decision: maps help debugging but add package size. Revisit before the switch. |
| `skipLibCheck` | `true` | `aws-ssl-profiles` ships a declaration file the compiler rejects. Fixing it upstream lets this go back to `false`. |

## Commands

```sh
npm run src:typecheck
npm run src:build
npm run src:test # Node.js
npm run src:test:bun
npm run src:test:deno
```

`npm run lint` already covers `src`.

## Transcribing a file

1. Pick a file, starting from the leaves (constants, packets, parsers) and moving up to the connection, the promise wrapper and the entry points.
2. Create its twin in `src` and transcribe it statement by statement.
3. Run the typecheck and the linter.
4. Open a pull request against `js-to-ts`, one file or one small directory each.

## Keeping up with `master`

`master` keeps changing the shipped JavaScript while the transcription happens here. Merge it into `js-to-ts` regularly. Since `src` and the shipped files never overlap, the merge never conflicts, it only brings changes the transcribed files may not reflect yet. Before merging, list what changed:

```sh
git fetch origin
git diff --stat HEAD...origin/master -- index.js promise.js lib
```

Merge, then port each change to its twin in `src`. Files not transcribed yet need nothing, since they will be transcribed from the current JavaScript later.

## Testing

The existing test suite, unchanged, is the acceptance test. The `src:test` scripts build `dist`, copy the tests next to it and run them there, so every relative import in a test resolves to the built output instead of the shipped JavaScript. `FILTER` works as usual, a MySQL server is needed as for `npm test`, and the run only makes sense once the entry points and everything they import exist in `src`.

The dedicated workflow typechecks and builds `src` on every pull request that touches it, then loads the built entry points on the Node.js versions the regular CI already tests, once they exist. The regular workflows never look at `src`.

## Known questions

- The callback entry point exposes part of its API through lazy getters, so the promise API is not loaded eagerly and the module graph stays free of cycles, which the circular-import check enforces. ES module syntax has no lazy export, so the transcribed entry points need an explicit CommonJS getter and a lint exception for it.
- Named placeholders are loaded lazily on first use. The transcription must keep that.
- The row parsers generate code from strings. The generated source stays untyped by nature: type the generator inputs and the returned function, nothing inside the string.

## The switch

When `src` is complete, one pull request makes the built output the shipped code:

1. Point the build output at the repository root. It then writes the entry points and `lib` exactly where they are today, so the package manifest, coverage, CodeQL and every test import keep working unchanged.
2. Delete the JavaScript sources from git, ignore the generated paths, and build before packing.
3. The emitted declarations become the type contract. Remove the hand-written typings and the manual compile checks: compiling `src` in its approved state is the type test.
4. Fold the `src` scripts and workflow into the regular ones and delete the helper tools.
5. Release as a major version, with a changelog entry stating that runtime behavior is unchanged.
28 changes: 28 additions & 0 deletions src/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"include": ["**/*.ts"],
"compilerOptions": {
"rootDir": ".",
"outDir": "../dist",
"target": "es2020",
"lib": ["es2020"],
"module": "node16",
"moduleResolution": "node16",
"types": ["node"],
"strict": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"declarationMap": false,
"sourceMap": false,
"removeComments": false,
"newLine": "lf",
"noEmitOnError": true
}
}
27 changes: 27 additions & 0 deletions src/types/generate-function.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
declare module 'generate-function' {
type Formatters = {
s(value: unknown): string;
d(value: unknown): string;
o(value: unknown): string;
};

type GeneratedFunction = {
(format?: string, ...args: unknown[]): GeneratedFunction;
scope: Record<string, unknown>;
formats: Formatters;
sym(name?: string): string;
property(name: string | number): string;
property(object: string, name: string | number): string;
toString(): string;
toFunction<T = unknown>(scope?: Record<string, unknown>): T;
};

type GenerateFunction = {
(format?: string, ...args: unknown[]): GeneratedFunction;
formats: Formatters;
};

const generateFunction: GenerateFunction;

export = generateFunction;
}
27 changes: 27 additions & 0 deletions src/types/named-placeholders.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
declare module 'named-placeholders' {
type CompiledQuery = [sql: string, values: unknown[]];

type QueryCache = {
get(query: string): unknown;
set(query: string, tree: unknown): unknown;
};

type CompilerOptions = {
placeholder?: string;
cache?: number | QueryCache | false;
};

type Compile = (
query: string,
params?: Record<string, unknown>
) => CompiledQuery;

type CreateCompiler = {
(options?: CompilerOptions): Compile;
toNumbered(query: string, params: Record<string, unknown>): CompiledQuery;
};

const createCompiler: CreateCompiler;

export = createCompiler;
}
4 changes: 4 additions & 0 deletions tools/src/clean.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { rm } from 'node:fs/promises';
import { dist } from './paths.mts';

await rm(dist, { recursive: true, force: true });
4 changes: 4 additions & 0 deletions tools/src/paths.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { join, resolve } from 'node:path';

export const root = resolve(import.meta.dirname, '../..');
export const dist = join(root, 'dist');
39 changes: 39 additions & 0 deletions tools/src/poku.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { multiSuite } from '@pokujs/multi-suite';
import { defineConfig, listFiles } from 'poku';
import { hasPrivileges } from '../common.js';

const commonConfig = defineConfig({
reporter: 'compact',
deno: {
allow: ['all'],
},
});

const parallel = defineConfig({
...commonConfig,
include: ['dist/test/unit', 'dist/test/integration'],
timeout: 30000,
concurrency: 8,
});

const sequential = defineConfig({
...commonConfig,
timeout: 60000,
sequential: true,
plugins: [
{
async discoverFiles() {
if (!(await hasPrivileges())) {
console.log('\n› Skipping global tests: insufficient privileges');
return [];
}

return listFiles('dist/test/global');
},
},
],
});

export default defineConfig({
plugins: [multiSuite([parallel, sequential])],
});
17 changes: 17 additions & 0 deletions tools/src/prepare-tests.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { access, copyFile, cp, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { exit } from 'node:process';
import { dist, root } from './paths.mts';

try {
await access(join(dist, 'index.js'));
} catch {
console.error(
'dist/index.js not found. Run "npm run src:build" after transcribing src/index.ts.'
);
exit(1);
}

await rm(join(dist, 'test'), { recursive: true, force: true });
await cp(join(root, 'test'), join(dist, 'test'), { recursive: true });
await copyFile(join(root, 'package.json'), join(dist, 'package.json'));
Loading
Loading