Skip to content

Commit e2bbc97

Browse files
kvzclaude
andauthored
chore: convert src/_util/ to TypeScript, remove Babel (#502)
* Update CHANGELOG.md * chore: convert src/_util/ to TypeScript, remove Babel - Convert all src/_util/*.js to TypeScript with proper types - Remove 9 Babel dependencies (babel-cli, babel-core, babel-register, etc.) - Use Node's native type stripping (--experimental-strip-types) - Update Node engine requirement: >= 10 → >= 22 - Convert test/util/test-util.js to ESM imports - Use named exports throughout (export { Util }) - Fix indent-string API call (v2.1.0 requires string indent arg) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: convert test files to TypeScript, add stray .js lint - test/util/test-util.js → test/util/test-util.ts - test/module/module.js → test/module/module.ts - Add @types/mocha for test type definitions - Update tsconfig.json to include test files - Update test scripts to use --experimental-strip-types - Add lint:no-stray-js to catch .js files that should be .ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 86debe1 commit e2bbc97

File tree

12 files changed

+377
-2584
lines changed

12 files changed

+377
-2584
lines changed

CHANGELOG.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,19 @@ Ideas that will be planned and find their way into a release at one point
3838
- [x] CI integration: `verified:` header in function files, `yarn test:parity` for CI
3939
- [x] Badge: "Verified against PHP 8.3" (added to README, PR #501)
4040
- [ ] Modernize, e.g.:
41-
- [ ] Migrate Babel 6 → native ESM (Node 20+)
41+
- [x] Migrate Babel 6 → TS (Node 22+ typestripping) - removed Babel, using `node --experimental-strip-types`
4242
- [ ] Migrate Mocha → Vitest
43-
- [ ] Dual CJS/ESM exports
44-
- [ ] Drop Node < 20 support
45-
- [ ] Publish as `locutus@3.0.0` with breaking changes
43+
- [ ] Migrate custom `test/browser/app.js` and `yarn browser:watch`/browserify → Vitest with Playwright support, running a few generated test in a real browser
44+
- [x] Drop Node < 22 support (now requires Node >= 22)
4645
- [x] ESLint/Prettier → Biome (done in v2.0.33)
47-
- [ ] CJS → ESM
48-
- [ ] Custom tagged releases (`CONTRIBUTING.md`) → Changesets bundled in PRs
49-
- [ ] JS → TS for infra scripts (use Node v24 native type stripping to run)
46+
- [ ] Migrate CJS → ESM (should we do this for all source functions? think so, but with Dual CJS/ESM exports. With https://github.com/colinhacks/zshy? What's best these days?)
47+
- [ ] Migrate Custom tagged releases (`CONTRIBUTING.md`) → Changesets bundled in PRs
48+
- [x] Migrate JS → TS for infra scripts (use Node v22+ native type stripping to run)
5049
- [ ] TypeScript:
51-
- [ ] Convert `src/_util/` to TypeScript
50+
- [x] Convert `src/_util/` to TypeScript
5251
- [ ] Generate types from JSDoc in function files
5352
- [ ] Per-function type exports
54-
- [ ] Strict mode compatible. Node type stripping compatible
53+
- [x] Strict mode compatible. Node type stripping compatible
5554
- [ ] Expansion (port more functions to the different languages), we'll go from most feasible + sensible, to least :)
5655
- [ ] Docs/Website:
5756
- [ ] Jekyll → Next.js 16 SSG
@@ -62,6 +61,12 @@ Ideas that will be planned and find their way into a release at one point
6261

6362
Released: TBA. [Diff](https://github.com/locutusjs/locutus/compare/v2.0.33...main).
6463

64+
### Infrastructure
65+
- [x] Converted `src/_util/` to TypeScript (headerSchema.ts, headerFormatter.ts, formatHeaders.ts, util.ts, cli.ts)
66+
- [x] Removed Babel 6 dependencies, now using Node's native type stripping (`--experimental-strip-types`)
67+
- [x] Updated Node engine requirement from >= 10 to >= 22
68+
- [x] Updated test files to use ESM imports
69+
6570
## v2.0.33
6671

6772
Released: 2026-01-07. [Diff](https://github.com/locutusjs/locutus/compare/v2.0.32...v2.0.33).

package.json

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,27 +28,28 @@
2828
"scripts": {
2929
"browser:bundle": "browserify test/browser/app.js --outfile test/browser/bundle.js",
3030
"browser:watch": "budo test/browser/app.js --live --serve test/browser/bundle.js",
31-
"build:dist": "babel src --out-dir dist --source-maps && cp package.json README.md dist/",
32-
"build:indices": "babel-node src/_util/cli.js reindex",
33-
"build:tests:noskip": "rimraf test/generated && babel-node src/_util/cli.js writetests --noskip",
34-
"build:tests": "rimraf test/generated && babel-node src/_util/cli.js writetests",
31+
"build:dist": "rimraf dist && cp -r src dist && rm -rf dist/_util && cp package.json README.md dist/",
32+
"build:indices": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON src/_util/cli.ts reindex",
33+
"build:tests:noskip": "rimraf test/generated && node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON src/_util/cli.ts writetests --noskip",
34+
"build:tests": "rimraf test/generated && node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON src/_util/cli.ts writetests",
3535
"build": "npm-run-all 'build:*'",
36-
"injectweb": "rimraf website/source/{c,golang,php,python,ruby} && babel-node src/_util/cli.js injectweb",
37-
"check": "npm-run-all --serial fix:biome lint lint:ts lint:headers test:languages",
36+
"injectweb": "rimraf website/source/{c,golang,php,python,ruby} && node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON src/_util/cli.ts injectweb",
37+
"check": "npm-run-all --serial fix:biome lint lint:ts lint:headers lint:no-stray-js test:languages",
3838
"fix:biome": "biome check --write .",
39-
"fix:headers": "babel-node src/_util/formatHeaders.js fix",
40-
"lint:headers": "babel-node src/_util/formatHeaders.js check",
39+
"fix:headers": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON src/_util/formatHeaders.ts fix",
40+
"lint:headers": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON src/_util/formatHeaders.ts check",
41+
"lint:no-stray-js": "bash -c 'found=$(find src test -name \"*.js\" | grep -vE \"^src/(c|golang|php|python|ruby)/|/index\\.js$|^test/generated/|^test/browser/\"); if [ -n \"$found\" ]; then echo \"Stray .js files found (should be .ts):\"; echo \"$found\"; exit 1; fi'",
4142
"lint:ts": "tsc --noEmit -p tsconfig.json",
4243
"fix:markdown": "remark {README,CONTRIBUTING}.md --output",
4344
"fix": "npm-run-all --serial 'fix:**'",
4445
"lint": "biome check .",
45-
"playground:start": "cd test/browser && babel-node server.js",
46-
"test:languages:noskip": "yarn build:tests:noskip && cross-env DEBUG='locutus:*' mocha --require babel-register --reporter spec 'src/**/*.mocha.js' 'test/**/test-*.js'",
47-
"test:languages": "yarn build:tests && cross-env DEBUG='locutus:*' mocha --require babel-register --reporter spec 'src/**/*.mocha.js' 'test/**/test-*.js'",
48-
"test:module": "babel-node test/module/module.js",
46+
"playground:start": "cd test/browser && node server.js",
47+
"test:languages:noskip": "yarn build:tests:noskip && cross-env DEBUG='locutus:*' mocha --reporter spec 'src/**/*.mocha.js' 'test/**/test-*.js'",
48+
"test:languages": "yarn build:tests && cross-env DEBUG='locutus:*' mocha --reporter spec 'src/**/*.mocha.js' 'test/**/test-*.js'",
49+
"test:module": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON test/module/module.ts",
4950
"test:parity": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON test/parity/index.ts",
5051
"test:parity:php": "node --experimental-strip-types --disable-warning=MODULE_TYPELESS_PACKAGE_JSON test/parity/index.ts php",
51-
"test:util": "mocha --require babel-register --reporter spec test/util/",
52+
"test:util": "mocha --reporter spec --node-option experimental-strip-types --node-option disable-warning=MODULE_TYPELESS_PACKAGE_JSON 'test/util/**/*.ts'",
5253
"test": "npm-run-all test:languages test:util test:module",
5354
"website:install": "cd website && yarn",
5455
"website:deploy": "cd website && yarn deploy",
@@ -58,23 +59,14 @@
5859
},
5960
"devDependencies": {
6061
"@biomejs/biome": "^2.3.11",
62+
"@types/mocha": "^10.0.10",
6163
"@types/node": "^25.0.3",
6264
"async": "2.6.4",
63-
"babel-cli": "6.26.0",
64-
"babel-core": "6.26.3",
65-
"babel-plugin-add-module-exports": "1.0.4",
66-
"babel-plugin-es6-promise": "1.1.1",
67-
"babel-plugin-syntax-async-functions": "6.13.0",
68-
"babel-plugin-transform-async-to-generator": "6.24.1",
69-
"babel-plugin-transform-object-assign": "6.22.0",
70-
"babel-preset-es2015": "6.24.1",
71-
"babel-register": "6.26.0",
7265
"browserify": "17.0.1",
7366
"budo": "11.8.4",
7467
"chai": "6",
7568
"cross-env": "10",
7669
"debug": "^4.4.3",
77-
"es6-promise": "4.2.8",
7870
"esprima": "4.0.1",
7971
"globby": "4.1.0",
8072
"indent-string": "2.1.0",
@@ -92,7 +84,7 @@
9284
},
9385
"packageManager": "yarn@4.12.0",
9486
"engines": {
95-
"node": ">= 10",
87+
"node": ">= 22",
9688
"yarn": ">= 1"
9789
},
9890
"readmeFilename": "README.md"

