--- URL: "atscript.dev/" LLMS_URL: "atscript.dev/index.md" layout: "home" hero2: kicker: "One model. Types · DB · UI." text: "Define your data once." tagline: "Generate TypeScript types, runtime validation, DB schema, REST routes, and a full UI — forms, tables and multi-step flows — from a single `.as` model." actions: - theme: "brand" text: "Start with TypeScript" link: "/packages/typescript/quick-start" - theme: "alt" text: "Explore Database" link: "https://db.atscript.dev/guide/quick-start" - theme: "alt" text: "Explore UI" link: "https://ui.atscript.dev/" - theme: "alt" text: "Explore Auth + RBAC" link: "https://aooth.moost.org" --- --- URL: "atscript.dev/_fragments/ad-hoc-annotations" LLMS_URL: "atscript.dev/_fragments/ad-hoc-annotations.md" --- Ad-hoc annotations let you attach metadata to an existing interface or type without modifying its original definition. This works with both `interface` and `type` definitions, including primitive-based types and union types. This is useful when the original type is defined in another file, shared across modules, or when you need context-specific metadata variations. ## Syntax There are two forms: **mutating** and **non-mutating**. ### Mutating Injects annotations directly into the target definition at runtime: ```atscript import { User } from './user' annotate User { @meta.label 'Full Name' name @meta.label 'Email Address' email } ``` The `User` type's metadata is modified in-place when this module is loaded. No new type is created. ### Non-mutating (Alias) Creates a new named type that inherits the target's structure with overridden annotations: ```atscript import { User } from './user' export annotate User as UserForm { @meta.label 'Full Name' name @meta.label 'Email Address' email } ``` `UserForm` is a standalone type with its own class, type definition, and metadata. `User` remains unchanged. ## Entry Syntax Each entry inside the annotate block references a property of the target type by name. Annotations placed before the entry are applied to that property. ```atscript annotate User { @meta.label 'Name' // annotation for the property name // property reference @ui.placeholder 'you@example.com' @expect.maxLength 100 email // multiple annotations on one property } ``` ### Deep Property Chains For nested object properties, use dot notation: ```atscript annotate User { @meta.label 'Street Address' address.street @meta.label 'City' address.city } ``` This navigates into the nested `address` structure and annotates its `city` and `street` properties. ## Top-level Annotations Annotations placed before the `annotate` keyword apply to the type itself (not to individual properties): ```atscript @meta.description 'User registration form' annotate User as RegistrationForm { @meta.label 'Username' name } ``` For mutating annotate, top-level annotations modify the target's own metadata: ```atscript @meta.description 'Admin user' annotate User { @meta.label 'Admin Name' name } ``` This sets `User.metadata.get("meta.description")` to `"Admin user"` at runtime. For non-mutating annotate, top-level annotations on the alias replace the original's. Annotations from the original that are not overridden are carried over to the alias. ## Annotation Merging When ad-hoc annotations target properties that already have annotations, the merge strategy determines how values combine. ### Replace Strategy (default) For annotations with the default replace strategy, the ad-hoc annotation **replaces** the original: ```atscript export interface User { @meta.label 'Original Name' name: string } annotate User { @meta.label 'Admin Name' // Replaces 'Original Name' name } // Result: name's label is 'Admin Name' ``` For repeatable annotations (`multiple: true`) with replace strategy, the **entire set** is replaced: ```atscript export interface Config { @tag 'alpha' @tag 'beta' feature: string } annotate Config { @tag 'production' // Replaces both 'alpha' and 'beta' feature } // Result: feature's tags are ['production'], not ['alpha', 'beta', 'production'] ``` ::: info `@tag` is a user-registered annotation `@tag` is not built in — it stands in for any custom repeatable annotation you register in `atscript.config`. See [Custom Annotations](/packages/typescript/custom-annotations) for how to define one. ::: ### Append Strategy For annotations configured with `mergeStrategy: 'append'`, ad-hoc values are **added** to the existing ones: ```atscript export interface User { @expect.pattern '^[A-Z]', '', 'Must start uppercase' name: string } annotate User { @expect.pattern '.{3,}', '', 'Min 3 chars' // Added, not replaced name } // Result: both patterns are validated ``` ### Non-mutating Aliases Non-mutating annotate creates a new type with annotations merged from the original. The original is not affected: ```atscript @meta.description 'Base user' export interface User { @meta.label 'Name' @tag 'core' name: string } export annotate User as AdminUser { @meta.label 'Admin Name' // Replaces 'Name' @tag 'admin' // Appends to ['core'] → ['admin', 'core'] name } // User is unchanged. AdminUser has merged annotations. ``` ## Annotating Types The examples above use `interface` targets, but ad-hoc annotations work equally well with `type` definitions. ### Primitive-based Types For types based on primitives or primitive unions, use top-level annotations with an empty block: ```atscript export type Username = string | number // Mutating: adds a label to Username itself @meta.label 'User Name' annotate Username {} // Non-mutating: creates a labeled alias @meta.label 'Form Name' export annotate Username as FormName {} ``` Since primitive types have no properties, the block body is empty — only top-level annotations apply. ### Union Types with Properties For types that are unions of object structures, you can annotate properties by name. The compiler resolves each property across all union branches: ```atscript type Response = { status: string data: string } | { status: string error: string } annotate Response { @meta.label 'Status' status } ``` When a property like `status` appears in multiple union branches, the annotation is applied to every matching branch at runtime. Properties that don't exist in any branch are reported as errors: ```atscript annotate Response { @meta.label 'Color' color // Error: unknown property "color" in "Response" } ``` ## Cross-file Usage Ad-hoc annotations work with imported types. Import the target type, then annotate it: ```atscript import { User } from './user' // Mutating: modifies the imported User annotate User { @meta.label 'Name' name } // Non-mutating: creates a local alias export annotate User as AdminUser { @meta.label 'Admin Name' name } ``` ## Export Rules Non-mutating annotate can be exported: ```atscript export annotate User as UserForm { ... } ``` Mutating annotate **cannot** be exported (it modifies an existing type, not defines a new one): ```atscript // Error: Cannot export mutating ad-hoc annotations block export annotate User { ... } ``` ## Tree-shaking When using a bundler (Vite, Rollup, etc.) with `unplugin-atscript`, tree-shaking works automatically: - **Mutating annotate** produces side effects (runtime metadata mutations), so the module is included when the target type is used - **Non-mutating annotate** produces a standalone class. If `UserForm` is never imported by consuming code, the bundler removes it The unplugin sets `moduleSideEffects: false` to enable this behavior. ## Generated Output ### Non-mutating A non-mutating annotate generates a standalone class identical in structure to the target, with annotations merged: ```atscript // user-form.as import { User } from './user' export annotate User as UserForm { @meta.label 'Form Name' name } ``` The generated JS creates `UserForm` as its own class with the full type definition inlined, applying `"Form Name"` as the label for `name` while preserving all other annotations from `User`. ### Mutating A mutating annotate generates runtime mutation code that modifies the target's metadata directly: ```atscript // admin.as import { User } from './user' annotate User { @meta.label 'Admin Name' name @meta.label 'Admin City' address.city } ``` The generated code imports `User` and mutates metadata on its `name` and `address.city` properties at runtime, applying the annotation merge strategy (replace or append) for each annotation. --- URL: "atscript.dev/_fragments/annotations" LLMS_URL: "atscript.dev/_fragments/annotations.md" --- Annotations are metadata declarations that provide additional information about types, interfaces, and properties. ## Purpose Annotations serve multiple purposes: - **UI metadata** - Labels, placeholders, descriptions for schema-driven UI tools - **Validation constraints** - Min/max values, patterns, length restrictions - **Database metadata** - Collection names, indexes, field strategies - **Documentation** - Descriptions and multi-line documentation - **Custom metadata** - Any domain-specific information ## Where to Apply Annotations can be applied anywhere: ```atscript @meta.description 'User entity' // Interface annotation export interface User { @meta.id // Property annotation id: string } @expect.minLength 3 // Type annotation export type Username = string ``` ## Annotation Inheritance ### Type to Property When a property uses a type with annotations, annotations merge with property having priority: ```atscript @expect.minLength 3 @expect.maxLength 20 export type Username = string export interface User { @expect.maxLength 15 // Overrides type's maxLength username: Username // Inherits minLength: 3, gets maxLength: 15 } ``` ### Property References When a property references another property, annotations merge in order: 1. Final type annotations 2. Referenced property annotations 3. Current property annotations (highest priority) ### Merge Strategies When annotations are merged (from type inheritance, property references, or ad-hoc annotations), the **merge strategy** determines how same-named annotations combine: **Replace** (default) — Higher-priority annotations replace lower-priority ones entirely: ```atscript @expect.min 3 export type PositiveInt = number export interface Config { @expect.min 10 // Replaces type's @expect.min 3 threshold: PositiveInt // Result: @expect.min is 10 } ``` **Append** — Both values are kept, accumulating into an array: ```atscript @expect.pattern '^[a-z]+$' export type SafeString = string export interface Form { @expect.pattern '^\S+$', 'i', 'No spaces' code: SafeString // Result: both patterns are validated } ``` The strategy is configured per annotation via `AnnotationSpec`. Most annotations use replace. The built-in `@expect.pattern` uses append. ### Repeatable Annotations Annotations marked with `multiple: true` can appear more than once on the same node. Their values are stored as arrays: ```atscript export interface User { @expect.pattern '^[A-Z]' @expect.pattern '.{3,}' name: string } ``` When merged, how repeated annotations combine depends on the merge strategy: - **`multiple: true` + replace** (default) — The higher-priority set replaces the entire array - **`multiple: true` + append** — Values from both sides are concatenated into a single array ## Annotation Syntax ```atscript @meta.label 'User Name' // With argument @meta.sensitive // Flag (no argument) @expect.pattern "^[A-Z]", "i" // Multiple arguments @meta.documentation 'Line 1' // Can be repeated @meta.documentation 'Line 2' ``` Arguments can be optional. Annotations without arguments are flag annotations. ## Core Annotations Atscript provides common-purpose annotations: ### Meta Annotations (@meta.\*) - `@meta.label 'text'` - Human-readable label - `@meta.id` - Marks identifier field (multiple fields form composite PK) - `@meta.description 'text'` - Field description - `@meta.documentation 'text'` - Multi-line docs (repeatable) - `@meta.sensitive` - Marks sensitive data - `@meta.readonly` - Read-only field - `@meta.default 'value'` - Default value (string as-is, other types parsed as JSON) - `@meta.example 'value'` - Example value (string as-is, other types parsed as JSON) ### UI Annotations (@ui.\*) `@ui.*` annotations (placeholders, form layout hints, component overrides) are provided by a separate UI plugin, not by `@atscript/typescript` itself. See the UI plugin docs for the full reference. The annotation _shape_ shown in this guide (`@ui.placeholder 'text'`, `@ui.component 'name'`, etc.) follows the same syntax rules as the other namespaces above. ### Validation Annotations (@expect.\*) - `@expect.minLength 5, "Custom error message"` - Minimum string/array length (optional message) - `@expect.maxLength 100, "Custom error message"` - Maximum string/array length (optional message) - `@expect.min 0, "Custom error message"` - Minimum number value (optional message) - `@expect.max 100, "Custom error message"` - Maximum number value (optional message) - `@expect.int "Custom error message"` - Must be integer (optional message) - `@expect.pattern "regex", "flags", "message"` - Pattern validation (repeatable, optional message) - `@expect.array.uniqueItems "Custom error message"` - Enforce unique items in an array (by key fields if defined, otherwise by deep equality; optional message) - `@expect.array.key "Custom error message"` - Mark a field as a key inside an array of objects (used for uniqueness checks, lookups, and patch operations; does not enforce uniqueness by itself; optional message) `@expect.array.key` has compile-time constraints: the field must be `string` or `number`, cannot be optional, and multiple key fields form a **composite key**. All validation annotations accept an optional custom error message as the last argument. When validation fails, the custom message is used instead of the default error message. #### Array Annotations Example `@expect.array.uniqueItems` and `@expect.array.key` work together to enforce unique array elements by identity fields: ```atscript interface Order { @meta.id id: number @expect.array.uniqueItems "Duplicate line items" items: OrderItem[] } interface OrderItem { @expect.array.key productId: number quantity: number price: number } ``` For primitive arrays, `@expect.array.uniqueItems` checks by deep equality — no `@expect.array.key` needed: ```atscript interface Product { @expect.array.uniqueItems "Tags must be unique" tags: string[] } ``` ### Form Validation (@meta.required) - `@meta.required` or `@meta.required "Custom error message"` - For strings: must contain at least one non-whitespace character. For booleans: must be `true` (optional message) ::: tip Form Validation Use `string.required` or `@meta.required` to ensure required string fields are not empty or whitespace-only. A plain `string` type accepts `''` as valid — `@meta.required` catches this common form validation gap. For checkboxes, `@meta.required` on a `boolean` field ensures the value is `true` (e.g., "accept terms"). ::: ### Emit Annotations (@emit.\*) - `@emit.jsonSchema` — Pre-compute and embed the JSON Schema for an interface at build time, regardless of the global `jsonSchema` plugin setting ### Database Annotations (@db.\*) Database annotations (tables, columns, indexes, relations, views, schema sync) are provided by a separate package, `@atscript/db/plugin`, and are documented at [https://db.atscript.dev](https://db.atscript.dev). Install the DB plugin from that ecosystem to register `@db.*` annotations in your project. ### Special Annotation Argument Types Some annotations accept special argument types beyond strings and numbers: - **Ref arguments** — Type references using dot-notation chains (e.g., `User.id`). Custom annotations can declare `type: 'ref'` arguments. - **Query arguments** — SQL-like expressions in backticks (e.g., `` `Task.status != 'done'` ``). Custom annotations can declare `type: 'query'` arguments. --- URL: "atscript.dev/_fragments/configuration" LLMS_URL: "atscript.dev/_fragments/configuration.md" --- Atscript uses a configuration file to control parsing, validation, and code generation. This configuration is automatically picked up by: - **VSCode extension** - For enhanced IntelliSense and real-time validation - **Build tools** - When using `unplugin-atscript` with Vite, Rollup, Rolldown, etc. - **CLI** - When running `asc` commands ## Configuration File Create `atscript.config.js` in your project root. Supported formats: - `atscript.config.js` - CommonJS - `atscript.config.mjs` - ESM module - `atscript.config.cjs` - Explicit CommonJS - `atscript.config.ts` - TypeScript (bundled with rolldown) - `atscript.config.mts` - TypeScript ESM - `atscript.config.cts` - TypeScript CommonJS When multiple formats coexist in the same directory, TypeScript variants (`.ts` / `.mts` / `.cts`) take precedence over JavaScript variants. ## defineConfig Helper The `defineConfig` helper provides type checking and IntelliSense: ```typescript import { defineConfig } from '@atscript/core' export default defineConfig({ // Full type checking and autocomplete }) ``` ## Configuration Options ### Input Options #### `rootDir` - **Type:** `string` - **Default:** Config file's directory (or `process.cwd()` if no config file) - **Description:** Root directory containing `.as` files ```javascript rootDir: 'src' // Look for .as files in src/ ``` #### `entries` - **Type:** `string[]` - **Default:** All `.as` files in rootDir - **Description:** Specific entry files to process ```javascript entries: ['types/user.as', 'types/product.as'] ``` #### `include` - **Type:** `string[]` - **Default:** `['**/*.as']` - **Description:** Glob patterns for files to include ```javascript include: ['**/*.as', '!**/*.test.as'] ``` #### `exclude` - **Type:** `string[]` - **Default:** `['node_modules']` - **Description:** Glob patterns for files to exclude ```javascript exclude: ['**/temp/**', '**/*.draft.as'] ``` #### `unknownAnnotation` - **Type:** `'allow' | 'warn' | 'error'` - **Default:** `'error'` - **Description:** How to handle unknown annotations ```javascript unknownAnnotation: 'allow' // Accept any annotation unknownAnnotation: 'warn' // Warn but continue unknownAnnotation: 'error' // Treat as error (strict) ``` #### `primitives` - **Type:** `Record` - **Description:** Custom primitive types and extensions ```javascript primitives: { string: { extensions: { url: { type: 'string', documentation: 'URL format', annotations: { 'expect.pattern': { pattern: '^https?://.+$', message: 'Invalid URL' } } }, coord: { type: 'number', documentation: 'Geographic coordinate', annotations: { 'expect.min': -180, 'expect.max': 180 } } } } } ``` ::: tip Annotations map Custom primitives apply implicit annotations via the `annotations` map. Each key is the annotation name (e.g., `'expect.pattern'`, `'expect.min'`); each value matches the annotation's argument shape — a single primitive for one-argument annotations, an object keyed by argument name when there are multiple arguments, or an array when the annotation is repeatable. The legacy `expect: { ... }` shape was removed. ::: #### `annotations` - **Type:** `AnnotationsTree` - **Description:** Custom annotation definitions ```javascript import { AnnotationSpec } from '@atscript/core' annotations: { ui: { hidden: new AnnotationSpec({ description: 'Hide field in UI', nodeType: ['prop'], }) } } ``` `AnnotationSpec` accepts the following options: | Option | Type | Default | Description | | --------------- | ----------------------- | ----------- | ------------------------------------------------------------------------------------------- | | `description` | `string` | — | Documentation shown in IntelliSense | | `argument` | `object \| object[]` | — | Argument definition(s) with `name`, `type`, optional `values` | | `nodeType` | `string[]` | — | Restrict to node types (e.g., `['prop']`, `['interface']`) | | `multiple` | `boolean` | `false` | Allow the annotation to appear more than once on the same node. Values are stored as arrays | | `mergeStrategy` | `'replace' \| 'append'` | `'replace'` | How same-named annotations combine during merging. Only relevant when `multiple: true` | ::: tip When `multiple: true` and `mergeStrategy: 'replace'` (the default), the higher-priority set of values replaces the lower-priority set entirely. With `mergeStrategy: 'append'`, values from both sides are concatenated. ::: #### `plugins` - **Type:** `Plugin[]` - **Description:** Active plugins for code generation ```javascript import ts from '@atscript/typescript' plugins: [ts()] ``` Plugins from external ecosystems follow the same pattern — e.g. database plugins like `@atscript/db-*` live in a [separate repo](https://db.atscript.dev) and are imported the same way. ### Output Options #### `format` - **Type:** `string` - **Default:** Plugin-dependent (the TypeScript plugin emits `.d.ts` when `format` is unset or set to `'dts'`; pass `'js'` to emit runtime metadata instead) - **Description:** Output format that plugins should generate ```javascript format: 'dts' // TypeScript plugin: Generate .d.ts files format: 'js' // TypeScript plugin: Generate .js files with metadata ``` The format is an open string field - each plugin decides which formats to support. ::: info Format Usage - **VSCode extension**: Uses this setting to determine what files to generate on save - **asc CLI**: Uses this setting by default (overridden by `-f` flag if provided) - **unplugin-atscript**: Ignores this setting (always generates what the bundler needs) ::: #### `outDir` - **Type:** `string` - **Default:** Same as source directory - **Description:** Output directory for generated files ```javascript outDir: 'dist' // Output to dist/ instead of source location ``` ## Config File Lookup ### VSCode Extension Looks for the nearest config file starting from the `.as` file location: 1. Check the folder containing the `.as` file 2. Check parent folder 3. Continue up the directory tree until workspace root is reached ### Build Tools (unplugin-atscript) Similar lookup strategy: 1. Start from the `.as` file location 2. Search upward through parent directories 3. Stop at current working directory (cwd) This allows for monorepo setups where different packages can have their own Atscript configurations. ## Loading Priority When multiple config formats exist in the same directory: 1. `atscript.config.ts` / `atscript.config.mts` 2. `atscript.config.js` / `atscript.config.mjs` 3. Default configuration if no file found --- URL: "atscript.dev/_fragments/imports-exports" LLMS_URL: "atscript.dev/_fragments/imports-exports.md" --- ## Key Limitations 1. **No default imports/exports** - Only named exports and imports are supported 2. **No namespace or rename syntax** - No `import * as`, `export * as`, or `import { a as b }` 3. **`.as` files can only import `.as` files** - Cannot import files from the target language ## Named Exports ```atscript // user.as - Named exports only export interface User { id: string name: string } export type UserID = string export type Status = 'active' | 'inactive' // Private (not exported) interface InternalConfig { debug: boolean } ``` ## Importing in .as Files In `.as` files, omit the file extension: ```atscript // app.as import { User, UserID, Status } from './user' import { Product } from '../models/product' export interface Order { user: User items: Product[] } ``` ## Valid Import/Export Examples ### Basic Named Import/Export ```atscript // types.as export interface Person { name: string } export type ID = string ``` ```atscript // main.as import { Person, ID } from './types' export interface Employee extends Person { employeeId: ID } ``` ### Multiple Imports from Same File ```atscript // models.as export interface User { } export interface Product { } export interface Order { } export type Status = string ``` ```atscript // app.as import { User, Product, Order, Status } from './models' ``` ### Nested Directory Imports ```atscript // domain/user.as import { BaseEntity } from '../shared/base' import { Address } from './types/address' ``` ## Importing from Packages (node_modules) Atscript supports importing `.as` files published in npm packages. Use bare specifiers (no `./` prefix) to import from `node_modules`: ```atscript // app.as import { User } from 'my-lib/user' import { Product } from '@my-org/models/product' ``` Like relative imports, the `.as` extension is omitted. The resolver automatically appends it. ### How Resolution Works When Atscript encounters a bare import specifier (one that doesn't start with `.` or `/`): 1. The specifier is parsed into a **package name** and **subpath**: - `my-lib/user` → package `my-lib`, subpath `./user.as` - `@my-org/models/product` → package `@my-org/models`, subpath `./product.as` 2. The resolver walks up from the importing file's directory, checking for `node_modules//package.json` at each level. 3. If found, it checks the `exports` field for a matching subpath entry with the `atscript` condition (pointing to the raw `.as` source file). 4. If no `exports` match, it falls back to looking for the file directly at `node_modules//.as`. ### Publishing Packages with .as Files To publish a package that exports `.as` files, declare them in `package.json` using the standard `exports` field with an `"atscript"` condition: ```json { "name": "@my-org/models", "exports": { "./user.as": { "atscript": "./src/user.as", "types": "./dist/user.as.d.ts", "import": "./dist/user.as.mjs" }, "./product.as": { "atscript": "./src/product.as", "types": "./dist/product.as.d.ts", "import": "./dist/product.as.mjs" } } } ``` - **`atscript`** — points to the raw `.as` source (used by the Atscript compiler and LSP) - **`types`** — TypeScript declarations (generated by `asc -f dts`) - **`import`** — compiled JavaScript (generated by `asc -f js`) For packages with many `.as` files, use wildcard patterns: ```json { "exports": { "./*.as": { "atscript": "./src/*.as", "types": "./dist/*.as.d.ts", "import": "./dist/*.as.mjs" } } } ``` **Simplest option**: If your `.as` files are at the expected path, no `exports` configuration is needed. The resolver falls back to direct file access: ``` node_modules/@my-org/models/ user.as ← import { User } from '@my-org/models/user' product.as ← import { Product } from '@my-org/models/product' package.json ← no exports field needed ``` ## Invalid Syntax (Not Supported) ```atscript // ❌ Default exports export default interface User { } // ❌ Default imports import User from './user' // ❌ Namespace imports import * as models from './models' // ❌ Export namespace export * as utils from './utils' // ❌ Import with rename import { User as UserModel } from './user' // ❌ Re-exports export { User } from './user' // ❌ Importing non-.as files import { helper } from './helper.ts' ``` --- URL: "atscript.dev/_fragments/interfaces-types" LLMS_URL: "atscript.dev/_fragments/interfaces-types.md" --- ## Start With Interfaces Most `.as` files are just interfaces with a few annotations and semantic types: ```atscript export interface User { id: string name: string age: number isActive: boolean } ``` Use an interface when you want a named object shape that can later be imported, validated, and inspected at runtime. ## Nest Objects Naturally Inline nested objects work well when the nested shape is local to one model: ```atscript export interface User { id: string profile: { name: string avatar?: string } settings: { theme: 'light' | 'dark' } } ``` If you want to reuse a nested shape across files, give it its own interface or type alias and import it. ## Use Type Aliases For Reusable Values Type aliases are useful for named primitives, unions, and reusable constraints: ```atscript @expect.minLength 3 @expect.maxLength 20 export type Username = string export type Status = 'pending' | 'success' | 'error' export type ID = string | number ``` Like interfaces, exported type aliases also exist at runtime and can be validated. ## Common Property Patterns ### Optional Properties ```atscript export interface Config { name: string bio?: string } ``` ### Arrays, Tuples, And Literals ```atscript export interface Data { tags: string[] coords: [number, number] status: 'pending' | 'done' } ``` ### Dynamic Keys If a model needs open-ended keys, use wildcard or pattern properties: ```atscript export interface EnvConfig { NODE_ENV: 'development' | 'production' [/^PUBLIC_.*/]: string } ``` That is useful for configuration objects, custom metadata maps, or other flexible records. ## Reuse Types By Reference ```atscript import { Address } from './address' export interface User { address: Address friends: User[] manager?: User } ``` You can reference other types, self-reference, and use arrays of references naturally. ## Advanced Composition ### Interface Extends ```atscript interface BaseEntity { id: string createdAt: string.isoDate } interface Timestamped { updatedAt: string } export interface Post extends BaseEntity, Timestamped { title: string body: string } ``` Rules: - Own properties are added to the inherited ones - Prop-level annotations are inherited from parents - Interface-level annotations are not inherited - Overriding a parent property in a child is not allowed — to **add** annotations to an inherited property without redeclaring it, use a mutating [`annotate`](/packages/typescript/ad-hoc-annotations) block on the parent - Self-extends and circular extends are detected as errors ### Intersection Types ```atscript interface Timestamped { createdAt: string } export type Post = { title: string } & Timestamped ``` Intersections are useful when you want to combine types inline instead of declaring a new parent interface. Use `type` (not `interface`) for the intersection itself — interfaces cannot end with a trailing `& Other`. ## Practical Example ```atscript import { Address } from './address' export interface User { id: string.uuid username: string.required email: string.email profile: { displayName: string bio?: string [/^social_.*/]: string } addresses: Address[] status: 'active' | 'inactive' | 'pending' createdAt: string.isoDate } ``` --- URL: "atscript.dev/_fragments/primitives" LLMS_URL: "atscript.dev/_fragments/primitives.md" --- Primitives are the building blocks of every `.as` model. In day-to-day app code, the main thing to learn is that Atscript lets you refine primitives with semantic extensions like `string.email` and `number.int`. ## Basic Primitive Types Atscript supports the following primitive types: - **`string`** - Text values - **`number`** - Numeric values - **`decimal`** - Decimal number stored as string to preserve precision - **`boolean`** - True/false values - **`null`** - Null value - **`undefined`** - Undefined value - **`void`** - No value ```atscript export interface BasicTypes { text: string count: number price: decimal isEnabled: boolean empty: null nothing: void } ``` ## Semantic Types Primitives can be extended with dot notation: ```atscript export interface User { email: string.email name: string.required age: number.int.positive } ``` These semantic types do two useful things: 1. they make the model easier to read 2. they attach validation behavior automatically ### String Extensions The most common string extensions are: ```atscript export interface User { id: string.uuid email: string.email phone: string.phone name: string.required birthDate: string.date createdAt: string.isoDate website: string.url serverIp: string.ip initial: string.char } ``` Use them when the field has a real meaning that is stronger than plain `string`. Other string extensions include `string.ipv4` and `string.ipv6` for protocol-specific IP address validation. ### Number Extensions The most common number extensions are: ```atscript export interface Product { quantity: number.int price: number.positive discount: number.negative weight: number.double } ``` Useful built-ins: - `number.int` — integer only - `number.positive` — minimum `0` - `number.negative` — maximum `0` - `number.single` / `number.double` — numeric intent tags #### Sized Integer Types For fields that need a specific bit width, `number.int` provides sized extensions: ```atscript export interface SensorData { reading: number.int.int16 flags: number.int.uint8 port: number.int.uint16.port offset: number.int.int32 } ``` Signed: `int8`, `int16`, `int32`, `int64`. Unsigned: `uint8`, `uint16`, `uint32`, `uint64`. Aliases: `uint8.byte` (byte value), `uint16.port` (network port). ### Boolean Extensions Boolean extensions are mostly useful for required checkboxes and flags: ```atscript export interface Settings { agreed: boolean.required alwaysOn: boolean.true disabled: boolean.false } ``` `boolean.required` is the one most application code needs. It means the value must be `true`. ## Combining Extensions You can combine extensions when the field needs more than one rule: ```atscript export interface Metrics { retries: number.int.positive loss: number.double.negative } ``` Prefer semantic types over separate `@expect.*` annotations when a built-in semantic type already says what you mean. ### Decimal Type Use `decimal` for fields that need exact decimal precision — prices, financial amounts, measurements: ```atscript export interface Product { @db.column.precision 10, 2 price: decimal @db.column.precision 8, 4 exchangeRate: decimal } ``` At runtime, `decimal` values are strings (e.g., `"19.99"`). This preserves precision and survives JSON transport without any loss — unlike floating-point `number`, which can introduce rounding errors for decimal fractions. In SQL databases, `decimal` maps to the native `DECIMAL` type (with precision/scale from `@db.column.precision`). Use `number` for general-purpose numerics and `decimal` when exact decimal representation matters. ## Advanced Primitives ### Timestamp Variants Atscript provides timestamp-oriented numeric tags: `number.timestamp`, `number.timestamp.created`, and `number.timestamp.updated`. Timestamps are stored as `number` and constrained to integers — the unit (seconds, milliseconds, microseconds) is project-decided. The primitive itself does not enforce a unit. Storing timestamps as numbers is a deliberate choice — numbers are JSON-native, so timestamps pass through HTTP boundaries (client ↔ server) without any serialization or hydration step. Using `Date` objects would require walking every response to convert strings back to `Date` instances on both sides of the transport layer. These are advanced because they matter more for DB integrations than for basic TypeScript usage. ### `phantom` `phantom` is a special non-data type for runtime-discoverable elements that should not appear in TypeScript data, validation, or JSON Schema. It is useful for advanced UI tooling and type traversal, but most application code can ignore it until needed. ## Best Practices 1. Use semantic types when the field has real meaning beyond a plain primitive. 2. Let built-in semantic types carry validation instead of duplicating the same rule by hand. 3. Reach for `string.required`, `string.email`, and `number.int` early — they cover many common cases. 4. Use `decimal` instead of `number` when exact decimal precision matters (prices, financial data). 5. Save advanced primitives like `phantom` and timestamp variants for pages or features that really need them. ::: tip Combining Extensions - `number.int.positive` — positive integers only - `number.double.negative` — negative double-precision numbers - `number.single.positive` — positive single-precision numbers - `number.int.uint16.port` — network port number ::: --- URL: "atscript.dev/_fragments/why-atscript" LLMS_URL: "atscript.dev/_fragments/why-atscript.md" --- ## The Problem: Scattered Data Definitions In modern software projects, especially business applications, data structure definitions are scattered across multiple layers and files: - **Type definitions** in your programming language (TypeScript interfaces, Go structs, Java classes) - **Validation rules** in separate validation libraries or schemas - **Database constraints** in migration files or ORM configurations - **UI metadata** like labels and descriptions in frontend components - **API documentation** in OpenAPI/Swagger files - **Database indexes** in database-specific DDL scripts This scattering leads to: - **Duplication** - The same information repeated in different formats - **Inconsistency** - Changes in one place not reflected in others - **Maintenance burden** - Multiple files to update for a single change - **No single source of truth** - Unclear which definition is authoritative ## The Solution: Unified Data Definition Atscript brings order to this chaos by providing a single place to define: ```atscript @db.table 'users' @meta.description 'User entity for our application' export interface User { @meta.id @db.index.unique 'email_idx' @meta.label 'User Email' @meta.description 'Primary contact email' email: string.email @meta.label 'Full Name' @expect.minLength 2 @expect.maxLength 100 @db.index.fulltext 'search_idx' name: string @meta.label 'Age' @expect.min 13 @expect.max 150 @expect.int age: number @meta.label 'Account Status' @meta.documentation 'Indicates if the user can access the system' @db.index.plain 'status_idx' isActive: boolean } ``` From this single definition, Atscript can already drive several parts of your stack today, and it is designed to expand further from the same model over time. ## What Atscript Gives You Today - TypeScript types with full type safety - Runtime validators with model-defined constraints - JSON Schema and runtime metadata export - Database annotations and integrations via [Database Layer](https://db.atscript.dev/guide/) - REST/CRUD integrations in the TypeScript ecosystem ## Where the Model Is Going Atscript is growing toward a wider model-driven workflow where the same `.as` definition can shape: - UI form tools - Table and list tools - API contracts and integrations - TypeScript code - Database schema and operations The direction is one model across the full data flow, while keeping the current docs precise about what is already available and what is planned next. ## Core Design Principles ### 1. One Model Across the Stack - Keep structure, validation, metadata, and data-layer hints in one place - Make the model the source of truth instead of generating more duplicated config - Grow capabilities outward from the same definition instead of creating new parallel schemas ### 2. Everything Is Extensible - **Types are extensible**: Create semantic types like `string.email`, `number.positive` - **Annotations are extensible**: Add any metadata your project needs - **Plugins are powerful**: Generate code for any language or framework ### 3. Annotations for Everything Atscript uses annotations to attach any kind of metadata: - `@meta.*` - Human-readable information - `@expect.*` - Validation constraints - `@db.*` - Database configuration (tables, indexes, columns, defaults) - `@your.custom` - Whatever your project needs ### 4. Language-Agnostic by Design The core model and plugin system are built so Atscript can be adopted by different language targets over time. Today, TypeScript is the first supported plugin and the most complete workflow. - Clean, TypeScript-like syntax keeps the model easy to read - The plugin system allows other languages to adopt the same model pattern - Future language targets can build on the same core concepts instead of reinventing the schema ## Real-World Benefits ### For Development Teams - **Single source of truth** - One place to define data structures - **Consistency guaranteed** - All layers use the same definitions - **Faster development** - No need to maintain multiple schemas - **Type safety everywhere** - From database to UI ### For Business Logic - **Business rules in one place** - Validation constraints with the data - **Self-documenting** - Metadata makes code more readable - **Audit-friendly** - Clear data governance and constraints ### For System Architecture - **Microservices contracts** - Share types between services - **API-first design** - Generate OpenAPI from types - **Database from annotations** - Tables, indexes, and CRUD from `@db.*` annotations - **Cross-platform** - Same types for backend and frontend ## Who Benefits from Atscript Today? - **TypeScript backend and full-stack teams** tired of maintaining duplicate schemas - **Projects** with validation, metadata, and DB rules spread across multiple files - **Teams** that want one source of truth today and a more model-driven stack over time --- URL: "atscript.dev/packages/moost-validator" LLMS_URL: "atscript.dev/packages/moost-validator.md" --- # @atscript/moost-validator Use Atscript models as runtime validation contracts in Moost handlers. `@atscript/moost-validator` gives you two small integration points: - `validatorPipe()` validates handler arguments whose runtime type comes from an `.as` model - `validationErrorTransform()` turns `ValidatorError` into `HttpError(400)` for HTTP apps ::: tip Best Path For New Users If you are evaluating this package for the first time, read these in order: 1. [Why Atscript In Moost?](/packages/moost-validator/why-atscript-validation) 2. [Validation Pipe](/packages/moost-validator/validation-pipe) 3. [Error Handling](/packages/moost-validator/error-handling) ::: ## What This Package Gives You - one `.as` model for TypeScript types and runtime validation - automatic validation in Moost handlers - the same Atscript validator options you already use elsewhere - optional HTTP-friendly error conversion when you use `@moostjs/event-http` ## Public API Each integration point has a global form (apply app-wide) and a decorator form (apply to one controller or handler): | Export | Form | Use it to | | ------------------------------- | ------------------- | --------------------------------------------------------------------- | | `validatorPipe(opts?)` | global pipe | Validate handler args against their `.as` type — see [Validation Pipe](/packages/moost-validator/validation-pipe) | | `UseValidatorPipe(opts?)` | parameter decorator | Same validation, scoped to a single handler argument | | `validationErrorTransform()` | global interceptor | Convert `ValidatorError` → `HttpError(400)` — see [Error Handling](/packages/moost-validator/error-handling) | | `UseValidationErrorTransform()` | method decorator | Same conversion, scoped to one handler | Both pipes accept `Partial` (the standard Atscript [validator options](/packages/typescript/validation)). ## Installation ::: code-group ```bash [pnpm] pnpm add @atscript/moost-validator ``` ```bash [npm] npm install @atscript/moost-validator ``` ::: ### Peer Dependencies You also need: - `@atscript/core` - `@atscript/typescript` - `moost` - `@moostjs/event-http` ::: tip Non-HTTP setups `@moostjs/event-http` is a hard peer because the package re-exports `validationErrorTransform()`, which references `HttpError` from that package. If you build a non-HTTP Moost app you can still install `@moostjs/event-http` to satisfy the peer and simply ignore the HTTP transform — only `validatorPipe()` is used in that case. ::: ## Quick Start ### 1. Define A DTO In Atscript ```atscript export interface CreateUserDto { @meta.label 'Display Name' @expect.minLength 2, 'Name must be at least 2 characters' name: string email: string.email @expect.minLength 8, 'Password must be at least 8 characters' password: string } ``` ### 2. Register The Pipe And HTTP Error Transform ```typescript import { Moost } from 'moost' import { MoostHttp } from '@moostjs/event-http' import { validatorPipe, validationErrorTransform } from '@atscript/moost-validator' import { UsersController } from './users.controller' const app = new Moost() app.adapter(new MoostHttp()) app.applyGlobalPipes(validatorPipe()) app.applyGlobalInterceptors(validationErrorTransform()) app.registerControllers(UsersController) await app.init() ``` ### 3. Use The DTO In A Controller ```typescript import { Controller } from 'moost' import { Body, Post } from '@moostjs/event-http' import { CreateUserDto } from './create-user.dto.as' @Controller('users') export class UsersController { @Post() async create(@Body() dto: CreateUserDto) { // dto has already been validated return { id: '123', ...dto } } } ``` Without the pipe, the TypeScript annotation alone does not validate request data. With the pipe in place, your handler runs only after Atscript validation succeeds. ## HTTP And Non-HTTP Usage - `validatorPipe()` is the core integration and works anywhere Moost resolves handler arguments - `validationErrorTransform()` is HTTP-specific because it converts `ValidatorError` into `HttpError` If you are not building an HTTP app, keep `validatorPipe()` and use your own error interceptor for your adapter or event format. ## Next Steps - [Why Atscript In Moost?](/packages/moost-validator/why-atscript-validation) — what this package removes from your handlers - [Validation Pipe](/packages/moost-validator/validation-pipe) — global setup, PATCH payloads, unknown props, and common options - [Error Handling](/packages/moost-validator/error-handling) — how the built-in HTTP error transform works --- URL: "atscript.dev/packages/moost-validator/error-handling" LLMS_URL: "atscript.dev/packages/moost-validator/error-handling.md" --- # Error Handling `validationErrorTransform()` is the HTTP-focused companion to `validatorPipe()`. It catches `ValidatorError` and turns it into `HttpError(400)`, so your handlers do not need to shape validation responses manually. ## The Most Common HTTP Setup ```typescript import { Moost } from 'moost' import { MoostHttp } from '@moostjs/event-http' import { validatorPipe, validationErrorTransform } from '@atscript/moost-validator' const app = new Moost() app.adapter(new MoostHttp()) app.applyGlobalPipes(validatorPipe()) app.applyGlobalInterceptors(validationErrorTransform()) ``` This is the best default for HTTP apps. ## What The Built-In Transform Does When it catches `ValidatorError`, it creates: - `HttpError(400)` - `message` from `ValidatorError.message` - `statusCode: 400` - `_body` containing the full `error.errors` array That is the contract implemented by the package today. ## Why The Docs Do Not Promise One Exact JSON Shape The transform produces an `HttpError`. The final serialized HTTP body depends on how your HTTP layer renders that error. So the stable guarantee is: - failed validation becomes HTTP 400 - the top-level message comes from the first validation error - the full validator error list is attached to the HTTP error payload If your API needs a very specific error envelope, write your own interceptor. ## Apply The Transform Per Controller Or Handler ### Per Controller ```typescript import { Controller } from 'moost' import { UseValidationErrorTransform } from '@atscript/moost-validator' @UseValidationErrorTransform() @Controller('users') export class UsersController {} ``` ### Per Handler ```typescript import { Controller } from 'moost' import { Body, Post } from '@moostjs/event-http' import { UseValidationErrorTransform } from '@atscript/moost-validator' import { CreateUserDto } from './create-user.dto.as' @Controller('users') export class UsersController { @Post() @UseValidationErrorTransform() async create(@Body() dto: CreateUserDto) {} } ``` ## Write A Custom Error Shape If you want a different HTTP response format, create your own interceptor: ```typescript import { ValidatorError } from '@atscript/typescript/utils' import { HttpError } from '@moostjs/event-http' import { defineInterceptor, TInterceptorPriority } from 'moost' const customValidationErrors = () => defineInterceptor( { error(error, reply) { if (error instanceof ValidatorError) { reply( new HttpError(400, { statusCode: 400, message: 'Validation failed', code: 'VALIDATION_FAILED', errors: error.errors.map(item => ({ field: item.path, reason: item.message, })), }) ) } }, }, TInterceptorPriority.CATCH_ERROR ) ``` This is the right approach when you want a stable public API envelope instead of the package's default `HttpError` payload. ## Non-HTTP Apps The built-in transform is intentionally HTTP-specific because it returns `HttpError`. If your Moost app uses another adapter: - keep `validatorPipe()` - replace `validationErrorTransform()` with an interceptor that maps `ValidatorError` into your own event or reply shape ## Next Steps - [Validation Pipe](/packages/moost-validator/validation-pipe) — the validator integration itself - [Validation Guide](/packages/typescript/validation) — validator options and lower-level behavior - [Why Atscript In Moost?](/packages/moost-validator/why-atscript-validation) — when this package is the right fit --- URL: "atscript.dev/packages/moost-validator/validation-pipe" LLMS_URL: "atscript.dev/packages/moost-validator/validation-pipe.md" --- # Validation Pipe `validatorPipe(opts?)` is the main entry point in `@atscript/moost-validator`. It checks a handler argument's runtime type. If that type is an Atscript annotated type, it runs the model's validator before your handler executes. ## The Most Common Setup Register it globally: ```typescript import { Moost } from 'moost' import { validatorPipe } from '@atscript/moost-validator' const app = new Moost() app.applyGlobalPipes(validatorPipe()) ``` That is the best default for most apps. ## Validate Request Bodies This is the most common use case: ```typescript import { Controller } from 'moost' import { Body, Post } from '@moostjs/event-http' import { CreateUserDto } from './create-user.dto.as' @Controller('users') export class UsersController { @Post() async create(@Body() dto: CreateUserDto) { // dto has already been validated return this.users.create(dto) } } ``` If validation fails, the pipe throws `ValidatorError`. For HTTP apps, pair it with [Error Handling](/packages/moost-validator/error-handling). ## Validate PATCH Payloads For partial updates, pass Atscript validator options to the pipe: ```typescript import { Controller } from 'moost' import { Patch, Body } from '@moostjs/event-http' import { UseValidatorPipe } from '@atscript/moost-validator' import { UpdateUserDto } from './update-user.dto.as' @Controller('users') export class UsersController { @Patch(':id') @UseValidatorPipe({ partial: true }) async patch(@Body() dto: UpdateUserDto) { return this.users.patch(dto) } } ``` Use `partial: 'deep'` when nested objects should also allow missing fields. ## Strip Unknown Properties This is useful for request bodies that may contain extra fields: ```typescript app.applyGlobalPipes( validatorPipe({ unknownProps: 'strip', }) ) ``` Options: - `'error'` — reject unknown props - `'ignore'` — keep them - `'strip'` — remove them from the value ## Apply It Per Controller Or Handler Global registration is usually best, but the decorator form is useful when only part of the app uses Atscript DTOs. ### Per Controller ```typescript import { Controller } from 'moost' import { UseValidatorPipe } from '@atscript/moost-validator' @UseValidatorPipe() @Controller('users') export class UsersController {} ``` ### Per Handler ```typescript import { Controller } from 'moost' import { Body, Post } from '@moostjs/event-http' import { UseValidatorPipe } from '@atscript/moost-validator' import { CreateUserDto } from './create-user.dto.as' @Controller('users') export class UsersController { @Post() @UseValidatorPipe() async create(@Body() dto: CreateUserDto) {} } ``` `UseValidatorPipe(opts?)` is sugar for `@Pipe(validatorPipe(opts))`. ## Validate Params And Query Values Carefully The pipe validates the value it receives. It does not coerce strings into numbers, booleans, or dates. That means: - body payloads are usually a good fit because JSON parsing already gives you numbers, booleans, arrays, and objects - raw HTTP params and query values are often strings, so string-based Atscript types fit best there Good example: ```atscript export type EmailQuery = string.email ``` ```typescript import { Controller } from 'moost' import { Get, Query } from '@moostjs/event-http' import { EmailQuery } from './queries.as' @Controller('users') export class UsersController { @Get('search') async search(@Query('email') email: EmailQuery) { return this.users.searchByEmail(email) } } ``` If a param or query needs numeric validation, parse it before it reaches this pipe or validate it as a string-shaped contract instead. ## Optional Params Parameters marked with Moost's `@Optional()` decorator are skipped by the pipe when their value is `undefined` or `null`. This matters for slots that are genuinely missing at the framework level — optional query strings, optional CLI flags — where the underlying Atscript type itself is not nullable. ```typescript import { Controller, Optional } from 'moost' import { Get, Query } from '@moostjs/event-http' import { WikiName } from './types.as' @Controller('search') export class SearchController { @Get() async search(@Query('wiki') @Optional() wiki?: WikiName) { // `wiki` is undefined when the query string is absent — no validation runs. // When present, `WikiName` constraints (minLength, pattern, ...) are enforced. } } ``` The pipe still validates the value whenever one **is** provided, so `?wiki=` (empty) or `?wiki=bad name` will fail with `ValidatorError` as usual. For request bodies, this is rarely needed — bodies are usually required at the framework level, and individual properties can be marked optional inside the Atscript interface itself. ## Use Reusable Validated Primitive Types This is one of the nicest patterns in Atscript + Moost. Define a validated primitive once: ```atscript export type Email = string.email ``` Then use those types directly in handlers: ```typescript import { Controller } from 'moost' import { Body, Post } from '@moostjs/event-http' import { Email } from './types.as' @Controller('newsletter') export class NewsletterController { @Post('subscribe') async subscribe(@Body() email: Email) { return this.newsletter.subscribe(email) } } ``` That is hard to model with `class-validator`, because its validation model is centered on decorated classes and their properties. Atscript validates the type itself, so standalone reusable primitives work naturally. ## Options At A Glance `validatorPipe(opts?)` accepts the same `Partial` object as Atscript's runtime validator. Most useful options: - `partial` — allow missing properties (PATCH) - `unknownProps` — `'strip' | 'ignore' | 'error'` - `errorLimit` — stop after N errors ### Advanced options These are available but rarely needed in HTTP request validation. Reach for them only when you understand the underlying validator behavior: - `skipList` — paths to skip during validation - `replace` — value-replacement hook used by validator plugins - `plugins` — custom validator plugin functions If you already know the TypeScript validator API, the same options work here. See [Validation Reference](/packages/typescript/validation-reference) for the full low-level option details. ## Beyond HTTP The pipe is not tied to HTTP. It works anywhere Moost resolves handler arguments. What is HTTP-specific is the built-in error transform, because that part returns `HttpError(400)`. ## Next Steps - [Error Handling](/packages/moost-validator/error-handling) — convert `ValidatorError` into HTTP responses - [Validation Guide](/packages/typescript/validation) — deeper validator behavior and options - [Why Atscript In Moost?](/packages/moost-validator/why-atscript-validation) — when this package is the right fit --- URL: "atscript.dev/packages/moost-validator/why-atscript-validation" LLMS_URL: "atscript.dev/packages/moost-validator/why-atscript-validation.md" --- # Why Atscript In Moost? In Moost, a parameter type annotation is not validation by itself. This controller: ```typescript import { Controller } from 'moost' import { Body, Post } from '@moostjs/event-http' import { CreateUserDto } from './create-user.dto.as' @Controller('users') export class UsersController { @Post() async create(@Body() dto: CreateUserDto) { return this.users.create(dto) } } ``` looks safe, but `dto` is still just request data unless something validates it at runtime. ## The Usual Alternatives Without `@atscript/moost-validator`, you usually end up with one of these: - manual `CreateUserDto.validator().validate(...)` calls inside handlers - a second validation layer such as Zod or class-validator - no runtime validation at all All three add friction. The first adds boilerplate. The second duplicates the model. The third is not safe. ## Without Moost Validator ```typescript import { Controller } from 'moost' import { Body, Post, HttpError } from '@moostjs/event-http' import { CreateUserDto } from './create-user.dto.as' @Controller('users') export class UsersController { @Post() async create(@Body() body: unknown) { const validator = CreateUserDto.validator() if (!validator.validate(body, true)) { throw new HttpError(400, { message: validator.errors[0]?.message || 'Validation failed', statusCode: 400, _body: validator.errors, }) } return this.users.create(body) } } ``` This works, but now every handler has to remember to validate, shape an error, and keep that pattern consistent. ## With Moost Validator ```typescript import { Controller } from 'moost' import { Body, Post } from '@moostjs/event-http' import { validatorPipe, validationErrorTransform } from '@atscript/moost-validator' app.applyGlobalPipes(validatorPipe()) app.applyGlobalInterceptors(validationErrorTransform()) @Controller('users') export class UsersController { @Post() async create(@Body() dto: CreateUserDto) { return this.users.create(dto) } } ``` Now the handler only describes the contract. The validation logic stays in one place. ## What Changes In Practice - your `.as` model becomes the controller contract - request validation happens before business logic - the same Atscript validator options work in Moost too - HTTP error conversion is consistent across handlers ## Compared To class-validator In Nest-Style Apps If you come from NestJS-style validation, the biggest difference is where the validation rules live. With `class-validator`, validation is attached to class properties with decorators. For nested object graphs, that usually means: - defining a separate class for each nested structure you want to validate - adding validation decorators to each property - adding nested-validation wiring such as `@ValidateNested()` - often pairing it with `class-transformer` so nested values are turned into class instances That works, but it is easy to end up repeating information that is already present in the TypeScript type shape. With Atscript, the model is the validation contract: ```atscript export interface Address { city: string zip: string } export interface CreateUserDto { email: string.email address: Address } ``` There is no second class layer for nested shapes. The `.as` model already carries both the structure and the validation rules. ## Reusable Validated Primitive Types Another difference is validated primitives. With Atscript, you can define a reusable primitive type once: ```atscript export type Email = string.email ``` and use it directly in a handler: ```typescript import { Controller } from 'moost' import { Body, Post } from '@moostjs/event-http' import { Email } from './types.as' @Controller('newsletter') export class NewsletterController { @Post() async subscribe(@Body() email: Email) { return this.newsletter.subscribe(email) } } ``` That handler argument is validated automatically by `validatorPipe()`. This is a strong fit for Atscript because validation is driven by the annotated type itself. In contrast, `class-validator` is built around decorators on class properties, so it does not give you the same standalone, reusable validated primitive-type pattern for direct handler arguments. ## When This Package Fits Best Use `@atscript/moost-validator` when: - your app already uses Atscript models - you want runtime validation in Moost without handler boilerplate - you want one source of truth for TypeScript types and validation rules ## What It Does Not Do - it does not create Atscript models for you - it does not coerce values into the right type - it does not replace the need for a custom error shape if your API needs one The pipe validates the value it receives. If you need coercion or a non-HTTP error format, add those pieces separately. ## Next Steps - [Validation Pipe](/packages/moost-validator/validation-pipe) — the main integration point - [Error Handling](/packages/moost-validator/error-handling) — built-in HTTP behavior and custom error shapes - [TypeScript Quick Start](/packages/typescript/quick-start) — if you have not set up Atscript models yet --- URL: "atscript.dev/packages/typescript" LLMS_URL: "atscript.dev/packages/typescript.md" --- # Atscript for TypeScript The TypeScript guide is the main place to evaluate Atscript today. TypeScript is the first supported target, and this section is organized as a practical learning path first, with lower-level reference material separated out for later. ::: tip Best Path For New Users If you are evaluating Atscript for the first time, read these in order: 1. [Why Atscript?](/packages/typescript/why-atscript) 2. [Quick Start](/packages/typescript/quick-start) 3. [Build Setup](/packages/typescript/build-setup) 4. [Validation Guide](/packages/typescript/validation) ::: ## What You Can Do Today - Define models once in `.as` files - Generate TypeScript types and runtime metadata - Validate data from the same model - Export JSON Schema - Feed the same model into DB integrations ## Recommended Learning Path ### 1. Get A First Success - [Why Atscript?](/packages/typescript/why-atscript) — the problem Atscript solves for TypeScript applications - [Quick Start](/packages/typescript/quick-start) — define one model, generate files, and validate data - [Build Setup](/packages/typescript/build-setup) — wire Atscript into Vite, Rollup, esbuild, or another bundler ### 2. Learn The Core Language - [Interfaces & Types](/packages/typescript/interfaces-types) — the `.as` syntax you will use most - [Imports & Exports](/packages/typescript/imports-exports) — how `.as`, `.as.d.ts`, and `.as.js` fit together - [Primitives](/packages/typescript/primitives) — semantic types like `string.email` and `number.int` - [Annotations Guide](/packages/typescript/annotations) — practical metadata and validation annotations ### 3. Use The Runtime Tools - [Validation Guide](/packages/typescript/validation) — validate unknown input with type narrowing - [Metadata](/packages/typescript/metadata-export) — read labels, placeholders, and other annotations at runtime - [JSON Schema](/packages/typescript/json-schema) — generate JSON Schema from types - [Serialization](/packages/typescript/serialization) — serialize types for backend-to-frontend transfer ### 4. Configure And Automate - [Installation](/packages/typescript/installation) — packages, prerequisites, and optional tooling - [Configuration](/packages/typescript/configuration) — plugin options and config file settings - [CLI](/packages/typescript/cli) — generate files from the command line ### 5. Go Deeper When You Need To - [Atscript Validation vs Others](/packages/typescript/validation-comparison) — side-by-side comparison with Zod and class-validator - [Ad-hoc Annotations](/packages/typescript/ad-hoc-annotations) — annotate existing types without editing their source - [Annotations Reference](/packages/typescript/annotations-reference) — inheritance, merge rules, and the full annotation catalog - [Validation Reference](/packages/typescript/validation-reference) — validator options, plugin hooks, and lower-level API details - [Type Definitions](/packages/typescript/type-definitions) — the annotated runtime type system and traversal - [Code Generation](/packages/typescript/code-generation) — what Atscript emits and how imports work - [Custom Primitives](/packages/typescript/custom-primitives) — define your own primitive extensions - [Custom Annotations](/packages/typescript/custom-annotations) — define your own annotation types --- URL: "atscript.dev/packages/typescript/ad-hoc-annotations" LLMS_URL: "atscript.dev/packages/typescript/ad-hoc-annotations.md" --- # Ad-hoc Annotations ## Next Steps - [Annotations](/packages/typescript/annotations) — Core annotation system - [Interfaces & Types](/packages/typescript/interfaces-types) — Type definitions - [Build Setup](/packages/typescript/build-setup) — Bundler integration --- URL: "atscript.dev/packages/typescript/annotations" LLMS_URL: "atscript.dev/packages/typescript/annotations.md" --- # Annotations Guide Annotations attach metadata to a type, interface, or property. In practice, most application code uses annotations for three things: - labels and app-facing metadata - validation rules - UI-facing hints that stay close to the model ## Start With One Small Example ```atscript export interface User { @meta.label 'Full Name' @expect.minLength 2 name: string @meta.label 'Email Address' email: string.email @ui.placeholder 'Optional biography' // @ui.* requires the UI plugin — see below bio?: string } ``` That model now carries: - the TypeScript data shape - runtime validation rules - metadata that application code can read later ## `@meta.*`: Labels And App Metadata Use `@meta.*` annotations for information your app or tooling may want to read at runtime. Common examples: - `@meta.label` — a human-friendly field name - `@meta.description` — longer help text - `@meta.readonly` — read-only fields - `@meta.sensitive` — fields that deserve extra care - `@meta.example` — example data for docs or tooling ```atscript export interface Product { @meta.label 'Product Name' @meta.description 'Shown in the catalog and checkout' name: string } ``` ## `@expect.*`: Validation Rules Use `@expect.*` when the validation rule is specific to this field and is not already covered by a semantic type. ```atscript export interface SignupForm { @expect.minLength 3 @expect.maxLength 20 username: string @expect.min 18 age: number.int } ``` Common validation annotations: - `@expect.minLength` - `@expect.maxLength` - `@expect.min` - `@expect.max` - `@expect.int` - `@expect.pattern` If the field already reads naturally as `string.email` or `number.int.positive`, prefer the semantic type instead of repeating the same rule with annotations. ## `@ui.*`: UI-Facing Hints `@ui.*` annotations (placeholders, component overrides, form layout hints) are not built into `@atscript/typescript`. They are provided by a separate UI plugin in a sibling package — see that plugin's documentation for the full reference and installation steps. The `@meta.*` and `@expect.*` namespaces shown above ship in the core toolkit and work without any additional plugin. ## Reuse Annotations Through Named Types Annotations on a named type are reused when that type is referenced elsewhere. ```atscript @meta.label 'Username' @expect.minLength 3 export type Username = string export interface User { username: Username } ``` That is useful for rules and labels you want to repeat consistently across models. ## The One Merge Rule Most Users Need If a property adds its own annotation, the property-level value wins over the reused one. ```atscript @expect.minLength 3 export type Username = string export interface User { @expect.minLength 5 username: Username } ``` In that case, `username` uses `5`, not `3`. For deeper details like append-vs-replace behavior, repeatable annotations, and the full catalog, use the [Annotations Reference](/packages/typescript/annotations-reference). ## Keep Annotations Practical - Use semantic types first, annotations second. - Put labels and descriptions on the model, not in a separate UI config file. - Keep validation rules close to the field they apply to. - Use custom annotations only when your app or tooling will actually read them. - For DB-specific annotations, use the [Database Layer](https://db.atscript.dev/guide/) docs. ## Next Steps - [Annotations Reference](/packages/typescript/annotations-reference) — full syntax, inheritance, and built-in annotation catalog - [Validation Guide](/packages/typescript/validation) — how annotations become runtime validation - [Ad-hoc Annotations](/packages/typescript/ad-hoc-annotations) — annotate existing types without modifying their definition - [Custom Annotations](/packages/typescript/custom-annotations) — define your own annotation types - [Metadata](/packages/typescript/metadata-export) — access annotations at runtime --- URL: "atscript.dev/packages/typescript/annotations-reference" LLMS_URL: "atscript.dev/packages/typescript/annotations-reference.md" --- # Annotations Reference Use this page when you need the full annotation syntax, inheritance rules, merge behavior, and the built-in annotation catalog. --- URL: "atscript.dev/packages/typescript/build-setup" LLMS_URL: "atscript.dev/packages/typescript/build-setup.md" --- # Build Setup Integrate Atscript into your build process using `unplugin-atscript`. This plugin automatically compiles `.as` files during the build, using your [configuration file](/packages/typescript/configuration). ## Installation ```bash npm install -D unplugin-atscript ``` ## Vite — Node.js Library The most common setup: build a Node.js library with external dependencies. The plugin compiles `.as` files while Vite handles bundling. ```javascript // vite.config.js import { defineConfig } from 'vite' import atscript from 'unplugin-atscript/vite' export default defineConfig({ plugins: [atscript()], build: { lib: { entry: 'src/index.ts', formats: ['es'], }, rollupOptions: { external: [/node_modules/], }, }, }) ``` ## Vite — UI Application For frontend projects (e.g. Vue, React), add the Atscript plugin alongside your framework plugin: ```javascript // vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import atscript from 'unplugin-atscript/vite' export default defineConfig({ plugins: [atscript(), vue()], }) ``` This lets you import `.as` types directly in your components — for example, to drive form rendering from metadata or validate user input against your type definitions. ## Other Bundlers `unplugin-atscript` supports all major bundlers. Import from the bundler-specific entry point: ::: code-group ```javascript [Rollup] // rollup.config.js import atscript from 'unplugin-atscript/rollup' export default { plugins: [atscript()], } ``` ```javascript [esbuild] // build.js import { build } from 'esbuild' import atscript from 'unplugin-atscript/esbuild' build({ plugins: [atscript()], entryPoints: ['src/index.ts'], bundle: true, outdir: 'dist', }) ``` ```javascript [Rolldown] // rolldown.config.js import atscript from 'unplugin-atscript/rolldown' export default { plugins: [atscript()], } ``` ```javascript [Webpack] // webpack.config.js import atscript from 'unplugin-atscript/webpack' export default { plugins: [atscript()], } ``` ```javascript [Rspack] // rspack.config.js import atscript from 'unplugin-atscript/rspack' export default { plugins: [atscript()], } ``` ```javascript [Farm] // farm.config.js import atscript from 'unplugin-atscript/farm' export default { plugins: [atscript()], } ``` ::: ## Options The plugin takes the same options on every bundler entry. There is just one: | Option | Type | Default | Effect | | -------- | --------- | ------- | ------------------------------------------------------------------------------------------ | | `strict` | `boolean` | `true` | Fail the build on parse/diagnostic **errors**. `false` = log errors but keep building. | ```javascript atscript({ strict: false }) // warn-only — useful mid-refactor or in CI pre-flight ``` With `strict: false`, a `.as` file that fails to compile yields an empty module (`module.exports = {}`), so anything importing it will likely break at runtime — keep `strict: true` for normal builds. Everything else (primitives, annotations, plugins, `include`/`exclude`) lives in [`atscript.config.*`](/packages/typescript/configuration), which the plugin auto-discovers. The plugin only intercepts `*.as` imports; per-file filtering is delegated to the bundler. ## How It Works 1. **Config Discovery** — the plugin finds your `atscript.config.*` by searching upward from each `.as` file 2. **Plugin Execution** — runs the plugins defined in your configuration 3. **Runtime JS** — for each imported `.as` it emits the runtime metadata module (the same output as `asc -f js`) 4. **Import Resolution** — lets you import `.as` files directly in TypeScript/JavaScript In development the plugin compiles on demand with hot module replacement (native on Vite/Webpack/Rspack/Farm; via watch mode on Rollup/Rolldown). In production it pre-compiles during the build. ::: warning Type artifacts are not written by the bundler plugin `unplugin-atscript` never writes `.as.d.ts` or the project-level `atscript.d.ts` to disk. For type checking and IDE support, generate types with the [CLI](/packages/typescript/cli): `asc -f dts` (e.g. as a `postinstall` and pre-build step), or let the [VSCode extension](/packages/vscode/) regenerate them on save. (Declaration *bundling* in library builds is a separate concern — see [below](#library-builds-with-declaration-bundling).) ::: ## Library Builds with Declaration Bundling If you build a **library** whose TypeScript entries re-export `.as` symbols and bundle declarations with `rolldown-plugin-dts` (used by `tsdown`) or `rollup-plugin-dts`, the plugin serves type declarations to the declaration pass automatically: when the declaration bundler resolves an `.as` import from a generated declaration module, `unplugin-atscript` responds with the same declarations `asc -f dts` would produce, rendered fresh from the `.as` source. Re-exported symbols stay fully typed in the bundled `.d.ts`. ```typescript // src/index.ts — a library entry re-exporting an Atscript model export { User } from './models/user.as' ``` ```typescript // tsdown.config.ts import { defineConfig } from 'tsdown' import atscript from 'unplugin-atscript/rolldown' export default defineConfig({ entry: ['src/index.ts'], dts: true, plugins: [atscript()], }) ``` The only requirement is that `atscript()` is present in the plugin list of the build that bundles declarations — the same plugin instance covers both the runtime and declaration passes. **Symptom of a missing plugin:** a re-exported `.as` symbol works as a *value* but loses all properties in a *type* position, and the emitted declaration imports it from a JS chunk: ```typescript // dist/index.d.mts — broken: no declaration exists for the .mjs chunk import { t as User } from './user-ABC123.mjs' ``` If you see this, the plugin was not wired into the declaration build (or you are on an `unplugin-atscript` version that predates declaration support). ## Bundling for Production `@atscript/typescript` ships two entries: the default `tsPlugin()` factory (build-time only) and `@atscript/typescript/utils` (runtime helpers like `Validator`, `ValidatorError`, `isAnnotatedType`). If you externalize `@atscript/typescript` to keep build-time code out of your bundle, you **must also externalize `@atscript/typescript/utils`** — otherwise the runtime helpers get inlined into your bundle while downstream consumers (`@atscript/moost-validator`, plugins, your own code) import them from `node_modules`. Two copies of `ValidatorError` means `instanceof` returns `false`, error interceptors silently miss validation errors, and they escape as 500s. The simplest fix is to externalize the whole namespace so every subpath comes along: ```typescript // rollup / rolldown / esbuild / rspack config export default { external: [/^@atscript\//], } ``` The same applies to subpath imports from any other `@atscript/*` package. ## Next Steps - [Configuration](/packages/typescript/configuration) — config file options - [CLI](/packages/typescript/cli) — build from the command line --- URL: "atscript.dev/packages/typescript/cli" LLMS_URL: "atscript.dev/packages/typescript/cli.md" --- # CLI The `asc` command compiles `.as` files using your project's Atscript configuration. ## Usage ```bash npx asc [options] ``` ## Options | Option | Description | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `-c, --config ` | Path to config file. If omitted, auto-detects `atscript.config.{js,mjs,cjs,ts,mts,cts}` walking up from the CWD (JS variants are checked first in each directory). | | `-f, --format ` | Output format (`dts` or `js`). If omitted, the TypeScript plugin emits `.d.ts` (its default branch); pass `-f js` to get `.js` output. | | `--noEmit` | Run diagnostics only, don't write files | | `--skipDiag` | Skip diagnostics, always emit files | | `--help` | Display help | ## Examples ```bash # Generate .d.ts files (default) npx asc # Generate .js files with runtime metadata npx asc -f js # Use a specific config file npx asc -c path/to/atscript.config.ts # Validate without writing files (CI/lint check) npx asc --noEmit # Emit without running diagnostics npx asc --skipDiag ``` The CLI logs created files, errors, and warnings with color-coded output. It exits with code `1` if any errors are found (unless `--skipDiag` is set). ::: tip If no config file is found, the CLI still runs with the TypeScript plugin enabled and emits `.d.ts` — so `npx asc` works out of the box. ::: ## Database Schema Sync The CLI also includes a `db sync` command for synchronizing your database schema with your `.as` definitions. ::: info The `db sync` subcommand is bundled with `@atscript/typescript` so that a single `asc` binary covers both codegen and schema sync, but it drives adapters from the separate [`@atscript/db-*`](https://db.atscript.dev) packages — the adapter (`@atscript/db-sqlite`, `@atscript/db-mongo`, …) must be installed and referenced from the config's `db` section for this command to do anything. Full reference at [db.atscript.dev](https://db.atscript.dev/). ::: ```bash npx asc db sync [options] ``` | Option | Description | | --------------------- | ------------------------------------- | | `-c, --config ` | Path to config file | | `--dry-run` | Show planned changes without applying | | `--yes` | Skip confirmation prompt (for CI/CD) | | `--force` | Re-sync even if schema hash matches | | `--safe` | Skip destructive operations (drops) | ```bash # Preview changes npx asc db sync --dry-run # Auto-approve for CI npx asc db sync --yes # Safe mode — only additive changes npx asc db sync --safe ``` This requires a `db` section in your config: ```typescript export default defineConfig({ // ... db: { adapter: '@atscript/db-sqlite', connection: './myapp.db', }, }) ``` See the [Schema Sync guide](https://db.atscript.dev/sync/) for full documentation. ## Next Steps - [Configuration](/packages/typescript/configuration) — config file options - [Build Setup](/packages/typescript/build-setup) — bundler integration - [Schema Sync](https://db.atscript.dev/sync/) — database migration guide --- URL: "atscript.dev/packages/typescript/code-generation" LLMS_URL: "atscript.dev/packages/typescript/code-generation.md" --- # Code Generation The TypeScript plugin generates code from `.as` files in two formats: `.d.ts` for type checking and `.js` for runtime metadata. ## DTS Format The default format. Generates TypeScript declaration files for static type checking and IDE support. Given this `.as` file: ```atscript @meta.description 'A product in the catalog' export interface Product { @meta.label 'Product Name' @expect.minLength 3 name: string @expect.min 0 price: number inStock: boolean } ``` The generated `.d.ts`: ```typescript export declare class Product { name: string price: number inStock: boolean static __is_atscript_annotated_type: true static type: TAtscriptTypeObject static metadata: TMetadataMap static validator: (opts?: Partial) => Validator static toJsonSchema: () => any static toExampleData?: () => any } export declare namespace Product { type DataType = Product } ``` Key points: - Interfaces become `declare class` with instance properties and static members - The static members provide access to type metadata, validation, JSON Schema, and example data at runtime - `toExampleData` is always optional — when `exampleData: true` is set in plugin options, it's rendered without deprecation; otherwise it's marked `@deprecated` - `Product.DataType` is a type alias for the data shape — useful for generic utilities ### Interface Extends When an interface uses `extends`, the first parent becomes the TypeScript `extends` target; properties from additional parents and own properties are merged into the class body. Inherited annotations are merged into the runtime metadata tree, so `metadata.get(...)` works on every inherited prop without you needing to think about how it was assembled. For types (not interfaces), a companion `namespace` carries the same `__is_atscript_annotated_type`, `type`, `metadata`, `validator`, `toJsonSchema`, and `toExampleData` static members. ## JS Format Generates JavaScript files with full runtime metadata. Use this when you need validation, metadata access, or serialization. The generated `.js` registers the type tree at module load via internal builder calls — you should not need to read or hand-write it. The `.d.ts` is the consumer-facing surface; the `.js` exists to make the static members on each generated class actually work at runtime. Set [`jsonSchema`](/packages/typescript/configuration#jsonschema) and [`exampleData`](/packages/typescript/configuration#exampledata) in plugin options to control what runtime helpers get pulled in. ## When to Use Which | | `.d.ts` | `.js` | | ---------------------- | ----------------- | ---------------------------- | | **Type checking** | Yes | Yes (with `.d.ts` alongside) | | **IDE IntelliSense** | Yes | Yes | | **Runtime validation** | Needs `.js` | Yes | | **Metadata access** | Needs `.js` | Yes | | **JSON Schema** | Needs `.js` | Yes | | **Bundle size** | Zero (types only) | Includes runtime metadata | In practice, use `.d.ts` during development (generated by VSCode extension on save) and `.js` for builds that need runtime features. ## Next Steps - [CLI](/packages/typescript/cli) — build `.as` files from the command line - [Type Definitions](/packages/typescript/type-definitions) — understand the generated type structure - [Validation](/packages/typescript/validation) — validate data against generated types --- URL: "atscript.dev/packages/typescript/configuration" LLMS_URL: "atscript.dev/packages/typescript/configuration.md" --- # Configuration ## Config File Create an `atscript.config.js` (or `.ts`) in your project root: ```javascript import { defineConfig } from '@atscript/core' import ts from '@atscript/typescript' export default defineConfig({ rootDir: 'src', format: 'dts', plugins: [ts()], }) ``` ### Options | Option | Type | Default | Description | | ------------------- | ------------------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `rootDir` | `string` | Config file's directory | Directory containing your `.as` files | | `include` | `string[]` | `['**/*.as']` | Glob patterns for `.as` files to compile. Exclude test-fixture directories (`**/test/**`, `**/__test__/**`, `**/__tests__/**`) — see [Testing Fixtures](/packages/typescript/testing-fixtures) for the dedicated fixture-compilation helper | | `exclude` | `string[]` | `['node_modules']` | Glob patterns to ignore | | `format` | `string` | Plugin-dependent | Default output format for [CLI](/packages/typescript/cli). The TypeScript plugin supports `'dts'` (type declarations) and `'js'` (runtime code); defaults to `dts` when omitted | | `unknownAnnotation` | `'error' \| 'warn' \| 'allow'` | `'error'` | How to handle annotations not defined in config | | `plugins` | `TAtscriptPlugin[]` | `[]` | Active plugins | | `annotations` | `object` | — | Custom annotation definitions (see [Custom Annotations](/packages/typescript/custom-annotations)) | ## Plugin Options The TypeScript plugin accepts options via `ts({ ... })`: ```javascript plugins: [ts({ jsonSchema: 'lazy' })] ``` ### `jsonSchema` Controls how JSON Schema support is handled in generated code. On the frontend, pulling in the `buildJsonSchema` function adds unnecessary weight when you don't need it — so Atscript lets you choose the right trade-off for your use case. | Value | Import added | `toJsonSchema()` behavior | | ------------------- | -------------------------- | --------------------------------------------------- | | `false` _(default)_ | None | Throws a runtime error | | `'lazy'` | `buildJsonSchema` imported | Computed on first call, cached | | `'bundle'` | None | Pre-computed at build time, embedded as static JSON | ```javascript // Default — no JSON schema overhead (best for frontend) plugins: [ts()] // Backend — lazy compute on demand plugins: [ts({ jsonSchema: 'lazy' })] // Backend — pre-compute at build time for fastest runtime plugins: [ts({ jsonSchema: 'bundle' })] ``` Individual interfaces can also opt into build-time embedding via the `@emit.jsonSchema` annotation, regardless of the global setting. See [JSON Schema](/packages/typescript/json-schema) for full usage details, annotation constraints, and examples. ### `exampleData` Controls whether generated types include a `toExampleData()` static method. When enabled, each generated class gets a method that creates example data using `@meta.example` annotations. | Value | `toExampleData()` behavior | | ------------------- | ------------------------------------------------------------------- | | `false` _(default)_ | Not rendered in `.js`; `.d.ts` marks it as optional + `@deprecated` | | `true` | Calls `createDataFromAnnotatedType(this, { mode: 'example' })` | ```javascript // Default — no example data method plugins: [ts()] // Enable — each type gets toExampleData() plugins: [ts({ exampleData: true })] ``` Unlike `toJsonSchema`, there is no caching — `toExampleData()` creates a new data object on each call. This is intentional since it acts as a factory function. ::: tip Manual use is always available Even with `exampleData: false`, you can import `createDataFromAnnotatedType` from `@atscript/typescript/utils` and call it directly. The config option only affects the _generated_ `.toExampleData()` method. ```typescript import { createDataFromAnnotatedType } from '@atscript/typescript/utils' import { Product } from './product.as' const example = createDataFromAnnotatedType(Product, { mode: 'example' }) ``` ::: ## The `atscript.d.ts` File When you run `asc -f dts`, an `atscript.d.ts` file is generated alongside your output. It declares the global `AtscriptMetadata` interface and `AtscriptPrimitiveTags` type — these provide TypeScript IntelliSense for all annotations and semantic type tags used in your project. Add it to your `tsconfig.json`: ```json { "include": ["src/**/*", "atscript.d.ts"] } ``` ::: warning Re-generate after config changes Run `npx asc -f dts` whenever you change your `atscript.config` — for example, after adding plugins, custom annotations, or new primitives. This regenerates `atscript.d.ts` so that your IDE picks up the updated annotation types and semantic tags. Without this step, you may see incorrect IntelliSense or missing type information when working with `.metadata` and `.type.tags`. ::: ## Next Steps - [CLI](/packages/typescript/cli) — build from the command line - [Testing Fixtures](/packages/typescript/testing-fixtures) — compile `.as` files in tests with `prepareFixtures()` - [Build Setup](/packages/typescript/build-setup) — bundler integration - [Custom Annotations](/packages/typescript/custom-annotations) — define your own annotation types - [Custom Primitives](/packages/typescript/custom-primitives) — define your own primitive extensions --- URL: "atscript.dev/packages/typescript/custom-annotations" LLMS_URL: "atscript.dev/packages/typescript/custom-annotations.md" --- # Custom Annotations You can define your own annotation types in `atscript.config.ts`. Custom annotations get full IntelliSense support, type checking, and are available in runtime metadata — just like the built-in `@meta.*`, `@expect.*`, and `@ui.*` annotations. For the full `AnnotationSpec` API — argument shapes, merge strategies, parse-time validation, AST-modifying hooks — see [Custom Annotations — Plugin Development](/plugin-development/annotation-system). ## Allowing Unknown Annotations By default, Atscript reports an error for annotations not defined in config. You can relax this: ```javascript import { defineConfig } from '@atscript/core' import ts from '@atscript/typescript' export default defineConfig({ rootDir: 'src', plugins: [ts()], unknownAnnotation: 'allow', // 'error' (default) | 'warn' | 'allow' }) ``` This is useful for quick prototyping, but for production projects, define your annotations explicitly for better tooling support. ## Quick Example Add annotations under the `annotations` key using `AnnotationSpec`: ```javascript import { defineConfig, AnnotationSpec } from '@atscript/core' import ts from '@atscript/typescript' export default defineConfig({ rootDir: 'src', plugins: [ts()], annotations: { grid: { hidden: new AnnotationSpec({ description: 'Hide column in data grid', nodeType: ['prop'], }), column: new AnnotationSpec({ description: 'Table column width', argument: { name: 'width', type: 'number', }, }), tag: new AnnotationSpec({ description: 'Display tag', multiple: true, mergeStrategy: 'append', argument: { name: 'value', type: 'string', }, }), }, }, }) ``` Then use them in `.as` files: ```atscript export interface User { @grid.hidden internalId: string @grid.column 200 @grid.tag 'primary' @grid.tag 'searchable' name: string } ``` Custom annotations appear in runtime metadata alongside built-in ones: ```typescript import { User } from './user.as' const nameProp = User.type.props.get('name') nameProp?.metadata.get('grid.column') // 200 nameProp?.metadata.get('grid.tag') // ['primary', 'searchable'] ``` ## AnnotationSpec Options | Option | Type | Description | | --------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `description` | `string` | Shown in IntelliSense hover | | `nodeType` | `string[]` | Where annotation can be applied: `'interface'`, `'type'`, `'prop'` | | `argument` | `object \| object[]` | Argument definition(s): `{ name, type, optional?, description?, values? }`. `type` must be one of `'string' \| 'number' \| 'boolean' \| 'ref' \| 'query'` | | `multiple` | `boolean` | Whether the annotation can appear more than once on the same node | | `mergeStrategy` | `'replace' \| 'append'` | How values combine during [annotation inheritance](/packages/typescript/annotations#annotation-inheritance). Default: `'replace'` | | `defType` | `string[]` | Restrict to the underlying definition type. Valid values: `'string'`, `'number'`, `'boolean'`, `'decimal'`, `'phantom'`, `'null'`, `'void'`, `'never'`, `'object'`, `'array'`, `'union'`, `'intersection'` | | `validate` | `function` | Custom validation function for complex checks | | `modify` | `function` | Hook to modify the AST after annotation is parsed | ## Next Steps - [Annotations](/packages/typescript/annotations) — built-in annotation types and inheritance rules - [Custom Primitives](/packages/typescript/custom-primitives) — define custom primitive extensions - [Configuration](/packages/typescript/configuration) — full config file reference --- URL: "atscript.dev/packages/typescript/custom-primitives" LLMS_URL: "atscript.dev/packages/typescript/custom-primitives.md" --- # Custom Primitives You can extend the built-in primitive types with your own semantic extensions via `atscript.config.ts`. Custom primitives work exactly like built-in ones — they appear in IntelliSense, carry validation constraints, and generate appropriate type tags. For the full primitive system (complex shapes, container primitives, semantic tags, phantom design), see [Custom Primitives — Plugin Development](/plugin-development/primitives-type-tags). ## Quick Example Add custom extensions under the `primitives` key in your config: ```javascript import { defineConfig } from '@atscript/core' import ts from '@atscript/typescript' export default defineConfig({ rootDir: 'src', plugins: [ts()], primitives: { string: { extensions: { url: { type: 'string', documentation: 'URL format', annotations: { 'expect.pattern': { pattern: '^https?://.+$', message: 'Invalid URL', }, }, }, slug: { type: 'string', documentation: 'URL-safe slug', annotations: { 'expect.pattern': { pattern: '^[a-z0-9-]+$', message: 'Invalid slug', }, }, }, }, }, number: { extensions: { percentage: { type: 'number', documentation: 'Percentage value (0–100)', annotations: { 'expect.min': 0, 'expect.max': 100, }, }, }, }, }, }) ``` Then use them in `.as` files with dot notation: ```atscript export interface Page { url: string.url slug: string.slug completeness: number.percentage } ``` The validator will automatically enforce the constraints — no `@expect.*` annotations needed. ## What You Can Define Each primitive extension supports: | Field | Description | | --------------- | ------------------------------------------------------------------------------------------------------------- | | `type` | The base type (`'string'`, `'number'`, `'boolean'`, `'phantom'`, etc.) — **inherited** from parent if omitted | | `documentation` | Description shown in IntelliSense — inherited from parent if omitted | | `extensions` | Nested sub-extensions (e.g., `number.int.positive`) | | `isContainer` | If `true`, the primitive cannot be used directly — one of its extensions must be chosen | | `tags` | Array of semantic tags (e.g., `['created']`) — inherited from parent, used by DB adapters and runtime tools | | `annotations` | Implicit annotations applied to any field using this primitive (e.g., `{ 'expect.int': true, 'expect.min': 0 }`) — merged with parent's annotations | ::: warning isContainer When `isContainer: true`, the primitive itself cannot be used as a type — only its extensions are valid: ```atscript field: ui // ✗ Error — ui is a container, must use an extension field: ui.action // ✓ Correct — uses the extension ``` ::: ::: tip Inheritance Extensions automatically inherit `type`, `documentation`, `annotations`, and `tags` from their parent primitive. You only need to specify fields you want to override or add. This is how built-in extensions like `string.email` work — they inherit `type: 'string'` from `string` and only add their own `expect.pattern` annotation. ::: ## Phantom Namespaces You can define entirely new primitive namespaces with `type: 'phantom'` to create families of non-data UI elements. These are omitted from TypeScript types and skipped by validation, but discoverable at runtime — perfect for form renderers. ```javascript primitives: { ui: { type: 'phantom', isContainer: true, documentation: 'Non-data UI elements for form rendering', extensions: { action: { documentation: 'An action element (button, link)' }, divider: { documentation: 'A visual divider between form sections' }, paragraph: { documentation: 'A block of informational text' }, }, }, } ``` ```atscript export interface CheckoutForm { @meta.label 'Email' email: string.email @meta.label 'Shipping Address' shippingHeader: ui.divider @meta.label 'Street' street: string } ``` Annotations are always namespaced — use `@meta.label` (built-in), not bare `@label`. Custom annotations follow the same rule (`@grid.column`, `@form.section`, etc.). ## Next Steps - [Primitives](/packages/typescript/primitives) — built-in primitive types and semantic extensions - [Custom Annotations](/packages/typescript/custom-annotations) — define custom annotation types - [Configuration](/packages/typescript/configuration) — full config file reference --- URL: "atscript.dev/packages/typescript/imports-exports" LLMS_URL: "atscript.dev/packages/typescript/imports-exports.md" --- # Imports & Exports Atscript imports and exports work similar to TypeScript with some specific limitations and rules. ## Importing .as Files in TypeScript TypeScript files **must** include the `.as` extension: ```typescript // app.ts import { User, UserID, Status } from './user.as' import { Product } from '../models/product.as' // Use as both type and runtime object const user: User = { id: '1', name: 'John' } const validator = User.validator() const metadata = User.metadata ``` ### Importing from Packages in TypeScript When importing `.as` types from an npm package in TypeScript, use the `.as` extension (added automatically by the Atscript compiler when generating `.js`/`.d.ts` output): ```typescript // app.ts — importing .as types from a published package import { User } from '@my-org/models/user.as' import { Product } from 'shared-types/product.as' ``` The bundler (`unplugin-atscript`) and TypeScript resolve these through the package's `exports` field: - **Bundlers** use the `"import"` condition to find the compiled `.as.mjs`/`.as.js` - **TypeScript** uses the `"types"` condition to find the `.as.d.ts` declarations If the package doesn't use `exports`, bundlers resolve the `.as` file directly from `node_modules` and compile it on the fly via the unplugin `load()` hook. ## Setup for TypeScript Integration For TypeScript to import `.as` files: 1. **With VSCode Extension**: Automatically generates `.as.d.ts` files on save 2. **With CLI**: Run `asc -f dts` to generate TypeScript definitions 3. **With Bundler**: Use `unplugin-atscript` for automatic compilation Example generated structure: ``` src/ user.as # Source file user.as.js # Generated JavaScript (with asc -f js) user.as.d.ts # Generated TypeScript definitions app.ts # Can import from './user.as' ``` ## Next Steps - [Primitives](/packages/typescript/primitives) — Primitive types and extensions - [Annotations](/packages/typescript/annotations) — Metadata system --- URL: "atscript.dev/packages/typescript/installation" LLMS_URL: "atscript.dev/packages/typescript/installation.md" --- # Installation ::: info This installation guide is specific to TypeScript/JavaScript. Support for other languages is planned through community-contributed plugins. ::: ## Prerequisites - Node.js 16 or higher - npm, pnpm, yarn, or bun package manager ## Install Atscript ::: code-group ```bash [npm] npm install @atscript/typescript npm install -D @atscript/core ``` ```bash [pnpm] pnpm add @atscript/typescript pnpm add -D @atscript/core ``` ```bash [yarn] yarn add @atscript/typescript yarn add -D @atscript/core ``` ```bash [bun] bun add @atscript/typescript bun add -D @atscript/core ``` ::: ### What Gets Installed - **`@atscript/core`** — parser, AST, and plugin system (dev dependency — build-time only) - **`@atscript/typescript`** — TypeScript/JavaScript code generation, the `asc` CLI, and runtime utilities (Validator, JSON Schema, serialization, type traversal) ### Optional Packages - **`unplugin-atscript`** — Build tool integration (Vite, Webpack, Rollup, esbuild, Rolldown, Rspack, Farm). See [Build Setup](/packages/typescript/build-setup). - **`@atscript/moost-validator`** — Moost framework validation integration. See [Moost Validator](/packages/moost-validator/). - **Database integrations** — `@atscript/db`, `@atscript/db-sqlite`, `@atscript/db-mongo`, `@atscript/db-mysql`, `@atscript/moost-db`, etc. live in a [separate repo](https://db.atscript.dev) and are installed alongside `@atscript/typescript` when needed. ## Verify Installation After installation, verify that the `asc` compiler is available: ::: code-group ```bash [npx] npx asc --help ``` ```bash [pnpm] pnpm exec asc --help ``` ```bash [yarn] yarn asc --help ``` ```bash [bunx] bunx asc --help ``` ::: You should see the available command options: ``` Options: -c, --config Path to config file -f, --format Output format (dts or js) --noEmit Only run diagnostics, no file output --skipDiag Skip diagnostics, always emit --help Show help ``` ## VSCode Extension (Recommended) For the best development experience, install the [Atscript VSCode extension](https://marketplace.visualstudio.com/items?itemName=moost.atscript-as): 1. Open VSCode 2. Go to Extensions (Cmd/Ctrl + Shift + X) 3. Search for "Atscript" 4. Install the extension by Moost The extension provides: - Syntax highlighting for `.as` files - IntelliSense support - Error checking - Auto-generation of `.d.ts` files on save ## AI Agent Skill Atscript ships a unified skill for AI coding agents (Claude Code, Cursor, Windsurf, Codex, etc.) covering every `@atscript/*` package with progressive-disclosure reference docs. ```bash npx skills add moostjs/atscript ``` Restart your agent after installing. Learn more about AI agent skills at [skills.sh](https://skills.sh). ## Next Steps - [Quick Start](/packages/typescript/quick-start) — Create your first .as file - [Configuration](/packages/typescript/configuration) — Set up the config file --- URL: "atscript.dev/packages/typescript/interfaces-types" LLMS_URL: "atscript.dev/packages/typescript/interfaces-types.md" --- # Interfaces & Types Atscript provides TypeScript-like syntax with annotations and semantic types. ## TypeScript Usage In TypeScript, use type aliases as both types and validators: ```typescript import { Username } from './types.as' const name: Username = 'john_doe' const validator = Username.validator() if (validator.validate(input, true)) { // input is Username } ``` ## Next Steps - [Imports & Exports](/packages/typescript/imports-exports) — Module system - [Primitives](/packages/typescript/primitives) — Semantic types - [Annotations](/packages/typescript/annotations) — Metadata system - [Ad-hoc Annotations](/packages/typescript/ad-hoc-annotations) — Annotate existing types without modification --- URL: "atscript.dev/packages/typescript/json-schema" LLMS_URL: "atscript.dev/packages/typescript/json-schema.md" --- # JSON Schema Atscript types can be converted to [JSON Schema](https://json-schema.org/) for use with API documentation tools, form generators, or any system that consumes JSON Schema. ## Enabling JSON Schema By default, JSON Schema support is disabled (`jsonSchema: false`) to keep generated output lightweight. To enable it, set the `jsonSchema` plugin option — see [Configuration — `jsonSchema`](/packages/typescript/configuration#jsonschema) for the available modes and examples. ## Usage Two ways to get a JSON Schema from an Atscript type: ```typescript import { Product } from './product.as' import { buildJsonSchema } from '@atscript/typescript/utils' // Option 1: from the generated type directly (requires jsonSchema: 'lazy' or 'bundle') const schema = Product.toJsonSchema() // Option 2: using the standalone function (always available) const schema = buildJsonSchema(Product) ``` The `.toJsonSchema()` method on generated types requires the `jsonSchema` plugin option to be set to `'lazy'` or `'bundle'` — otherwise it throws a runtime error. Alternatively, add the [`@emit.jsonSchema`](#per-interface-override-emit-jsonschema) annotation to individual interfaces. ::: tip Manual use is always available Even with `jsonSchema: false`, you can import `buildJsonSchema` from `@atscript/typescript/utils` and call it directly. The config option only affects the _generated_ `.toJsonSchema()` method — the standalone function always works. ```typescript import { buildJsonSchema } from '@atscript/typescript/utils' import { Product } from './product.as' const schema = buildJsonSchema(Product) // works regardless of config ``` ::: ## Per-Interface Override: `@emit.jsonSchema` The `@emit.jsonSchema` annotation forces build-time JSON Schema embedding for a specific interface, regardless of the global `jsonSchema` setting. This is useful when you've disabled JSON Schema globally but need it for select types: ```atscript @emit.jsonSchema export interface ApiResponse { status: string @expect.minLength 1 message: string } ``` `ApiResponse.toJsonSchema()` will return the pre-computed schema even if `jsonSchema: false` is set in the plugin config. The annotation can only be applied to interfaces (top-level). ## Annotation Constraints Annotations from `.as` files are translated into JSON Schema constraints: | Annotation | JSON Schema | Notes | | ------------------- | ------------------------ | -------------------------------------------------------------- | | `@meta.required` | `minLength: 1` | For string fields. Ensures at least one non-whitespace char | | `@expect.minLength` | `minLength` / `minItems` | `minLength` for strings, `minItems` for arrays | | `@expect.maxLength` | `maxLength` / `maxItems` | `maxLength` for strings, `maxItems` for arrays | | `@expect.min` | `minimum` | | | `@expect.max` | `maximum` | | | `@expect.int` | `type: 'integer'` | Changes `number` to `integer` | | `@expect.pattern` | `pattern` / `allOf` | Single pattern uses `pattern`, multiple become `allOf` entries | ::: warning Pattern properties are dropped Atscript's wildcard-key syntax (`[/regex/]: Type`) has no equivalent in `buildJsonSchema` output — those entries are silently omitted from the generated schema. If you need pattern-keyed maps in JSON Schema, model them as `additionalProperties` with an external post-process step. ::: ## Example Given this `.as` file: ```atscript export interface Product { @expect.minLength 3 @expect.maxLength 100 name: string @expect.min 0 price: number tags: string[] } ``` `buildJsonSchema(Product)` produces: ```json { "type": "object", "properties": { "name": { "type": "string", "minLength": 3, "maxLength": 100 }, "price": { "type": "number", "minimum": 0 }, "tags": { "type": "array", "items": { "type": "string" } } }, "required": ["name", "price", "tags"] } ``` ## Named Types: `$defs` and `$ref` Types compiled from `.as` files carry a stable `id` (the type name from the source file). When `buildJsonSchema` encounters named object types nested inside other types (e.g., union items, object properties), it automatically extracts them into `$defs` and references them via `$ref`: ```atscript interface Cat { petType: 'cat' name: string } interface Dog { petType: 'dog' breed: string } export type CatOrDog = Cat | Dog ``` `buildJsonSchema(CatOrDog)` produces: ```json { "$defs": { "Cat": { "type": "object", "properties": { "petType": { "const": "cat", "type": "string" }, "name": { "type": "string" } }, "required": ["petType", "name"] }, "Dog": { "type": "object", "properties": { "petType": { "const": "dog", "type": "string" }, "breed": { "type": "string" } }, "required": ["petType", "breed"] } }, "oneOf": [{ "$ref": "#/$defs/Cat" }, { "$ref": "#/$defs/Dog" }], "discriminator": { "propertyName": "petType", "mapping": { "cat": "#/$defs/Cat", "dog": "#/$defs/Dog" } } } ``` Key behaviors: - Only **named object types** (those with an `id`) are extracted to `$defs`. Primitives, unions, and arrays stay inline. - The **root type** is never extracted — it IS the schema. - If the same named type is referenced multiple times, it appears once in `$defs` and all occurrences become `$ref`. - Types without an `id` (e.g., inline anonymous objects, hand-built types via `defineAnnotatedType()`) produce inline schemas as before. ### Programmatic `id` For types built programmatically (not compiled from `.as` files), you can assign an `id` using the builder API: ```typescript import { defineAnnotatedType } from '@atscript/typescript/utils' const address = defineAnnotatedType('object') .prop('street', defineAnnotatedType().designType('string').$type) .prop('city', defineAnnotatedType().designType('string').$type) .id('Address') ``` ## Discriminated Unions When a union type consists entirely of objects that share a common property with distinct literal values, `buildJsonSchema` automatically detects it as a discriminated union and emits `oneOf` with a `discriminator` object instead of plain `anyOf`. When the union items have named types, the discriminator mapping uses `$ref` paths into `$defs` (as shown above). Detection is fully automatic — no annotations required. The rules are: - All union items must be objects - Exactly **one** property must have a `const` literal value across **all** items - All literal values for that property must be **distinct** If these conditions aren't met, the union falls back to `anyOf`. ::: tip Advanced: `detectDiscriminator` For tooling that needs to inspect discriminated unions without going through full JSON Schema generation, `@atscript/typescript/utils` exports a public helper: ```typescript import { detectDiscriminator } from '@atscript/typescript/utils' const info = detectDiscriminator(unionType.type.items) // info is undefined when no discriminator is detected, // or { propertyName, mapping: Record } ``` This is the same detection used internally by `buildJsonSchema`. ::: ## Converting from JSON Schema You can also convert a JSON Schema object back into an Atscript annotated type using `fromJsonSchema`: ```typescript import { fromJsonSchema, buildJsonSchema } from '@atscript/typescript/utils' const schema = { type: 'object', properties: { name: { type: 'string', minLength: 3 }, age: { type: 'number', minimum: 0 }, }, required: ['name', 'age'], } const type = fromJsonSchema(schema) // The result is a fully functional annotated type type.validator().validate({ name: 'Alice', age: 30 }) // passes // Round-trip: buildJsonSchema(fromJsonSchema(schema)) preserves the schema const roundTripped = buildJsonSchema(type) ``` `fromJsonSchema` supports the full subset of JSON Schema produced by `buildJsonSchema`, including: - Object types with `properties` and `required` - Arrays with `items`, `minItems`, `maxItems` - Tuples (array with `items` as an array and `additionalItems: false`) - Unions (`anyOf`, `oneOf`) - Intersections (`allOf`) - Primitives (`string`, `number`, `integer`, `boolean`, `null`) - Literals (`const`) - Enums (`enum` — converted to union of literals) - Constraints: `minLength`, `maxLength`, `minimum`, `maximum`, `pattern` - `$ref` / `$defs` — references are resolved automatically from `$defs` or `definitions` ::: tip Use case: external schemas `fromJsonSchema` is useful for importing type definitions from external JSON Schema sources (OpenAPI specs, form generators, etc.) and using them with Atscript's validator at runtime. ::: ::: warning Unsupported features Features like `not`, `if/then/else`, `patternProperties`, and `additionalProperties` have no Atscript equivalent and are silently ignored. ::: ## Merging Schemas (OpenAPI / Swagger) `mergeJsonSchemas` combines multiple annotated types into a single schema map with shared `$defs` — useful for building OpenAPI `components/schemas`: ```typescript import { mergeJsonSchemas } from '@atscript/typescript/utils' import { CatOrDog } from './pets.as' import { Order } from './orders.as' const merged = mergeJsonSchemas([CatOrDog, Order]) // merged.schemas.CatOrDog — the CatOrDog schema (oneOf with $ref) // merged.schemas.Order — the Order schema // merged.$defs: { Cat, Dog, ... } — shared definitions, deduplicated ``` Each type passed to `mergeJsonSchemas` must have an `id` (all types compiled from `.as` files do). The function: 1. Calls `buildJsonSchema` on each type 2. Hoists all `$defs` into a shared pool, deduplicating by name 3. Returns individual schemas (without their `$defs`) alongside the merged definitions ## Next Steps - [Validation](/packages/typescript/validation) — runtime validation with type guards - [Serialization](/packages/typescript/serialization) — serialize types for transport --- URL: "atscript.dev/packages/typescript/metadata-export" LLMS_URL: "atscript.dev/packages/typescript/metadata-export.md" --- # Metadata All annotations from `.as` files are preserved as runtime metadata on generated types. This is what makes Atscript more than just a type system — annotations like labels, descriptions, and validation rules are accessible in your TypeScript code. ## Accessing Metadata Every generated type has a `metadata` field — a typed `Map` keyed by annotation names: ```typescript import { Product } from './product.as' // Top-level annotations (on the interface itself) Product.metadata.get('meta.description') // 'A product in the catalog' // Property-level annotations const nameProp = Product.type.props.get('name') nameProp?.metadata.get('meta.label') // 'Product Name' nameProp?.metadata.get('expect.minLength') // { length: 3, message?: string } ``` The metadata map is typed via the global `AtscriptMetadata` interface, so you get IntelliSense for annotation keys and their value types. ## Iterating Properties Walk all properties and their metadata using the `props` map: ```typescript import { Product } from './product.as' for (const [key, prop] of Product.type.props.entries()) { const label = prop.metadata.get('meta.label') || key const required = !prop.optional console.log(`${label} (${key}): ${required ? 'required' : 'optional'}`) } ``` ## Tags Semantic types like `string.email` or `number.positive` produce tags that are available at runtime: ```typescript import { User } from './user.as' const emailProp = User.type.props.get('email') emailProp?.type.tags // Set { 'email', 'string' } ``` Tags let you make runtime decisions based on semantic types — for example, rendering an email input vs a plain text input. ## Nested Types Every nested node — array element, union member, nested-object property — is itself a `TAtscriptAnnotatedType` with its own `.metadata.get(...)`. Reach into the right field on the parent (`type.of` for arrays, `type.items` for unions/intersections/tuples, `type.props.get(...)` for objects) and call `metadata.get` on what you find: ```typescript import { User } from './models.as' const addressProp = User.type.props.get('address') if (addressProp?.type.kind === 'object') { const cityProp = addressProp.type.props.get('city') cityProp?.metadata.get('meta.label') } ``` Use [`forAnnotatedType()`](/packages/typescript/type-definitions#forAnnotatedType) to traverse nested types generically — see [Type Traversal](/packages/typescript/type-definitions#type-traversal) for recursive walking patterns and practical examples. ## Practical Example: Build A Field List ```typescript import { Product } from './product.as' function buildFieldList() { if (Product.type.kind !== 'object') return [] return Array.from(Product.type.props.entries()).map(([key, prop]) => ({ name: key, label: prop.metadata.get('meta.label') || key, required: !prop.optional, placeholder: prop.metadata.get('ui.placeholder'), sensitive: prop.metadata.get('meta.sensitive') || false, readonly: prop.metadata.get('meta.readonly') || false, })) } const fields = buildFieldList() // [{ name: 'name', label: 'Product Name', required: true, ... }, ...] ``` For more advanced traversal patterns — recursive walking, flattening nested types, collecting metadata across the type tree — see [Type Traversal](/packages/typescript/type-definitions#type-traversal). ## Next Steps - [Validation](/packages/typescript/validation) — annotations drive validation rules automatically - [Type Definitions](/packages/typescript/type-definitions) — the annotated type system in depth - [Serialization](/packages/typescript/serialization) — serialize types with metadata for transport --- URL: "atscript.dev/packages/typescript/primitives" LLMS_URL: "atscript.dev/packages/typescript/primitives.md" --- # Primitives ## Type Tags in TypeScript When compiled, semantic types preserve their extensions as tags: ```typescript // Generated TypeScript export declare class User { email: string /* email */ age: number /* int */ // ... } // Runtime metadata access const emailProp = User.type.props.get('email') const emailTags = emailProp?.type.tags // ['email', 'string'] const ageProp = User.type.props.get('age') const ageTags = ageProp?.type.tags // ['int', 'number'] ``` These tags can be used by: - Validators to apply appropriate rules - UI generators to choose correct input types - API documentation generators - Database schema generators ## DOs and DON'Ts - **DO treat semantic types as format checks, not semantic truth.** They validate with regex patterns: `string.date` accepts `99/99/9999` (it matches the `MM/DD/YYYY` shape), and `string.email` uses a pragmatic pattern, not full RFC 5322. Add application-level checks when calendar validity or deliverability actually matters. - **DON'T use `@expect.minLength 1` when you mean `string.required`.** `string.required` rejects whitespace-only strings like `" "`; `minLength 1` accepts them. - **DON'T stack `@expect.pattern` on a semantic type to broaden it.** Patterns are conjunctive — every pattern must match — so an extra pattern on `string.email` only narrows it further. For alternative formats, define a [custom primitive](/packages/typescript/custom-primitives) with a single alternation regex (`^(?:formA|formB)$`). ## Next Steps - [Annotations](/packages/typescript/annotations) — add metadata to your types - [Custom Primitives](/packages/typescript/custom-primitives) — define your own primitive extensions - [Type Definitions](/packages/typescript/type-definitions) — how tags and metadata are accessed at runtime --- URL: "atscript.dev/packages/typescript/quick-start" LLMS_URL: "atscript.dev/packages/typescript/quick-start.md" --- # Quick Start A practical first run for app developers evaluating Atscript. ::: tip What You Will Build In this guide, you will: 1. define one `.as` model 2. generate the TypeScript and runtime files Atscript needs 3. validate invalid input against that model 4. wire Atscript into a real build once the basics make sense ::: ::: info Current Scope Atscript is language-agnostic by design, but TypeScript is the first supported target today. If you are evaluating Atscript right now, this is the best place to start. ::: ## Phase 1: Prove The Workflow ### 1. Install Packages ```bash npm install @atscript/typescript npm install -D @atscript/core ``` `@atscript/typescript` ships the runtime utilities you will use from application code. `@atscript/core` provides the compiler and plugin foundation at build time. ### 2. Create A `.as` File Create `src/user.as`: ```atscript export interface User { @meta.label 'User Name' @expect.minLength 2 name: string @meta.label 'Email Address' email: string.email @expect.min 0 age: number.int } ``` This file already contains: - the data shape - validation rules - metadata that runtime tools can read later ### 3. Add A Minimal Config Create `atscript.config.js` in your project root: ```javascript import { defineConfig } from '@atscript/core' import ts from '@atscript/typescript' export default defineConfig({ rootDir: 'src', plugins: [ts()], }) ``` For this quick start, the default TypeScript plugin is enough. ### 4. Generate The Files Atscript Uses Run: ```bash npx asc -f dts npx asc -f js ``` This gives you: - `src/user.as.d.ts` for TypeScript and editor support - `src/user.as.js` for runtime use - `atscript.d.ts` for typed annotation keys in your project Add `atscript.d.ts` to your `tsconfig.json`: ```json { "include": ["src/**/*", "atscript.d.ts"] } ``` ### 5. Try The Runtime In One Small Script Create `src/demo.mjs`: ```javascript import { User } from './user.as.js' const validator = User.validator() const input = { name: 'A', email: 'not-an-email', age: -5, } if (validator.validate(input, true)) { console.log('Valid user:', input) } else { console.log('Validation errors:') for (const err of validator.errors) { console.log(`- ${err.path}: ${err.message}`) } } ``` Run it: ```bash node src/demo.mjs ``` Expected output: ```text Validation errors: - name: Expected minimum length 2 - email: Expected valid email - age: Expected minimum value 0 ``` At this point you have already proven the core Atscript workflow: - define the model once - generate runtime and type files - validate data from that model ### 6. Use The Model In TypeScript Once `src/user.as.d.ts` exists, your TypeScript code can import from `./user.as`: ```typescript import { User } from './user.as' const user: User = { name: 'Ada', email: 'ada@example.com', age: 28, } const validator = User.validator() const emailProp = User.type.props.get('email') console.log(emailProp?.metadata.get('meta.label')) validator.validate(user) ``` Use `./user.as.js` when you want to run the generated JavaScript directly without a bundler. Use `./user.as` in TypeScript source once the declaration file exists and your app build knows how to compile the `.as` runtime file. ## Phase 2: Integrate Atscript Into Your Build Once the CLI flow makes sense, add Atscript to your normal app build so you do not need to run `asc -f js` by hand. Install the bundler plugin: ```bash npm install -D unplugin-atscript ``` Vite example: ```javascript // vite.config.js import { defineConfig } from 'vite' import atscript from 'unplugin-atscript/vite' export default defineConfig({ plugins: [atscript()], }) ``` Now your app code can import `.as` files directly and let the build handle compilation. See [Build Setup](/packages/typescript/build-setup) for Rollup, esbuild, Webpack, Rspack, and more. ## Optional But Helpful - Install the [Atscript VSCode extension](https://marketplace.visualstudio.com/items?itemName=moost.atscript-as) for syntax highlighting, diagnostics, and automatic `.as.d.ts` generation on save - Read [Imports & Exports](/packages/typescript/imports-exports) if you want a clearer picture of how `.as`, `.as.d.ts`, and `.as.js` fit together ## Next Steps - [Build Setup](/packages/typescript/build-setup) — integrate Atscript into your real app build - [Validation Guide](/packages/typescript/validation) — common validation tasks in application code - [Interfaces & Types](/packages/typescript/interfaces-types) — the core `.as` syntax - [Annotations Guide](/packages/typescript/annotations) — practical metadata and validation annotations --- URL: "atscript.dev/packages/typescript/serialization" LLMS_URL: "atscript.dev/packages/typescript/serialization.md" --- # Serialization The serialization API converts runtime annotated types to and from a plain JSON format. This enables transferring type definitions between backend and frontend, storing them in databases, or caching compiled types. ```typescript import { serializeAnnotatedType, deserializeAnnotatedType, buildJsonSchema, } from '@atscript/typescript/utils' ``` ## Purpose Serialize type definitions on the server and send them to the client. The client deserializes them and uses them for validation, live form tools, or schema-driven UI helpers without bundling the original `.as` files. ## Basic Usage ```typescript import { Product } from './product.as' // Serialize to a JSON-safe object const serialized = serializeAnnotatedType(Product) const json = JSON.stringify(serialized) // ... transmit, store, or cache ... // Deserialize back to a live type const restored = deserializeAnnotatedType(JSON.parse(json)) // The restored type is fully functional restored.validator().validate(data) buildJsonSchema(restored) ``` ## Deserialized Types Are Live A deserialized type is a fully functional `TAtscriptAnnotatedType`: - `.validator()` creates a working `Validator` instance - Works with `buildJsonSchema()` and `forAnnotatedType()` - `isAnnotatedType()` returns `true` - Metadata is accessible via `.metadata.get()` - The `id` field (type name) is preserved through serialization, so `buildJsonSchema` will still produce `$defs`/`$ref` for deserialized types ## Versioning The serialized output includes a `$v` field with the format version (currently `2`). If the format changes in a future release, `deserializeAnnotatedType()` will throw when it encounters an incompatible version, so you know to re-serialize from the source types. ```typescript import { SERIALIZE_VERSION } from '@atscript/typescript/utils' // SERIALIZE_VERSION === 2 ``` ## FK References FK references (`.ref`) are stripped from serialized output by default. Pass `refDepth: 1` to include immediate refs when the client needs to discover the target table (e.g., for value-help dropdowns on FK fields). Integer values expand `N` full levels; a fractional `.5` part (e.g. `refDepth: 0.5` or `refDepth: 1.5`) emits a shallow `{ id, metadata }` target at the tail level instead of the full body — handy for keeping payloads small when the client only needs the target's identity. For the full ref-control semantics, see the [`@atscript/db` docs](https://db.atscript.dev). ## Filtering Annotations Use `TSerializeOptions` to control which annotations are included in the output. This is useful for stripping sensitive or server-only metadata before sending types to the client. **Strip specific annotations:** ```typescript const serialized = serializeAnnotatedType(Product, { ignoreAnnotations: ['db.table', 'db.mongo.collection'], }) ``` **Transform annotations with a callback:** ```typescript const serialized = serializeAnnotatedType(Product, { processAnnotation({ key, value, path, kind }) { // Only keep meta.*, expect.*, and ui.* annotations if (key.startsWith('meta.') || key.startsWith('expect.') || key.startsWith('ui.')) { return { key, value } } // Return undefined to strip }, }) ``` The `processAnnotation` callback receives: - `key` — annotation name (e.g. `'meta.label'`) - `value` — annotation value - `path` — property path as a `string[]` array (e.g. `['address', 'city']`) - `kind` — type kind at this node (`''`, `'object'`, `'array'`, etc.) ## Example: Server-Driven Field Tools A practical use case: the server serializes a type definition and the client uses it to build a field list with labels and placeholders. **Server** (Express endpoint): ```typescript import { User } from './user.as' import { serializeAnnotatedType } from '@atscript/typescript/utils' app.get('/api/form/user', (req, res) => { const schema = serializeAnnotatedType(User, { ignoreAnnotations: ['db.table', 'db.mongo.collection'], // strip server-only metadata }) res.json(schema) }) ``` **Client** (Vue component): ```vue ``` The field list, labels, and placeholders are all driven by annotations defined in the `.as` file, so the client does not need to duplicate that configuration. ## Next Steps - [Type Definitions](/packages/typescript/type-definitions) — the annotated type system - [Validation](/packages/typescript/validation) — validate data against types - [Metadata](/packages/typescript/metadata-export) — access annotations at runtime --- URL: "atscript.dev/packages/typescript/testing-fixtures" LLMS_URL: "atscript.dev/packages/typescript/testing-fixtures.md" --- # Testing with `.as` fixtures When your tests depend on Atscript-compiled types — annotated interfaces with `@db.*`, `@meta.id`, custom annotations, or anything runtime metadata — compile those fixtures at test time from real `.as` files rather than hand-writing `defineAnnotatedType` (`$()`) builder chains. The builder API is designed for generated code and has subtle behaviors (metadata propagation, lazy flatten timing) that make hand-written fixtures unreliable. `@atscript/typescript/test-utils` ships a single helper, `prepareFixtures()`, that compiles a set of `.as` files under a directory and writes the generated `.as.js` / `.as.d.ts` artifacts next to their sources. ## Quick start ```ts import path from 'path' import { beforeAll, describe, it } from 'vitest' import { prepareFixtures } from '@atscript/typescript/test-utils' const fixturesDir = path.join(__dirname, 'fixtures') beforeAll(() => prepareFixtures({ rootDir: fixturesDir, entries: ['user.as'] })) it('validates compiled user', async () => { const { User } = await import('./fixtures/user.as.js') // ... }) ``` `tsPlugin()` is injected for you — pass only the additional plugins your tests need. For example, `dbPlugin()` from [`@atscript/db`](https://db.atscript.dev) (sibling repo — install separately): ```ts import { dbPlugin } from '@atscript/db/plugin' await prepareFixtures({ rootDir: fixturesDir, entries: ['order.as'], plugins: [dbPlugin()], }) ``` ## Signature ```ts interface PrepareFixturesOptions { rootDir: string // absolute path to the fixtures directory plugins?: TAtscriptPlugin[] // additional plugins (tsPlugin is auto-injected) include?: string[] // glob patterns; default ['**/*.as'] entries?: string[] // explicit filenames; takes precedence over include formats?: Array<'js' | 'dts'> // default ['js', 'dts'] } function prepareFixtures(options: PrepareFixturesOptions): Promise ``` Defaults applied when the option is omitted: | Option | Default | Notes | | --------- | --------------- | ----------------------------------------------------------------- | | `plugins` | `[]` | `tsPlugin()` is always injected before any caller-supplied plugin | | `include` | `['**/*.as']` | Used only when `entries` is not provided | | `entries` | `undefined` | When set, narrows compilation to exactly those filenames | | `formats` | `['js', 'dts']` | Both formats generated each call; written only when content changed | Generated `.as.js` / `.as.d.ts` artifacts are intended as test-run outputs — gitignore them (`*.as.js`, `*.as.d.ts`). `prepareFixtures()` recompiles them each run and rewrites only the ones whose content changed, so up-to-date artifacts keep their mtime. ## Production `.as` vs test fixtures If your project has both production `.as` files (under `src/`) and `.as` test fixtures, separate their lifecycles: **Production `.as`** — point `atscript.config.ts` at the production globs and add a `postinstall` script so `.as.d.ts` exists on fresh install: ```ts // atscript.config.ts import { defineConfig } from '@atscript/core' import tsPlugin from '@atscript/typescript' export default defineConfig({ include: ['src/**/*.as'], exclude: ['**/test/**', '**/__test__/**', '**/__tests__/**'], plugins: [tsPlugin()], }) ``` ```json // package.json { "scripts": { "postinstall": "asc" } } ``` **Test fixtures** — compile in test-setup hooks with whatever plugin set the tests need (often a different set than production — feature-flag plugins, WIP plugins, or test-only mocks): ```ts // src/__tests__/setup.ts import path from 'path' import { prepareFixtures } from '@atscript/typescript/test-utils' import { dbPlugin } from '@atscript/db/plugin' beforeAll(() => prepareFixtures({ rootDir: path.join(__dirname, 'fixtures'), plugins: [dbPlugin()], }) ) ``` ## See also - [Configuration](./configuration) — `atscript.config.ts` shape and include/exclude globs - [CLI](./cli) — the `asc` command used for production `.as` type generation --- URL: "atscript.dev/packages/typescript/type-definitions" LLMS_URL: "atscript.dev/packages/typescript/type-definitions.md" --- # Type Definitions Every Atscript type compiles to a runtime `TAtscriptAnnotatedType` object that carries the type structure, metadata, and a validator factory. This page covers the runtime type system, automatic DataType inferring, and type traversal. All runtime utilities are imported from `@atscript/typescript/utils`. ## The Annotated Type `TAtscriptAnnotatedType` is the core runtime representation of any Atscript type: ```typescript interface TAtscriptAnnotatedType> { __is_atscript_annotated_type: true type: T // the type definition (object, array, union, etc.) metadata: TMetadataMap // annotation values validator: (opts?) => Validator // creates a validator instance optional?: boolean id?: string // stable type name (e.g. "Cat") — used for $defs/$ref in JSON Schema ref?: { type: () => TAtscriptAnnotatedType; field: string } // FK reference to another type/field (set on @db.rel.FK) } ``` Generated interfaces expose this as static members: ```typescript import { Product } from './product.as' Product.type // TAtscriptTypeObject — the type structure Product.metadata // TMetadataMap — top-level annotations Product.validator() // Validator — with type guard support ``` ## Type Kinds The `type` field is one of these shapes, distinguished by `kind`: | Kind | Interface | Description | | ---------------- | ---------------------- | ------------------------------------------------------------ | | `''` | `TAtscriptTypeFinal` | Primitives and literals — `designType`, optional `value` | | `'object'` | `TAtscriptTypeObject` | Named `props` (Map) and `propsPatterns` (regex-matched keys) | | `'array'` | `TAtscriptTypeArray` | Element type in `of` | | `'union'` | `TAtscriptTypeComplex` | Alternatives in `items` | | `'intersection'` | `TAtscriptTypeComplex` | Combined types in `items` | | `'tuple'` | `TAtscriptTypeComplex` | Positional types in `items` | Each shape also has a `tags` set — semantic type tags like `'email'`, `'uuid'`, `'positive'` that come from primitives like `string.email`. ## DataType Inferring Each type definition interface carries a phantom `DataType` generic: ```typescript interface TAtscriptTypeObject> { // ... __dataType?: DataType // phantom — never set at runtime } ``` This allows TypeScript to automatically infer the data shape. When you call `.validator()` on a generated type, the `Validator` extracts `DataType` from the type definition — so `validate()` acts as a **type guard** without any manual generic parameters: ```typescript import { Product } from './product.as' function processData(data: unknown) { const validator = Product.validator() if (validator.validate(data, true)) { // TypeScript knows data is Product here console.log(data.name, data.price) } } ``` Use `TAtscriptDataType` to extract the DataType from any annotated type — this is the recommended utility for end users: ```typescript import type { TAtscriptDataType } from '@atscript/typescript/utils' import { Product } from './product.as' type ProductData = TAtscriptDataType // ProductData = { name: string; price: number; tags: string[] } ``` When the phantom `__dataType` is `unknown` (unset), `TAtscriptDataType` falls back to the constructor's instance type — so it works seamlessly with both typed and untyped generated interfaces. For lower-level use, `InferDataType` extracts the DataType from a raw type definition (not an annotated type): ```typescript import type { InferDataType, TAtscriptTypeObject } from '@atscript/typescript/utils' type MyData = InferDataType> // MyData = { name: string; age: number } ``` ## Type Traversal Atscript types form a tree — objects contain props, arrays wrap an element type, unions hold alternatives. The `forAnnotatedType()` helper provides type-safe dispatch over this tree, replacing manual `switch (def.type.kind)` patterns. ### `forAnnotatedType()` Supply a handler for each type kind. Each handler receives the correctly narrowed type: ```typescript import { forAnnotatedType } from '@atscript/typescript/utils' const description = forAnnotatedType(someType, { final: d => `primitive: ${d.type.designType}`, object: d => `object with ${d.type.props.size} props`, array: d => `array`, union: d => `union of ${d.type.items.length}`, intersection: d => `intersection of ${d.type.items.length}`, tuple: d => `tuple of ${d.type.items.length}`, phantom: d => `phantom element`, // optional }) ``` | Handler | Receives | Key fields | | ---------------------- | ---------------------------------------------- | ------------------------------------------- | | `final` | `TAtscriptAnnotatedType` | `designType`, `value`, `tags` | | `object` | `TAtscriptAnnotatedType` | `props` (Map), `propsPatterns` | | `array` | `TAtscriptAnnotatedType` | `of` (element type) | | `union` | `TAtscriptAnnotatedType` | `items` (alternatives) | | `intersection` | `TAtscriptAnnotatedType` | `items` (combined types) | | `tuple` | `TAtscriptAnnotatedType` | `items` (positional types) | | `phantom` _(optional)_ | `TAtscriptAnnotatedType` | Same as `final`, `designType === 'phantom'` | The optional `phantom` handler intercepts [phantom](/packages/typescript/primitives#phantom-type) types before they reach `final`. If omitted, phantom types fall through to `final`. ### Recursive Walking `forAnnotatedType` dispatches a single node — recursion is up to you. For most cases reach for the built-in [`flattenAnnotatedType()`](#flattenannotatedtype) below; if you need full control over path formatting and what to collect, recurse into `props` (objects), `of` (arrays), and `items` (unions/intersections/tuples) and call `forAnnotatedType` again on each child. ### Example: Collecting Form Field Metadata Given this `.as` file: ```atscript export interface SignupForm { @meta.label "Full Name" @ui.placeholder "Enter your name" name: string @meta.label "Email Address" email: string.email @meta.label "Password" @meta.sensitive password: string @meta.label "Already have an account? Sign in" @ui.component "link" @ui.attr "href", "/login" signIn: phantom } ``` Walk the type to build a form descriptor: ```typescript import { SignupForm } from './signup-form.as' import { forAnnotatedType, isPhantomType } from '@atscript/typescript/utils' import type { TAtscriptAnnotatedType } from '@atscript/typescript/utils' interface FormField { key: string label: string type: 'input' | 'phantom' tags: string[] metadata: Record } function collectFields(def: TAtscriptAnnotatedType): FormField[] { if (def.type.kind !== 'object') return [] const fields: FormField[] = [] for (const [key, prop] of def.type.props.entries()) { const label = (prop.metadata.get('meta.label') as string) || key if (isPhantomType(prop)) { // Non-data element — collect its annotations for the renderer fields.push({ key, label, type: 'phantom', tags: [...prop.type.tags], metadata: Object.fromEntries(prop.metadata), }) continue } fields.push({ key, label, type: 'input', tags: [...prop.type.tags], metadata: Object.fromEntries(prop.metadata), }) } return fields } const fields = collectFields(SignupForm) // [ // { key: 'name', label: 'Full Name', type: 'input', tags: ['string'], ... }, // { key: 'email', label: 'Email Address', type: 'input', tags: ['email', 'string'], ... }, // { key: 'password', label: 'Password', type: 'input', tags: ['string'], ... }, // { key: 'signIn', label: 'Already have an account? Sign in', type: 'phantom', // tags: ['phantom'], metadata: { 'meta.label': '...', 'ui.component': 'link', ... } }, // ] ``` ### Example: Flattening Nested Types Walk a deeply nested type to produce a flat map of all leaf paths and their `designType`: ```typescript import { forAnnotatedType, isPhantomType } from '@atscript/typescript/utils' import type { TAtscriptAnnotatedType } from '@atscript/typescript/utils' function flattenType(def: TAtscriptAnnotatedType, prefix = ''): Record { return forAnnotatedType(def, { final: d => ({ [prefix || '(root)']: d.type.designType }), phantom: () => ({}), // skip phantom props object(d) { let result: Record = {} for (const [key, prop] of d.type.props.entries()) { if (!isPhantomType(prop)) { Object.assign(result, flattenType(prop, prefix ? `${prefix}.${key}` : key)) } } return result }, array: d => flattenType(d.type.of, `${prefix}[]`), union(d) { let result: Record = {} for (const item of d.type.items) { Object.assign(result, flattenType(item, prefix)) } return result }, intersection(d) { let result: Record = {} for (const item of d.type.items) { Object.assign(result, flattenType(item, prefix)) } return result }, tuple(d) { let result: Record = {} d.type.items.forEach((item, i) => { Object.assign(result, flattenType(item, `${prefix}[${i}]`)) }) return result }, }) } // flattenType(Product) → { 'name': 'string', 'price': 'number', 'tags[]': 'string', ... } ``` This manual pattern gives full control over path formatting and what to collect. The [Validator](/packages/typescript/validation), [JSON Schema builder](/packages/typescript/json-schema), and [serializer](/packages/typescript/serialization) all use similar recursive walks internally. For the common case of producing a flat `Map` of dot-separated paths to their annotated types, use the built-in `flattenAnnotatedType()` described below. ### `flattenAnnotatedType()` Tools built on Atscript — form builders, query builders, index managers, schema generators — often need to iterate over every concrete field in a complex, deeply nested type. Manually walking objects, arrays, unions, intersections, and tuples is repetitive and error-prone. `flattenAnnotatedType()` handles this in one call: ```typescript import { flattenAnnotatedType } from '@atscript/typescript/utils' import { Product } from './product.as' const flatMap = flattenAnnotatedType(Product) // Map { // '' → root object type, // 'name' → string type, // 'price' → number type, // 'address' → nested object type, // 'address.street' → string type, // 'address.city' → string type, // 'tags' → array type, // 'items' → array type, // 'items.label' → string type, // 'items.count' → number type, // } ``` Each entry in the map is a full `TAtscriptAnnotatedType` — with metadata, tags, and validator support — so downstream tools can inspect annotations at any depth without re-walking the tree. **Union merging:** When multiple union branches contribute different types at the same path, `flattenAnnotatedType` merges them into a synthetic union type. For example, `{ a: string } | { a: number, b: string }` produces `a → union(string, number)` and `b → string`. **Arrays:** The function recurses into array element types using the same path prefix (no `[]` suffix), so `items: { label: string }[]` produces `items.label → string`. #### Options ```typescript interface TFlattenOptions { /** Called for each field with a non-empty path. */ onField?: (path: string, type: TAtscriptAnnotatedType, metadata: TMetadataMap) => void /** Metadata key to tag top-level array fields (e.g. 'mongo.__topLevelArray'). */ topLevelArrayTag?: string /** When true, phantom-typed properties are skipped. Included by default. */ excludePhantomTypes?: boolean } ``` **`onField`** is called for every field after it is added to the flat map. This is the hook point for domain-specific logic — for example, `@atscript/db-mongo` uses it to extract index definitions from field metadata: ```typescript const flatMap = flattenAnnotatedType(collectionType, { topLevelArrayTag: 'mongo.__topLevelArray', excludePhantomTypes: true, onField: (path, _type, metadata) => { // read @index, @unique, @textSearch etc. from metadata prepareIndexesForField(path, metadata) }, }) ``` **`topLevelArrayTag`** marks array fields that are direct properties of the root object (not arrays nested inside other arrays or unions). This is useful for systems like MongoDB that treat top-level arrays differently. **`excludePhantomTypes`** controls whether [phantom](/packages/typescript/primitives#phantom-type) properties appear in the result. By default phantom types are included — form builders may want to render them as non-data UI elements (links, headings). Set to `true` when only data-bearing fields matter (e.g. database schemas). ## Type Guards - `isAnnotatedType(value)` — returns `true` if the value is a `TAtscriptAnnotatedType` - `isAnnotatedTypeOfPrimitive(type)` — returns `true` if the type resolves to a primitive shape (not object or array). Recursively checks union/intersection/tuple members. - `isPhantomType(def)` — returns `true` if the type is a [phantom](/packages/typescript/primitives#phantom-type) type (`kind === ''` and `designType === 'phantom'`) ## Default Data Generation `createDataFromAnnotatedType()` creates a data object that conforms to an annotated type's shape. It supports four modes for controlling how values are resolved. ### Generated `toExampleData()` Method When `exampleData: true` is set in the [plugin options](/packages/typescript/configuration#exampledata), each generated class includes a `toExampleData()` static method: ```typescript import { Product } from './product.as' // Uses @meta.example annotations, creates a new object each call const example = Product.toExampleData() ``` This is equivalent to calling `createDataFromAnnotatedType(Product, { mode: 'example' })` manually. Without the plugin option, the method is not rendered in `.js` and the `.d.ts` declaration marks it as `@deprecated`. ### Basic Usage ```typescript import { createDataFromAnnotatedType } from '@atscript/typescript/utils' import { Product } from './product.as' // Empty mode (default) — structural defaults only const empty = createDataFromAnnotatedType(Product) // { name: '', price: 0, tags: [] } // Default mode — reads @meta.default annotations const withDefaults = createDataFromAnnotatedType(Product, { mode: 'default' }) // Example mode — reads @meta.example annotations const withExamples = createDataFromAnnotatedType(Product, { mode: 'example' }) // DB mode — reads @db.default and @db.default.increment/uuid/now annotations const withDbDefaults = createDataFromAnnotatedType(Product, { mode: 'db' }) ``` ### Modes | Mode | Value source | Optional props | | ----------- | --------------------------------------------------------------------------------------------- | --------------------------------------- | | `'empty'` | Structural defaults (`''`, `0`, `false`, `[]`, `{}`) | Always skipped | | `'default'` | `@meta.default` annotations | Skipped unless annotated | | `'example'` | `@meta.example` annotations | Always included | | `'db'` | `@db.default` (parsed) or `@db.default.increment`/`uuid`/`now` (returns function name string) | Skipped unless annotated | | `function` | Custom resolver callback | Skipped unless resolver returns a value | ### Annotations `@meta.default` and `@meta.example` accept a string argument. For string fields, the value is used as-is. For other types, it is parsed as JSON: ```atscript export interface User { @meta.default "unknown" name: string @meta.default "0" age: number @meta.example '{"street": "123 Main St", "city": "Springfield"}' address: Address @meta.example '["admin", "user"]' roles: string[] } ``` When an annotation is set on a complex type (object, array) and passes validation, the entire subtree is used as-is — inner properties are not traversed. If validation fails, the annotation is ignored and the utility falls back to building from inner properties. ### Validation All resolved values are validated against the full type definition (including `@expect.*` constraints). If a default or example value doesn't pass validation, it is silently ignored and the structural default is used instead. ### Custom Resolver Pass a function to compute values per field: ```typescript const data = createDataFromAnnotatedType(Product, { mode: (prop, path) => { if (path === 'name') return 'Custom Name' if (path === 'price') return 9.99 return undefined // fall through to structural default }, }) ``` The resolver receives the `TAtscriptAnnotatedType` for each field and the dot-separated path. Return `undefined` to use the structural default. Returned values are validated against the type — invalid values are skipped. ### Optional Props Optional properties are **omitted** from the output (the key is not present in the object) unless the active mode provides a value for them. The exception is **`'example'` mode**, which always includes all optional props (using `@meta.example` annotations when available, structural defaults otherwise): ```atscript export interface User { name: string @meta.default "buddy" nickname?: string bio?: string } ``` ```typescript createDataFromAnnotatedType(User, { mode: 'empty' }) // { name: '' } — both optional props omitted createDataFromAnnotatedType(User, { mode: 'default' }) // { name: '', nickname: 'buddy' } — bio omitted, nickname included createDataFromAnnotatedType(User, { mode: 'example' }) // { name: '', nickname: '', bio: '' } — all props included ``` ### Arrays in Example Mode In `'example'` mode, arrays generate **one sample item** from the element type instead of an empty array. This makes examples much more useful: ```typescript // empty mode: { tags: [], items: [] } // example mode: { tags: [''], items: [{ name: '', price: 0 }] } ``` If `@meta.example` is annotated on the array itself, that annotation takes priority over the generated item. ## Next Steps - [Validation](/packages/typescript/validation) — validate data against annotated types - [JSON Schema](/packages/typescript/json-schema) — generate JSON Schema from types - [Serialization](/packages/typescript/serialization) — serialize types for transport --- URL: "atscript.dev/packages/typescript/validation" LLMS_URL: "atscript.dev/packages/typescript/validation.md" --- # Validation Guide Every generated Atscript type has a `.validator()` method for runtime validation. In normal application code, you usually use it for: - validating request or form input - narrowing `unknown` data to a real TypeScript type - stripping or rejecting unexpected properties - handling partial updates safely ```typescript import { Product } from './product.as' const validator = Product.validator() ``` ## 1. Validate One Value ### Safe Mode Safe mode returns `false` on failure and acts as a type guard: ```typescript if (validator.validate(data, true)) { // TypeScript now knows data is Product console.log(data.name, data.price) } else { console.log(validator.errors) } ``` This is the most practical mode for request handlers and form submissions. ### Throwing Mode Throwing mode raises `ValidatorError` on failure: ```typescript try { validator.validate(data) // data passed validation } catch (error) { console.error(error.message) console.error(error.errors) } ``` ## 2. Narrow `unknown` Data The validator doubles as a type guard, so it works well at the edge of your app: ```typescript import { Product } from './product.as' function handleRequest(body: unknown) { if (Product.validator().validate(body, true)) { // body is Product here saveProduct(body) } } ``` No manual generic parameters are needed. Atscript already knows the data shape from the model. ## 3. Control Unexpected Properties Use `unknownProps` when inputs may contain extra fields: ```typescript const validator = Product.validator({ unknownProps: 'strip', }) ``` Options: - `'error'` — fail validation if an extra field is present - `'ignore'` — leave extra fields alone - `'strip'` — remove extra fields from the value `'strip'` is often useful for request payloads. ## 4. Validate Partial Updates Use `partial` when validating patch-like payloads: ```typescript const patchValidator = Product.validator({ partial: true, }) ``` Options: - `false` — all required properties must be present - `true` — allow missing properties at the top level - `'deep'` — allow missing properties at every level `'deep'` is useful for nested patch payloads. ## 5. Put Rules On The Model Validation rules come from your `.as` file: ```atscript export interface User { @expect.minLength 3 username: string email: string.email @expect.min 18 age: number.int } ``` That means one model definition can give you: - static typing - runtime validation - metadata for other tools ## 6. Write Better Error Messages Most validation annotations accept a custom message as the last argument: ```atscript export interface SignupForm { @meta.required "Please enter your name" @expect.minLength 3, "Username must be at least 3 characters" username: string } ``` When validation fails, the custom message is used instead of the default one. ## 7. Know The Common Built-Ins The most common validation rules come from: - semantic types like `string.email`, `string.required`, `number.int.positive` - `@expect.minLength` - `@expect.maxLength` - `@expect.min` - `@expect.max` - `@expect.pattern` For array uniqueness and other less common rules, see the [Validation Reference](/packages/typescript/validation-reference). ## DOs and DON'Ts - **DO expect extra fields to fail validation by default.** `unknownProps` defaults to `'error'` — if a payload may carry extra fields, choose `'strip'` or `'ignore'` deliberately. - **DON'T forget that `'strip'` mutates your input.** It deletes the extra fields from the object you passed in — clone the payload first if you still need the original. - **DO create the validator once and reuse it in hot paths.** `.validator()` builds a fresh instance on every call. Reuse is safe: each `validate()` call resets `validator.errors` before running. - **DO raise `errorLimit` when a form needs every message.** Validation stops collecting after 10 errors by default, so long forms may show an incomplete error list. ## When To Read The Reference Use the reference page when you need: - every validator option in one place - the exact `validate()` signature - `ValidatorError` - array uniqueness rules - plugin hooks and external context - manual `Validator` construction from runtime types ## Next Steps - [Validation Reference](/packages/typescript/validation-reference) — options, plugins, and lower-level API details - [Annotations Guide](/packages/typescript/annotations) — keep validation rules on the model - [Type Definitions](/packages/typescript/type-definitions) — understand the runtime type system behind the validator --- URL: "atscript.dev/packages/typescript/validation-comparison" LLMS_URL: "atscript.dev/packages/typescript/validation-comparison.md" --- # Atscript Validation vs Others How does Atscript validation compare to popular TypeScript validation libraries? This page puts three approaches side by side — same scenarios, same constraints. - **Zod** — the most popular schema-first validation library - **class-validator** — decorator-based DTOs, the NestJS standard ## Simple Object A user with a name (2–50 chars), email, and optional age (integer, 18+): ::: code-group ```atscript [Atscript] export interface User { @expect.minLength 2 @expect.maxLength 50 name: string email: string.email @expect.min 18 age?: number.int } ``` ```typescript [Zod] import { z } from 'zod' const User = z.object({ name: z.string().min(2).max(50), email: z.string().email(), age: z.number().int().min(18).optional(), }) type User = z.infer ``` ```typescript [class-validator] import { IsString, IsEmail, IsInt, IsOptional, MinLength, MaxLength, Min } from 'class-validator' export class User { @IsString() @MinLength(2) @MaxLength(50) name: string @IsEmail() email: string @IsOptional() @IsInt() @Min(18) age?: number } ``` ::: Atscript reads like a type definition with constraints — because it is one. Zod is schema-first (you infer the type from it, so there's no duplication), but every field still needs `z.string()`, `z.number()`, etc. — the syntax is a schema DSL, not a type language. Class-validator requires a decorator for every property, including `@IsString()` for something already typed as `string`. ## Deeply Nested Objects An order with an array of items and nested addresses: ::: code-group ```atscript [Atscript] export interface Order { items: { @expect.minLength 1 productId: string @expect.min 1 quantity: number.int }[] shipping: { street: string city: string @expect.pattern "^[0-9]{5}$" zip: string } billing?: { street: string city: string @expect.pattern "^[0-9]{5}$" zip: string } } ``` ```typescript [Zod] import { z } from 'zod' const Order = z.object({ items: z.array( z.object({ productId: z.string().min(1), quantity: z.number().int().min(1), }) ), shipping: z.object({ street: z.string(), city: z.string(), zip: z.string().regex(/^[0-9]{5}$/), }), billing: z .object({ street: z.string(), city: z.string(), zip: z.string().regex(/^[0-9]{5}$/), }) .optional(), }) ``` ```typescript [class-validator] import { IsString, IsInt, Min, MinLength, Matches, ValidateNested, IsArray, IsOptional, } from 'class-validator' import { Type } from 'class-transformer' export class OrderItem { @IsString() @MinLength(1) productId: string @IsInt() @Min(1) quantity: number } export class Address { @IsString() street: string @IsString() city: string @IsString() @Matches(/^[0-9]{5}$/) zip: string } export class Order { @IsArray() @ValidateNested({ each: true }) @Type(() => OrderItem) items: OrderItem[] @ValidateNested() @Type(() => Address) shipping: Address @IsOptional() @ValidateNested() @Type(() => Address) billing?: Address } ``` ::: Atscript and Zod both support inline nested structures. Class-validator **must** declare a separate class for every nested shape, wired up with `@ValidateNested()` and `@Type(() => ClassName)` on each field. Arrays add `{ each: true }`. Optional fields add `@IsOptional()`. ## Primitive Types Validating a standalone email string or a positive integer — not wrapped in an object: ::: code-group ```atscript [Atscript] export type Email = string.email export type PositiveInt = number.int & number.positive ``` ```typescript [Zod] const Email = z.string().email() const PositiveInt = z.number().int().positive() ``` ```typescript [class-validator] // Not possible — class-validator requires a wrapper class: class EmailDto { @IsEmail() value: string } // No way to validate a bare string or number ``` ::: Zod supports standalone primitives. Class-validator does not — every validated value must be a class property. In Atscript, `string.email` is a semantic type that carries the email regex as a built-in constraint. You can use it as a property type, a standalone parameter type, or compose it with `&`. ## Complex Types Complex type compositions — unions, intersections, inline objects mixed with primitives — are where the syntax differences become most pronounced: ::: code-group ```atscript [Atscript] export interface ApiResponse { result: string | number | { @expect.minLength 1 data: unknown[] total: number.int } metadata?: { requestId: string.uuid timing: number.positive } | string } ``` ```typescript [Zod] import { z } from 'zod' const ApiResponse = z.object({ result: z.union([ z.string(), z.number(), z.object({ data: z.array(z.unknown()).min(1), total: z.number().int(), }), ]), metadata: z .union([ z.object({ requestId: z.string().uuid(), timing: z.number().positive(), }), z.string(), ]) .optional(), }) ``` ```typescript [class-validator] // Not practically achievable. // class-validator has no support for union types. // You would need custom validation logic for every // union field, defeating the purpose of the library. ``` ::: Zod's `z.union()` works the same way as Atscript's `|` — it tries each variant and accepts the first match. The validation behavior is equivalent. The difference is syntax: Atscript writes `string | number | { ... }` inline, just like TypeScript. Zod requires `z.union([...])` with full schema definitions for every branch — same result, more ceremony. Class-validator has no union support at all. ### Intersections and Type Composition Composing types with `&` is a common TypeScript pattern. In Atscript it works exactly as you'd expect — and the merged result validates correctly: ::: code-group ```atscript [Atscript] export interface Timestamped { createdAt: string.isoDate updatedAt: string.isoDate } export interface Authored { @expect.minLength 1 authorId: string authorName: string } // Compose with & export type Article = Timestamped & Authored & { @expect.minLength 1 @expect.maxLength 200 title: string body: string } // Inline intersection works too export interface Log { entry: { level: string } & { message: string } } ``` ```typescript [Zod] import { z } from 'zod' const Timestamped = z.object({ createdAt: z.string().datetime(), updatedAt: z.string().datetime(), }) const Authored = z.object({ authorId: z.string().min(1), authorName: z.string(), }) // Option 1: .extend() — only adds new fields to one schema, // so you need to spread .shape to combine two existing schemas const Article = z.object({ ...Timestamped.shape, ...Authored.shape, title: z.string().min(1).max(200), body: z.string(), }) // Option 2: z.intersection() — validates both schemas // independently (does NOT merge properties), and returns // a ZodIntersection that loses .pick(), .omit(), .extend() // const Article = z.intersection(Timestamped, Authored) // Inline intersection of two objects: const Log = z.object({ entry: z.intersection(z.object({ level: z.string() }), z.object({ message: z.string() })), // ↑ entry is ZodIntersection, not ZodObject }) ``` ```typescript [class-validator] // Use class inheritance — but only single inheritance: class Timestamped { @IsDateString() createdAt: string @IsDateString() updatedAt: string } class Authored { @IsString() @MinLength(1) authorId: string @IsString() authorName: string } // Can only extend one class — no multiple inheritance. // Must manually duplicate properties from the second: class Article extends Timestamped { // ← Authored properties copied by hand @IsString() @MinLength(1) authorId: string @IsString() authorName: string @IsString() @MinLength(1) @MaxLength(200) title: string @IsString() body: string } ``` ::: In Atscript, `Type1 & Type2 & { ... }` merges all properties into a single validated type — just like TypeScript's `&`. Zod has no direct equivalent: `z.intersection()` does **not** merge properties into one object schema — it returns a `ZodIntersection` that loses object methods like `.pick()` and `.extend()`. To actually merge, you spread `.shape` into a new `z.object()` — a workaround, not a first-class feature. Class-validator only has single class inheritance — composing two unrelated types means manually copying properties. ## Validation Options Real-world validation isn't just "validate or reject." PATCH endpoints need partial validation. Some fields should be skipped. Custom rules need to plug in. ### Partial Validation Atscript supports partial validation as a first-class option when creating a validator: ```typescript // Top-level partial — missing required fields are OK Product.validator({ partial: true }).validate(data) // Deep partial — missing fields at any nesting depth are OK Product.validator({ partial: 'deep' }).validate(data) // Custom — fine-grained control per type and path Product.validator({ partial: (type, path) => path.startsWith('metadata'), }).validate(data) ``` Zod's `.partial()` works for top-level properties, but `.deepPartial()` — the recursive version needed for nested PATCH operations — was [deprecated in Zod 3.21 and removed in Zod v4](https://github.com/colinhacks/zod/issues/2854) with no built-in replacement. The Zod v4 changelog states: _"There is no direct alternative to this API."_ This has been a [significant pain point](https://github.com/colinhacks/zod/issues/2854) for the community, with over 100 reactions and over 70 comments asking for a solution. Developers must either use third-party packages, write their own recursive utilities, or maintain separate creation and update schemas. Class-validator has no partial validation concept at all. You must define separate DTO classes for create and update operations, manually marking fields with `@IsOptional()` in the update variant. ### Plugins and Skip Lists Atscript's validator accepts plugins — functions that intercept validation to add custom logic — and a `skipList` to exclude specific property paths: ```typescript Product.validator({ plugins: [myCustomPlugin], skipList: new Set(['internalId', 'audit.createdBy']), }).validate(data) ``` A plugin receives the type definition, the value, and a context object with `error()` and `path`. It can accept the value (`true`), reject it (`false`), or fall through to default validation (`undefined`). This makes it straightforward to add domain-specific rules without modifying the type definition. Zod achieves custom validation through `.refine()` and `.superRefine()` — methods that attach to individual schema nodes. There's no way to inject cross-cutting validation logic across an entire schema from the outside. Class-validator supports custom decorator-based validators, but each one requires defining a class that implements `ValidatorConstraintInterface`. ## Summary | | Atscript | Zod | class-validator | | -------------------------- | ------------------------------------- | ------------------------------------------ | -------------------------------------------- | | **Syntax** | Type definitions with constraints | Schema DSL with method chains | Decorator stacks on classes | | **Nesting** | Inline — no extra declarations | Inline — no extra declarations | Separate class per nested shape | | **Primitives** | Standalone validated types | Standalone schemas | Requires wrapper class | | **Unions & intersections** | `\|` and `&` — native syntax | `z.union()`, `z.intersection()` (no merge) | Not supported / single inheritance | | **Partial validation** | `partial: true \| 'deep' \| function` | `.partial()` only (deep removed) | Manual duplicate DTOs | | **Custom logic** | Pluggable at validator level | `.refine()` per schema node | Custom validator class per rule | | **TypeScript integration** | Generates `.d.ts` directly | `z.infer<>` utility type | `reflect-metadata` + experimental decorators | | **Type guards** | `validate(data, true)` narrows input | `.parse()` returns typed data | None | | **Ecosystem** | Growing | Largest (form libs, adapters) | NestJS standard | ## More Than Validation The key difference isn't just syntax or performance — it's scope. Zod and class-validator are validation libraries. Atscript is a type and metadata description language. The same `.as` file that defines validation constraints can also carry `@label` for UI display names, `@db.index.*` for database indexes, `@description` for documentation, and any custom annotations your project needs. All of this metadata is accessible at runtime through a single import. Other libraries validate data; Atscript **describes** it. ```atscript export interface Product { @label "Product Name" @expect.minLength 1 name: string @label "Price (USD)" price: number.positive @label "SKU" @db.index.unique sku: string @label "Description" @description "Shown on the product detail page" summary?: string } ``` One file. Types, validation, labels, database metadata — all in one place, all shared across your stack. --- URL: "atscript.dev/packages/typescript/validation-reference" LLMS_URL: "atscript.dev/packages/typescript/validation-reference.md" --- # Validation Reference Use this page when you need the full validator API, lower-level options, or plugin hooks. ## `validate()` Signature ```typescript validate(value: any, safe?: boolean, context?: unknown): value is TT ``` `DataType` is inferred from the Atscript model, so the validator narrows data automatically. ## Validator Options Pass options to `.validator()` or `new Validator(type, opts)`: ```typescript const validator = Product.validator({ partial: true, unknownProps: 'strip', errorLimit: 5, }) ``` ### `partial` Controls whether missing required properties are errors: - `false` (default) — all required properties must be present - `true` — missing properties are allowed at the top level only - `'deep'` — missing properties are allowed at all levels - `(type, path) => boolean` — custom logic per node ### `unknownProps` How to handle properties not defined in the type: - `'error'` (default) - `'ignore'` - `'strip'` ### `errorLimit` Maximum number of errors to collect before stopping. Default: `10`. ### `skipList` - **Type:** `Set` Skip specific property paths. The set matches dot-separated paths (relative to each object) — entries like `'internalId'` skip a top-level prop, `'audit.createdBy'` skips a nested prop: ```typescript Product.validator({ skipList: new Set(['internalId', 'audit.createdBy']), }) ``` ### `replace` - **Type:** `(type: TAtscriptAnnotatedType, path: string) => TAtscriptAnnotatedType` Replace a type definition dynamically during validation. Useful when one field should validate against a different type per request — for example, swapping a generic `unknown`-shaped payload for a concrete schema chosen at runtime. The function is called once per encountered type (results are cached), receives the original `TAtscriptAnnotatedType` and the dot-separated path, and must return either the original type or a replacement annotated type: ```typescript Product.validator({ replace: (type, path) => (path === 'status' ? customStatusType : type), }) ``` ## Built-In Validation Rules Annotations from `.as` files are enforced automatically: | Annotation | Applies to | Validates | | --------------------------- | ------------------------ | ---------------------------------------------------------------------- | | `@meta.required` | string, boolean | String: at least one non-whitespace character. Boolean: must be `true` | | `@expect.minLength` | string, array | Minimum length | | `@expect.maxLength` | string, array | Maximum length | | `@expect.min` | number | Minimum value | | `@expect.max` | number | Maximum value | | `@expect.int` | number | Must be integer | | `@expect.pattern` | string | Regex match | | `@expect.array.uniqueItems` | array | No duplicate items | | `@expect.array.key` | array element properties | Marks identity fields for uniqueness and patch operations | Semantic types like `string.email`, `string.required`, and `number.positive` add validation behavior through their built-in annotation definitions. ::: tip Decimal format Values typed as `decimal` are stored as strings to preserve precision. The validator enforces the regex `/^[+-]?\d+(\.\d+)?$/` — anything else (NaN, scientific notation, leading/trailing whitespace) is rejected with `Invalid decimal format`. ::: ## Array Uniqueness `@expect.array.uniqueItems` and `@expect.array.key` work together: ```atscript interface Order { @expect.array.uniqueItems "Duplicate line items" items: OrderItem[] } interface OrderItem { @expect.array.key productId: number quantity: number } ``` - `@expect.array.key` identifies the fields that make an item unique - `@expect.array.uniqueItems` enforces uniqueness during validation - multiple key fields form a composite key For primitive arrays like `string[]`, uniqueness falls back to deep equality. ## Error Handling `ValidatorError` extends `Error` and includes structured details: ```typescript import { ValidatorError } from '@atscript/typescript/utils' try { validator.validate(data) } catch (e) { if (e instanceof ValidatorError) { for (const err of e.errors) { console.log(err.path) console.log(err.message) console.log(err.details) } } } ``` After safe validation, errors are available on `validator.errors`. ## Plugins Plugins can intercept validation and either accept, reject, or fall through: ```typescript import type { TValidatorPlugin } from '@atscript/typescript/utils' const skipSensitive: TValidatorPlugin = (ctx, def) => { if (def.metadata.get('meta.sensitive')) { return true } return undefined } const validator = Product.validator({ plugins: [skipSensitive] }) ``` ### External Context Plugins can receive context through the third argument to `validate()`: ```typescript const roleAware: TValidatorPlugin = ctx => { const { context } = ctx if (context && (context as { role: string }).role === 'admin') { return true } return undefined } Product.validator({ plugins: [roleAware] }).validate(data, true, { role: 'admin' }) ``` The plugin context exposes `opts`, `validateAnnotatedType`, `error`, `path`, and `context`. ### Reporting Custom Errors from a Plugin Plugins may push their own structured errors into the active validator via `ctx.error(message, path?, details?)`: | Argument | Type | Notes | | --------- | --------- | -------------------------------------------------------------------------------------- | | `message` | `string` | Required. Becomes the `message` of a `TError` entry. | | `path` | `string?` | Optional. Defaults to the current dot-separated path being validated. | | `details` | `TError[]?` | Optional. Nested error breakdown — useful when an alternative-tested branch fails. | ```typescript const requirePositiveAmount: TValidatorPlugin = (ctx, def, value) => { if (def.metadata.get('meta.label') === 'Amount' && typeof value === 'number' && value <= 0) { ctx.error('Amount must be positive', ctx.path, [ { path: ctx.path, message: `Got ${value}` }, ]) return false } return undefined } ``` ## Manual Validator Construction For deserialized or programmatically built types: ```typescript import { Validator, deserializeAnnotatedType } from '@atscript/typescript/utils' const type = deserializeAnnotatedType(jsonData) const validator = new Validator(type) validator.validate(someValue) ``` ## Next Steps - [Validation Guide](/packages/typescript/validation) — the practical application flow - [Type Definitions](/packages/typescript/type-definitions) — the runtime type system behind validation - [Serialization](/packages/typescript/serialization) — validate deserialized types --- URL: "atscript.dev/packages/typescript/why-atscript" LLMS_URL: "atscript.dev/packages/typescript/why-atscript.md" --- # Why Atscript? For most TypeScript applications, the same model ends up being described several times: - once as a TypeScript type - once as a validation schema - once again as labels or UI hints - sometimes again as JSON Schema or API documentation That duplication slows changes down and makes it easy for one layer to drift out of sync with another. ## Before Atscript Here is a common TypeScript setup: ```typescript // types/user.ts export interface User { email: string name: string age: number } // validation/user.ts export const UserSchema = z.object({ email: z.string().email(), name: z.string().min(2), age: z.number().int().min(0), }) // ui/user-fields.ts export const userFields = { email: { label: 'Email Address', type: 'email' }, name: { label: 'Full Name' }, age: { label: 'Age', type: 'number' }, } ``` The shape is repeated, the rules are repeated, and the field labels live somewhere else again. ## With Atscript Put that information in one `.as` file instead: ```atscript export interface User { @meta.label 'Email Address' email: string.email @meta.label 'Full Name' @expect.minLength 2 name: string @expect.min 0 age: number.int } ``` Then use it from TypeScript: ```typescript import { User } from './user.as' import { buildJsonSchema } from '@atscript/typescript/utils' const validator = User.validator() const emailField = User.type.props.get('email') const schema = buildJsonSchema(User) console.log(emailField?.metadata.get('meta.label')) // -> 'Email Address' if (validator.validate(input, true)) { saveUser(input) } else { console.log(validator.errors) } ``` ## What Changes In Practice - You describe the model once instead of maintaining parallel type and schema files. - Validation rules stay on the model instead of being repeated in another DSL. - Labels and other metadata stay next to the fields they describe. - Runtime tools can read the same model without inventing a second configuration format. ## What Atscript Gives You Today - TypeScript types with runtime metadata - Runtime validation from the same model - JSON Schema export - A clear path into DB integrations and other model-driven tooling ## What It Does Not Force You To Learn Up Front You do not need to understand the internal type tree or plugin system to get value from Atscript. For most application work, you only need to know: - how to write a `.as` file - how to import the generated type - how to validate data - how to read metadata when you need it The lower-level runtime APIs are available later, when you need advanced tooling or custom integrations. ## Next Steps - [Quick Start](/packages/typescript/quick-start) — get a working `User.validator()` in a small project - [Build Setup](/packages/typescript/build-setup) — wire Atscript into your real app build - [Validation Guide](/packages/typescript/validation) — validate request data and partial updates --- URL: "atscript.dev/packages/vscode" LLMS_URL: "atscript.dev/packages/vscode.md" --- # VSCode Extension The Atscript VSCode extension provides first-class editor support for `.as` files — syntax highlighting, IntelliSense, real-time diagnostics, navigation, and automatic `.d.ts` generation on save. ::: info LSP Requirement The extension provides syntax highlighting out of the box. For IntelliSense, diagnostics, and other LSP features, `@atscript/core` must be installed in your project. See [Installation](/packages/vscode/installation) for details. ::: ## What's in This Section - [Installation](/packages/vscode/installation) — install the extension and set up your project - [Features](/packages/vscode/features) — syntax highlighting, IntelliSense, diagnostics, navigation, and more - [Configuration](/packages/vscode/configuration) — project configuration and editor settings ## Quick Overview | Feature | Description | | --------------------------- | -------------------------------------------------------------------------------- | | **Syntax Highlighting** | Full grammar support for `.as` files — works immediately, no dependencies needed | | **IntelliSense** | Context-aware completions for annotations, types, imports, and properties | | **Diagnostics** | Real-time error reporting and unused token hints | | **Go to Definition** | Navigate to type and interface declarations across files | | **Find References** | Locate all usages of a type or interface | | **Rename Symbol** | Rename types/interfaces and update all references | | **Hover Information** | Inline documentation for annotations and type references | | **Signature Help** | Annotation argument signatures as you type | | **Auto `.d.ts` Generation** | Generates TypeScript declarations on save | | **Config Watching** | Automatically reloads when `atscript.config.*` files change | ## Requirements - VSCode 1.80.0 or higher - `@atscript/core` — installed in your project root for LSP features (see [Installation](/packages/vscode/installation)) --- URL: "atscript.dev/packages/vscode/configuration" LLMS_URL: "atscript.dev/packages/vscode/configuration.md" --- # Configuration The VSCode extension does not have its own settings. Instead, it uses the same `atscript.config.*` file as the rest of the Atscript toolchain. ## Config File The extension looks for a configuration file named `atscript.config` with one of these extensions: - `.js`, `.mjs`, `.cjs`, `.ts`, `.mts`, `.cts` JS variants are checked before TS variants in each directory. The config file is resolved by walking up the directory tree from the `.as` file's location. This means different subdirectories in a monorepo can have different configurations. ### Minimal Example ```js // atscript.config.js import { defineConfig } from '@atscript/core' export default defineConfig({ // your configuration here }) ``` For full configuration options, see the [Configuration reference](/packages/typescript/configuration). ## What Configuration Affects The config file controls what the extension knows about your project: - **Annotations** — which annotations are available for completions and validation - **Primitives** — which primitive types are recognized - **Plugins** — extensions that add custom annotations and primitives (e.g., `@atscript/db-mongo`) Without a config file, the extension uses the default set of annotations and primitives from `@atscript/core`. ## Config Reloading The extension watches for changes to `atscript.config.*` files. When you modify your config, the language server reloads automatically — no need to restart VSCode. If a config file fails to load (syntax error, missing dependency, etc.), the extension falls back to defaults and will retry when the file is fixed. --- URL: "atscript.dev/packages/vscode/features" LLMS_URL: "atscript.dev/packages/vscode/features.md" --- # Features ## Syntax Highlighting The extension provides full TextMate grammar support for `.as` files. Syntax highlighting works immediately after installation — **no dependencies required**. Highlighted elements include: - **Keywords** — `import`, `export`, `from`, `type`, `interface`, `annotate`, `as` - **Type and interface names** — entity declarations - **Annotations** — `@name` and `@namespace.name` patterns - **Annotation arguments** — strings, numbers, booleans (`true`, `false`), `null`, `undefined` - **Properties** — required (`name:`) and optional (`name?:`) with distinct styling - **Operators** — `|`, `&`, `=` - **Comments** — line (`//`) and block (`/* */`) - **Import paths** — string literals in import statements - **Strings** — single and double quoted ### File Nesting The extension automatically configures VSCode to nest generated `.as.d.ts` files under their corresponding `.as` files in the Explorer panel: ``` src/ models/ user.as └─ user.as.d.ts ← nested automatically ``` ## IntelliSense ::: info Requires `@atscript/core` IntelliSense and all features below require `@atscript/core` to be installed in your project root. See [Installation](/packages/vscode/installation). ::: The extension provides context-aware completions triggered by typing or by specific characters (`` ` ``, `@`, `.`, `,`, `{`, `'`, `"`). ### Keyword Completions At the top level of an `.as` file, the extension suggests: - `import`, `export`, `annotate`, `interface`, `type` - After `export`: `annotate`, `interface`, `type` ### Annotation Completions When typing `@`, all available annotations are suggested based on the current context (interface-level vs. property-level). For namespaced annotations like `@meta.label`, typing the dot triggers a follow-up suggestion for the second part. Annotation arguments are also completed — predefined allowed values appear for each argument position, and boolean arguments suggest `true`/`false`. ### Type Completions When typing in a type position (after `:`, `=`, `|`, `&`), the extension suggests: - All declared types and interfaces in the current file - All imported types - All primitive types from the configuration - **Exported types from other workspace files** — selecting one automatically adds the import statement ### Import Completions Inside `import { ... }`, the extension suggests exported symbols from the target file, excluding symbols already imported. Inside the `from '...'` path, it provides file system completions for `.as` files and directories. ### Property Chain Completions For nested property access (e.g., `address.city`), the extension resolves the type chain and suggests valid sub-properties. This also works inside `annotate` blocks for deep property annotations. ## Diagnostics The extension reports errors in real-time as you type, with a short debounce delay for performance. **Errors** reported include: - Syntax errors - Unknown identifiers (unresolved type references) - Invalid annotations (wrong context or arguments) - Unknown properties in `annotate` blocks **Hints** include: - Unused tokens — displayed as faded/dimmed text, helping you identify dead code Diagnostics update automatically when related files change. If you modify a type that other files import, those files are re-validated too. ## Navigation ### Go to Definition `Cmd+Click` (macOS) or `Ctrl+Click` (Windows/Linux) on any type reference to jump to its declaration. Works across files for imported types. ### Find References Right-click a type or interface name and select **"Find All References"** to see every usage across the workspace. ### Rename Symbol Right-click a type or interface name and select **"Rename Symbol"** (`F2`) to rename it and update all references across files. ## Hover Information Hover over an annotation to see its documentation, including argument types and descriptions. Hover over a type reference to see the documentation from its definition. ## Signature Help When typing annotation arguments, the extension shows parameter signatures — argument names, types, and positions. Triggered automatically when typing `,` or space inside annotation parentheses. ## Auto `.d.ts` Generation Every time you save an `.as` file, the extension automatically generates the corresponding `.as.d.ts` TypeScript declaration file. This keeps your type declarations in sync without manual build steps. ## Config Watching The extension watches for changes to `atscript.config.*` files (`.js`, `.ts`, `.mjs`, `.mts`, `.cjs`, `.cts`). When a config file changes, the language server automatically reloads to pick up new annotations, primitives, and plugins. --- URL: "atscript.dev/packages/vscode/installation" LLMS_URL: "atscript.dev/packages/vscode/installation.md" --- # Installation ## Install the Extension 1. Open VSCode 2. Go to Extensions (`Cmd+Shift+X` on macOS / `Ctrl+Shift+X` on Windows/Linux) 3. Search for **"Atscript"** 4. Install the extension by **Moost** Alternatively, install from the [VS Marketplace](https://marketplace.visualstudio.com/items?itemName=moost.atscript-as) or via the command line: ```bash code --install-extension moost.atscript-as ``` ## Project Setup Syntax highlighting works immediately after installation — no additional setup needed. For LSP features (IntelliSense, diagnostics, go-to-definition, hover, rename, etc.), the extension requires `@atscript/core` to be installed in your project root: ```bash npm install @atscript/core ``` ::: tip If `@atscript/core` is not found when the extension activates, it will show a warning and automatically retry every 60 seconds. Once you install the dependency, the language server will start on the next retry — no manual reload required. You can also trigger an immediate restart via the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) → **"Atscript: Restart Language Server"**. ::: ## What Works Without `@atscript/core` | Feature | Without `@atscript/core` | With `@atscript/core` | | ------------------------------------- | ------------------------ | --------------------- | | Syntax highlighting | Yes | Yes | | File nesting (`.as.d.ts` under `.as`) | Yes | Yes | | IntelliSense / completions | No | Yes | | Diagnostics | No | Yes | | Go to Definition | No | Yes | | Find References | No | Yes | | Rename Symbol | No | Yes | | Hover information | No | Yes | | Signature help | No | Yes | | Auto `.d.ts` generation | No | Yes | | Config watching | No | Yes | --- URL: "atscript.dev/plugin-development" LLMS_URL: "atscript.dev/plugin-development.md" --- # Plugin Development Atscript plugins extend the language in three practical ways: - add custom annotations - add custom primitives - generate output files from `.as` models You do not need to learn the entire parser or runtime internals up front. For most plugins, the useful path is: 1. add one annotation or primitive with `config()` 2. optionally add a `render()` hook if you generate files 3. test the result with a small fixture 4. wire it into CLI, build tools, or editor workflows ::: warning Early Documentation Plugin development is part of Atscript's long-term architecture, but this section is still early and incomplete. If you are new to Atscript, start with the [TypeScript guide](/packages/typescript/) first. ::: ## Best Path For New Plugin Authors ::: tip Recommended Order If you are building your first plugin, read these in order: 1. [Custom Annotations](/plugin-development/annotation-system) 2. [Custom Primitives](/plugin-development/primitives-type-tags) 3. [Building a Code Generator](/plugin-development/code-generation) 4. [Testing Plugins](/plugin-development/testing-plugins) 5. [VSCode & Build Integration](/plugin-development/tooling-integration) ::: Keep [Plugin Architecture](/plugin-development/architecture) and [Plugin Hooks Reference](/plugin-development/plugin-hooks) for later, when you need the deeper model. ## What Plugins Can Do | Capability | Hook | Example | | -------------------------------- | -------------- | ------------------------------------------- | | Add semantic types (primitives) | `config()` | `geo.latitude`, `color.hex` | | Define annotation specs | `config()` | `@api.endpoint`, `@store.collection` | | Remap or virtualize module paths | `resolve()` | Path aliases, virtual modules | | Provide virtual file content | `load()` | Synthetic `.as` modules | | Post-process parsed documents | `onDocument()` | Inject virtual props, run custom checks | | Generate output files | `render()` | `.d.ts`, `.js`, `.py`, `.json` — any format | | Aggregate across all documents | `buildEnd()` | Global type declarations, indexes | ## Choose The Smallest Plugin Shape Most plugin work starts in one of these shapes: - `Language extension`: only `config()` - add custom annotations or primitives - no output files yet - `Generator plugin`: `config()` plus `render()` - read parsed models - generate `.d.ts`, `.json`, `.py`, or other files - `Project-wide generator`: `config()` plus `render()` plus `buildEnd()` - generate registries, manifests, or shared declarations across files Start with the smallest one that solves your problem. ## Your First Plugin Here's a minimal plugin that adds a `@api.deprecated` annotation: ```typescript import { createAtscriptPlugin, AnnotationSpec } from '@atscript/core' export const apiPlugin = () => createAtscriptPlugin({ name: 'api', config() { return { annotations: { api: { deprecated: new AnnotationSpec({ description: 'Mark this field as deprecated in the API', nodeType: ['prop', 'interface'], argument: { name: 'message', type: 'string', optional: true, }, }), }, }, } }, }) ``` `createAtscriptPlugin` is a type-safe identity function — it returns the object you pass in, but gives you full TypeScript IntelliSense on the hook signatures. This is already enough to make Atscript understand a new annotation. If your goal is syntax support or metadata only, you can stop at this stage. ## Registering Your Plugin Add the plugin to your `atscript.config.ts`: ```typescript import { defineConfig } from '@atscript/core' import { tsPlugin } from '@atscript/typescript' import { apiPlugin } from './plugins/api-plugin' export default defineConfig({ rootDir: 'src', plugins: [tsPlugin(), apiPlugin()], }) ``` Plugins execute in array order. Each plugin's `config()` output is merged with the accumulated config using deep defaults (`defu`), so multiple plugins can contribute primitives and annotations without conflicts. ## The Plugin Interface ```typescript interface TAtscriptPlugin { name: string config?( config: TAtscriptConfig ): Promise | TAtscriptConfig | undefined resolve?(id: string): Promise | string | undefined load?(id: string): Promise | string | undefined onDocument?(doc: AtscriptDoc): Promise | void render?( doc: AtscriptDoc, format: TAtscriptRenderFormat ): Promise | TPluginOutput[] | undefined buildEnd?( output: TOutput[], format: TAtscriptRenderFormat, repo: AtscriptRepo ): Promise | void } ``` All hooks except `name` are optional. In practice: - start with `config()` for annotations and primitives - add `render()` when you need generated output - add `buildEnd()` only when you need cross-file aggregation You usually do not need `resolve()`, `load()`, or `onDocument()` for a first plugin. ## Recommended Reading Paths ### Path A: Extend The Model Language 1. **[Custom Annotations](/plugin-development/annotation-system)** — add new annotation names with typed arguments and validation. 2. **[Custom Primitives](/plugin-development/primitives-type-tags)** — add semantic types like `geo.latitude` or `openapi.date`. 3. **[Testing Plugins](/plugin-development/testing-plugins)** — verify that the plugin really works on `.as` fixtures. ### Path B: Generate Files 1. **[Custom Annotations](/plugin-development/annotation-system)** or **[Custom Primitives](/plugin-development/primitives-type-tags)** — define what your generator reads. 2. **[Building a Code Generator](/plugin-development/code-generation)** — write a `render()` hook that walks one document and emits output. 3. **[Testing Plugins](/plugin-development/testing-plugins)** — lock behavior with snapshots. 4. **[VSCode & Build Integration](/plugin-development/tooling-integration)** — decide which outputs run in the CLI, bundler, and editor. ### Path C: Deepen Your Mental Model 1. **[Plugin Architecture](/plugin-development/architecture)** — understand the document model and the processing pipeline. 2. **[Plugin Hooks Reference](/plugin-development/plugin-hooks)** — exact signatures and hook behavior. 3. **[Validation Specification](/plugin-development/validation-spec)** — implement validation in non-TypeScript targets. ## Prerequisites - Familiarity with [Atscript syntax](/packages/typescript/interfaces-types) (interfaces, types, annotations) - A working TypeScript development environment - `@atscript/core` as a dependency (the only required package for plugin development) --- URL: "atscript.dev/plugin-development/annotation-system" LLMS_URL: "atscript.dev/plugin-development/annotation-system.md" --- # Custom Annotations Annotations are the metadata layer in Atscript. They can carry labels, validation rules, API hints, UI hints, or any other model-level information your plugin needs. For a first plugin, you usually only need: - a name - a `nodeType` - zero or more typed arguments Start there. Validation callbacks, merge strategies, and AST mutation are useful later, but they are not required for a useful first annotation. ## The AnnotationSpec Class Every annotation is defined by an `AnnotationSpec` instance: ```typescript import { AnnotationSpec } from '@atscript/core' new AnnotationSpec({ description: 'Mark field as searchable', nodeType: ['prop'], argument: { name: 'weight', type: 'number', optional: true }, multiple: false, mergeStrategy: 'replace', }) ``` ### TAnnotationSpecConfig Options | Option | Type | Default | Description | | --------------- | ----------------------- | ----------- | -------------------------------------------------------------------------------------------------- | | `description` | `string` | — | Documentation shown in IntelliSense hover | | `nodeType` | `TNodeEntity[]` | — | Where annotation can appear: `'interface'`, `'type'`, `'prop'` | | `argument` | `object \| object[]` | — | Argument definition(s) | | `multiple` | `boolean` | `false` | Allow the annotation to appear more than once on the same node | | `mergeStrategy` | `'replace' \| 'append'` | `'replace'` | How values combine during annotation inheritance | | `defType` | `string[]` | — | Restrict to specific value types. See [Available `defType` values](#simple-alternative-deftype). | | `validate` | `function` | — | Custom validation at parse time | | `modify` | `function` | — | AST mutation after validation | ## Registering Annotations via config() Annotations are registered in a nested tree structure. The tree path becomes the dot-notation name: ```typescript import { createAtscriptPlugin, AnnotationSpec } from '@atscript/core' export const apiPlugin = () => createAtscriptPlugin({ name: 'api', config() { return { annotations: { api: { // @api.* namespace endpoint: new AnnotationSpec({ // @api.endpoint description: 'REST endpoint for this interface', nodeType: ['interface'], argument: { name: 'path', type: 'string' }, }), method: new AnnotationSpec({ // @api.method description: 'HTTP method', nodeType: ['interface'], argument: { name: 'method', type: 'string', values: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], }, }), field: { // @api.field.* sub-namespace readonly: new AnnotationSpec({ // @api.field.readonly description: 'Field is read-only in API responses', nodeType: ['prop'], }), writeOnly: new AnnotationSpec({ // @api.field.writeOnly description: 'Field is accepted in requests but excluded from responses', nodeType: ['prop'], }), }, }, }, } }, }) ``` The nesting depth is arbitrary — `@api.field.readonly` comes from `annotations.api.field.readonly`. ## Annotation Arguments Each argument is defined with `TAnnotationArgument`: ```typescript interface TAnnotationArgument { name: string type: 'string' | 'number' | 'boolean' | 'ref' | 'query' optional?: boolean description?: string values?: string[] // Enum — restrict to specific values } ``` The argument types correspond to the tokens accepted in `.as` source: | `type` | Accepts | | ----------- | ------------------------------------------------------------------------------------ | | `'string'` | Quoted string literal (`"text"`) | | `'number'` | Numeric literal (`42`, `-1.5`) | | `'boolean'` | Identifier `true` / `false` | | `'ref'` | Bare identifier referencing another type (e.g. `User`) | | `'query'` | Backtick-delimited query expression — used by DB plugins for SQL-like filter syntax | ### No Arguments (Flag Annotation) Omit `argument` entirely: ```typescript new AnnotationSpec({ description: 'Mark field as deprecated', nodeType: ['prop', 'interface'], }) ``` Usage: `@api.deprecated` (no arguments) ### Single Argument Pass a single object: ```typescript new AnnotationSpec({ description: 'Display label for the field', argument: { name: 'text', type: 'string' }, }) ``` Usage: `@meta.label "Full Name"` ### Multiple Arguments Pass an array of objects. Arguments are positional: ```typescript new AnnotationSpec({ description: 'Vector search index', argument: [ { name: 'dimensions', type: 'number' }, { name: 'similarity', type: 'string', optional: true, values: ['cosine', 'euclidean', 'dotProduct'], }, { name: 'indexName', type: 'string', optional: true }, ], }) ``` Usage: `@search.vector 512, "cosine", "my-index"` ### Enum Values The `values` field restricts which strings are accepted — the compiler reports an error for any other value: ```typescript new AnnotationSpec({ argument: { name: 'strategy', type: 'string', values: ['replace', 'merge'], }, }) ``` Usage: `@patch.strategy "replace"` (accepted) vs `@patch.strategy "upsert"` (error) ## Merge Strategies When annotations are inherited through type references, the merge strategy controls how values combine: ### `'replace'` (Default) The annotation on the child/inner type overwrites the parent's: ```typescript new AnnotationSpec({ mergeStrategy: 'replace', // default argument: { name: 'value', type: 'string' }, }) ``` ```atscript interface Base { @meta.label "Base Name" name: string } annotate Base as Extended { @meta.label "Extended Name" // overwrites "Base Name" name } ``` ### `'append'` Values accumulate — both parent and child annotations are preserved as an array: ```typescript new AnnotationSpec({ multiple: true, mergeStrategy: 'append', argument: { name: 'tag', type: 'string' }, }) ``` ```atscript interface Base { @tag "searchable" name: string } annotate Base as Tagged { @tag "sortable" // both "searchable" and "sortable" are kept name } ``` ::: tip `mergeStrategy: 'append'` almost always pairs with `multiple: true` — otherwise the base annotation would error on duplicates. ::: ## Custom Validation For validation logic beyond type checks and argument counts, provide a `validate` function: ```typescript validate(mainToken: Token, args: Token[], doc: AtscriptDoc): TMessages | undefined ``` | Parameter | Description | | ----------- | --------------------------------------------------------------------------------------------------------- | | `mainToken` | The annotation token (e.g., `@api.endpoint`). Access the parent node via `mainToken.parentNode`. | | `args` | Array of argument tokens. Each has `.text` (raw value), `.type` (token type), `.range` (source location). | | `doc` | The `AtscriptDoc` instance for resolving types and querying the document. | Return an array of diagnostic messages, or `undefined` if valid: ```typescript interface TMessage { severity: 1 | 2 | 3 | 4 // 1=Error, 2=Warning, 3=Info, 4=Hint message: string range: { start: Position; end: Position } } ``` ::: tip Built-in validation runs before your `validate` callback. The `AnnotationSpec` class automatically checks `multiple`, `nodeType`, argument count, argument types, `values`, and `defType`. Your callback only needs to handle domain-specific logic. ::: ### Example: Validate a Required Sibling Property A `@store.collection` annotation that requires an `id` field of type `string` or `number`: ```typescript new AnnotationSpec({ nodeType: ['interface'], validate(token, args, doc) { const parent = token.parentNode if (!isInterface(parent) || !parent.props.has('id')) { return [ { severity: 1, message: '@store.collection requires an "id" property', range: token.range, }, ] } const errors = [] const idProp = parent.props.get('id')! if (idProp.token('optional')) { errors.push({ severity: 1, message: '"id" cannot be optional on a @store.collection', range: idProp.token('identifier')!.range, }) } // Resolve the property type and check it is string or number let def = idProp.getDefinition() if (isRef(def)) { def = doc.unwindType(def.id!, def.chain)?.def || def } if (isPrimitive(def) && !['string', 'number'].includes(def.type!)) { errors.push({ severity: 1, message: '"id" must be of type string or number', range: idProp.token('identifier')!.range, }) } return errors.length > 0 ? errors : undefined }, }) ``` ### Example: Validate Field Type Restrict an annotation to object or array fields: ```typescript new AnnotationSpec({ nodeType: ['prop'], argument: { name: 'strategy', type: 'string', values: ['replace', 'merge'], }, validate(token, args, doc) { const field = token.parentNode! const definition = field.getDefinition() if (!definition) return // Resolve references let def = definition if (isRef(def)) { def = doc.unwindType(def.id!, def.chain)?.def || def } if (!isStructure(def) && !isInterface(def) && !isArray(def)) { return [ { severity: 1, message: 'Patch strategy requires an object or array type', range: token.range, }, ] } }, }) ``` ### Simple Alternative: defType For basic type restrictions, use `defType` instead of a full `validate` function: ```typescript new AnnotationSpec({ description: 'Decimal precision for numeric display', defType: ['number'], // only valid on number fields argument: { name: 'digits', type: 'number' }, }) ``` Available `defType` values: - Final scalar kinds: `'string'`, `'number'`, `'boolean'`, `'decimal'`, `'phantom'`, `'null'`, `'void'`, `'never'` - Composite kinds: `'object'`, `'array'`, `'union'`, `'intersection'` `'object'` matches both interfaces and inline structures; `'union'` / `'intersection'` match group nodes. ## AST Modification with modify() The `modify` hook runs after successful validation and can mutate the AST. This is a powerful feature for plugins that need to inject computed properties or restructure the parsed document. ```typescript modify(mainToken: Token, args: Token[], doc: AtscriptDoc): void ``` ### Example: Auto-Add an ID Property An `@store.collection` annotation that automatically adds an `id` property when the interface doesn't already have one: ```typescript new AnnotationSpec({ nodeType: ['interface'], modify(token, args, doc) { const parent = token.parentNode const struc = parent?.getDefinition() if (isInterface(parent) && !parent.props.has('id') && isStructure(struc)) { struc.addVirtualProp({ name: 'id', type: 'string', documentation: 'Primary identifier', }) } }, }) ``` Now every `@store.collection` interface automatically gets `id: string` without the author writing it explicitly: ```atscript @store.collection "users" export interface User { // id: string — injected automatically email: string.email name: string } ``` ### Example: Inject Timestamp Fields A plugin that auto-adds created/updated timestamps: ```typescript new AnnotationSpec({ description: 'Automatically add timestamp fields', nodeType: ['interface'], modify(token, args, doc) { const parent = token.parentNode const struc = parent?.getDefinition() if (isInterface(parent) && isStructure(struc)) { if (!parent.props.has('createdAt')) { struc.addVirtualProp({ name: 'createdAt', type: 'number.timestamp', documentation: 'Creation timestamp', }) } if (!parent.props.has('updatedAt')) { struc.addVirtualProp({ name: 'updatedAt', type: 'number.timestamp', documentation: 'Last update timestamp', }) } } }, }) ``` ::: tip `modify` runs once per annotation occurrence. If `multiple: true` and the annotation appears twice, `modify` runs twice. Make sure your modifications are idempotent (check before adding). ::: ## Complete Plugin Example Here's a full plugin combining primitives and annotations for an API documentation system: ```typescript import { createAtscriptPlugin, AnnotationSpec, isInterface } from '@atscript/core' export const openApiPlugin = () => createAtscriptPlugin({ name: 'openapi', config() { return { primitives: { openapi: { extensions: { date: { type: 'string', documentation: 'ISO 8601 date string (format: date)', tags: ['date'], annotations: { 'expect.pattern': { pattern: '^\\d{4}-\\d{2}-\\d{2}$', message: 'Expected ISO date format (YYYY-MM-DD)', }, }, }, dateTime: { type: 'string', documentation: 'ISO 8601 date-time string (format: date-time)', tags: ['dateTime'], annotations: { 'expect.pattern': { pattern: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}', message: 'Expected ISO date-time format', }, }, }, }, }, }, annotations: { openapi: { schema: new AnnotationSpec({ description: 'OpenAPI schema name for this interface', nodeType: ['interface'], argument: { name: 'name', type: 'string', optional: true }, }), tag: new AnnotationSpec({ description: 'OpenAPI tag for grouping endpoints', nodeType: ['interface'], multiple: true, mergeStrategy: 'append', argument: { name: 'tag', type: 'string' }, }), deprecated: new AnnotationSpec({ description: 'Mark as deprecated in OpenAPI spec', nodeType: ['prop', 'interface'], }), example: new AnnotationSpec({ description: 'Example value for OpenAPI documentation', nodeType: ['prop'], argument: { name: 'value', type: 'string' }, }), }, }, } }, }) ``` Usage in `.as` files: ```atscript @openapi.schema "CreateUserRequest" @openapi.tag "users" export interface CreateUser { @meta.label "Email Address" @openapi.example "user@example.com" email: string.email @meta.label "Full Name" @openapi.example "Jane Doe" name: string.required @meta.label "Date of Birth" @openapi.example "1990-01-15" birthday?: openapi.date } ``` ## Next Steps - [Building a Code Generator](/plugin-development/code-generation) — generate output files that consume your annotations and primitives - [Plugin Hooks Reference](/plugin-development/plugin-hooks) — all six hooks in detail --- URL: "atscript.dev/plugin-development/architecture" LLMS_URL: "atscript.dev/plugin-development/architecture.md" --- # Plugin Architecture This page is a deeper mental model for plugin authors. If you are building your first plugin, you do not need to absorb every class and node type before you start. Most first plugins only need: - `config()` to register primitives or annotations - `render()` to generate one file per document - `buildEnd()` only if you need project-wide output Come back to this page when you need to inspect documents more deeply or understand how the pipeline fits together. ## The Processing Pipeline Every `.as` file passes through these stages: ``` .as source → Tokenizer (lexical analysis) → Parser pipes (syntax analysis) → SemanticNode tree (resolved types) → AtscriptDoc (queryable document) → Plugin hooks (config, onDocument, render, buildEnd) → Output files (.d.ts, .js, .py, etc.) ``` Plugins don't modify the parser. Instead, they hook into the pipeline at specific points — contributing primitives and annotations before parsing, post-processing after parsing, and generating output files at the end. ## What Most Plugin Authors Need First For a practical first plugin, this is the minimum mental model: 1. `config()` runs once and adds primitives or annotations. 2. Atscript parses `.as` files into an `AtscriptDoc`. 3. `render(doc, format)` receives that parsed document and can emit files. 4. `buildEnd(output, format, repo)` is optional and only matters when output depends on multiple files. That is enough to build annotation plugins, primitive plugins, and many generators without going deeper into parser internals. ## Key Classes You Interact With Plugin authors mainly work with two classes from `@atscript/core`: | Class | Where you see it | Role | | -------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------- | | `AtscriptDoc` | `onDocument(doc)`, `render(doc, format)` | A single parsed `.as` file. Contains the node tree, annotations, imports, and provides query methods. | | `SemanticNode` | Returned from doc queries (`doc.nodes`, …) | Base class for all AST nodes. Subclasses represent interfaces, types, props, refs, etc. | The `buildEnd(output, format, repo)` hook also receives an `AtscriptRepo` — useful for cross-document queries (`repo.getUsedAnnotations()`, `repo.getPrimitivesTags()`) — but you do not construct or own it directly. ## AST Node Types The parsed AST is a tree of `SemanticNode` subclasses. Each node has an `entity` string that identifies its kind: | Entity | Node Class | Description | | ------------- | ----------------------- | ----------------------------------------------------- | | `'interface'` | `SemanticInterfaceNode` | An interface declaration with named properties | | `'type'` | `SemanticTypeNode` | A type alias (`type Foo = ...`) | | `'prop'` | `SemanticPropNode` | A property within an interface | | `'ref'` | `SemanticRefNode` | A reference to another type by name | | `'structure'` | `SemanticStructureNode` | An inline object structure (the body of an interface) | | `'group'` | `SemanticGroup` | A union (`\|`) or intersection (`&`) of nodes | | `'tuple'` | `SemanticTupleNode` | A tuple type (`[A, B, C]`) | | `'array'` | `SemanticArrayNode` | An array type | | `'const'` | `SemanticConstNode` | A literal value (`"hello"`, `42`) | | `'primitive'` | `SemanticPrimitiveNode` | A built-in or plugin-defined primitive type | | `'import'` | `SemanticImportNode` | An import statement | | `'annotate'` | `SemanticAnnotateNode` | An `annotate` block (ad-hoc annotations) | ### Type Guards The core exports type guard functions for narrowing nodes: ```typescript import { isInterface, isType, isProp, isRef, isStructure, isGroup, isArray, isConst, isPrimitive, isImport, isAnnotate, } from '@atscript/core' if (isInterface(node)) { // node is SemanticInterfaceNode for (const [name, prop] of node.props) { // prop is SemanticPropNode } } if (isRef(node)) { // node is SemanticRefNode console.log(node.id) // referenced type name console.log(node.chain.map(t => t.text)) // property access chain (e.g., ["address", "street"]) } ``` ### Reading Node Properties Every `SemanticNode` provides: ```typescript node.entity // 'interface' | 'type' | 'ref' | etc. node.id // Name identifier (e.g., 'User', 'string') node.token('export') // Access specific tokens — returns Token | undefined node.getDefinition() // Get the node's body/definition (e.g., the structure of an interface) node.annotations // Raw annotation tokens on this node ``` ## Plugin Lifecycle Hooks fire in this order during a build: ### 1. `config()` — At Startup Called once when the `PluginManager` initializes. Each plugin returns primitives and annotations to merge into the shared config. Results are merged using `defu` (deep defaults — the first plugin to define a key wins). ``` Default config (built-in primitives + @expect.* + @meta.* + @emit.*) ← Plugin A config() merged ← Plugin B config() merged = Final merged config ``` ### 2. `resolve(id)` — When Opening a Document Called for each document ID. Plugins can remap paths (e.g., virtual modules, aliases). All plugins are called; the last non-`undefined` return wins. ### 3. `load(id)` — When Loading Source Content Called to provide file content. The first plugin to return a string wins. If no plugin provides content, the file is read from disk. ### 4. `onDocument(doc)` — After Parsing Called after a document is parsed and its AST is built. All plugins receive the call in sequence. Use this to inject virtual properties, run custom validation, or transform the AST. ### 5. `render(doc, format)` — During Code Generation Called once per document per format. Each plugin checks the format string and returns output files (or nothing). All plugins contribute — their outputs are concatenated. ### 6. `buildEnd(output, format, repo)` — After All Documents Called once after all documents have been rendered. Use this for cross-document aggregation like generating global type declarations or index files. ## Config Merging The `PluginManager` merges configs using `defu` — a deep-defaults utility where the **first defined value wins**. This means: - Built-in defaults (primitives like `string`, `number`, `boolean` and annotations like `@expect.*`, `@meta.*`) are applied last - Plugin `config()` return values fill in before defaults - If two plugins both define the same primitive or annotation, the one listed first in the `plugins` array wins ```typescript // Plugin A returns: { primitives: { foo: { type: 'string' } } } // Plugin B returns: { primitives: { foo: { type: 'number' } } } // Result: foo has type: 'string' (Plugin A wins because it's first) ``` This means plugins can safely add new primitives and annotations without worrying about conflicting with each other — as long as they use unique namespace prefixes. ## Document API The `AtscriptDoc` is the primary object you'll work with in `onDocument()` and `render()` hooks. Key methods: ### `doc.nodes` The top-level node list. Iterate this to walk all interfaces, types, imports, and annotate blocks: ```typescript for (const node of doc.nodes) { if (isInterface(node)) { // Process interface } else if (isType(node)) { // Process type alias } } ``` ### `doc.unwindType(name, chain?)` Resolves a type reference to its terminal definition. Follows type aliases and chains: ```typescript // Given: type Email = string.email // doc.unwindType('Email') → resolves to the string.email primitive definition const resolved = doc.unwindType(ref.id, ref.chain) if (resolved?.def) { // resolved.def is the terminal SemanticNode // resolved.name is the resolved name } ``` ### `doc.evalAnnotationsForNode(node)` Returns the complete set of annotations for a node, including inherited annotations from parent types: ```typescript const annotations = doc.evalAnnotationsForNode(propNode) // Returns: TAnnotationTokens[] | undefined // Each entry has { name, token, args } — args is Token[] for (const ann of annotations || []) { ann.name // e.g. 'meta.label' ann.args[0]?.text // first argument as raw text } ``` ### `doc.getUnusedTokens()` Returns identifiers that are imported but never referenced — useful for import filtering in code generation. ### `doc.getDeclarationOwnerNode(name)` Looks up a top-level declaration by name. Returns `{ doc, node?, token? }` — the document that owns the identifier, the owning semantic node (if any), and the defining token — or `undefined` if not found. Follows imports across files. ## Next Steps - [Custom Primitives](/plugin-development/primitives-type-tags) — add semantic types to your plugin - [Custom Annotations](/plugin-development/annotation-system) — define annotation specs with validation --- URL: "atscript.dev/plugin-development/code-generation" LLMS_URL: "atscript.dev/plugin-development/code-generation.md" --- # Building a Code Generator Code generation is the most powerful feature of the Atscript plugin system. A code generator reads parsed `.as` documents and produces output files — type declarations, runtime modules, data classes, JSON schemas, or any format you need. For a first generator, do not start by learning every node class. Start by answering three practical questions: 1. which declarations in `doc.nodes` do you care about? 2. how do you turn each declaration into one target-language construct? 3. what metadata or annotations do you need to read along the way? This page focuses on the smallest useful `render()` flow first, then goes deeper into the document model. ## The render() Hook Code generation happens in the `render()` hook. It receives a parsed `AtscriptDoc` and a format string, and returns an array of output files: ```typescript render(doc: AtscriptDoc, format: string): TPluginOutput[] | undefined { if (format === 'myformat') { return [{ fileName: `${doc.name}.ext`, content: generateOutput(doc), }] } } ``` `TPluginOutput` is simply `{ fileName: string, content: string }`. The `fileName` is relative — the build system resolves it to an absolute path based on `outDir` config. The `format` string is a plain string with no registry. Your plugin checks it with `if` statements and returns nothing for formats it doesn't handle. Multiple plugins can produce output for the same format — their outputs are concatenated. ## A First Useful Generator Start with the smallest possible generator: one that turns interfaces into a flat text file. ```typescript import { createAtscriptPlugin, isInterface } from '@atscript/core' export const namesPlugin = () => createAtscriptPlugin({ name: 'names', render(doc, format) { if (format !== 'names') return [] const lines = doc.nodes.filter(isInterface).map(node => `interface ${node.id}`) return [ { fileName: `${doc.name}.names.txt`, content: lines.join('\n'), }, ] }, }) ``` Once that works, add property handling, type resolution, and annotation reads one step at a time. ## Iterating a Document `doc.nodes` is the top-level node list of a parsed `.as` file. It contains all declarations in source order: ```typescript for (const node of doc.nodes) { switch (node.entity) { case 'interface': // SemanticInterfaceNode — a named structure with properties break case 'type': // SemanticTypeNode — a type alias (type Foo = ...) break case 'import': // SemanticImportNode — an import statement break case 'annotate': // SemanticAnnotateNode — an annotate block (ad-hoc annotations) break } } ``` These are the four top-level entities your code generator needs to handle. Each has different data to extract. ## Interfaces and Their Properties An `SemanticInterfaceNode` represents a named interface with properties. This is the most common entity you'll generate output for. ```typescript function processInterface(node: SemanticInterfaceNode, doc: AtscriptDoc) { const name = node.id! // interface name const isExported = !!node.token('export') // was it exported? for (const [propName, prop] of node.props) { const isOptional = !!prop.token('optional') // field marked with ? const definition = prop.getDefinition() // the field's type definition // definition is a SemanticNode — could be ref, primitive, structure, group, array, const } } ``` ### What `getDefinition()` Returns Every node with a type body has `getDefinition()`. The returned `SemanticNode` varies: - **For an interface** — returns the `SemanticStructureNode` (the body containing props) - **For a type alias** — returns the aliased type (ref, group, primitive, etc.) - **For a property** — returns the field's type definition ## Resolving Type References When a definition is a `ref`, it points to another named type. You need to resolve it to understand the actual type: ```typescript import { isRef, isPrimitive } from '@atscript/core' const def = prop.getDefinition() if (isRef(def)) { const resolved = doc.unwindType(def.id!, def.chain) if (resolved?.def) { if (isPrimitive(resolved.def)) { // Terminal: it's a primitive like string, number, string.email, etc. resolved.def.type // underlying scalar: 'string', 'number', 'boolean', 'decimal', etc. resolved.def.config // full TPrimitiveConfig (type, tags, documentation, annotations, …) } else { // It references another interface/type — use the resolved name resolved.name } } } ``` `doc.unwindType(name, chain?)` recursively follows type aliases until it reaches either a primitive or a non-alias declaration. The optional `chain` handles property-access chains like `SomeType["nested"]["field"]`. ## Handling All Definition Kinds Use type guards to dispatch on definition kind. This is the core of any code generator — mapping Atscript's type system to your target language: ```typescript import { isRef, isPrimitive, isStructure, isGroup, isArray, isConst, isInterface, } from '@atscript/core' function resolveType(def: SemanticNode | undefined, doc: AtscriptDoc): string { if (!def) return 'unknown' if (isPrimitive(def)) { // A scalar primitive — map def.type to your target language // def.type is 'string' | 'number' | 'boolean' | 'decimal' | 'void' | 'null' | 'phantom' // def.config.tags provides semantic tags for finer discrimination return mapToTargetLanguage(def.type) } if (isRef(def)) { // A reference to another type — resolve it const resolved = doc.unwindType(def.id!, def.chain) if (resolved?.def && isPrimitive(resolved.def)) { return mapToTargetLanguage(resolved.def.type) } // Non-primitive reference — use the type name directly return def.id! } if (isStructure(def)) { // An inline object literal — has its own props map // Iterate def like an interface (it has a similar structure) return handleInlineObject(def) } if (isGroup(def)) { // A union (|) or intersection (&) of types const items = def.unwrap() // array of child SemanticNodes const op = def.op // '|' or '&' // Also check def.entity === 'tuple' for tuple types [A, B, C] return items.map(item => resolveType(item, doc)).join(op) } if (isArray(def)) { // An array type — element type is def.getDefinition() const elementDef = def.getDefinition() return `${resolveType(elementDef, doc)}[]` } if (isConst(def)) { // A literal constant value: "hello", 42, true // The raw text is on the identifier token. const literal = def.token('identifier')?.text ?? '' return literal } return 'unknown' } ``` ### Phantom Types When resolving types, check for `def.type === 'phantom'` on primitives. Phantom properties are non-data fields — they exist for runtime discovery (UI hints, layout elements) but should not appear in the generated data type. See [Custom Primitives — Phantom Primitives](/plugin-development/primitives-type-tags#phantom-primitives) for the full design intent. ### Union vs Intersection vs Tuple `SemanticGroup` covers unions and intersections; tuples are their own node class (`SemanticTupleNode`) but `isGroup()` returns `true` for both. Distinguish them by `entity` and `op`: ```typescript if (isGroup(def)) { if (def.entity === 'tuple') { // Fixed-length typed array: [string, number] const items = def.unwrap() } else if (def.op === '|') { // Union: string | number const items = def.unwrap() } else if (def.op === '&') { // Intersection: TypeA & TypeB const items = def.unwrap() } } ``` ## Reading Annotations Annotations carry metadata that code generators can use to produce richer output — labels, validation rules, indexes, API hints, or any custom metadata. ### Direct Annotations `node.annotations` gives you only annotations written directly on this node: ```typescript for (const ann of prop.annotations) { ann.name // 'meta.label', 'expect.minLength', 'db.index.unique', etc. ann.args // array of Token objects ann.args[0]?.text // first argument's value as string } ``` ### Merged Annotations (With Inheritance) `doc.evalAnnotationsForNode(node)` returns the complete annotation set including annotations inherited through type references and annotate blocks. The return value is `TAnnotationTokens[] | undefined`: ```typescript const merged = doc.evalAnnotationsForNode(prop) // TAnnotationTokens[] | undefined for (const ann of merged || []) { ann.name // e.g. 'meta.label', 'expect.minLength', 'db.index.unique' ann.token // the @-prefixed annotation token (with .range, .text, .parentNode) ann.args // Token[] — argument tokens, each with .text / .type / .range const value = ann.args[0]?.text } ``` The same annotation name can appear multiple times in the array when the annotation spec uses `multiple: true, mergeStrategy: 'append'`. ### When to Use Which - **`node.annotations`** — Direct annotations only. Use when you want only what the author explicitly wrote on this specific node. - **`doc.evalAnnotationsForNode(node)`** — Merged with inherited annotations from type references. Use when you want the complete picture. ::: tip Avoiding Duplicate Annotations When a property is a simple reference to another type, that referenced type already carries its own annotations. If your code generator emits annotation data for both the reference and the target, you may get duplicates. Consider using `node.annotations` (direct only) for simple refs and `evalAnnotationsForNode` for everything else. ::: ## Import Handling Import nodes represent dependencies between `.as` files. During code generation you typically need to: 1. Determine which imported identifiers are actually used 2. Emit corresponding import statements in your target language `doc.getUnusedTokens()` returns identifiers that were imported but never referenced: ```typescript const unused = new Set(doc.getUnusedTokens().map(t => t.text)) function processImport(node: SemanticImportNode) { const usedRefs = [] const def = node.getDefinition() if (isGroup(def)) { for (const child of def.unwrap()) { if (isRef(child) && !unused.has(child.id!)) { usedRefs.push(child.id!) } } } else if (isRef(def) && !unused.has(def.id!)) { usedRefs.push(def.id!) } if (usedRefs.length > 0) { const fromPath = node.token('path')!.text // Emit an import in your target language using usedRefs and fromPath } } ``` ## Annotate Blocks `SemanticAnnotateNode` represents `annotate Target { ... }` blocks. These come in two forms: - **Non-mutating** (`annotate Target as Alias { ... }`): Creates a new type alias with additional annotations. Your code generator should emit a new declaration for `Alias`. - **Mutating** (`annotate Target { ... }`): Adds annotations to an existing type without creating a new name. These annotations are already reflected in `evalAnnotationsForNode` when you process the target — you typically don't need to emit separate output for these. ```typescript if (isAnnotate(node)) { const targetName = node.token('target')?.text const aliasName = node.id // undefined for mutating const isMutating = !aliasName if (!isMutating) { // Non-mutating: emit a new type that extends/aliases the target // with the additional annotations from this block } // Mutating annotate blocks don't need separate output — // their annotations are merged into the target automatically } ``` ## The buildEnd() Hook For output that spans all documents — global registries, index files, manifests — use `buildEnd()`: ```typescript createAtscriptPlugin({ name: 'my-plugin', async buildEnd(output, format, repo) { if (format !== 'myformat') return // Access repo for cross-document queries const usedAnnotations = await repo.getUsedAnnotations() const tags = await repo.getPrimitivesTags() // Add a new file to the output output.push({ content: generateGlobalFile(usedAnnotations, tags), fileName: 'registry.ext', source: '', target: '/absolute/path/to/output/registry.ext', }) }, }) ``` ### Typical Use Cases Common reasons to use `buildEnd`: - **Global type registry** — collect all annotation types or primitive tags used across the project into a single declaration file - **Index / barrel file** — generate an entry point that re-exports all generated modules - **Manifest / schema file** — produce a JSON manifest listing all interfaces, their annotations, and relationships ## Multi-Format Support A single plugin can handle multiple output formats. A common pattern is to have one format for static type declarations and another for runtime metadata or executable code: ```typescript import { DEFAULT_FORMAT } from '@atscript/core' render(doc, format) { if (format === 'types' || format === DEFAULT_FORMAT) { return [{ fileName: `${doc.name}.types.out`, content: generateTypes(doc) }] } if (format === 'runtime') { return [{ fileName: `${doc.name}.runtime.out`, content: generateRuntime(doc) }] } } ``` `DEFAULT_FORMAT` is a well-known constant triggered by the VSCode extension on save and by the CLI when no `-f` flag is given. Handle it for your plugin's primary output — typically type declarations. See [VSCode & Build Integration](/plugin-development/tooling-integration#the-default-format-constant) for the full details. If your plugin needs extra sidecar files, prefer generating them through the CLI or explicit formats. Bundlers only need the JavaScript module they import, not arbitrary extra artifacts. Users trigger specific formats via the CLI: ```bash npx asc # all plugins' default output (DEFAULT_FORMAT) npx asc -f types # only the 'types' format npx asc -f runtime # only the 'runtime' format ``` ## Two-Pass Rendering When generated output contains declarations that reference each other, you may need a two-pass approach: **Pass 1**: Iterate `doc.nodes` and emit declaration shells — names and basic structure, without populating cross-references. **Pass 2**: Go back over the collected declarations and fill in metadata, annotations, and references to other declarations that are now guaranteed to exist. This pattern arises in any target language where a symbol must be declared before it can be referenced. By deferring metadata population to a second pass, all declarations are available for cross-referencing. ## Next Steps - [Plugin Hooks Reference](/plugin-development/plugin-hooks) — complete reference for all hooks - [Validation Specification](/plugin-development/validation-spec) — implement data validation against your generated types - [Testing Plugins](/plugin-development/testing-plugins) — test your code generator with snapshots - [VSCode & Build Integration](/plugin-development/tooling-integration) — integrate with the build pipeline and editor --- URL: "atscript.dev/plugin-development/plugin-hooks" LLMS_URL: "atscript.dev/plugin-development/plugin-hooks.md" --- # Plugin Hooks Reference This page documents all six hooks in the `TAtscriptPlugin` interface — their signatures, when they fire, execution semantics, and practical examples. ## Hook Summary | Hook | When | Return | Execution | | -------------------------------- | ----------------------- | ------------------------------ | ------------------------------------ | | `config(config)` | At startup (once) | `TAtscriptConfig \| undefined` | Sequential, merged with `defu` | | `resolve(id)` | Opening a document | `string \| undefined` | Sequential, last non-undefined wins | | `load(id)` | Loading source content | `string \| undefined` | Sequential, first non-undefined wins | | `onDocument(doc)` | After parsing | `void` | Sequential, all plugins called | | `render(doc, format)` | Code generation | `TPluginOutput[] \| undefined` | Sequential, outputs concatenated | | `buildEnd(output, format, repo)` | After all docs rendered | `void` | Sequential, all plugins called | All hooks can be synchronous or async (return `Promise`). All are optional — implement only the ones you need. ## config() ```typescript config?( config: TAtscriptConfig ): Promise | TAtscriptConfig | undefined ``` Called once during `PluginManager` initialization, before any documents are opened. This is where you register primitives, annotations, and config options. **Execution**: Plugins are called sequentially in array order. Each plugin receives the accumulated config. Return values are merged using `defu` (deep defaults — the first defined value wins). **Example — Register primitives and annotations:** ```typescript config() { return { primitives: { geo: { extensions: { latitude: { type: 'number', annotations: { 'expect.min': -90, 'expect.max': 90 }, }, longitude: { type: 'number', annotations: { 'expect.min': -180, 'expect.max': 180 }, }, }, }, }, annotations: { geo: { coordinates: new AnnotationSpec({ nodeType: ['interface'], description: 'GeoJSON coordinate system', }), }, }, } } ``` **Example — Set unknownAnnotation policy:** ```typescript config() { return { unknownAnnotation: 'warn', // 'error' | 'warn' | 'allow' } } ``` ## resolve() ```typescript resolve?(id: string): Promise | string | undefined ``` Called when the repo opens a document or resolves an import. Allows you to remap module IDs — useful for virtual modules, path aliases, or rewriting import paths. **Execution**: All plugins are called. The last non-`undefined` return wins. If all plugins return `undefined`, the original ID is used. **Example — Virtual module:** ```typescript resolve(id) { if (id.includes('virtual:generated-types')) { return '/absolute/path/to/generated-types.as' } } ``` **Example — Path alias:** ```typescript resolve(id) { if (id.startsWith('@models/')) { return id.replace('@models/', '/project/src/models/') } } ``` ## load() ```typescript load?(id: string): Promise | string | undefined ``` Called to get the source content for a document. Allows plugins to provide virtual file content without a real file on disk. **Execution**: Plugins are called sequentially. The first to return a non-`undefined` string wins (early exit). If no plugin provides content, the file is read from disk. **Example — Virtual file:** ```typescript load(id) { if (id.endsWith('generated-enums.as')) { return ` export type Status = "active" | "inactive" | "pending" export type Role = "admin" | "user" | "guest" ` } } ``` **Example — Preprocessing:** ```typescript load(id) { if (id.endsWith('.as.template')) { const raw = readFileSync(id.replace('.template', ''), 'utf8') return raw.replace(/\$\{VERSION\}/g, '1.0.0') } } ``` ## onDocument() ```typescript onDocument?(doc: AtscriptDoc): Promise | void ``` Called after a document is parsed and its AST is fully built. All plugins receive the call in sequence — there is no early exit. **Use cases**: Post-parse processing, injecting virtual properties, running custom validation, patching the AST. **Example — Add computed fields:** ```typescript onDocument(doc) { for (const node of doc.nodes) { if (isInterface(node)) { const struc = node.getDefinition() if (isStructure(struc) && node.props.has('firstName') && node.props.has('lastName')) { struc.addVirtualProp({ name: 'fullName', type: 'string', documentation: 'Computed: firstName + lastName', }) } } } } ``` ## render() ```typescript render?( doc: AtscriptDoc, format: TAtscriptRenderFormat ): Promise | TPluginOutput[] | undefined ``` Called once per document per format during the build phase. This is the primary code generation hook. **Execution**: All plugins are called. Their outputs are concatenated — multiple plugins can produce output for the same document and format. **Return type**: Array of `{ fileName: string, content: string }`. Return an empty array or `undefined` to produce no output for this format. **Example — Primary output with DEFAULT_FORMAT:** Plugins should handle `DEFAULT_FORMAT` for output that is essential during development (e.g., type declarations). This format is triggered by the VSCode extension on save and by the CLI when no `-f` flag is given. See [VSCode & Build Integration](/plugin-development/tooling-integration#the-default-format-constant) for details. ```typescript import { DEFAULT_FORMAT } from '@atscript/core' render(doc, format) { if (format === 'dts' || format === DEFAULT_FORMAT) { return [{ fileName: `${doc.name}.d.ts`, content: new TypeRenderer(doc).render(), }] } if (format === 'js') { return [{ fileName: `${doc.name}.js`, content: new RuntimeRenderer(doc).render(), }] } } ``` **Example — Multiple output files per document:** ```typescript render(doc, format) { if (format === 'graphql') { return [ { fileName: `${doc.name}.graphql`, content: generateSchema(doc) }, { fileName: `${doc.name}.resolvers.ts`, content: generateResolvers(doc) }, ] } } ``` ## buildEnd() ```typescript buildEnd?( output: TOutput[], format: TAtscriptRenderFormat, repo: AtscriptRepo ): Promise | void ``` Called once after all documents have been rendered for a given format. Use this for cross-document aggregation. **Parameters**: - `output` — Mutable array of all output files. You can push new files or modify existing ones. - `format` — The format string (same as passed to `render()`). - `repo` — The `AtscriptRepo` instance for querying across all documents. **`TOutput` extends `TPluginOutput`** with: - `source` — The source document path - `target` — The resolved output file path **Example — Generate an index file:** ```typescript async buildEnd(output, format, repo) { if (format !== 'python') return const exports = output .filter(o => o.fileName.endsWith('.py')) .map(o => { const module = o.fileName.replace('.py', '') return `from .${module} import *` }) output.push({ content: exports.join('\n'), fileName: '__init__.py', source: '', target: '/output/__init__.py', }) } ``` **Example — Collect project-wide metadata:** ```typescript import { DEFAULT_FORMAT } from '@atscript/core' async buildEnd(output, format, repo) { if (format !== 'dts' && format !== DEFAULT_FORMAT) return const annotations = await repo.getUsedAnnotations() const tags = await repo.getPrimitivesTags() output.push({ content: generateGlobalTypes(annotations, tags), fileName: 'global-types.ext', source: '', target: '/output/global-types.ext', }) } ``` ## Plugin Ordering Plugins execute in the order they appear in the `plugins` array: ```typescript export default defineConfig({ plugins: [pluginA(), pluginB(), pluginC()], }) ``` **For `config()`**: Plugin A's return merges first. Since `defu` uses "first defined wins", Plugin A's values take priority over Plugin B's for the same key. Built-in defaults merge last. **For `resolve()`**: All plugins are called; the last non-undefined return wins. So Plugin C's result takes priority if all three return a value. **For `load()`**: First non-undefined return wins. So Plugin A's content takes priority if all three return content. **For `render()`**: All plugins contribute. Their output arrays are concatenated in order (Plugin A's files, then B's, then C's). **For `onDocument()` and `buildEnd()`**: All plugins are called in order with no return value merging. ## Next Steps - [Testing Plugins](/plugin-development/testing-plugins) — test your hooks with Vitest - [VSCode & Build Integration](/plugin-development/tooling-integration) — how hooks are triggered by tooling --- URL: "atscript.dev/plugin-development/primitives-type-tags" LLMS_URL: "atscript.dev/plugin-development/primitives-type-tags.md" --- # Custom Primitives Primitives are the fundamental scalar types in Atscript — `string`, `number`, `boolean`, `decimal`, and their semantic extensions like `string.email` or `number.int`. Plugins can add new primitive types and extensions that work identically to built-in ones: they appear in IntelliSense, carry constraint annotations, and generate appropriate type tags at runtime. For a first plugin, keep the scope small: - start with one scalar extension like `geo.latitude` - attach validation through built-in `@expect.*` annotations via the primitive's `annotations` map - only reach for object, tuple, or phantom primitives when your plugin really needs them ## What Primitives Are A primitive in Atscript has: - An underlying **type** — one of the final scalar types (`string`, `number`, `boolean`, `decimal`, `void`, `null`, `phantom`) or a complex type definition - Optional **documentation** — shown in hover tooltips in the editor - Optional **annotations** — applied automatically to every use of the primitive (e.g., `expect.min: 0`) - Optional **semantic tags** — string identifiers for runtime type discrimination - Optional **extensions** — sub-primitives accessed via dot notation (e.g., `string.email`) When a primitive has extensions, it becomes a namespace. `string` is a usable type on its own, and `string.email` is a more specific variant that inherits everything from `string` plus its own annotations. ## The TPrimitiveConfig Interface Primitives are defined with `TPrimitiveConfig`: ```typescript interface TPrimitiveConfig { type?: TPrimitiveTypeDef documentation?: string tags?: string[] isContainer?: boolean annotations?: Record extensions?: Record> } ``` | Field | Type | Description | | --------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------- | | `type` | `TPrimitiveTypeDef` | Underlying scalar or complex type. Inherited from parent if omitted. | | `documentation` | `string` | Markdown text shown in IntelliSense. Inherited from parent if omitted. | | `tags` | `string[]` | Semantic tags for runtime discrimination (e.g., `['email']`). | | `isContainer` | `boolean` | If `true`, the primitive itself cannot be used — one of its extensions must be chosen. | | `annotations` | `Record` | Annotations applied automatically wherever this primitive is used. Merged with parent's map. | | `extensions` | `Record>` | Sub-primitives accessible via dot notation. | ### The `annotations` Map Each entry maps a fully-qualified annotation name (the same name used in `.as` files) to its value. The value shape matches the annotation's argument list: ```typescript type TPrimitiveAnnotationArg = string | number | boolean type TPrimitiveAnnotationArgs = Record type TPrimitiveAnnotationValue = | boolean // no-arg annotation (e.g., 'expect.int': true) | string // single string arg | number // single number arg (e.g., 'expect.min': 0) | TPrimitiveAnnotationArgs // multi named args (e.g., { pattern: '...', message: '...' }) | (TPrimitiveAnnotationArg | TPrimitiveAnnotationArgs)[] // multiple occurrences (with multiple: true) ``` Annotations declared in a primitive's `annotations` map are **identical** to writing the same annotation in `.as` source on every field that uses the primitive. They participate in normal validation and inheritance. ## The Final Scalar Types `TPrimitiveTypeFinal` is the set of underlying scalar kinds a primitive can resolve to: ```typescript type TPrimitiveTypeFinal = | 'string' | 'number' | 'boolean' | 'decimal' | 'void' | 'null' | 'phantom' ``` - `string`, `number`, `boolean` — the standard scalars - `decimal` — string-backed arbitrary-precision numeric (`^[+-]?\d+(\.\d+)?$`). See [Validation Specification](/plugin-development/validation-spec#decimal-format-check). - `void`, `null` — terminal value types - `phantom` — non-data primitive used for runtime-discoverable metadata fields (see [Phantom Primitives](#phantom-primitives)) `never` is also a valid primitive name (no `type` field — represents the impossible type). ## Adding Primitives via config() Register primitives in your plugin's `config()` hook: ```typescript import { createAtscriptPlugin } from '@atscript/core' export const geoPlugin = () => createAtscriptPlugin({ name: 'geo', config() { return { primitives: { geo: { isContainer: true, documentation: 'Geographic data types', extensions: { latitude: { type: 'number', documentation: 'Latitude coordinate (-90 to 90)', tags: ['latitude'], annotations: { 'expect.min': -90, 'expect.max': 90 }, }, longitude: { type: 'number', documentation: 'Longitude coordinate (-180 to 180)', tags: ['longitude'], annotations: { 'expect.min': -180, 'expect.max': 180 }, }, postalCode: { type: 'string', documentation: 'Postal/ZIP code', tags: ['postalCode'], annotations: { 'expect.pattern': { pattern: '^[A-Z0-9 -]{3,10}$', flags: 'i', message: 'Invalid postal code format', }, }, }, }, }, }, } }, }) ``` Usage in `.as` files: ```atscript export interface Location { @meta.label "Latitude" lat: geo.latitude @meta.label "Longitude" lng: geo.longitude @meta.label "ZIP Code" zip: geo.postalCode } ``` ### Real-World Example: Built-In `string.email` The built-in `string` primitive ships with several extensions, including `string.email`: ```typescript // Shape used by Atscript's built-in primitives (excerpt) primitives: { string: { type: 'string', documentation: 'Represents textual data.', extensions: { email: { documentation: 'Represents an email address.', annotations: { 'expect.pattern': { pattern: '^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$', message: 'Invalid email format.', }, }, }, uuid: { documentation: 'Represents a UUID.', annotations: { 'expect.pattern': { pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', flags: 'i', message: 'Invalid UUID format.', }, }, }, }, }, } ``` ::: tip Pattern strings, not regex literals `expect.pattern` accepts the pattern as a **string** (matching how `.as` source writes it), with an optional `flags` field. JavaScript `RegExp` literals are not the wire form — write `pattern: '^[A-Z]+$'`, not `/^[A-Z]+$/`. ::: ## Complex Type Definitions The `type` field accepts `TPrimitiveTypeDef`, which can be: ### Scalar Types A plain string for simple types: ```typescript type: 'string' // textual data type: 'number' // numeric data type: 'boolean' // true/false type: 'decimal' // arbitrary-precision string-backed numeric type: 'void' // no value type: 'null' // null value type: 'phantom' // metadata-only (excluded from generated types and validation) ``` ### Array Type An array of a given element type: ```typescript type: { kind: 'array', of: 'number' } // number[] type: { kind: 'array', of: 'string' } // string[] ``` ### Union Type One of several possible types: ```typescript type: { kind: 'union', items: ['string', 'number'], // string | number } ``` ### Intersection Type A combination of types: ```typescript type: { kind: 'intersection', items: ['string', 'number'], // string & number } ``` ### Tuple Type A fixed-length array with typed positions: ```typescript type: { kind: 'tuple', items: ['number', 'number'], // [number, number] } ``` ### Object Type A structured type with named properties: ```typescript type: { kind: 'object', props: { x: 'number', y: 'number', label: { kind: 'final', value: 'string', optional: true }, }, propsPatterns: {}, } ``` ### Optional Wrapper Any final scalar can be wrapped as optional: ```typescript type: { kind: 'final', value: 'string', optional: true } // string | undefined ``` ### Composing Complex Types Complex types nest arbitrarily. For example, a GeoJSON point: ```typescript geo: { extensions: { point: { type: { kind: 'object', props: { type: 'string', // "Point" coordinates: { kind: 'tuple', items: ['number', 'number'], // [longitude, latitude] }, }, propsPatterns: {}, }, documentation: 'GeoJSON Point with [longitude, latitude] coordinates', }, }, } ``` ## Annotation-Backed Validation Primitives don't run validation themselves. Instead, they declare annotations in their `annotations` map, and the runtime validator enforces those annotations exactly as if they had been written in `.as` source. The built-in `@expect.*` and `@meta.*` annotations cover the common cases: ### For String Types ```typescript annotations: { 'expect.pattern': { pattern: '^[a-z0-9-]+$', message: 'Invalid format' }, 'expect.minLength': 1, 'expect.maxLength': 255, 'meta.required': true, // non-empty, non-whitespace } ``` Multiple patterns can be expressed as an array, but they are **conjunctive** — the value must match **all of them** (same as stacking `@expect.pattern` in `.as` source). Use an array when each rule is a separate requirement with its own error message: ```typescript annotations: { 'expect.pattern': [ { pattern: '[0-9]', message: 'Must contain a digit' }, { pattern: '[A-Z]', message: 'Must contain an uppercase letter' }, ], } ``` For *alternative* formats ("either this shape or that one"), combine them into a single alternation regex — this is how the built-in `string.date` and `string.ip` primitives are defined: ```typescript annotations: { 'expect.pattern': { pattern: '^(?:\\d{4}-\\d{2}-\\d{2}|\\d{2}/\\d{2}/\\d{4})$', message: 'Invalid date format', }, } ``` ### For Number Types ```typescript annotations: { 'expect.min': 0, 'expect.max': 100, 'expect.int': true, } ``` ### For Boolean Types ```typescript annotations: { 'meta.required': true, // must be true (e.g. "accept terms" checkbox) } ``` For full semantics (evaluation order, error formats, partial mode, etc.) see [Validation Specification](/plugin-development/validation-spec). ## Container Primitives and Inheritance When `isContainer: true`, the primitive itself can't be used directly — only its extensions can. This is useful for creating namespaces: ```typescript config() { return { primitives: { color: { isContainer: true, documentation: 'Color value types', extensions: { hex: { type: 'string', documentation: 'Hex color (#RGB or #RRGGBB)', annotations: { 'expect.pattern': { pattern: '^#[\\da-f]{3,8}$', flags: 'i', }, }, }, rgb: { type: { kind: 'tuple', items: ['number', 'number', 'number'] }, documentation: 'RGB color as [r, g, b]', }, name: { type: 'string', documentation: 'Named CSS color', }, }, }, }, } } ``` Using `color` directly produces a compiler error: ```atscript // Error: 'color' is a container — use color.hex, color.rgb, or color.name background: color // OK background: color.hex ``` ### Inheritance Rules Extensions inherit from their parent: - **`type`** — inherited if not specified (so `string.email` has `type: 'string'`) - **`documentation`** — inherited if not specified - **`annotations`** — merged with parent's map (child entries are added on top) - **`tags`** — inherited from parent This means you can define a base type once and specialize it: ```typescript primitives: { id: { type: 'string', documentation: 'An identifier string', annotations: { 'expect.minLength': 1 }, extensions: { uuid: { documentation: 'UUID v4 identifier', annotations: { 'expect.pattern': { pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$', flags: 'i', }, }, // inherits type: 'string' and expect.minLength: 1 }, slug: { documentation: 'URL-safe slug', annotations: { 'expect.pattern': { pattern: '^[a-z0-9-]+$' }, 'expect.maxLength': 100, }, // inherits type: 'string' and expect.minLength: 1 }, }, }, } ``` ### Nested Extensions Extensions can have their own extensions, creating multi-level hierarchies: ```typescript primitives: { number: { type: 'number', extensions: { int: { annotations: { 'expect.int': true }, extensions: { positive: { annotations: { 'expect.min': 0 } }, negative: { annotations: { 'expect.max': 0 } }, }, }, }, }, } ``` This gives you `number.int`, `number.int.positive`, and `number.int.negative` — each inheriting and accumulating annotations from the levels above. ## Semantic Tags Tags are string identifiers attached to primitive instances. They give runtime code a way to discriminate between primitives that share the same underlying scalar type. For example, `string.email` and `string.uuid` both have `type: 'string'` — but their tags (`['email']` vs `['uuid']`) let runtime code tell them apart without inspecting the primitive name. ```typescript primitives: { currency: { type: 'number', tags: ['currency'], documentation: 'A monetary value', extensions: { usd: { tags: ['usd'], documentation: 'US Dollars' }, eur: { tags: ['eur'], documentation: 'Euros' }, }, }, } ``` Tags are inherited — `currency.usd` carries both the `'currency'` tag from its parent and its own `'usd'` tag. Your code generator should make these tags available at runtime so that consuming code can query them (e.g., to choose a currency formatter based on the tag). ## Phantom Primitives Primitives with `type: 'phantom'` represent **non-data properties** — fields that carry metadata and annotations but do not contribute to the data shape of a structure. They exist for runtime discovery (e.g., a form renderer or code generator can find them in the type tree) but they are not part of the actual data model. ### Purpose and Intent The core idea: a structure can contain fields that describe **UI elements, layout hints, or actions** alongside real data fields. These phantom fields: - **Should not appear in the data type** — they don't represent actual data that gets stored, transmitted, or validated. A language plugin should exclude them from the generated type/class shape (or mark them as non-data in whatever way the target language supports). - **Should be skipped by validation** — since they carry no data, validators should ignore them. - **Should be discoverable at runtime** — the whole point is that code walking the type tree can find these fields and their annotations. A form renderer, for example, can use them to insert dividers, headings, or action buttons between real data fields. ### How to Handle Phantom Types in Your Plugin When building a code generator, you need to decide how your target language handles phantom fields. The key principle: **phantom fields must not affect the data contract**. Some approaches: - **Exclude from the generated type entirely** — the simplest approach. The field exists only in the runtime type metadata, not in the language-level type. - **Include but mark as non-data** — in languages with richer type systems, you might use a special marker type, a decorator, or a comment to indicate the field is phantom. - **Separate data type from metadata type** — generate two representations: a clean data type without phantom fields, and a full metadata type that includes them. The important thing is that serialization, deserialization, and validation of real data should never encounter phantom fields. ### Example ```typescript primitives: { ui: { type: 'phantom', isContainer: true, documentation: 'Non-data UI elements for form rendering', extensions: { divider: { documentation: 'Visual divider between sections' }, paragraph: { documentation: 'Informational text block' }, action: { documentation: 'Interactive element (button, link)' }, }, }, } ``` ```atscript export interface RegistrationForm { @meta.label "Full Name" name: string.required @meta.label "By signing up you agree to our terms." terms: ui.paragraph @meta.label "Submit" @ui.component "primary-button" submit: ui.action } ``` Here `terms` and `submit` are phantom — they describe UI elements, not data. The actual data shape of `RegistrationForm` has only one field: `name`. But a form renderer walking the full type tree finds all three fields in source order and can render a text block and a button alongside the input field. You can detect phantom types in your code generator by checking if a property's resolved type is a primitive with `type: 'phantom'`. Use `doc.unwindType()` to resolve references, then inspect the primitive config. ## Next Steps - [Custom Annotations](/plugin-development/annotation-system) — define your own annotation specs with validation - [Building a Code Generator](/plugin-development/code-generation) — generate output files from your custom types --- URL: "atscript.dev/plugin-development/testing-plugins" LLMS_URL: "atscript.dev/plugin-development/testing-plugins.md" --- # Testing Plugins Testing is where plugin docs become practical. Start with one end-to-end test that: 1. builds a small fixture 2. runs your plugin 3. asserts on generated output or diagnostics Once that works, add snapshots and narrower tests for edge cases. This page covers the standard patterns using Vitest. ## Test Setup Install the test dependencies: ```bash npm install -D vitest @atscript/core ``` Configure Vitest in your package's `vitest.config.ts` (or use the workspace config if you're in the Atscript monorepo): ```typescript import { defineConfig } from 'vitest/config' export default defineConfig({ test: { include: ['src/**/*.spec.ts'], }, }) ``` ## The build() + generate() Pattern The standard test pattern creates a build from fixture files and verifies the output: ```typescript import path from 'path' import { build, AnnotationSpec } from '@atscript/core' import { describe, expect, it } from 'vitest' import { myPlugin } from './plugin' const wd = path.join(path.dirname(import.meta.url.slice(7)), '..') describe('my-plugin', () => { it('should render interface', async () => { const repo = await build({ rootDir: wd, entries: ['test/fixtures/basic.as'], plugins: [myPlugin()], }) const out = await repo.generate({ format: 'myformat' }) expect(out).toHaveLength(1) expect(out[0].fileName).toBe('basic.as.ext') expect(out[0].content).toContain('class User') }) }) ``` ### How It Works 1. `build()` creates an `AtscriptRepo`, opens and parses all `.as` files listed in `entries` 2. `repo.generate({ format })` calls `render(doc, format)` for each document, collecting all output 3. You assert on the output array — file count, file names, and content ### Registering Test Annotations If your fixtures use annotations, register them in the `build()` config: ```typescript const annotations = { label: new AnnotationSpec({ argument: { name: 'value', type: 'string' }, }), tag: new AnnotationSpec({ multiple: true, mergeStrategy: 'append', argument: { name: 'value', type: 'string' }, }), } const repo = await build({ rootDir: wd, entries: ['test/fixtures/annotated.as'], plugins: [myPlugin()], annotations, }) ``` ## Snapshot Testing For code generation, snapshot tests are the most effective approach — they catch any unexpected change in the generated output. ### Using toMatchFileSnapshot() Vitest's `toMatchFileSnapshot()` stores each snapshot as a separate file, making diffs easy to review: ```typescript it('should generate correct output', async () => { const repo = await build({ rootDir: wd, entries: ['test/fixtures/interface.as'], plugins: [myPlugin()], annotations, }) const out = await repo.generate({ format: 'myformat' }) await expect(out[0].content).toMatchFileSnapshot( path.join(wd, 'test/__snapshots__/interface.ext') ) }) ``` On the first run, the snapshot file is created. On subsequent runs, the output is compared against the saved snapshot. ### Updating Snapshots When you intentionally change the code generator, update snapshots with: ```bash vitest run -u ``` ### Organizing Snapshots A clean directory layout keeps fixtures and snapshots organized: ``` test/ fixtures/ interface.as type.as imports.as __snapshots__/ interface.ext type.ext imports.ext ``` Each fixture has a corresponding snapshot file. This makes it easy to review generated output by simply opening the snapshot file. ## Testing Annotations ### Verifying Registration The easiest way to verify a primitive is registered is to use it in a fixture and check that it parses with no diagnostics: ```atscript // test/fixtures/uses-geo.as export interface Location { lat: geo.latitude lng: geo.longitude } ``` ```typescript import { build } from '@atscript/core' import { myPlugin } from './plugin' it('should expose geo.latitude / geo.longitude', async () => { const repo = await build({ rootDir: wd, entries: ['test/fixtures/uses-geo.as'], plugins: [myPlugin()], }) const diagnostics = await repo.diagnostics() const [, messages] = [...diagnostics.entries()][0] expect(messages.filter(m => m.severity === 1)).toHaveLength(0) }) ``` The same trick works for annotations — write a fixture that uses each annotation and assert that no errors are produced. A fixture referencing an unregistered primitive or annotation yields a diagnostic, so the absence of errors confirms the registration. ### Verifying Annotation Behavior Test that annotations produce correct metadata in the generated output: ```typescript it('should emit annotation metadata', async () => { const repo = await build({ rootDir: wd, entries: ['test/fixtures/annotated.as'], plugins: [myPlugin()], annotations: { label: new AnnotationSpec({ argument: { name: 'value', type: 'string' }, }), }, }) const out = await repo.generate({ format: 'myformat' }) expect(out[0].content).toContain('label') expect(out[0].content).toContain('Full Name') }) ``` ## Testing Diagnostics Diagnostics are error and warning messages from annotation validation. Test them using `repo.diagnostics()`: ```typescript it('should report error for invalid annotation target', async () => { const repo = await build({ rootDir: wd, entries: ['test/fixtures/invalid-annotation.as'], plugins: [myPlugin()], }) const diagnostics = await repo.diagnostics() // diagnostics is a Map const [, messages] = [...diagnostics.entries()][0] expect(messages.length).toBeGreaterThan(0) expect(messages[0].severity).toBe(1) // 1 = Error expect(messages[0].message).toContain('only valid on') }) ``` ### Testing Valid Files Have No Errors ```typescript it('should have no diagnostics for valid file', async () => { const repo = await build({ rootDir: wd, entries: ['test/fixtures/valid.as'], plugins: [myPlugin()], }) const diagnostics = await repo.diagnostics() const [, messages] = [...diagnostics.entries()][0] expect(messages.filter(m => m.severity === 1)).toHaveLength(0) }) ``` ## Fixture File Best Practices ### Keep Fixtures Small and Focused Each fixture should test one concept: ```atscript // test/fixtures/basic-interface.as export interface User { name: string email: string.email } ``` ```atscript // test/fixtures/optional-fields.as export interface Settings { theme?: string language?: string notifications: boolean } ``` ```atscript // test/fixtures/with-annotations.as export interface Product { @meta.label "Product Name" @expect.minLength 1 name: string.required @meta.label "Price" @expect.min 0 price: number } ``` ### Test Edge Cases Separately Create dedicated fixtures for: - Empty interfaces - Type aliases (simple, union, intersection) - Nested structures (inline objects) - Array types - Import/export chains - Annotate blocks (mutating and non-mutating) - Phantom types ### Store Fixtures Alongside Source ``` src/ plugin.ts plugin.spec.ts test/ fixtures/ basic.as annotations.as imports.as __snapshots__/ basic.ext annotations.ext imports.ext ``` ## Testing Multiple Formats If your plugin supports multiple formats, test each format separately: ```typescript it('should generate types', async () => { const repo = await build({ rootDir: wd, entries: ['test/fixtures/user.as'], plugins: [myPlugin()], }) const types = await repo.generate({ format: 'types' }) await expect(types[0].content).toMatchFileSnapshot( path.join(wd, 'test/__snapshots__/user.types.out') ) const runtime = await repo.generate({ format: 'runtime' }) await expect(runtime[0].content).toMatchFileSnapshot( path.join(wd, 'test/__snapshots__/user.runtime.out') ) }) ``` ## Testing buildEnd Output The `buildEnd` hook adds files to the output array. Test it by checking for extra files: ```typescript it('should generate index file via buildEnd', async () => { const repo = await build({ rootDir: wd, entries: ['test/fixtures/user.as', 'test/fixtures/post.as'], plugins: [myPlugin()], }) const out = await repo.generate({ format: 'myformat' }) // buildEnd should have added an extra file const indexFile = out.find(o => o.fileName === 'index.generated.ext') expect(indexFile).toBeDefined() expect(indexFile!.content).toContain('user') expect(indexFile!.content).toContain('post') }) ``` ## Next Steps - [VSCode & Build Integration](/plugin-development/tooling-integration) — deploy your tested plugin into the toolchain - [Building a Code Generator](/plugin-development/code-generation) — the code generation patterns being tested --- URL: "atscript.dev/plugin-development/tooling-integration" LLMS_URL: "atscript.dev/plugin-development/tooling-integration.md" --- # VSCode & Build Integration Once your plugin is built and tested, you need to integrate it with the developer toolchain — the CLI, build tools, and the VSCode extension. This page covers how each tool triggers code generation and how your plugin fits in. ## Three Entry Points Code generation flows through the same `build()` → `generate()` → `write()` pipeline regardless of how it's triggered: | Entry Point | Trigger | Formats | | -------------------------- | ---------------------------- | ------------------------------------------------------- | | **CLI** (`asc`) | Manual invocation | `DEFAULT_FORMAT` when no `-f` flag; any format via `-f` | | **Build tools** (unplugin) | File change during build/dev | `'js'` for the imported module | | **VSCode extension** | File save | `DEFAULT_FORMAT` | All three discover your plugin through `atscript.config.ts` and call `render(doc, format)` on every registered plugin. ## The `DEFAULT_FORMAT` Constant `@atscript/core` exports a well-known format constant called `DEFAULT_FORMAT`. This is the format passed to `render()` and `buildEnd()` when: - A `.as` file is **saved in VSCode** (or any editor with Atscript support) - The CLI is run **without a `-f` flag** (`npx asc`) Every plugin that produces output essential for the development experience should handle `DEFAULT_FORMAT` alongside its own named formats: ```typescript import { createAtscriptPlugin, DEFAULT_FORMAT } from '@atscript/core' export const myPlugin = () => createAtscriptPlugin({ name: 'my-plugin', render(doc, format) { if (format === 'myformat' || format === DEFAULT_FORMAT) { // Generate the primary output for this plugin return [{ fileName: `${doc.name}.ext`, content: generate(doc) }] } }, }) ``` This way, when a user saves a file in their editor, **all plugins** generate their primary output automatically — not just a single hardcoded format. ::: tip When to handle DEFAULT_FORMAT Handle `DEFAULT_FORMAT` for output that developers need continuously during editing — type declarations, type stubs, schema files. Don't handle it for expensive or one-off outputs like bundled runtime code or documentation generation — those should only run via explicit CLI invocation with `-f`. ::: ## CLI Usage The Atscript CLI (`asc`) triggers code generation. When called without `-f`, it uses `DEFAULT_FORMAT`, which triggers every plugin's primary output: ```bash # Generate all plugins' default output npx asc # Generate with a specific format npx asc -f myformat # Generate TypeScript declarations only npx asc -f dts # Generate JavaScript runtime only npx asc -f js # Specify a config file npx asc -c path/to/atscript.config.ts -f myformat # Check diagnostics without generating files npx asc --noEmit ``` When `-f` is provided, it's passed directly to `render(doc, format)` as the format string. Your plugin only needs to handle the format strings it cares about: ```typescript render(doc, format) { if (format === 'myformat' || format === DEFAULT_FORMAT) { return [{ fileName: `${doc.name}.ext`, content: generate(doc) }] } // Return nothing for other formats } ``` ### Watch Mode The CLI doesn't have a built-in `--watch` flag. For file-watching, use a tool like `chokidar-cli` or `nodemon`: ```bash # Using chokidar npx chokidar "src/**/*.as" -c "npx asc -f myformat" # Using nodemon npx nodemon --watch src --ext as --exec "npx asc -f myformat" ``` ## Build Tool Integration (unplugin) The `unplugin-atscript` package integrates Atscript into Vite, Rollup, Webpack, Rspack, esbuild, and other bundlers. It transforms `.as` imports into JavaScript at build time. ### Setup ```bash npm install -D unplugin-atscript @atscript/typescript ``` ```typescript // vite.config.ts import { defineConfig } from 'vite' import atscript from 'unplugin-atscript/vite' export default defineConfig({ plugins: [atscript()], }) ``` ### How It Works 1. When your bundler encounters an `import { User } from './user.as'`, the unplugin intercepts it 2. It loads the Atscript config (including your plugin) and parses the `.as` file 3. It calls `render(doc, 'js')` to generate JavaScript output 4. The first generated JS module is passed to the bundler as the imported module content Your plugin still participates automatically: - if it contributes primitives or annotations in `config()`, those affect parsing and generation during the build - if it transforms the document in `onDocument()`, those changes affect later rendering - if it produces the primary `js` module output, that output can be used by the bundler Build-tool integration is not the right place for arbitrary extra files. If your plugin needs sidecar artifacts, generate them through the CLI or another explicit format instead of relying on the bundler import path. ### Supported Bundlers | Bundler | Import | | -------- | ---------------------------- | | Vite | `unplugin-atscript/vite` | | Rollup | `unplugin-atscript/rollup` | | Rolldown | `unplugin-atscript/rolldown` | | Webpack | `unplugin-atscript/webpack` | | Rspack | `unplugin-atscript/rspack` | | esbuild | `unplugin-atscript/esbuild` | | Farm | `unplugin-atscript/farm` | ## VSCode Extension On-Save The Atscript VSCode extension triggers code generation on every save of a `.as` file. It passes `DEFAULT_FORMAT` to the build pipeline, so every registered plugin that handles `DEFAULT_FORMAT` generates its output automatically. ### How It Works When you save a `.as` file in VSCode: 1. The extension's language server receives the `textDocument/didSave` notification 2. It opens the document and resolves the `atscript.config` 3. It creates a `BuildRepo` for just the saved file 4. It calls `bld.write({ format: DEFAULT_FORMAT })` — triggering all plugins' primary output This means your plugin's `render()` hook fires on every save, as long as it handles `DEFAULT_FORMAT`. The TypeScript plugin generates `.d.ts` files, and a custom plugin can generate other primary development-time artifacts the same way. ### What Not to Generate On-Save On-save generation should be fast and produce only what's needed for the development experience. Avoid handling `DEFAULT_FORMAT` for: - Expensive computations (full bundled runtime code) - Large aggregate outputs (project-wide manifests) - Outputs that don't affect the editing experience For those, provide a separate named format that users trigger via `npx asc -f myformat`. ## Package Distribution When publishing your plugin as an npm package: ### Package Structure ``` my-atscript-plugin/ src/ plugin.ts # Plugin factory function index.ts # Re-export dist/ index.mjs # ESM build index.cjs # CJS build index.d.ts # Type declarations package.json ``` ### package.json ```json { "name": "atscript-plugin-myformat", "version": "1.0.0", "main": "dist/index.cjs", "module": "dist/index.mjs", "types": "dist/index.d.ts", "peerDependencies": { "@atscript/core": ">=0.1.0" }, "devDependencies": { "@atscript/core": "^0.1.0", "vitest": "^3.0.0" } } ``` ### Key Points - Declare `@atscript/core` as a **peer dependency** — users must install it alongside your plugin - Export a **factory function** (not a plugin instance) so users call `myPlugin()` in their config - Use `createAtscriptPlugin()` for type safety - Follow the naming convention: `atscript-plugin-*` or `@scope/atscript-plugin-*` ### User Configuration Users add your plugin to their `atscript.config.ts`: ```typescript import { defineConfig } from '@atscript/core' import { tsPlugin } from '@atscript/typescript' import { myPlugin } from 'atscript-plugin-myformat' export default defineConfig({ rootDir: 'src', plugins: [tsPlugin(), myPlugin()], }) ``` And run generation: ```bash # All plugins' default output npx asc # Only your specific format npx asc -f myformat ``` ## Summary | Feature | CLI | Build Tools | VSCode | | ------------------------ | ------------------ | ------------------- | --------- | | `DEFAULT_FORMAT` | When no `-f` flag | No | On save | | Custom formats | Any (`-f` flag) | `'js'` only | No | | Primitives & annotations | Active | Active | Active | | Diagnostics | Via `--noEmit` | On error | Real-time | | File watching | Via external tools | Built-in (HMR) | On save | | Output writing | Automatic | In-memory (bundled) | Automatic | Your plugin's primitives and annotations work everywhere — the editor provides IntelliSense and validation regardless of which formats your plugin generates. Code generation is triggered differently depending on the tool, but the `render()` hook works identically in all cases. ## Next Steps - [Overview](/plugin-development/) — back to the guide start - [Building a Code Generator](/plugin-development/code-generation) — the render() hook in detail - [Testing Plugins](/plugin-development/testing-plugins) — verify everything works before publishing --- URL: "atscript.dev/plugin-development/validation-spec" LLMS_URL: "atscript.dev/plugin-development/validation-spec.md" --- # Validation Specification This page is a **language-agnostic specification** for implementing data validation against Atscript type definitions. It defines the exact behavior your validator must follow — what to check, in what order, and what errors to produce. The TypeScript `Validator` class in `packages/typescript/src/runtime/validator.ts` is the reference implementation that follows this specification. ## Two Constraint Dimensions Atscript separates two completely orthogonal concerns. Understanding this distinction is essential before implementing anything else. ### Presence: The Optional Flag The `?` token on a property controls whether the field may be absent (undefined): ```atscript interface User { name: string // required — must exist in the data nickname?: string // optional — may be absent } ``` At runtime, the `optional` flag is set on the type definition for that field. Your validator's first check for every field should be: > If `optional` is true AND the value is `undefined` → **pass immediately**, skip all further checks. This check happens **before** type dispatch and before any constraint annotations are evaluated. ### Content: @meta.required `@meta.required` is a **value constraint**, not a presence constraint. It controls what counts as a valid value when the field _is_ present: - **For strings**: the value must contain at least one non-whitespace character. Empty strings and whitespace-only strings fail. - **For booleans**: the value must be `true`. `false` fails. `@meta.required` only applies to `string` and `boolean` types. ### The Shorthand: string.required / boolean.required `string.required` and `boolean.required` are primitive extensions that automatically inject a `@meta.required` annotation. They are not separate types — `name: string.required` is exactly equivalent to: ```atscript @meta.required name: string ``` ### The Four Combinations These two dimensions compose independently: | `.as` syntax | Absent (`undefined`) | Empty string `""` | Non-empty string | | ------------------------ | -------------------- | ----------------- | ---------------- | | `name: string` | Fail | Pass | Pass | | `name: string.required` | Fail | Fail | Pass | | `name?: string` | Pass | Pass | Pass | | `name?: string.required` | Pass | Fail | Pass | The last row is the subtle case: the field is optional (may be absent), but _if present_, it must be non-empty. ## Type Dispatch ### The Algorithm Every validation call follows this sequence: 1. **Optional check**: If the field is optional AND the value is `undefined` → return pass 2. **Plugins**: Run validator plugins in order. If any returns a definitive result (`true` or `false`), use it. If all return "no opinion" → continue 3. **Type dispatch**: Branch on the type kind: | Kind | Handler | | --------------------- | ------------------------------- | | Primitive (scalar) | [Primitives](#primitives) | | Literal (const value) | [Literals](#literals) | | Phantom | [Phantom](#phantom-types) | | Object | [Objects](#objects) | | Array | [Arrays](#arrays) | | Union | [Unions](#unions) | | Intersection | [Intersections](#intersections) | | Tuple | [Tuples](#tuples) | ### Primitives Check that the runtime type of the value matches the declared primitive type: | Declared type | Check | | ------------- | ----------------------------------------------------------------------- | | `string` | `typeof value === 'string'` | | `number` | `typeof value === 'number'` | | `boolean` | `typeof value === 'boolean'` | | `decimal` | `typeof value === 'string'` AND value matches `^[+-]?\d+(\.\d+)?$` | | `null` | `value === null` | | `undefined` | `value === undefined` | | `any` | Always pass | | `never` | Always fail | ::: warning Array disambiguation In languages where arrays are a subtype of objects (like JavaScript), check for arrays first: `Array.isArray(value) ? 'array' : typeof value`. An array should not match `'object'`. ::: After the type check passes, run [constraint annotations](#constraint-annotations) for that type (`@expect.*`, `@meta.required`). If the type check fails, emit an error like: `"Expected string, got number"`. #### `decimal` format check `decimal` is a string-backed primitive that preserves arbitrary-precision numerics. The runtime type is `string`, but the validator also enforces a canonical format so every downstream consumer (SQL DECIMAL columns, Mongo, JSON transport) sees the same shape. The format `^[+-]?\d+(\.\d+)?$` is intentionally strict: - **Non-empty**: at least one digit is required. `""` is rejected. - **Optional sign**: `+`, `-`, or none. - **Integer part required**: `.5` is rejected (some DB engines reject it on input). - **Optional fractional part**: if `.` is present, at least one digit must follow. `5.` is rejected. - **No scientific notation**: `"1e3"` is rejected — scientific form is a `number`-think shape that silently widens precision. - **No whitespace, thousands separators, or locale formatting**: those are presentation concerns, not on-wire form. - **No `NaN` / `Infinity`**: no DECIMAL column on any supported engine can store these. | Sample | Result | | ------------- | --------- | | `"0"` | Pass | | `"0.000"` | Pass | | `"-12.34"` | Pass | | `"+5"` | Pass | | `""` | Fail | | `".5"` | Fail | | `"5."` | Fail | | `"1.2.3"` | Fail | | `" 1.5 "` | Fail | | `"1,000"` | Fail | | `"1e3"` | Fail | | `"NaN"` | Fail | | `"-Infinity"` | Fail | | `123` (num) | Fail | Emit `"Expected string (decimal), got "` when the runtime type is wrong, and `"Invalid decimal format: "` when the type is `string` but the format check fails. The two failures are reported separately so callers can tell "wrong type" from "malformed digit string" apart. ### Literals A literal (const) type has a specific value baked into the type definition (e.g., `"active"`, `42`, `true`). Validate with strict equality: ``` if value !== expected_value → fail ``` Error: `"Expected 42, got 100"` ### Phantom Types Phantom types always pass validation. They represent non-data fields — runtime-discoverable metadata that carries no actual value. **In standalone validation**: return pass immediately. **Inside object validation**: skip phantom-typed properties entirely. Do not validate them, and do not count them as declared properties. If the actual data object has a key whose name matches a phantom property, treat it as an [unknown property](#unknown-properties-policy). See [Custom Primitives — Phantom Primitives](/plugin-development/primitives-type-tags#phantom-primitives) for the design intent. ### Objects 1. **Type check**: value must be a non-null, non-array object 2. **Iterate declared properties**: for each property in the type definition: - Skip phantom-typed properties - If the value is `undefined` and [partial mode](#partial-validation) applies → skip - Otherwise, validate recursively (push the property name onto the error path) 3. **Handle unknown keys**: for each key present in the data but not declared in the type → apply the [unknown properties policy](#unknown-properties-policy) Error on type mismatch: `"Expected object"` ### Arrays 1. **Type check**: value must be an array 2. **Length constraints**: check `@expect.minLength` and `@expect.maxLength` on the **element count** (not character length) 3. **Element validation**: validate each element against the declared element type. Push `[index]` onto the error path for each element. 4. **Error accumulation**: continue validating remaining elements after a failure (up to the error limit) Error on type mismatch: `"Expected array"` ### Unions A union (`A | B | C`) passes if the value matches **any one** branch: 1. Try each branch in order 2. First branch that passes → return pass (short-circuit) 3. If no branch passes → emit an aggregate error with details from all branches Error: `"Value does not match any of the allowed types: [string(0)], [number(1)]"` with a `details` array containing the errors from each branch attempt. ::: tip Branch error isolation Each branch attempt should be evaluated in its own error scope. If a branch fails, its errors are captured but not committed to the main error list. Only if _all_ branches fail are the captured errors included as `details` in the aggregate error. ::: ### Intersections An intersection (`A & B`) passes if the value matches **all** items: 1. Validate against each item in order 2. First failure → return fail immediately (short-circuit) ### Tuples A tuple (`[A, B, C]`) is a fixed-length array where each position has a specific type: 1. **Type check**: value must be an array 2. **Length check**: `value.length` must equal the number of items in the tuple type (exactly) 3. **Positional validation**: validate each element against the corresponding type at that index. Push `[index]` onto the error path. Error on wrong length: `"Expected array of length 3"` ## Constraint Annotations Constraint annotations are metadata attached to type definitions that add validation rules beyond basic type checking. They are evaluated **after** the base type check passes. ### String Constraints Applied when the value is a valid string: | Annotation | Condition | Default error | | --------------------------------- | --------------------------- | ------------------------------------------------------------- | | `@meta.required` | `value.trim().length === 0` | `"Must not be empty"` | | `@expect.minLength N` | `value.length < N` | `"Expected minimum length of N characters, got M characters"` | | `@expect.maxLength N` | `value.length > N` | `"Expected maximum length of N characters, got M characters"` | | `@expect.pattern "regex" [flags]` | `!regex.test(value)` | `"Value is expected to match pattern \"...\""` | **Evaluation order**: `@meta.required` first (fail fast on empty), then `@expect.minLength`, then `@expect.maxLength`, then `@expect.pattern`. **Pattern stacking**: `@expect.pattern` can appear multiple times on the same field (`multiple: true`, `mergeStrategy: 'append'`). All patterns must match — the first failing pattern produces the error. ### Number Constraints Applied when the value is a valid number: | Annotation | Condition | Default error | | --------------- | ----------------- | ----------------------------- | | `@expect.int` | `value % 1 !== 0` | `"Expected integer, got N"` | | `@expect.min N` | `value < N` | `"Expected minimum N, got M"` | | `@expect.max N` | `value > N` | `"Expected maximum N, got M"` | **Evaluation order**: `@expect.int` first, then `@expect.min`, then `@expect.max`. ### Boolean Constraints Applied when the value is a valid boolean: | Annotation | Condition | Default error | | ---------------- | ---------------- | ------------------- | | `@meta.required` | `value !== true` | `"Must be checked"` | ### Array Constraints Applied at the array level (before element validation): | Annotation | Condition | Default error | | --------------------------- | ------------------ | --------------------------------------------------- | | `@expect.minLength N` | `value.length < N` | `"Expected minimum length of N items, got M items"` | | `@expect.maxLength N` | `value.length > N` | `"Expected maximum length of N items, got M items"` | | `@expect.array.uniqueItems` | duplicate found | `"Duplicate items are not allowed"` | Note: for strings, the messages say "characters"; for arrays, the messages say "items". **Evaluation order**: `@expect.minLength` first, then `@expect.maxLength`, then `@expect.array.uniqueItems` (before element validation). #### Unique Items `@expect.array.uniqueItems` enforces that no two elements in the array are equal. How equality is determined depends on the element type and `@expect.array.key` annotations: 1. **Primitive arrays** (`string[]`, `number[]`) — duplicates are detected by deep equality (JSON serialization). 2. **Object arrays with `@expect.array.key` fields** — uniqueness is checked by key fields only. Multiple key fields form a composite key. 3. **Object arrays without key fields** — duplicates are detected by deep equality of the whole object. `@expect.array.key` is a property-level annotation placed on fields **inside** the array element type. It marks which fields form the element's identity. Constraints: - The field must be `string` or `number` type - The field cannot be optional (`?`) - Multiple `@expect.array.key` fields form a **composite key** - `@expect.array.key` does **not** enforce uniqueness by itself — it only declares identity fields for lookups and patch operations. Add `@expect.array.uniqueItems` on the array field to enforce uniqueness. ```atscript // Primitive array — no duplicates by deep equality @expect.array.uniqueItems tags: string[] // Object array — unique by composite key (code + region) @expect.array.uniqueItems items: { @expect.array.key code: string @expect.array.key region: string quantity: number }[] ``` ### Custom Error Messages All constraint annotations support an optional trailing `message` argument in `.as` files: ```atscript @expect.minLength 3 "Name is too short" @meta.required "This field cannot be blank" ``` When a custom message is provided, use it instead of the default error text. How your code generator stores annotation arguments at runtime is entirely up to your plugin — there is no prescribed metadata shape. The spec only requires that the constraint value and the optional custom message are both accessible to the validator at runtime. ## Error Reporting ### Error Structure Each validation error has: ``` { path: string // dot-separated location (e.g., "address.city") message: string // human-readable error description details?: Error[] // nested errors (used for union branch failures) } ``` ### Path Tracking Maintain a path stack during recursive validation. Push property names when entering object fields, push index identifiers when entering array elements, and pop when leaving. Join the stack into a human-readable location string for error messages. The exact path format (dot-separated, bracket notation, or a mix) is up to your implementation. The important thing is that the path clearly identifies the location of the error in the data structure. ### Error Limit Accept a configurable maximum number of errors (default: 10). Once exceeded, stop collecting and return failure early. This prevents excessive error output on deeply invalid data. ### Union Error Aggregation When a union fails (no branch matches), produce a single top-level error with all branch errors nested in `details`. This lets the consumer see exactly why each branch was rejected. ## Partial Validation Partial validation relaxes presence checks for required fields. It is useful for validating partial updates (PATCH operations) where only some fields are provided. ### Modes | Mode | Behavior | | ----------------- | ------------------------------------------------------------- | | `false` (default) | All required props must be present | | `true` | Skip undefined checks at the top-level object only | | `'deep'` | Skip undefined checks at all nesting levels | | Function | Called per-object to decide whether to apply partial behavior | ### Interaction with Content Constraints Partial mode only affects **presence** — whether an `undefined` value for a required field is accepted. When a value _is_ present, all content constraints (`@meta.required`, `@expect.*`) still run normally. Example: with `partial: true` and `name: string.required`: - `{ }` (name absent) → **pass** (partial skips the presence check) - `{ name: "" }` (name present but empty) → **fail** (`@meta.required` still rejects empty strings) - `{ name: "Alice" }` → **pass** ## Unknown Properties Policy When the data object contains keys that are not declared in the type definition: | Policy | Behavior | | ------------------- | ---------------------------------------------------------- | | `'error'` (default) | Emit `"Unexpected property"` error | | `'strip'` | Delete the key from the data object (destructive mutation) | | `'ignore'` | Silently skip | ### Pattern Properties Before applying the unknown property policy, check pattern properties — regex-matched wildcards that accept keys matching a pattern. If a key matches one or more patterns, validate the value against the pattern's type instead of treating it as unknown. If a key matches multiple patterns, try each pattern's type until one passes. If none pass, report validation errors from the first matching pattern. ## Validator Plugins A validator may support a plugin mechanism allowing users to intercept and customize validation: - **Input**: the type definition, the value being validated, and a context object - **Output**: `true` (accept), `false` (reject), or "no opinion" (fall through to built-in logic) - **Execution**: after the optional check, before type dispatch. Plugins run in registration order; the first definitive result wins. Plugins receive access to a context that includes: - An error reporting function - The current path - A recursive validation function (for delegating to sub-validations) - An arbitrary user-supplied context value (e.g., user roles, request metadata) ## Implementation Checklist A complete validator implementation must handle: - [ ] Optional flag — undefined bypass before all other checks - [ ] Plugin hook execution point - [ ] Primitive type checks (string, number, boolean, null, undefined, any, never) - [ ] Literal/const value equality - [ ] Phantom type passthrough - [ ] Object validation with declared property iteration - [ ] Phantom property skipping within objects - [ ] Unknown property policy (error / strip / ignore) - [ ] Pattern property matching for dynamic keys - [ ] Array type check and element recursion - [ ] Array length constraints (`@expect.minLength`, `@expect.maxLength`) - [ ] `@expect.array.uniqueItems` — no duplicate items (by key fields or deep equality) - [ ] Union try-all-branches with error aggregation - [ ] Intersection all-must-pass with fail-fast - [ ] Tuple exact-length positional validation - [ ] `@meta.required` for strings (non-empty) and booleans (must be true) - [ ] `@expect.minLength` / `@expect.maxLength` for strings - [ ] `@expect.min` / `@expect.max` for numbers - [ ] `@expect.int` for numbers - [ ] `@expect.pattern` for strings (with multiple pattern support) - [ ] Custom error messages from annotation arguments - [ ] Path tracking for error location reporting - [ ] Error limit to cap accumulated errors - [ ] Partial validation modes (top-level, deep, function) ## Next Steps - [Custom Primitives](/plugin-development/primitives-type-tags) — how primitive types and their constraints are defined - [Custom Annotations](/plugin-development/annotation-system) — defining your own constraint annotations - [Building a Code Generator](/plugin-development/code-generation) — generating the type tree that validators consume --- URL: "atscript.dev/roadmap" LLMS_URL: "atscript.dev/roadmap.md" --- # Ecosystem The Atscript stack is built on three layers: - **`.as` files** — the single source of truth for data models, metadata, and validation rules. - **HTTP / API runtime** — [wooksjs](https://wooks.moost.org) and [moostjs](https://moost.org) carry routing, controllers, decorators, and adapters. - **Everything else is generated from the model** — TypeScript types, runtime validation, DB schema, REST endpoints, forms, smart tables, and multi-step workflow flows all derive from your `.as` files. ## Status labels - **Available today** — documented and production-ready. - **Planned** — directionally important, not yet shipped. - **Early** — available to explore, docs or workflow may be incomplete. ## Get started - [TypeScript Quick Start](/packages/typescript/quick-start) - [DB Quick Start](https://db.atscript.dev/guide/quick-start) - [UI Quick Start](https://ui.atscript.dev/guide/quick-start) - [Why Atscript?](/packages/typescript/why-atscript)