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

@proedis/cli

v2.1.2

Published

Proedis command line interface to scaffold projects, generate React components and build models from an OpenAPI specification

Readme

@proedis/cli

Your API already knows what your enums and your models look like. Stop typing them twice. ⌨️

npm license


⚠️ This tool talks to a Proedis-style API. Every command reads documents carrying the x-api-enum, x-api-response-dto, x-element-name and x-element-namespace extensions emitted by the Proedis .NET generator. Pointed at a generic OpenAPI document, scaffold models will find no models to generate and scaffold hooks no operation it can name. That is a deliberate scope, not an oversight.

✨ What's in the box

One binary, three commands, all fed by the same API. 📡

| Command | Downloads | Writes | | --- | --- | --- | | proedis scaffold enums | the shared objects definition | typed enum unions, their constants, and the @proedis/modeler configuration | | proedis scaffold models | the OpenAPI document | class-transformer models, their barrel, and the endpoint namespaces | | proedis scaffold hooks | the OpenAPI document | a named hook, query key, arguments and props type per operation, grouped by resource |

They build on each other, so run them in that order: the hooks import the models, and the models carry the enums.

Everything is rendered before anything is written, so a document the scaffolder cannot handle leaves your working tree exactly as it found it.

📦 Installation

yarn add --dev @proedis/cli

Nothing else is required to run it. Two packages are worth having in the project it generates into, because the generated code imports them:

  • @proedis/modeler — the generated models, enums and configuration are built on it
  • eslint — optional, but the generated files are formatted with your project's ESLint and your rules when it can be resolved. Without it the files are still written and correct, just unformatted

🚀 Quick start

yarn proedis scaffold enums

It asks for a host and an endpoint, remembers them in .proedis.yml, and reports what happened:

✔ Downloaded Enums Definition

Found 2 enums, 3 values.

All paths will be resolved from root src:
 - Saving Constants in ./constants
 - Saving Enums in ./interfaces/enums
 - Saving Utilities in ./interfaces/shared-objects

  A src/interfaces/enums/OrderState.ts
  A src/constants/enums/OrderState.ts
  = src/constants/shared-objects.colors.ts (kept, this file is yours to edit)
  …

Scaffold complete.
  15 created, 0 updated, 0 unchanged, 0 kept
  14 files fixed by ESLint

Answer everything upfront and it never stops to ask — which is what makes it usable from a script:

yarn proedis scaffold enums --host https://api.example.com --endpoint /v1/common/shared-objects -y

📖 API

🧩 proedis scaffold <element>

| Option | What it does | | --- | --- | | --host <host> | the host serving the definition, skipping its prompt | | --endpoint <endpoint> | the endpoint serving the definition, skipping its prompt | | --spec <file> | generate from a definition on disk, skipping the download entirely | | --save-spec <file> | save the downloaded definition, so a later run can be fed from it | | --check | report what a run would change and write nothing, exiting non-zero when it would | | -y, --yes | answer every optional prompt affirmatively |

--spec and --check are what make the generated code verifiable where there is no API to ask. Commit the definition next to the code generated from it, and a pipeline can tell whether the two still agree:

# on your machine, against a running API
proedis scaffold models --host https://localhost:5001 --save-spec ./api/openapi.json

# in CI, against what the repository committed
proedis scaffold models --spec ./api/openapi.json --check

A check reports three kinds of drift — a file that is missing, one that is stale, and one left orphan inside a directory the definition no longer fills. The last is the one a naive comparison misses: a real run would have deleted it while emptying the directory, so an output that looks clean file by file is not necessarily the output a run produces.

Host and endpoint are remembered per command in a .proedis.yml at the project root, and offered as the defaults next time:

scaffold-enums:
  endpoint: /v1/common/shared-objects
  host: https://api.example.com

⚠️ They are written after a successful download, never before — a host that just failed is not one worth suggesting again. Anything else you put in a section survives a run: only the keys a command answered are rewritten.