src/_util/cli.js

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/_util/cli.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/usr/bin/env node
2+
3+
import { Util } from './util.ts'
4+
5+
const util = new Util(process.argv)
6+
const command = process.argv[2] as keyof Util
7+
8+
if (typeof util[command] === 'function') {
9+
;(util[command] as (cb: (err: Error | null) => void) => void)(function (err) {
10+
if (err) {
11+
throw err
12+
}
13+
console.log('Done')
14+
})
15+
} else {
16+
console.error(`Unknown command: ${command}`)
17+
console.error('Available commands: reindex, writetests, injectweb')
18+
process.exit(1)
19+
}
Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
* CLI for header formatter
55
*
66
* Usage:
7-
* node formatHeaders.js check - Check if all headers are properly formatted
8-
* node formatHeaders.js fix - Fix all header formatting issues
7+
* node formatHeaders.ts check - Check if all headers are properly formatted
8+
* node formatHeaders.ts fix - Fix all header formatting issues
99
*/
1010

11-
const path = require('path')
12-
const { checkAll, formatAll } = require('./headerFormatter')
11+
import path from 'node:path'
12+
import { fileURLToPath } from 'node:url'
13+
import { checkAll, formatAll } from './headerFormatter.ts'
1314

15+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
1416
const srcDir = path.dirname(__dirname)
1517
const command = process.argv[2]
1618

