npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lbdudc/linea-core

v0.1.1

Published

Motor autocontenido de Líneas de Producto Software (SPL): feature models, validación de specs y derivación de productos. Funciona 100% offline.

Readme

linea-core

Motor autocontenido de Líneas de Producto Software (SPL): feature models, validación de specs y derivación de productos.

Es una librería de funciones JavaScript/TypeScript sin estado (ADR-014): cada función recibe todo lo que necesita por parámetros y devuelve resultados explícitos. Sin estado interno entre llamadas, sin singletons, sin inyección de dependencias, sin configuración global y sin base de datos propia.

Funciona 100 % offline (ADR-002): ni GitLab, ni HTTP, ni telemetría. Toda integración remota vive en linea-server (ADR-003).

Instalación

El paquete se consume como dependencia git mientras no se publique en npm:

{
  "dependencies": {
    "@lbdudc/linea-core": "git+https://gitlab.lbd.udc.es/linea/linea-core.git"
  }
}

npm install ejecuta preparenpm run build, que compila TypeScript a dist/. No hace falta ningún paso manual.

Aviso: si instalas con npm ci --ignore-scripts, prepare no se ejecuta y el paquete queda sin dist/, por lo que fallará al importarlo. Es una limitación inherente a las dependencias git; desaparece cuando se publique en npm.

Requiere Node >= 20.11. Verificado también bajo Bun 1.3.x.

Uso

import {
  readFeatureModel,
  convertFeatureModel,
  parseFeatureModel,
  computeMandatoryFeatures,
  estimateFeatureCount,
  normalizeUvl,
  resolveLinePaths
} from '@lbdudc/linea-core'

// Leer el feature model de un directorio de trabajo de línea (UVL o XML).
const { type, path, content } = readFeatureModel({ lineDir: '/tmp/mi-linea' })

// Convertir de notación.
const { content: xml } = convertFeatureModel({ content, from: 'UVL', to: 'XML' })

// Inspeccionar.
const model = parseFeatureModel({ content, type })
const { voidModel, mandatory } = computeMandatoryFeatures({ model })

// Helpers de texto y de rutas.
const features = estimateFeatureCount({ uvl: normalizeUvl({ uvl: content }) })
const paths = resolveLinePaths({ lineDir: '/tmp/mi-linea' })

Selección de features

import {
  normalizeFeatureSelection,
  validateFeatureSelection,
  assertFeatureSelection,
  validateModelAgainstSelections
} from '@lbdudc/linea-core'

// Acepta las tres formas históricas de spec.features.
const features = normalizeFeatureSelection({ spec })

// Sin excepciones: devuelve un veredicto.
const { valid, completed, error, errorType } = validateFeatureSelection({ model, features })

// Con excepción: lanza FeatureSelectionInvalidError.
const expanded = assertFeatureSelection({ model, features })

// ¿Sigue el modelo admitiendo todos estos productos?
const { valid: ok, broken } = validateModelAgainstSelections({
  model,
  selections: [{ name: 'producto-a', features: ['A', 'B'] }]
})

Spec de producto

import {
  validateProductSpec,
  normalizeProductSpec,
  incrementVersion,
  buildEngineSpec
} from '@lbdudc/linea-core'

// Sin excepciones. errors[].messageKey es SIEMPRE una clave i18n.
const { valid, errors } = validateProductSpec({ spec })

const completo = normalizeProductSpec({ spec })      // aplica los defaults
incrementVersion({ version: '1.2.3', index: 1 })     // '1.3.0'
const engineSpec = buildEngineSpec({ spec })         // forma que espera spl-js-engine

Índice de productos (ADR-011)

import {
  readProductsIndex,
  addProductToIndex,
  writeProductsIndex,
  reconcileProductsIndex
} from '@lbdudc/linea-core'

// Una línea sin products.json devuelve un índice vacío, no lanza.
const index = readProductsIndex({ lineDir: '/tmp/mi-linea' })

// Todas las mutaciones son INMUTABLES: devuelven un índice nuevo.
// `version` es OPCIONAL: si se omite, la entrada no lleva la clave (los
// products.json anteriores al campo siguen valiendo y no cambian de bytes).
const siguiente = addProductToIndex({
  index,
  product: {
    name: 'mi-producto',
    repositorio: '/lab/mi-linea/mi-producto',
    version: '1.0.0'
  }
})
writeProductsIndex({ lineDir: '/tmp/mi-linea', index: siguiente })

// `removed` es informativo: no se poda nada (no hay BD que reconciliar).
const { index: reconciliado, added, removed } = reconcileProductsIndex({
  index,
  repositories: [{ name: 'mi-producto', repositorio: '/lab/mi-linea/mi-producto' }]
})

La serialización es determinista (orden alfabético por name, solo los dos campos normativos, salto de línea final) para que los commits de linea-server no generen diffs de ruido.

La API pública es una sola entrada (.). Todo lo que hay bajo src/ aparte de src/index.ts es privado y puede cambiar sin aviso.

Convención de llamada

