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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ tskm is two things that fit together:

- **An AOT (ahead-of-time) type compiler** — instead of deriving a type from a schema with `z.infer<typeof schema>` at every use site, tskm pre-computes it once and writes the fully-expanded, concrete type to a real file. The answer comes from the actual TypeScript type checker, queried out of process.

It is a bun-workspaces monorepo: `tskm` (runtime), `@tskm/compiler` (AOT codegen + CLI), `@tskm/vite` (Vite plugin).
It is a bun-workspaces monorepo: `@tskm/core` (runtime), `@tskm/compiler` (AOT codegen + CLI), `@tskm/vite` (Vite plugin).

## Why tskm?

Expand All @@ -35,7 +35,7 @@ tskm goes the **opposite direction — schema → type, not type → validator
**Validate at runtime** — schemas are values; `parse` / `safeParse` are standalone functions:

```ts
import { object, string, number, array, pipe, minLength, parse, safeParse } from "tskm"
import { object, string, number, array, pipe, minLength, parse, safeParse } from "@tskm/core"

const userSchema = object({
name: pipe(string(), minLength(2)),
Expand All @@ -54,7 +54,7 @@ const result = safeParse(userSchema, { name: "", age: 1, tags: [] })

```ts
// user.schema.ts (you write — note: no `type User = Infer<...>` needed)
import { object, string, number, array, pipe, minLength } from "tskm"
import { object, string, number, array, pipe, minLength } from "@tskm/core"

