From 42f1482d7f80d085ef085cfe3315da070a81c753 Mon Sep 17 00:00:00 2001 From: Alina Sireneva Date: Mon, 4 Mar 2024 20:59:27 +0300 Subject: [PATCH] build(core): improved tree-shakeability --- packages/core/build.config.cjs | 55 ++++++++++++++++++- .../core/src/highlevel/utils/inspectable.ts | 8 ++- packages/core/src/highlevel/utils/memoize.ts | 4 +- packages/core/src/utils/links/common.ts | 2 +- scripts/build-package.js | 4 +- 5 files changed, 68 insertions(+), 5 deletions(-) diff --git a/packages/core/build.config.cjs b/packages/core/build.config.cjs index 1c80ab09..c9699247 100644 --- a/packages/core/build.config.cjs +++ b/packages/core/build.config.cjs @@ -1,4 +1,6 @@ -module.exports = ({ path, transformFile, packageDir, outDir }) => ({ +const KNOWN_DECORATORS = ['memoizeGetters', 'makeInspectable'] + +module.exports = ({ path, glob, transformFile, packageDir, outDir }) => ({ esmOnlyDirectives: true, esmImportDirectives: true, final() { @@ -7,5 +9,56 @@ module.exports = ({ path, transformFile, packageDir, outDir }) => ({ transformFile(path.join(outDir, 'cjs/network/network-manager.js'), replaceVersion) transformFile(path.join(outDir, 'esm/network/network-manager.js'), replaceVersion) + + // make decorators properly tree-shakeable + // very fragile, but it works for now :D + const decoratorsRegex = new RegExp( + `(${KNOWN_DECORATORS.join('|')})\\((.+?)\\);`, + 'gs', + ) + + const replaceDecorators = (content, file) => { + if (!KNOWN_DECORATORS.some((d) => content.includes(d))) return null + + const countPerClass = new Map() + + content = content.replace(decoratorsRegex, (_, name, args) => { + const [clsName_, ...rest] = args.split(',') + const clsName = clsName_.trim() + + const count = (countPerClass.get(clsName) || 0) + 1 + countPerClass.set(clsName, count) + + const prevName = count === 1 ? clsName : `${clsName}$${count - 1}` + const localName = `${clsName}$${count}` + + return `const ${localName} = /*#__PURE__*/${name}(${prevName}, ${rest.join(',')});` + }) + + if (countPerClass.size === 0) { + throw new Error('No decorator usages found, but known names were used') + } + + const customExports = [] + + for (const [clsName, count] of countPerClass) { + const needle = new RegExp(`^export class(?= ${clsName} ({|extends ))`, 'm') + + if (!content.match(needle)) { + throw new Error(`Class ${clsName} not found in ${file}`) + } + + content = content.replace(needle, 'class') + customExports.push( + `export { ${clsName}$${count} as ${clsName} }`, + ) + } + + return content + '\n' + customExports.join('\n') + '\n' + } + + for (const f of glob.sync(path.join(outDir, 'esm/highlevel/types/**/*.js'))) { + transformFile(f, replaceDecorators) + } }, }) diff --git a/packages/core/src/highlevel/utils/inspectable.ts b/packages/core/src/highlevel/utils/inspectable.ts index 6a072283..fad49e67 100644 --- a/packages/core/src/highlevel/utils/inspectable.ts +++ b/packages/core/src/highlevel/utils/inspectable.ts @@ -33,7 +33,11 @@ function getAllGettersNames(obj: T): (keyof T)[] { * > (getter that caches after its first invocation is also * > considered pure in this case) */ -export function makeInspectable(obj: new (...args: any[]) => T, props?: (keyof T)[], hide?: (keyof T)[]): void { +export function makeInspectable( + obj: new (...args: any[]) => T, + props?: (keyof T)[], + hide?: (keyof T)[], +): typeof obj { const getters: (keyof T)[] = props ? props : [] for (const key of getAllGettersNames(obj.prototype)) { @@ -67,4 +71,6 @@ export function makeInspectable(obj: new (...args: any[]) => T, props?: (keyo return ret } obj.prototype[customInspectSymbol] = obj.prototype.toJSON + + return obj } diff --git a/packages/core/src/highlevel/utils/memoize.ts b/packages/core/src/highlevel/utils/memoize.ts index 7cdb35e9..5687ec90 100644 --- a/packages/core/src/highlevel/utils/memoize.ts +++ b/packages/core/src/highlevel/utils/memoize.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-assignment */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function memoizeGetters(cls: new (...args: any[]) => T, fields: (keyof T)[]) { +export function memoizeGetters(cls: new (...args: any[]) => T, fields: (keyof T)[]): typeof cls { for (const field of fields) { const desc = Object.getOwnPropertyDescriptor(cls.prototype, field) if (!desc) continue @@ -25,4 +25,6 @@ export function memoizeGetters(cls: new (...args: any[]) => T, fields: (keyof configurable: true, }) } + + return cls } diff --git a/packages/core/src/utils/links/common.ts b/packages/core/src/utils/links/common.ts index 796d1dd0..0d4a8ce2 100644 --- a/packages/core/src/utils/links/common.ts +++ b/packages/core/src/utils/links/common.ts @@ -57,7 +57,7 @@ function writeQuery(query: InputQuery) { return `?${str}` } -export function deeplinkBuilder(params: BuildDeeplinkOptions): Deeplink { +/* @__NO_SIDE_EFFECTS__ */ export function deeplinkBuilder(params: BuildDeeplinkOptions): Deeplink { const { internalBuild, internalParse, externalBuild, externalParse } = params const fn_ = (options: T & CommonDeeplinkOptions) => { diff --git a/scripts/build-package.js b/scripts/build-package.js index 4122427f..df898901 100644 --- a/scripts/build-package.js +++ b/scripts/build-package.js @@ -17,7 +17,8 @@ function exec(cmd, params) { function transformFile(file, transform) { const content = fs.readFileSync(file, 'utf8') - fs.writeFileSync(file, transform(content)) + const res = transform(content, file) + if (res != null) fs.writeFileSync(file, res) } const buildConfig = { @@ -44,6 +45,7 @@ const buildConfig = { config = config({ fs, path, + glob, exec, transformFile, packageDir,