@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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +473 -0
  3. package/dist/Vellum.d.ts +460 -0
  4. package/dist/Vellum.d.ts.map +1 -0
  5. package/dist/Vellum.js +479 -0
  6. package/dist/Vellum.js.map +1 -0
  7. package/dist/VellumProvider.d.ts +28 -0
  8. package/dist/VellumProvider.d.ts.map +1 -0
  9. package/dist/VellumProvider.js +33 -0
  10. package/dist/VellumProvider.js.map +1 -0
  11. package/dist/augmentations.d.ts +22 -0
  12. package/dist/augmentations.d.ts.map +1 -0
  13. package/dist/augmentations.js +16 -0
  14. package/dist/augmentations.js.map +1 -0
  15. package/dist/config.d.ts +33 -0
  16. package/dist/config.d.ts.map +1 -0
  17. package/dist/config.js +31 -0
  18. package/dist/config.js.map +1 -0
  19. package/dist/configure.d.ts +17 -0
  20. package/dist/configure.d.ts.map +1 -0
  21. package/dist/configure.js +78 -0
  22. package/dist/configure.js.map +1 -0
  23. package/dist/errors.d.ts +9 -0
  24. package/dist/errors.d.ts.map +1 -0
  25. package/dist/errors.js +19 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/index.d.ts +44 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +44 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/native/generated.d.ts +255 -0
  32. package/dist/native/generated.d.ts.map +1 -0
  33. package/dist/native/generated.js +7 -0
  34. package/dist/native/generated.js.map +1 -0
  35. package/dist/native.d.ts +50 -0
  36. package/dist/native.d.ts.map +1 -0
  37. package/dist/native.js +194 -0
  38. package/dist/native.js.map +1 -0
  39. package/dist/responder.d.ts +29 -0
  40. package/dist/responder.d.ts.map +1 -0
  41. package/dist/responder.js +101 -0
  42. package/dist/responder.js.map +1 -0
  43. package/dist/services/main.d.ts +15 -0
  44. package/dist/services/main.d.ts.map +1 -0
  45. package/dist/services/main.js +39 -0
  46. package/dist/services/main.js.map +1 -0
  47. package/dist/signers.d.ts +81 -0
  48. package/dist/signers.d.ts.map +1 -0
  49. package/dist/signers.js +98 -0
  50. package/dist/signers.js.map +1 -0
  51. package/index.darwin-arm64.node +0 -0
  52. package/index.darwin-x64.node +0 -0
  53. package/index.linux-arm64-gnu.node +0 -0
  54. package/index.linux-x64-gnu.node +0 -0
  55. package/index.win32-x64-msvc.node +0 -0
  56. package/package.json +67 -0
  57. package/scripts/build-napi-types.mjs +69 -0
  58. package/scripts/copy-napi.mjs +48 -0
  59. package/scripts/generate-metrics.py +150 -0
  60. package/scripts/generate-napi-types.mjs +156 -0
  61. package/src/Vellum.ts +829 -0
  62. package/src/VellumProvider.ts +51 -0
  63. package/src/augmentations.ts +25 -0
  64. package/src/config.ts +47 -0
  65. package/src/configure.ts +92 -0
  66. package/src/errors.ts +20 -0
  67. package/src/index.ts +74 -0
  68. package/src/native/generated.ts +375 -0
  69. package/src/native.ts +358 -0
  70. package/src/responder.ts +116 -0
  71. package/src/services/main.ts +48 -0
  72. 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
+ )