Skip to content

Commit 0d0dbaf

Browse files
JLHwungmathiasbynens
authored andcommitted
perf: share type declarations via typesVersions
Every property directory shipped four identical declaration files (`ranges.d.mts`, `regex.d.mts`, `code-points.d.mts`, `symbols.d.mts`), about 4,000 files per package. They are now written once under `types/` and selected through a generated `typesVersions` map in package.json. Mapping directories (Case_Folding, Simple_Case_Mapping, Special_Casing, Decomposition_Mapping) keep their own `code-points`/`symbols` declarations, since their value types differ per directory; more specific patterns route them there. The build removes per-directory declarations left over from earlier runs. `decode-ranges.d.mts` now exports the `UnicodeRange` type at module level; previously `ranges.d.mts` imported a name that was only declared inside a namespace, so TypeScript consumers saw an error. The README now documents TypeScript 4.7 as the minimum, which is what `.d.mts` requires. Verified with TypeScript 5.9 (node16, bundler and node10 resolution) and TypeScript 4.7: imports resolve to the same types as before. A new test checks the declaration layout and the typesVersions targets. For Unicode 18.0.0 this shrinks the package from 378 KB to 312 KB packed (1.38 MB to 1.08 MB unpacked) and from 8,352 to 4,320 files. Assisted-By: Claude Fable 5.1 Issue: #109
1 parent f13f84c commit 0d0dbaf

10 files changed

Lines changed: 119 additions & 23 deletions

File tree

