skmtcdocs

Projects and workspaces

The .skmtc/<project>/ directory layout — what files exist, which are derived, which are user-edited. A SKMTC project is a generator configuration, not a consuming application: it specifies which…

A project is not the consuming app — it's the generator configuration the consuming app pulls code from. One repository can contain multiple SKMTC projects, each generating into a different part of the consuming app (or into different apps).

The one-paragraph essence

A SKMTC root is a directory containing a .skmtc/ folder. Inside .skmtc/ is one directory per project. Each project directory holds the project's deno.json (which generators are installed), its .settings/client.json (which schema, which output path, which enrichments), its worker.ts and bundle.js (derived from deno.json, the things the Worker actually loads), and optionally local generator source directories for any cloned generators. Generated output lands wherever client.json#settings.basePath points.

The SKMTC root

The SKMTC root is the nearest ancestor directory containing a .skmtc/ folder. The CLI discovers it by walking up from the current working directory.

my-app/                          ← SKMTC root (nearest .skmtc/ ancestor)
├── .skmtc/                      ← root marker
│   ├── my-api/                  ← project "my-api"
│   ├── my-graphql/              ← project "my-graphql"
│   └── ...
├── src/                         ← consuming app source
└── package.json                 ← consuming app

Created by skmtc init. After init, all skmtc commands run from anywhere inside the root will find it.

Project directory layout

.skmtc/<project>/
├── deno.json                    ← user-edited (or via CLI)
├── deno.lock                    ← auto-managed by Deno
├── worker.ts                    ← derived from deno.json
├── bundle.js                    ← derived from worker.ts (present for every project)
├── .settings/
│   ├── client.json              ← user-edited (schema, basePath, enrichments)
│   ├── manifest.json            ← derived; overwritten every generate run
│   ├── logs.txt                 ← derived; bundle/generate stdout
│   └── error-logs.txt           ← derived; bundle/generate stderr
└── <gen-name>/                  ← only if cloned generators exist
    ├── deno.json
    ├── mod.ts
    └── src/...

Each file's role:

deno.json

The project's dependency manifest. JSR imports of installed generators plus core peer dependencies:

{
  "imports": {
    "@skmtc/core": "jsr:@skmtc/core@^0.3.7",
    "@skmtc/worker": "jsr:@skmtc/worker@^0.2.0",
    "@skmtc/gen-zod": "jsr:@skmtc/gen-zod@^0.0.55",
    "@skmtc/gen-typescript": "jsr:@skmtc/gen-typescript@^0.0.55"
  }
}

User-edited via skmtc install, skmtc clone, or skmtc remove. Hand-editing is supported but the CLI commands also do validation (peer-pin checks) that hand-editing skips.

deno.lock

Auto-managed by Deno. Pins exact versions and content hashes. Ensures reproducible installs across machines.

worker.ts

A derived file — the CLI templates it from deno.json#imports:

import toWorker from '@skmtc/worker'
import gen1 from '@skmtc/gen-zod'
import gen2 from '@skmtc/gen-typescript'
// ... one import per generator

export default toWorker(() =>
  Object.fromEntries([gen1, gen2].map(g => [g.id, g]))
)

Regenerated by skmtc bundle and skmtc dev. Never hand-edited. Hand-edits are lost on next bundle.

bundle.js

The compiled output of deno bundle worker.ts -o bundle.js. This is what the SKMTC Worker actually loads at generate time.

Present for every project, including remote-only ones (all generators installed from JSR). skmtc generate loads the project-local bundle.js regardless of where the generator source comes from, so a matching worker.ts/bundle.js must exist.

Bundle freshness — i.e., whether bundle.js matches the current deno.json — is checked by skmtc doctor. The project-bundle/ check surfaces stale bundles.

.settings/client.json

The user-facing configuration. Schema source, output path, generator filters, enrichments:

{
  "source": "./openapi.json",
  "settings": {
    "basePath": "src/generated",
    "skip": [],
    "include": [],
    "enrichments": { /* per-generator */ }
  }
}

User-edited. The source, basePath, and enrichments are the three knobs most users adjust.

.settings/manifest.json

A derived file — overwritten every skmtc generate run. Contains the canonical record of the last run: parse issues, per-(generator, operation) results, file metadata, timing.

See error handling philosophy for the structure and manifest-format.md reference for the full schema.

.settings/logs.txt and .settings/error-logs.txt

stdout and stderr captures from skmtc bundle runs. Useful for debugging bundle failures.

<gen-name>/

Present only for cloned generators. Contains the generator's source code — deno.json, mod.ts, src/. The directory is what skmtc clone writes to.

After cloning, the deno.json#imports entry points to this local directory (e.g., "@skmtc/gen-zod": "./gen-zod/mod.ts") instead of a JSR specifier.

basePath alignment with the consuming app

The basePath field in client.json is load-bearing: it controls both where on disk generated files land AND the @ alias in the consuming app's bundler resolver.

