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`
|
ignorando su valor. Se descubrió al dividir los componentes; `pi-bar-chart`
|
||||||
es la primera versión que realmente renderiza barras.
|
es la primera versión que realmente renderiza barras.
|
||||||
|
|
||||||
**`lodash` se importa por función y con extensión** (`lodash/isEqual.js`,
|
**Sin lodash.** Las tres funciones que se usaban (`isEqual`, `some` con un
|
||||||
`lodash/some.js`, `lodash/capitalize.js`), nunca `import _ from 'lodash'`:
|
objeto como criterio y `capitalize`) están en `util/common/compare.ts`, con la
|
||||||
lodash es CommonJS y el import completo mete la librería entera en el bundle de
|
misma semántica. lodash es CommonJS y sin mapa de `exports`: entero pesaba en el
|
||||||
quien use un select. La extensión hace falta porque lodash no declara
|
bundle de quien usara un select, y por funciones (`lodash/isEqual`) Node en ESM
|
||||||
`exports`: un empaquetador resuelve `lodash/isEqual`, pero Node en ESM (Vitest,
|
(Vitest, SSR) no lo resolvía.
|
||||||
SSR) no, y la app falla al cargar `@pi/ui`.
|
|
||||||
|
|
||||||
**El loader de servidor de traducciones ya no está en el barrel.**
|
**El loader de servidor de traducciones ya no está en el barrel.**
|
||||||
`util/translate/loaders/translate-server.loader` usa `fs`, `path` y `process`;
|
`util/translate/loaders/translate-server.loader` usa `fs`, `path` y `process`;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"lib": {
|
"lib": {
|
||||||
"entryFile": "src/index.ts"
|
"entryFile": "src/index.ts"
|
||||||
},
|
},
|
||||||
"allowedNonPeerDependencies": ["lodash", "luxon", "ngx-infinite-scroll"],
|
"allowedNonPeerDependencies": ["luxon", "ngx-infinite-scroll"],
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"glob": "**/*.scss",
|
"glob": "**/*.scss",
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
"chart.js": "^4.0.0"
|
"chart.js": "^4.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lodash": "^4.17.0",
|
|
||||||
"luxon": "^3.0.0",
|
"luxon": "^3.0.0",
|
||||||
"ngx-infinite-scroll": "^22.0.0"
|
"ngx-infinite-scroll": "^22.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ComponentFamilyClass, theming } from '../../../styles';
|
|||||||
import { Date } from '../../../util/models';
|
import { Date } from '../../../util/models';
|
||||||
import { PiBaseChipComponent } from '../base-chip.component';
|
import { PiBaseChipComponent } from '../base-chip.component';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import capitalize from 'lodash/capitalize.js';
|
import { capitalize } from '../../../util/common/compare';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pi-chip-date',
|
selector: 'pi-chip-date',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { TranslateService, TranslatePipe } from '@ngx-translate/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 { PiBaseSelectComponent } from '../base-select.component';
|
||||||
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
import { PiSelectOptionComponent } from '../select-option/select-option.component';
|
||||||
import { Device } from '../../../util/device-configs';
|
import { Device } from '../../../util/device-configs';
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||||
import { PiBaseSelectComponent } from '../base-select.component';
|
import { PiBaseSelectComponent } from '../base-select.component';
|
||||||
import { PiSelectOptionComponent } from '../select-option/select-option.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 { MatCheckboxChange, MatCheckbox } from '@angular/material/checkbox';
|
||||||
import { NgTemplateOutlet } from '@angular/common';
|
import { NgTemplateOutlet } from '@angular/common';
|
||||||
import { PiIconComponent } from '../../icon/icon/icon.component';
|
import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||||
@@ -114,7 +114,9 @@ export class PiSelectMultipleComponent
|
|||||||
get selectedOptions(): Array<any> {
|
get selectedOptions(): Array<any> {
|
||||||
const value = this.value();
|
const value = this.value();
|
||||||
if (!value) return [];
|
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 { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||||
import { PiBaseSelectComponent } from '../base-select.component';
|
import { PiBaseSelectComponent } from '../base-select.component';
|
||||||
import { PiSelectOptionComponent } from '../select-option/select-option.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 { NgTemplateOutlet } from '@angular/common';
|
||||||
import { PiIconComponent } from '../../icon/icon/icon.component';
|
import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||||
import { PiDisabledDirective } from '../../../util/status/directives/status.directive';
|
import { PiDisabledDirective } from '../../../util/status/directives/status.directive';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
import { TranslateService, TranslatePipe } from '@ngx-translate/core';
|
||||||
import { PiBaseSelectComponent } from '../base-select.component';
|
import { PiBaseSelectComponent } from '../base-select.component';
|
||||||
import { PiSelectOptionComponent } from '../select-option/select-option.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 { NgTemplateOutlet } from '@angular/common';
|
||||||
import { PiIconComponent } from '../../icon/icon/icon.component';
|
import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||||
import { PiDisabledDirective } from '../../../util/status/directives/status.directive';
|
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);
|
||||||
|
}
|
||||||
328
package-lock.json
generated
328
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,6 @@
|
|||||||
"@ngx-translate/http-loader": "^18.0.0",
|
"@ngx-translate/http-loader": "^18.0.0",
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"lodash": "^4.18.1",
|
|
||||||
"luxon": "^3.7.2",
|
"luxon": "^3.7.2",
|
||||||
"ngx-infinite-scroll": "^22.0.0",
|
"ngx-infinite-scroll": "^22.0.0",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
@@ -71,7 +70,6 @@
|
|||||||
"@swc/helpers": "0.5.18",
|
"@swc/helpers": "0.5.18",
|
||||||
"@types/express": "^4.17.21",
|
"@types/express": "^4.17.21",
|
||||||
"@types/jest": "~30.0.0",
|
"@types/jest": "~30.0.0",
|
||||||
"@types/lodash": "^4.17.24",
|
|
||||||
"@types/luxon": "^3.7.2",
|
"@types/luxon": "^3.7.2",
|
||||||
"@types/node": "^24.0.0",
|
"@types/node": "^24.0.0",
|
||||||
"@types/supertest": "^6.0.3",
|
"@types/supertest": "^6.0.3",
|
||||||
|
|||||||
Reference in New Issue
Block a user