‎index.mjs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,14 +334,22 @@ const generateData = async (version) => {
334334
);
335335
fs.writeFileSync(
336336
path.resolve(__dirname, `output/unicode-${version}/package.json`),
337-
compilePackage({ version: version }),
337+
compilePackage({
338+
version: version,
339+
typesVersions: JSON.stringify(utils.typesVersions(dirMap), null, '\t')
340+
// Indent to match the surrounding JSON.
341+
.replace(/\n/g, '\n\t'),
342+
}),
338343
);
339344
fs.mkdirSync(
340345
path.resolve(__dirname, `output/unicode-${version}/.github/workflows`),
341346
{
342347
recursive: true,
343348
},
344349
);
350+
fs.mkdirSync(path.resolve(__dirname, `output/unicode-${version}/types`), {
351+
recursive: true,
352+
});
345353
const staticFiles = [
346354
'.github/workflows/publish-on-tag.yml',
347355
'.gitattributes',
@@ -357,6 +365,7 @@ const generateData = async (version) => {
357365
'decode-names.d.mts',
358366
'decode-mapping.mjs',
359367
'decode-mapping.d.mts',
368+
...utils.SHARED_DECLARATIONS.map((file) => `types/${file}.d.mts`),
360369
];
361370
for (const file of staticFiles) {
362371
fs.copyFileSync(

‎scripts/utils.mjs‎

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ const brotliInline = (data) => {
3535
return `JSON.parse(${brotliInlineBuffer(JSON.stringify(data))})`;
3636
};
3737

38+
// Modules whose declarations are shared via `types/*.d.mts` and
39+
// `typesVersions` instead of being repeated in every directory.
40+
const SHARED_DECLARATIONS = ['ranges', 'regex', 'code-points', 'symbols'];
41+
42+
// Directories under these types declare their own `code-points`/`symbols` types.
43+
const MAPPING_TYPES = [
44+
'Case_Folding',
45+
'Simple_Case_Mapping',
46+
'Special_Casing',
47+
'Decomposition_Mapping',
48+
];
49+
3850
const range = (start, stop) => {
3951
// Inclusive, e.g. `range(1, 3)` → `[1, 2, 3]`.
4052
const result = [];
@@ -183,11 +195,7 @@ const writeFiles = (options) => {
183195
const codePoints = map[item];
184196
const type =
185197
typeof options.type === 'function' ? options.type(item) : options.type;
186-
const isCodePointMapping =
187-
type === 'Case_Folding' ||
188-
type === 'Simple_Case_Mapping' ||
189-
type === 'Special_Casing' ||
190-
type === 'Decomposition_Mapping';
198+
const isCodePointMapping = MAPPING_TYPES.includes(type);
191199
const isNamesCanon = type === 'Names' && !subType;
192200
const isNameAliases = type === 'Names' && subType === 'name-aliases';
193201
const subdir = isNameAliases
@@ -259,18 +267,10 @@ const writeFiles = (options) => {
259267
path.resolve(dir, 'ranges.mjs'),
260268
`import decodeRanges from '../../decode-ranges.mjs';\n\nexport default decodeRanges('${encodedRanges}');\n`,
261269
);
262-
writeFileIfChanged(
263-
path.resolve(dir, 'ranges.d.mts'),
264-
'import type { UnicodeRange } from "../../decode-ranges.mjs";\n\ndeclare const ranges: UnicodeRange[];\nexport default ranges;\n',
265-
);
266270
writeFileIfChanged(
267271
path.resolve(dir, 'regex.mjs'),
268272
'export default /' + regenerate(codePoints).toString() + '/;\n',
269273
);
270-
writeFileIfChanged(
271-
path.resolve(dir, 'regex.d.mts'),
272-
'declare const regex: RegExp;\nexport default regex;\n',
273-
);
274274
if (codePointsSizeLt(codePoints, 10)) {
275275
const codePointsAsArray =
276276
codePoints instanceof regenerate ? codePoints.toArray() : codePoints;
@@ -314,15 +314,26 @@ const writeFiles = (options) => {
314314
path.resolve(dir, 'code-points.mjs'),
315315
codePointsFileContent,
316316
);
317-
writeFileIfChanged(
318-
path.resolve(dir, 'code-points.d.mts'),
319-
`declare const codePoints: ${codePointsType};\nexport default codePoints;\n`,
320-
);
321317
writeFileIfChanged(path.resolve(dir, 'symbols.mjs'), symbolsFileContent);
322-
writeFileIfChanged(
323-
path.resolve(dir, 'symbols.d.mts'),
324-
`declare const symbols: ${symbolsType};\nexport default symbols;\n`,
325-
);
318+
if (isCodePointMapping) {
319+
// Mapping directories declare their own types, since the value type
320+
// differs per directory (`Map<number, number>` vs. `Map<number, number[]>`).
321+
writeFileIfChanged(
322+
path.resolve(dir, 'code-points.d.mts'),
323+
`declare const codePoints: ${codePointsType};\nexport default codePoints;\n`,
324+
);
325+
writeFileIfChanged(
326+
path.resolve(dir, 'symbols.d.mts'),
327+
`declare const symbols: ${symbolsType};\nexport default symbols;\n`,
328+
);
329+
} else {
330+
// All other directories share the declarations in `types/`, selected
331+
// through `typesVersions` in package.json. Remove per-directory
332+
// declarations left over from earlier builds.
333+
for (const file of SHARED_DECLARATIONS) {
334+
fs.rmSync(path.resolve(dir, file + '.d.mts'), { force: true });
335+
}
336+
}
326337
}
327338
if (options.type === 'Bidi_Mirroring_Glyph') {
328339
const type = options.type;
@@ -415,7 +426,26 @@ const readDataFile = (version, type) => {
415426
}
416427
};
417428

429+
// The `typesVersions` map for package.json. TypeScript picks the most specific
430+
// matching pattern, so mapping directories keep their own declarations while
431+
// everything else resolves to the shared ones in `types/`.
432+
const typesVersions = (dirMap) => {
433+
const map = {};
434+
for (const type of MAPPING_TYPES) {
435+
if (hasKey(dirMap, type)) {
436+
map[`${type}/*/code-points.mjs`] = [`${type}/*/code-points.d.mts`];
437+
map[`${type}/*/symbols.mjs`] = [`${type}/*/symbols.d.mts`];
438+
}
439+
}
440+
for (const file of SHARED_DECLARATIONS) {
441+
map[`*/${file}.mjs`] = [`types/${file}.d.mts`];
442+
}
443+
return { '*': map };
444+
};
445+
418446
export default {
447+
SHARED_DECLARATIONS,
448+
typesVersions,
419449
range,
420450
append,
421451
extend,

‎static/decode-ranges.d.mts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,8 @@ declare function decodeRanges(input: string): decodeRanges.UnicodeRange[];
2222
*/
2323
declare function decodeDeltas(input: string): number[];
2424

25+
type UnicodeRange = decodeRanges.UnicodeRange;
26+
2527
export { decodeDeltas };
28+
export type { UnicodeRange };
2629
export default decodeRanges;

‎static/types/code-points.d.mts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare const codePoints: number[];
2+
export default codePoints;

‎static/types/ranges.d.mts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type { UnicodeRange } from '../decode-ranges.mjs';
2+
3+
declare const ranges: UnicodeRange[];
4+
export default ranges;

‎static/types/regex.d.mts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare const regex: RegExp;
2+
export default regex;

‎static/types/symbols.d.mts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
declare const symbols: string[];
2+
export default symbols;

‎templates/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ npm install @unicode/unicode-<%= version %> --save-dev
1212

1313
**Note:** _@unicode/unicode-<%= version %>_ is supposed to be used in build scripts (i.e. as a `devDependency`), and not at runtime (i.e. as a regular `dependency`).
1414

15+
TypeScript declarations are included. They require TypeScript 4.7 or newer, which is the first version that understands `.d.mts` declaration files.
16+
1517
## Regular expressions
1618

1719
The Unicode data modules ship with pre-compiled regular expressions for categories, scripts, script extensions, blocks, and properties. But maybe you want to create a single regular expression that combines several categories, scripts, etc. In that case, [***you should use Regenerate***](https://mths.be/regenerate). For example, to construct a regex that matches all symbols in the Arabic and Greek scripts as per Unicode v6.3.0:

‎templates/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@
2525
"type": "git",
2626
"url": "git+https://github.com/node-unicode/unicode-<%= version %>.git"
2727
},
28-
"bugs": "https://github.com/node-unicode/node-unicode-data/issues"
28+
"bugs": "https://github.com/node-unicode/node-unicode-data/issues",
29+
"typesVersions": <%= typesVersions %>
2930
}

‎tests/tests.mjs‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,47 @@ suite(`The generated latest Unicode js`, () => {
165165
}
166166
}
167167
});
168+
test('type declarations should be shared via typesVersions', async (t) => {
169+
const root = path.resolve(__dirname, '..', 'output', `unicode-${newest}`);
170+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json')));
171+
// Every typesVersions target that is not a wildcard must exist.
172+
for (const targets of Object.values(pkg.typesVersions['*'])) {
173+
for (const target of targets) {
174+
if (!target.includes('*')) {
175+
t.assert.ok(fs.existsSync(path.join(root, target)), target);
176+
}
177+
}
178+
}
179+
for (const type of fs.readdirSync(root)) {
180+
const typeDir = path.join(root, type);
181+
if (type.startsWith('.') || !fs.statSync(typeDir).isDirectory()) {
182+
continue;
183+
}
184+
const isMapping = Object.hasOwn(
185+
pkg.typesVersions['*'],
186+
`${type}/*/symbols.mjs`,
187+
);
188+
for (const name of fs.readdirSync(typeDir)) {
189+
const dir = path.join(typeDir, name);
190+
if (!fs.existsSync(path.join(dir, 'symbols.mjs'))) {
191+
continue;
192+
}
193+
// Mapping directories keep their own declarations; others share.
194+
t.assert.strictEqual(
195+
fs.existsSync(path.join(dir, 'symbols.d.mts')),
196+
isMapping,
197+
dir,
198+
);
199+
t.assert.strictEqual(
200+
fs.existsSync(path.join(dir, 'code-points.d.mts')),
201+
isMapping,
202+
dir,
203+
);
204+
t.assert.ok(!fs.existsSync(path.join(dir, 'ranges.d.mts')), dir);
205+
t.assert.ok(!fs.existsSync(path.join(dir, 'regex.d.mts')), dir);
206+
}
207+
}
208+
});
168209
test('every mapping symbols.mjs should match code-points.mjs', async (t) => {
169210
await assertSymbolsMatchCodePoints(t, newest);
170211
});

0 commit comments

Comments
 (0)