Todas las funciones públicas reciben un único objeto de parámetros con nombres, incluidas las de un solo argumento: normalizeUvl({ uvl }), estimateFeatureCount({ uvl }), resolveLinePaths({ lineDir }), readTextFile({ filePath }), resolveAsset({ segments }). Es más verboso que un argumento posicional, pero mantiene un contrato uniforme para linea-server y linea-cli y permite añadir parámetros sin romper a nadie.

Crear una línea

import { buildLineScaffold, writeLineScaffold } from '@lbdudc/linea-core'

// PURO: no toca disco ni red. Devuelve los ficheros en el mismo orden que
// LineService.createLine, listos para gitlabSession.createFile.
const files = buildLineScaffold({ name: 'MiLinea', models: ['UML'] })
// -> [{ path: 'src/platform/model.uvl', content: '...', encoding: 'text' }, ...]

// IMPURO: los materializa en disco (modo local de linea-cli).
const { dir } = writeLineScaffold({ dir: '/tmp/mi-linea', name: 'MiLinea' })

El par puro/impuro es deliberado: linea-server sube los ficheros a GitLab sin que el core toque nada, y linea-cli escribe el árbol en local desde la misma descripción.

Derivar un producto

import { deriveProduct, deriveProductToZip } from '@lbdudc/linea-core'

const spec = { basicData: { name: 'MiTienda', version: '1.0.0' }, features: ['Shop', 'Payment'] }

await deriveProduct({ lineDir: '/tmp/mi-linea', spec, outputDir: '/tmp/producto' })
const zip = await deriveProductToZip({ lineDir: '/tmp/mi-linea', spec })  // Buffer

Errores: selección inválida → FeatureSelectionInvalidError; plataforma inservible o cualquier otro fallo del motor → LinePlatformNotFoundError (si una anotación llama a una función que extra.js no define, el mensaje es la clave error_messages.missing_function_error).

La derivación no borra nada: los directorios son del llamador.

Aviso al escribir un extra.js: no uses comentarios //. El procesador de plantillas elimina todos los saltos de línea del código que construye (processor.js:28), así que un comentario de línea se traga el resto de la función generada y el fichero sale vacío. Usa /* ... */.

Archivo, diff y formato

import { extractArchive, diffDirectories, formatDirectory } from '@lbdudc/linea-core'

const { dir } = await extractArchive({ source: '/tmp/a.zip', destDir: '/tmp/out', prefix: 'milinea' })
await formatDirectory({ dir })
const changes = diffDirectories({ oldDir: '/tmp/anterior', newDir: dir })
// -> [{ action: 'update', file_path: 'src/App.java', content: '...', encoding: 'text' }, ...]

FileChange usa el vocabulario de acciones de commit de GitLab (action, file_path, content, encoding) a propósito, para que linea-server se lo pase tal cual a gitlabSession.commit sin adaptador. Es un formato neutro de cambio de fichero: una forma de datos, no una dependencia. file_path es siempre POSIX y relativo.

Analizar un feature model

import {
  createAnalysisRuntime, disposeAnalysisRuntime,
  analyzeFeatureModel, analyzeFeatureModelBdd, analyzeMetric
} from '@lbdudc/linea-core'

const runtime = await createAnalysisRuntime()   // ~2,3 s: arranca Pyodide
try {
  const info = await analyzeFeatureModel({ uvl, runtime })
  // -> { coreFeatures, satisfiable, deadFeatures, maxDepth, ..., _skipped: [] }
  const bdd = await analyzeFeatureModelBdd({ uvl, runtime })
  const one = await analyzeMetric({ uvl, metric: 'deadFeatures', runtime })
} finally {
  disposeAnalysisRuntime({ runtime })
}

El runtime es del llamador (ADR-014): no hay singleton de módulo. Puede omitirse, y entonces cada llamada crea el suyo — correcto pero lento, así que quien analice más de una vez debería mantener uno.

Dos cosas que hay que saber antes de usarlo en un servidor:

  • disposeAnalysisRuntime NO libera memoria. Pyodide no ofrece teardown, así que los ~180 MB de heap WASM se quedan hasta que muera el proceso; dispose solo inutiliza el handle. Crear un runtime por petición filtra un intérprete cada vez: hay que mantener unos pocos y reutilizarlos.
  • Si faltan los wheels vendorizados, el arranque falla en el acto. No hay degradación silenciosa: es el canario de ADR-021. packageCacheDir de Pyodide es una caché y no un override, así que sin esa comprobación un wheel ausente se descargaría de cdn.jsdelivr.net y el módulo estaría usando la red sin que nadie se enterase.

El contrato de salida es idéntico al de linea/flamapy-server/main.py, incluidos los umbrales (SKIP_ENUM, SKIP_BDD) y el patrón safe(): un fallo individual devuelve null en su campo y lo anota en _skipped, sin abortar el resto de la respuesta.

Errores de validación: messageKey es una clave i18n