export const userSchema = object({
name: pipe(string(), minLength(2)),
Expand Down Expand Up @@ -102,12 +102,12 @@ it runs the compiler on `buildStart` and watches during `vite dev`.

The compiler never reads your schema at runtime — the inferred type only exists in the type system, so it asks the type checker directly:

1. **Discover** — [`oxc-parser`](https://oxc.rs) scans each source file (syntactically) for exported `const`s whose factory is imported from `tskm`, and for explicit `type T = Infer<typeof X>` markers.
1. **Discover** — [`oxc-parser`](https://oxc.rs) scans each source file (syntactically) for exported `const`s whose factory is imported from `@tskm/core`, and for explicit `type T = Infer<typeof X>` markers.
2. **Query** — for each schema, the compiler writes a tiny sibling file (`<base>.tskm-query.ts`, deleted afterward) next to the source that declares a marker against the schema's output type:

```ts
import { userSchema } from "./user.schema"
import type { InferOutput } from "tskm"
import type { InferOutput } from "@tskm/core"
type __P<T> = { [K in keyof T]: T[K] } & {}
declare const __tskm_0: __P<InferOutput<typeof userSchema>>
```
Expand Down
12 changes: 6 additions & 6 deletions bun.lock

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

2 changes: 1 addition & 1 deletion examples/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"private": true,
"type": "module",
"dependencies": {
"tskm": "workspace:*"
"@tskm/core": "workspace:*"
},
"devDependencies": {
"@tskm/compiler": "workspace:*"
Expand Down
2 changes: 1 addition & 1 deletion examples/basic/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { safeParse } from "tskm"
import { safeParse } from "@tskm/core"
import type { Product, User } from "./user.schema.gen.ts"
import { productSchema, userSchema } from "./user.schema.ts"

Expand Down
2 changes: 1 addition & 1 deletion examples/basic/src/user.schema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { minLength, number, object, pipe, string } from "tskm"
import { minLength, number, object, pipe, string } from "@tskm/core"

export const userSchema = object({
name: pipe(string(), minLength(2)),
Expand Down
2 changes: 1 addition & 1 deletion examples/basic/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"skipLibCheck": true,
"noEmit": true,
"paths": {
"tskm": ["../../packages/tskm/src/index.ts"]
"@tskm/core": ["../../packages/tskm/src/index.ts"]
}
},
"include": ["src"]
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tskm/compiler",
"version": "0.0.1",
"version": "0.0.2",
"description": "AOT schema-to-type compiler for tskm, powered by the tsgo (Corsa) checker API",
"type": "module",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler/src/discovery.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { parseSync } from "oxc-parser"

/** The runtime package whose exports mark a value as a tskm schema. */
const RUNTIME_MODULE = "tskm"
const RUNTIME_MODULE = "@tskm/core"

/** Type-level aliases that mark `export type T = ...<typeof schema>` AOT targets. */
const INFER_ALIASES = new Set(["Infer", "InferOutput"])
Expand Down Expand Up @@ -73,7 +73,7 @@ function calleeName(init: OxcNode | undefined): string | undefined {

/**
* Reads an `export type T = Infer<typeof X>` (or `InferOutput<...>`, or
* `import("tskm").InferOutput<typeof X>`) alias and returns the referenced
* `import("@tskm/core").InferOutput<typeof X>`) alias and returns the referenced
* schema name plus the declared alias name.
*/
function readInferAlias(decl: OxcNode | undefined): DiscoveredSchema | undefined {
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/inplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ const START_LINE = /^\/\/ @tskm-gen (\w+) from (\w+) #([0-9a-f]{8})$/
const END_LINE = /^\/\/ @tskm-end (\w+)$/

// First-run marker: only the SINGLE-LINE form is supported. Accepts the bare
// `Infer`/`InferOutput` reference and the `import("tskm").Infer*<...>` form — matching
// `Infer`/`InferOutput` reference and the `import("@tskm/core").Infer*<...>` form — matching
// discovery's alias set. (`InferInput` is intentionally excluded: the resolver always
// queries `InferOutput`, so an input marker would be silently filled with the output type.)
const INFER_MARKER =
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/src/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function buildQueryBody(
if (names.length > 0) {
lines.push(`import { ${names.join(", ")} } from "${sourceImportPath}"`)
}
lines.push(`import type { InferOutput } from "tskm"`)
lines.push(`import type { InferOutput } from "@tskm/core"`)
lines.push(`type __P<T> = { [K in keyof T]: T[K] } & {}`)
schemas.forEach((schema, i) => {
lines.push(`declare const ${markers[i]}: __P<InferOutput<typeof ${schema.name}>>`)
Expand Down
32 changes: 16 additions & 16 deletions packages/compiler/test/discovery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe("deriveTypeName", () => {
describe("discoverSchemas — const (alias-origin) factory declarations", () => {
it("discovers an exported const built from a tskm runtime import", () => {
const src = `
import { object, string } from "tskm"
import { object, string } from "@tskm/core"
export const userSchema = object({ name: string() })
`
const { schemas, diagnostics } = discoverSchemas("a.ts", src)
Expand All @@ -49,7 +49,7 @@ describe("discoverSchemas — const (alias-origin) factory declarations", () =>

it("ignores consts whose callee is not a tskm runtime import", () => {
const src = `
import { object } from "tskm"
import { object } from "@tskm/core"
import { other } from "elsewhere"
export const x = other({ a: 1 })
`
Expand All @@ -59,7 +59,7 @@ describe("discoverSchemas — const (alias-origin) factory declarations", () =>

it("ignores a const whose init is not a call expression", () => {
const src = `
import { object } from "tskm"
import { object } from "@tskm/core"
export const notACall = 42
`
const { schemas } = discoverSchemas("a.ts", src)
Expand All @@ -68,7 +68,7 @@ describe("discoverSchemas — const (alias-origin) factory declarations", () =>

it("ignores a call whose callee is not a plain identifier (member expression)", () => {
const src = `
import { v } from "tskm"
import { v } from "@tskm/core"
export const x = v.object({ a: 1 })
`
const { schemas } = discoverSchemas("a.ts", src)
Expand All @@ -77,7 +77,7 @@ describe("discoverSchemas — const (alias-origin) factory declarations", () =>

it("ignores non-exported consts", () => {
const src = `
import { string } from "tskm"
import { string } from "@tskm/core"
const internalSchema = string()
`
const { schemas } = discoverSchemas("a.ts", src)
Expand All @@ -86,7 +86,7 @@ describe("discoverSchemas — const (alias-origin) factory declarations", () =>

it("honors runtime-import local aliasing (import { object as o })", () => {
const src = `
import { object as o, string } from "tskm"
import { object as o, string } from "@tskm/core"
export const petSchema = o({ name: string() })
`
const { schemas } = discoverSchemas("a.ts", src)
Expand All @@ -110,8 +110,8 @@ describe("discoverSchemas — const (alias-origin) factory declarations", () =>
describe("discoverSchemas — Infer alias markers", () => {
it("discovers an export type T = Infer<typeof X> marker", () => {
const src = `
import { string } from "tskm"
import type { Infer } from "tskm"
import { string } from "@tskm/core"
import type { Infer } from "@tskm/core"
const xSchema = string()
export type X = Infer<typeof xSchema>
`
Expand All @@ -129,19 +129,19 @@ describe("discoverSchemas — Infer alias markers", () => {
expect(schemas).toEqual([{ name: "ySchema", typeName: "Y", origin: "alias" }])
})

it('discovers the import("tskm").InferOutput<...> qualified form', () => {
it('discovers the import("@tskm/core").InferOutput<...> qualified form', () => {
const src = `
const zSchema = {}
export type Z = import("tskm").InferOutput<typeof zSchema>
export type Z = import("@tskm/core").InferOutput<typeof zSchema>
`
const { schemas } = discoverSchemas("a.ts", src)
expect(schemas).toEqual([{ name: "zSchema", typeName: "Z", origin: "alias" }])
})

it('discovers the import("tskm").Infer<...> qualified form', () => {
it('discovers the import("@tskm/core").Infer<...> qualified form', () => {
const src = `
const wSchema = {}
export type W = import("tskm").Infer<typeof wSchema>
export type W = import("@tskm/core").Infer<typeof wSchema>
`
const { schemas } = discoverSchemas("a.ts", src)
expect(schemas).toEqual([{ name: "wSchema", typeName: "W", origin: "alias" }])
Expand All @@ -159,7 +159,7 @@ describe("discoverSchemas — Infer alias markers", () => {
it("ignores an import-type whose qualifier is not an Infer alias", () => {
const src = `
const qSchema = {}
export type Q = import("tskm").SomethingElse<typeof qSchema>
export type Q = import("@tskm/core").SomethingElse<typeof qSchema>
`
const { schemas } = discoverSchemas("a.ts", src)
expect(schemas).toHaveLength(0)
Expand Down Expand Up @@ -202,8 +202,8 @@ describe("discoverSchemas — Infer alias markers", () => {
describe("discoverSchemas — multiple schemas & dedup", () => {
it("collects const and alias origins together from one file", () => {
const src = `
import { object, string, number } from "tskm"
import type { Infer } from "tskm"
import { object, string, number } from "@tskm/core"
import type { Infer } from "@tskm/core"
export const userSchema = object({ name: string() })
export const ageSchema = number()
const hiddenSchema = string()
Expand All @@ -219,7 +219,7 @@ describe("discoverSchemas — multiple schemas & dedup", () => {

it("does not re-add a const name already seen", () => {
const src = `
import { string } from "tskm"
import { string } from "@tskm/core"
export const dupSchema = string(), other = string()
`
const { schemas } = discoverSchemas("a.ts", src)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { array, number, object, optional, pipe, string, transform } from "tskm"
import { array, number, object, optional, pipe, string, transform } from "@tskm/core"

export const accountSchema = object({
id: string(),
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/test/fixtures/basic/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"noEmit": true,
"baseUrl": ".",
"paths": {
"tskm": ["../../../../tskm/src/index.ts"]
"@tskm/core": ["../../../../tskm/src/index.ts"]
}
},
"include": ["src"]
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/test/inplace.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const query = fileURLToPath(
new URL("./fixtures/basic/src/widget.schema.tskm-query.ts", import.meta.url),
)

const SOURCE = `import { object, string, number, type Infer } from "tskm"
const SOURCE = `import { object, string, number, type Infer } from "@tskm/core"

export const widgetSchema = object({
id: string(),
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler/test/inplace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ describe("emitInplace — first run (Infer marker conversion)", () => {
expect(readFileSync(file, "utf8")).toBe(result.content)
})

it('accepts the import("tskm").InferOutput<...> marker form', () => {
const source = `export type User = import("tskm").InferOutput<typeof userSchema>\n`
it('accepts the import("@tskm/core").InferOutput<...> marker form', () => {
const source = `export type User = import("@tskm/core").InferOutput<typeof userSchema>\n`
const file = tmpFile("b.ts", source)
const result = emitInplace(file, source, [{ typeName: "User", typeString: "{ id: string }" }], {
version: VERSION,
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler/test/jsonschema.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe.skipIf(!bun)("generateJsonSchema — isolated subprocess (real import)"
// file, so module output cannot break parsing.
writeFileSync(
noisySource,
`import { object, string } from "tskm"\nconsole.log("side effect on stdout")\nexport const noisySchema = object({ a: string() })\n`,
`import { object, string } from "@tskm/core"\nconsole.log("side effect on stdout")\nexport const noisySchema = object({ a: string() })\n`,
)
const result = await generateJsonSchema({
root: fixtureRoot,
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler/test/watch.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const query = fileURLToPath(
)

function inplaceSource(sizeField: string): string {
return `import { object, string, number, type Infer } from "tskm"
return `import { object, string, number, type Infer } from "@tskm/core"

export const watchSchema = object({
id: string(),
Expand All @@ -28,7 +28,7 @@ export type WatchTarget = Infer<typeof watchSchema>
}

function sidecarSource(extraField: string): string {
return `import { object, string, number } from "tskm"
return `import { object, string, number } from "@tskm/core"

export const watchSchema = object({
id: string(),
Expand Down
2 changes: 1 addition & 1 deletion packages/tskm/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "tskm",
"name": "@tskm/core",
"version": "0.0.1",
"description": "Standard Schema compliant, functional validation library with AOT type compilation",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion scripts/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type Runner = (cmd: string, args: string[], opts?: { cwd?: string }) => C
export type PkgInfo = { name: string; version: string; dir: string }

// Dependency order: vite depends on compiler, so compiler must reach the registry first.
export const PUBLISH_ORDER = ["@tskm/compiler", "tskm", "@tskm/vite"] as const
export const PUBLISH_ORDER = ["@tskm/compiler", "@tskm/core", "@tskm/vite"] as const

const DEP_FIELDS = [
"dependencies",
Expand Down
4 changes: 2 additions & 2 deletions scripts/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function packPackage(pkg: PkgInfo, versionMap: Map<string, string>, destDir: str
const CHECKS: Record<string, string> = {
// tskm: zero-dependency library — a known named export must be callable.
"check-tskm.mjs":
"import * as m from 'tskm'; if (typeof m.email !== 'function') { throw new Error('tskm: expected named export `email` to be a function'); } console.log('tskm import OK');",
"import * as m from '@tskm/core'; if (typeof m.email !== 'function') { throw new Error('tskm: expected named export `email` to be a function'); } console.log('tskm import OK');",
// @tskm/compiler: a known named export must be present after install from the tarball.
"check-compiler.mjs":
"import * as m from '@tskm/compiler'; if (typeof m.defineConfig !== 'function') { throw new Error('@tskm/compiler: expected `defineConfig` export'); } console.log('@tskm/compiler import OK');",
Expand Down Expand Up @@ -89,7 +89,7 @@ function main(): void {
private: true,
type: "module",
dependencies: {
tskm: `file:${tarballs.get("tskm")}`,
"@tskm/core": `file:${tarballs.get("@tskm/core")}`,
"@tskm/compiler": `file:${tarballs.get("@tskm/compiler")}`,
"@tskm/vite": `file:${tarballs.get("@tskm/vite")}`,
"@typescript/native-preview": nativePreview,
Expand Down
Loading