@c9up/vellum 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +473 -0
- package/dist/Vellum.d.ts +460 -0
- package/dist/Vellum.d.ts.map +1 -0
- package/dist/Vellum.js +479 -0
- package/dist/Vellum.js.map +1 -0
- package/dist/VellumProvider.d.ts +28 -0
- package/dist/VellumProvider.d.ts.map +1 -0
- package/dist/VellumProvider.js +33 -0
- package/dist/VellumProvider.js.map +1 -0
- package/dist/augmentations.d.ts +22 -0
- package/dist/augmentations.d.ts.map +1 -0
- package/dist/augmentations.js +16 -0
- package/dist/augmentations.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +31 -0
- package/dist/config.js.map +1 -0
- package/dist/configure.d.ts +17 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +78 -0
- package/dist/configure.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +19 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/native/generated.d.ts +255 -0
- package/dist/native/generated.d.ts.map +1 -0
- package/dist/native/generated.js +7 -0
- package/dist/native/generated.js.map +1 -0
- package/dist/native.d.ts +50 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +194 -0
- package/dist/native.js.map +1 -0
- package/dist/responder.d.ts +29 -0
- package/dist/responder.d.ts.map +1 -0
- package/dist/responder.js +101 -0
- package/dist/responder.js.map +1 -0
- package/dist/services/main.d.ts +15 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +39 -0
- package/dist/services/main.js.map +1 -0
- package/dist/signers.d.ts +81 -0
- package/dist/signers.d.ts.map +1 -0
- package/dist/signers.js +98 -0
- package/dist/signers.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +67 -0
- package/scripts/build-napi-types.mjs +69 -0
- package/scripts/copy-napi.mjs +48 -0
- package/scripts/generate-metrics.py +150 -0
- package/scripts/generate-napi-types.mjs +156 -0
- package/src/Vellum.ts +829 -0
- package/src/VellumProvider.ts +51 -0
- package/src/augmentations.ts +25 -0
- package/src/config.ts +47 -0
- package/src/configure.ts +92 -0
- package/src/errors.ts +20 -0
- package/src/index.ts +74 -0
- package/src/native/generated.ts +375 -0
- package/src/native.ts +358 -0
- package/src/responder.ts +116 -0
- package/src/services/main.ts +48 -0
- package/src/signers.ts +149 -0
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@c9up/vellum",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "PDF toolkit — convert PDF pages to images, author and inspect documents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"LICENSE",
|
|
10
|
+
"README.md",
|
|
11
|
+
"dist",
|
|
12
|
+
"index.*.node",
|
|
13
|
+
"scripts",
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./provider": {
|
|
22
|
+
"types": "./dist/VellumProvider.d.ts",
|
|
23
|
+
"import": "./dist/VellumProvider.js"
|
|
24
|
+
},
|
|
25
|
+
"./configure": {
|
|
26
|
+
"types": "./dist/configure.d.ts",
|
|
27
|
+
"import": "./dist/configure.js"
|
|
28
|
+
},
|
|
29
|
+
"./services/main": {
|
|
30
|
+
"types": "./dist/services/main.d.ts",
|
|
31
|
+
"import": "./dist/services/main.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "^2.4.10",
|
|
36
|
+
"@c9up/ream": "^0.2.0",
|
|
37
|
+
"@types/node": "^22.19.15",
|
|
38
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
39
|
+
"typescript": "^6.0.2",
|
|
40
|
+
"vitest": "^4.1.2"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/C9up/vellum.git"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@c9up/ream": "^0.2.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"@c9up/ream": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsc -p tsconfig.build.json",
|
|
59
|
+
"build:napi": "cargo build --release -p vellum-engine-napi && pnpm build:napi-types && node scripts/copy-napi.mjs",
|
|
60
|
+
"build:napi-types": "node scripts/build-napi-types.mjs",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"test:rust": "cargo test",
|
|
63
|
+
"lint": "biome check src/ tests/",
|
|
64
|
+
"test:coverage": "vitest run --coverage",
|
|
65
|
+
"typecheck": "tsc --noEmit"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Regenerate `src/native/generated.ts` from the Rust.
|
|
4
|
+
*
|
|
5
|
+
* Node rather than a shell script because this runs inside `build:napi`, and
|
|
6
|
+
* that runs on the Windows prebuild runner too: a bash `mktemp` there hands
|
|
7
|
+
* cargo a `/tmp/...` path the native proc-macro cannot write to, so the
|
|
8
|
+
* type-def file comes back empty and the build fails for no visible reason.
|
|
9
|
+
*
|
|
10
|
+
* One crate at a time on purpose: with napi-derive 2 the writes all went to a
|
|
11
|
+
* single `TYPE_DEF_TMP_PATH` and a parallel build interleaved them
|
|
12
|
+
* while cargo compiles, and a parallel build interleaves the writes —
|
|
13
|
+
* definitions go missing, silently, and the generated file comes out short.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execFileSync } from 'node:child_process'
|
|
17
|
+
import { createRequire } from 'node:module'
|
|
18
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
19
|
+
import { tmpdir } from 'node:os'
|
|
20
|
+
import { dirname, join } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
|
|
23
|
+
const CRATES = ['vellum-engine-napi']
|
|
24
|
+
|
|
25
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
26
|
+
const output = join('src', 'native', 'generated.ts')
|
|
27
|
+
const scratch = mkdtempSync(join(tmpdir(), 'napi-types-'))
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const lines = []
|
|
31
|
+
for (const crate of CRATES) {
|
|
32
|
+
const perCrate = join(scratch, crate)
|
|
33
|
+
writeFileSync(perCrate, '')
|
|
34
|
+
execFileSync('cargo', ['build', '-p', crate], {
|
|
35
|
+
cwd: packageRoot,
|
|
36
|
+
env: { ...process.env, NAPI_TYPE_DEF_TMP_FOLDER: scratch },
|
|
37
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
38
|
+
})
|
|
39
|
+
const emitted = readFileSync(perCrate, 'utf8').split('\n').filter(Boolean)
|
|
40
|
+
if (emitted.length === 0) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`[napi-types] ${crate} emitted nothing — is napi-derive's "type-def" feature on?`,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
lines.push(...emitted)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const combined = join(scratch, 'combined.jsonl')
|
|
49
|
+
writeFileSync(combined, `${lines.join('\n')}\n`)
|
|
50
|
+
|
|
51
|
+
execFileSync(
|
|
52
|
+
process.execPath,
|
|
53
|
+
[join('scripts', 'generate-napi-types.mjs'), combined, output],
|
|
54
|
+
{ cwd: packageRoot, stdio: 'inherit' },
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// Formatted here rather than excluded from the linter: the file is checked
|
|
58
|
+
// in, so it should read like the rest of the tree — and formatting it at
|
|
59
|
+
// generation means it never shows up as a diff someone has to fix by hand.
|
|
60
|
+
// Resolved through the package entry rather than `node_modules/.bin`, whose
|
|
61
|
+
// shim is a shell script the Windows runner cannot execute directly.
|
|
62
|
+
const biome = createRequire(import.meta.url).resolve('@biomejs/biome/bin/biome')
|
|
63
|
+
execFileSync(process.execPath, [biome, 'format', '--write', output], {
|
|
64
|
+
cwd: packageRoot,
|
|
65
|
+
stdio: ['ignore', 'ignore', 'inherit'],
|
|
66
|
+
})
|
|
67
|
+
} finally {
|
|
68
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
69
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { copyFileSync, existsSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { arch, env, platform } from 'node:process'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
const root = join(here, '..')
|
|
8
|
+
const CRATE = 'vellum_engine_napi'
|
|
9
|
+
const TAG = '[vellum:napi]'
|
|
10
|
+
|
|
11
|
+
const tripleMap = {
|
|
12
|
+
'x86_64-unknown-linux-gnu': { suffix: 'linux-x64-gnu', os: 'linux' },
|
|
13
|
+
'aarch64-unknown-linux-gnu': { suffix: 'linux-arm64-gnu', os: 'linux' },
|
|
14
|
+
'x86_64-apple-darwin': { suffix: 'darwin-x64', os: 'darwin' },
|
|
15
|
+
'aarch64-apple-darwin': { suffix: 'darwin-arm64', os: 'darwin' },
|
|
16
|
+
'x86_64-pc-windows-msvc': { suffix: 'win32-x64-msvc', os: 'win32' },
|
|
17
|
+
}
|
|
18
|
+
const hostSuffixMap = {
|
|
19
|
+
'linux-x64': 'linux-x64-gnu', 'linux-arm64': 'linux-arm64-gnu',
|
|
20
|
+
'darwin-x64': 'darwin-x64', 'darwin-arm64': 'darwin-arm64', 'win32-x64': 'win32-x64-msvc',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const triple = env.CARGO_BUILD_TARGET ?? ''
|
|
24
|
+
let suffix, os, releaseDir
|
|
25
|
+
if (triple) {
|
|
26
|
+
const entry = tripleMap[triple]
|
|
27
|
+
if (!entry) throw new Error(`${TAG} unsupported CARGO_BUILD_TARGET: ${triple}`)
|
|
28
|
+
suffix = entry.suffix; os = entry.os
|
|
29
|
+
releaseDir = join(root, 'target', triple, 'release')
|
|
30
|
+
} else {
|
|
31
|
+
suffix = hostSuffixMap[`${platform}-${arch}`]; os = platform
|
|
32
|
+
releaseDir = join(root, 'target', 'release')
|
|
33
|
+
if (!suffix) throw new Error(`${TAG} unsupported platform/arch: ${platform}-${arch}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const candidates =
|
|
37
|
+
os === 'win32'
|
|
38
|
+
? [join(releaseDir, `${CRATE}.dll`), join(releaseDir, `lib${CRATE}.dll`)]
|
|
39
|
+
: os === 'darwin'
|
|
40
|
+
? [join(releaseDir, `lib${CRATE}.dylib`)]
|
|
41
|
+
: [join(releaseDir, `lib${CRATE}.so`)]
|
|
42
|
+
|
|
43
|
+
const source = candidates.find((candidate) => existsSync(candidate))
|
|
44
|
+
if (!source) throw new Error(`${TAG} native library not found. Looked for:\n${candidates.map((p) => `- ${p}`).join('\n')}`)
|
|
45
|
+
|
|
46
|
+
const target = join(root, `index.${suffix}.node`)
|
|
47
|
+
copyFileSync(source, target)
|
|
48
|
+
console.log(`${TAG} copied ${source} -> ${target}`)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Derive the standard-font advance widths in crates/vellum-engine/src/metrics.rs.
|
|
2
|
+
|
|
3
|
+
A standard font is referenced without being embedded, so the reader lays the
|
|
4
|
+
text out with the published Adobe metrics. The URW base-35 fonts are the
|
|
5
|
+
metric-compatible clones of those 14, which is what makes them usable as a
|
|
6
|
+
source. The result is cross-checked here against published Adobe values, and
|
|
7
|
+
again by the tests in metrics.rs — so a table that drifted would not compile
|
|
8
|
+
its way into a release.
|
|
9
|
+
|
|
10
|
+
Run it only to regenerate the table; the build does not need it. It wants the
|
|
11
|
+
AFM files (Fedora: urw-base35-fonts-legacy, Debian: fonts-urw-base35) and
|
|
12
|
+
fonttools, for the Adobe Glyph List.
|
|
13
|
+
|
|
14
|
+
python3 scripts/generate-metrics.py /tmp/widths.py
|
|
15
|
+
"""
|
|
16
|
+
import re
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from fontTools.agl import AGL2UV
|
|
21
|
+
|
|
22
|
+
AFM = Path("/usr/share/fonts/urw-base35")
|
|
23
|
+
|
|
24
|
+
FONTS = [
|
|
25
|
+
("Helvetica", "NimbusSans-Regular"),
|
|
26
|
+
("HelveticaBold", "NimbusSans-Bold"),
|
|
27
|
+
("HelveticaOblique", "NimbusSans-Italic"),
|
|
28
|
+
("TimesRoman", "NimbusRoman-Regular"),
|
|
29
|
+
("TimesBold", "NimbusRoman-Bold"),
|
|
30
|
+
("TimesItalic", "NimbusRoman-Italic"),
|
|
31
|
+
("Courier", "NimbusMonoPS-Regular"),
|
|
32
|
+
("CourierBold", "NimbusMonoPS-Bold"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Unicode -> AGL glyph name, inverted from the AGL itself rather than typed.
|
|
36
|
+
UV2NAME = {}
|
|
37
|
+
for name, uv in AGL2UV.items():
|
|
38
|
+
UV2NAME.setdefault(uv, name)
|
|
39
|
+
|
|
40
|
+
# WinAnsi is Latin-1 except over 0x80-0x9F. This block is the ONLY hand-written
|
|
41
|
+
# data here, and it is cross-checked against the Rust encoder below.
|
|
42
|
+
HIGH = {
|
|
43
|
+
0x80: 0x20AC, 0x82: 0x201A, 0x83: 0x0192, 0x84: 0x201E, 0x85: 0x2026,
|
|
44
|
+
0x86: 0x2020, 0x87: 0x2021, 0x88: 0x02C6, 0x89: 0x2030, 0x8A: 0x0160,
|
|
45
|
+
0x8B: 0x2039, 0x8C: 0x0152, 0x8E: 0x017D, 0x91: 0x2018, 0x92: 0x2019,
|
|
46
|
+
0x93: 0x201C, 0x94: 0x201D, 0x95: 0x2022, 0x96: 0x2013, 0x97: 0x2014,
|
|
47
|
+
0x98: 0x02DC, 0x99: 0x2122, 0x9A: 0x0161, 0x9B: 0x203A, 0x9C: 0x0153,
|
|
48
|
+
0x9E: 0x017E, 0x9F: 0x0178,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def win_ansi_names():
|
|
53
|
+
"""WinAnsi byte -> AFM glyph name."""
|
|
54
|
+
names = {}
|
|
55
|
+
for code in range(0x20, 0x7F):
|
|
56
|
+
names[code] = UV2NAME[code]
|
|
57
|
+
for code, uv in HIGH.items():
|
|
58
|
+
names[code] = UV2NAME[uv]
|
|
59
|
+
for code in range(0xA0, 0x100):
|
|
60
|
+
if code in UV2NAME:
|
|
61
|
+
names[code] = UV2NAME[code]
|
|
62
|
+
# Adobe's WinAnsiEncoding gives these the width of the glyph they stand in
|
|
63
|
+
# for: a no-break space is a space, a soft hyphen is a hyphen.
|
|
64
|
+
names[0xA0] = "space"
|
|
65
|
+
names[0xAD] = "hyphen"
|
|
66
|
+
# AGLFN drops the superscript digits as deprecated, but WinAnsi has them
|
|
67
|
+
# and every AFM names them the old way.
|
|
68
|
+
names[0xB2] = "twosuperior"
|
|
69
|
+
names[0xB3] = "threesuperior"
|
|
70
|
+
names[0xB9] = "onesuperior"
|
|
71
|
+
return names
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def widths_of(afm_name):
|
|
75
|
+
"""Glyph name -> advance width, in 1/1000 of an em."""
|
|
76
|
+
widths = {}
|
|
77
|
+
text = (AFM / f"{afm_name}.afm").read_text(encoding="latin-1")
|
|
78
|
+
for line in text.splitlines():
|
|
79
|
+
if not line.startswith("C "):
|
|
80
|
+
continue
|
|
81
|
+
width = re.search(r"WX\s+(-?\d+)\s*;", line)
|
|
82
|
+
name = re.search(r"N\s+(\S+)\s*;", line)
|
|
83
|
+
if width and name:
|
|
84
|
+
widths[name.group(1)] = int(width.group(1))
|
|
85
|
+
return widths
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main():
|
|
89
|
+
names = win_ansi_names()
|
|
90
|
+
tables = {}
|
|
91
|
+
missing = []
|
|
92
|
+
for font, afm in FONTS:
|
|
93
|
+
widths = widths_of(afm)
|
|
94
|
+
table = [0] * 256
|
|
95
|
+
for code, name in names.items():
|
|
96
|
+
if name in widths:
|
|
97
|
+
table[code] = widths[name]
|
|
98
|
+
else:
|
|
99
|
+
missing.append((font, code, name))
|
|
100
|
+
tables[font] = table
|
|
101
|
+
|
|
102
|
+
if missing:
|
|
103
|
+
print(f"MISSING {len(missing)} glyphs, first 10: {missing[:10]}", file=sys.stderr)
|
|
104
|
+
|
|
105
|
+
# Cross-check against the published Adobe AFM values. If the URW clones had
|
|
106
|
+
# drifted, or the glyph-name mapping were wrong, these would not line up.
|
|
107
|
+
known = {
|
|
108
|
+
("Helvetica", " "): 278, ("Helvetica", "A"): 667, ("Helvetica", "W"): 944,
|
|
109
|
+
("Helvetica", "i"): 222, ("Helvetica", "M"): 833, ("Helvetica", "a"): 556,
|
|
110
|
+
("Helvetica", "."): 278, ("Helvetica", "0"): 556,
|
|
111
|
+
("HelveticaBold", " "): 278, ("HelveticaBold", "A"): 722,
|
|
112
|
+
("HelveticaBold", "a"): 556, ("HelveticaBold", "i"): 278,
|
|
113
|
+
("TimesRoman", " "): 250, ("TimesRoman", "A"): 722, ("TimesRoman", "a"): 444,
|
|
114
|
+
("TimesRoman", "W"): 944, ("TimesRoman", "i"): 278, ("TimesRoman", "."): 250,
|
|
115
|
+
("TimesBold", "A"): 722, ("TimesBold", "a"): 500,
|
|
116
|
+
("TimesItalic", "A"): 611, ("TimesItalic", "a"): 500,
|
|
117
|
+
("Courier", "A"): 600, ("Courier", "i"): 600, ("Courier", " "): 600,
|
|
118
|
+
("CourierBold", "W"): 600,
|
|
119
|
+
}
|
|
120
|
+
bad = []
|
|
121
|
+
for (font, char), expected in known.items():
|
|
122
|
+
got = tables[font][ord(char)]
|
|
123
|
+
if got != expected:
|
|
124
|
+
bad.append((font, char, expected, got))
|
|
125
|
+
if bad:
|
|
126
|
+
print(f"MISMATCH: {bad}", file=sys.stderr)
|
|
127
|
+
return 1
|
|
128
|
+
|
|
129
|
+
# Helvetica-Oblique must share Helvetica's widths; that is what makes it a
|
|
130
|
+
# slanted Helvetica rather than a different font.
|
|
131
|
+
assert tables["HelveticaOblique"] == tables["Helvetica"], "oblique drifted"
|
|
132
|
+
assert all(w in (0, 600) for w in tables["Courier"]), "Courier is not monospace"
|
|
133
|
+
# Every WinAnsi code except the control range, DEL and the five the
|
|
134
|
+
# encoding leaves undefined.
|
|
135
|
+
undefined = {0x7F, 0x81, 0x8D, 0x8F, 0x90, 0x9D}
|
|
136
|
+
for font, _ in FONTS:
|
|
137
|
+
for code in range(0x20, 0x100):
|
|
138
|
+
has = tables[font][code] != 0
|
|
139
|
+
assert has == (code not in undefined), f"{font} code {code:#x}"
|
|
140
|
+
|
|
141
|
+
print(f"OK — {len(known)} known values matched, {len(names)} codes covered")
|
|
142
|
+
for font, _ in FONTS:
|
|
143
|
+
defined = sum(1 for w in tables[font] if w)
|
|
144
|
+
print(f" {font}: {defined} codes")
|
|
145
|
+
Path(sys.argv[1]).write_text(repr(tables))
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
sys.exit(main())
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate the TypeScript surface of the native module FROM the Rust.
|
|
4
|
+
*
|
|
5
|
+
* `napi-derive`'s `type-def` feature emits one JSON line per exported item
|
|
6
|
+
* while cargo compiles. Those lines are the source of truth: they are derived
|
|
7
|
+
* from the `#[napi]` items themselves, so a signature cannot drift from the
|
|
8
|
+
* Rust without this file changing.
|
|
9
|
+
*
|
|
10
|
+
* Hand-written interfaces are what this replaces, and they do drift — nothing
|
|
11
|
+
* on the TypeScript side notices when a `pub fn` gains a parameter or stops
|
|
12
|
+
* being `async`.
|
|
13
|
+
*
|
|
14
|
+
* One thing napi-rs cannot infer: the shape of a `JsFunction` callback. It
|
|
15
|
+
* emits `(...args: any[]) => any`. Any such parameter is refined below, by
|
|
16
|
+
* name, and the script fails if a refinement no longer matches anything — so
|
|
17
|
+
* the table cannot rot either.
|
|
18
|
+
*
|
|
19
|
+
* TYPE_DEF_TMP_PATH=<file> cargo build -p <crate>
|
|
20
|
+
* node scripts/generate-napi-types.mjs <file> <out.d.ts>
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Callback signatures napi-rs erases to `any`, restored here.
|
|
27
|
+
*
|
|
28
|
+
* Keyed by `<owner>.<method>`; the value replaces the whole parameter. Every
|
|
29
|
+
* entry must match, or the script fails: a stale refinement is how a hand
|
|
30
|
+
* annotation quietly stops describing the Rust.
|
|
31
|
+
*
|
|
32
|
+
* Empty while this crate exposes no `JsFunction` parameter — add an entry the
|
|
33
|
+
* day one appears, rather than letting an `any` through.
|
|
34
|
+
*/
|
|
35
|
+
const CALLBACK_REFINEMENTS = {}
|
|
36
|
+
|
|
37
|
+
const [input, output] = process.argv.slice(2)
|
|
38
|
+
if (!input || !output) {
|
|
39
|
+
console.error('usage: generate-napi-types.mjs <type-def file> <out.d.ts>')
|
|
40
|
+
process.exit(2)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const entries = readFileSync(input, 'utf8')
|
|
44
|
+
.split('\n')
|
|
45
|
+
.map((l) => l.trim())
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
.map((l) => JSON.parse(l))
|
|
48
|
+
|
|
49
|
+
const used = new Set()
|
|
50
|
+
|
|
51
|
+
/** Apply the refinements that belong to `owner`, tracking which ones matched. */
|
|
52
|
+
function refine(owner, body) {
|
|
53
|
+
let out = body
|
|
54
|
+
for (const [key, { from, to }] of Object.entries(CALLBACK_REFINEMENTS)) {
|
|
55
|
+
const [entryOwner] = key.split('.')
|
|
56
|
+
if (entryOwner !== owner) continue
|
|
57
|
+
if (out.includes(from)) {
|
|
58
|
+
out = out.replace(from, to)
|
|
59
|
+
used.add(key)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* JSDoc as emitted by napi-derive, indented to sit above its member.
|
|
67
|
+
*
|
|
68
|
+
* `*` followed by `/` inside the text closes the comment early — a Rust doc
|
|
69
|
+
* example holding a cron expression (`0 */5 * * *`) is enough to do it, and
|
|
70
|
+
* the generated file then fails to parse. Escaped rather than stripped, so the
|
|
71
|
+
* example still reads correctly.
|
|
72
|
+
*/
|
|
73
|
+
function docBlock(doc, indent = '') {
|
|
74
|
+
if (!doc) return ''
|
|
75
|
+
// Escape every `*/` EXCEPT the one that closes the block. A Rust doc example
|
|
76
|
+
// holding a cron expression (`0 */5 * * *`) closes the comment early
|
|
77
|
+
// otherwise, and the generated file stops parsing — but escaping the closer
|
|
78
|
+
// too breaks it just as thoroughly, whether the block spans one line or many.
|
|
79
|
+
const closer = doc.lastIndexOf('*/')
|
|
80
|
+
const escaped =
|
|
81
|
+
closer === -1
|
|
82
|
+
? doc
|
|
83
|
+
: doc.slice(0, closer).replaceAll('*/', '*\\/') + doc.slice(closer)
|
|
84
|
+
return (
|
|
85
|
+
escaped
|
|
86
|
+
.split('\n')
|
|
87
|
+
.filter((l) => l.length > 0)
|
|
88
|
+
.map((l) => `${indent}${l}`)
|
|
89
|
+
.join('\n') + '\n'
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const interfaces = entries.filter((e) => e.kind === 'interface')
|
|
94
|
+
const structs = entries.filter((e) => e.kind === 'struct')
|
|
95
|
+
const impls = new Map(entries.filter((e) => e.kind === 'impl').map((e) => [e.name, e]))
|
|
96
|
+
const fns = entries.filter((e) => e.kind === 'fn')
|
|
97
|
+
|
|
98
|
+
const out = [
|
|
99
|
+
'// GENERATED FROM THE RUST — do not edit.',
|
|
100
|
+
'//',
|
|
101
|
+
"// Produced by scripts/generate-napi-types.mjs from napi-derive's type-def",
|
|
102
|
+
'// output. Editing this file by hand puts it back where it started: a',
|
|
103
|
+
'// description that can disagree with the code it describes.',
|
|
104
|
+
'',
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
for (const iface of interfaces) {
|
|
108
|
+
out.push(docBlock(iface.js_doc))
|
|
109
|
+
out.push(`export interface ${iface.name} {`)
|
|
110
|
+
for (const line of iface.def.split('\n')) {
|
|
111
|
+
out.push(line ? ` ${line.trim()}` : '')
|
|
112
|
+
}
|
|
113
|
+
out.push('}', '')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const struct of structs) {
|
|
117
|
+
const impl = impls.get(struct.name)
|
|
118
|
+
out.push(docBlock(struct.js_doc))
|
|
119
|
+
out.push(`export declare class ${struct.name} {`)
|
|
120
|
+
if (impl) {
|
|
121
|
+
for (const line of refine(struct.name, impl.def).split('\n')) {
|
|
122
|
+
out.push(line ? ` ${line.trim()}` : '')
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
out.push('}', '')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const fn of fns) {
|
|
129
|
+
out.push(docBlock(fn.js_doc))
|
|
130
|
+
// napi-derive emits the whole declaration for a function, unlike a struct
|
|
131
|
+
// where `def` holds only the members.
|
|
132
|
+
const declaration = fn.def.trim()
|
|
133
|
+
// napi-derive 3 emits a bare `function name(...)` where 2 emitted the
|
|
134
|
+
// signature after the name; concatenating onto the former produced
|
|
135
|
+
// `function xfunction x(...)`.
|
|
136
|
+
const rendered = declaration.startsWith('export declare function')
|
|
137
|
+
? declaration
|
|
138
|
+
: declaration.startsWith('function ')
|
|
139
|
+
? `export declare ${declaration}`
|
|
140
|
+
: `export declare function ${fn.name}${declaration}`
|
|
141
|
+
out.push(`${rendered};`, '')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const stale = Object.keys(CALLBACK_REFINEMENTS).filter((k) => !used.has(k))
|
|
145
|
+
if (stale.length > 0) {
|
|
146
|
+
console.error(
|
|
147
|
+
`[napi-types] refinement(s) that matched nothing: ${stale.join(', ')}\n` +
|
|
148
|
+
'[napi-types] the Rust changed under them — update or remove the entry.',
|
|
149
|
+
)
|
|
150
|
+
process.exit(1)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
writeFileSync(output, out.join('\n'))
|
|
154
|
+
console.log(
|
|
155
|
+
`[napi-types] ${output} — ${interfaces.length} interface(s), ${structs.length} class(es), ${fns.length} function(s)`,
|
|
156
|
+
)
|