@@ -47,6 +49,6 @@ if (command === 'check') {
4749
}
4850
process.exit(0)
4951
} else {
50-
console.error('Usage: node formatHeaders.js [check|fix]')
52+
console.error('Usage: node formatHeaders.ts [check|fix]')
5153
process.exit(1)
5254
}
Lines changed: 47 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,46 @@
44
* Ensures all header comment keys are vertically aligned with colons at a consistent position.
55
*/
66

7-
const fs = require('fs')
8-
const path = require('path')
9-
const globby = require('globby')
10-
const { isValidHeaderKey } = require('./headerSchema')
7+
import fs from 'node:fs'
8+
import path from 'node:path'
9+
import globby from 'globby'
10+
import { isValidHeaderKey } from './headerSchema.ts'
11+
12+
interface ParsedLine {
13+
isHeader: boolean
14+
key?: string
15+
value?: string
16+
}
17+
18+
interface FormatResult {
19+
formatted: string
20+
changed: boolean
21+
}
22+
23+
interface CheckResult {
24+
ok: boolean
25+
filepath: string
26+
}
27+
28+
interface FormatFileResult {
29+
changed: boolean
30+
filepath: string
31+
}
32+
33+
interface CheckAllResult {
34+
ok: boolean
35+
badFiles: string[]
36+
}
37+
38+
interface FormatAllResult {
39+
formattedCount: number
40+
files: string[]
41+
}
1142

