build(core): improved tree-shakeability

This commit is contained in:
alina 🌸 2024-03-04 20:59:27 +03:00
parent 56b2fe70d3
commit 42f1482d7f
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
5 changed files with 68 additions and 5 deletions

View file

@ -1,4 +1,6 @@
module.exports = ({ path, transformFile, packageDir, outDir }) => ({ const KNOWN_DECORATORS = ['memoizeGetters', 'makeInspectable']
module.exports = ({ path, glob, transformFile, packageDir, outDir }) => ({
esmOnlyDirectives: true, esmOnlyDirectives: true,
esmImportDirectives: true, esmImportDirectives: true,
final() { 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, 'cjs/network/network-manager.js'), replaceVersion)
transformFile(path.join(outDir, 'esm/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)
}
}, },
}) })

View file

@ -33,7 +33,11 @@ function getAllGettersNames<T>(obj: T): (keyof T)[] {
* > (getter that caches after its first invocation is also * > (getter that caches after its first invocation is also
* > considered pure in this case) * > considered pure in this case)
*/ */
export function makeInspectable<T>(obj: new (...args: any[]) => T, props?: (keyof T)[], hide?: (keyof T)[]): void { export function makeInspectable<T>(
obj: new (...args: any[]) => T,
props?: (keyof T)[],
hide?: (keyof T)[],
): typeof obj {
const getters: (keyof T)[] = props ? props : [] const getters: (keyof T)[] = props ? props : []
for (const key of getAllGettersNames<T>(obj.prototype)) { for (const key of getAllGettersNames<T>(obj.prototype)) {
@ -67,4 +71,6 @@ export function makeInspectable<T>(obj: new (...args: any[]) => T, props?: (keyo
return ret return ret
} }
obj.prototype[customInspectSymbol] = obj.prototype.toJSON obj.prototype[customInspectSymbol] = obj.prototype.toJSON
return obj
} }

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-assignment */
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function memoizeGetters<T>(cls: new (...args: any[]) => T, fields: (keyof T)[]) { export function memoizeGetters<T>(cls: new (...args: any[]) => T, fields: (keyof T)[]): typeof cls {
for (const field of fields) { for (const field of fields) {
const desc = Object.getOwnPropertyDescriptor(cls.prototype, field) const desc = Object.getOwnPropertyDescriptor(cls.prototype, field)
if (!desc) continue if (!desc) continue
@ -25,4 +25,6 @@ export function memoizeGetters<T>(cls: new (...args: any[]) => T, fields: (keyof
configurable: true, configurable: true,
}) })
} }
return cls
} }

View file

@ -57,7 +57,7 @@ function writeQuery(query: InputQuery) {
return `?${str}` return `?${str}`
} }
export function deeplinkBuilder<T>(params: BuildDeeplinkOptions<T>): Deeplink<T> { /* @__NO_SIDE_EFFECTS__ */ export function deeplinkBuilder<T>(params: BuildDeeplinkOptions<T>): Deeplink<T> {
const { internalBuild, internalParse, externalBuild, externalParse } = params const { internalBuild, internalParse, externalBuild, externalParse } = params
const fn_ = (options: T & CommonDeeplinkOptions) => { const fn_ = (options: T & CommonDeeplinkOptions) => {

View file

@ -17,7 +17,8 @@ function exec(cmd, params) {
function transformFile(file, transform) { function transformFile(file, transform) {
const content = fs.readFileSync(file, 'utf8') 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 = { const buildConfig = {
@ -44,6 +45,7 @@ const buildConfig = {
config = config({ config = config({
fs, fs,
path, path,
glob,
exec, exec,
transformFile, transformFile,
packageDir, packageDir,