Two of those keys decide where the generated code goes, which a single package project never needs and a monorepo always does:

scaffold-models:
  output: packages/model/entities/src
scaffold-hooks:
  output: packages/api/generated/src
  models: '@proedis/yard-model-entities'

output is the directory the command writes into, relative to the project root, and it replaces the src it would have used. For the hooks, models is the specifier their imports point at: across packages the models cannot be reached by a relative path, they have to be imported by the name of the package holding them. Leave both out and everything lands where it always did.

🎨 scaffold enums

Expects Record<string, { name: string, label: string, value: number }[]> and writes, under src/:

| Path | Holds | | --- | --- | | interfaces/enums/ | one string union per enum, plus ComposedSharedObjects | | interfaces/shared-objects/ | the SharedObject shape and the union of every enum name | | constants/enums/ | one frozen collection per enum, plus the registry | | constants/shared-objects.ts | getSharedObjects, getSharedObject, getSharedObjectLabel | | constants/shared-objects.{colors,icons}.ts | the token maps, yours to edit | | modeler.configuration.ts | the ModelerOverride declaration and the Enum.configure* calls |

The last two rows are optional and asked for separately.

The generated code names no UI kit. Colors and icons are typed through EnumsColors and EnumsIcons from @proedis/modeler, which resolve to whatever your application declares:

declare module '@proedis/modeler' {
  export interface ModelerOverride {
    enums: ComposedSharedObjects;
    color: string;
    icon: string;
  }
}

Declared as string the two tokens behave exactly as the built-in fallback, and the block exists so you can see a configuration that would otherwise be invisible. Swap color for your kit's colour type — MantineColor, say — and both shared-objects.colors.ts and every Enum.color are constrained to it immediately; the same goes for icon.

The zod helper in shared-objects.ts is emitted only when your project can resolve zod — including when it is declared by the root manifest of a monorepo and used from a workspace that never mentions it.

🏗️ scaffold models

Expects an OpenAPI document and writes, under src/:

| Path | Holds | | --- | --- | | models/scaffold/<namespace>/ | one class-transformer model per DTO, foldered by x-element-namespace | | models/scaffold/index.ts | the barrel, which installs the virtuals when there are any | | models/virtuals/index.ts | the barrel of your computed properties, when you have written any | | namespaces/index.ts | Path, PathMethods, PathRouteParams, PathQueryParams |

OpenAPI types map like this:

| Schema | Becomes | | --- | --- | | string + format: date-time \| date | @AsDayJs() DateTime | | string + format: date-span | @AsTimeSpan() TimeSpan | | string + format: uuid | string | | string + x-api-enum | Enum<'Name'> / Flags<'Name'>, or the plain union | | integer / number | number | | $ref | @Type(() => Model) |

A property whose type nothing maps stops the run and names itself, rather than writing a file that does not compile:

Cannot map property 'Job.payload': no property type handles {"type":"file","nullable":false}

🪄 Virtuals: what a generated model cannot say about itself

A generated model describes the payload, and nothing else. A display name, a derived flag, a total are none of the API's business, and putting them in a type that extends the model does not work: the hooks answer with the generated one, and so does every relation nested inside it.

defineVirtuals, from @proedis/modeler, declares them on the model instead. This command does not write those files: it finds them and wires them up. Create one yourself, named after the model it belongs to, under models/virtuals/:

// src/models/virtuals/AccountCompleteDto.ts
import { defineVirtuals } from '@proedis/modeler';

import { AccountCompleteDto } from '../scaffold/responses/accounts/AccountCompleteDto';


declare module '../scaffold/responses/accounts/AccountCompleteDto' {
  interface AccountCompleteDto {
    readonly displayName: string;
  }
}

defineVirtuals(AccountCompleteDto, {
  displayName() {
    return [ this.lastName, this.firstName ].filter(Boolean).join(' ');
  }
});