Example: a Vite app with vite.config.ts setting @./src would configure basePath: "src/generated" (relative to the SKMTC root). Generators produce paths like @/forms/CreateUserForm.generated.tsx, which:

  • On disk lands at <root>/src/generated/forms/CreateUserForm.generated.tsx
  • In the consuming app's TS imports resolves to <root>/src/forms/... via the bundler

These two paths must match. If they diverge, generated imports break at runtime.

For Vite/Next/Webpack/Rollup, the @ alias is typically configured in their respective config files. SKMTC doesn't manage the bundler config — you align them manually.

Global state at ~/.skmtc/

Not all project state is per-project. SKMTC also maintains a global state directory:

~/.skmtc/
├── auth-sb-api-auth-token.txt   ← Supabase auth for the sandbox API
├── <project-name>/              ← shadow project state (per-project)
│   ├── schema.json              ← cached schema
│   └── manifest.json            ← shadow manifest
└── ...

The global state is mostly used by the hosted sandbox API path. For local-only setups, you can mostly ignore it. When debugging "why isn't this working," check both <root>/.skmtc/<project>/ and ~/.skmtc/<project>/.

Multiple projects in one repo

A repo can have many projects:

my-app/
├── .skmtc/
│   ├── api-v1/         ← old API generator config
│   ├── api-v2/         ← new API generator config
│   ├── graphql/        ← GraphQL operations
│   └── mocks/          ← MSW handlers
├── src/
│   ├── generated/      ← api-v2 output
│   ├── generated-mocks/← mocks output (different basePath)
│   └── ...

Each project has its own deno.json, client.json, generators, and output location. The CLI subcommands take the project name as an argument:

skmtc generate api-v2
skmtc generate mocks

This is the right pattern when you want different generator sets, different schemas, or different output locations. There's no performance benefit to combining multiple "logical" projects into one — generators run independently per project anyway.

Initialization

skmtc init <projectName> <basePath>:

  1. Creates .skmtc/<projectName>/
  2. Writes deno.json with the core peer-dep pin and an empty imports list
  3. Writes .settings/client.json with basePath and no source
  4. Returns { type: 'created', projectName, basePath }

If the project already exists, init returns { type: 'existed' } and exits cleanly — it's idempotent.

The basePath argument:

  • Must be relative to the SKMTC root. Absolute paths are rejected with exit code 2 and a recipe error.
  • Must match the consumer app's bundler alias. The CLI doesn't verify this — the user does — but skmtc doctor's project-base-path/<project> check surfaces obvious mistakes (missing, absolute, etc.).

Common questions

Can multiple projects share generators?

The JSR imports in each project's deno.json are independent. If both projects install @skmtc/gen-zod, each pins its own version. Deno's content-addressed cache means the package only downloads once per version, so the disk cost is low even with duplication.

If you clone a generator: the clone is project-local. It exists in one project's directory only. To share a clone, you'd need to publish your customized version to JSR and install it in both projects.

How do I add or remove generators?

Use skmtc install <generator-id> <project> and skmtc remove <project> <generator-id>. Hand-editing deno.json works but skips the CLI's pre-flight checks (peer-pin compatibility, auto-rebundle). Recommended: use the CLI for state changes; only hand-edit for advanced pinning needs.

What happens to a project's bundle.js when I install a new generator?

skmtc install triggers a post-install rebundle unconditionally — regardless of whether the project has any clones. The newly installed generator is bundled into bundle.js so generate picks it up. (The same holds for a project with only installed generators: it still has a local bundle.js, and install refreshes it.)

Where do tests for cloned generators live?

There's no convention. Some teams put them adjacent to the cloned source (.skmtc/<project>/<gen-name>/src/*.test.ts); some put them in the consuming app's test directory; some skip them. Cloned generators are project source, so the test location is the user's call.

Can I version-control .skmtc/<project>/?

Yes — and you should. The directory contains:

  • User-edited config (deno.json, client.json) — definitely commit
  • Derived files (worker.ts, bundle.js, manifest.json, logs.txt) — gitignore is reasonable
  • Cloned generator source (<gen-name>/) — definitely commit (it's your code now)

A reasonable .gitignore:

.skmtc/**/bundle.js
.skmtc/**/worker.ts
.skmtc/**/.settings/manifest.json
.skmtc/**/.settings/*.txt

Can SKMTC live outside a Node/pnpm project?

Yes. SKMTC requires Deno; the consuming app can be anything (Vite, Next, Bun, even a non-JS project that doesn't import the generated code). The CLI doesn't read package.json.

What if I have multiple SKMTC roots in nested directories?

The CLI discovers the nearest ancestor .skmtc/. If you have nested roots, only the inner one is found. To work with the outer root, run the CLI from a directory outside the inner root.

In practice, one SKMTC root per repository is the convention.

Further reading

On this page