Install
Same three ways as every jen pack. The pack writes source files into your project – nothing it writes depends on the pack or on any library.
Globally
$ npm install -g @codejen/jen @codejen/pack-js $ jen js:element --name=my-counter --attrs=count:number
Locally
$ npm install --save-dev @codejen/jen @codejen/pack-js $ npx jen js:element --name=my-counter --attrs=count:number
Not at all
Ask for a js: generator without the pack installed and jen fetches @codejen/pack-js into a throwaway directory, runs it once and removes it again (first-party @codejen scope only; JEN_NO_FETCH=1 turns it off).
$ npx @codejen/jen js:element --name=my-counter --attrs=count:number
Output styles
Every generator writes in one of three styles, picked from the nearest config file above the folder the files go to:
- TypeScript, with a
tsconfig.json:.tsfiles using only erasable syntax – noenum, no namespaces, no parameter properties – so Node runs them directly, and any bundler strips them - JSDoc, with a
jsconfig.json, or atsconfig.jsonthat setscheckJs:.jsfiles with// @ts-checkand types in JSDoc comments, checked by your editor without a build step - Plain JavaScript, with neither: the same
.jsfiles without the types
--types=ts, --types=jsdoc or --types=none overrides it. Relative imports end in .ts where the tsconfig allows that (allowImportingTsExtensions or rewriteRelativeImportExtensions), else in .js, which TypeScript maps to the .ts file.
Generators that take --withTest write a test next to the file: with Vitest if the nearest package.json lists it or there's a Vite or Vitest config, else with node:test. --runner=node or --runner=vitest overrides it.
The keyboard modules are plain files in the pack's templates/ folder, as .ts and as .js with JSDoc; the plain JavaScript is the JSDoc file with its types taken out. Elements and type patterns are made in code, from your names and types.
Projects
A new project in its own directory, at the latest versions on npm – so these two need the network. --name is the directory and the package name; jen stops if that directory already exists and isn't empty. Like every jen run, the files go through the plan: --dry-run lists them first, and nothing is written before you see it. Nothing is installed either: run npm install in the project afterwards.
A new project starts as plain JavaScript, whatever tsconfig.json is around it; --types=ts makes it TypeScript. Every js: generator you run in the project later writes in its style.
js:vite
Runs npm create vite@latest with the template for --framework and --types – vanilla, vue, react, svelte or any other template create-vite offers – without its questions, in a scratch directory, and hands what it makes to jen's plan. An unknown framework fails with create-vite's own list.
$ jen js:vite --name=shop --framework=react --types=ts js:vite (pack @codejen/pack-js) – create a new Vite project in its own directory, from npm create vite@latest + shop/.gitignore + shop/.oxlintrc.json + shop/README.md + shop/index.html + shop/package.json + shop/public/favicon.svg + shop/public/icons.svg + shop/src/App.css + shop/src/App.tsx + shop/src/assets/hero.png + shop/src/assets/react.svg + shop/src/assets/vite.svg + shop/src/index.css + shop/src/main.tsx + shop/tsconfig.app.json + shop/tsconfig.json + shop/tsconfig.node.json + shop/vite.config.ts ✓ 18 written
Params
name
the project and its new directory, e.g. shop
framework
a create-vite template: vanilla, vue, react, preact, lit, svelte, solid, qwik …
types
ts or none (plain JavaScript)
js:esbuild
A page with a counter button, bundled by esbuild into dist/: build bundles once, dev rebuilds on every change and serves the project on esbuild's own server. esbuild has no npm create, so the files come from the pack, and package.json gets the latest versions from npm. Besides --types=ts it takes --types=jsdoc, with a jsconfig.json. esbuild only strips types, so a TypeScript project gets a separate check script. --withTest adds a first test with node:test – --runner=vitest switches – and later --withTest runs use the same runner.
$ jen js:esbuild --name=tool --types=ts --withTest js:esbuild (pack @codejen/pack-js) – create a new project bundled by esbuild, in its own directory + tool/package.json + tool/index.html + tool/.gitignore + tool/src/style.css + tool/src/main.ts + tool/src/counter.ts + tool/src/counter.test.ts + tool/tsconfig.json ✓ 8 written
Params
name
the project and its new directory, e.g. shop
types
ts, jsdoc or none (plain JavaScript)
withTest
add a test setup and a first test
runner
node (node:test) or vitest
Web components
js:element
A custom element as a class, registered with customElements.define. Every attribute in --attrs gets a property that reflects it, converted to its type: numbers through Number(), booleans as present or absent (toggleAttribute), kebab-case names as camelCase properties (max-value becomes maxValue). observedAttributes lists them, and attributeChangedCallback takes only their names. Events from --events get a typed event map: emit('change', { value: 1 }) checks the detail, an event without a detail takes none, and listeners added with addEventListener('change', …) know event.detail.value is a number. Events bubble, and leave a shadow root the element sits in.
It renders into the light DOM: its children stay where they are, page styles reach them, and nothing needs a slot. Its own styles go in a constructable stylesheet, adopted once by the document – or the shadow root the element is in. With --shadow=open or --shadow=closed, it gets a shadow root with a <slot> instead, and the stylesheet is shared by every instance's shadow root. --form makes it form-associated: ElementInternals sends its value with the form, and it gets form, validity, checkValidity() and reportValidity(). In TypeScript, the tag joins HTMLElementTagNameMap, so document.querySelector('star-rating') is a StarRating.
--into loads the element: in a page, its tag and a module script go in before </body>; in a module like src/main.ts, an import line goes in above the first line of code.
$ jen js:element --name=star-rating --attrs=max:number,readonly:boolean --events="change:{value:number}" --form --into=index.html --dir=src/components js:element (pack @codejen/pack-js) – create a custom element with typed attributes and events (--into loads it from a page or module) + src/components/star-rating.ts ~ index.html + <star-rating></star-rating> ~ index.html + <script type="module" src="./src/components/star-rating.ts"></script> ✓ 2 written
--attrs=count:number,label,open:boolean --events='change:{value:number}': the properties are typed, and every change shows in the attributesJSDoc can't add to a global type from a .js file, so in the JSDoc style the tag goes into an elements.d.ts instead – the nearest one with a // jen:elements marker, or a new one next to the element:
$ jen js:element --name=my-counter --attrs=count:number --events=change --dir=src/components js:element (pack @codejen/pack-js) – create a custom element with typed attributes and events (--into loads it from a page or module) + src/components/my-counter.js + src/components/elements.d.ts ~ src/components/elements.d.ts + 'my-counter': import('./my-counter.js').MyCounter; ✓ 2 written
Params
name
the tag, lowercase with a hyphen: star-rating makes the class StarRating
attrs
comma-separated name:type attributes – string (default), number or boolean
events
comma-separated name:{detail} events, e.g. "change:{value: number}"; without a detail type, none
shadow
none (light DOM), or open or closed for a shadow root
form
form-associated, with a value attribute
into
a page or module to load the element from
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
Keyboard patterns
The keyboard interaction of the ARIA Authoring Practices Guide, as plain modules any page or element can use. Each exports one function that takes the container and returns a function that removes its listeners again – or pass signal from an AbortController. Arrow keys follow the writing direction, so they swap in right-to-left text.
js:roving
Roving tabindex: a group of controls is one stop in the tab order, and arrow keys move within it. It knows the items and arrows from the container's role – toolbars go left and right, listboxes up and down, radio groups both ways – and aria-orientation, items, orientation and wrap change that. In tab lists, radio groups and listboxes, selection follows focus: aria-selected or aria-checked moves along, and a tab list shows the panel its tab aria-controls. onChange hears about every move.
$ jen js:roving --dir=src/lib js:roving (pack @codejen/pack-js) – create roving tabindex for toolbars, tab lists, radio groups and listboxes (ARIA APG) + src/lib/roving-tabindex.ts ✓ 1 written
Params
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
js:menu
Menus and menubars: arrow keys move through the items, Home and End jump to the ends, and typing a letter or two moves to the item that starts with them. On a popover menu, its popovertarget button opens it with Arrow Down or Arrow Up (at the last item), the first item gets focus, and Escape, Tab or choosing an item closes it and returns focus to the button. A submenu's items stay out of their parent's way.
$ jen js:menu --dir=src/lib js:menu (pack @codejen/pack-js) – create menu and menubar keyboard interaction: arrows, Home/End, typeahead, Escape, popover-aware (ARIA APG) + src/lib/menu.ts ✓ 1 written
Params
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
js:grid
Two-dimensional navigation for role="grid": arrow keys move between cells, Home and End to the ends of the row, Ctrl+Home and Ctrl+End to the first and last cell, Page Up and Page Down by pageSize rows. A cell with a control in it focuses the control. rows and cells take other selectors, and onMove reports the row and column.
$ jen js:grid --dir=src/lib js:grid (pack @codejen/pack-js) – create two-dimensional grid navigation with arrows, Home/End, Page Up/Down (ARIA APG) + src/lib/grid-navigation.ts ✓ 1 written
Params
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
js:hotkeys
A registry for keyboard shortcuts: const keys = createHotkeys(), then keys.bind('mod+k', openSearch, { description: 'Search' }). mod is ⌘ on Apple devices and Ctrl elsewhere. Symbols like ? match whatever Shift your keyboard layout needs for them. Bindings are ignored while someone types in a field, unless they set inputs: true. A binding with a scope only fires inside that element, and wins over a global one. createHotkeys({ help: true }) binds ? to a dialog that lists every binding with a description.
$ jen js:hotkeys --dir=src/lib js:hotkeys (pack @codejen/pack-js) – create a hotkeys registry: scoped bindings, modifiers, ignores typing, optional help overlay + src/lib/hotkeys.ts ✓ 1 written
Params
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
Type patterns
The patterns of the C++ pack's types, for TypeScript – without the syntax Node can't strip. In the JSDoc style they come with the same types, and in plain JavaScript they still work at runtime.
js:class
A class with #private fields, a getter for each, and a static create() to put async setup or validation in later. --fields=id:number,label:string makes the fields and an options type for them. --disposable adds [Symbol.dispose](), so using timer = Timer.create(…) cleans it up at the end of the block (in TypeScript, that needs esnext.disposable in lib).
$ jen js:class --name=timer --fields=id:number,label:string --disposable --withTest --dir=src/lib js:class (pack @codejen/pack-js) – create a class with #private fields, getters and a static create() (--disposable adds Symbol.dispose) + src/lib/timer.ts + src/lib/timer.test.ts ✓ 2 written
Params
name
the type, any case: user-id and UserId both make UserId in user-id.ts
fields
comma-separated name:type fields, e.g. id:number,label:string
disposable
add Symbol.dispose, for using declarations
withTest
add a test next to it
runner
node (node:test) or vitest (default: vitest in a Vite or Vitest project)
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
js:enum
An object as const instead of TypeScript's enum: Direction.Up is 'up', the type Direction is the union of the values, and isDirection(value) checks a value from outside.
$ jen js:enum --name=direction --values=up,down,left,right --withTest --dir=src/lib js:enum (pack @codejen/pack-js) – create an enum as an `as const` object with its union type and a type guard + src/lib/direction.ts + src/lib/direction.test.ts ✓ 2 written
Params
name
the type, any case: user-id and UserId both make UserId in user-id.ts
values
comma-separated values, e.g. "draft, published"
withTest
add a test next to it
runner
node (node:test) or vitest (default: vitest in a Vite or Vitest project)
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
js:union
A tagged union: an interface for each case with a kind, the union of them, and matchShape(shape, { circle: …, square: … }). The handlers have to cover every case, and the switch inside ends in assertNever – add a case and forget it somewhere, and TypeScript says where.
$ jen js:union --name=shape --cases="circle:{radius:number},square:{size:number}" --withTest --dir=src/lib js:union (pack @codejen/pack-js) – create a tagged union type with an exhaustively checked match function + src/lib/shape.ts + src/lib/shape.test.ts ✓ 2 written
Params
name
the type, any case: user-id and UserId both make UserId in user-id.ts
cases
comma-separated kind:{fields} cases, or a bare kind without fields
withTest
add a test next to it
runner
node (node:test) or vitest (default: vitest in a Vite or Vitest project)
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
js:brand
A branded type: a UserId is a string, but not every string is a UserId. userId(value) checks a value and brands it, isUserId(value) is the check on its own – put your validation there. --underlying takes string (the default), number (finite only) or bigint.
$ jen js:brand --name=user-id --dir=src/lib js:brand (pack @codejen/pack-js) – create a branded type with a type guard and a checked constructor + src/lib/user-id.ts ✓ 1 written
Params
name
the type, any case: user-id and UserId both make UserId in user-id.ts
underlying
the branded type: string, number or bigint
withTest
add a test next to it
runner
node (node:test) or vitest (default: vitest in a Vite or Vitest project)
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
js:error
An Error subclass, named after itself so stack traces and logs say what went wrong, with extra fields from --fields and the standard cause: throw new NotFoundError('No such file', { path, cause }). The name gets an Error at the end if it doesn't have one.
$ jen js:error --name=not-found --fields=path:string --dir=src/lib js:error (pack @codejen/pack-js) – create an Error subclass with its own name, extra fields and cause + src/lib/not-found-error.ts ✓ 1 written
Params
name
the type, any case: user-id and UserId both make UserId in user-id.ts
fields
comma-separated name:type extra fields, e.g. status:number
withTest
add a test next to it
runner
node (node:test) or vitest (default: vitest in a Vite or Vitest project)
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
js:emitter
An event emitter on the platform's own EventTarget, with the same typed event map, emit() and listeners as js:element – for stores and services outside the DOM.
$ jen js:emitter --name=store --events="change:{value:number},reset" --dir=src/lib js:emitter (pack @codejen/pack-js) – create an EventTarget subclass with a typed event map + src/lib/store.ts ✓ 1 written
Params
name
the type, any case: user-id and UserId both make UserId in user-id.ts
events
comma-separated name:{detail} events, e.g. "change:{value: number}"; without a detail type, none
withTest
add a test next to it
runner
node (node:test) or vitest (default: vitest in a Vite or Vitest project)
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
Modules and tests
js:module
A module with one exported function of the same name, to fill in – and with --withTest, its test.
$ jen js:module --name=format-date --withTest --dir=src/lib js:module (pack @codejen/pack-js) – create a module exporting a function of the same name + src/lib/format-date.ts + src/lib/format-date.test.ts ✓ 2 written
Params
name
any case: format-date makes formatDate() in format-date.ts
withTest
add a test next to it
runner
node (node:test) or vitest (default: vitest in a Vite or Vitest project)
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
js:test
A test file for a module that's already there: it imports every function, class and constant the module exports and starts a test for each, with node:test or Vitest. For a module that doesn't exist yet, it imports the module as a whole, ready for test-first.
$ jen js:test --name=slugify --dir=src/lib js:test (pack @codejen/pack-js) – create a test file for a module: node:test, or Vitest in a Vite project + src/lib/slugify.test.ts ✓ 1 written
Params
name
the module to test, e.g. format-date
runner
node (node:test) or vitest (default: vitest in a Vite or Vitest project)
types
ts, jsdoc or none (default: from the nearest tsconfig.json or jsconfig.json)
Credits
The keyboard patterns follow the ARIA Authoring Practices Guide by the W3C Web Accessibility Initiative – roving tabindex and keyboard shortcuts from its keyboard interface practices, menus from the menu and menubar pattern, grids from the grid pattern. Each module credits it in its first line.
js:vite runs create-vite by the Vite team, and js:esbuild's project layout follows it; each esbuild project's index.html credits it.
The code is written fresh for this pack, not copied. The pack is ISC licensed.