From 075fdc18916bb6e280ffaacf19321a1fded10b71 Mon Sep 17 00:00:00 2001 From: onmax Date: Sat, 14 Feb 2026 09:59:57 +0100 Subject: [PATCH 1/4] feat(ts): add lightweight enums entrypoint for smaller graph --- packages/ts/package.json | 30 ++- packages/ts/rollup.config.js | 3 +- packages/ts/scripts/check-vite-graph.mjs | 257 +++++++++++++++++++++++ packages/ts/src/enums.ts | 26 +++ 4 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 packages/ts/scripts/check-vite-graph.mjs create mode 100644 packages/ts/src/enums.ts diff --git a/packages/ts/package.json b/packages/ts/package.json index f6f6a43f3..fa25987b7 100644 --- a/packages/ts/package.json +++ b/packages/ts/package.json @@ -30,13 +30,38 @@ "module": "./index.js", "typings": "./index.d.ts", "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js", + "default": "./index.js" + }, + "./maps": { + "types": "./maps.d.ts", + "import": "./maps.js", + "default": "./maps.js" + }, + "./enums": { + "types": "./enums.d.ts", + "import": "./enums.js", + "default": "./enums.js" + }, + "./dist/*": "./dist/*", + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist/*.js", + "default": "./dist/*.js" + }, + "./package.json": "./package.json" + }, "sideEffects": [ "styles/index.js", "dist/styles/index.js" ], "scripts": { "build": "sha=$(tar cf - ./src | shasum); if [[ $(echo $sha) == $(< .srcsha) ]] && [[ -d \"./dist\" ]]; then echo \"Lib Build Exists\"; else npm run forcebuild; echo $sha > .srcsha; fi", - "forcebuild": "rimraf dist; rollup -c; rm -rf dist/.cache; cp LICENSE README.md package.json ./dist", + "forcebuild": "rimraf dist; rollup -c; rm -rf dist/.cache; cp LICENSE README.md package.json ./dist; cp -R docs ./dist/docs", + "check:vite-graph": "node ./scripts/check-vite-graph.mjs", "publish:dist": "cd ./dist; npm publish" }, "devDependencies": { @@ -50,7 +75,8 @@ "rollup-plugin-rename-node-modules": "^1.3.1", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.31.1", - "rollup-plugin-visualizer": "^4.2.2" + "rollup-plugin-visualizer": "^4.2.2", + "vite": "^6.2.4" }, "dependencies": { "@emotion/css": "^11.7.1", diff --git a/packages/ts/rollup.config.js b/packages/ts/rollup.config.js index 33af42a39..080f3c340 100644 --- a/packages/ts/rollup.config.js +++ b/packages/ts/rollup.config.js @@ -48,7 +48,7 @@ const plugins = [ export default [ { - input: 'src/index.ts', + input: ['src/index.ts', 'src/enums.ts'], external: regexesOfPackages, treeshake: false, output: { @@ -62,6 +62,7 @@ export default [ }, { input: 'src/maps.ts', + external: regexesOfPackages, output: { dir: 'dist', format: 'esm', diff --git a/packages/ts/scripts/check-vite-graph.mjs b/packages/ts/scripts/check-vite-graph.mjs new file mode 100644 index 000000000..353a9c83c --- /dev/null +++ b/packages/ts/scripts/check-vite-graph.mjs @@ -0,0 +1,257 @@ +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { existsSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { build } from 'vite' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const packageRoot = path.resolve(__dirname, '..') +const distRoot = path.join(packageRoot, 'dist') +const srcRoot = path.join(packageRoot, 'src') +const shouldAssert = process.argv.includes('--assert') + +const heavyPackages = ['elkjs', 'three', 'leaflet', 'maplibre-gl'] + +const fixtures = [ + { + name: 'root-enum', + code: "import { CurveType } from '@unovis/ts'; console.log(CurveType.MonotoneX)", + }, + { + name: 'enums-entrypoint', + code: "import { CurveType } from '@unovis/ts/enums'; console.log(CurveType.MonotoneX)", + }, + { + name: 'granular-curve', + code: "import { CurveType } from '@unovis/ts/types/curve'; console.log(CurveType.MonotoneX)", + }, + { + name: 'line-chart', + code: "import { Line } from '@unovis/ts'; console.log(Boolean(Line))", + }, + { + name: 'graph-control', + code: "import { Graph } from '@unovis/ts'; console.log(Boolean(Graph))", + }, +] + +function normalizeId (id) { + return id.replace(/\\/g, '/') +} + +function parseExportTargets (source) { + return source + .split('\n') + .map(line => line.trim()) + .filter(line => line.startsWith('export') && line.includes(' from ')) + .map((line) => { + const quote = line.includes("'") ? "'" : '"' + const start = line.indexOf(quote) + const end = line.lastIndexOf(quote) + return start >= 0 && end > start ? line.slice(start + 1, end) : null + }) + .filter(Boolean) +} + +function detectHeavyPackages (moduleIds) { + const heavy = new Set() + for (const id of moduleIds) { + for (const pkg of heavyPackages) { + if ( + id.includes(`/node_modules/${pkg}/`) || + id.includes(`/node_modules/.pnpm/${pkg}@`) || + id.includes(`/node_modules/.pnpm/${pkg}_`) + ) { + heavy.add(pkg) + } + } + } + return [...heavy].sort() +} + +function makeLocalResolverPlugin () { + return { + name: 'resolve-unovis-local-dist', + resolveId (source) { + if (source === '@unovis/ts') return path.join(distRoot, 'index.js') + if (!source.startsWith('@unovis/ts/')) return null + + const subPath = source.slice('@unovis/ts/'.length) + if (subPath.startsWith('dist/')) return path.join(distRoot, subPath.slice('dist/'.length)) + if (subPath.endsWith('.js')) return path.join(distRoot, subPath) + return path.join(distRoot, `${subPath}.js`) + }, + } +} + +function makeCapturePlugin (collectedModules) { + return { + name: 'capture-module-graph', + moduleParsed (info) { + if (!info?.id) return + collectedModules.add(normalizeId(info.id)) + }, + } +} + +async function runFixture (fixture) { + const tmpRoot = await mkdtemp(path.join(os.tmpdir(), 'unovis-vite-graph-')) + const entryFile = path.join(tmpRoot, `${fixture.name}.ts`) + const modules = new Set() + + try { + await writeFile(entryFile, fixture.code, 'utf8') + + await build({ + configFile: false, + logLevel: 'silent', + root: tmpRoot, + plugins: [ + makeLocalResolverPlugin(), + makeCapturePlugin(modules), + ], + build: { + write: false, + minify: false, + sourcemap: false, + target: 'esnext', + rollupOptions: { + input: entryFile, + }, + }, + }) + } finally { + await rm(tmpRoot, { recursive: true, force: true }) + } + + const moduleIds = [...modules] + const nodeModuleIds = moduleIds.filter(id => id.includes('/node_modules/')) + const heavyDeps = detectHeavyPackages(moduleIds) + + return { + fixture: fixture.name, + modulesTransformed: moduleIds.length, + nodeModulesTransformed: nodeModuleIds.length, + heavyDeps, + } +} + +function printRows (rows) { + const columns = [ + ['fixture', 'fixture'], + ['modules', 'modulesTransformed'], + ['node_modules', 'nodeModulesTransformed'], + ['heavy deps', 'heavyDeps'], + ] + + const printable = rows.map(row => ({ + ...row, + heavyDeps: row.heavyDeps.length ? row.heavyDeps.join(', ') : '-', + })) + + const widths = columns.map(([title, key]) => { + const contentWidth = Math.max(...printable.map(row => String(row[key]).length)) + return Math.max(title.length, contentWidth) + }) + + const line = columns + .map(([title], i) => title.padEnd(widths[i], ' ')) + .join(' | ') + const divider = widths.map(w => '-'.repeat(w)).join('-|-') + + console.log(line) + console.log(divider) + + for (const row of printable) { + console.log(columns.map(([, key], i) => String(row[key]).padEnd(widths[i], ' ')).join(' | ')) + } +} + +function evaluateAssertions (rows) { + const byName = new Map(rows.map(r => [r.fixture, r])) + const root = byName.get('root-enum') + const enums = byName.get('enums-entrypoint') + const granular = byName.get('granular-curve') + const failures = [] + + const factor = (a, b) => { + if (!a || !b) return 0 + if (!b.nodeModulesTransformed) return Number.POSITIVE_INFINITY + return a.nodeModulesTransformed / b.nodeModulesTransformed + } + + const enumsFactor = factor(root, enums) + const granularFactor = factor(root, granular) + + if (!(enumsFactor >= 10 || granularFactor >= 10)) { + failures.push(`expected >=10x node_modules reduction (root vs enums/granular), got enums=${enumsFactor.toFixed(2)}x granular=${granularFactor.toFixed(2)}x`) + } + + if (enums?.heavyDeps.length) { + failures.push(`expected no heavy deps for enums-entrypoint, found: ${enums.heavyDeps.join(', ')}`) + } + + if (granular?.heavyDeps.length) { + failures.push(`expected no heavy deps for granular-curve, found: ${granular.heavyDeps.join(', ')}`) + } + + return { failures, enumsFactor, granularFactor } +} + +async function main () { + const required = [path.join(distRoot, 'index.js'), path.join(distRoot, 'enums.js')] + const missing = required.filter(file => !existsSync(file)) + if (missing.length) { + const relMissing = missing.map(file => path.relative(packageRoot, file)) + throw new Error(`Missing build artifacts: ${relMissing.join(', ')}. Run \"pnpm --filter @unovis/ts forcebuild\" first.`) + } + + const [indexSource, typesSource] = await Promise.all([ + readFile(path.join(srcRoot, 'index.ts'), 'utf8'), + readFile(path.join(srcRoot, 'types.ts'), 'utf8'), + ]) + + const indexTargets = parseExportTargets(indexSource) + const typeTargets = parseExportTargets(typesSource) + + console.log('Vite module-graph analysis for @unovis/ts') + console.log('') + console.log(`src/index.ts re-export targets: ${indexTargets.length}`) + console.log(indexTargets.map(t => ` - ${t}`).join('\n')) + console.log('') + console.log(`src/types.ts re-export targets: ${typeTargets.length}`) + console.log(typeTargets.map(t => ` - ${t}`).join('\n')) + console.log('') + + const rows = [] + for (const fixture of fixtures) { + rows.push(await runFixture(fixture)) + } + + printRows(rows) + console.log('') + + const { failures, enumsFactor, granularFactor } = evaluateAssertions(rows) + const enumsFactorLabel = Number.isFinite(enumsFactor) ? `${enumsFactor.toFixed(2)}x` : 'infinite' + const granularFactorLabel = Number.isFinite(granularFactor) ? `${granularFactor.toFixed(2)}x` : 'infinite' + console.log(`root-enum -> enums-entrypoint reduction: ${enumsFactorLabel}`) + console.log(`root-enum -> granular-curve reduction: ${granularFactorLabel}`) + + if (shouldAssert) { + if (failures.length) { + console.log('') + console.log('assertion failures:') + for (const failure of failures) console.log(` - ${failure}`) + process.exitCode = 1 + return + } + console.log('') + console.log('assertions passed') + } +} + +main().catch((error) => { + console.error(error.message) + process.exitCode = 1 +}) diff --git a/packages/ts/src/enums.ts b/packages/ts/src/enums.ts new file mode 100644 index 000000000..bcd3852a2 --- /dev/null +++ b/packages/ts/src/enums.ts @@ -0,0 +1,26 @@ +export { CurveType } from 'types/curve' +export { Arrangement, Orientation, Position, PositionStrategy } from 'types/position' +export { Shape } from 'types/shape' +export { ComponentType, Sizing } from 'types/component' +export { FitMode, TextAlign, TrimMode, VerticalAlign } from 'types/text' +export { FindNearestDirection } from 'types/data' +export { Direction } from 'types/direction' + +export { AxisType } from 'components/axis/types' +export { ChordLabelAlignment } from 'components/chord-diagram/types' +export { LeafletMapPointShape, LeafletMapRenderer } from 'components/leaflet-map/types' +export { + GraphFitViewAlignment, + GraphLayoutType, + GraphLinkArrowStyle, + GraphLinkStyle, + GraphNodeSelectionHighlightMode, + GraphNodeShape, +} from 'components/graph/types' +export { VisControlsOrientation } from 'components/vis-controls/types' +export { FreeBrushMode } from 'components/free-brush/types' +export { BulletLegendOrientation, BulletShape } from 'components/bullet-legend/types' +export { XYLabelPositioning } from 'components/xy-labels/types' +export { NestedDonutDirection, NestedDonutSegmentLabelAlignment } from 'components/nested-donut/types' +export { PlotlineLabelOrientation, PlotlineLabelPosition, PlotlineLineStylePresets } from 'components/plotline/types' +export { PlotbandLabelOrientation, PlotbandLabelPosition } from 'components/plotband/types' From 8f4cea5efbd9eaa9d1d38ab9bb25ac70196aae40 Mon Sep 17 00:00:00 2001 From: onmax Date: Sat, 14 Feb 2026 09:59:58 +0100 Subject: [PATCH 2/4] chore(ts): use import type for heavy deps --- packages/ts/src/components/graph/config.ts | 2 +- packages/ts/src/components/leaflet-map/index.ts | 2 +- packages/ts/src/components/leaflet-map/renderer/map-style.ts | 2 +- packages/ts/src/components/leaflet-map/types.ts | 5 +++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/ts/src/components/graph/config.ts b/packages/ts/src/components/graph/config.ts index 422b061b3..3cf33c988 100644 --- a/packages/ts/src/components/graph/config.ts +++ b/packages/ts/src/components/graph/config.ts @@ -2,7 +2,7 @@ import { D3BrushEvent } from 'd3-brush' import { D3DragEvent } from 'd3-drag' import { D3ZoomEvent, ZoomTransform } from 'd3-zoom' import { Selection } from 'd3-selection' -import { ElkShape } from 'elkjs' +import type { ElkShape } from 'elkjs' // Core import type { GraphDataModel } from 'data-models/graph' diff --git a/packages/ts/src/components/leaflet-map/index.ts b/packages/ts/src/components/leaflet-map/index.ts index e08f3ddb0..56d090f5e 100644 --- a/packages/ts/src/components/leaflet-map/index.ts +++ b/packages/ts/src/components/leaflet-map/index.ts @@ -2,7 +2,7 @@ import { select, Selection } from 'd3-selection' import { packSiblings } from 'd3-hierarchy' import type L from 'leaflet' import Supercluster, { ClusterFeature, PointFeature } from 'supercluster' -import { StyleSpecification } from 'maplibre-gl' +import type { StyleSpecification } from 'maplibre-gl' // Core import { ComponentCore } from 'core/component' diff --git a/packages/ts/src/components/leaflet-map/renderer/map-style.ts b/packages/ts/src/components/leaflet-map/renderer/map-style.ts index 98c190452..6b4e9665e 100644 --- a/packages/ts/src/components/leaflet-map/renderer/map-style.ts +++ b/packages/ts/src/components/leaflet-map/renderer/map-style.ts @@ -1,4 +1,4 @@ -import { StyleSpecification, LayerSpecification } from 'maplibre-gl' +import type { StyleSpecification, LayerSpecification } from 'maplibre-gl' import mapLibreBaseSettings from './mapboxgl/mapboxgl-settings.json' import mapLibreDarkTheme from './mapboxgl/mapboxgl-dark-theme.json' diff --git a/packages/ts/src/components/leaflet-map/types.ts b/packages/ts/src/components/leaflet-map/types.ts index 57dae7fec..a85923421 100644 --- a/packages/ts/src/components/leaflet-map/types.ts +++ b/packages/ts/src/components/leaflet-map/types.ts @@ -1,5 +1,6 @@ -import { LatLng } from 'leaflet' -import Supercluster, { ClusterProperties, PointFeature } from 'supercluster' +import type { LatLng } from 'leaflet' +import type Supercluster from 'supercluster' +import type { ClusterProperties, PointFeature } from 'supercluster' import { GenericDataRecord } from 'types/data' export enum LeafletMapRenderer { From 468ddbb0772560186d75e82b47984d7d166f3447 Mon Sep 17 00:00:00 2001 From: onmax Date: Sat, 14 Feb 2026 10:12:23 +0100 Subject: [PATCH 3/4] chore(ts): remove unused enums harness entrypoint --- packages/ts/package.json | 30 +-- packages/ts/rollup.config.js | 3 +- packages/ts/scripts/check-vite-graph.mjs | 257 ----------------------- packages/ts/src/enums.ts | 26 --- 4 files changed, 3 insertions(+), 313 deletions(-) delete mode 100644 packages/ts/scripts/check-vite-graph.mjs delete mode 100644 packages/ts/src/enums.ts diff --git a/packages/ts/package.json b/packages/ts/package.json index fa25987b7..f6f6a43f3 100644 --- a/packages/ts/package.json +++ b/packages/ts/package.json @@ -30,38 +30,13 @@ "module": "./index.js", "typings": "./index.d.ts", "type": "module", - "exports": { - ".": { - "types": "./index.d.ts", - "import": "./index.js", - "default": "./index.js" - }, - "./maps": { - "types": "./maps.d.ts", - "import": "./maps.js", - "default": "./maps.js" - }, - "./enums": { - "types": "./enums.d.ts", - "import": "./enums.js", - "default": "./enums.js" - }, - "./dist/*": "./dist/*", - "./*": { - "types": "./dist/*.d.ts", - "import": "./dist/*.js", - "default": "./dist/*.js" - }, - "./package.json": "./package.json" - }, "sideEffects": [ "styles/index.js", "dist/styles/index.js" ], "scripts": { "build": "sha=$(tar cf - ./src | shasum); if [[ $(echo $sha) == $(< .srcsha) ]] && [[ -d \"./dist\" ]]; then echo \"Lib Build Exists\"; else npm run forcebuild; echo $sha > .srcsha; fi", - "forcebuild": "rimraf dist; rollup -c; rm -rf dist/.cache; cp LICENSE README.md package.json ./dist; cp -R docs ./dist/docs", - "check:vite-graph": "node ./scripts/check-vite-graph.mjs", + "forcebuild": "rimraf dist; rollup -c; rm -rf dist/.cache; cp LICENSE README.md package.json ./dist", "publish:dist": "cd ./dist; npm publish" }, "devDependencies": { @@ -75,8 +50,7 @@ "rollup-plugin-rename-node-modules": "^1.3.1", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.31.1", - "rollup-plugin-visualizer": "^4.2.2", - "vite": "^6.2.4" + "rollup-plugin-visualizer": "^4.2.2" }, "dependencies": { "@emotion/css": "^11.7.1", diff --git a/packages/ts/rollup.config.js b/packages/ts/rollup.config.js index 080f3c340..33af42a39 100644 --- a/packages/ts/rollup.config.js +++ b/packages/ts/rollup.config.js @@ -48,7 +48,7 @@ const plugins = [ export default [ { - input: ['src/index.ts', 'src/enums.ts'], + input: 'src/index.ts', external: regexesOfPackages, treeshake: false, output: { @@ -62,7 +62,6 @@ export default [ }, { input: 'src/maps.ts', - external: regexesOfPackages, output: { dir: 'dist', format: 'esm', diff --git a/packages/ts/scripts/check-vite-graph.mjs b/packages/ts/scripts/check-vite-graph.mjs deleted file mode 100644 index 353a9c83c..000000000 --- a/packages/ts/scripts/check-vite-graph.mjs +++ /dev/null @@ -1,257 +0,0 @@ -import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' -import { existsSync } from 'node:fs' -import os from 'node:os' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import { build } from 'vite' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const packageRoot = path.resolve(__dirname, '..') -const distRoot = path.join(packageRoot, 'dist') -const srcRoot = path.join(packageRoot, 'src') -const shouldAssert = process.argv.includes('--assert') - -const heavyPackages = ['elkjs', 'three', 'leaflet', 'maplibre-gl'] - -const fixtures = [ - { - name: 'root-enum', - code: "import { CurveType } from '@unovis/ts'; console.log(CurveType.MonotoneX)", - }, - { - name: 'enums-entrypoint', - code: "import { CurveType } from '@unovis/ts/enums'; console.log(CurveType.MonotoneX)", - }, - { - name: 'granular-curve', - code: "import { CurveType } from '@unovis/ts/types/curve'; console.log(CurveType.MonotoneX)", - }, - { - name: 'line-chart', - code: "import { Line } from '@unovis/ts'; console.log(Boolean(Line))", - }, - { - name: 'graph-control', - code: "import { Graph } from '@unovis/ts'; console.log(Boolean(Graph))", - }, -] - -function normalizeId (id) { - return id.replace(/\\/g, '/') -} - -function parseExportTargets (source) { - return source - .split('\n') - .map(line => line.trim()) - .filter(line => line.startsWith('export') && line.includes(' from ')) - .map((line) => { - const quote = line.includes("'") ? "'" : '"' - const start = line.indexOf(quote) - const end = line.lastIndexOf(quote) - return start >= 0 && end > start ? line.slice(start + 1, end) : null - }) - .filter(Boolean) -} - -function detectHeavyPackages (moduleIds) { - const heavy = new Set() - for (const id of moduleIds) { - for (const pkg of heavyPackages) { - if ( - id.includes(`/node_modules/${pkg}/`) || - id.includes(`/node_modules/.pnpm/${pkg}@`) || - id.includes(`/node_modules/.pnpm/${pkg}_`) - ) { - heavy.add(pkg) - } - } - } - return [...heavy].sort() -} - -function makeLocalResolverPlugin () { - return { - name: 'resolve-unovis-local-dist', - resolveId (source) { - if (source === '@unovis/ts') return path.join(distRoot, 'index.js') - if (!source.startsWith('@unovis/ts/')) return null - - const subPath = source.slice('@unovis/ts/'.length) - if (subPath.startsWith('dist/')) return path.join(distRoot, subPath.slice('dist/'.length)) - if (subPath.endsWith('.js')) return path.join(distRoot, subPath) - return path.join(distRoot, `${subPath}.js`) - }, - } -} - -function makeCapturePlugin (collectedModules) { - return { - name: 'capture-module-graph', - moduleParsed (info) { - if (!info?.id) return - collectedModules.add(normalizeId(info.id)) - }, - } -} - -async function runFixture (fixture) { - const tmpRoot = await mkdtemp(path.join(os.tmpdir(), 'unovis-vite-graph-')) - const entryFile = path.join(tmpRoot, `${fixture.name}.ts`) - const modules = new Set() - - try { - await writeFile(entryFile, fixture.code, 'utf8') - - await build({ - configFile: false, - logLevel: 'silent', - root: tmpRoot, - plugins: [ - makeLocalResolverPlugin(), - makeCapturePlugin(modules), - ], - build: { - write: false, - minify: false, - sourcemap: false, - target: 'esnext', - rollupOptions: { - input: entryFile, - }, - }, - }) - } finally { - await rm(tmpRoot, { recursive: true, force: true }) - } - - const moduleIds = [...modules] - const nodeModuleIds = moduleIds.filter(id => id.includes('/node_modules/')) - const heavyDeps = detectHeavyPackages(moduleIds) - - return { - fixture: fixture.name, - modulesTransformed: moduleIds.length, - nodeModulesTransformed: nodeModuleIds.length, - heavyDeps, - } -} - -function printRows (rows) { - const columns = [ - ['fixture', 'fixture'], - ['modules', 'modulesTransformed'], - ['node_modules', 'nodeModulesTransformed'], - ['heavy deps', 'heavyDeps'], - ] - - const printable = rows.map(row => ({ - ...row, - heavyDeps: row.heavyDeps.length ? row.heavyDeps.join(', ') : '-', - })) - - const widths = columns.map(([title, key]) => { - const contentWidth = Math.max(...printable.map(row => String(row[key]).length)) - return Math.max(title.length, contentWidth) - }) - - const line = columns - .map(([title], i) => title.padEnd(widths[i], ' ')) - .join(' | ') - const divider = widths.map(w => '-'.repeat(w)).join('-|-') - - console.log(line) - console.log(divider) - - for (const row of printable) { - console.log(columns.map(([, key], i) => String(row[key]).padEnd(widths[i], ' ')).join(' | ')) - } -} - -function evaluateAssertions (rows) { - const byName = new Map(rows.map(r => [r.fixture, r])) - const root = byName.get('root-enum') - const enums = byName.get('enums-entrypoint') - const granular = byName.get('granular-curve') - const failures = [] - - const factor = (a, b) => { - if (!a || !b) return 0 - if (!b.nodeModulesTransformed) return Number.POSITIVE_INFINITY - return a.nodeModulesTransformed / b.nodeModulesTransformed - } - - const enumsFactor = factor(root, enums) - const granularFactor = factor(root, granular) - - if (!(enumsFactor >= 10 || granularFactor >= 10)) { - failures.push(`expected >=10x node_modules reduction (root vs enums/granular), got enums=${enumsFactor.toFixed(2)}x granular=${granularFactor.toFixed(2)}x`) - } - - if (enums?.heavyDeps.length) { - failures.push(`expected no heavy deps for enums-entrypoint, found: ${enums.heavyDeps.join(', ')}`) - } - - if (granular?.heavyDeps.length) { - failures.push(`expected no heavy deps for granular-curve, found: ${granular.heavyDeps.join(', ')}`) - } - - return { failures, enumsFactor, granularFactor } -} - -async function main () { - const required = [path.join(distRoot, 'index.js'), path.join(distRoot, 'enums.js')] - const missing = required.filter(file => !existsSync(file)) - if (missing.length) { - const relMissing = missing.map(file => path.relative(packageRoot, file)) - throw new Error(`Missing build artifacts: ${relMissing.join(', ')}. Run \"pnpm --filter @unovis/ts forcebuild\" first.`) - } - - const [indexSource, typesSource] = await Promise.all([ - readFile(path.join(srcRoot, 'index.ts'), 'utf8'), - readFile(path.join(srcRoot, 'types.ts'), 'utf8'), - ]) - - const indexTargets = parseExportTargets(indexSource) - const typeTargets = parseExportTargets(typesSource) - - console.log('Vite module-graph analysis for @unovis/ts') - console.log('') - console.log(`src/index.ts re-export targets: ${indexTargets.length}`) - console.log(indexTargets.map(t => ` - ${t}`).join('\n')) - console.log('') - console.log(`src/types.ts re-export targets: ${typeTargets.length}`) - console.log(typeTargets.map(t => ` - ${t}`).join('\n')) - console.log('') - - const rows = [] - for (const fixture of fixtures) { - rows.push(await runFixture(fixture)) - } - - printRows(rows) - console.log('') - - const { failures, enumsFactor, granularFactor } = evaluateAssertions(rows) - const enumsFactorLabel = Number.isFinite(enumsFactor) ? `${enumsFactor.toFixed(2)}x` : 'infinite' - const granularFactorLabel = Number.isFinite(granularFactor) ? `${granularFactor.toFixed(2)}x` : 'infinite' - console.log(`root-enum -> enums-entrypoint reduction: ${enumsFactorLabel}`) - console.log(`root-enum -> granular-curve reduction: ${granularFactorLabel}`) - - if (shouldAssert) { - if (failures.length) { - console.log('') - console.log('assertion failures:') - for (const failure of failures) console.log(` - ${failure}`) - process.exitCode = 1 - return - } - console.log('') - console.log('assertions passed') - } -} - -main().catch((error) => { - console.error(error.message) - process.exitCode = 1 -}) diff --git a/packages/ts/src/enums.ts b/packages/ts/src/enums.ts deleted file mode 100644 index bcd3852a2..000000000 --- a/packages/ts/src/enums.ts +++ /dev/null @@ -1,26 +0,0 @@ -export { CurveType } from 'types/curve' -export { Arrangement, Orientation, Position, PositionStrategy } from 'types/position' -export { Shape } from 'types/shape' -export { ComponentType, Sizing } from 'types/component' -export { FitMode, TextAlign, TrimMode, VerticalAlign } from 'types/text' -export { FindNearestDirection } from 'types/data' -export { Direction } from 'types/direction' - -export { AxisType } from 'components/axis/types' -export { ChordLabelAlignment } from 'components/chord-diagram/types' -export { LeafletMapPointShape, LeafletMapRenderer } from 'components/leaflet-map/types' -export { - GraphFitViewAlignment, - GraphLayoutType, - GraphLinkArrowStyle, - GraphLinkStyle, - GraphNodeSelectionHighlightMode, - GraphNodeShape, -} from 'components/graph/types' -export { VisControlsOrientation } from 'components/vis-controls/types' -export { FreeBrushMode } from 'components/free-brush/types' -export { BulletLegendOrientation, BulletShape } from 'components/bullet-legend/types' -export { XYLabelPositioning } from 'components/xy-labels/types' -export { NestedDonutDirection, NestedDonutSegmentLabelAlignment } from 'components/nested-donut/types' -export { PlotlineLabelOrientation, PlotlineLabelPosition, PlotlineLineStylePresets } from 'components/plotline/types' -export { PlotbandLabelOrientation, PlotbandLabelPosition } from 'components/plotband/types' From 981de1f090f2cd8f33fd2f2c077a757fb0b5bf54 Mon Sep 17 00:00:00 2001 From: onmax Date: Sat, 14 Feb 2026 09:59:57 +0100 Subject: [PATCH 4/4] feat(ts): add lightweight enums entrypoint for smaller graph --- packages/ts/package.json | 30 ++- packages/ts/rollup.config.js | 3 +- packages/ts/scripts/check-vite-graph.mjs | 257 +++++++++++++++++++++++ packages/ts/src/enums.ts | 26 +++ 4 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 packages/ts/scripts/check-vite-graph.mjs create mode 100644 packages/ts/src/enums.ts diff --git a/packages/ts/package.json b/packages/ts/package.json index f6f6a43f3..fa25987b7 100644 --- a/packages/ts/package.json +++ b/packages/ts/package.json @@ -30,13 +30,38 @@ "module": "./index.js", "typings": "./index.d.ts", "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js", + "default": "./index.js" + }, + "./maps": { + "types": "./maps.d.ts", + "import": "./maps.js", + "default": "./maps.js" + }, + "./enums": { + "types": "./enums.d.ts", + "import": "./enums.js", + "default": "./enums.js" + }, + "./dist/*": "./dist/*", + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist/*.js", + "default": "./dist/*.js" + }, + "./package.json": "./package.json" + }, "sideEffects": [ "styles/index.js", "dist/styles/index.js" ], "scripts": { "build": "sha=$(tar cf - ./src | shasum); if [[ $(echo $sha) == $(< .srcsha) ]] && [[ -d \"./dist\" ]]; then echo \"Lib Build Exists\"; else npm run forcebuild; echo $sha > .srcsha; fi", - "forcebuild": "rimraf dist; rollup -c; rm -rf dist/.cache; cp LICENSE README.md package.json ./dist", + "forcebuild": "rimraf dist; rollup -c; rm -rf dist/.cache; cp LICENSE README.md package.json ./dist; cp -R docs ./dist/docs", + "check:vite-graph": "node ./scripts/check-vite-graph.mjs", "publish:dist": "cd ./dist; npm publish" }, "devDependencies": { @@ -50,7 +75,8 @@ "rollup-plugin-rename-node-modules": "^1.3.1", "rollup-plugin-terser": "^7.0.2", "rollup-plugin-typescript2": "^0.31.1", - "rollup-plugin-visualizer": "^4.2.2" + "rollup-plugin-visualizer": "^4.2.2", + "vite": "^6.2.4" }, "dependencies": { "@emotion/css": "^11.7.1", diff --git a/packages/ts/rollup.config.js b/packages/ts/rollup.config.js index 33af42a39..080f3c340 100644 --- a/packages/ts/rollup.config.js +++ b/packages/ts/rollup.config.js @@ -48,7 +48,7 @@ const plugins = [ export default [ { - input: 'src/index.ts', + input: ['src/index.ts', 'src/enums.ts'], external: regexesOfPackages, treeshake: false, output: { @@ -62,6 +62,7 @@ export default [ }, { input: 'src/maps.ts', + external: regexesOfPackages, output: { dir: 'dist', format: 'esm', diff --git a/packages/ts/scripts/check-vite-graph.mjs b/packages/ts/scripts/check-vite-graph.mjs new file mode 100644 index 000000000..353a9c83c --- /dev/null +++ b/packages/ts/scripts/check-vite-graph.mjs @@ -0,0 +1,257 @@ +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { existsSync } from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { build } from 'vite' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const packageRoot = path.resolve(__dirname, '..') +const distRoot = path.join(packageRoot, 'dist') +const srcRoot = path.join(packageRoot, 'src') +const shouldAssert = process.argv.includes('--assert') + +const heavyPackages = ['elkjs', 'three', 'leaflet', 'maplibre-gl'] + +const fixtures = [ + { + name: 'root-enum', + code: "import { CurveType } from '@unovis/ts'; console.log(CurveType.MonotoneX)", + }, + { + name: 'enums-entrypoint', + code: "import { CurveType } from '@unovis/ts/enums'; console.log(CurveType.MonotoneX)", + }, + { + name: 'granular-curve', + code: "import { CurveType } from '@unovis/ts/types/curve'; console.log(CurveType.MonotoneX)", + }, + { + name: 'line-chart', + code: "import { Line } from '@unovis/ts'; console.log(Boolean(Line))", + }, + { + name: 'graph-control', + code: "import { Graph } from '@unovis/ts'; console.log(Boolean(Graph))", + }, +] + +function normalizeId (id) { + return id.replace(/\\/g, '/') +} + +function parseExportTargets (source) { + return source + .split('\n') + .map(line => line.trim()) + .filter(line => line.startsWith('export') && line.includes(' from ')) + .map((line) => { + const quote = line.includes("'") ? "'" : '"' + const start = line.indexOf(quote) + const end = line.lastIndexOf(quote) + return start >= 0 && end > start ? line.slice(start + 1, end) : null + }) + .filter(Boolean) +} + +function detectHeavyPackages (moduleIds) { + const heavy = new Set() + for (const id of moduleIds) { + for (const pkg of heavyPackages) { + if ( + id.includes(`/node_modules/${pkg}/`) || + id.includes(`/node_modules/.pnpm/${pkg}@`) || + id.includes(`/node_modules/.pnpm/${pkg}_`) + ) { + heavy.add(pkg) + } + } + } + return [...heavy].sort() +} + +function makeLocalResolverPlugin () { + return { + name: 'resolve-unovis-local-dist', + resolveId (source) { + if (source === '@unovis/ts') return path.join(distRoot, 'index.js') + if (!source.startsWith('@unovis/ts/')) return null + + const subPath = source.slice('@unovis/ts/'.length) + if (subPath.startsWith('dist/')) return path.join(distRoot, subPath.slice('dist/'.length)) + if (subPath.endsWith('.js')) return path.join(distRoot, subPath) + return path.join(distRoot, `${subPath}.js`) + }, + } +} + +function makeCapturePlugin (collectedModules) { + return { + name: 'capture-module-graph', + moduleParsed (info) { + if (!info?.id) return + collectedModules.add(normalizeId(info.id)) + }, + } +} + +async function runFixture (fixture) { + const tmpRoot = await mkdtemp(path.join(os.tmpdir(), 'unovis-vite-graph-')) + const entryFile = path.join(tmpRoot, `${fixture.name}.ts`) + const modules = new Set() + + try { + await writeFile(entryFile, fixture.code, 'utf8') + + await build({ + configFile: false, + logLevel: 'silent', + root: tmpRoot, + plugins: [ + makeLocalResolverPlugin(), + makeCapturePlugin(modules), + ], + build: { + write: false, + minify: false, + sourcemap: false, + target: 'esnext', + rollupOptions: { + input: entryFile, + }, + }, + }) + } finally { + await rm(tmpRoot, { recursive: true, force: true }) + } + + const moduleIds = [...modules] + const nodeModuleIds = moduleIds.filter(id => id.includes('/node_modules/')) + const heavyDeps = detectHeavyPackages(moduleIds) + + return { + fixture: fixture.name, + modulesTransformed: moduleIds.length, + nodeModulesTransformed: nodeModuleIds.length, + heavyDeps, + } +} + +function printRows (rows) { + const columns = [ + ['fixture', 'fixture'], + ['modules', 'modulesTransformed'], + ['node_modules', 'nodeModulesTransformed'], + ['heavy deps', 'heavyDeps'], + ] + + const printable = rows.map(row => ({ + ...row, + heavyDeps: row.heavyDeps.length ? row.heavyDeps.join(', ') : '-', + })) + + const widths = columns.map(([title, key]) => { + const contentWidth = Math.max(...printable.map(row => String(row[key]).length)) + return Math.max(title.length, contentWidth) + }) + + const line = columns + .map(([title], i) => title.padEnd(widths[i], ' ')) + .join(' | ') + const divider = widths.map(w => '-'.repeat(w)).join('-|-') + + console.log(line) + console.log(divider) + + for (const row of printable) { + console.log(columns.map(([, key], i) => String(row[key]).padEnd(widths[i], ' ')).join(' | ')) + } +} + +function evaluateAssertions (rows) { + const byName = new Map(rows.map(r => [r.fixture, r])) + const root = byName.get('root-enum') + const enums = byName.get('enums-entrypoint') + const granular = byName.get('granular-curve') + const failures = [] + + const factor = (a, b) => { + if (!a || !b) return 0 + if (!b.nodeModulesTransformed) return Number.POSITIVE_INFINITY + return a.nodeModulesTransformed / b.nodeModulesTransformed + } + + const enumsFactor = factor(root, enums) + const granularFactor = factor(root, granular) + + if (!(enumsFactor >= 10 || granularFactor >= 10)) { + failures.push(`expected >=10x node_modules reduction (root vs enums/granular), got enums=${enumsFactor.toFixed(2)}x granular=${granularFactor.toFixed(2)}x`) + } + + if (enums?.heavyDeps.length) { + failures.push(`expected no heavy deps for enums-entrypoint, found: ${enums.heavyDeps.join(', ')}`) + } + + if (granular?.heavyDeps.length) { + failures.push(`expected no heavy deps for granular-curve, found: ${granular.heavyDeps.join(', ')}`) + } + + return { failures, enumsFactor, granularFactor } +} + +async function main () { + const required = [path.join(distRoot, 'index.js'), path.join(distRoot, 'enums.js')] + const missing = required.filter(file => !existsSync(file)) + if (missing.length) { + const relMissing = missing.map(file => path.relative(packageRoot, file)) + throw new Error(`Missing build artifacts: ${relMissing.join(', ')}. Run \"pnpm --filter @unovis/ts forcebuild\" first.`) + } + + const [indexSource, typesSource] = await Promise.all([ + readFile(path.join(srcRoot, 'index.ts'), 'utf8'), + readFile(path.join(srcRoot, 'types.ts'), 'utf8'), + ]) + + const indexTargets = parseExportTargets(indexSource) + const typeTargets = parseExportTargets(typesSource) + + console.log('Vite module-graph analysis for @unovis/ts') + console.log('') + console.log(`src/index.ts re-export targets: ${indexTargets.length}`) + console.log(indexTargets.map(t => ` - ${t}`).join('\n')) + console.log('') + console.log(`src/types.ts re-export targets: ${typeTargets.length}`) + console.log(typeTargets.map(t => ` - ${t}`).join('\n')) + console.log('') + + const rows = [] + for (const fixture of fixtures) { + rows.push(await runFixture(fixture)) + } + + printRows(rows) + console.log('') + + const { failures, enumsFactor, granularFactor } = evaluateAssertions(rows) + const enumsFactorLabel = Number.isFinite(enumsFactor) ? `${enumsFactor.toFixed(2)}x` : 'infinite' + const granularFactorLabel = Number.isFinite(granularFactor) ? `${granularFactor.toFixed(2)}x` : 'infinite' + console.log(`root-enum -> enums-entrypoint reduction: ${enumsFactorLabel}`) + console.log(`root-enum -> granular-curve reduction: ${granularFactorLabel}`) + + if (shouldAssert) { + if (failures.length) { + console.log('') + console.log('assertion failures:') + for (const failure of failures) console.log(` - ${failure}`) + process.exitCode = 1 + return + } + console.log('') + console.log('assertions passed') + } +} + +main().catch((error) => { + console.error(error.message) + process.exitCode = 1 +}) diff --git a/packages/ts/src/enums.ts b/packages/ts/src/enums.ts new file mode 100644 index 000000000..bcd3852a2 --- /dev/null +++ b/packages/ts/src/enums.ts @@ -0,0 +1,26 @@ +export { CurveType } from 'types/curve' +export { Arrangement, Orientation, Position, PositionStrategy } from 'types/position' +export { Shape } from 'types/shape' +export { ComponentType, Sizing } from 'types/component' +export { FitMode, TextAlign, TrimMode, VerticalAlign } from 'types/text' +export { FindNearestDirection } from 'types/data' +export { Direction } from 'types/direction' + +export { AxisType } from 'components/axis/types' +export { ChordLabelAlignment } from 'components/chord-diagram/types' +export { LeafletMapPointShape, LeafletMapRenderer } from 'components/leaflet-map/types' +export { + GraphFitViewAlignment, + GraphLayoutType, + GraphLinkArrowStyle, + GraphLinkStyle, + GraphNodeSelectionHighlightMode, + GraphNodeShape, +} from 'components/graph/types' +export { VisControlsOrientation } from 'components/vis-controls/types' +export { FreeBrushMode } from 'components/free-brush/types' +export { BulletLegendOrientation, BulletShape } from 'components/bullet-legend/types' +export { XYLabelPositioning } from 'components/xy-labels/types' +export { NestedDonutDirection, NestedDonutSegmentLabelAlignment } from 'components/nested-donut/types' +export { PlotlineLabelOrientation, PlotlineLabelPosition, PlotlineLineStylePresets } from 'components/plotline/types' +export { PlotbandLabelOrientation, PlotbandLabelPosition } from 'components/plotband/types'