🌳 fix: make cereale/min tree-shake too, and correct what I overclaimed
An audit of the last commit found two real problems and several claims of mine that went further than the evidence. **cereale/min was not tree-shakable.** scripts/build-bundle.mjs used esbuild's `minify: true`, whose minifyWhitespace pass strips comments — /*#__PURE__*/ annotations included. The published entry point therefore reproduced exactly the bug the previous commit fixed: one decorator came out at 5,066 bytes with all 26 unrelated rule messages, against 1,837 from the per-module entry. Every source-level check stayed green, because they all bundled src/ and the annotations are stripped on the way into dist/. It is now minified for syntax and identifiers but not whitespace: 33.9 KB raw and 9.6 KB gzipped against 26.0/8.7, so about a kilobyte over the wire for a file that behaves correctly however it is used. One decorator via cereale/min is now 1,996 bytes. The build asserts the annotation count survives, and treeshake.test.ts now bundles the published artifact as well as the source — the gap that let this through. **sideEffects was partly inert.** `./dist/cjs/metadata.js` could never match: the build writes dist/cjs/package.json, which becomes the nearest descriptor for everything beneath it, so bundlers read sideEffects from there. That file now carries its own declaration. `./src/metadata.ts` was missing while src/ is published, which declared the Symbol.metadata install droppable in the source tree. Five module-scope caches in utils.ts are annotated for the same reason as the rules. I checked the audit's third blocker — that the Symbol.metadata install is dropped by bundlers — and it is not. It survives every case where it is load-bearing (a decorator import, toPlainSync, modelOf, and a decorated model bundled with an app). It is dropped only when importing nothing but flattenErrors, which needs no metadata, so that is correct. Corrections to my own wording: - "all three bundlers land within 20 bytes" held only for the one-decorator row; larger imports differ by up to a few hundred bytes - "measured through three bundlers, and pinned by a test" read as though the test covered all three; it covers esbuild, on source and on dist - "every rule is a top-level call" — 30 of the 68 are - the docs page "loads nothing from the network" — it fetches its own vendored compiler, same-origin, on first Run. It has no third-party dependencies, which is the claim I should have made - cereale/min's size, everywhere it appears Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SAcqrz3FcadkYr3xG32CjK
This commit is contained in:
+13
-7
@@ -9,16 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### `cereale/min` — one file, no bundler
|
### `cereale/min` — one file, no bundler
|
||||||
|
|
||||||
The whole library flattened into a single minified ES module: **25.5 KB, 8.6 KB gzipped**, for
|
The whole library flattened into a single minified ES module: **33.9 KB, 9.6 KB gzipped**, for
|
||||||
import maps, `<script type="module">`, Deno and Workers. It is built from `dist/esm/index.js`,
|
import maps, `<script type="module">`, Deno and Workers. It is built from `dist/esm/index.js`,
|
||||||
so the decorator lowering and the ES2025 target are whatever `tsc` produced — esbuild only
|
so the decorator lowering and the ES2025 target are whatever `tsc` produced — esbuild only
|
||||||
flattens and minifies.
|
flattens and minifies.
|
||||||
|
|
||||||
It is an addition, not a replacement, and the measurement is why. Bundled through esbuild the
|
It is an addition, not a replacement. The per-module build stays the default `import`: it keeps
|
||||||
flat and per-module builds produce consumer bundles within **2 bytes** of each other; through
|
readable stack traces for anyone not loading source maps, and it is what a bundler should be
|
||||||
rollup + terser the flat one is 165 bytes smaller; unused decorators tree-shake out of both.
|
given.
|
||||||
Since the size argument is a wash, the per-module build stays the default `import` for the one
|
|
||||||
thing it does better — readable stack traces for anyone not loading source maps.
|
The flat file is minified for syntax and identifiers but **not** whitespace. Full minification
|
||||||
|
strips comments — including the `/*#__PURE__*/` annotations below — which silently made
|
||||||
|
`cereale/min` un-tree-shakable: one decorator came out at 5,066 bytes against 1,837 from the
|
||||||
|
per-module entry, with all 26 unrelated rule messages back in the output. Keeping the
|
||||||
|
annotations costs about a kilobyte gzipped and is asserted by the build.
|
||||||
|
|
||||||
### Tree-shaking
|
### Tree-shaking
|
||||||
|
|
||||||
@@ -31,7 +35,9 @@ The cause is that every rule is a top-level call — `export const IsString = ru
|
|||||||
proves such a call side-effect-free by reading the factory, which is why rollup was already
|
proves such a call side-effect-free by reading the factory, which is why rollup was already
|
||||||
producing 1,823 bytes and hid the problem from a single-bundler measurement. esbuild and
|
producing 1,823 bytes and hid the problem from a single-bundler measurement. esbuild and
|
||||||
webpack will not do that analysis, and keep the call. Thirty declarations now carry
|
webpack will not do that analysis, and keep the call. Thirty declarations now carry
|
||||||
`/*#__PURE__*/`, and all three bundlers land within 20 bytes of each other.
|
`/*#__PURE__*/`. On the single-decorator import that exposed the problem the three bundlers now
|
||||||
|
land within 19 bytes of each other; on larger imports they still differ by up to a few hundred,
|
||||||
|
which is ordinary bundler variation rather than anything left unshaken.
|
||||||
|
|
||||||
Measured, minified, across esbuild / rollup / webpack:
|
Measured, minified, across esbuild / rollup / webpack:
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -328,7 +328,7 @@ await esbuild.build({
|
|||||||
|
|
||||||
## A single file, no bundler
|
## A single file, no bundler
|
||||||
|
|
||||||
`cereale/min` is the whole library flattened into one minified ES module (25.5 KB, 8.6 KB
|
`cereale/min` is the whole library flattened into one minified ES module (33.9 KB, 9.6 KB
|
||||||
gzipped) for import maps, `<script type="module">`, Deno and Workers.
|
gzipped) for import maps, `<script type="module">`, Deno and Workers.
|
||||||
|
|
||||||
**If you are using a bundler, do not use it.** It is the whole library in one file, so nothing
|
**If you are using a bundler, do not use it.** It is the whole library in one file, so nothing
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ user.greet(); // your methods are still there
|
|||||||
|
|
||||||
**[avalon-vanguard.github.io/cereale](https://avalon-vanguard.github.io/cereale/)** — an
|
**[avalon-vanguard.github.io/cereale](https://avalon-vanguard.github.io/cereale/)** — an
|
||||||
interactive playground that runs this library in your browser, the full decorator reference,
|
interactive playground that runs this library in your browser, the full decorator reference,
|
||||||
and the toolchain matrix. The page is self-contained and loads nothing from the network; it is
|
and the toolchain matrix. The page has no third-party dependencies — no CDN, no analytics, no webfonts; the only thing it
|
||||||
served from `docs/` on `main`, and `npm run build:docs` rebuilds its assets to open locally.
|
fetches is its own vendored compiler, and only when you first press Run. It is served from
|
||||||
|
`docs/` on `main`, and `npm run build:docs` rebuilds its assets to open locally.
|
||||||
|
|
||||||
## Where it fits
|
## Where it fits
|
||||||
|
|
||||||
@@ -498,29 +499,38 @@ it is one `Symbol.toStringTag` read per object.
|
|||||||
|
|
||||||
Cereale tree-shakes. Every rule is declared so that a bundler can drop the ones you did not
|
Cereale tree-shakes. Every rule is declared so that a bundler can drop the ones you did not
|
||||||
import, which matters for a library with 68 decorators — you pay for what you name and nothing
|
import, which matters for a library with 68 decorators — you pay for what you name and nothing
|
||||||
else. Minified bytes, measured through three bundlers, and
|
else. Minified bytes, measured through esbuild, rollup and webpack. The table was measured by hand;
|
||||||
[pinned by a test](src/treeshake.test.ts) so it cannot quietly regress:
|
what is [pinned by a test](src/treeshake.test.ts) is the property behind it — that a given
|
||||||
|
import drops the parts of the library it does not reach — checked through esbuild on both the
|
||||||
|
source and the published bundle:
|
||||||
|
|
||||||
| What you import | esbuild | rollup | webpack |
|
| What you import | esbuild | rollup | webpack |
|
||||||
| --- | ---: | ---: | ---: |
|
| --- | ---: | ---: | ---: |
|
||||||
| `flattenErrors` | 287 | 292 | 291 |
|
| `flattenErrors` | 287 | 292 | 291 |
|
||||||
| one decorator | 1,837 | 1,823 | 1,818 |
|
| one decorator | 1,837 | 1,823 | 1,818 |
|
||||||
| `validateSync` | 3,722 | 3,554 | 3,823 |
|
| `validateSync` | 3,722 | 3,554 | 3,738 |
|
||||||
| `toPlainSync` | 7,744 | 7,769 | 7,832 |
|
| `toPlainSync` | 7,744 | 7,769 | 7,771 |
|
||||||
| `toInstanceSync` | 7,900 | 7,942 | 7,956 |
|
| `toInstanceSync` | 7,900 | 7,942 | 7,944 |
|
||||||
| a typical DTO — 5 decorators, map, validate, format errors | 10,395 | 10,402 | 10,372 |
|
| a typical DTO — 5 decorators, map, validate, format errors | 10,395 | 10,402 | 10,360 |
|
||||||
| the whole library | 26,266 | 25,671 | 26,879 |
|
| the whole library | 26,266 | 25,671 | 26,879 |
|
||||||
|
|
||||||
The serializer and the deserializer drop independently: read JSON and you do not pay for
|
The serializer and the deserializer drop independently: read JSON and you do not pay for
|
||||||
writing it. The validator is kept by both, because `validate` defaults to `true` and the entry
|
writing it. The validator is kept by both, because `validate` defaults to `true` and the entry
|
||||||
points reference it whatever a given call site passes.
|
points reference it whatever a given call site passes.
|
||||||
|
|
||||||
This did not come for free. Every rule is a top-level call — `export const IsString = rule(…)` —
|
This did not come for free. Thirty of the rules are declared as top-level calls —
|
||||||
and rollup can prove such a call side-effect-free by reading the factory, but esbuild and
|
`export const IsString = rule(…)` — and rollup can prove such a call side-effect-free by reading
|
||||||
webpack will not. Without a `/*#__PURE__*/` annotation on each of them, importing one decorator
|
the factory, but esbuild and webpack will not. Without a `/*#__PURE__*/` annotation on each of
|
||||||
pulled in the message and validator of all 68: **4,909 bytes instead of 1,837**. Nothing failed;
|
them, importing one decorator pulled in the message and validator of all 68: **4,909 bytes
|
||||||
the library was simply three times heavier in every consumer's bundle, and the only way to find
|
instead of 1,837**. Nothing failed; the library was simply three times heavier in every
|
||||||
out was to measure.
|
consumer's bundle, and the only way to find out was to measure. Note what that means for
|
||||||
|
measuring: rollup alone would have shown nothing wrong.
|
||||||
|
|
||||||
|
`cereale/min` tree-shakes too, which took a second fix — esbuild's `minify` strips comments,
|
||||||
|
annotations included, so the flat bundle was silently reproducing the same bug (5,066 bytes for
|
||||||
|
one decorator). It is now minified for syntax and identifiers but not whitespace: 33.9 KB raw,
|
||||||
|
9.6 KB gzipped, about a kilobyte over the wire more than full minification would give. Still,
|
||||||
|
if you are using a bundler, import from `cereale` rather than `cereale/min`.
|
||||||
|
|
||||||
## Notes and Limitations
|
## Notes and Limitations
|
||||||
|
|
||||||
|
|||||||
+2
-2
File diff suppressed because one or more lines are too long
+2
-2
@@ -24,8 +24,8 @@
|
|||||||
},
|
},
|
||||||
"sideEffects": [
|
"sideEffects": [
|
||||||
"./dist/esm/metadata.js",
|
"./dist/esm/metadata.js",
|
||||||
"./dist/cjs/metadata.js",
|
"./dist/cereale.min.js",
|
||||||
"./dist/cereale.min.js"
|
"./src/metadata.ts"
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Flattens the ESM build into a single minified module.
|
* Flattens the ESM build into a single minified module.
|
||||||
*
|
*
|
||||||
* This is an *addition*, not a replacement. `dist/esm` stays the default `import`, because
|
* This is an *addition*, not a replacement. `dist/esm` stays the default `import`: it keeps
|
||||||
* measuring says the flat file buys a consumer nothing: bundled through esbuild the two come
|
* readable stack traces for anyone who does not load source maps, and it is what a bundler
|
||||||
* out within 2 bytes of each other, through rollup+terser the flat one is ~165 bytes smaller,
|
* should be handed.
|
||||||
* and unused decorators tree-shake out of both. What the per-module build keeps is readable
|
|
||||||
* stack traces for anyone who does not load source maps.
|
|
||||||
*
|
*
|
||||||
* Where the single file does earn its place is everywhere a bundler is not involved: a
|
* Where the single file does earn its place is everywhere a bundler is not involved: a
|
||||||
* `<script type="module">` tag, a CDN, an import map, Deno, or a Worker. That is what
|
* `<script type="module">` tag, a CDN, an import map, Deno, or a Worker. That is what
|
||||||
@@ -33,7 +31,17 @@ await build({
|
|||||||
bundle: true,
|
bundle: true,
|
||||||
format: 'esm',
|
format: 'esm',
|
||||||
target: 'esnext',
|
target: 'esnext',
|
||||||
minify: true,
|
// NOT `minify: true`. That turns on `minifyWhitespace`, which strips comments — including
|
||||||
|
// the /*#__PURE__*/ annotations that make the rules droppable. A bundler fed the fully
|
||||||
|
// minified file re-inherits the exact bug those annotations fixed: one decorator came out
|
||||||
|
// at 5,066 bytes against 1,837 from the per-module entry, with all 26 unrelated rule
|
||||||
|
// messages back in the output.
|
||||||
|
//
|
||||||
|
// Syntax and identifier minification keep them. The cost is 34.6 KB raw against 26.0 KB,
|
||||||
|
// but 9.8 KB gzipped against 8.7 KB — about a kilobyte over the wire, which is a fair price
|
||||||
|
// for a file that behaves correctly however someone ends up using it.
|
||||||
|
minifySyntax: true,
|
||||||
|
minifyIdentifiers: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
legalComments: 'none',
|
legalComments: 'none',
|
||||||
banner: { js: `/*! cereale ${pkg.version} | MIT | ${pkg.homepage} */` },
|
banner: { js: `/*! cereale ${pkg.version} | MIT | ${pkg.homepage} */` },
|
||||||
@@ -50,11 +58,32 @@ const emitted = await readFile(outfile, 'utf8');
|
|||||||
if (!emitted.includes(`cereale ${pkg.version}`)) {
|
if (!emitted.includes(`cereale ${pkg.version}`)) {
|
||||||
throw new Error('the bundle banner does not carry the current version');
|
throw new Error('the bundle banner does not carry the current version');
|
||||||
}
|
}
|
||||||
for (const name of ['toInstanceSync', 'IsString', 'standardDecorators']) {
|
for (const name of ['toInstanceSync', 'IsString']) {
|
||||||
if (name === 'standardDecorators') continue; // cereale/vite is a separate entry point
|
|
||||||
if (!emitted.includes(name)) throw new Error(`${name} is missing from the flat bundle`);
|
if (!emitted.includes(name)) throw new Error(`${name} is missing from the flat bundle`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The whole reason this file is not fully minified. Asserting it here means a future change to
|
||||||
|
// the minify options fails the build rather than silently tripling what a bundler keeps.
|
||||||
|
const annotations = (emitted.match(/__PURE__/g) ?? []).length;
|
||||||
|
const expected = (await readFile(path.join(dist, 'esm/decorators.js'), 'utf8').then(
|
||||||
|
(s) => (s.match(/__PURE__/g) ?? []).length
|
||||||
|
));
|
||||||
|
if (annotations < expected) {
|
||||||
|
throw new Error(
|
||||||
|
`the flat bundle kept ${annotations} /*#__PURE__*/ annotations but dist/esm/decorators.js has ` +
|
||||||
|
`${expected}. Minification stripped them, so anything bundling cereale/min would keep every ` +
|
||||||
|
'unused rule. Do not turn on minifyWhitespace here.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// dist/cjs/package.json is the nearest descriptor for every module beneath it, so bundlers
|
||||||
|
// read `sideEffects` from there rather than from the root manifest. Declaring it only at the
|
||||||
|
// root leaves the whole CommonJS build undeclared.
|
||||||
|
await writeFile(
|
||||||
|
path.join(dist, 'cjs/package.json'),
|
||||||
|
JSON.stringify({ type: 'commonjs', sideEffects: ['./metadata.js'] }, null, 2) + '\n'
|
||||||
|
);
|
||||||
|
|
||||||
const size = (await stat(outfile)).size;
|
const size = (await stat(outfile)).size;
|
||||||
const gzip = gzipSync(emitted).length;
|
const gzip = gzipSync(emitted).length;
|
||||||
console.log(`dist/cereale.min.js ${(size / 1024).toFixed(1)} KB (${(gzip / 1024).toFixed(1)} KB gzipped)`);
|
console.log(`dist/cereale.min.js ${(size / 1024).toFixed(1)} KB (${(gzip / 1024).toFixed(1)} KB gzipped)`);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { build } from 'esbuild';
|
import { build } from 'esbuild';
|
||||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -136,4 +137,37 @@ describe('tree-shaking', () => {
|
|||||||
// "shaken" result upstream means shaken rather than misspelled.
|
// "shaken" result upstream means shaken rather than misspelled.
|
||||||
expectShaken(code, Object.values(MARKER), []);
|
expectShaken(code, Object.values(MARKER), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The cases above bundle `src/`, which is where the annotations are written — so they cannot
|
||||||
|
* see what happens to them on the way into `dist/`. That is exactly where this broke: the
|
||||||
|
* flat bundle behind `cereale/min` was built with esbuild's `minify: true`, whose
|
||||||
|
* `minifyWhitespace` pass strips comments, annotations included. The published entry point
|
||||||
|
* kept all 26 unrelated rules (5,066 bytes against 1,837) while every source-level check
|
||||||
|
* stayed green.
|
||||||
|
*/
|
||||||
|
describe('the published artifacts', () => {
|
||||||
|
const dist = path.resolve('dist');
|
||||||
|
const built = existsSync(path.join(dist, 'cereale.min.js'));
|
||||||
|
|
||||||
|
it.runIf(built)('cereale/min tree-shakes as well as the per-module entry', async () => {
|
||||||
|
const code = await bundle(`
|
||||||
|
import { IsString } from ${JSON.stringify(path.join(dist, 'cereale.min.js'))};
|
||||||
|
export const d = IsString();
|
||||||
|
`.replace('CEREALE', 'unused'));
|
||||||
|
|
||||||
|
expectShaken(code, [MARKER.isString], [MARKER.isLatitude, MARKER.isSemVer, MARKER.serializer]);
|
||||||
|
expect(code.length).toBeLessThan(3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.runIf(built)('keeps its purity annotations through minification', async () => {
|
||||||
|
const flat = readFileSync(path.join(dist, 'cereale.min.js'), 'utf8');
|
||||||
|
const perModule = readFileSync(path.join(dist, 'esm/decorators.js'), 'utf8');
|
||||||
|
const count = (s: string) => (s.match(/__PURE__/g) ?? []).length;
|
||||||
|
|
||||||
|
expect(count(perModule), 'src annotations should reach dist/esm').toBeGreaterThan(20);
|
||||||
|
expect(count(flat), 'minification stripped the annotations from the flat bundle')
|
||||||
|
.toBeGreaterThanOrEqual(count(perModule));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-5
@@ -42,7 +42,7 @@ export class JsonMappingError extends Error {
|
|||||||
* next steps in a pollution chain. This library exists to parse request bodies, so the
|
* next steps in a pollution chain. This library exists to parse request bodies, so the
|
||||||
* transform layer drops them rather than trusting callers to sanitise first.
|
* transform layer drops them rather than trusting callers to sanitise first.
|
||||||
*/
|
*/
|
||||||
const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
const FORBIDDEN_KEYS = /*#__PURE__*/ new Set(['__proto__', 'constructor', 'prototype']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stands in for the value of a property that is never serialized, so that a failing password
|
* Stands in for the value of a property that is never serialized, so that a failing password
|
||||||
@@ -96,7 +96,7 @@ interface OutboundProperty {
|
|||||||
serializer?: any;
|
serializer?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const outboundCache = new WeakMap<ClassModel, { version: number; byStrategy: Map<unknown, Map<string, OutboundProperty>> }>();
|
const outboundCache = /*#__PURE__*/ new WeakMap<ClassModel, { version: number; byStrategy: Map<unknown, Map<string, OutboundProperty>> }>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves how one property is written out, memoized per (prototype, naming strategy).
|
* Resolves how one property is written out, memoized per (prototype, naming strategy).
|
||||||
@@ -167,7 +167,7 @@ interface InboundNames {
|
|||||||
|
|
||||||
// Name maps are derived purely from decorator metadata, which is fixed once a class is
|
// Name maps are derived purely from decorator metadata, which is fixed once a class is
|
||||||
// declared, so they are cached per (prototype, naming strategy).
|
// declared, so they are cached per (prototype, naming strategy).
|
||||||
const inboundCache = new WeakMap<ClassModel, { version: number; byStrategy: Map<unknown, InboundNames> }>();
|
const inboundCache = /*#__PURE__*/ new WeakMap<ClassModel, { version: number; byStrategy: Map<unknown, InboundNames> }>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the JSON-name -> property-key lookup used when reading a payload.
|
* Builds the JSON-name -> property-key lookup used when reading a payload.
|
||||||
@@ -614,7 +614,7 @@ interface CachedPlan {
|
|||||||
// Turning a class model into a per-property plan is cheap, but doing it on every call was
|
// Turning a class model into a per-property plan is cheap, but doing it on every call was
|
||||||
// measurably not: profiling showed roughly half of all validation time re-deriving answers
|
// measurably not: profiling showed roughly half of all validation time re-deriving answers
|
||||||
// that cannot change. Plans are memoized per model and invalidated by the model version.
|
// that cannot change. Plans are memoized per model and invalidated by the model version.
|
||||||
const planCache = new WeakMap<ClassModel, CachedPlan>();
|
const planCache = /*#__PURE__*/ new WeakMap<ClassModel, CachedPlan>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collapses rules that are genuinely identical.
|
* Collapses rules that are genuinely identical.
|
||||||
@@ -663,7 +663,7 @@ function validationPlan(model: ClassModel): PropertyPlan[] {
|
|||||||
|
|
||||||
// Serializers and deserializers are stateless by contract, so one instance per class is
|
// Serializers and deserializers are stateless by contract, so one instance per class is
|
||||||
// enough. Constructing a fresh one for every property of every object was pure waste.
|
// enough. Constructing a fresh one for every property of every object was pure waste.
|
||||||
const converterCache = new WeakMap<object, any>();
|
const converterCache = /*#__PURE__*/ new WeakMap<object, any>();
|
||||||
|
|
||||||
function converterFor(clazz: any): any {
|
function converterFor(clazz: any): any {
|
||||||
let instance = converterCache.get(clazz);
|
let instance = converterCache.get(clazz);
|
||||||
|
|||||||
Reference in New Issue
Block a user