Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions packages/devframe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
40 changes: 28 additions & 12 deletions packages/devframe/src/utils/colors.ts
Original file line number Diff line number Diff line change
@@ -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}` ``).
Expand All @@ -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),
}
18 changes: 15 additions & 3 deletions packages/devframe/src/utils/human-id.ts
Original file line number Diff line number Diff line change
@@ -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)}`
}
113 changes: 113 additions & 0 deletions packages/devframe/test/runtime-agnostic.test.ts
Original file line number Diff line number Diff line change
@@ -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<string>()
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([])
})
}
})
2 changes: 0 additions & 2 deletions packages/devframe/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
20 changes: 1 addition & 19 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/__snapshots__/tsnapi/devframe/utils/colors.snapshot.js
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading