diff --git a/packages/devframe/package.json b/packages/devframe/package.json index 4f8ac8a..558c750 100644 --- a/packages/devframe/package.json +++ b/packages/devframe/package.json @@ -84,9 +84,7 @@ }, "devDependencies": { "@modelcontextprotocol/sdk": "catalog:deps", - "ansis": "catalog:deps", "get-port-please": "catalog:deps", - "human-id": "catalog:inlined", "immer": "catalog:deps", "launch-editor": "catalog:deps", "mlly": "catalog:build", @@ -101,13 +99,11 @@ "whenexpr": "catalog:deps" }, "inlinedDependencies": { - "ansis": "4.3.0", "bundle-name": "4.1.0", "default-browser": "5.5.0", "default-browser-id": "5.0.1", "define-lazy-prop": "3.0.0", "get-port-please": "3.2.0", - "human-id": "4.1.3", "immer": "11.1.8", "is-docker": "3.0.0", "is-in-ssh": "1.0.0", diff --git a/packages/devframe/src/utils/colors.ts b/packages/devframe/src/utils/colors.ts index ecea9ff..82649e1 100644 --- a/packages/devframe/src/utils/colors.ts +++ b/packages/devframe/src/utils/colors.ts @@ -1,5 +1,3 @@ -import ansis from 'ansis' - /** * A colorizer — callable as a function (`colors.red('foo')`) or as a * tagged template (``colors.red`foo ${bar}` ``). @@ -26,15 +24,33 @@ export interface Colors { underline: ColorFn } +function makeColor(open: number, close: number): ColorFn { + const o = `\x1B[${open}m` + const c = `\x1B[${close}m` + return ((arg: unknown, ...values: unknown[]): string => { + if (Array.isArray(arg) && 'raw' in arg) { + const strings = arg as unknown as TemplateStringsArray + let out = '' + for (let i = 0; i < strings.length; i++) { + out += strings[i] + if (i < values.length) + out += String(values[i]) + } + return `${o}${out}${c}` + } + return `${o}${String(arg)}${c}` + }) as ColorFn +} + export const colors: Colors = { - blue: ansis.blue, - cyan: ansis.cyan, - gray: ansis.gray, - green: ansis.green, - red: ansis.red, - yellow: ansis.yellow, - bold: ansis.bold, - dim: ansis.dim, - reset: ansis.reset, - underline: ansis.underline, + blue: makeColor(34, 39), + cyan: makeColor(36, 39), + gray: makeColor(90, 39), + green: makeColor(32, 39), + red: makeColor(31, 39), + yellow: makeColor(33, 39), + bold: makeColor(1, 22), + dim: makeColor(2, 22), + reset: makeColor(0, 0), + underline: makeColor(4, 24), } diff --git a/packages/devframe/src/utils/human-id.ts b/packages/devframe/src/utils/human-id.ts index fb71b15..2e3b312 100644 --- a/packages/devframe/src/utils/human-id.ts +++ b/packages/devframe/src/utils/human-id.ts @@ -1,9 +1,21 @@ -import { humanId as humanIdImpl } from 'human-id' +// Vendored from `human-id@4.1.3` (MIT — Copyright (c) 2018 RienNeVaPlus). +// The upstream is shipped as CommonJS, which tsdown wraps with a shim that +// imports `node:module`. Inlining the relevant subset keeps this entry +// runtime-agnostic (see `test/runtime-agnostic.test.ts`). +const adjectives = ['afraid', 'all', 'beige', 'better', 'big', 'blue', 'bold', 'brave', 'breezy', 'bright', 'brown', 'bumpy', 'busy', 'calm', 'chatty', 'chilly', 'chubby', 'clean', 'clear', 'clever', 'cold', 'common', 'cool', 'cozy', 'crisp', 'cuddly', 'curly', 'curvy', 'cute', 'cyan', 'dark', 'deep', 'dirty', 'dry', 'dull', 'eager', 'early', 'easy', 'eight', 'eighty', 'eleven', 'empty', 'every', 'fair', 'famous', 'fancy', 'fast', 'few', 'fiery', 'fifty', 'fine', 'five', 'flat', 'floppy', 'fluffy', 'forty', 'four', 'frank', 'free', 'fresh', 'fruity', 'full', 'funky', 'funny', 'fuzzy', 'gentle', 'giant', 'gold', 'good', 'goofy', 'great', 'green', 'grumpy', 'happy', 'heavy', 'hip', 'honest', 'hot', 'huge', 'humble', 'hungry', 'icy', 'itchy', 'jolly', 'khaki', 'kind', 'large', 'late', 'lazy', 'legal', 'lemon', 'light', 'little', 'long', 'loose', 'loud', 'lovely', 'lucky', 'major', 'many', 'metal', 'mighty', 'modern', 'moody', 'neat', 'new', 'nice', 'nine', 'ninety', 'odd', 'old', 'olive', 'open', 'orange', 'perky', 'petite', 'pink', 'plain', 'plenty', 'polite', 'pretty', 'proud', 'public', 'puny', 'purple', 'quick', 'quiet', 'rare', 'ready', 'real', 'red', 'rich', 'ripe', 'salty', 'seven', 'shaggy', 'shaky', 'sharp', 'shiny', 'short', 'shy', 'silent', 'silly', 'silver', 'six', 'sixty', 'slick', 'slimy', 'slow', 'small', 'smart', 'smooth', 'social', 'soft', 'solid', 'some', 'sour', 'sparkly', 'spicy', 'spotty', 'stale', 'strict', 'strong', 'sunny', 'sweet', 'swift', 'tall', 'tame', 'tangy', 'tasty', 'ten', 'tender', 'thick', 'thin', 'thirty', 'three', 'tidy', 'tiny', 'tired', 'tough', 'tricky', 'true', 'twelve', 'twenty', 'two', 'upset', 'vast', 'violet', 'wacky', 'warm', 'wet', 'whole', 'wicked', 'wide', 'wild', 'wise', 'witty', 'yellow', 'young', 'yummy'] + +const nouns = ['actors', 'ads', 'adults', 'aliens', 'animals', 'ants', 'apes', 'apples', 'areas', 'baboons', 'badgers', 'bags', 'balloons', 'bananas', 'banks', 'bars', 'baths', 'bats', 'beans', 'bears', 'beds', 'beers', 'bees', 'berries', 'bikes', 'birds', 'boats', 'bobcats', 'books', 'bottles', 'boxes', 'breads', 'brooms', 'buckets', 'bugs', 'buses', 'bushes', 'buttons', 'camels', 'cameras', 'candies', 'candles', 'canyons', 'carpets', 'carrots', 'cars', 'cases', 'cats', 'chairs', 'chefs', 'chicken', 'cities', 'clocks', 'cloths', 'clouds', 'clowns', 'clubs', 'coats', 'cobras', 'coins', 'colts', 'comics', 'cooks', 'corners', 'cougars', 'cows', 'crabs', 'crews', 'cups', 'cycles', 'dancers', 'days', 'deer', 'deserts', 'dingos', 'dodos', 'dogs', 'dolls', 'donkeys', 'donuts', 'doodles', 'doors', 'dots', 'dragons', 'drinks', 'dryers', 'ducks', 'eagles', 'ears', 'eels', 'eggs', 'emus', 'ends', 'experts', 'eyes', 'facts', 'falcons', 'fans', 'feet', 'files', 'flies', 'flowers', 'forks', 'foxes', 'friends', 'frogs', 'games', 'garlics', 'geckos', 'geese', 'ghosts', 'gifts', 'glasses', 'goats', 'grapes', 'groups', 'guests', 'hairs', 'hands', 'hats', 'heads', 'hoops', 'hornets', 'horses', 'hotels', 'hounds', 'houses', 'humans', 'icons', 'ideas', 'impalas', 'insects', 'islands', 'items', 'jars', 'jeans', 'jobs', 'jokes', 'keys', 'kids', 'kings', 'kiwis', 'knives', 'lamps', 'lands', 'laws', 'lemons', 'lies', 'lights', 'lilies', 'lines', 'lions', 'lizards', 'llamas', 'loops', 'mails', 'mammals', 'mangos', 'maps', 'masks', 'meals', 'melons', 'memes', 'meteors', 'mice', 'mirrors', 'moles', 'moments', 'monkeys', 'months', 'moons', 'moose', 'mugs', 'nails', 'needles', 'news', 'nights', 'numbers', 'olives', 'onions', 'oranges', 'otters', 'owls', 'pandas', 'pans', 'pants', 'papayas', 'papers', 'parents', 'parks', 'parrots', 'parts', 'paths', 'paws', 'peaches', 'pears', 'peas', 'pens', 'pets', 'phones', 'pianos', 'pigs', 'pillows', 'places', 'planes', 'planets', 'plants', 'plums', 'poems', 'poets', 'points', 'pots', 'pugs', 'pumas', 'queens', 'rabbits', 'radios', 'rats', 'ravens', 'readers', 'regions', 'results', 'rice', 'rings', 'rivers', 'rockets', 'rocks', 'rooms', 'roses', 'rules', 'sails', 'schools', 'seals', 'seas', 'sheep', 'shirts', 'shoes', 'showers', 'shrimps', 'sides', 'signs', 'singers', 'sites', 'sloths', 'snails', 'snakes', 'socks', 'spiders', 'spies', 'spoons', 'squids', 'stamps', 'stars', 'states', 'steaks', 'streets', 'suits', 'suns', 'swans', 'symbols', 'tables', 'taxes', 'taxis', 'teams', 'teeth', 'terms', 'things', 'ties', 'tigers', 'times', 'tips', 'tires', 'toes', 'tools', 'towns', 'toys', 'trains', 'trams', 'trees', 'turkeys', 'turtles', 'vans', 'views', 'walls', 'wasps', 'waves', 'ways', 'webs', 'weeks', 'windows', 'wings', 'wolves', 'wombats', 'words', 'worlds', 'worms', 'yaks', 'years', 'zebras', 'zoos'] + +const verbs = ['accept', 'act', 'add', 'admire', 'agree', 'allow', 'appear', 'argue', 'arrive', 'ask', 'attack', 'attend', 'bake', 'bathe', 'battle', 'beam', 'beg', 'begin', 'behave', 'bet', 'boil', 'bow', 'brake', 'brush', 'build', 'burn', 'buy', 'call', 'camp', 'care', 'carry', 'change', 'cheat', 'check', 'cheer', 'chew', 'clap', 'clean', 'cough', 'count', 'cover', 'crash', 'create', 'cross', 'cry', 'cut', 'dance', 'decide', 'deny', 'design', 'dig', 'divide', 'do', 'double', 'doubt', 'draw', 'dream', 'dress', 'drive', 'drop', 'drum', 'eat', 'end', 'enjoy', 'enter', 'exist', 'fail', 'fall', 'feel', 'fetch', 'film', 'find', 'fix', 'flash', 'float', 'flow', 'fly', 'fold', 'follow', 'fry', 'give', 'glow', 'go', 'grab', 'greet', 'grin', 'grow', 'guess', 'hammer', 'hang', 'happen', 'heal', 'hear', 'help', 'hide', 'hope', 'hug', 'hunt', 'invent', 'invite', 'itch', 'jam', 'jog', 'join', 'joke', 'judge', 'juggle', 'jump', 'kick', 'kiss', 'kneel', 'knock', 'know', 'laugh', 'lay', 'lead', 'learn', 'leave', 'lick', 'lie', 'like', 'listen', 'live', 'look', 'lose', 'love', 'make', 'march', 'marry', 'mate', 'matter', 'melt', 'mix', 'move', 'nail', 'notice', 'obey', 'occur', 'open', 'own', 'pay', 'peel', 'pick', 'play', 'poke', 'post', 'press', 'prove', 'pull', 'pump', 'punch', 'push', 'raise', 'read', 'refuse', 'relate', 'relax', 'remain', 'repair', 'repeat', 'reply', 'report', 'rescue', 'rest', 'retire', 'return', 'rhyme', 'ring', 'roll', 'rule', 'run', 'rush', 'say', 'scream', 'search', 'see', 'sell', 'send', 'serve', 'shake', 'share', 'shave', 'shine', 'shop', 'shout', 'show', 'sin', 'sing', 'sink', 'sip', 'sit', 'sleep', 'slide', 'smash', 'smell', 'smile', 'smoke', 'sneeze', 'sniff', 'sort', 'speak', 'spend', 'stand', 'stare', 'start', 'stay', 'stick', 'stop', 'strive', 'study', 'swim', 'switch', 'take', 'talk', 'tan', 'tap', 'taste', 'teach', 'tease', 'tell', 'thank', 'think', 'throw', 'tickle', 'tie', 'trade', 'train', 'travel', 'try', 'turn', 'type', 'unite', 'vanish', 'visit', 'wait', 'walk', 'warn', 'wash', 'watch', 'wave', 'wear', 'win', 'wink', 'wish', 'wonder', 'work', 'worry', 'write', 'yawn', 'yell'] + +function pick(arr: readonly string[]): string { + return arr[(Math.random() * arr.length) | 0] +} /** * Generate a human-readable, lowercase, dash-separated random ID - * (e.g. `bright-orange-tiger`). + * (e.g. `bright-orange-tigers-jump`). */ export function humanId(): string { - return humanIdImpl({ separator: '-', capitalize: false }) + return `${pick(adjectives)}-${pick(nouns)}-${pick(verbs)}` } diff --git a/packages/devframe/test/runtime-agnostic.test.ts b/packages/devframe/test/runtime-agnostic.test.ts new file mode 100644 index 0000000..cbd3375 --- /dev/null +++ b/packages/devframe/test/runtime-agnostic.test.ts @@ -0,0 +1,113 @@ +import { existsSync, readFileSync } from 'node:fs' +import { builtinModules } from 'node:module' +import { dirname, relative, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' + +// Entries that must run in any JS runtime (browser, edge, Node). +// Keep in sync with `packages/devframe/package.json` exports — explicit list +// so additions are a conscious choice. +const AGNOSTIC_ENTRIES = [ + 'client/index.mjs', + 'utils/colors.mjs', + 'utils/events.mjs', + 'utils/hash.mjs', + 'utils/human-id.mjs', + 'utils/nanoid.mjs', + 'utils/promise.mjs', + 'utils/shared-state.mjs', + 'utils/streaming-channel.mjs', + 'utils/structured-clone.mjs', + 'utils/when.mjs', +] as const + +const distRoot = fileURLToPath(new URL('../dist/', import.meta.url)) + +const nodeBuiltins = new Set([ + ...builtinModules, + ...builtinModules.map(m => `node:${m}`), +]) + +const IMPORT_FROM_RE = /(?:import|export)[^'"`;]*?from\s*['"]([^'"]+)['"]/g +const SIDE_EFFECT_IMPORT_RE = /(?:^|[\s;{])import\s*['"]([^'"]+)['"]/g +const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g +const REQUIRE_RE = /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g + +interface Offender { + importer: string + specifier: string + statement: string +} + +function collectImports(src: string): string[] { + const ids: string[] = [] + for (const re of [IMPORT_FROM_RE, SIDE_EFFECT_IMPORT_RE, DYNAMIC_IMPORT_RE, REQUIRE_RE]) { + for (const match of src.matchAll(re)) + ids.push(match[1]) + } + return ids +} + +function statementFor(src: string, specifier: string): string { + for (const re of [IMPORT_FROM_RE, SIDE_EFFECT_IMPORT_RE, DYNAMIC_IMPORT_RE, REQUIRE_RE]) { + for (const match of src.matchAll(re)) { + if (match[1] === specifier) + return match[0].trim() + } + } + return '' +} + +function scanTransitiveBuiltins(entryAbs: string): Offender[] { + const visited = new Set() + const offenders: Offender[] = [] + const queue: string[] = [entryAbs] + + while (queue.length) { + const file = queue.shift()! + if (visited.has(file)) + continue + visited.add(file) + + if (!existsSync(file)) + continue + const src = readFileSync(file, 'utf8') + + for (const id of collectImports(src)) { + if (nodeBuiltins.has(id)) { + offenders.push({ + importer: relative(distRoot, file), + specifier: id, + statement: statementFor(src, id), + }) + continue + } + // Relative import — follow it. + if (id.startsWith('./') || id.startsWith('../')) { + const resolved = resolve(dirname(file), id) + queue.push(resolved) + } + // Bare specifiers other than node builtins are treated as resolved + // (they'd be installed-as-dep; not part of this check). + } + } + + return offenders +} + +describe('runtime-agnostic dist entries', () => { + for (const entry of AGNOSTIC_ENTRIES) { + it(entry, () => { + const filePath = resolve(distRoot, entry) + expect(existsSync(filePath), `Missing ${entry} — run \`pnpm build\` first`).toBe(true) + + const offenders = scanTransitiveBuiltins(filePath) + const formatted = offenders.map(o => ` ${o.importer}: ${o.statement}`) + + expect( + formatted, + `${entry} (transitively) must not import node builtins`, + ).toEqual([]) + }) + } +}) diff --git a/packages/devframe/tsdown.config.ts b/packages/devframe/tsdown.config.ts index 1009e9e..9c68fd5 100644 --- a/packages/devframe/tsdown.config.ts +++ b/packages/devframe/tsdown.config.ts @@ -70,13 +70,11 @@ export default defineConfig({ ], onlyBundle: [ 'acorn', - 'ansis', 'bundle-name', 'default-browser', 'default-browser-id', 'define-lazy-prop', 'get-port-please', - 'human-id', 'immer', 'is-docker', 'is-in-ssh', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 940ae2e..4de7e55 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,9 +40,6 @@ catalogs: '@valibot/to-json-schema': specifier: ^1.7.0 version: 1.7.0 - ansis: - specifier: ^4.3.0 - version: 4.3.0 birpc: specifier: ^4.0.0 version: 4.0.0 @@ -140,9 +137,6 @@ catalogs: '@antfu/utils': specifier: ^9.3.0 version: 9.3.0 - human-id: - specifier: ^4.1.3 - version: 4.1.3 ua-parser-modern: specifier: ^0.1.1 version: 0.1.1 @@ -165,8 +159,8 @@ catalogs: version: 8.18.1 overrides: - semver: ^7.8.0 chokidar: ^5.0.0 + semver: ^7.8.0 importers: @@ -337,15 +331,9 @@ importers: '@modelcontextprotocol/sdk': specifier: catalog:deps version: 1.29.0(zod@4.4.3) - ansis: - specifier: catalog:deps - version: 4.3.0 get-port-please: specifier: catalog:deps version: 3.2.0 - human-id: - specifier: catalog:inlined - version: 4.1.3 immer: specifier: catalog:deps version: 11.1.8 @@ -4325,10 +4313,6 @@ packages: httpxy@0.5.1: resolution: {integrity: sha512-JPhqYiixe1A1I+MXDewWDZqeudBGU8Q9jCHYN8ML+779RQzLjTi78HBvWz4jMxUD6h2/vUL12g4q/mFM0OUw1A==} - human-id@4.1.3: - resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} - hasBin: true - human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -10456,8 +10440,6 @@ snapshots: httpxy@0.5.1: {} - human-id@4.1.3: {} - human-signals@5.0.0: {} iconv-lite@0.6.3: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 96cc869..7cbfe88 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -33,7 +33,6 @@ catalogs: deps: '@modelcontextprotocol/sdk': ^1.29.0 '@valibot/to-json-schema': ^1.7.0 - ansis: ^4.3.0 birpc: ^4.0.0 cac: ^7.0.0 get-port-please: ^3.2.0 @@ -69,7 +68,6 @@ catalogs: preact: ^10.29.1 inlined: '@antfu/utils': ^9.3.0 - human-id: ^4.1.3 ua-parser-modern: ^0.1.1 testing: '@playwright/test': ^1.50.0 diff --git a/tests/__snapshots__/tsnapi/devframe/utils/colors.snapshot.js b/tests/__snapshots__/tsnapi/devframe/utils/colors.snapshot.js index 07944a1..d6fa07f 100644 --- a/tests/__snapshots__/tsnapi/devframe/utils/colors.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/utils/colors.snapshot.js @@ -1,6 +1,6 @@ /** * Generated by tsnapi — public API snapshot of `devframe/utils/colors` */ -// #region Other -export { colors } +// #region Variables +export var colors /* const */ // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/devframe/utils/human-id.snapshot.js b/tests/__snapshots__/tsnapi/devframe/utils/human-id.snapshot.js index 1035a68..83b3df5 100644 --- a/tests/__snapshots__/tsnapi/devframe/utils/human-id.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/utils/human-id.snapshot.js @@ -1,6 +1,6 @@ /** * Generated by tsnapi — public API snapshot of `devframe/utils/human-id` */ -// #region Other -export { humanId } +// #region Functions +export function humanId() {} // #endregion \ No newline at end of file