From there displayName is a property of AccountCompleteDto: every hook answering with one has it, every relation carrying one has it, and it is absent from toObject and from anything sent back. @proedis/modeler documents the mechanism; three things are this command's business.

Where the file goes, and what it is called. models/virtuals/, beside the generated folder rather than inside it, because models/scaffold/ is emptied on every run. One file per model, named exactly after it: that convention is what the next two points rely on.

Run scaffold models again after adding the first one. It then writes models/virtuals/index.ts, importing every file there, and has the barrel of the models import that, first thing:

import '../virtuals';

Installing a virtual is a side effect, so something has to import it, and this is the file everything else goes through. Left to whoever needs a computed property, the type would promise it in files where nobody had imported anything, and every instance would answer undefined. A project with no virtuals gets none of this: no folder, no import, no trace. ⚠️ If you move the models into a package declaring "sideEffects": false, exclude that path or a production bundle may drop the import.

A run refuses to generate a model that would shadow one of its own virtuals. The day the API starts sending a field of that name, the payload wins at runtime and the getter is never reached. Nothing else would have said so:

The document now describes a property declared as a virtual: RegistryMinimalDto.displayName.
The payload would shadow the getter, so the virtual would never be reached: remove it from
the virtuals file, and read the value the server sends.

Declare virtuals readonly. It is what makes that same collision a compile error too, and not only a caught one on the next scaffold: the writable field the payload brings cannot merge with a readonly declaration.

🪝 scaffold hooks

One hook per operation, named after the operation itself and grouped by the resource it acts on — the first static segment of its route — under src/:

| Path | Holds | | --- | --- | | hooks/scaffold/<resource>.ts | every operation of that resource, with the models it needs imported once | | hooks/scaffold/index.ts | the barrel |

⚠️ The tag would be the natural grouping and is not used: on a document where it is empty for most operations, everything piles into one file of fifteen thousand lines. The resource is always there, and it is how a hook gets looked up.

Which hook it becomes follows the method and the answer:

| Operation | Becomes | | --- | --- | | GET | useClientQuery<Dto>, with the model as its transformer | | GET answering with a page | usePaginatedClientQuery<Item>, taking a PaginatedRequest | | POST PUT PATCH DELETE | useClientMutation<Body, Response>, handing the payload to the request |

Every query is written in three pieces, because the hook is not the only way to spend them, plus a props type the three of them share:

export type GetSingleActivityProps = {
  id: string;
};

export function getSingleActivityQueryKey(id: string): string[];
export function getSingleActivityQueryKey(props: GetSingleActivityProps): string[];
export function getSingleActivityQueryKey(idOrProps: string | GetSingleActivityProps): string[] {
  const { id } = typeof idOrProps === 'object'
    ? idOrProps
    : { id: idOrProps } as GetSingleActivityProps;

  return [ 'activities', id ];
}

export function getSingleActivityQueryArgs(id: string): readonly [ string[], { transformer: typeof ActivityCompleteDto } ];
export function getSingleActivityQueryArgs(props: GetSingleActivityProps): readonly [ string[], { transformer: typeof ActivityCompleteDto } ];
export function getSingleActivityQueryArgs(idOrProps: string | GetSingleActivityProps) /* … */

export function useGetSingleActivity(id: string, options?: Options): ReturnType<typeof useClientQuery<ActivityCompleteDto>>;
export function useGetSingleActivity(props: GetSingleActivityProps, options?: Options): ReturnType<typeof useClientQuery<ActivityCompleteDto>>;
export function useGetSingleActivity(idOrProps: string | GetSingleActivityProps, options?: Options) {
  const { id } = typeof idOrProps === 'object'
    ? idOrProps
    : { id: idOrProps } as GetSingleActivityProps;

  return useClientQuery<ActivityCompleteDto>(...getSingleActivityQueryArgs(id), options);
}