validateProductSpec y validateProductsIndex devuelven { path, code, messageKey }. messageKey es siempre una clave, nunca una frase legible: pásala por la función de traducción del consumidor y ramifica con code, que es estable.

Cinco claves vienen del monolito y ya están traducidas; diez son nuevas y todavía no, así que hoy se pintarían literales. Ver la tabla del CHANGELOG.md, que es la lista de trabajo para quien migre webui y cli.

Notación explícita, nunca inferida

Las funciones que reciben un modelo exigen el type exacto ('UVL' o 'XML'). El monolito lo relaja con (type ?? 'UVL').toUpperCase() porque line.featureModelType puede venir undefined de Mongo en líneas antiguas (LineService.js:284,401,432). Aplicar ese valor por defecto es responsabilidad del llamador: pasa line.featureModelType ?? 'UVL'.

Estructura de un directorio de línea

Se corresponde con paths.platform.* de linea/server/src/resources/properties.yml:

<lineDir>/
  products.json                     índice de productos (ADR-011)
  src/platform/
    model.uvl | model.xml           feature model (UVL preferido)
    config.json                     configuración del motor de derivación
    extra.js                        helpers de plantilla
    transformation.js               transformación de la spec
    code/                           código anotado que consume spl-js-engine

Desarrollo

npm install
npm run typecheck    # tsc --noEmit sobre src, test y scripts
npm run lint         # eslint 9 flat config + typescript-eslint
npm run test         # vitest run
npm run build        # tsc + copia de assets a dist/

Los ficheros que no son TypeScript bajo src/ (por ejemplo src/line/templates/**) son assets: scripts/copy-assets.mjs los replica en dist/ conservando su ruta relativa, y resolveAsset() los localiza vía import.meta.url. Así la ruta relativa al módulo es la misma en fuente y en dist/.

Decisiones de arquitectura relevantes

| ADR | Qué fija | |---|---| | ADR-001 | Separación en 4 módulos; core no depende de nadie | | ADR-002 | linea-core funciona sin conexión a internet | | ADR-003 | Solo linea-server habla con GitLab | | ADR-008 | Análisis de feature models con @lbdudc/flamapy.js | | ADR-009 | Persistencia en GitLab; el core trabaja sobre directorios | | ADR-011 | Índice de productos en products.json | | ADR-014 | linea-core es una librería de funciones sin estado |

Paridad funcional: el monolito linea/ es la referencia de comportamiento. Cada función documenta en su JSDoc el fichero y la función original que migra.

Deuda técnica conocida

  • spl-js-engine apunta a un commit de una rama sin publicar. La versión 4.0.5 del registro npm no tiene soporte UVL en absoluto (fromUVL y toUVL son undefined, y falta feature-model-uvl.js), así que es obligatorio usar la rama #next. Está fijada al commit cd67af2fd2f4019e99cbeb5ac4a8622f5549e68e por reproducibilidad y vía git+https:// (nunca git+ssh://, que obligaría a cada consumidor a tener claves de GitHub). Migrar a una versión publicada en cuanto exista.
  • Ruido en consola del parser UVL. uvl-parser/src/FeatureModel.js:22-28 decide si su argumento es una ruta con fs.statSync y hace console.error('Error: ' + e) cuando no lo es, volcando el modelo entero a stderr en cada parseo. src/feature-model/engine.ts filtra exactamente ese mensaje. Retirar el filtro si el upstream lo corrige.
  • Licencia sin confirmar. package.json declara UNLICENSED como marcador. TODO: actualizar cuando dirección responda.
  • Wheels de Python vendorizados (ADR-021). @lbdudc/flamapy.js descarga micropip y packaging de cdn.jsdelivr.net en una instalación limpia, lo que rompe ADR-002, y sin red no falla: devuelve undefined en silencio. Por eso src/analysis/ no usa su setupPyodide sino un arranque propio, y lleva los dos wheels (319 KB) dentro del paquete. Es una mitigación con fecha de caducidad: si el laboratorio hace que @lbdudc/flamapy.js incluya micropip y acepte configurar indexURL/packageCacheDir, hay que retirar el vendorizado.
  • disposeAnalysisRuntime no libera memoria (ADR-029). Pyodide no expone teardown, así que los ~180 MB de heap WASM de cada intérprete siguen ocupados hasta que muere el proceso; dispose solo inutiliza el handle. linea-server no puede crear un runtime por petición: debe mantener unos pocos y reutilizarlos.
  • Los tests de análisis corren cerca del techo de memoria. Por eso están repartidos en cuatro ficheros (Vitest da un worker por fichero, y es la única forma de mantener los heaps WASM separados): analyze.test.ts comparte dos runtimes para sus 22 tests, y runtime.test.ts y wheels-missing.test.ts aíslan los que arrancan su propia instancia. No añadas más instancias a un fichero existente; crea uno nuevo. Se observó una vez un fallo intermitente de 2 tests bajo carga que no se reprodujo en seis pasadas posteriores, así que el margen es estrecho: si el runner de CI tiene menos memoria que una máquina de desarrollo, este es el primer sitio donde lo vas a notar.