1243
/**
1344
* Parse a header line to extract key and value
14-
* @param {string} line - A line from the file
15-
* @returns {{ isHeader: boolean, key?: string, value?: string }}
1645
*/
17-
function parseHeaderLine(line) {
18-
// Match header comment pattern: exactly 2-space indent, //, optional whitespace, key: value
19-
// This ensures we only match function header comments, not comments inside the code
46+
export function parseHeaderLine(line: string): ParsedLine {
2047
const match = line.match(/^ {2}\/\/\s*([a-z][a-z 0-9]*?):\s*(.*)$/)
2148
if (!match) {
2249
return { isHeader: false }
@@ -25,7 +52,6 @@ function parseHeaderLine(line) {
2552
const [, key, value] = match
2653
const keyLower = key.toLowerCase().trim()
2754

28-
// Only format recognized header keys
2955
if (!isValidHeaderKey(keyLower)) {
3056
return { isHeader: false }
3157
}
@@ -39,38 +65,29 @@ function parseHeaderLine(line) {
3965

4066
/**
4167
* Format a header line with proper alignment
42-
* @param {string} key - The header key
43-
* @param {string} value - The header value
44-
* @param {number} longestKeyLength - Length of longest key in this file
45-
* @returns {string} - Formatted line
4668
*/
47-
function formatHeaderLine(key, value, longestKeyLength) {
48-
// Padding = longest_key_length - current_key_length + 1 (minimum 1 space)
69+
export function formatHeaderLine(key: string, value: string, longestKeyLength: number): string {
4970
const padding = ' '.repeat(longestKeyLength - key.length + 1)
5071
return ` //${padding}${key}: ${value}`
5172
}
5273

5374
/**
5475
* Format all headers in a file's content
55-
* @param {string} content - File content
56-
* @returns {{ formatted: string, changed: boolean }}
5776
*/
58-
function formatHeaders(content) {
77+
export function formatHeaders(content: string): FormatResult {
5978
const lines = content.split('\n')
6079

61-
// First pass: find all header keys and determine the longest one in this file
6280
const parsedLines = lines.map((line) => parseHeaderLine(line))
63-
const headerKeys = parsedLines.filter((p) => p.isHeader).map((p) => p.key)
81+
const headerKeys = parsedLines.filter((p) => p.isHeader && p.key).map((p) => p.key as string)
6482
const longestKeyLength = headerKeys.length > 0 ? Math.max(...headerKeys.map((k) => k.length)) : 0
6583

66-
// Second pass: format with proper alignment
6784
let changed = false
68-
const formattedLines = []
85+
const formattedLines: string[] = []
6986

7087
for (let i = 0; i < lines.length; i++) {
7188
const parsed = parsedLines[i]
7289

73-
if (parsed.isHeader) {
90+
if (parsed.isHeader && parsed.key && parsed.value !== undefined) {
7491
const formatted = formatHeaderLine(parsed.key, parsed.value, longestKeyLength)
7592
if (formatted !== lines[i]) {
7693
changed = true
@@ -89,10 +106,8 @@ function formatHeaders(content) {
89106

90107
/**
91108
* Check if a file's headers are properly formatted
92-
* @param {string} filepath - Path to file
93-
* @returns {{ ok: boolean, filepath: string }}
94109
*/
95-
function checkFile(filepath) {
110+
export function checkFile(filepath: string): CheckResult {
96111
const content = fs.readFileSync(filepath, 'utf-8')
97112
const { changed } = formatHeaders(content)
98113
return {
@@ -103,10 +118,8 @@ function checkFile(filepath) {
103118

104119
/**
105120
* Format a file's headers in place
106-
* @param {string} filepath - Path to file
107-
* @returns {{ changed: boolean, filepath: string }}
108121
*/
109-
function formatFile(filepath) {
122+
export function formatFile(filepath: string): FormatFileResult {
110123
const content = fs.readFileSync(filepath, 'utf-8')
111124
const { formatted, changed } = formatHeaders(content)
112125

@@ -119,13 +132,11 @@ function formatFile(filepath) {
119132

120133
/**
121134
* Check all function files for proper header formatting
122-
* @param {string} srcDir - Source directory
123-
* @returns {{ ok: boolean, badFiles: string[] }}
124135
*/
125-
function checkAll(srcDir) {
136+
export function checkAll(srcDir: string): CheckAllResult {
126137
const pattern = [path.join(srcDir, '**/*.js'), '!**/index.js', '!**/_util/**']
127138
const files = globby.sync(pattern)
128-
const badFiles = []
139+
const badFiles: string[] = []
129140

130141
for (const file of files) {
131142
const result = checkFile(file)
@@ -142,13 +153,11 @@ function checkAll(srcDir) {
142153

143154
/**
144155
* Format all function files
145-
* @param {string} srcDir - Source directory
146-
* @returns {{ formattedCount: number, files: string[] }}
147156
*/
148-
function formatAll(srcDir) {
157+
export function formatAll(srcDir: string): FormatAllResult {
149158
const pattern = [path.join(srcDir, '**/*.js'), '!**/index.js', '!**/_util/**']
150159
const files = globby.sync(pattern)
151-
const formattedFiles = []
160+
const formattedFiles: string[] = []
152161

153162
for (const file of files) {
154163
const result = formatFile(file)
@@ -162,13 +171,3 @@ function formatAll(srcDir) {
162171
files: formattedFiles,
163172
}
164173
}
165-
166-
module.exports = {
167-
parseHeaderLine,
168-
formatHeaderLine,
169-
formatHeaders,
170-
checkFile,
171-
formatFile,
172-
checkAll,
173-
formatAll,
174-
}

0 commit comments

Comments
 (0)