Drop lodash from @pi/ui
The three functions it used (isEqual, some with an object as criterion and capitalize) live in util/common/compare.ts with the same behaviour, so apps no longer pull a CommonJS library that Node's ESM loader cannot resolve by path. TaskStream's initial bundle goes down to 1.88 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -195,12 +195,11 @@ Detalles a tener en cuenta:
|
||||
ignorando su valor. Se descubrió al dividir los componentes; `pi-bar-chart`
|
||||
es la primera versión que realmente renderiza barras.
|
||||
|
||||
**`lodash` se importa por función y con extensión** (`lodash/isEqual.js`,
|
||||
`lodash/some.js`, `lodash/capitalize.js`), nunca `import _ from 'lodash'`:
|
||||
lodash es CommonJS y el import completo mete la librería entera en el bundle de
|
||||
quien use un select. La extensión hace falta porque lodash no declara
|
||||
`exports`: un empaquetador resuelve `lodash/isEqual`, pero Node en ESM (Vitest,
|
||||
SSR) no, y la app falla al cargar `@pi/ui`.
|
||||
**Sin lodash.** Las tres funciones que se usaban (`isEqual`, `some` con un
|
||||
objeto como criterio y `capitalize`) están en `util/common/compare.ts`, con la
|
||||
misma semántica. lodash es CommonJS y sin mapa de `exports`: entero pesaba en el
|
||||
bundle de quien usara un select, y por funciones (`lodash/isEqual`) Node en ESM
|
||||
(Vitest, SSR) no lo resolvía.
|
||||
|
||||
**El loader de servidor de traducciones ya no está en el barrel.**
|
||||
`util/translate/loaders/translate-server.loader` usa `fs`, `path` y `process`;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"lib": {
|
||||
"entryFile": "src/index.ts"
|
||||
},
|
||||
"allowedNonPeerDependencies": ["lodash", "luxon", "ngx-infinite-scroll"],
|
||||
"allowedNonPeerDependencies": ["luxon", "ngx-infinite-scroll"],
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*.scss",
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"chart.js": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.0",
|
||||
"luxon": "^3.0.0",
|
||||
"ngx-infinite-scroll": "^22.0.0"
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ComponentFamilyClass, theming } from '../../../styles';
|
||||
import { Date } from '../../../util/models';
|
||||
import { PiBaseChipComponent } from '../base-chip.component';
|
||||
import { DateTime } from 'luxon';
|
||||
import capitalize from 'lodash/capitalize.js';
|
||||
import { capitalize } from '../../../util/common/compare';
|
||||
|
||||
@Component({
|
||||
selector: 'pi-chip-date',
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||
import isEqual from 'lodash/isEqual.js';
|
||||
import { isEqual } from '../../../util/common/compare';
|
||||
import { PiBaseSelectComponent } from '../base-select.component';
|
||||
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
||||
import { Device } from '../../../util/device-configs';
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||
import { PiBaseSelectComponent } from '../base-select.component';
|
||||
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
||||
import some from 'lodash/some.js';
|
||||
import { isMatch } from '../../../util/common/compare';
|
||||
import { MatCheckboxChange, MatCheckbox } from '@angular/material/checkbox';
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||
@@ -114,7 +114,9 @@ export class PiSelectMultipleComponent
|
||||
get selectedOptions(): Array<any> {
|
||||
const value = this.value();
|
||||
if (!value) return [];
|
||||
return this.options().filter((option) => some(value, option));
|
||||
return this.options().filter((option) =>
|
||||
value.some((selected: unknown) => isMatch(selected, option)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||
import { PiBaseSelectComponent } from '../base-select.component';
|
||||
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
||||
import isEqual from 'lodash/isEqual.js';
|
||||
import { isEqual } from '../../../util/common/compare';
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||
import { PiDisabledDirective } from '../../../util/status/directives/status.directive';
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||
import { PiBaseSelectComponent } from '../base-select.component';
|
||||
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
||||
import isEqual from 'lodash/isEqual.js';
|
||||
import { isEqual } from '../../../util/common/compare';
|
||||
import { NgTemplateOutlet } from '@angular/common';
|
||||
import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||
import { PiDisabledDirective } from '../../../util/status/directives/status.directive';
|
||||
|
||||
39
libs/ui/src/lib/util/common/compare.spec.ts
Normal file
39
libs/ui/src/lib/util/common/compare.spec.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { capitalize, isEqual, isMatch } from './compare';
|
||||
|
||||
describe('compare', () => {
|
||||
it('isEqual compara en profundidad', () => {
|
||||
expect(
|
||||
isEqual({ id: 1, tags: ['a', 'b'] }, { tags: ['a', 'b'], id: 1 }),
|
||||
).toBe(true);
|
||||
expect(isEqual({ id: 1 }, { id: 1, extra: true })).toBe(false);
|
||||
expect(isEqual([1, [2, 3]], [1, [2, 3]])).toBe(true);
|
||||
expect(isEqual([1, 2], [2, 1])).toBe(false);
|
||||
expect(isEqual(new Date(0), new Date(0))).toBe(true);
|
||||
expect(isEqual(NaN, NaN)).toBe(true);
|
||||
expect(isEqual(null, undefined)).toBe(false);
|
||||
expect(isEqual('1', 1)).toBe(false);
|
||||
expect(isEqual({ a: { b: 1 } }, { a: { b: 2 } })).toBe(false);
|
||||
});
|
||||
|
||||
it('isMatch acepta lo que contiene el criterio, como `_.some(lista, objeto)`', () => {
|
||||
const option = { id: 2, name: 'Dos' };
|
||||
expect(
|
||||
[
|
||||
{ id: 1, name: 'Uno' },
|
||||
{ id: 2, name: 'Dos', extra: 1 },
|
||||
].some((v) => isMatch(v, option)),
|
||||
).toBe(true);
|
||||
expect([{ id: 2 }].some((v) => isMatch(v, option))).toBe(false);
|
||||
expect(isMatch({ a: { b: 1, c: 2 } }, { a: { b: 1 } })).toBe(true);
|
||||
expect(isMatch({ tags: ['a', 'b', 'c'] }, { tags: ['c', 'a'] })).toBe(true);
|
||||
expect(isMatch({ tags: ['a'] }, { tags: ['z'] })).toBe(false);
|
||||
expect(isMatch('x', 'x')).toBe(true);
|
||||
expect(isMatch(null, { id: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
it('capitalize pone la primera en mayúscula y el resto en minúscula', () => {
|
||||
expect(capitalize('lun')).toBe('Lun');
|
||||
expect(capitalize('HOY')).toBe('Hoy');
|
||||
expect(capitalize('')).toBe('');
|
||||
});
|
||||
});
|
||||
66
libs/ui/src/lib/util/common/compare.ts
Normal file
66
libs/ui/src/lib/util/common/compare.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Las tres utilidades de lodash que usaba la librería (`isEqual`, `some` con
|
||||
* un objeto como criterio y `capitalize`), con la misma semántica en los
|
||||
* casos que se dan aquí: valores JSON, fechas, arrays y objetos planos. Sin
|
||||
* lodash, `@pi/ui` no arrastra una librería CommonJS al bundle de nadie.
|
||||
*/
|
||||
|
||||
/** Igualdad profunda: `isEqual({ a: [1] }, { a: [1] })` es `true`. */
|
||||
export function isEqual(a: unknown, b: unknown): boolean {
|
||||
if (Object.is(a, b)) return true;
|
||||
if (a instanceof Date && b instanceof Date)
|
||||
return a.getTime() === b.getTime();
|
||||
if (Array.isArray(a) || Array.isArray(b)) {
|
||||
return (
|
||||
Array.isArray(a) &&
|
||||
Array.isArray(b) &&
|
||||
a.length === b.length &&
|
||||
a.every((item, i) => isEqual(item, b[i]))
|
||||
);
|
||||
}
|
||||
if (!isPlainObject(a) || !isPlainObject(b)) return false;
|
||||
const keys = Object.keys(a);
|
||||
return (
|
||||
keys.length === Object.keys(b).length &&
|
||||
keys.every((key) => hasOwn(b, key) && isEqual(a[key], b[key]))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Si `value` contiene lo que pide `source`: cada propiedad de `source` está
|
||||
* en `value` con un valor que a su vez la contiene (y el resto da igual).
|
||||
* Es lo que hace `_.some(lista, objeto)` con cada elemento.
|
||||
*/
|
||||
export function isMatch(value: unknown, source: unknown): boolean {
|
||||
if (isPlainObject(source)) {
|
||||
return (
|
||||
isPlainObject(value) &&
|
||||
Object.keys(source).every(
|
||||
(key) => hasOwn(value, key) && isMatch(value[key], source[key]),
|
||||
)
|
||||
);
|
||||
}
|
||||
if (Array.isArray(source)) {
|
||||
// Como lodash: cada elemento pedido tiene que estar en el array.
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
source.every((wanted) => value.some((item) => isMatch(item, wanted)))
|
||||
);
|
||||
}
|
||||
return isEqual(value, source);
|
||||
}
|
||||
|
||||
/** Primera letra en mayúscula y el resto en minúscula: «lun» → «Lun». */
|
||||
export function capitalize(text: string): string {
|
||||
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
if (typeof value !== 'object' || value === null) return false;
|
||||
const proto = Object.getPrototypeOf(value);
|
||||
return proto === Object.prototype || proto === null;
|
||||
}
|
||||
|
||||
function hasOwn(object: object, key: string): boolean {
|
||||
return Object.prototype.hasOwnProperty.call(object, key);
|
||||
}
|
||||
Reference in New Issue
Block a user