The props type is what makes the operation nameable from outside. A component that renders one activity declares its own props with it, and hands them over as they are — no destructuring to call the hook, and nothing to keep in sync when the operation gains a parameter:

export interface ActivityCardProps extends GetSingleActivityProps {
  readonly compact?: boolean;
}

export function useActivityCard(props: ActivityCardProps) {
  return useGetSingleActivity(props);
}

Both shapes are accepted by all three functions: one argument each, or the props object. Route parameters are required in either — a key missing one of them is not the key of anything, and making them optional only makes that mistake callable.

The key is the route split on slashes with its parameters in place, so nothing rebuilds the url at runtime. Invalidating one entry is calling it; invalidating the whole resource is a separate function, one per file, because it is a different request and should be spelled out rather than be the result of a forgotten argument — @proedis/react-query treats a key as a prefix filter:

const invalidateEveryActivity = useQueryInvalidation([ activitiesQueryKey() ]);
const invalidateThisActivity = useQueryInvalidation([ getSingleActivityQueryKey(id) ]);

The arguments are the key and the request config, ready to spread. Anything built on useClientQuery takes the same pair and decides the options itself, instead of repeating the key and the transformer of an endpoint it does not own:

export function useActivityWhileVisible(id: string, isVisible: boolean) {
  return useClientQuery(...getSingleActivityQueryArgs(id), { enabled: isVisible, staleTime: 30_000 });
}

Every signature declares its return type, derived from the function being called — an overload without one is silently any, which would throw away the typing this whole chain exists for, and no internal type is imported to spell it out. A page is queried through usePaginatedClientQuery, whose transformer describes the item: the envelope stays generic, which is why no class is generated per page shape.

🏷️ Where the names come from

The name is the one the API gives the operation, in x-element-name. Two cases are handled rather than assumed:

  • Names that collide. The same handler can serve several routes — a list and the same list projected onto another DTO — and the document names the operation, not the route. Those are told apart by the path segments they do not share, or by their route parameters when even those match: useGetAccountAssignedEstates and useGetAccountAssignedEstatesById. Never by the order the document happens to list them in.
  • Names that cannot be identifiers. A handler written as an inline lambda is named after the class the compiler generated for it, and an endpoint the framework never named carries a fully qualified method signature. Those operations are skipped: no hook, which is visible, rather than a file that does not parse.

⚠️ The generated hooks import from @proedis/react-client, so a project scaffolding them needs it installed.

♻️ These files are a clone, not a draft

Every command empties and rewrites its output directories on every run, announcing which ones before doing it. That is the point: they mirror a truth that lives in your API, so anything the server stops returning has to disappear here too.

Two files are the exception — shared-objects.colors.ts and shared-objects.icons.ts — plus modeler.configuration.ts. They are yours, they are reported as kept, and they are never overwritten. The virtuals under models/virtuals/ are never touched either: they are read, and only their barrel is generated. ⚠️ Which also means an existing project will not pick up an upstream change to those templates: delete them to have them regenerated.

🧹 Formatting

Generated files are fixed with the ESLint installed in your project, using your rules, run from the directory holding your configuration. That last detail matters in a monorepo: a relative parserOptions.project resolves against the working directory, so running from a workspace while the config lives at the root used to fail on every file.

If ESLint cannot run, the scaffold still succeeds and says so — the files are correct, only unformatted.

🔀 Migrating to 1.x

First public release. init and generate existed in the source but were never published, and are gone: scaffold is the whole surface.

🤝 Compatibility

| Requirement | Range | | --- | --- | | Runtime | Node >=22.13.0 | | Generated code needs | @proedis/modeler, plus @proedis/react-client for the hooks | | Keys pair well with | @proedis/react-query, whose invalidation takes them as prefix filters | | Formatting needs | any resolvable eslint, 8 or 9, flat config or eslintrc | | typescript | >=5.2.0 |

📄 License

MIT © Proedis S.r.l.