Skip to content

Instantly share code, notes, and snippets.

@cblanquera
Created August 27, 2025 13:54
Show Gist options
  • Save cblanquera/0bff04d142b1dda0dfd2b4805c9c4f84 to your computer and use it in GitHub Desktop.
Save cblanquera/0bff04d142b1dda0dfd2b4805c9c4f84 to your computer and use it in GitHub Desktop.
Cline Rule Examples

Nest

Hierarchical data management utilities for nested objects and arrays.

type NestMap = {
  database: Record<string, { host: string, port: number}>
};
const config = new Nest<NestMap>({
  database: {
    postgres: {
      host: 'localhost',
      port: 5432
    }
  }
});

Properties

The following properties are available when instantiating a nest.

Property Type Description
data object Raw nested data structure
size number Total number of leaf nodes

Methods

The following methods are available when instantiating a nest.

Retrieving Data

The following examples shows how to retrieve data from a nest.

config.get('database', 'postgres', 'port'); //--> 5432
config.get<number>('database', 'postgres', 'port'); //--> 5432

Parameters

Parameter Type Description
...path string[] A path of object keys leading to the value

Returns

The value given the object key path. If you don't provide a generic, will follow the type map provided.

Setting Data

The following example shows how to add or update data in a nest.

config.set('database', 'postgres', 'port', 5432); //--> Nest

Parameters

Parameter Type Description
...path string[] A path of object keys leading to the value to be set
value any The new value to set

Returns

The nest object to allow chainability.

Checking For Data

The following example shows how to check if an object key path has been set.

config.has('database', 'postgres', 'port'); //--> true

Parameters

Parameter Type Description
...path string[] A path of object keys leading to the value to be set
value any The new value to set

Returns

true if the object key path is set, false otherwise.

Deleting Data

The following example shows how to delete a value.

config.delete('database', 'postgres', 'port'); //--> Nest

Parameters

Parameter Type Description
...path string[] A path of object keys leading to the value to be deleted

Returns

The nest object to allow chainability.

Purging Data

The following example shows how to purge the nest.

config.clear(); //--> Nest

Returns

The nest object to allow chainability.

Getting the Top-Level Keys

The following example shows how to get the top-level keys in a nest.

config.keys(); //--> [ 'database' ]

Returns

An array of object key strings.

Getting the Top-Level Values

The following example shows how to get the top-level values in a nest.

config.values(); //--> [ { postgres: { host: 'localhost', port: 5432 } } ]

Returns

An array of arbitrary values.

Retrieving Data Using a Key Path

The following examples shows how to retrieve data from a nest using a key dot path.

config.path('database.postgres.port'); //--> 5432
config.path<number>('database.postgres.port'); //--> 5432
config.path('database', 'mysql', 3306); //--> 3306

Parameters

Parameter Type Description
path string An object key path separated by dots leading to the value

Returns

The value given the object key path. If you don't provide a generic, will follow the type map provided.

Converting a Nest to a JSON

The following examples shows how to generate a JSON string from a nest.

config.toString('database.postgres.port'); 
//--> { database: { postgres: { host: 'localhost', port: 5432 } } }

Returns

A JSON string derrived from the nest.

{{CLASS NAME}}

{{DESCRIPTION OF CLASS}}

{{EXAMPLE INSTANTIATION}}

Static Properties

The following properties can be accessed directly from {{CLASS NAME}} itself.

Property Type Description
{{TABLE OF PROPERTIES}}

Static Methods

The following methods can be accessed directly from {{CLASS NAME}} itself.

{{SHORT DESCRIPTION OF METHOD, SHOULD START WITH AN ACTION/VERB FOLLOWED BY A NOUN}}

{{LONG DESCRIPTION OF METHOD}}

{{EXAMPLE USAGE OF METHOD}}

Parameters

Parameter Type Description
{{TABLE OF PARAMETERS}}

Returns

{{LAMEN DESCRIPTION OF RETURN TYPE}}

[REPEAT]

Properties

The following properties are available when instantiating a {{CLASS NAME}}.

Property Type Description
{{TABLE OF PROPERTIES}}

Methods

The following methods are available when instantiating a {{CLASS NAME}}.

{{SHORT DESCRIPTION OF METHOD, SHOULD START WITH AN ACTION/VERB FOLLOWED BY A NOUN}}

{{LONG DESCRIPTION OF METHOD}}

{{EXAMPLE USAGE OF METHOD}}

Parameters

Parameter Type Description
{{TABLE OF PARAMETERS}}

Returns

{{LAMEN DESCRIPTION OF RETURN TYPE}}

[REPEAT]

Before contributing to anyone's code, be sure to study how the existing project code is expressed. It's important that all code in a project closely matches each others.

❌ Do not use your own standards irrespective of the existing project's code. Even if they have not explicitly set a standard.

Char Length

Code should be written like a book. A book has pages and each line of each page has a common max length. Imagine a child's eye moving from left to right when reading a book. As that child reads while growing up, their eyes move a lot less. Eventually a person moves from letter to letter, to word by word, to line by line, and some can even do page by page (speed reading). This is because as a person reads more and more they develop their sight peripherals, but there is a limit to how far peripherals can reach (less than 180 degrees). Around when an individual's sight peripherals reach their limit is when the eyes need to move. Eyes moving constantly without rest can cause eye stress. Unlike a book, there is no max length a line of code could be and contributes to eye stress for everyone reading code not having a standard.

Every line of code should not exceed 80 characters.

If you are using VSCode

  1. Go to File > Preferences > Settings.
  2. Search for "rulers"
  3. Click "Edit in settings.json"
  4. Add the following to settings.json
"editor.rulers": [ 72, 80, 100, 120 ],

To keep code readable and prevent horizontal scrolling, follow a consistent line breaking (drop down) style.

//✅ Good
import type { ErrorMap } from '../types.js';

//❌ Bad
import type { 
  ErrorMap 
} from '../types.js';

//✅ Good
import type { 
  ErrorMap,
  SchemaColumnInfo, 
  SchemaSerialOptions 
} from '../types.js';

//❌ Bad
import type { ErrorMap, SchemaColumnInfo, SchemaSerialOptions } from '../types.js';

//✅ Good
function fooBar(zoo: number) {
  //...
}

//❌ Bad 
function fooBar(
  zoo: number
) {
  //...
}

//✅ Good 
function fooBar(
  zoo: number, 
  baz: string, 
  hash: Record<string, string>, 
  list?: string[]
) {
  //...
}

//❌ Bad
function fooBar(zoo: number, baz: string, hash: Record<string, string>, list?: string[]) {
  //...
}

//✅ Good
const fooBar = [ 'foo' ];
//❌ Bad
const fooBar = [
  'foo'
];

//✅ Good
const fooBar = [
  'foo',
  'bar',
  'zoo',
  'baz',
  'foobar',
  'zoobaz',
  'foobarzoo'
];
//❌ Bad
const fooBar = [ 'foo', 'bar', 'zoo', 'baz', 'foobar', 'zoobaz', 'foobarzoo'];

//✅ Good
const fooBar = { foo: 'bar' };
//❌ Bad
const fooBar = { 
  foo: 'bar' 
};

//✅ Good
const fooBar = { 
  foo: 'bar',
  bar: 'bar',
  zoo: 'zoo',
  baz: 'baz',
  foobar: 'foobar',
  zoobaz: 'zoobaz',
  foobarzoo: 'foobarzoo'
};
//❌ Bad
const fooBar = { foo: 'bar', bar: 'bar', zoo: 'zoo', baz: 'baz', foobar: 'foobar', zoobaz: 'zoobaz', foobarzoo: 'foobarzoo' };

Indentation

In all circumstances use 2 spaces for indents.

  • ❌ Do not use 4 spaces for indents.
  • ❌ Do not use [tab] for indents.

Quotes

By default, prefer single quotes (') over double quotes (").

  • Use template quotes (`) for templating use or for quotes with multiple lines.
  • Use double quotes for HTML and React attributes
Context Preferred
JavaScript strings 'single quotes'
Multiline/templates `template string`
HTML/JSX attributes "double quotes"

Semicolons

In general end statements with a semicolon.

//✅ Good
const name = 'John';
//❌ Bad
const name = 'John'

//✅ Good
const { name } = props;
//❌ Bad
const { name } = props

//✅ Good
fooBar();
//❌ Bad
fooBar()

Conditional blocks should not end with semicolons.

//✅ Good
if (true) {}
//❌ Bad
if (true) {};

while and for iteration blocks should not end with semicolons. do while blocks should end with semicolon.

//✅ Good
while (true) {}
//❌ Bad
while (true) {};

//✅ Good
do {} while (true);
//❌ Bad
do {} while (true)

//✅ Good
for (;;) {}
//❌ Bad
for (;;) {};

Function declarations should not have semicolons.

//✅ Good
function fooBar() {}
//❌ Bad
function fooBar() {};

All exported variables should have semicolons.

//✅ Good
export const fooBar = 'foobar';
//❌ Bad
export const fooBar = 'foobar'

//✅ Good
export function fooBar() {};
//❌ Bad
export function fooBar() {}

//✅ Good
export default function fooBar() {};
//❌ Bad
export default function fooBar() {}

Comments

Only use /*...*/ for JSDoc-style comments. In Typescript there is no need to add @param to the JSDoc.

//✅ Good
/**
 * Does the foobar
 */
function fooBar(foo: string) {}

//❌ Bad
/**
 * Does the foobar
 *
 * @param string foo
 */
function fooBar(foo: string) {}

Only use inline comments // inside of functions and blocks. For single line inline comments, do not add a space after //.

//This is a good comment
// This is a bad comment

//This is a long comment that describes
// complicated logic. You can use spaces
// so they know it's connected as one
// comment block

//These are directions
// 1. Step 1
// 2. Step 2

//These are flows
// - if good then send
// - otherwise dont send

Spaces Between Data Block

A data block are non scalar data structure like arrays and objects. Data blocks should have a space in between the opening and closing where values are present. Consider the following examples.

//✅ Good
const fooBar = [];
//❌ Bad
const fooBar = [ ];

//✅ Good
const fooBar = {};
//❌ Bad
const fooBar = { };

//✅ Good
const fooBar = [ 'foo', 'bar' ];
//❌ Bad
const fooBar = ['foo','bar'];

//✅ Good
const fooBar = { foo: 'foo', bar: 'bar' };
//❌ Bad
const fooBar = {foo:'foo',bar:'bar'};

//✅ Good
const fooBar = { foo, bar };
//❌ Bad
const fooBar = {foo,bar};

Naming Conventions

When deciding on a name please consider the following.

  • Other developers need to be able to clearly understand what the variable, function, class, etc. is without looking at the JavaDoc or comment about it.
  • Other foreign developers use translation to figure out what a variable, function, class is.

With that said, avoid single letter or abbreviations for names.

//❌ Bad
const x = 'foobar';
//❌ Bad
const lat = 90.123;
//❌ Bad
const cntry = 'United States';
//❌ Bad
function getCLR() {}

Variables

  • Use camelCase
  • Use nouns for values: user, orderList
  • Variables representing booleans, can use is, has, can, should, valid
  • Variables representing functions, follow function naming
  • Variables representing class definitions, follow class naming

Functions

  • Use camelCase
  • Use verbs or verb phrasing (verb followed by noun): get(), getUser(), submitForm(), resetTimer()
  • For React components, follow class naming

Classes & Components

  • Use PascalCase
  • Use nouns for values: User, EventEmitter, TaskQueue

Folders & Files

In general you should use kebab-case to name your folders and files. Files that export default a class or component should use PascalCase.

Item Naming Example
Files (JS/TS) kebab-case user-service.ts
Classes PascalCase User.ts
Components PascalCase UserCard.tsx
Folders kebab-case user-profile/
Class Index PascalCase User/index.ts
Component Index PascalCase UserCard/index.tsx

Imports

Organize imports by node modules like node:path, node:fs, packages (in node_modules) and local imports as well as type imports and actual imports. Consider the following.

//node
import type { IncomingMessage } from 'node:http';
import { resolve } from 'node:path';
import fs from 'node:fs';
//modules
import type { Request } from '@whatwg/fetch';
import { Mailer, send } from 'simple-mailer'; 
import mustache from 'mustache';
//local
import type { User, Auth } from '../types.js';
import { getUser } from './helpers';
import Session from './Session.js';

When importing native node modules, prefix the name with node:.

//✅ Good
import fs from 'node:fs';
//❌ Bad
import fs from 'fs';

Components

Always wrap components inside parenthesis.

//✅ Good
const AsideMenu = () => (<aside>...</aside>); 
//❌ Bad
const AsideMenu = () => <aside>...</aside>; 
//❌ Bad
const AsideMenu = () => <aside>...</aside>

//✅ Good
function LeftMenu() {
  return (<div>...</div>);
}
//✅ Good
function LeftMenu() {
  return (
    <div>...</div>
  );
}
//❌ Bad
function LeftMenu() {
  return <div>...</div>
}

When defining component prop arguments, it's important that it's easily readable. For complex props consider separating the type into a separate type definition. Consider the following.

//✅ Good
function LeftMenu(props: LeftMenuProps) {
  //props
  const { items, active, error } = props;
}
//✅ Okay
function LeftMenu(props: { items: string[] }) {}
//✅ Okay
function LeftMenu({ items }: { items: string[] }) {}
//✅ Okay
function LeftMenu({ items, active, error }: {
  items: string[],
  active: string,
  error?: string
}) {
  //...
}
//❌ Bad
function LeftMenu({
  items,
  active,
  error
}: {
  items: string[],
  active: string,
  error?: string
}) {
  //...
}

Organize your components in the following order.

function LeftMenu(props: LeftMenuProps) {
  //1. props
  const { items, error, show, active } = props;
  //2. hooks
  const [ opened, open ] = useState(show);
  const [ selected, select ] = useState(active);
  //3. variables
  const icon = opened ? 'chevron-down': 'chevron-left';
  //4. handlers
  const toggle = () => open(opened => !opened);
  //5. effects
  useEffect(() => {
    error && notify('error', error);
  }, []);
  //6. render
  return (
    <aside>...</aside>
  );
}

Instead of setting up multiple hooks in a component, consider making a hook wrapper.

function useLeftMenu(config: LeftMenuProps) {
  //1. config
  const { items, error, show, active } = config;
  //2. hooks
  const [ opened, open ] = useState(show);
  const [ selected, select ] = useState(active);
  //3. variables
  const icon = opened ? 'chevron-down': 'chevron-left';
  //4. handlers
  const toggle = () => open(opened => !opened);
  //5. effects
  useEffect(() => {
    error && notify('error', error);
  }, []);
  //6. Return usable variables
  return { opened, icon, toggle };
}

function LeftMenu(props: LeftMenuProps) {
  //1. hooks
  const { opened, icon, toggle } = useLeftMenu(props);
  //2. render
  return (
    <aside>...</aside>
  );
}

Git Commits

When committing your code please consider the following.

  • Use clear, concise commit messages. Longer messages are encouraged.
  • Start your message with a verb: Added, Created, Removed, etc.
  • Be specific about the file types you are committing: pages, views, model, etc.
  • If the commit is not working yet add (wip) tag which means work in progress.
  • Add the Github Issue number/s (ie #123) related to this commit.
  • Add how long you spent on this commit using short hand time format: 5m meaning 5 minutes, 42h meaning 42 hours. Do not use days, weeks, months.
  • Avoid committing all your unsaved changes blindly. Manually design your commits per issue or logical grouping.
//❌ Bad
updated code
//❌ Bad
login form
//❌ Bad
did this and that and that and this..
✅ Good
"Created login form view components (wip) #123 4h"

Dependencies

  • Only install packages you truly need.
  • Actively remove packages you are no longer using.
  • Prefer native APIs and utilities before reaching for libraries.
  • Document why a non-obvious package is used.

Environment Variables

  • Do not hardcode secrets or API keys.
  • Keep .env files out of git.

Final Notes

  • Syntax and formatting mistakes are generally forgivable as long as they are methodical, logical and consistent.
  • Consistency beats preference. When in doubt, match existing patterns.
  • Don’t be afraid to ask questions or propose improvements.
  • Keep your pull requests small and focused.

as follows:

  1. Documentation Structure:

    • Use separate markdown files for each major component or class (e.g., EventEmitter, ExpressEmitter).
    • Ensure each file follows a consistent structure with sections for features, public properties, methods, and examples.
    • A description should always follow a header. If you can determine a proper description, then talk about what the next sections are about. I you made the same mistake with #### methods. FYI i define headers as lines that start with a hashtag.
  2. Method Documentation:

    • Document each method with a clear description, parameter table, and example usage.
    • Avoid using tables for return values; instead, describe the return type in the method description.
  3. Parameter Tables:

    • Use tables to list parameters for each method, including type, default value (if applicable), and description.
  4. Examples:

    • Provide practical examples for each method to illustrate usage.
    • Ensure examples are relevant and demonstrate typical use cases.
  5. Public vs. Protected/Private:

    • Focus on documenting public properties and methods.
    • Avoid documenting protected or private members unless necessary for understanding public API behavior.
    • Just call public properties, properties in the documentation.
  6. Type Parameters:

    • Clearly define type parameters for generic classes (e.g., ItemQueue<I>, TaskQueue<A>).
    • Provide examples that demonstrate how to declare and use these types.
  7. Feedback Integration:

    • Continuously integrate user feedback to refine documentation.
    • Address specific critiques, such as missing methods or incorrect examples, promptly.
  8. Consistency and Clarity:

    • Maintain consistency in formatting, terminology, and style across all documentation files.
    • Ensure clarity and accessibility for the target audience, including junior developers.
  9. Code Char Length:

Code should be written like a book. A book has pages and each line of each page has a common max length. Imagine a child's eye moving from left to right when reading a book. As that child reads while growing up, their eyes move a lot less. Eventually a person moves from letter to letter, to word by word, to line by line, and some can even do page by page (speed reading). This is because as a person reads more and more they develop their sight peripherals, but there is a limit to how far peripherals can reach (less than 180 degrees). Around when an individual's sight peripherals reach their limit is when the eyes need to move. Eyes moving constantly without rest can cause eye stress. Unlike a book, there is no max length a line of code could be and contributes to eye stress for everyone reading code not having a standard.

Unless it cannot be avoided, every line of code should not exceed 80 characters.

Idea File Format Specification

A comprehensive guide to the .idea schema file format for defining application data structures, relationships, and code generation configurations.

Table of Contents

Introduction

The .idea file format is a declarative schema definition language designed to simplify application development by providing a single source of truth for data structures, relationships, and code generation. It enables developers to define their application's data model once and generate multiple outputs including database schemas, TypeScript interfaces, API documentation, forms, and more.

Key Benefits

  • Single Source of Truth: Define your data model once, use it everywhere
  • Type Safety: Generate type-safe code across multiple languages and frameworks
  • Rapid Development: Automatically generate boilerplate code, forms, and documentation
  • Consistency: Ensure consistent data structures across your entire application
  • Extensibility: Plugin system allows custom code generation for any target

Who Should Use This

  • Junior Developers: Easy-to-understand syntax with comprehensive examples
  • Senior Developers: Powerful features for complex applications
  • CTOs/Technical Leaders: Reduce development time and improve code consistency

Syntax Overview

The .idea file format uses a simplified syntax that eliminates the need for traditional separators like commas (,) and colons (:) found in JSON or JavaScript. The parser can logically determine separations, making the syntax cleaner and more readable.

Key Syntax Rules

  1. No Separators Required: The parser intelligently determines where values begin and end
  2. Double Quotes Only: All strings must use double quotes (") - single quotes are not supported
  3. Logical Structure: The parser understands context and can differentiate between keys, values, and nested structures

Syntax Comparison

Traditional JavaScript/JSON:

// JavaScript object
{ foo: "bar", bar: "foo" }

// JavaScript array
[ "foo", "bar" ]

// Nested structure
{
  user: {
    name: "John",
    age: 30,
    active: true
  },
  tags: ["admin", "user"]
}

Equivalent .idea syntax:

// Object structure
{ foo "bar" bar "foo" }

// Array structure
[ "foo" "bar" ]

// Nested structure
{
  user {
    name "John"
    age 30
    active true
  }
  tags ["admin" "user"]
}

Data Type Representation

// Strings - always use double quotes
name "John Doe"
description "A comprehensive user management system"

// Numbers - no quotes needed
age 30
price 99.99
count -5

// Booleans - no quotes needed
active true
verified false

// Arrays - space-separated values
tags ["admin" "user" "moderator"]
numbers [1 2 3 4 5]
mixed ["text" 123 true]

// Objects - nested key-value pairs
profile {
  firstName "John"
  lastName "Doe"
  settings {
    theme "dark"
    notifications true
  }
}

Comments

Comments in .idea files use the standard // syntax:

// This is a single-line comment
model User {
  id String @id // Inline comment
  name String @required
  // Another comment
  email String @unique
}

/*
  Multi-line comments are also supported
  for longer explanations
*/

Data Types

The .idea format supports four primary data types that form the building blocks of your application schema.

Enum

Enums define a set of named constants with associated values, perfect for representing fixed sets of options like user roles, status values, or categories.

Syntax

enum EnumName {
  KEY1 "Display Value 1"
  KEY2 "Display Value 2"
  KEY3 "Display Value 3"
}

Structure

  • EnumName: The identifier used to reference this enum
  • KEY: The constant name (typically uppercase)
  • "Display Value": Human-readable label for the constant

Example

enum UserRole {
  ADMIN "Administrator"
  MODERATOR "Moderator"
  USER "Regular User"
  GUEST "Guest User"
}

enum OrderStatus {
  PENDING "Pending Payment"
  PAID "Payment Confirmed"
  SHIPPED "Order Shipped"
  DELIVERED "Order Delivered"
  CANCELLED "Order Cancelled"
}

enum Priority {
  LOW "Low Priority"
  MEDIUM "Medium Priority"
  HIGH "High Priority"
  URGENT "Urgent"
}

Generated Output

When processed, enums generate language-specific constants:

TypeScript:

export enum UserRole {
  ADMIN = "Administrator",
  MODERATOR = "Moderator",
  USER = "Regular User",
  GUEST = "Guest User"
}

JSON:

{
  "enum": {
    "UserRole": {
      "ADMIN": "Administrator",
      "MODERATOR": "Moderator",
      "USER": "Regular User",
      "GUEST": "Guest User"
    }
  }
}

Prop

Props are reusable property configurations that define common field behaviors, validation rules, and UI components. They promote consistency and reduce duplication across your schema.

Syntax

prop PropName {
  property "value"
  nested {
    property "value"
  }
}

Structure

  • PropName: The identifier used to reference this prop
  • property: Configuration key-value pairs
  • nested: Grouped configuration options

Example

prop Email {
  type "email"
  format "email"
  validation {
    required true
    pattern "^[^\s@]+@[^\s@]+\.[^\s@]+$"
  }
  ui {
    component "EmailInput"
    placeholder "Enter your email address"
    icon "envelope"
  }
}

prop Password {
  type "password"
  validation {
    required true
    minLength 8
    pattern "^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]"
  }
  ui {
    component "PasswordInput"
    placeholder "Enter a secure password"
    showStrength true
  }
}

prop Currency {
  type "number"
  format "currency"
  validation {
    min 0
    precision 2
  }
  ui {
    component "CurrencyInput"
    symbol "$"
    locale "en-US"
  }
}

Usage in Models

Props are referenced using the @field attribute:

model User {
  email String @field.input(Email)
  password String @field.input(Password)
}

Type

Types define custom data structures with multiple columns, similar to objects or structs in programming languages. They're perfect for representing complex data that doesn't warrant a full model.

Syntax

type TypeName {
  columnName DataType @attribute1 @attribute2
  anotherColumn DataType @attribute
}

Structure

  • TypeName: The identifier used to reference this type
  • columnName: The field name within the type
  • DataType: The data type (String, Number, Boolean, Date, etc.)
  • @attribute: Optional attributes for validation, UI, or behavior

Example

type Address {
  street String @required @field.input(Text)
  city String @required @field.input(Text)
  state String @field.select
  postalCode String @field.input(Text)
  country String @default("US") @field.select
  coordinates {
    latitude Number @field.input(Number)
    longitude Number @field.input(Number)
  }
}

type ContactInfo {
  email String @required @field.input(Email)
  phone String @field.input(Phone)
  website String @field.input(URL)
  socialMedia {
    twitter String @field.input(Text)
    linkedin String @field.input(Text)
    github String @field.input(Text)
  }
}

type Money {
  amount Number @required @field.input(Currency)
  currency String @default("USD") @field.select
  exchangeRate Number @field.input(Number)
}

Usage in Models

Types are used as column data types:

model Company {
  name String @required
  address Address @required
  contact ContactInfo
  revenue Money
}

Model

Models represent the core entities in your application, typically corresponding to database tables or API resources. They define the structure, relationships, and behavior of your data.

Syntax

model ModelName {
  columnName DataType @attribute1 @attribute2
  relationColumn RelatedModel @relation
}

model ModelName! {  // Mutable model
  // columns...
}

Structure

  • ModelName: The identifier for this model
  • !: Optional non-mergeable indicator (prevents automatic merging when using use directives)
  • columnName: The field name
  • DataType: Built-in types (String, Number, Boolean, Date) or custom types/enums
  • @attribute: Attributes for validation, relationships, UI, etc.

Merging Behavior

By default, when importing schemas with use directives, models with the same name are automatically merged. The ! suffix prevents this behavior:

// base-schema.idea
model User {
  id String @id
  name String @required
}

// extended-schema.idea
use "./base-schema.idea"

// This will merge with the imported User model
model User {
  email String @required
  created Date @default("now()")
}

// This will NOT merge - it completely replaces the imported User
model User! {
  id String @id
  username String @required
  password String @required
}

Example

model User! {
  id String @id @default("nanoid()")
  email String @unique @required @field.input(Email)
  username String @unique @required @field.input(Text)
  password String @required @field.input(Password)
  profile UserProfile?
  role UserRole @default("USER")
  active Boolean @default(true)
  lastLogin Date?
  created Date @default("now()")
  updated Date @default("updated()")
}

model UserProfile {
  id String @id @default("nanoid()")
  userId String @relation(User.id)
  firstName String @required @field.input(Text)
  lastName String @required @field.input(Text)
  bio String @field.textarea
  avatar String @field.upload
  address Address
  contact ContactInfo
  preferences {
    theme String @default("light")
    language String @default("en")
    notifications Boolean @default(true)
  }
}

model Post {
  id String @id @default("nanoid()")
  title String @required @field.input(Text)
  slug String @unique @generated
  content String @required @field.richtext
  excerpt String @field.textarea
  authorId String @relation(User.id)
  author User @relation(User, authorId)
  status PostStatus @default("DRAFT")
  tags String[] @field.tags
  publishedAt Date?
  created Date @default("now()")
  updated Date @default("updated()")
}

enum PostStatus {
  DRAFT "Draft"
  PUBLISHED "Published"
  ARCHIVED "Archived"
}

Schema Elements

Attributes (@)

Attributes provide metadata and configuration for columns, types, and models. They define validation rules, UI components, relationships, and behavior. Attributes can be attached to any schema element and are processed by plugins according to their specific needs.

There are no reserved or pre-defined attributes in idea. You can define any arbitrary attributes in your schema. It's up to the plugins to recognize and process them.

Attribute Syntax

Attributes always start with the at symbol (@) followed by letters, numbers, and periods. They can be expressed in several forms:

// Simple boolean attribute (sets value to true)
@filterable

// Function with single argument
@label("Name")

// Function with multiple arguments
@is.cgt(3 "Name should be more than 3 characters")

// Function with object argument
@view.image({ width 100 height 100 })

// Nested attribute names using periods
@field.input(Email)
@validation.required
@ui.component("CustomInput")

Attribute Value Types

Attributes can hold different types of values:

// Boolean (implicit true)
@required
@unique
@filterable

// String values
@label("User Name")
@placeholder("Enter your name")
@description("This field is required")

// Number values
@min(0)
@max(100)
@precision(2)

// Object values
@validation({ required true minLength 3 })
@ui({ component "Input" placeholder "Enter text" })
@options({ multiple true searchable false })

// Array values
@tags(["admin" "user" "guest"])
@options(["small" "medium" "large"])
@toolbar(["bold" "italic" "underline"])

// Mixed arguments
@between(1 100 "Value must be between 1 and 100")
@pattern("^[a-zA-Z]+$" "Only letters allowed")

Attribute Scope

Attributes can be applied to different schema elements:

// Model-level attributes
model User @table("users") @index(["email" "created"]) {
  // Column-level attributes
  id String @id @default("nanoid()")
  name String @required @minLength(2)
}

// Type-level attributes
type Address @serializable @cacheable(3600) {
  street String @required
  city String @required
}

Columns

Columns define the individual fields within models and types, specifying their data type, constraints, and behavior.

Data Types

Type Description Example
String Text data name String
Number Numeric data age Number
Boolean True/false values active Boolean
Date Date/time values created Date
JSON JSON objects metadata JSON
CustomType User-defined types address Address
EnumType Enum values role UserRole

Optional and Array Types

model User {
  name String          // Required string
  bio String?          // Optional string
  tags String[]        // Array of strings
  addresses Address[]  // Array of custom types
  metadata JSON?       // Optional JSON
}

Nested Objects

model User {
  profile {
    firstName String
    lastName String
    social {
      twitter String?
      github String?
    }
  }
  settings {
    theme String @default("light")
    notifications Boolean @default(true)
  }
}

Schema Structure

A complete .idea schema file can contain multiple elements organized in a specific structure:

// 1. Plugin declarations
plugin "./plugins/generate-types.js" {
  output "./generated/types.ts"
}

plugin "./plugins/generate-database.js" {
  output "./database/schema.sql"
  dialect "postgresql"
}

// 2. Use statements (imports)
use "./shared/common-types.idea"
use "./auth/user-types.idea"

// 3. Prop definitions
prop Email {
  type "email"
  validation { required true }
}

prop Text {
  type "text"
  validation { maxLength 255 }
}

// 4. Enum definitions
enum UserRole {
  ADMIN "Administrator"
  USER "Regular User"
}

// 5. Type definitions
type Address {
  street String @required
  city String @required
  country String @default("US")
}

// 6. Model definitions
model User! {
  id String @id @default("nanoid()")
  email String @unique @field.input(Email)
  name String @field.input(Text)
  role UserRole @default("USER")
  address Address?
  active Boolean @default(true)
  created Date @default("now()")
}

Schema Directives

Schema directives are special declarations that control how the schema is processed and what outputs are generated.

Use

The use directive imports definitions from other .idea files, enabling modular schema organization and reusability. When importing, data types with the same name are automatically merged unless the ! (non-mergeable) indicator is used.

Syntax

use "path/to/schema.idea"
use "./relative/path/schema.idea"
use "../parent/directory/schema.idea"

Structure

  • Path: Relative or absolute path to the .idea file to import
  • Automatic Merging: Data types with matching names are merged by default
  • Merge Prevention: Use ! suffix to prevent merging

Example

shared/common.idea:

// Common types used across multiple schemas
type Address {
  street String @required
  city String @required
  country String @default("US")
}

enum Status {
  ACTIVE "Active"
  INACTIVE "Inactive"
}

prop Email {
  type "email"
  validation {
    required true
    format "email"
  }
}

user/user-schema.idea:

// Import common definitions
use "../shared/common.idea"

// Extend the Status enum (will merge with imported one)
enum Status {
  PENDING "Pending Approval"
  SUSPENDED "Temporarily Suspended"
}

// Use imported types and props
model User {
  id String @id @default("nanoid()")
  email String @field.input(Email)
  address Address
  status Status @default("PENDING")
}

Result after merging:

// The Status enum now contains all values
enum Status {
  ACTIVE "Active"           // From common.idea
  INACTIVE "Inactive"       // From common.idea
  PENDING "Pending Approval"    // From user-schema.idea
  SUSPENDED "Temporarily Suspended" // From user-schema.idea
}

Preventing Merging with !

When you want to prevent automatic merging and keep definitions separate, use the ! suffix:

base-schema.idea:

enum UserRole {
  USER "Regular User"
  ADMIN "Administrator"
}

extended-schema.idea:

use "./base-schema.idea"

// This will NOT merge with the imported UserRole
// Instead, it will override it completely
enum UserRole! {
  GUEST "Guest User"
  MEMBER "Member"
  MODERATOR "Moderator"
  ADMIN "Administrator"
}

Use Cases

  1. Shared Types: Define common types once and reuse across multiple schemas
  2. Modular Organization: Split large schemas into manageable, focused files
  3. Team Collaboration: Different teams can work on separate schema files
  4. Environment-Specific: Override certain definitions for different environments

Best Practices

// ✅ Good - organize by domain
use "./shared/common-types.idea"
use "./auth/user-types.idea"
use "./commerce/product-types.idea"

// ✅ Good - clear file naming
use "./enums/status-enums.idea"
use "./types/address-types.idea"
use "./props/form-props.idea"

// ❌ Avoid - unclear imports
use "./stuff.idea"
use "./misc.idea"

Plugin

The plugin directive configures code generation plugins that process your schema and generate various outputs like TypeScript interfaces, database schemas, API documentation, and more.

Syntax

plugin "path/to/plugin.js" {
  configKey "configValue"
  nestedConfig {
    option "value"
    flag true
  }
}

Structure

  • Path: Relative or absolute path to the plugin file
  • Configuration Block: Key-value pairs that configure the plugin behavior
  • Nested Configuration: Support for complex configuration structures

Example

// TypeScript interface generation
plugin "./plugins/typescript-generator.js" {
  output "./src/types/schema.ts"
  namespace "Schema"
  exportType "named"
  includeComments true
  formatting {
    indent 2
    semicolons true
    trailingCommas true
  }
}

// Database schema generation
plugin "./plugins/database-generator.js" {
  output "./database/schema.sql"
  dialect "postgresql"
  includeIndexes true
  includeForeignKeys true
  tablePrefix "app_"
  options {
    dropExisting false
    addTimestamps true
    charset "utf8mb4"
  }
}

// API documentation generation
plugin "./plugins/openapi-generator.js" {
  output "./docs/api.yaml"
  version "1.0.0"
  title "My API Documentation"
  description "Comprehensive API documentation generated from schema"
  servers [
    {
      url "https://api.example.com/v1"
      description "Production server"
    }
    {
      url "https://staging-api.example.com/v1"
      description "Staging server"
    }
  ]
  security {
    type "bearer"
    scheme "JWT"
  }
}

// Form generation
plugin "./plugins/form-generator.js" {
  output "./src/components/forms/"
  framework "react"
  styling "tailwind"
  validation "zod"
  features {
    typescript true
    storybook true
    tests true
  }
  components {
    inputWrapper "FormField"
    submitButton "SubmitButton"
    errorMessage "ErrorText"
  }
}

Plugin Configuration Options

Common configuration patterns across different plugin types:

plugin "./plugins/my-plugin.js" {
  // Output configuration
  output "./generated/output.ts"
  outputDir "./generated/"
  
  // Format and style options
  format "typescript" // or "javascript", "json", "yaml"
  style "camelCase"   // or "snake_case", "kebab-case"
  
  // Feature flags
  includeComments true
  generateTests false
  addValidation true
  
  // Framework-specific options
  framework "react"     // or "vue", "angular", "svelte"
  styling "tailwind"    // or "bootstrap", "material", "custom"
  
  // Advanced configuration
  templates {
    model "./templates/model.hbs"
    enum "./templates/enum.hbs"
  }
  
  // Custom options (plugin-specific)
  customOptions {
    apiVersion "v1"
    includeMetadata true
    compressionLevel 9
  }
}

Multiple Plugin Execution

You can configure multiple plugins to generate different outputs from the same schema:

// Generate TypeScript types
plugin "./plugins/typescript.js" {
  output "./src/types/index.ts"
}

// Generate database schema
plugin "./plugins/database.js" {
  output "./database/schema.sql"
  dialect "postgresql"
}

// Generate API documentation
plugin "./plugins/docs.js" {
  output "./docs/api.md"
  format "markdown"
}

// Generate form components
plugin "./plugins/forms.js" {
  output "./src/forms/"
  framework "react"
}

// Generate validation schemas
plugin "./plugins/validation.js" {
  output "./src/validation/index.ts"
  library "zod"
}

Plugin Development

Plugins are JavaScript/TypeScript modules that export a default function:

// Example plugin structure
import type { PluginProps } from '@stackpress/idea-transformer/types';

interface MyPluginConfig {
  output: string;
  format?: 'typescript' | 'javascript';
  includeComments?: boolean;
}

export default async function myPlugin(
  props: PluginProps<{ config: MyPluginConfig }>
) {
  const { config, schema, transformer, cwd } = props;
  
  // Validate configuration
  if (!config.output) {
    throw new Error('Output path is required');
  }
  
  // Process schema
  const content = generateContent(schema, config);
  
  // Write output
  const outputPath = await transformer.loader.absolute(config.output);
  await writeFile(outputPath, content);
  
  console.log(`✅ Generated: ${outputPath}`);
}

Processing Flow

The .idea file format follows a structured processing flow:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   .idea File    │───▶│     Parser      │───▶│   AST (JSON)    │
│                 │    │                 │    │                 │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                              │                        │
                              ▼                        ▼
                       ┌─────────────────┐    ┌─────────────────┐
                       │   Validation    │    │   Transformer   │
                       │                 │    │                 │
                       └─────────────────┘    └─────────────────┘
                                                       │
                                                       ▼
                                              ┌─────────────────┐
                                              │    Plugins      │
                                              │                 │
                                              └─────────────────┘
                                                       │
                                                       ▼
                                              ┌─────────────────┐
                                              │ Generated Code  │
                                              │                 │
                                              └─────────────────┘

Processing Steps

  1. Parsing: Convert .idea syntax into Abstract Syntax Tree (AST)
  2. Validation: Check for syntax errors, type consistency, and constraint violations
  3. Transformation: Convert AST into structured JSON configuration
  4. Plugin Execution: Run configured plugins to generate output files
  5. Code Generation: Create TypeScript, SQL, documentation, forms, etc.

Plugin System

The plugin system enables extensible code generation from your schema definitions.

Plugin Declaration

plugin "./path/to/plugin.js" {
  output "./generated/output.ts"
  format "typescript"
  options {
    strict true
    comments true
  }
}

Common Plugin Types

Plugin Type Purpose Output
TypeScript Generator Generate interfaces and types .ts files
Database Schema Generate SQL DDL .sql files
API Documentation Generate OpenAPI specs .json/.yaml files
Form Generator Generate HTML forms .html files
Validation Schema Generate Zod/Joi schemas .ts files
Mock Data Generate test fixtures .json files

Plugin Development

import type { PluginProps } from '@stackpress/idea-transformer/types';

export default async function myPlugin(props: PluginProps<{}>) {
  const { config, schema, transformer } = props;
  
  // Process schema and generate output
  const content = generateFromSchema(schema);
  
  // Write to configured output path
  const outputPath = await transformer.loader.absolute(config.output);
  await writeFile(outputPath, content);
}

Complete Examples

E-commerce Application Schema

// E-commerce application schema
plugin "./plugins/generate-types.js" {
  output "./src/types/schema.ts"
}

plugin "./plugins/generate-database.js" {
  output "./database/schema.sql"
  dialect "postgresql"
}

plugin "./plugins/generate-api-docs.js" {
  output "./docs/api.yaml"
  format "openapi"
}

// Reusable props
prop Email {
  type "email"
  validation {
    required true
    format "email"
  }
  ui {
    placeholder "Enter email address"
    icon "envelope"
  }
}

prop Currency {
  type "number"
  format "currency"
  validation {
    min 0
    precision 2
  }
  ui {
    symbol "$"
    locale "en-US"
  }
}

prop Text {
  type "text"
  validation {
    maxLength 255
  }
}

// Enums
enum UserRole {
  ADMIN "Administrator"
  CUSTOMER "Customer"
  VENDOR "Vendor"
}

enum OrderStatus {
  PENDING "Pending"
  CONFIRMED "Confirmed"
  SHIPPED "Shipped"
  DELIVERED "Delivered"
  CANCELLED "Cancelled"
}

enum PaymentStatus {
  PENDING "Pending"
  COMPLETED "Completed"
  FAILED "Failed"
  REFUNDED "Refunded"
}

// Types
type Address {
  street String @required @field.input(Text)
  city String @required @field.input(Text)
  state String @required @field.select
  postalCode String @required @field.input(Text)
  country String @default("US") @field.select
}

type Money {
  amount Number @required @field.input(Currency)
  currency String @default("USD")
}

// Models
model User! {
  id String @id @default("nanoid()")
  email String @unique @required @field.input(Email)
  username String @unique @required @field.input(Text)
  firstName String @required @field.input(Text)
  lastName String @required @field.input(Text)
  role UserRole @default("CUSTOMER")
  addresses Address[] @relation(UserAddress.userId)
  orders Order[] @relation(Order.userId)
  active Boolean @default(true)
  emailVerified Boolean @default(false)
  created Date @default("now()")
  updated Date @default("updated()")
}

model Category {
  id String @id @default("nanoid()")
  name String @unique @required @field.input(Text)
  slug String @unique @generated
  description String @field.textarea
  parentId String? @relation(Category.id)
  parent Category? @relation(Category, parentId)
  children Category[] @relation(Category.parentId)
  products Product[] @relation(Product.categoryId)
  active Boolean @default(true)
  created Date @default("now()")
}

model Product! {
  id String @id @default("nanoid()")
  name String @required @field.input(Text)
  slug String @unique @generated
  description String @field.richtext
  shortDescription String @field.textarea
  sku String @unique @required @field.input(Text)
  price Money @required
  comparePrice Money?
  cost Money?
  categoryId String @relation(Category.id)
  category Category @relation(Category, categoryId)
  images String[] @field.upload
  inventory {
    quantity Number @default(0)
    trackQuantity Boolean @default(true)
    allowBackorder Boolean @default(false)
  }
  seo {
    title String @field.input(Text)
    description String @field.textarea
    keywords String[] @field.tags
  }
  active Boolean @default(true)
  featured Boolean @default(false)
  created Date @default("now()")
  updated Date @default("updated()")
}

model Order {
  id String @id @default("nanoid()")
  orderNumber String @unique @generated
  userId String @relation(User.id)
  user User @relation(User, userId)
  items OrderItem[] @relation(OrderItem.orderId)
  status OrderStatus @default("PENDING")
  paymentStatus PaymentStatus @default("PENDING")
  shippingAddress Address @required
  billingAddress Address @required
  subtotal Money @required
  tax Money @required
  shipping Money @required
  total Money @required
  notes String? @field.textarea
  created Date @default("now()")
  updated Date @default("updated()")
}

model OrderItem {
  id String @id @default("nanoid()")
  orderId String @relation(Order.id)
  order Order @relation(Order, orderId)
  productId String @relation(Product.id)
  product Product @relation(Product, productId)
  quantity Number @required @min(1)
  price Money @required
  total Money @required
}

Blog Application Schema

// Blog application schema
plugin "./plugins/generate-types.js" {
  output "./src/types/blog.ts"
}

plugin "./plugins/generate-forms.js" {
  output "./src/components/forms/"
  framework "react"
}

// Props
prop RichText {
  type "richtext"
  validation {
    required true
    minLength 100
  }
  ui {
    toolbar ["bold", "italic", "link", "image"]
    placeholder "Write your content here..."
  }
}

prop Slug {
  type "text"
  validation {
    pattern "^[a-z0-9-]+$"
    maxLength 100
  }
  ui {
    placeholder "url-friendly-slug"
  }
}

// Enums
enum PostStatus {
  DRAFT "Draft"
  PUBLISHED "Published"
  ARCHIVED "Archived"
}

enum CommentStatus {
  PENDING "Pending Moderation"
  APPROVED "Approved"
  REJECTED "Rejected"
}

// Models
model Author! {
  id String @id @default("nanoid()")
  email String @unique @required @field.input(Email)
  name String @required @field.input(Text)
  bio String @field.textarea
  avatar String @field.upload
  social {
    twitter String? @field.input(Text)
    github String? @field.input(Text)
    website String? @field.input(URL)
  }
  posts Post[] @relation(Post.authorId)
  active Boolean @default(true)
  created Date @default("now()")
}

model Category {
  id String @id @default("nanoid()")
  name String @unique @required @field.input(Text)
  slug String @unique @field.input(Slug)
  description String @field.textarea
  color String @field.color
  posts Post[] @relation(PostCategory.categoryId)
  created Date @default("now()")
}

model Tag {
  id String @id @default("nanoid()")
  name String @unique @required @field.input(Text)
  slug String @unique @field.input(Slug)
  posts Post[] @relation(PostTag.tagId)
  created Date @default("now()")
}

model Post! {
  id String @id @default("nanoid()")
  title String @required @field.input(Text)
  slug String @unique @field.input(Slug)
  excerpt String @field.textarea
  content String @required @field.input(RichText)
  featuredImage String @field.upload
  authorId String @relation(Author.id)
  author Author @relation(Author, authorId)
  categories Category[] @relation(PostCategory.postId)
  tags Tag[] @relation(PostTag.postId)
  status PostStatus @default("DRAFT")
  publishedAt Date? @field.datetime
  seo {
    title String @field.input(Text)
    description String @field.textarea
    keywords String[] @field.tags
  }
  stats {
    views Number @default(0)
    likes Number @default(0)
    shares Number @default(0)
  }
  comments Comment[] @relation(Comment.postId)
  created Date @default("now()")
  updated Date @default("updated()")
}

model Comment {
  id String @id @default("nanoid()")
  postId String @relation(Post.id)
  post Post @relation(Post, postId)
  authorName String @required @field.input(Text)
  authorEmail String @required @field.input(Email)
  content String @required @field.textarea
  status CommentStatus @default("PENDING")
  parentId String? @relation(Comment.id)
  parent Comment? @relation(Comment, parentId)
  replies Comment[] @relation(Comment.parentId)
  created Date @default("now()")
}

Best Practices

1. Schema Organization

Use Descriptive Names

// ✅ Good
enum UserAccountStatus {
  ACTIVE "Active Account"
  SUSPENDED "Temporarily Suspended"
  DEACTIVATED "Permanently Deactivated"
}

// ❌ Avoid
enum Status {
  A "Active"
  S "Suspended"
  D "Deactivated"
}

Group Related Elements

// User-related enums
enum UserRole { /* ... */ }
enum UserStatus { /* ... */ }

// User-related types
type UserProfile { /* ... */ }
type UserPreferences { /* ... */ }

// User-related models
model User { /* ... */ }
model UserSession { /* ... */ }

Use Consistent Naming Conventions

// Models: PascalCase
model UserAccount { /* ... */ }

// Enums: PascalCase
enum OrderStatus { /* ... */ }

// Props: PascalCase
prop EmailInput { /* ... */ }

// Columns: camelCase
model User {
  firstName String
  lastName String
  emailAddress String
}

2. Type Safety

Define Custom Types for Complex Data

type Money {
  amount Number @required @min(0)
  currency String @default("USD")
}

type Coordinates {
  latitude Number @required @min(-90) @max(90)
  longitude Number @required @min(-180) @max(180)
}

model Product {
  price Money @required
  location Coordinates?
}

Use Enums for Fixed Sets of Values

// ✅ Good - type-safe and self-documenting
enum Priority {
  LOW "Low Priority"
  MEDIUM "Medium Priority"
  HIGH "High Priority"
  URGENT "Urgent"
}

model Task {
  priority Priority @default("MEDIUM")
}

// ❌ Avoid - error-prone and unclear
model Task {
  priority String @default("medium")
}

3. Validation and Constraints

Use Appropriate Validation Attributes

model User {
  email String @required @unique @pattern("^[^\s@]+@[^\s@]+\.[^\s@]+$")
  age Number @min(13) @max(120)
  username String @required @minLength(3) @maxLength(30) @pattern("^[a-zA-Z0-9_]+$")
  bio String @maxLength(500)
  tags String[] @maxItems(10)
}

Provide Meaningful Defaults

model User {
  role UserRole @default("USER")
  active Boolean @default(true)
  emailVerified Boolean @default(false)
  created Date @default("now()")
  updated Date @default("updated()")
  preferences {
    theme String @default("light")
    language String @default("en")
    notifications Boolean @default(true)
  }
}

4. Relationships

Use Clear Relationship Patterns

// One-to-many relationship
model User {
  id String @id
  posts Post[] @relation(Post.authorId)
}

model Post {
  id String @id
  authorId String @relation(User.id)
  author User @relation(User, authorId)
}

// Many-to-many relationship
model Post {
  id String @id
  tags Tag[] @relation(PostTag.postId)
}

model Tag {
  id String @id
  posts Post[] @relation(PostTag.tagId)
}

model PostTag {
  postId String @relation(Post.id)
  tagId String @relation(Tag.id)
}

5. Plugin Configuration

Organize Plugins by Purpose

// Type generation
plugin "./plugins/typescript-generator.js" {
  output "./src/types/schema.ts"
  namespace "Schema"
}

// Database schema
plugin "./plugins/database-generator.js" {
  output "./database/schema.sql"
  dialect "postgresql"
}

// API documentation
plugin "./plugins/openapi-generator.js" {
  output "./docs/api.yaml"
  version "1.0.0"
}

// Form generation
plugin "./plugins/form-generator.js" {
  output "./src/components/forms/"
  framework "react"
  styling "tailwind"
}

Error Handling

Common Errors and Solutions

1. Invalid Schema Structure

Error: Invalid Schema

Cause: Syntax errors or malformed declarations

Solution:

// ❌ Invalid - missing quotes around enum values
enum Status {
  ACTIVE Active
  INACTIVE Inactive
}

// ✅ Valid - proper enum syntax
enum Status {
  ACTIVE "Active"
  INACTIVE "Inactive"
}

2. Missing Required Properties

Error: Expecting a columns property

Cause: Models and types must have column definitions

Solution:

// ❌ Invalid - empty model
model User {
}

// ✅ Valid - model with columns
model User {
  id String @id
  name String @required
}

3. Duplicate Declarations

Error: Duplicate [name]

Cause: Multiple declarations with the same name

Solution:

// ❌ Invalid - duplicate model names
model User {
  id String @id
}

model User {  // Duplicate!
  name String
}

// ✅ Valid - unique names
model User {
  id String @id
}

model UserProfile {
  name String
}

4. Unknown References

Error: Unknown reference [name]

Cause: Referencing undefined props, types, or enums

Solution:

// ❌ Invalid - EmailInput prop not defined
model User {
  email String @field.input(EmailInput)
}

// ✅ Valid - define prop first
prop EmailInput {
  type "email"
  validation { required true }
}

model User {
  email String @field.input(EmailInput)
}

5. Type Mismatches

Error: Type mismatch

Cause: Using incompatible types or attributes

Solution:

// ❌ Invalid - Boolean can't have @minLength
model User {
  active Boolean @minLength(5)
}

// ✅ Valid - appropriate attributes for type
model User {
  active Boolean @default(true)
  name String @minLength(2) @maxLength(50)
}

Error Prevention

1. Use TypeScript for Plugin Development

import type { PluginProps, SchemaConfig } from '@stackpress/idea-transformer/types';

export default async function myPlugin(props: PluginProps<{}>) {
  // TypeScript will catch type errors at compile time
  const { config, schema } = props;
  
  // Validate configuration
  if (!config.output) {
    throw new Error('Output path is required');
  }
  
  // Process schema safely
  if (schema.model) {
    for (const [modelName, model] of Object.entries(schema.model)) {
      // Process each model
    }
  }
}

2. Validate Schema Before Processing

// Always validate required fields
model User {
  id String @id @required
  email String @required @unique
  name String @required
}

// Use appropriate data types
model Product {
  price Number @min(0)        // Not String
  active Boolean              // Not Number
  created Date @default("now()") // Not String
}

3. Use Consistent Patterns

// Consistent ID patterns
model User {
  id String @id @default("nanoid()")
}

model Post {
  id String @id @default("nanoid()")
}

// Consistent timestamp patterns
model User {
  created Date @default("now()")
  updated Date @default("updated()")
}

4. Test Schema Changes

# Parse schema to check for errors
npm run idea:parse schema.idea

# Transform schema to validate plugins
npm run idea:transform schema.idea

# Generate output to verify results
npm run idea:generate

Conclusion

The .idea file format provides a powerful, declarative approach to defining application schemas that can generate multiple outputs from a single source of truth. By following the patterns and best practices outlined in this specification, development teams can:

Key Advantages

  • Reduce Development Time: Generate boilerplate code, forms, documentation, and database schemas automatically
  • Improve Consistency: Ensure data structures remain consistent across frontend, backend, and database layers
  • Enhance Type Safety: Generate type-safe code for TypeScript, preventing runtime errors
  • Simplify Maintenance: Update schema once and regenerate all dependent code
  • Enable Rapid Prototyping: Quickly iterate on data models and see changes across the entire application

Getting Started

  1. Define Your Schema: Start with a simple .idea file containing your core models
  2. Add Plugins: Configure plugins to generate the outputs you need (TypeScript, SQL, forms, etc.)
  3. Iterate and Refine: Add validation rules, relationships, and UI configurations as needed
  4. Integrate with Build Process: Add schema generation to your CI/CD pipeline

Next Steps

  • Explore Plugin Ecosystem: Discover available plugins for your technology stack
  • Create Custom Plugins: Develop plugins for specific requirements not covered by existing ones
  • Join the Community: Contribute to the .idea ecosystem and share your experiences

Support and Resources

  • Documentation: Comprehensive guides for parser, transformer, and plugin development
  • Examples: Real-world schema examples for common application types
  • Community: Active community of developers using and contributing to the .idea ecosystem

The .idea file format represents a significant step forward in application development efficiency, providing the tools needed to build robust, type-safe applications with minimal boilerplate code. Whether you're a junior developer learning the ropes, a senior developer optimizing workflows, or a technical leader evaluating new tools, the .idea format offers clear benefits for modern application development.

Start small, think big, and let the .idea format transform how you build applications.

Idea Parser

A TypeScript library for parsing .idea schema files into Abstract Syntax Trees (AST) and converting them to readable JSON configurations. This library is designed to help developers work with schema definitions in a structured and type-safe manner.

Installation

Install the package using npm:

npm install @stackpress/idea-parser

Quick Start

The library provides two main functions for parsing schema files:

Basic Usage

import { parse, final } from '@stackpress/idea-parser';

// Parse a schema file into JSON (includes references)
const schemaCode = `
prop Text { type "text" }
enum Roles {
  ADMIN "Admin"
  USER "User"
}
model User {
  id String @id
  name String @field.input(Text)
  role Roles
}
`;

// Parse with references intact
const parsedSchema = parse(schemaCode);

// Parse and clean up references (final version)
const finalSchema = final(schemaCode);

Difference Between parse and final

  • parse(code: string): Converts schema code to JSON while preserving prop and use references
  • final(code: string): Like parse but removes prop and use references for a clean final output

Core Concepts

Schema Structure

An .idea schema file can contain several types of declarations:

  1. Plugins: External integrations and configurations
  2. Use statements: Import other schema files
  3. Props: Reusable property configurations
  4. Enums: Enumerated value definitions
  5. Types: Custom type definitions with columns
  6. Models: Database model definitions

Processing Flow

The library follows this processing flow:

Raw Schema Code → SchemaTree → Compiler → JSON Output
  1. Raw Code: Your .idea schema file content
  2. SchemaTree: Parses the entire file into an Abstract Syntax Tree
  3. Compiler: Converts AST tokens into structured JSON
  4. JSON Output: Final configuration object

API Reference

Main Functions

parse(code: string)

Converts schema code into a JSON representation with references preserved.

Parameters:

  • code (string): The schema code to parse

Returns:

  • SchemaConfig: JSON object representing the parsed schema

Example:

import { parse } from '@stackpress/idea-parser';

const result = parse(`
prop Text { type "text" }
model User {
  name String @field.input(Text)
}
`);

console.log(result);
// Output includes prop references: { prop: { Text: { type: "text" } }, ... }

final(code: string)

Converts schema code into a clean JSON representation with references resolved and removed.

Parameters:

  • code (string): The schema code to parse

Returns:

  • FinalSchemaConfig: Clean JSON object without prop/use references

Example:

import { final } from '@stackpress/idea-parser';

const result = final(`
prop Text { type "text" }
model User {
  name String @field.input(Text)
}
`);

console.log(result);
// Output has resolved references: { model: { User: { ... } } }
// No 'prop' section in output

Core Classes

  • Compiler: Static methods for converting AST tokens to JSON
  • Lexer: Tokenization and parsing utilities
  • SchemaTree: Main parser for complete schema files
  • Syntax Trees: Individual parsers for different schema elements
  • Tokens: AST token structures and type definitions

Exception Handling

The library uses a custom Exception class that extends the standard Exception class for better error reporting.

import { Exception } from '@stackpress/idea-parser';

try {
  const result = parse(invalidCode);
} catch (error) {
  if (error instanceof Exception) {
    console.log('Parsing error:', error.message);
  }
}

Examples

Complete Schema Example

import { final } from '@stackpress/idea-parser';

const schemaCode = `
plugin "./database-plugin" {
  provider "postgresql"
  url env("DATABASE_URL")
}

prop Text { type "text" }
prop Email { type "email" format "email" }

enum UserRole {
  ADMIN "Administrator"
  USER "Regular User"
  GUEST "Guest User"
}

type Address {
  street String @field.input(Text) @is.required
  city String @field.input(Text) @is.required
  country String @field.select
  postal String @field.input(Text)
}

model User! {
  id String @id @default("nanoid()")
  email String @field.input(Email) @is.required @is.unique
  name String @field.input(Text) @is.required
  role UserRole @default("USER")
  address Address?
  active Boolean @default(true)
  created Date @default("now()")
  updated Date @default("updated()")
}
`;

const result = final(schemaCode);
console.log(JSON.stringify(result, null, 2));

Working with Individual Components

import { Compiler, EnumTree, ModelTree } from '@stackpress/idea-parser';

// Parse individual enum
const enumCode = `enum Status { ACTIVE "Active" INACTIVE "Inactive" }`;
const enumAST = EnumTree.parse(enumCode);
const [enumName, enumConfig] = Compiler.enum(enumAST);

// Parse individual model
const modelCode = `model User { id String @id name String }`;
const modelAST = ModelTree.parse(modelCode);
const [modelName, modelConfig] = Compiler.model(modelAST);

Best Practices

1. Use Type Safety

The library is built with TypeScript and provides comprehensive type definitions:

import type { SchemaConfig, ModelConfig } from '@stackpress/idea-parser';

const schema: SchemaConfig = parse(code);

2. Handle Errors Gracefully

Always wrap parsing operations in try-catch blocks:

import { parse, Exception } from '@stackpress/idea-parser';

try {
  const result = parse(schemaCode);
  // Process result
} catch (error) {
  if (error instanceof Exception) {
    console.error('Schema parsing failed:', error.message);
    // Handle parsing error
  } else {
    console.error('Unexpected error:', error);
    // Handle other errors
  }
}

3. Choose the Right Function

  • Use parse() when you need to preserve references for further processing
  • Use final() when you want a clean output for final consumption

4. Validate Schema Structure

Ensure your schema follows the expected structure:

// Good: Proper model structure
model User {
  id String @id
  name String
}

// Bad: Missing required properties
model User {
  // Missing columns - will throw error
}

5. Use Meaningful Names

Choose descriptive names for your schema elements:

// Good
enum UserStatus { ACTIVE "Active" SUSPENDED "Suspended" }
prop EmailInput { type "email" format "email" }

// Less clear
enum Status { A "Active" S "Suspended" }
prop Input { type "email" }

Error Handling

Common errors and their solutions:

Invalid Schema Structure

Error: "Invalid Schema"

Solution: Ensure your schema follows the correct syntax and structure.

Missing Required Properties

Error: "Expecting a columns property"

Solution: Models and types must have a columns definition.

Duplicate Declarations

Error: "Duplicate [name]"

Solution: Each declaration name must be unique within the schema.

Unknown References

Error: "Unknown reference [name]"

Solution: Ensure all referenced props and types are defined before use.

Lexer

The Lexer class implements the Parser interface and provides tokenization and parsing utilities for schema code. It manages a dictionary of token definitions and handles the parsing process by matching patterns against the input code.

import { Lexer } from '@stackpress/idea-parser';

Properties

The following properties are available when instantiating a Lexer.

Property Type Description
dictionary Record<string, Definition> Shallow copy of all token definitions
index number Current parsing position in the code

Methods

The following methods are available when instantiating a Lexer.

Cloning the Lexer

The following example shows how to create an exact copy of the lexer's current state.

import { Lexer } from '@stackpress/idea-parser';

const lexer = new Lexer();
lexer.load('enum Status { ACTIVE "Active" }');
lexer.define('enum', enumReader);

// Clone preserves code, index position, and all definitions
const clonedLexer = lexer.clone();
console.log(clonedLexer.index); // Same index as original
console.log(clonedLexer.dictionary); // Same definitions as original

Returns

A new Lexer instance with identical state to the original.

Defining Token Patterns

The following example shows how to register token definitions for parsing.

import { Lexer } from '@stackpress/idea-parser';
import definitions from '@stackpress/idea-parser/definitions';

const lexer = new Lexer();

// Load all predefined token definitions
Object.keys(definitions).forEach((key) => {
  lexer.define(key, definitions[key]);
});

// Define a custom token
lexer.define('customKeyword', (code, start, lexer) => {
  if (code.substring(start, start + 6) === 'custom') {
    return {
      type: 'Keyword',
      value: 'custom',
      start: start,
      end: start + 6
    };
  }
  return undefined;
});

Parameters

Parameter Type Description
key string Unique identifier for the token definition
reader Reader Function that attempts to match and parse the token

Returns

Void (modifies the lexer's internal dictionary).

Expecting Specific Tokens

The following example shows how to require specific tokens at the current position.

import { Lexer } from '@stackpress/idea-parser';
import definitions, { data } from '@stackpress/idea-parser/definitions';

const lexer = new Lexer();
Object.keys(definitions).forEach((key) => {
  lexer.define(key, definitions[key]);
});

// Parse a float literal
lexer.load('4.4');
const floatToken = lexer.expect('Float');
console.log(floatToken.value); // 4.4
console.log(floatToken.start); // 0
console.log(floatToken.end); // 3

// Parse any data type (scalar, object, or array)
lexer.load('"hello world"');
const stringToken = lexer.expect(data);
console.log(stringToken.value); // 'hello world'

// Expect one of multiple token types
lexer.load('true');
const booleanToken = lexer.expect(['Boolean', 'String', 'Integer']);
console.log(booleanToken.value); // true

Parameters

Parameter Type Description
keys string | string[] Token definition key(s) to expect

Returns

The matched token object, or throws an exception if no match is found.

Getting Token Definitions

The following example shows how to retrieve registered token definitions.

import { Lexer } from '@stackpress/idea-parser';

const lexer = new Lexer();
lexer.define('String', definitions['String']);

const definition = lexer.get('String');
if (definition) {
  console.log('Definition key:', definition.key); // 'String'
  console.log('Reader function:', typeof definition.reader); // 'function'
} else {
  console.log('Definition not found');
}

Parameters

Parameter Type Description
key string The token definition key to retrieve

Returns

The Definition object if found, undefined otherwise.

Loading Code

The following example shows how to load code for parsing.

import { Lexer } from '@stackpress/idea-parser';

const lexer = new Lexer();

// Load code from the beginning
lexer.load('enum Status { ACTIVE "Active" }');
console.log(lexer.index); // 0

// Load code starting from a specific position
lexer.load('enum Status { ACTIVE "Active" }', 5);
console.log(lexer.index); // 5
console.log(lexer.substring(5, 11)); // 'Status'

Parameters

Parameter Type Description
code string The source code to parse
index number Starting position in the code (default: 0)

Returns

The Lexer instance to allow method chaining.

Matching Tokens

The following example shows how to find the first matching token from available definitions.

import { Lexer } from '@stackpress/idea-parser';

const lexer = new Lexer();
lexer.define('literal', (code, start) => {
  if (code.startsWith('42', start)) {
    return { type: 'Literal', value: 42, start, end: start + 2 };
  }
  return undefined;
});

const code = '42';

// Match against specific token types
const match = lexer.match(code, 0, ['literal']);
if (match) {
  console.log('Matched:', match.type); // 'Literal'
  console.log('Value:', match.value); // 42
}

// Match against all available definitions (pass undefined for keys)
const match2 = lexer.match('42', 0, undefined);
if (match2) {
  console.log('Matched:', match2.type); // 'Literal'
}

Parameters

Parameter Type Description
code string The code to match against
start number Starting position for matching
keys string[] Optional array of definition keys to try (default: all definitions)

Returns

The first matching token object, or null if no match is found.

Testing for Next Tokens

The following example shows how to check if the next tokens match without consuming them.

import { Lexer } from '@stackpress/idea-parser';
import definitions from '@stackpress/idea-parser/definitions';

const lexer = new Lexer();
Object.keys(definitions).forEach((key) => {
  lexer.define(key, definitions[key]);
});

lexer.load('enum Status { ACTIVE "Active" }');

// Test if next token matches (doesn't advance index)
if (lexer.next('AnyIdentifier')) {
  console.log('Next token is an identifier');
  console.log(lexer.index); // Still 0
}

// Test for multiple possible tokens
if (lexer.next(['String', 'Integer', 'Boolean'])) {
  console.log('Next token is a literal value');
}

Parameters

Parameter Type Description
names string | string[] Token definition key(s) to test for

Returns

true if the next token(s) match, false otherwise. Does not advance the index.

Optional Token Parsing

The following example shows how to optionally parse tokens without throwing errors.

import { Lexer } from '@stackpress/idea-parser';
import definitions from '@stackpress/idea-parser/definitions';

const lexer = new Lexer();
Object.keys(definitions).forEach((key) => {
  lexer.define(key, definitions[key]);
});

lexer.load('/* some comment */ enum Status');

// Try to parse optional whitespace/comments
const comment = lexer.optional('note');
if (comment) {
  console.log('Found comment:', comment.value); // '/* some comment */'
}

// Skip optional whitespace
lexer.optional('whitespace');

// Now parse the enum keyword
const enumToken = lexer.expect('AnyIdentifier');
console.log(enumToken.name); // 'enum'

Parameters

Parameter Type Description
names string | string[] Token definition key(s) to try parsing

Returns

The matched token object if found, undefined otherwise.

Reading Ahead

The following example shows how to read the next available token.

import { Lexer } from '@stackpress/idea-parser';

const lexer = new Lexer();
lexer.load(''); // Empty string

// Read the next token (tries all definitions)
const nextToken = lexer.read();
console.log(nextToken); // undefined (no tokens in empty string)

lexer.load('42');
lexer.define('Integer', definitions['Integer']);

const token = lexer.read();
if (token) {
  console.log('Token type:', token.type); // 'Literal'
  console.log('Token value:', token.value); // 42
}

Returns

The next available token object, or undefined if no token can be parsed.

Getting Substrings

The following example shows how to extract portions of the source code.

import { Lexer } from '@stackpress/idea-parser';

const lexer = new Lexer();
lexer.load('some code');

// Extract specific portions of code
const substring = lexer.substring(5, 9);
console.log(substring); // 'code'

// Return empty string when start and end are the same
const empty = lexer.substring(5, 5);
console.log(empty); // ''

Parameters

Parameter Type Description
start number Starting position in the code
end number Ending position in the code

Returns

The substring of code between start and end positions.

Finding Next Space

The following example shows how to find the next whitespace character (useful for error reporting).

import { Lexer } from '@stackpress/idea-parser';

const lexer = new Lexer();
lexer.load('enum Status { ACTIVE "Active" }');

// Find next space from current position
const spaceIndex = lexer.nextSpace();
console.log(spaceIndex); // 4 (position of space after 'enum')

// If no space found, returns code length
lexer.load('enumStatus');
const endIndex = lexer.nextSpace();
console.log(endIndex); // 10 (length of code)

Returns

The index of the next space character, or the code length if no space is found.

Parsing Complex Data Structures

The Lexer can parse complex nested data structures using the predefined token definitions:

Parsing Objects

import { Lexer } from '@stackpress/idea-parser';
import { Compiler } from '@stackpress/idea-parser';
import definitions, { data } from '@stackpress/idea-parser/definitions';

const lexer = new Lexer();
Object.keys(definitions).forEach((key) => {
  lexer.define(key, definitions[key]);
});

// Parse a simple object
lexer.load('{ foo "bar" bar 4.4 }');
const objectToken = lexer.expect('Object');
const compiled = Compiler.object(objectToken);
console.log(compiled); // { foo: 'bar', bar: 4.4 }

// Parse nested objects
lexer.load('{ foo "bar" zoo { foo false bar null } }');
const nestedToken = lexer.expect('Object');
const nestedCompiled = Compiler.object(nestedToken);
console.log(nestedCompiled.zoo.foo); // false
console.log(nestedCompiled.zoo.bar); // null

Parsing Arrays

// Parse a simple array
lexer.load('[ 4.4 "bar" false null ]');
const arrayToken = lexer.expect('Array');
const compiledArray = Compiler.array(arrayToken);
console.log(compiledArray); // [4.4, 'bar', false, null]

// Parse nested arrays
lexer.load('[ 4.4 "bar" [ 4 true ] ]');
const nestedArrayToken = lexer.expect('Array');
const nestedArray = Compiler.array(nestedArrayToken);
console.log(nestedArray[2]); // [4, true]

// Parse array of objects
lexer.load('[ { label "US" value "United States" } { label "CA" value "Canada" } ]');
const objectArrayToken = lexer.expect('Array');
const objectArray = Compiler.array(objectArrayToken);
console.log(objectArray[0].label); // 'US'
console.log(objectArray[1].value); // 'Canada'

Parsing Comments

// Parse block comments
lexer.load('/* some comment */');
const noteToken = lexer.expect('note');
console.log(noteToken.type); // '_Note'
console.log(noteToken.value); // '/* some comment */'

// Parse line comments
lexer.load('//some comment');
const commentToken = lexer.expect('comment');
console.log(commentToken.type); // '_Comment'
console.log(commentToken.value); // '//some comment'

// Parse multiline block comments
lexer.load(`/* 
  some 
  // comment
*/`);
const multilineToken = lexer.expect('note');
console.log(multilineToken.value); // Contains newlines and nested //

Error Handling

The Lexer provides detailed error information when parsing fails:

Unknown Definitions

const lexer = new Lexer();

// Throws: "Unknown definition unknownKey"
try {
  lexer.expect('unknownKey');
} catch (error) {
  console.log(error.message); // "Unknown definition unknownKey"
}

// Throws: "Unknown definition missingKey"
try {
  lexer.match('some code', 0, ['missingKey']);
} catch (error) {
  console.log(error.message); // "Unknown definition missingKey"
}

Unexpected Tokens

import { Exception } from '@stackpress/idea-parser';

const lexer = new Lexer();
lexer.define('String', definitions['String']);
lexer.load('42'); // Number, not string

try {
  lexer.expect('String'); // Expecting string but found number
} catch (error) {
  if (error instanceof Exception) {
    console.log(error.message); // "Unexpected 42 expecting String"
    console.log(error.start); // Position where error occurred
    console.log(error.end); // End position for error highlighting
  }
}

Predefined Token Definitions

The library comes with comprehensive predefined token definitions:

Literals

  • Null: Matches null keyword
  • Boolean: Matches true and false
  • String: Matches quoted strings "hello"
  • Float: Matches decimal numbers 4.4, -3.14
  • Integer: Matches whole numbers 42, -10
  • Environment: Matches environment variables env("VAR_NAME")

Identifiers

  • AnyIdentifier: Matches any valid identifier [a-z_][a-z0-9_]*
  • UpperIdentifier: Matches uppercase identifiers [A-Z_][A-Z0-9_]*
  • CapitalIdentifier: Matches capitalized identifiers [A-Z_][a-zA-Z0-9_]*
  • CamelIdentifier: Matches camelCase identifiers [a-z_][a-zA-Z0-9_]*
  • LowerIdentifier: Matches lowercase identifiers [a-z_][a-z0-9_]*
  • AttributeIdentifier: Matches attribute identifiers @field.input

Structural

  • Array: Matches array expressions [ ... ]
  • Object: Matches object expressions { ... }
  • {, }, [, ], (, ): Bracket and brace tokens
  • !: Final modifier token

Whitespace and Comments

  • whitespace: Matches any whitespace \s+
  • space: Matches spaces only
  • line: Matches line breaks
  • note: Matches block comments /* ... */
  • comment: Matches line comments // ...

Data Groups

  • scalar: Array of scalar types ['Null', 'Boolean', 'String', 'Float', 'Integer', 'Environment']
  • data: Array of all data types [...scalar, 'Object', 'Array']

Usage with AST

The Lexer is typically used by AST classes for parsing specific language constructs:

import { Lexer, EnumTree } from '@stackpress/idea-parser';

// AST classes configure lexers with appropriate definitions
const lexer = new Lexer();
EnumTree.definitions(lexer); // Adds enum-specific token definitions

lexer.load('enum Status { ACTIVE "Active" INACTIVE "Inactive" }');

const enumTree = new EnumTree(lexer);
const result = enumTree.enum(); // Parse enum using configured lexer

Advanced Usage Patterns

Backtracking with Clone

// Save current state for potential backtracking
const checkpoint = lexer.clone();

try {
  // Try to parse complex structure
  const result = parseComplexStructure(lexer);
  return result;
} catch (error) {
  // Restore state and try alternative parsing
  const restoredLexer = checkpoint;
  return parseAlternativeStructure(restoredLexer);
}

Conditional Parsing

// Parse different types based on lookahead
if (lexer.next('AnyIdentifier')) {
  const identifier = lexer.expect('AnyIdentifier');
  if (identifier.name === 'enum') {
    // Parse enum declaration
  } else if (identifier.name === 'model') {
    // Parse model declaration
  }
}

Custom Reader Functions

Reader functions should follow this pattern:

import type { Reader } from '@stackpress/idea-parser';

const customReader: Reader = (code, start, lexer) => {
  // Check if pattern matches at current position
  if (!code.startsWith('custom', start)) {
    return undefined; // No match
  }
  
  // Calculate end position
  const end = start + 6;
  
  // Return token object
  return {
    type: 'CustomToken',
    value: 'custom',
    start: start,
    end: end
  };
};

// Register the custom reader
lexer.define('custom', customReader);

Compiler

The Compiler class provides static methods for converting Abstract Syntax Tree (AST) tokens into structured JSON configurations. It serves as the bridge between parsed tokens and the final JSON output.

import { Compiler } from '@stackpress/idea-parser';

Static Methods

The following methods can be accessed directly from the Compiler class.

Converting Array Tokens

The following example shows how to compile array tokens into actual arrays.

import { Compiler } from '@stackpress/idea-parser';

// Example array token from parsing ["value1", "value2", "value3"]
const arrayToken = {
  type: 'ArrayExpression',
  elements: [
    { type: 'Literal', value: 'value1' },
    { type: 'Literal', value: 'value2' },
    { type: 'Literal', value: 'value3' }
  ]
};

const result = Compiler.array(arrayToken);
console.log(result); // ['value1', 'value2', 'value3']

Parameters

Parameter Type Description
token ArrayToken The array token to compile
references UseReferences Reference map for resolving identifiers (default: false)

Returns

An array containing the compiled elements.

Converting Data Tokens

The following example shows how to compile various data tokens into their actual values.

import { Compiler } from '@stackpress/idea-parser';

// Compile different types of data tokens
const literalResult = Compiler.data({ type: 'Literal', value: 'hello' });
console.log(literalResult); // 'hello'

const objectResult = Compiler.data({
  type: 'ObjectExpression',
  properties: [
    {
      key: { name: 'name' },
      value: { type: 'Literal', value: 'John' }
    }
  ]
});
console.log(objectResult); // { name: 'John' }

Parameters

Parameter Type Description
token DataToken The data token to compile (can be object, array, literal, or identifier)
references UseReferences Reference map for resolving identifiers (default: false)

Returns

The compiled data value based on the token type.

Converting Enum Declarations

The following example shows how to compile enum declarations into JSON configurations.

import { Compiler } from '@stackpress/idea-parser';

// Example enum token from parsing: enum Status { ACTIVE "Active" INACTIVE "Inactive" }
const enumToken = {
  kind: 'enum',
  declarations: [{
    id: { name: 'Status' },
    init: {
      properties: [
        { key: { name: 'ACTIVE' }, value: { type: 'Literal', value: 'Active' } },
        { key: { name: 'INACTIVE' }, value: { type: 'Literal', value: 'Inactive' } }
      ]
    }
  }]
};

const [name, config] = Compiler.enum(enumToken);
console.log(name); // 'Status'
console.log(config); // { ACTIVE: 'Active', INACTIVE: 'Inactive' }

Parameters

Parameter Type Description
token DeclarationToken The enum declaration token to compile

Returns

A tuple containing the enum name and its configuration object.

Converting Schema to Final JSON

The following example shows how to compile a schema token into a final JSON configuration.

import { Compiler } from '@stackpress/idea-parser';

// This method removes prop and use references for a clean final output
const finalSchema = Compiler.final(schemaToken);
console.log(finalSchema);
// Output will not contain 'prop' or 'use' sections

Parameters

Parameter Type Description
token SchemaToken The schema token to compile into final form

Returns

A FinalSchemaConfig object with references resolved and removed.

Converting Identifier Tokens

The following example shows how to resolve identifier tokens to their actual values.

import { Compiler } from '@stackpress/idea-parser';

// With references provided
const references = { MyProp: { type: 'text' } };
const result1 = Compiler.identifier({ name: 'MyProp' }, references);
console.log(result1); // { type: 'text' }

// Without references (returns template string)
const result2 = Compiler.identifier({ name: 'MyProp' }, false);
console.log(result2); // '${MyProp}'

// With empty references (throws error)
try {
  Compiler.identifier({ name: 'UnknownProp' }, {});
} catch (error) {
  console.log(error.message); // 'Unknown reference UnknownProp'
}

Parameters

Parameter Type Description
token IdentifierToken The identifier token to resolve
references UseReferences Reference map for resolving the identifier

Returns

The resolved value from references, a template string, or throws an error.

Converting Literal Tokens

The following example shows how to extract values from literal tokens.

import { Compiler } from '@stackpress/idea-parser';

const stringLiteral = Compiler.literal({ type: 'Literal', value: 'hello' });
console.log(stringLiteral); // 'hello'

const numberLiteral = Compiler.literal({ type: 'Literal', value: 42 });
console.log(numberLiteral); // 42

const booleanLiteral = Compiler.literal({ type: 'Literal', value: true });
console.log(booleanLiteral); // true

Parameters

Parameter Type Description
token LiteralToken The literal token to extract value from

Returns

The literal value (string, number, boolean, etc.).

Converting Model Declarations

The following example shows how to compile model declarations into JSON configurations.

import { Compiler } from '@stackpress/idea-parser';

// Example model token from parsing: model User { id String @id name String }
const modelToken = {
  kind: 'model',
  mutable: false, // model User! would be true
  declarations: [{
    id: { name: 'User' },
    init: {
      properties: [
        {
          key: { name: 'columns' },
          value: {
            type: 'ObjectExpression',
            properties: [
              {
                key: { name: 'id' },
                value: {
                  type: 'ObjectExpression',
                  properties: [
                    { key: { name: 'type' }, value: { type: 'Literal', value: 'String' } },
                    { key: { name: 'attributes' }, value: { /* attributes */ } }
                  ]
                }
              }
            ]
          }
        }
      ]
    }
  }]
};

const [name, config] = Compiler.model(modelToken);
console.log(name); // 'User'
console.log(config.mutable); // false
console.log(config.columns); // Array of column configurations

Parameters

Parameter Type Description
token DeclarationToken The model declaration token to compile
references UseReferences Reference map for resolving identifiers (default: false)

Returns

A tuple containing the model name and its configuration object.

Converting Object Tokens

The following example shows how to compile object tokens into actual objects.

import { Compiler } from '@stackpress/idea-parser';

// Example object token from parsing { name "John" age 30 }
const objectToken = {
  type: 'ObjectExpression',
  properties: [
    { key: { name: 'name' }, value: { type: 'Literal', value: 'John' } },
    { key: { name: 'age' }, value: { type: 'Literal', value: 30 } }
  ]
};

const result = Compiler.object(objectToken);
console.log(result); // { name: 'John', age: 30 }

Parameters

Parameter Type Description
token ObjectToken The object token to compile
references UseReferences Reference map for resolving identifiers (default: false)

Returns

An object with compiled key-value pairs.

Converting Plugin Declarations

The following example shows how to compile plugin declarations into JSON configurations.

import { Compiler } from '@stackpress/idea-parser';

// Example plugin token from parsing: plugin "./database" { provider "postgresql" }
const pluginToken = {
  kind: 'plugin',
  declarations: [{
    id: { name: './database' },
    init: {
      properties: [
        { key: { name: 'provider' }, value: { type: 'Literal', value: 'postgresql' } }
      ]
    }
  }]
};

const [name, config] = Compiler.plugin(pluginToken);
console.log(name); // './database'
console.log(config); // { provider: 'postgresql' }

Parameters

Parameter Type Description
token DeclarationToken The plugin declaration token to compile

Returns

A tuple containing the plugin name and its configuration object.

Converting Prop Declarations

The following example shows how to compile prop declarations into JSON configurations.

import { Compiler } from '@stackpress/idea-parser';

// Example prop token from parsing: prop Text { type "text" format "lowercase" }
const propToken = {
  kind: 'prop',
  declarations: [{
    id: { name: 'Text' },
    init: {
      properties: [
        { key: { name: 'type' }, value: { type: 'Literal', value: 'text' } },
        { key: { name: 'format' }, value: { type: 'Literal', value: 'lowercase' } }
      ]
    }
  }]
};

const [name, config] = Compiler.prop(propToken);
console.log(name); // 'Text'
console.log(config); // { type: 'text', format: 'lowercase' }

Parameters

Parameter Type Description
token DeclarationToken The prop declaration token to compile
references UseReferences Reference map for resolving identifiers (default: false)

Returns

A tuple containing the prop name and its configuration object.

Converting Schema Declarations

The following example shows how to compile complete schema tokens into JSON configurations.

import { Compiler } from '@stackpress/idea-parser';

// Compile a complete schema with all declaration types
const schemaConfig = Compiler.schema(schemaToken);
console.log(schemaConfig);
// Output contains: { enum: {...}, prop: {...}, type: {...}, model: {...}, plugin: {...}, use: [...] }

// Compile with finalization (resolves references)
const finalizedConfig = Compiler.schema(schemaToken, true);
console.log(finalizedConfig);
// References are resolved in the output

Parameters

Parameter Type Description
token SchemaToken The schema token to compile
finalize boolean Whether to resolve references (default: false)

Returns

A SchemaConfig object containing all compiled declarations.

Converting Type Declarations

The following example shows how to compile type declarations into JSON configurations.

import { Compiler } from '@stackpress/idea-parser';

// Example type token from parsing: type Address { street String city String }
const typeToken = {
  kind: 'type',
  mutable: true, // type Address (mutable) vs type Address! (immutable)
  declarations: [{
    id: { name: 'Address' },
    init: {
      properties: [
        {
          key: { name: 'columns' },
          value: {
            type: 'ObjectExpression',
            properties: [
              {
                key: { name: 'street' },
                value: {
                  type: 'ObjectExpression',
                  properties: [
                    { key: { name: 'type' }, value: { type: 'Literal', value: 'String' } }
                  ]
                }
              }
            ]
          }
        }
      ]
    }
  }]
};

const [name, config] = Compiler.type(typeToken);
console.log(name); // 'Address'
console.log(config.mutable); // true
console.log(config.columns); // Array of column configurations

Parameters

Parameter Type Description
token DeclarationToken The type declaration token to compile
references UseReferences Reference map for resolving identifiers (default: false)

Returns

A tuple containing the type name and its configuration object.

Converting Use Declarations

The following example shows how to compile use (import) declarations.

import { Compiler } from '@stackpress/idea-parser';

// Example use token from parsing: use "./another.idea"
const useToken = {
  type: 'ImportDeclaration',
  source: { type: 'Literal', value: './another.idea' }
};

const importPath = Compiler.use(useToken);
console.log(importPath); // './another.idea'

Parameters

Parameter Type Description
token ImportToken The import declaration token to compile

Returns

The import path as a string.

Error Handling

The Compiler class throws Exception errors for various invalid conditions:

Invalid Token Types

// Throws: "Invalid data token type"
Compiler.data({ type: 'UnknownType' });

// Throws: "Invalid Enum"
Compiler.enum({ kind: 'notAnEnum' });

// Throws: "Invalid Plugin"
Compiler.plugin({ kind: 'notAPlugin' });

// Throws: "Invalid Prop"
Compiler.prop({ kind: 'notAProp' });

// Throws: "Invalid Schema"
Compiler.schema({ kind: 'notASchema' });

// Throws: "Invalid Type"
Compiler.type({ kind: 'notAType' });

// Throws: "Invalid Import"
Compiler.use({ type: 'NotAnImportDeclaration' });

Missing Required Properties

// Throws: "Expecting a columns property"
Compiler.model({
  kind: 'model',
  declarations: [{
    id: { name: 'User' },
    init: { properties: [] } // Missing columns
  }]
});

Unknown References

// Throws: "Unknown reference MyProp"
Compiler.identifier({ name: 'MyProp' }, {});

Duplicate Declarations

// Throws: "Duplicate MyEnum" when compiling schema with duplicate names
Compiler.schema(schemaWithDuplicates);

Type Processing

The Compiler automatically processes type information for models and types:

Type Modifiers

  • Optional types: String?{ type: 'String', required: false }
  • Array types: String[]{ type: 'String', multiple: true }
  • Combined: String[]?{ type: 'String', required: false, multiple: true }

Column Configuration

Models and types are converted from object format to array format to preserve column order:

// Input object format
{
  columns: {
    id: { type: 'String', attributes: { id: true } },
    name: { type: 'String', attributes: { required: true } }
  }
}

// Output array format
{
  columns: [
    { name: 'id', type: 'String', required: true, multiple: false, attributes: { id: true } },
    { name: 'name', type: 'String', required: true, multiple: false, attributes: { required: true } }
  ]
}

Usage with AST

The Compiler is typically used in conjunction with AST classes:

import { Compiler, EnumTree, ModelTree, SchemaTree } from '@stackpress/idea-parser';

// Parse and compile individual components
const enumAST = EnumTree.parse('enum Status { ACTIVE "Active" }');
const [enumName, enumConfig] = Compiler.enum(enumAST);

const modelAST = ModelTree.parse('model User { id String @id }');
const [modelName, modelConfig] = Compiler.model(modelAST);

// Parse and compile complete schema
const schemaAST = SchemaTree.parse(schemaCode);
const schemaConfig = Compiler.schema(schemaAST);

Syntax Trees

The AST classes are responsible for parsing specific parts of schema code into Abstract Syntax Trees (ASTs). Each AST class handles a different type of declaration or construct in the schema language.

import { 
  SchemaTree, 
  EnumTree, 
  ModelTree, 
  TypeTree, 
  PropTree, 
  PluginTree, 
  UseTree 
} from '@stackpress/idea-parser';

SchemaTree

Parses complete schema files containing multiple declarations.

Static Methods

Setting Up Schema Definitions

The following example shows how to configure a lexer for schema parsing.

import { SchemaTree, Lexer } from '@stackpress/idea-parser';

const lexer = new Lexer();
SchemaTree.definitions(lexer);

// Lexer now has definitions for all schema constructs:
// enum, prop, type, model, plugin, use keywords and structures

Parameters

Parameter Type Description
lexer Lexer The lexer instance to configure

Returns

The configured lexer instance.

Parsing Complete Schemas

The following example shows how to parse a complete schema file.

import { SchemaTree } from '@stackpress/idea-parser';

const schemaCode = `
plugin "./database" {
  provider "postgresql"
}

enum Status {
  ACTIVE "Active"
  INACTIVE "Inactive"
}

prop Text { type "text" }

model User {
  id String @id
  name String @field.input(Text)
  status Status
}
`;

const ast = SchemaTree.parse(schemaCode);
console.log(ast.type); // 'Program'
console.log(ast.kind); // 'schema'
console.log(ast.body.length); // 4 (plugin, enum, prop, model)

Parameters

Parameter Type Description
code string The complete schema code to parse

Returns

A SchemaToken representing the entire parsed schema.

Methods

Parsing Schema Content

The following example shows how to parse schema content with custom starting position.

import { SchemaTree } from '@stackpress/idea-parser';

const tree = new SchemaTree();
const schemaCode = 'enum Status { ACTIVE "Active" }';

const result = tree.parse(schemaCode, 0);
console.log(result.body[0].kind); // 'enum'

Parameters

Parameter Type Description
code string The schema code to parse
start number Starting position in the code (default: 0)

Returns

A SchemaToken containing all parsed declarations.

EnumTree

Parses enum declarations into AST tokens.

Static Methods

Setting Up Enum Definitions

The following example shows how to configure a lexer for enum parsing.

import { EnumTree, Lexer } from '@stackpress/idea-parser';

const lexer = new Lexer();
EnumTree.definitions(lexer);

// Adds 'EnumWord' token definition for 'enum' keyword
Parsing Enum Declarations

The following example shows how to parse enum declarations based on the test fixtures.

import { EnumTree } from '@stackpress/idea-parser';

const enumCode = `enum Roles {
  ADMIN "Admin"
  MANAGER "Manager"
  USER "User"
}`;

const ast = EnumTree.parse(enumCode);
console.log(ast.kind); // 'enum'
console.log(ast.declarations[0].id.name); // 'Roles'
console.log(ast.declarations[0].init.properties.length); // 3
console.log(ast.declarations[0].init.properties[0].key.name); // 'ADMIN'
console.log(ast.declarations[0].init.properties[0].value.value); // 'Admin'

Parameters

Parameter Type Description
code string The enum declaration code to parse
start number Starting position in the code (default: 0)

Returns

A DeclarationToken representing the parsed enum.

Methods

Parsing Enum Structure

The following example shows how to parse the enum structure.

const tree = new EnumTree();
tree._lexer.load('enum Status { ACTIVE "Active" INACTIVE "Inactive" }');

const enumToken = tree.enum();
console.log(enumToken.declarations[0].id.name); // 'Status'
console.log(enumToken.declarations[0].init.properties[0].key.name); // 'ACTIVE'
console.log(enumToken.declarations[0].init.properties[0].value.value); // 'Active'

Returns

A DeclarationToken representing the enum structure.

Parsing Enum Properties

The following example shows how individual enum properties are parsed.

// Inside enum parsing, after opening brace
const property = tree.property();
console.log(property.key.name); // e.g., 'ADMIN'
console.log(property.value.value); // e.g., 'Admin'

Returns

A PropertyToken representing a single enum key-value pair.

ModelTree

Parses model declarations (extends TypeTree for shared functionality).

Static Methods

Parsing Model Declarations

The following example shows how to parse model declarations based on the test fixtures.

import { ModelTree } from '@stackpress/idea-parser';

const modelCode = `model User @label("User" "Users") {
  id       String       @label("ID")         @id @default("nanoid(20)")
  username String       @label("Username")   @searchable @field.input(Text) @is.required
  password String       @label("Password")   @field.password @is.clt(80) @is.cgt(8) @is.required @list.hide @view.hide
  role     Roles        @label("Role")       @filterable @field.select @list.text(Uppercase) @view.text(Uppercase)
  address  Address?     @label("Address")    @list.hide
  age      Number       @label("Age")        @unsigned @filterable @sortable @field.number(Age) @is.gt(0) @is.lt(150)
  active   Boolean      @label("Active")     @default(true) @filterable @field.switch @list.yesno @view.yesno
  created  Date         @label("Created")    @default("now()") @filterable @sortable @list.date(Pretty)
}`;

const ast = ModelTree.parse(modelCode);
console.log(ast.kind); // 'model'
console.log(ast.mutable); // false (because of '!' modifier)
console.log(ast.declarations[0].id.name); // 'User'

Parameters

Parameter Type Description
code string The model declaration code to parse
start number Starting position in the code (default: 0)

Returns

A DeclarationToken representing the parsed model.

Methods

Parsing Model Structure

The following example shows how to parse the model structure.

const tree = new ModelTree();
tree._lexer.load('model User { id String @id }');

const modelToken = tree.model();
console.log(modelToken.kind); // 'model'
console.log(modelToken.mutable); // false (immutable due to '!')

Returns

A DeclarationToken representing the model structure.

TypeTree

Parses type declarations.

Static Methods

Parsing Type Declarations

The following example shows how to parse type declarations.

import { TypeTree } from '@stackpress/idea-parser';

const typeCode = `type Address @label("Address" "Addresses") {
  street String @field.input @is.required
  city String @field.input @is.required
  country String @field.select
  postal String @field.input
}`;

const ast = TypeTree.parse(typeCode);
console.log(ast.kind); // 'type'
console.log(ast.mutable); // true (mutable by default)
console.log(ast.declarations[0].id.name); // 'Address'

Parameters

Parameter Type Description
code string The type declaration code to parse
start number Starting position in the code (default: 0)

Returns

A DeclarationToken representing the parsed type.

Methods

Parsing Type Structure

The following example shows how to parse the type structure.

const tree = new TypeTree();
tree._lexer.load('type Address { street String city String }');

const typeToken = tree.type();
console.log(typeToken.kind); // 'type'
console.log(typeToken.mutable); // true (default for types)

Returns

A DeclarationToken representing the type structure.

Parsing Type Properties

The following example shows how type properties (columns) are parsed.

// Inside type parsing
const property = tree.property();
console.log(property.key.name); // e.g., 'street'
console.log(property.value); // Object containing type and attributes

Returns

A PropertyToken representing a type column definition.

Parsing Type Parameters

The following example shows how type parameters are parsed.

// For parsing generic type parameters
const parameter = tree.parameter();
console.log(parameter.key.name); // Parameter name
console.log(parameter.value); // Parameter type/constraint

Returns

A PropertyToken representing a type parameter.

PropTree

Parses prop (property configuration) declarations.

Static Methods

Parsing Prop Declarations

The following example shows how to parse prop declarations.

import { PropTree } from '@stackpress/idea-parser';

const propCode = `prop EmailInput {
  type "email"
  format "email"
  placeholder "Enter your email"
  required true
}`;

const ast = PropTree.parse(propCode);
console.log(ast.kind); // 'prop'
console.log(ast.declarations[0].id.name); // 'EmailInput'

Parameters

Parameter Type Description
code string The prop declaration code to parse
start number Starting position in the code (default: 0)

Returns

A DeclarationToken representing the parsed prop.

Methods

Parsing Prop Structure

The following example shows how to parse the prop structure.

const tree = new PropTree();
tree._lexer.load('prop Text { type "text" format "lowercase" }');

const propToken = tree.prop();
console.log(propToken.kind); // 'prop'
console.log(propToken.declarations[0].id.name); // 'Text'

Returns

A DeclarationToken representing the prop configuration.

PluginTree

Parses plugin declarations.

Static Methods

Parsing Plugin Declarations

The following example shows how to parse plugin declarations.

import { PluginTree } from '@stackpress/idea-parser';

const pluginCode = `plugin "./database-plugin" {
  provider "postgresql"
  url env("DATABASE_URL")
  previewFeatures ["fullTextSearch"]
}`;

const ast = PluginTree.parse(pluginCode);
console.log(ast.kind); // 'plugin'
console.log(ast.declarations[0].id.name); // './database-plugin'

Parameters

Parameter Type Description
code string The plugin declaration code to parse
start number Starting position in the code (default: 0)

Returns

A DeclarationToken representing the parsed plugin.

Methods

Parsing Plugin Structure

The following example shows how to parse the plugin structure.

const tree = new PluginTree();
tree._lexer.load('plugin "./custom" { provider "custom-provider" }');

const pluginToken = tree.plugin();
console.log(pluginToken.kind); // 'plugin'
console.log(pluginToken.declarations[0].id.name); // './custom'

Returns

A DeclarationToken representing the plugin configuration.

UseTree

Parses use (import) declarations.

Static Methods

Parsing Use Declarations

The following example shows how to parse use declarations.

import { UseTree } from '@stackpress/idea-parser';

const useCode = 'use "./shared/types.idea"';

const ast = UseTree.parse(useCode);
console.log(ast.type); // 'ImportDeclaration'
console.log(ast.source.value); // './shared/types.idea'

Parameters

Parameter Type Description
code string The use declaration code to parse
start number Starting position in the code (default: 0)

Returns

An ImportToken representing the parsed use statement.

Methods

Parsing Use Structure

The following example shows how to parse the use structure.

const tree = new UseTree();
tree._lexer.load('use "./another.idea"');

const useToken = tree.use();
console.log(useToken.type); // 'ImportDeclaration'
console.log(useToken.source.value); // './another.idea'

Returns

An ImportToken representing the import statement.

Usage Patterns

Parsing Individual Components

import { EnumTree, ModelTree, TypeTree } from '@stackpress/idea-parser';

// Parse individual enum
const enumAST = EnumTree.parse(`enum Roles {
  ADMIN "Admin"
  MANAGER "Manager"
  USER "User"
}`);

// Parse individual model
const modelAST = ModelTree.parse(`model User {
  id String @id
  username String @is.required
}`);

// Parse individual type
const typeAST = TypeTree.parse(`type Address {
  street String
  city String
}`);

Using with Compiler

import { EnumTree, Compiler } from '@stackpress/idea-parser';

// Parse and compile in one step
const enumAST = EnumTree.parse(`enum Status {
  ACTIVE "Active"
  INACTIVE "Inactive"
}`);
const [enumName, enumConfig] = Compiler.enum(enumAST);

console.log(enumName); // 'Status'
console.log(enumConfig); // { ACTIVE: 'Active', INACTIVE: 'Inactive' }

Custom AST Classes

You can extend AbstractTree to create custom parsers:

import { AbstractTree, Lexer } from '@stackpress/idea-parser';
import type { DeclarationToken } from '@stackpress/idea-parser';

class CustomTree extends AbstractTree<DeclarationToken> {
  static definitions(lexer: Lexer) {
    super.definitions(lexer);
    // Add custom token definitions
    lexer.define('CustomKeyword', (code, index) => {
      // Custom token reader implementation
    });
    return lexer;
  }

  static parse(code: string, start = 0) {
    return new this().parse(code, start);
  }

  parse(code: string, start = 0): DeclarationToken {
    this._lexer.load(code, start);
    return this.customDeclaration();
  }

  customDeclaration(): DeclarationToken {
    // Custom parsing logic
    const keyword = this._lexer.expect('CustomKeyword');
    // ... more parsing logic
    
    return {
      type: 'VariableDeclaration',
      kind: 'custom',
      start: keyword.start,
      end: this._lexer.index,
      declarations: [/* ... */]
    };
  }
}

Error Handling

AST classes provide detailed error information when parsing fails:

Syntax Errors

import { SchemaTree, Exception } from '@stackpress/idea-parser';

try {
  // Invalid syntax - missing closing brace
  SchemaTree.parse('enum Status { ACTIVE "Active"');
} catch (error) {
  if (error instanceof Exception) {
    console.log('Parse error:', error.message);
    console.log('Position:', error.start, '-', error.end);
  }
}

Unexpected Tokens

try {
  // Invalid - 'enum' keyword expected but found 'model'
  EnumTree.parse('model User { id String }');
} catch (error) {
  console.log('Expected enum but found model');
}

Empty Input Handling

import { EnumTree } from '@stackpress/idea-parser';

try {
  // Empty string will throw an error
  EnumTree.parse('');
} catch (error) {
  console.log('Error:', error.message); // 'Unexpected end of input'
}

Invalid Identifiers

import { ModelTree } from '@stackpress/idea-parser';

try {
  // Invalid - model names must be capitalized
  ModelTree.parse('model user { id String }');
} catch (error) {
  console.log('Expected CapitalIdentifier but got something else');
}

Integration with Main Functions

AST classes are used internally by the main parse and final functions:

// This is what happens internally:
import { SchemaTree, Compiler } from '@stackpress/idea-parser';

export function parse(code: string) {
  const ast = SchemaTree.parse(code);  // Parse to AST
  return Compiler.schema(ast);         // Compile to JSON
}

export function final(code: string) {
  const ast = SchemaTree.parse(code);  // Parse to AST
  return Compiler.final(ast);          // Compile and clean up
}

Performance Considerations

Lexer Reuse

AST classes can share lexer instances for better performance:

import { Lexer, SchemaTree } from '@stackpress/idea-parser';

// Create and configure lexer once
const lexer = new Lexer();
SchemaTree.definitions(lexer);

// Reuse for multiple parses
const tree = new SchemaTree(lexer);

const result1 = tree.parse(code1);
const result2 = tree.parse(code2);
const result3 = tree.parse(code3);

Cloning for Backtracking

AST classes use lexer cloning for safe parsing attempts:

// Inside tree parsing methods
const checkpoint = this._lexer.clone();

try {
  // Try to parse optional structure
  return this.parseOptionalStructure();
} catch (error) {
  // Restore lexer state and continue
  this._lexer = checkpoint;
  return this.parseAlternativeStructure();
}

Test-Driven Examples

Based on the test fixtures, here are real-world examples:

Enum with Multiple Values

const enumCode = `enum Roles {
  ADMIN "Admin"
  MANAGER "Manager"
  USER "User"
}`;

const ast = EnumTree.parse(enumCode);
// Produces a complete AST with all three enum values

Complex Model with Attributes

const modelCode = `model User @label("User" "Users") {
  id       String       @label("ID")         @id @default("nanoid(20)")
  username String       @label("Username")   @searchable @field.input(Text) @is.required
  password String       @label("Password")   @field.password @is.clt(80) @is.cgt(8) @is.required @list.hide @view.hide
  role     Roles        @label("Role")       @filterable @field.select @list.text(Uppercase) @view.text(Uppercase)
  address  Address?     @label("Address")    @list.hide
  age      Number       @label("Age")        @unsigned @filterable @sortable @field.number(Age) @is.gt(0) @is.lt(150)
  balance  Number[]     @label("Balance")    @filterable @sortable @field.number() @list.number() @view.number
  active   Boolean      @label("Active")     @default(true) @filterable @field.switch @list.yesno @view.yesno
  created  Date         @label("Created")    @default("now()") @filterable @sortable @list.date(Pretty)
  updated  Date         @label("Updated")    @default("updated()") @filterable @sortable @list.date(Pretty)
  company  Company?     @label("My Company") 
}`;

const ast = ModelTree.parse(modelCode);
// Produces a complete model AST with all columns and attributes

This demonstrates the parser's ability to handle:

  • Model mutability (! modifier)
  • Attributes (@label, @id, @default, etc.)
  • Optional types (Address?, Company?)
  • Array types (Number[])
  • Complex attribute parameters (@field.input(Text), @is.clt(80))

Tokens

Token types define the Abstract Syntax Tree (AST) structures used by the idea parser to represent parsed schema code. These types form the foundation of the parsing system, providing type-safe representations of schema elements.

import type { 
  SchemaToken, 
  DeclarationToken, 
  IdentifierToken,
  LiteralToken,
  ObjectToken,
  ArrayToken,
  PropertyToken,
  ImportToken
} from '@stackpress/idea-parser';

Core Token Types

The following types define the fundamental token structures used throughout the parsing system.

UnknownToken

Base token structure for unrecognized or generic tokens during parsing.

const unknownToken: UnknownToken = {
  type: 'CustomType',
  start: 0,
  end: 10,
  value: 'some value',
  raw: 'raw text'
};

Properties

Property Type Description
type string Token type identifier
start number Starting character position in source code
end number Ending character position in source code
value any Parsed value of the token
raw string Raw text from source code

Usage

Used as a fallback for tokens that don't match specific patterns and as a base structure for other token types.

IdentifierToken

Represents identifiers such as variable names, type names, and property keys.

const identifierToken: IdentifierToken = {
  type: 'Identifier',
  name: 'UserRole',
  start: 5,
  end: 13
};

Properties

Property Type Description
type 'Identifier' Always 'Identifier' for identifier tokens
name string The identifier name
start number Starting character position
end number Ending character position

Usage

Used throughout the parser for:

  • Enum names: enum UserRole
  • Model names: model User
  • Property names: name String
  • Type references: role UserRole

Examples from Tests

// From enum.json fixture
{
  "type": "Identifier",
  "start": 5,
  "end": 10,
  "name": "Roles"
}

// Property key identifier
{
  "type": "Identifier", 
  "start": 15,
  "end": 20,
  "name": "ADMIN"
}

LiteralToken

Represents literal values such as strings, numbers, booleans, and null.

const stringLiteral: LiteralToken = {
  type: 'Literal',
  start: 21,
  end: 28,
  value: 'Admin',
  raw: '"Admin"'
};

const numberLiteral: LiteralToken = {
  type: 'Literal',
  start: 10,
  end: 12,
  value: 42,
  raw: '42'
};

Properties

Property Type Description
type 'Literal' Always 'Literal' for literal tokens
start number Starting character position
end number Ending character position
value any The parsed literal value
raw string Raw text representation from source

Usage

Used for all scalar values in schema definitions:

  • String literals: "Admin", "localhost"
  • Number literals: 5432, 3.14
  • Boolean literals: true, false
  • Null literals: null

Examples from Tests

// From enum.json fixture
{
  "type": "Literal",
  "start": 21,
  "end": 28,
  "value": "Admin",
  "raw": "'Admin'"
}

ObjectToken

Represents object expressions containing key-value pairs.

const objectToken: ObjectToken = {
  type: 'ObjectExpression',
  start: 0,
  end: 64,
  properties: [
    {
      type: 'Property',
      kind: 'init',
      start: 15,
      end: 28,
      method: false,
      shorthand: false,
      computed: false,
      key: { type: 'Identifier', name: 'ADMIN', start: 15, end: 20 },
      value: { type: 'Literal', value: 'Admin', start: 21, end: 28, raw: '"Admin"' }
    }
  ]
};

Properties

Property Type Description
type 'ObjectExpression' Always 'ObjectExpression' for object tokens
start number Starting character position
end number Ending character position
properties PropertyToken[] Array of property tokens

Usage

Used for:

  • Enum definitions: { ADMIN "Admin", USER "User" }
  • Model column definitions: { id String @id, name String }
  • Plugin configurations: { provider "postgresql", url env("DATABASE_URL") }
  • Attribute parameters: @field.input({ type "text" })

Examples from Tests

The enum fixture shows an ObjectToken containing three PropertyTokens for ADMIN, MANAGER, and USER enum values.

ArrayToken

Represents array expressions containing ordered elements.

const arrayToken: ArrayToken = {
  type: 'ArrayExpression',
  start: 0,
  end: 25,
  elements: [
    { type: 'Literal', value: 'item1', start: 2, end: 9, raw: '"item1"' },
    { type: 'Literal', value: 'item2', start: 11, end: 18, raw: '"item2"' }
  ]
};

Properties

Property Type Description
type 'ArrayExpression' Always 'ArrayExpression' for array tokens
start number Starting character position
end number Ending character position
elements DataToken[] Array of data tokens

Usage

Used for:

  • Array type definitions: String[]
  • Plugin feature lists: previewFeatures ["fullTextSearch", "metrics"]
  • Attribute arrays: @is.oneOf(["admin", "user", "guest"])

PropertyToken

Represents key-value pairs within object expressions.

const propertyToken: PropertyToken = {
  type: 'Property',
  kind: 'init',
  start: 15,
  end: 28,
  method: false,
  shorthand: false,
  computed: false,
  key: {
    type: 'Identifier',
    name: 'ADMIN',
    start: 15,
    end: 20
  },
  value: {
    type: 'Literal',
    value: 'Admin',
    start: 21,
    end: 28,
    raw: '"Admin"'
  }
};

Properties

Property Type Description
type 'Property' Always 'Property' for property tokens
kind 'init' Always 'init' for initialization properties
start number Starting character position
end number Ending character position
method boolean Always false (not used for method properties)
shorthand boolean Always false (not used for shorthand properties)
computed boolean Always false (not used for computed properties)
key IdentifierToken Property key identifier
value DataToken Property value (literal, object, array, or identifier)

Usage

Used within ObjectTokens for:

  • Enum key-value pairs: ADMIN "Admin"
  • Model column definitions: id String
  • Plugin configuration options: provider "postgresql"
  • Attribute parameters: type "text"

Examples from Tests

From the enum fixture, each enum value is represented as a PropertyToken with an IdentifierToken key and LiteralToken value.

Declaration Tokens

The following types represent top-level declarations in schema files.

DeclarationToken

Represents variable declarations for enums, props, types, models, and plugins.

const enumDeclaration: DeclarationToken = {
  type: 'VariableDeclaration',
  kind: 'enum',
  start: 0,
  end: 64,
  declarations: [{
    type: 'VariableDeclarator',
    start: 5,
    end: 64,
    id: {
      type: 'Identifier',
      name: 'Roles',
      start: 5,
      end: 10
    },
    init: {
      type: 'ObjectExpression',
      start: 0,
      end: 64,
      properties: [/* property tokens */]
    }
  }]
};

Properties

Property Type Description
type 'VariableDeclaration' Always 'VariableDeclaration' for declarations
kind string Declaration type: 'enum', 'prop', 'type', 'model', 'plugin'
mutable boolean Optional mutability flag (for types and models)
start number Starting character position
end number Ending character position
declarations [DeclaratorToken] Array with single declarator token

Usage

Used by all tree parsers (EnumTree, PropTree, TypeTree, ModelTree, PluginTree) to represent their respective declarations. The kind property determines how the Compiler processes the declaration.

Examples from Tests

The enum fixture shows a complete DeclarationToken with kind 'enum' containing the Roles enum definition.

DeclaratorToken

Represents the declarator part of a variable declaration, containing the identifier and initialization.

const declaratorToken: DeclaratorToken = {
  type: 'VariableDeclarator',
  start: 5,
  end: 64,
  id: {
    type: 'Identifier',
    name: 'Roles',
    start: 5,
    end: 10
  },
  init: {
    type: 'ObjectExpression',
    start: 0,
    end: 64,
    properties: [/* property tokens */]
  }
};

Properties

Property Type Description
type 'VariableDeclarator' Always 'VariableDeclarator' for declarators
start number Starting character position
end number Ending character position
id IdentifierToken Declaration identifier (name)
init ObjectToken Initialization object containing the declaration body

Usage

Used within DeclarationTokens to separate the declaration name from its body. The id contains the name (e.g., "Roles", "User") and init contains the definition object.

ImportToken

Represents use statements for importing other schema files.

const importToken: ImportToken = {
  type: 'ImportDeclaration',
  start: 0,
  end: 25,
  specifiers: [],
  source: {
    type: 'Literal',
    value: './shared/types.idea',
    start: 4,
    end: 25,
    raw: '"./shared/types.idea"'
  }
};

Properties

Property Type Description
type 'ImportDeclaration' Always 'ImportDeclaration' for imports
start number Starting character position
end number Ending character position
specifiers [] Always empty array (not used for named imports)
source LiteralToken Source file path as literal token

Usage

Used by UseTree to represent use "./path/to/file.idea" statements. The Compiler extracts the source path for dependency resolution.

SchemaToken

Represents the complete parsed schema file containing all declarations and imports.

const schemaToken: SchemaToken = {
  type: 'Program',
  kind: 'schema',
  start: 0,
  end: 150,
  body: [
    // ImportTokens for use statements
    {
      type: 'ImportDeclaration',
      start: 0,
      end: 25,
      specifiers: [],
      source: { type: 'Literal', value: './types.idea', start: 4, end: 25, raw: '"./types.idea"' }
    },
    // DeclarationTokens for enums, props, types, models, plugins
    {
      type: 'VariableDeclaration',
      kind: 'enum',
      start: 27,
      end: 91,
      declarations: [/* declarator */]
    }
  ]
};

Properties

Property Type Description
type 'Program' Always 'Program' for complete schemas
kind 'schema' Always 'schema' for schema files
start number Starting character position (usually 0)
end number Ending character position
body (DeclarationToken|ImportToken)[] Array of all declarations and imports

Usage

Used by SchemaTree as the root token representing the entire parsed schema file. The Compiler processes the body array to generate the final schema configuration.

Union Types

The following types provide flexible token handling for different contexts.

Token

Union type for all possible token types that can be returned by readers.

type Token = DataToken | UnknownToken;

Usage

Used as the return type for lexer operations and reader functions. Allows handling both recognized data tokens and unknown tokens.

DataToken

Union type for tokens representing data values.

type DataToken = IdentifierToken | LiteralToken | ObjectToken | ArrayToken;

Usage

Used throughout the Compiler for processing data values. These tokens can be converted to actual JavaScript values using Compiler.data().

Parser Interface

The following types define the parser interface and reader functions.

Reader

Function type for token readers that attempt to parse specific patterns.

type Reader = (
  code: string, 
  start: number, 
  lexer: Parser
) => Token | undefined;

Parameters

Parameter Type Description
code string Source code being parsed
start number Starting position to attempt parsing
lexer Parser Parser instance for recursive parsing

Returns

Token object if pattern matches, undefined otherwise.

Usage

Used to define token recognition patterns in the definitions system. Each token type has a corresponding reader function.

Definition

Pairs a token key with its reader function for lexer registration.

type Definition = { 
  key: string, 
  reader: Reader 
};

Properties

Property Type Description
key string Unique identifier for the token type
reader Reader Function that attempts to parse the token

Usage

Used by the Lexer to register and manage token definitions. The key identifies the token type, and the reader attempts to parse it.

Parser

Interface defining the contract for parser implementations.

interface Parser {
  get dictionary(): Record<string, Definition>;
  get index(): number;
  clone(): Parser;
  define(key: string, reader: Reader, type?: string): void;
  expect<T = Token>(keys: string | string[]): T;
  get(key: string): Definition | undefined;
  load(code: string, index: number): this;
  match(code: string, start: number, keys?: string[]): Token | null;
  next(keys: string | string[]): boolean;
  optional<T = Token>(keys: string | string[]): T | undefined;
  read(): Token | undefined;
}

Usage

Implemented by the Lexer class to provide consistent parsing operations across all tree parsers.

Reference Types

The following types handle reference resolution and data processing.

UseReferences

Type for managing prop and type references during compilation.

type UseReferences = Record<string, any> | false;

Usage

Used by the Compiler to resolve identifier references:

  • false: Return template strings like ${PropName}
  • Record<string, any>: Resolve identifiers to actual values
  • Empty object {}: Throw error for unknown references

Scalar

Union type for primitive values that can be stored in schema configurations.

type Scalar = string | number | null | boolean;

Usage

Used in enum configurations and other places where only primitive values are allowed.

Data

Recursive type for nested data structures in schema configurations.

type Data = Scalar | Data[] | { [key: string]: Data };

Usage

Used throughout the system for representing complex nested data structures in plugin configurations, attributes, and other schema elements.

Usage Examples

Parsing and Token Generation

import { EnumTree } from '@stackpress/idea-parser';

const enumCode = `enum Roles {
  ADMIN "Admin"
  MANAGER "Manager"
  USER "User"
}`;

// Parse generates a DeclarationToken
const enumToken = EnumTree.parse(enumCode);
console.log(enumToken.kind); // 'enum'
console.log(enumToken.declarations[0].id.name); // 'Roles'

Token Processing with Compiler

import { Compiler } from '@stackpress/idea-parser';

// Convert DeclarationToken to configuration
const [enumName, enumConfig] = Compiler.enum(enumToken);
console.log(enumName); // 'Roles'
console.log(enumConfig); // { ADMIN: 'Admin', MANAGER: 'Manager', USER: 'User' }

Working with Complex Tokens

// ObjectToken processing
const objectToken: ObjectToken = {
  type: 'ObjectExpression',
  start: 0,
  end: 30,
  properties: [
    {
      type: 'Property',
      kind: 'init',
      start: 2,
      end: 15,
      method: false,
      shorthand: false,
      computed: false,
      key: { type: 'Identifier', name: 'type', start: 2, end: 6 },
      value: { type: 'Literal', value: 'text', start: 7, end: 13, raw: '"text"' }
    }
  ]
};

const compiled = Compiler.object(objectToken);
console.log(compiled); // { type: 'text' }

Error Handling with Tokens

import { Exception } from '@stackpress/idea-parser';

try {
  const invalidToken = { kind: 'invalid' } as DeclarationToken;
  Compiler.enum(invalidToken);
} catch (error) {
  if (error instanceof Exception) {
    console.log('Token error:', error.message); // 'Invalid Enum'
  }
}

Token Validation

Tokens include position information for error reporting and validation:

// Position information for error highlighting
const token: IdentifierToken = {
  type: 'Identifier',
  name: 'InvalidName',
  start: 10,
  end: 21
};

// Can be used to highlight errors in editors
const errorRange = { start: token.start, end: token.end };

Integration with AST

AST classes generate specific token types:

  • EnumTree: Generates DeclarationToken with kind: 'enum'
  • PropTree: Generates DeclarationToken with kind: 'prop'
  • TypeTree: Generates DeclarationToken with kind: 'type'
  • ModelTree: Generates DeclarationToken with kind: 'model'
  • PluginTree: Generates DeclarationToken with kind: 'plugin'
  • UseTree: Generates ImportToken
  • SchemaTree: Generates SchemaToken containing all other tokens

Each AST class uses the Lexer to generate appropriate tokens, which are then processed by the Compiler to produce the final JSON configuration.

Exception

The Exception class extends the Exception class from @stackpress/lib to provide enhanced error handling specific to the idea parser library. It includes position information and better error reporting for parsing failures.

import { Exception } from '@stackpress/idea-parser';

Overview

Exception is a specialized error class that extends the base Exception class with additional functionality for parser-specific error handling. It automatically includes position information when parsing fails, making it easier to identify and fix syntax errors in schema files.

Usage Examples

Basic Error Handling

import { parse, Exception } from '@stackpress/idea-parser';

try {
  const result = parse('invalid schema syntax');
} catch (error) {
  if (error instanceof Exception) {
    console.log('Parser error:', error.message);
    console.log('Error position:', error.start, '-', error.end);
    console.log('Stack trace:', error.stack);
  }
}

Position Information

Exception includes position information to help locate errors in the source code:

import { EnumTree, Exception } from '@stackpress/idea-parser';

try {
  // Missing closing brace
  EnumTree.parse('enum Status { ACTIVE "Active"');
} catch (error) {
  if (error instanceof Exception) {
    console.log('Error message:', error.message);
    console.log('Error starts at character:', error.start);
    console.log('Error ends at character:', error.end);
    
    // Can be used for syntax highlighting in editors
    const errorRange = { start: error.start, end: error.end };
  }
}

Common Error Scenarios

Syntax Errors
try {
  parse('enum Status { ACTIVE "Active"'); // Missing closing brace
} catch (error) {
  console.log(error.message); // "Unexpected end of input expecting }"
}
Invalid Token Types
try {
  parse('model user { id String }'); // Invalid - should be capitalized
} catch (error) {
  console.log(error.message); // "Expected CapitalIdentifier but got something else"
}
Unknown References
try {
  parse('model User { name String @field.input(UnknownProp) }');
} catch (error) {
  console.log(error.message); // "Unknown reference UnknownProp"
}
Duplicate Declarations
try {
  parse(`
    enum Status { ACTIVE "Active" }
    enum Status { INACTIVE "Inactive" }
  `);
} catch (error) {
  console.log(error.message); // "Duplicate Status"
}

Integration with AST

All AST classes throw Exception when parsing fails:

import { SchemaTree, EnumTree, ModelTree, Exception } from '@stackpress/idea-parser';

// Any parsing operation can throw Exception
try {
  const schema = SchemaTree.parse(schemaCode);
  const enumAST = EnumTree.parse(enumCode);
  const modelAST = ModelTree.parse(modelCode);
} catch (error) {
  if (error instanceof Exception) {
    // Handle parser-specific errors
    console.error('Parsing failed:', error.message);
  } else {
    // Handle other types of errors
    console.error('Unexpected error:', error);
  }
}

Error Recovery

While Exception indicates parsing failure, you can implement error recovery strategies:

import { parse, Exception } from '@stackpress/idea-parser';

function parseWithFallback(code: string, fallbackCode?: string) {
  try {
    return parse(code);
  } catch (error) {
    if (error instanceof Exception && fallbackCode) {
      console.warn('Primary parsing failed, trying fallback:', error.message);
      return parse(fallbackCode);
    }
    throw error; // Re-throw if no fallback or different error type
  }
}

Language Server Integration

Exception's position information makes it ideal for language server implementations:

import { parse, Exception } from '@stackpress/idea-parser';

function validateSchema(code: string) {
  try {
    parse(code);
    return { valid: true, errors: [] };
  } catch (error) {
    if (error instanceof Exception) {
      return {
        valid: false,
        errors: [{
          message: error.message,
          range: {
            start: error.start,
            end: error.end
          },
          severity: 'error'
        }]
      };
    }
    throw error;
  }
}

Inherited Features

Since Exception extends the base Exception class from @stackpress/lib, it inherits all the enhanced error handling features:

  • Template-based error messages
  • Enhanced stack trace parsing
  • Position information support
  • HTTP status code integration
  • Validation error aggregation

For more details on the base Exception functionality, refer to the @stackpress/lib Exception documentation.

Best Practices

Always Check Error Type

try {
  parse(schemaCode);
} catch (error) {
  if (error instanceof Exception) {
    // Handle parser errors specifically
    handleParserError(error);
  } else {
    // Handle other errors (network, file system, etc.)
    handleGenericError(error);
  }
}

Use Position Information

function highlightError(code: string, error: Exception) {
  const lines = code.split('\n');
  let currentPos = 0;
  
  for (let i = 0; i < lines.length; i++) {
    const lineEnd = currentPos + lines[i].length;
    
    if (error.start >= currentPos && error.start <= lineEnd) {
      const lineStart = error.start - currentPos;
      const lineEnd = Math.min(error.end - currentPos, lines[i].length);
      
      console.log(`Line ${i + 1}: ${lines[i]}`);
      console.log(' '.repeat(lineStart + 8) + '^'.repeat(lineEnd - lineStart));
      break;
    }
    
    currentPos = lineEnd + 1; // +1 for newline
  }
}

Provide Helpful Error Messages

function parseWithContext(code: string, filename?: string) {
  try {
    return parse(code);
  } catch (error) {
    if (error instanceof Exception) {
      const context = filename ? ` in ${filename}` : '';
      throw new Exception(
        `Parse error${context}: ${error.message}`,
        error.code
      ).withPosition(error.start, error.end);
    }
    throw error;
  }
}

This repository is a yarn workspace made of 3 packages, in the packages folder, and already published to NPM.

  • idea-parser
  • idea-transformer
  • idea

In general, with the exception of the root package.json, docs and .github, you should not edit files outside the packages folder without explicit permission.

The tasks asked by the user will always be regarding a specific package. You should not be editing files in multiple packages in the same task without explicit permission.

The following folders and files are found in the root of the project.

  • .context - This is documentation to be submitted to Context7 MCP. It's basically the same as the docs folder, but fixes the language in the code snippets.
  • docs - The official documentation of the project.
  • example - A working project example using idea to generate code
  • language - The source code for the language server already published to the VSCode marketplace. (currently using the typescript version of idea-parser)
  • packages - Published NPM packages; The source code for the relative work involved.

Please ignore the following folders:

  • .nyc_output
  • archives
  • coverage

In this project we use yarn to execute scripts. Please do not use npm. We use package.json to call scripts from packages like so.

  • yarn parser [command] - shortbut for yarn --cwd packages/idea-parser
  • yarn transformer [command] - shortbut for yarn --cwd packages/idea-transformer
  • yarn idea [command] - shortbut for yarn --cwd packages/idea
  • yarn build - calls all the build commands in packages/idea-parser, packages/idea-transformer, packages/idea and example
  • yarn test - runs all the test commands in packages/idea-parser and packages/idea-transformer
  • yarn build - runs all the build commands in packages/idea-parser, packages/idea-transformer, packages/idea and example

When creating documentation please use the root docs folder. Do not add documentation in the packages folder.

In packages/idea-parser, there is a folder called old. This is the original working parser written in typescript. It exists only temporarily as a reference in case context for the rust version of the compiler is needed. Please do not update any files in packages/idea-parser/old.

Idea Transformer

Comprehensive API documentation for the @stackpress/idea-transformer library - a powerful schema transformation tool that processes .idea schema files and executes plugins to generate code and other outputs.

Overview

The idea-transformer library provides a complete solution for:

  • Schema Processing: Load and parse .idea schema files with support for imports and merging
  • Plugin System: Execute transformation plugins that generate code, documentation, or other outputs
  • CLI Integration: Command-line interface for processing schemas in build pipelines
  • Type Safety: Full TypeScript support with comprehensive type definitions

Quick Start

npm install @stackpress/idea-transformer
import Transformer from '@stackpress/idea-transformer';

// Load and process a schema
const transformer = await Transformer.load('./schema.idea');
const schema = await transformer.schema();
await transformer.transform();

API Reference

Core Components

Component Description
Transformer Main class for loading and transforming schema files
Terminal Command-line interface for schema processing
Plugin Types Type definitions for creating plugins

Key Features

Schema Loading and Processing

  • Support for both .idea and .json schema files
  • Automatic dependency resolution with use directives
  • Intelligent schema merging based on mutability rules
  • Comprehensive error handling and validation

Plugin System

  • Type-safe plugin development with PluginProps and PluginWithCLIProps
  • Access to complete schema configuration and transformation context
  • CLI integration for interactive plugins
  • Flexible plugin configuration system

Command-Line Interface

  • Simple CLI for processing schemas in build pipelines
  • Configurable working directories and file extensions
  • Integration with npm scripts and build tools
  • Support for batch processing multiple schemas

Architecture

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Schema File   │───▶│   Transformer   │───▶│    Plugins      │
│   (.idea/.json) │    │                 │    │                 │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                              │                        │
                              ▼                        ▼
                       ┌─────────────────┐    ┌─────────────────┐
                       │  Schema Config  │    │ Generated Files │
                       │                 │    │                 │
                       └─────────────────┘    └─────────────────┘

Usage Patterns

Basic Schema Transformation

import Transformer from '@stackpress/idea-transformer';

// Load schema and execute plugins
const transformer = await Transformer.load('./schema.idea');
await transformer.transform();

CLI Usage

# Process schema file
node cli.js transform --input ./schema.idea

# Using short flag
node cli.js transform --i ./schema.idea

Plugin Development

import type { PluginProps } from '@stackpress/idea-transformer/types';

export default async function myPlugin(props: PluginProps<{}>) {
  const { config, schema, transformer, cwd } = props;
  
  // Process schema and generate output
  const content = generateFromSchema(schema);
  const outputPath = await transformer.loader.absolute(config.output);
  await writeFile(outputPath, content);
}

Common Use Cases

Code Generation

  • Generate TypeScript interfaces from schema models
  • Create enum definitions from schema enums
  • Build API client libraries from schema definitions
  • Generate database migration files

Documentation

  • Create API documentation from schema
  • Generate schema reference guides
  • Build interactive schema explorers
  • Create validation rule documentation

Validation

  • Generate validation schemas (Joi, Yup, Zod)
  • Create form validation rules
  • Build API request/response validators
  • Generate test fixtures and mock data

Build Integration

  • Integrate with webpack, rollup, or other bundlers
  • Add to npm scripts for automated generation
  • Use in CI/CD pipelines for consistent builds
  • Create watch mode for development workflows

Examples

TypeScript Interface Generation

// schema.idea
model User {
  id String @id
  name String @required
  email String @required @unique
  role UserRole
  profile Profile?
}

enum UserRole {
  ADMIN "admin"
  USER "user"
}

type Profile {
  bio String
  avatar String?
}

plugin "./generate-types.js" {
  output "./generated/types.ts"
}
// generate-types.js
export default async function generateTypes({ schema, config, transformer }) {
  let content = '';
  
  // Generate enums
  if (schema.enum) {
    for (const [name, enumDef] of Object.entries(schema.enum)) {
      content += `export enum ${name} {\n`;
      for (const [key, value] of Object.entries(enumDef)) {
        content += `  ${key} = "${value}",\n`;
      }
      content += '}\n\n';
    }
  }
  
  // Generate interfaces
  if (schema.model) {
    for (const [name, model] of Object.entries(schema.model)) {
      content += `export interface ${name} {\n`;
      for (const column of model.columns) {
        const optional = column.required ? '' : '?';
        content += `  ${column.name}${optional}: ${mapType(column.type)};\n`;
      }
      content += '}\n\n';
    }
  }
  
  const outputPath = await transformer.loader.absolute(config.output);
  await writeFile(outputPath, content);
}

CLI Integration

{
  "scripts": {
    "generate": "idea transform --input ./schema.idea",
    "build": "npm run generate && tsc",
    "dev": "npm run generate && npm run build -- --watch"
  }
}

Error Handling

The library provides comprehensive error handling with detailed error messages:

import { Exception } from '@stackpress/idea-parser';

try {
  const transformer = await Transformer.load('./schema.idea');
  await transformer.transform();
} catch (error) {
  if (error instanceof Exception) {
    console.error('Schema error:', error.message);
    console.error('Error code:', error.code);
  } else {
    console.error('Unexpected error:', error);
  }
}

Best Practices

Schema Organization

  • Use use directives to split large schemas into manageable files
  • Organize shared types and enums in separate files
  • Follow consistent naming conventions across schemas
  • Document complex relationships and business rules

Plugin Development

  • Always validate plugin configuration
  • Use TypeScript for type safety
  • Handle errors gracefully with meaningful messages
  • Use the transformer's file loader for consistent path resolution

Build Integration

  • Add schema generation to your build process
  • Use watch mode during development
  • Version control generated files when appropriate
  • Document the generation process for team members

Transformer

A class for loading, processing, and transforming schema files with plugin support and schema merging capabilities.

import Transformer from '@stackpress/idea-transformer';

const transformer = await Transformer.load('./schema.idea');
const schema = await transformer.schema();
await transformer.transform();

Overview

The Transformer class is the core component of the idea-transformer library. It handles:

  • Loading schema files (both .idea and .json formats)
  • Processing and merging schema configurations from multiple files
  • Executing plugins defined in the schema
  • Managing file dependencies and imports

Static Methods

The following methods can be accessed directly from the Transformer class.

Loading a Transformer

The following example shows how to create a new Transformer instance.

import Transformer from '@stackpress/idea-transformer';

// Load with default options
const transformer = await Transformer.load('./schema.idea');

// Load with custom options
const transformer = await Transformer.load('./schema.idea', {
  cwd: '/custom/working/directory',
  fs: customFileSystem
});

Parameters

Parameter Type Description
input string Path to the schema file to load
options FileLoaderOptions Optional configuration for file loading

Returns

A promise that resolves to a new Transformer instance configured with the specified input file and options.

Properties

The following properties are available when instantiating a Transformer.

Property Type Description
loader FileLoader File system loader for handling file operations
input string Absolute path to the input schema file

Methods

The following methods are available when instantiating a Transformer.

Loading Schema Configuration

The following example shows how to load and process the schema configuration.

const transformer = await Transformer.load('./schema.idea');
const schema = await transformer.schema();

console.log(schema.model); // Access model definitions
console.log(schema.enum); // Access enum definitions
console.log(schema.type); // Access type definitions
console.log(schema.prop); // Access prop definitions
console.log(schema.plugin); // Access plugin configurations

Returns

A promise that resolves to a SchemaConfig object containing all processed schema definitions.

Features

  • File Format Support: Automatically detects and handles both .idea and .json schema files
  • Dependency Resolution: Processes use directives to import and merge external schema files
  • Schema Merging: Intelligently merges child schemas into parent schemas based on mutability rules
  • Caching: Caches the processed schema to avoid redundant processing

Schema Merging Rules

When processing use directives, the transformer applies these merging rules:

  1. Props and Enums: Simple merge where parent takes precedence
  2. Types and Models:
    • If parent doesn't exist or is immutable: child is added
    • If parent is mutable: attributes and columns are merged
    • Child columns are prepended to parent columns
    • Parent attributes take precedence over child attributes

Transforming with Plugins

The following example shows how to execute all plugins defined in the schema.

const transformer = await Transformer.load('./schema.idea');

// Transform with no additional context
await transformer.transform();

// Transform with additional context
await transformer.transform({
  outputDir: './generated',
  debug: true
});

Parameters

Parameter Type Description
extras T Optional additional context to pass to plugins

Returns

A promise that resolves when all plugins have been executed.

Plugin Execution Process

  1. Validation: Ensures plugins are defined in the schema
  2. Module Resolution: Resolves plugin file paths relative to the schema file
  3. Dynamic Import: Loads plugin modules dynamically
  4. Context Injection: Passes context including transformer, schema, config, and extras
  5. Execution: Calls each plugin function with the injected context

Plugin Context

Each plugin receives a context object with the following properties:

{
  transformer: Transformer,  // The transformer instance
  config: PluginConfig,      // Plugin-specific configuration
  schema: SchemaConfig,      // Complete processed schema
  cwd: string,              // Current working directory
  ...extras                 // Any additional context passed to transform()
}

Usage Examples

Basic Schema Loading

import Transformer from '@stackpress/idea-transformer';

const transformer = await Transformer.load('./schema.idea');
const schema = await transformer.schema();

// Access different parts of the schema
console.log('Models:', Object.keys(schema.model || {}));
console.log('Enums:', Object.keys(schema.enum || {}));
console.log('Types:', Object.keys(schema.type || {}));

Working with Multiple Schema Files

// main.idea
/*
use "./shared/types.idea"
use "./shared/enums.idea"

model User {
  id String @id
  profile Profile  // From shared/types.idea
  role UserRole    // From shared/enums.idea
}
*/

const transformer = await Transformer.load('./main.idea');
const schema = await transformer.schema();

// The schema now includes definitions from all imported files
console.log(schema.type?.Profile);  // Available from shared/types.idea
console.log(schema.enum?.UserRole); // Available from shared/enums.idea

Plugin Development and Execution

// schema.idea
/*
plugin "./plugins/generate-types.js" {
  output "./generated/types.ts"
  format "typescript"
}
*/

// plugins/generate-types.js
export default function generateTypes({ transformer, config, schema, cwd }) {
  const outputPath = config.output;
  const format = config.format;
  
  // Generate TypeScript types based on schema
  let content = '';
  
  if (schema.model) {
    for (const [name, model] of Object.entries(schema.model)) {
      content += `export interface ${name} {\n`;
      for (const column of model.columns) {
        const optional = column.required ? '' : '?';
        content += `  ${column.name}${optional}: ${column.type};\n`;
      }
      content += '}\n\n';
    }
  }
  
  // Write generated content to file
  await writeFile(path.resolve(cwd, outputPath), content);
}

// Execute the transformation
const transformer = await Transformer.load('./schema.idea');
await transformer.transform({
  timestamp: new Date().toISOString()
});

Error Handling

import { Exception } from '@stackpress/idea-parser';

try {
  const transformer = await Transformer.load('./schema.idea');
  const schema = await transformer.schema();
  await transformer.transform();
} catch (error) {
  if (error instanceof Exception) {
    console.error('Schema processing error:', error.message);
    console.error('Error code:', error.code);
  } else {
    console.error('Unexpected error:', error);
  }
}

Custom File System

import { NodeFS } from '@stackpress/lib';

// Using custom file system
const customFS = new NodeFS();
const transformer = await Transformer.load('./schema.idea', {
  fs: customFS,
  cwd: '/custom/working/directory'
});

Error Scenarios

File Not Found

// Throws: "Input file /path/to/nonexistent.idea does not exist"
const transformer = await Transformer.load('./nonexistent.idea');
await transformer.schema(); // Error thrown here

No Plugins Defined

// If schema has no plugins defined
const transformer = await Transformer.load('./schema-without-plugins.idea');
await transformer.transform(); // Throws: "No plugins defined in schema file"

Invalid Plugin Module

// If plugin file doesn't export a function
const transformer = await Transformer.load('./schema.idea');
await transformer.transform(); // Plugin is silently skipped if not a function

Best Practices

Schema Organization

// Organize schemas hierarchically
// shared/base.idea - Common types and enums
// modules/user.idea - User-specific models
// main.idea - Main schema that imports others

use "./shared/base.idea"
use "./modules/user.idea"

// Additional models specific to this schema
model Application {
  id String @id
  users User[]
}

Plugin Development

// Always validate plugin configuration
export default async function myPlugin({ config, schema, transformer, cwd }) {
  // Validate required configuration
  if (!config.output) {
    throw new Error('Plugin requires output configuration');
  }
  
  // Use transformer's file loader for consistent path resolution
  const outputPath = await transformer.loader.absolute(config.output);
  
  // Process schema safely
  const models = schema.model || {};
  const enums = schema.enum || {};
  
  // Generate output...
}

Error Recovery

// Implement graceful error handling
async function processSchema(schemaPath) {
  try {
    const transformer = await Transformer.load(schemaPath);
    const schema = await transformer.schema();
    await transformer.transform();
    return { success: true, schema };
  } catch (error) {
    console.error(`Failed to process ${schemaPath}:`, error.message);
    return { success: false, error: error.message };
  }
}

Integration with Other Tools

Build Systems

// Integration with build tools
import Transformer from '@stackpress/idea-transformer';

export async function buildSchemas(inputDir, outputDir) {
  const schemaFiles = await glob(`${inputDir}/**/*.idea`);
  
  for (const schemaFile of schemaFiles) {
    const transformer = await Transformer.load(schemaFile);
    await transformer.transform({ outputDir });
  }
}

Testing

// Testing schema transformations
import { expect } from 'chai';

describe('Schema Transformation', () => {
  it('should process schema correctly', async () => {
    const transformer = await Transformer.load('./test-schema.idea');
    const schema = await transformer.schema();
    
    expect(schema.model).to.have.property('User');
    expect(schema.model.User.columns).to.have.length.greaterThan(0);
  });
});

Terminal

A command-line interface for processing schema files and executing transformations through terminal commands.

import Terminal from '@stackpress/idea-transformer/Terminal';

const terminal = await Terminal.load(['transform', '--input', './schema.idea']);
await terminal.run();

Overview

The Terminal class (exported as IdeaTerminal) extends the base Terminal class from @stackpress/lib to provide command-line functionality for the idea-transformer library. It handles:

  • Command-line argument parsing
  • Schema file transformation via CLI commands
  • Integration with the Transformer class for processing
  • Configurable working directories and file extensions

Static Methods

The following methods can be accessed directly from the Terminal class.

Loading a Terminal Instance

The following example shows how to create a new Terminal instance from command-line arguments.

import Terminal from '@stackpress/idea-transformer/Terminal';

// Load with command-line arguments
const args = ['transform', '--input', './schema.idea'];
const terminal = await Terminal.load(args);

// Load with custom options
const terminal = await Terminal.load(args, {
  cwd: '/custom/working/directory',
  extname: '.schema',
  brand: '[MY-TOOL]'
});

Parameters

Parameter Type Description
args string[] Command-line arguments array
options TerminalOptions Optional configuration for terminal behavior

Returns

A promise that resolves to a new Terminal instance configured with the specified arguments and options.

Properties

The following properties are available when instantiating a Terminal.

Property Type Description
cwd string Current working directory for file operations
extname string Default file extension for schema files (default: '.idea')

Methods

The following methods are available when instantiating a Terminal.

Running Terminal Commands

The Terminal automatically sets up event handlers for processing commands. The main command supported is transform.

const terminal = await Terminal.load(['transform', '--input', './schema.idea']);
await terminal.run();

Command Structure

The terminal expects commands in the following format:

transform --input <schema-file> [--i <schema-file>]

Flags

Flag Alias Description
--input --i Path to the schema file to process

Usage Examples

Basic Command Execution

import Terminal from '@stackpress/idea-transformer/Terminal';

// Process a schema file
const args = ['transform', '--input', './schema.idea'];
const terminal = await Terminal.load(args);
await terminal.run();

Using Short Flag Syntax

// Using the short flag alias
const args = ['transform', '--i', './schema.idea'];
const terminal = await Terminal.load(args);
await terminal.run();

Custom Working Directory

// Set custom working directory
const terminal = await Terminal.load(['transform', '--i', './schema.idea'], {
  cwd: '/path/to/project'
});
await terminal.run();

Custom File Extension

// Use custom file extension
const terminal = await Terminal.load(['transform', '--i', './schema.custom'], {
  extname: '.custom'
});
await terminal.run();

Custom Brand/Label

// Use custom terminal brand
const terminal = await Terminal.load(['transform', '--i', './schema.idea'], {
  brand: '[MY-SCHEMA-TOOL]'
});
await terminal.run();

Command-Line Integration

Direct Command-Line Usage

## Basic usage
node cli.js transform --input ./schema.idea

## Using short flag
node cli.js transform --i ./schema.idea

## With custom working directory
cd /path/to/project && node cli.js transform --i ./schema.idea

CLI Script Example

#!/usr/bin/env node
import Terminal from '@stackpress/idea-transformer/Terminal';

async function main() {
  try {
    const args = process.argv.slice(2);
    const terminal = await Terminal.load(args, {
      cwd: process.cwd(),
      brand: '[SCHEMA-CLI]'
    });
    await terminal.run();
  } catch (error) {
    console.error('CLI Error:', error.message);
    process.exit(1);
  }
}

main();

Package.json Integration

{
  "name": "my-schema-tool",
  "bin": {
    "schema": "./cli.js"
  },
  "scripts": {
    "build": "schema transform --i ./schema.idea",
    "dev": "schema transform --i ./dev-schema.idea"
  }
}

Default Behavior

File Path Resolution

When no input file is specified, the terminal uses a default path:

// Default file path construction
const defaultPath = `${terminal.cwd}/schema${terminal.extname}`;
// Example: "/current/directory/schema.idea"

Flag Processing

The terminal processes the following flags in order of preference:

  1. --input (full flag name)
  2. --i (short alias)
  3. Default file path if no flags provided
// These are equivalent:
['transform', '--input', './schema.idea']
['transform', '--i', './schema.idea']

// Uses default path: ./schema.idea
['transform']

Error Handling

Missing Schema File

try {
  const terminal = await Terminal.load(['transform', '--i', './nonexistent.idea']);
  await terminal.run();
} catch (error) {
  console.error('File not found:', error.message);
}

Invalid Command

try {
  const terminal = await Terminal.load(['invalid-command']);
  await terminal.run();
} catch (error) {
  console.error('Unknown command:', error.message);
}

Plugin Errors

// If plugins fail during transformation
try {
  const terminal = await Terminal.load(['transform', '--i', './schema.idea']);
  await terminal.run();
} catch (error) {
  console.error('Transformation failed:', error.message);
}

Advanced Usage

Custom Event Handlers

import Terminal from '@stackpress/idea-transformer/Terminal';

const terminal = await Terminal.load(['transform', '--i', './schema.idea']);

// Add custom event handler
terminal.on('custom-command', async (event) => {
  console.log('Custom command executed');
  // Custom logic here
});

await terminal.run();

Programmatic CLI Building

// Build CLI arguments programmatically
function buildCLIArgs(schemaFile: string, options: any = {}) {
  const args = ['transform'];
  
  if (schemaFile) {
    args.push('--input', schemaFile);
  }
  
  return args;
}

const args = buildCLIArgs('./my-schema.idea');
const terminal = await Terminal.load(args);
await terminal.run();

Batch Processing

import { glob } from 'glob';

async function processAllSchemas(pattern: string) {
  const schemaFiles = await glob(pattern);
  
  for (const schemaFile of schemaFiles) {
    console.log(`Processing ${schemaFile}...`);
    
    const terminal = await Terminal.load(['transform', '--i', schemaFile]);
    await terminal.run();
    
    console.log(`Completed ${schemaFile}`);
  }
}

// Process all .idea files in a directory
await processAllSchemas('./schemas/**/*.idea');

Environment-Based Configuration

const terminal = await Terminal.load(['transform', '--i', './schema.idea'], {
  cwd: process.env.SCHEMA_CWD || process.cwd(),
  extname: process.env.SCHEMA_EXT || '.idea',
  brand: process.env.CLI_BRAND || '[IDEA]'
});

await terminal.run();

Integration with Build Tools

Webpack Plugin

class SchemaTransformPlugin {
  constructor(options = {}) {
    this.options = options;
  }
  
  apply(compiler) {
    compiler.hooks.beforeCompile.tapAsync('SchemaTransformPlugin', async (params, callback) => {
      try {
        const terminal = await Terminal.load(['transform', '--i', this.options.schemaFile]);
        await terminal.run();
        callback();
      } catch (error) {
        callback(error);
      }
    });
  }
}

Gulp Task

import gulp from 'gulp';
import Terminal from '@stackpress/idea-transformer/Terminal';

gulp.task('transform-schema', async () => {
  const terminal = await Terminal.load(['transform', '--i', './schema.idea']);
  await terminal.run();
});

NPM Scripts

{
  "scripts": {
    "schema:build": "node -e \"import('./cli.js').then(m => m.default(['transform', '--i', './schema.idea']))\"",
    "schema:dev": "node -e \"import('./cli.js').then(m => m.default(['transform', '--i', './dev-schema.idea']))\"",
    "schema:watch": "nodemon --watch schema.idea --exec \"npm run schema:build\""
  }
}

Testing

Unit Testing

import { expect } from 'chai';
import Terminal from '@stackpress/idea-transformer/Terminal';

describe('Terminal Tests', () => {
  it('should process schema file', async () => {
    const terminal = await Terminal.load(['transform', '--i', './test-schema.idea'], {
      cwd: './test-fixtures'
    });
    
    expect(terminal.cwd).to.include('test-fixtures');
    
    // Run the terminal command
    await terminal.run();
    
    // Verify output files were created
    // ... assertions here
  });
  
  it('should use default options', async () => {
    const terminal = await Terminal.load(['transform']);
    
    expect(terminal.extname).to.equal('.idea');
    expect(terminal.cwd).to.be.a('string');
  });
});

Integration Testing

import fs from 'fs';
import path from 'path';

describe('Terminal Integration', () => {
  it('should generate expected output files', async () => {
    const outputDir = './test-output';
    const schemaFile = './test-schema.idea';
    
    // Clean output directory
    if (fs.existsSync(outputDir)) {
      fs.rmSync(outputDir, { recursive: true });
    }
    
    // Run transformation
    const terminal = await Terminal.load(['transform', '--i', schemaFile]);
    await terminal.run();
    
    // Verify expected files were created
    const expectedFiles = ['types.ts', 'enums.ts', 'models.ts'];
    for (const file of expectedFiles) {
      const filePath = path.join(outputDir, file);
      expect(fs.existsSync(filePath)).to.be.true;
    }
  });
});

Best Practices

Error Handling

// Always wrap terminal execution in try-catch
async function safeTransform(schemaFile: string) {
  try {
    const terminal = await Terminal.load(['transform', '--i', schemaFile]);
    await terminal.run();
    console.log(`✅ Successfully processed ${schemaFile}`);
  } catch (error) {
    console.error(`❌ Failed to process ${schemaFile}:`, error.message);
    throw error;
  }
}

Configuration Management

// Use configuration objects for reusable settings
const defaultConfig = {
  cwd: process.cwd(),
  extname: '.idea',
  brand: '[SCHEMA-TOOL]'
};

async function createTerminal(args: string[], config = defaultConfig) {
  return await Terminal.load(args, config);
}

Logging and Debugging

// Add logging for better debugging
const terminal = await Terminal.load(['transform', '--i', './schema.idea'], {
  cwd: process.cwd()
});

console.log(`Working directory: ${terminal.cwd}`);
console.log(`File extension: ${terminal.extname}`);

await terminal.run();

Plugin Types

Type definitions for creating plugins that work with the idea-transformer library, providing type safety and structure for plugin development.

import type { PluginProps, PluginWithCLIProps } from '@stackpress/idea-transformer/types';

// Basic plugin
export default function myPlugin(props: PluginProps<{}>) {
  // Plugin implementation
}

// CLI-aware plugin
export default function cliPlugin(props: PluginWithCLIProps) {
  // Plugin with CLI access
}

Overview

The plugin type system in idea-transformer provides type-safe interfaces for developing plugins that can process schema configurations and generate output files. These types ensure that plugins receive the correct context and maintain consistency across the plugin ecosystem.

Core Plugin Types

PluginProps

A generic type that defines the base structure for all plugin functions, providing access to essential transformation context.

type PluginProps<T extends {}> = T & {
  config: PluginConfig,
  schema: SchemaConfig,
  transformer: Transformer<{}>,
  cwd: string
};

Type Parameters

Parameter Type Description
T extends {} Additional properties to extend the base plugin props

Properties

Property Type Description
config PluginConfig Plugin-specific configuration from the schema
schema SchemaConfig Complete processed schema configuration
transformer Transformer<{}> The transformer instance executing the plugin
cwd string Current working directory for file operations

Usage

Use PluginProps when creating plugins that need access to the basic transformation context:

import type { PluginProps } from '@stackpress/idea-transformer/types';

export default async function generateTypes(props: PluginProps<{}>) {
  const { config, schema, transformer, cwd } = props;
  
  // Access plugin configuration
  const outputPath = config.output || './generated/types.ts';
  
  // Process schema models
  if (schema.model) {
    for (const [name, model] of Object.entries(schema.model)) {
      // Generate TypeScript interfaces
      console.log(`Generating interface for ${name}`);
    }
  }
  
  // Use transformer's file loader for path resolution
  const absolutePath = await transformer.loader.absolute(outputPath);
  
  // Write generated content
  // ... implementation
}

PluginWithCLIProps

A specialized type that extends PluginProps with CLI-specific properties, enabling plugins to interact with the command-line interface.

type PluginWithCLIProps = PluginProps<CLIProps>;

// Where CLIProps is:
type CLIProps = { cli: Terminal };

Properties

Inherits all properties from PluginProps<CLIProps> plus:

Property Type Description
cli Terminal Terminal instance for CLI interactions

Usage

Use PluginWithCLIProps when creating plugins that need to interact with the command-line interface:

import type { PluginWithCLIProps } from '@stackpress/idea-transformer/types';

export default async function interactivePlugin(props: PluginWithCLIProps) {
  const { config, schema, transformer, cwd, cli } = props;
  
  // Access CLI functionality
  console.log(`Working directory: ${cli.cwd}`);
  console.log(`File extension: ${cli.extname}`);
  
  // Use CLI for user interaction or logging
  // ... implementation
}

Plugin Development Guide

Basic Plugin Structure

import type { PluginProps } from '@stackpress/idea-transformer/types';
import fs from 'fs/promises';
import path from 'path';

export default async function myPlugin(props: PluginProps<{}>) {
  const { config, schema, transformer, cwd } = props;
  
  // 1. Validate configuration
  if (!config.output) {
    throw new Error('Plugin requires "output" configuration');
  }
  
  // 2. Process schema
  const content = processSchema(schema);
  
  // 3. Resolve output path
  const outputPath = await transformer.loader.absolute(config.output);
  
  // 4. Write output
  await fs.writeFile(outputPath, content, 'utf8');
}

function processSchema(schema: SchemaConfig): string {
  // Implementation for processing schema
  return '// Generated content';
}

CLI-Aware Plugin Structure

import type { PluginWithCLIProps } from '@stackpress/idea-transformer/types';
import fs from 'fs/promises';

export default async function cliPlugin(props: PluginWithCLIProps) {
  const { config, schema, transformer, cwd, cli } = props;
  
  // Access CLI properties
  const workingDir = cli.cwd;
  const fileExtension = cli.extname;
  
  // Use CLI for logging or user interaction
  console.log(`Processing schema in: ${workingDir}`);
  
  // Process based on CLI context
  if (config.interactive) {
    // Interactive mode logic
    console.log('Running in interactive mode...');
  }
  
  // Generate output
  const content = generateContent(schema, { workingDir, fileExtension });
  
  // Write to file
  const outputPath = path.resolve(workingDir, config.output);
  await fs.writeFile(outputPath, content, 'utf8');
  
  console.log(`Generated: ${outputPath}`);
}

Custom Plugin Props

You can extend the base plugin props with custom properties:

import type { PluginProps } from '@stackpress/idea-transformer/types';

// Define custom props
interface CustomProps {
  timestamp: string;
  version: string;
  debug: boolean;
}

// Use custom props in plugin
export default async function customPlugin(props: PluginProps<CustomProps>) {
  const { config, schema, transformer, cwd, timestamp, version, debug } = props;
  
  if (debug) {
    console.log(`Plugin executed at ${timestamp} for version ${version}`);
  }
  
  // Plugin implementation
}

// Usage with transformer
const transformer = await Transformer.load('./schema.idea');
await transformer.transform({
  timestamp: new Date().toISOString(),
  version: '1.0.0',
  debug: true
});

Plugin Examples

TypeScript Interface Generator

import type { PluginProps } from '@stackpress/idea-transformer/types';
import fs from 'fs/promises';
import path from 'path';

interface TypeGenConfig {
  output: string;
  namespace?: string;
  exportType?: 'named' | 'default';
}

export default async function generateInterfaces(
  props: PluginProps<{ config: TypeGenConfig }>
) {
  const { config, schema, transformer, cwd } = props;
  
  let content = '';
  
  // Add namespace if specified
  if (config.namespace) {
    content += `export namespace ${config.namespace} {\n`;
  }
  
  // Generate interfaces from models
  if (schema.model) {
    for (const [name, model] of Object.entries(schema.model)) {
      content += generateInterface(name, model);
    }
  }
  
  // Generate types from type definitions
  if (schema.type) {
    for (const [name, type] of Object.entries(schema.type)) {
      content += generateType(name, type);
    }
  }
  
  // Close namespace
  if (config.namespace) {
    content += '}\n';
  }
  
  // Write to output file
  const outputPath = await transformer.loader.absolute(config.output);
  await fs.writeFile(outputPath, content, 'utf8');
}

function generateInterface(name: string, model: any): string {
  let content = `export interface ${name} {\n`;
  
  for (const column of model.columns || []) {
    const optional = column.required ? '' : '?';
    const type = mapToTypeScript(column.type);
    content += `  ${column.name}${optional}: ${type};\n`;
  }
  
  content += '}\n\n';
  return content;
}

function generateType(name: string, type: any): string {
  // Implementation for generating TypeScript types
  return `export type ${name} = any; // TODO: Implement\n\n`;
}

function mapToTypeScript(schemaType: string): string {
  const typeMap: Record<string, string> = {
    'String': 'string',
    'Number': 'number',
    'Boolean': 'boolean',
    'Date': 'Date',
    'JSON': 'any'
  };
  
  return typeMap[schemaType] || 'any';
}

Enum Generator

import type { PluginProps } from '@stackpress/idea-transformer/types';
import fs from 'fs/promises';

export default async function generateEnums(props: PluginProps<{}>) {
  const { config, schema, transformer } = props;
  
  if (!schema.enum) {
    console.log('No enums found in schema');
    return;
  }
  
  let content = '// Generated Enums\n\n';
  
  for (const [name, enumDef] of Object.entries(schema.enum)) {
    content += `export enum ${name} {\n`;
    
    for (const [key, value] of Object.entries(enumDef)) {
      content += `  ${key} = "${value}",\n`;
    }
    
    content += '}\n\n';
  }
  
  const outputPath = await transformer.loader.absolute(config.output);
  await fs.writeFile(outputPath, content, 'utf8');
  
  console.log(`Generated enums: ${outputPath}`);
}

CLI-Interactive Plugin

import type { PluginWithCLIProps } from '@stackpress/idea-transformer/types';
import fs from 'fs/promises';
import path from 'path';

export default async function interactiveGenerator(props: PluginWithCLIProps) {
  const { config, schema, transformer, cli } = props;
  
  // Use CLI for interactive prompts
  console.log(`\n🚀 Interactive Generator`);
  console.log(`Working Directory: ${cli.cwd}`);
  console.log(`Schema Extension: ${cli.extname}`);
  
  // Process based on available schema elements
  const hasModels = schema.model && Object.keys(schema.model).length > 0;
  const hasEnums = schema.enum && Object.keys(schema.enum).length > 0;
  const hasTypes = schema.type && Object.keys(schema.type).length > 0;
  
  console.log(`\n📊 Schema Summary:`);
  console.log(`  Models: ${hasModels ? Object.keys(schema.model!).length : 0}`);
  console.log(`  Enums: ${hasEnums ? Object.keys(schema.enum!).length : 0}`);
  console.log(`  Types: ${hasTypes ? Object.keys(schema.type!).length : 0}`);
  
  // Generate based on configuration
  const outputs: string[] = [];
  
  if (config.generateModels && hasModels) {
    const modelContent = generateModels(schema.model!);
    const modelPath = path.resolve(cli.cwd, 'generated/models.ts');
    await fs.mkdir(path.dirname(modelPath), { recursive: true });
    await fs.writeFile(modelPath, modelContent, 'utf8');
    outputs.push(modelPath);
  }
  
  if (config.generateEnums && hasEnums) {
    const enumContent = generateEnums(schema.enum!);
    const enumPath = path.resolve(cli.cwd, 'generated/enums.ts');
    await fs.mkdir(path.dirname(enumPath), { recursive: true });
    await fs.writeFile(enumPath, enumContent, 'utf8');
    outputs.push(enumPath);
  }
  
  // Report results
  console.log(`\n✅ Generated ${outputs.length} files:`);
  outputs.forEach(file => console.log(`  📄 ${file}`));
}

function generateModels(models: Record<string, any>): string {
  // Implementation for generating models
  return '// Generated models\n';
}

function generateEnums(enums: Record<string, any>): string {
  // Implementation for generating enums
  return '// Generated enums\n';
}

Plugin Configuration

Schema Plugin Definition

// schema.idea
plugin "./plugins/my-plugin.js" {
  output "./generated/output.ts"
  format "typescript"
  options {
    strict true
    comments true
  }
}

Plugin Configuration Access

export default async function myPlugin(props: PluginProps<{}>) {
  const { config } = props;
  
  // Access top-level config
  const output = config.output;
  const format = config.format;
  
  // Access nested options
  const options = config.options || {};
  const strict = options.strict || false;
  const comments = options.comments || false;
  
  // Use configuration in plugin logic
  if (strict) {
    // Enable strict mode
  }
  
  if (comments) {
    // Add comments to generated code
  }
}

Error Handling

Plugin Error Handling

import type { PluginProps } from '@stackpress/idea-transformer/types';

export default async function safePlugin(props: PluginProps<{}>) {
  const { config, schema, transformer } = props;
  
  try {
    // Validate required configuration
    if (!config.output) {
      throw new Error('Missing required "output" configuration');
    }
    
    // Validate schema has required elements
    if (!schema.model || Object.keys(schema.model).length === 0) {
      throw new Error('Schema must contain at least one model');
    }
    
    // Process schema
    const content = await processSchema(schema);
    
    // Write output
    const outputPath = await transformer.loader.absolute(config.output);
    await writeOutput(outputPath, content);
    
    console.log(`✅ Plugin completed successfully: ${outputPath}`);
    
  } catch (error) {
    console.error(`❌ Plugin failed:`, error.message);
    throw error; // Re-throw to stop transformation
  }
}

Graceful Error Recovery

export default async function resilientPlugin(props: PluginProps<{}>) {
  const { config, schema, transformer } = props;
  
  const warnings: string[] = [];
  
  try {
    // Attempt primary functionality
    await primaryGeneration(schema, config);
  } catch (error) {
    warnings.push(`Primary generation failed: ${error.message}`);
    
    // Fallback to basic generation
    try {
      await fallbackGeneration(schema, config);
      warnings.push('Used fallback generation');
    } catch (fallbackError) {
      throw new Error(`Both primary and fallback generation failed: ${fallbackError.message}`);
    }
  }
  
  // Report warnings
  if (warnings.length > 0) {
    console.warn('Plugin completed with warnings:');
    warnings.forEach(warning => console.warn(`  ⚠️  ${warning}`));
  }
}

Best Practices

Type Safety

// Always use proper typing for plugin props
import type { PluginProps, PluginWithCLIProps } from '@stackpress/idea-transformer/types';

// Define custom config types
interface MyPluginConfig {
  output: string;
  format: 'typescript' | 'javascript';
  strict?: boolean;
}

// Use typed props
export default async function typedPlugin(
  props: PluginProps<{ config: MyPluginConfig }>
) {
  const { config } = props;
  
  // TypeScript will enforce config structure
  const output: string = config.output; // ✅ Type-safe
  const format: 'typescript' | 'javascript' = config.format; // ✅ Type-safe
  const strict: boolean = config.strict ?? false; // ✅ Type-safe with default
}

Configuration Validation

function validateConfig(config: any): asserts config is MyPluginConfig {
  if (!config.output || typeof config.output !== 'string') {
    throw new Error('Plugin requires "output" configuration as string');
  }
  
  if (!config.format || !['typescript', 'javascript'].includes(config.format)) {
    throw new Error('Plugin requires "format" to be "typescript" or "javascript"');
  }
}

export default async function validatedPlugin(props: PluginProps<{}>) {
  validateConfig(props.config);
  
  // Now config is properly typed
  const { output, format } = props.config;
}

File Operations

// Use transformer's file loader for consistent path resolution
export default async function filePlugin(props: PluginProps<{}>) {
  const { config, transformer } = props;
  
  // ✅ Use transformer.loader for path resolution
  const outputPath = await transformer.loader.absolute(config.output);
  
  // ✅ Create directories if needed
  await fs.mkdir(path.dirname(outputPath), { recursive: true });
  
  // ✅ Write file with proper error handling
  try {
    await fs.writeFile(outputPath, content, 'utf8');
  } catch (error) {
    throw new Error(`Failed to write output file: ${error.message}`);
  }
}

CLI Integration

// Use CLI props when available
export default async function adaptivePlugin(props: PluginWithCLIProps) {
  const { cli, config } = props;
  
  // Adapt behavior based on CLI context
  const outputDir = config.outputDir || path.join(cli.cwd, 'generated');
  const verbose = config.verbose || false;
  
  if (verbose) {
    console.log(`Generating files in: ${outputDir}`);
    console.log(`Working directory: ${cli.cwd}`);
    console.log(`File extension: ${cli.extname}`);
  }
  
  // Use CLI working directory for relative paths
  const absoluteOutputDir = path.resolve(cli.cwd, outputDir);
  
  // Generate files...
}

Ingest is a server framework designed for serverless environments (but can be used on regular servers too).

  • Ingest is similar to expressjs
  • Ingest routing, request, response build on top of @stackpress/lib (in this knowledge base).
  • Ingest routing is event driven (see: Stackpress Lib Context)
  • Ingest can be deployed to serverless like lambda, vercel, netlify, etc. (see: examples folder)
  • Ingest is highly pluggable. (see: examples/with-plugin)
  • Ingest supports bundlers and build scripts
  • Ingest can be used with NodeJS HTTP, WhatWG serverless enviroments

Multi-Routing Interface

Ingest provides 4 routers in one.

  • server.action
    • traditional express like routing interface
    • ex. server.action.get('/route', () => {})
  • server.entry
    • route by providing the path to the action router (export default)
    • ex. server.entry.get('/route', '/path/to/route/action')
  • server.import
    • route by providing a function that returns an import
    • ex. server.import.get('/route', () => import('./routes/home.js'));
  • server.view
    • route by providing the path to a view
    • ex. app.view.get('/route', '/view/home');
    • see: examples/with-handlebars

Inferred Routing

Ingest can automatically determine which router to use.

import { server } from '@stackpress/ingest/http';

const app = server();

//same as app.action.all('/:home', (req, res, ctx) => {});
app.all('/:home', (req, res, ctx) => {}); 

//same as app.import.put('/**', () => import('./routes/home.js'));
app.put('/**', () => import('./routes/home.js')); 

//same as app.view.get('/', '/view/home');
app.get('/', '/view/home'); 

Since server.entry(string, string) and server.view(string, string) have a similar interface, entry is not supported.

Build Support

Ingest exposes routing information that can be used by bundlers to produce build files. Consider the following example.

import { server } from '@stackpress/ingest/http';

const app = server();
app.on('request', (req, res) => {});
app.action.all('/:home', (req, res, ctx) => {});
app.entry.post('/*', path.join(__dirname, 'routes/home'));
app.import.put('/**', () => import('./routes/home.js'));
app.view.get('/', '/view/home');

console.log('listeners', server.listeners);
console.log('expressions', server.expressions);
console.log('routes', server.routes);
console.log('entries', server.entries);
console.log('imports', server.imports);
console.log('views', server.views);

Running the above script will output log the following maps.

listeners {
  request: Set(1) { { item: [Function (anonymous)], priority: 0 } },
  '/^[A-Z]+ \\/([^/]+)\\/*$/g': Set(1) { { item: [Function (anonymous)], priority: 0 } },
  '/^POST \\/([^/]+)\\/*$/g': Set(1) { { item: [Function: EntryFileAction], priority: 0 } },
  '/^PUT \\/(.*)\\/*$/g': Set(1) { { item: [Function: ImportFileAction], priority: 0 } },
  'GET /': Set(1) { { item: [Function: TemplateFileAction], priority: 0 } }
}
expressions Map(3) {
  '/^[A-Z]+ \\/([^/]+)\\/*$/g' => { pattern: '/:home', regexp: /^[A-Z]+ \/([^/]+)\/*$/g },
  '/^POST \\/([^/]+)\\/*$/g' => { pattern: 'POST /*', regexp: /^POST \/([^/]+)\/*$/g },
  '/^PUT \\/(.*)\\/*$/g' => { pattern: 'PUT /**', regexp: /^PUT \/(.*)\/*$/g }
}
routes Map(4) {
  '/^[A-Z]+ \\/([^/]+)\\/*$/g' => { method: 'ALL', path: '/:home' },
  '/^POST \\/([^/]+)\\/*$/g' => { method: 'POST', path: '/*' },
  '/^PUT \\/(.*)\\/*$/g' => { method: 'PUT', path: '/**' },
  'GET /' => { method: 'GET', path: '/' }
}
entries Map(1) {
  '/^POST \\/([^/]+)\\/*$/g' => Set(1) {
    {
      entry: '/project/routes/home',
      priority: 0
    }
  }
}
imports Map(1) {
  '/^PUT \\/(.*)\\/*$/g' => Set(1) { { import: [Function (anonymous)], priority: 0 } }
}
views Map(1) { 'GET /' => Set(1) { { entry: '/view/home', priority: 0 } } }

Where as:

  • The router smartly determines whether to use regular expressions or literal keys
  • server.listeners
    • access to event to listeners map
    • an event name can have multiple listeners
  • server.expressions
    • A map of regular expressions (String) to patterns and regexp (object)
    • Used to determine which keys in server.listeners, server.routes, server.entries, server.imports, server.views are regular expressions
  • server.routes
    • A map of event names to route information
  • server.entries
    • A map of event names to route action file paths (string)
  • server.imports
    • A map of event names to lazy import function
  • server.views
    • A map of event names to route view file paths (string)
    • Template engines like handlebars integrate using server.view.render and server.view.engine
    • see: examples/with-handlebars/src/server.ts

Plugin System Documentation

Overview

The plugin system provides a simple and elegant way to extend your application's functionality through modular components. Plugins can be easily toggled on or off, making your application highly configurable and maintainable.

How It Works

The plugin system follows a three-step process:

  1. Create a plugin file that exports a default function
  2. Register the plugin in your package.json
  3. Bootstrap the plugins when starting your server

Creating a Plugin

A plugin is simply a TypeScript/JavaScript file that exports a default function. This function receives the server instance and can configure it as needed.

Plugin Structure

import type { HttpServer } from '@stackpress/ingest';
import type { Config } from './config';

export default function plugin(server: HttpServer<Config>) {
  // Configure the server
  server.config.set(config);
  
  // Add middleware
  server.use(pages).use(tests).use(user).use(hooks);
  
  // Register components
  server.register('project', { welcome: 'Hello, World!!' });
  
  // Add event listeners
  server.on('request', (req, res) => {
    console.log('Request:', req.url);
  });
}

What Plugins Can Do

  • Set Configuration: Modify server settings and options
  • Add Middleware: Register route handlers and middleware functions
  • Register Components: Add reusable components to the server
  • Handle Events: Listen to server events and respond accordingly

Registering Plugins

To make your plugin available to the application, add its path to the plugins array in your package.json:

{
  "name": "your-app",
  "version": "1.0.0",
  "plugins": [
    "./src/plugin",
    "./src/another-plugin",
    "some-npm-package/plugin"
  ],
  "scripts": {
    "dev": "ts-node src/scripts/serve.ts"
  }
}

Plugin Paths

Plugins can be located:

  • Locally: ./src/plugin.ts or ./plugins/my-plugin.js
  • In node_modules: some-package/plugin or @scope/package/plugin
  • Anywhere accessible: Any valid module path

Bootstrapping Plugins

The magic happens when you call server.bootstrap() in your server startup script:

import server from '../server';

async function main() {
  // Load all plugins from package.json
  await server.bootstrap();
  
  // Start the server with plugins loaded
  server.create().listen(3000, () => {
    console.log('Server is running on port 3000');
  });
}

main().catch(console.error);

Bootstrap Process

  1. Reads the plugins array from package.json
  2. Imports each plugin module
  3. Executes the plugin function with the server instance
  4. Configures the server with all plugin modifications

Managing Plugins

Enabling a Plugin

Add the plugin path to the plugins array:

{
  "plugins": [
    "./src/plugin",
    "./src/new-plugin"  // ← Add this line
  ]
}

Disabling a Plugin

Remove or comment out the plugin path:

{
  "plugins": [
    "./src/plugin"
    // "./src/disabled-plugin"  ← Comment out or remove
  ]
}

Plugin Order

Plugins are loaded in the order they appear in the array. If plugin order matters for your use case, arrange them accordingly.

Benefits

🔧 Modularity

Keep your application organized by separating concerns into focused plugins.

🎛️ Easy Configuration

Toggle features on/off by simply editing the plugins array.

📦 Reusability

Share plugins across projects or publish them as npm packages.

🚀 Clean Startup

The bootstrap process keeps your server startup code clean and simple.

🔄 Hot Swapping

Easily experiment with different plugin combinations during development.

Example Use Cases

  • Authentication Plugin: Handle user login/logout functionality
  • Logging Plugin: Add request/response logging
  • API Plugin: Add REST or GraphQL endpoints
  • Static Files Plugin: Serve static assets
  • Database Plugin: Configure database connections
  • Monitoring Plugin: Add health checks and metrics

Best Practices

  1. Keep plugins focused: Each plugin should have a single responsibility
  2. Use TypeScript: Leverage type safety for better development experience
  3. Document your plugins: Include clear documentation for custom plugins
  4. Test plugins independently: Write unit tests for plugin functionality
  5. Handle errors gracefully: Ensure plugins don't crash the entire application

This plugin system makes your application highly modular and configurable while keeping the complexity hidden behind a simple, elegant interface.

This repository is a yarn workspace made of 1 package ingest, which is already published to NPM as @stackpress/ingest.

The following folders and files are found in the root of the project.

  • docs - The official documentation of the project.
  • example - Various working example project templates
  • ingest - The source code for the relative work involved.

Please ignore the following folders:

  • .nyc_output
  • archives
  • coverage
  • old

In this project we use yarn to execute scripts. Please do not use npm. We use package.json to call scripts from packages like so.

  • yarn build - generates the esm and cjs versions of the ingest framework
  • yarn test - runs the tests found in ingest/tests

When creating documentation please use the root docs folder. Do not add documentation in the ingest folder.

📖 Stackpress Library

Comprehensive API documentation for the Stackpress Library - a shared library used across Stackpress projects that standardizes type definitions and provides common utilities for developers.

Installation

npm install @stackpress/lib

API Reference

Data Structures

Data structures in programming are specialized formats for organizing, processing, storing, and retrieving data within a computer's memory.

Key Features:

  • Type-safe nested object manipulation
  • Path-based data access with dot notation
  • Built-in parsers for query strings, form data, and arguments
  • Chainable API for fluent data operations

Events

Event driven designed to support event chain reactions.

Key Features:

  • Priority-based event listeners
  • Type-safe event maps with TypeScript generics
  • Before/after hooks for event execution
  • Task queue management for event handling

Exception

Enhanced error handling with expressive error reporting and stack trace support. Provides better error information than standard JavaScript errors.

Key Features:

  • Template-based error messages
  • Validation error aggregation
  • Enhanced stack trace parsing
  • HTTP status code integration

Queues

Priority-based queue implementations for managing items and tasks with FIFO ordering. Includes both generic item queues and specialized task queues.

Key Features:

  • Priority-based ordering (higher numbers execute first)
  • FIFO ordering within same priority levels
  • Task execution with before/after hooks
  • Chainable API for queue operations

Routing

Event-driven routing system with generic Request and Response wrappers that work across different platforms (HTTP, terminal, web sockets).

Key Features:

  • Cross-platform request/response handling
  • Parameter extraction from routes
  • Event-driven architecture
  • Generic type support for different mediums

File System

Cross-platform file loading utilities for locating, loading, and importing files throughout your project and node_modules.

Key Features:

  • Cross-platform path resolution
  • Node modules discovery
  • Dynamic import support
  • Project root (@/) path shortcuts

Type Safety

The library is built with TypeScript and provides comprehensive type definitions. All components support generic types for enhanced type safety:

// Type-safe nested data
type ConfigMap = {
  database: { host: string; port: number };
  cache: { ttl: number };
};
const config = new Nest<ConfigMap>();

// Type-safe event handling
type EventMap = {
  'user-login': [string, Date];
  'data-update': [object];
};
const emitter = new EventEmitter<EventMap>();

// Type-safe queue operations
const queue = new TaskQueue<[number, string]>();

Browser Compatibility

Most components work in both Node.js and browser environments:

  • Nest - Full browser support
  • EventEmitter - Full browser support
  • Exception - Full browser support
  • Queue - Full browser support
  • ⚠️ FileLoader/NodeFS - Node.js only

Events

From browser clicks to CLI commands to backend routing, Stackpress treats everything as an event that flows seamlessly through your stack.

Behind user experiences are a chain reaction of events.

At its core, all software exists for users to interact with it. These interactions—whether a mouse click in the browser, a command in the terminal, or a tap on a mobile app, are all actions. Actions are events. Software, in one way or another, is always waiting for certain actions to occur and then responding to them. This means every application has an inherent level of event-driven design (IBM Developer, Wikipedia).

Modern event-driven systems are valued for being responsive, loosely coupled, scalable, and resilient (Confluent, PubNub).

  • 🔄 Responsive — React immediately when an event occurs, instead of waiting on rigid request/response cycles. This enables real-time behavior, such as updating a UI the moment data changes or triggering backend workflows instantly.
  • 🧩 Loosely Coupled — Components don’t need direct knowledge of each other; they communicate through events. This reduces dependencies, making systems easier to maintain and extend.
  • 📈 Scalable — Events are asynchronous, which means they can be queued, processed in parallel, and distributed across workers or servers. This makes handling high loads far simpler.
  • 🛡️ Resilient — Failures are isolated. If one listener fails, the rest of the system continues. Recovery strategies like retries or fallbacks can be added without rewriting business logic.

Stackpress builds on these principles by making events not just a layer, but the foundation of software design. A user interaction starts at the edge (browser, CLI, mobile app), but in a fully event-driven design these same events can propagate through the backend—across servers, databases, sockets, etc. setting of a chain reaction of events where one event triggers actions, and those actions can emit more events in turn, and so on.

In Stackpress, even a router is a type of event emitter. By treating routes, sockets, and commands as events, Stackpress provides a unified, generically designed emitter that simplifies building across environments.

EventEmitter

A class that implements the observer pattern for handling events with priority levels and task queues.

type EventMap = Record<string, [number]> & {
  'trigger something': [number];
  'process data': [string, object];
};

const emitter = new EventEmitter<EventMap>();

Properties

The following properties are available when instantiating an EventEmitter.

Property Type Description
after EventHook<M[keyof M]> Hook called after each task execution
before EventHook<M[keyof M]> Hook called before each task execution
event Event<M[keyof M]> Current event match information
listeners object Frozen shallow copy of all event listeners

Methods

The following methods are available when instantiating an EventEmitter.

Adding Event Listeners

The following example shows how to add event listeners with optional priority.

emitter.on('trigger something', async (x) => {
  console.log('something triggered', x + 1);
});

emitter.on('trigger something', async (x) => {
  console.log('high priority', x + 2);
}, 2); // Higher priority executes first

Parameters

Parameter Type Description
event N extends EventName<M> The event name to listen for
action TaskAction<M[N]> The callback function to execute
priority number Priority level (higher numbers execute first, default: 0)

Returns

The EventEmitter instance to allow method chaining.

Emitting Events

The following example shows how to emit events and trigger all registered listeners.

const result = await emitter.emit('trigger something', 42);
console.log(result.code); // 200 for success, 404 if no listeners

Parameters

Parameter Type Description
event N extends EventName<M> The event name to emit
...args M[N] Arguments to pass to the event listeners

Returns

A promise that resolves to a Status object indicating success or failure.

Removing Event Listeners

The following example shows how to remove a specific event listener.

const handler = async (x) => console.log(x);
emitter.on('trigger something', handler);
emitter.unbind('trigger something', handler);

Parameters

Parameter Type Description
event N extends EventName<M> The event name
action TaskAction<M[N]> The specific callback function to remove

Returns

The EventEmitter instance to allow method chaining.

Clearing All Event Listeners

The following example shows how to clear all listeners for a specific event.

emitter.clear('trigger something');

Parameters

Parameter Type Description
event N extends EventName<M> The event name to clear

Returns

The EventEmitter instance to allow method chaining.

Matching Events

The following example shows how to get possible event matches.

const matches = emitter.match('trigger something');
console.log(matches.get('trigger something')?.pattern);

Parameters

Parameter Type Description
event string The event name to match

Returns

A Map of event matches with their patterns and data.

Getting Task Queue

The following example shows how to get a task queue for a specific event.

const queue = emitter.tasks('trigger something');
console.log(queue.size); // Number of tasks for this event

Parameters

Parameter Type Description
event N extends EventName<M> The event name

Returns

A TaskQueue containing all tasks for the specified event.

Using Other Emitters

The following example shows how to merge listeners from another emitter.

const emitter1 = new EventEmitter();
const emitter2 = new EventEmitter();

emitter2.on('shared event', async () => console.log('from emitter2'));
emitter1.use(emitter2); // emitter1 now has emitter2's listeners

Parameters

Parameter Type Description
emitter EventEmitter<M> Another emitter to merge listeners from

Returns

The EventEmitter instance to allow method chaining.

Creating Task Queues

The following example shows how to create a new task queue (can be overridden in subclasses).

const queue = emitter.makeQueue();

Returns

A new TaskQueue instance for managing event tasks.

Setting Hooks

The following example shows how to set before and after hooks for event execution.

emitter.before = async (event) => {
  console.log('Before:', event.event);
  return true; // Continue execution
};

emitter.after = async (event) => {
  console.log('After:', event.event);
};

Parameters

Parameter Type Description
event Event<M[keyof M]> Event information including name, args, and metadata

Returns

For before hook: false to stop execution, any other value to continue. For after hook: return value is ignored.

ExpressEmitter

Event emitter with regex pattern matching and parameter extraction capabilities, extending EventEmitter with Express-like routing patterns.

type EventMap = {
  'user-login': [string, Date];
  'api-*': [object];
  ':method /api/users': [Request, Response];
};

const emitter = new ExpressEmitter<EventMap>('/');

Properties

The following properties are available when instantiating an ExpressEmitter.

Property Type Description
separator string Pattern separator character (default: '/')
expressions Map<string, EventExpression> Map of event names to regex expressions
after EventHook<M[keyof M]> Hook called after each task execution (inherited)
before EventHook<M[keyof M]> Hook called before each task execution (inherited)
event Event<M[keyof M]> Current event match information (inherited)
listeners object Frozen shallow copy of all event listeners (inherited)

Methods

The following methods are available when instantiating an ExpressEmitter.

Adding Pattern-Based Event Listeners

The following example shows how to add event listeners with pattern matching.

const emitter = new ExpressEmitter(' '); // Space separator

// Exact match
emitter.on('user login', async (data) => {
  console.log('User logged in:', data);
});

// Wildcard patterns
emitter.on('user *', async (data) => {
  console.log('User action:', data);
});

// Parameter extraction
emitter.on(':action user', async (data) => {
  console.log('Action on user:', emitter.event?.data.params.action);
});

// Multiple parameters
emitter.on(':method /api/:resource', async (req, res) => {
  const { method, resource } = emitter.event?.data.params || {};
  console.log(`${method} request for ${resource}`);
});

Parameters

Parameter Type Description
event N|RegExp Event name pattern or regular expression
action TaskAction<M[N]> The callback function to execute
priority number Priority level (higher numbers execute first, default: 0)

Returns

The ExpressEmitter instance to allow method chaining.

Adding Regex Event Listeners

The following example shows how to add event listeners using regular expressions.

// Global regex
emitter.on(/^user (.+)$/, async (data) => {
  const action = emitter.event?.data.args[0];
  console.log('User action:', action);
});

// Non-global regex
emitter.on(/user (login|logout)/i, async (data) => {
  const [action] = emitter.event?.data.args || [];
  console.log('User authentication:', action);
});

Parameters

Parameter Type Description
event RegExp Regular expression pattern
action TaskAction<M[N]> The callback function to execute
priority number Priority level (higher numbers execute first, default: 0)

Returns

The ExpressEmitter instance to allow method chaining.

Pattern Matching

The following example shows how to get all matching patterns for an event.

emitter.on('user *', handler1);
emitter.on(':action user', handler2);
emitter.on(/user (.+)/, handler3);

const matches = emitter.match('user login');
// Returns Map with all matching patterns and their extracted data

Parameters

Parameter Type Description
event string The event name to match against patterns

Returns

A Map of event matches with their patterns, parameters, and arguments.

Using Other ExpressEmitters

The following example shows how to merge patterns and listeners from another emitter.

const emitter1 = new ExpressEmitter('/');
const emitter2 = new ExpressEmitter('-');

emitter2.on('api-*', handler);
emitter2.on(':method-users', handler);

emitter1.use(emitter2); // Merges expressions and listeners

Parameters

Parameter Type Description
emitter EventEmitter<M> Another emitter to merge patterns from

Returns

The ExpressEmitter instance to allow method chaining.

Pattern Syntax

ExpressEmitter supports several pattern matching syntaxes:

Wildcard Patterns
// Single wildcard - matches one segment
emitter.on('user *', handler); // Matches: 'user login', 'user logout'

// Double wildcard - matches multiple segments  
emitter.on('api **', handler); // Matches: 'api/users/123/posts'
Parameter Extraction
// Named parameters
emitter.on(':method /api/users', handler);
// Matches: 'GET /api/users', 'POST /api/users'
// Extracts: { method: 'GET' }, { method: 'POST' }

// Multiple parameters
emitter.on(':method /api/:resource/:id', handler);
// Matches: 'GET /api/users/123'
// Extracts: { method: 'GET', resource: 'users', id: '123' }
Mixed Patterns
// Parameters with wildcards
emitter.on(':action user *', handler);
// Matches: 'login user data', 'logout user session'
// Extracts: { action: 'login' }, args: ['data']

// Complex routing patterns
emitter.on(':method /api/:version/users/*', handler);
// Matches: 'GET /api/v1/users/profile'
// Extracts: { method: 'GET', version: 'v1' }, args: ['profile']

Event Data Structure

When patterns match, the event data contains:

interface EventData {
  args: string[];        // Wildcard matches and regex groups
  params: object;        // Named parameter extractions
}

// Example for pattern ':method /api/:resource/*'
// Matching event 'GET /api/users/profile'
{
  args: ['profile'],           // Wildcard matches
  params: {                    // Named parameters
    method: 'GET',
    resource: 'users'
  }
}

Custom Separators

ExpressEmitter allows custom separators for different use cases:

// File path patterns
const fileEmitter = new ExpressEmitter('/');
fileEmitter.on('/src/*/index.js', handler);

// Command patterns  
const cmdEmitter = new ExpressEmitter(' ');
cmdEmitter.on(':command --:flag', handler);

// API patterns
const apiEmitter = new ExpressEmitter('-');
apiEmitter.on('api-:version-users', handler);

Regular Expression Support

ExpressEmitter supports both global and non-global regular expressions:

// Global regex - uses matchAll()
emitter.on(/user-(.+)/g, handler);

// Non-global regex - uses match()
emitter.on(/user-(.+)/, handler);

// Case insensitive
emitter.on(/USER-(.+)/i, handler);

Priority-Based Execution

Like EventEmitter, ExpressEmitter supports priority-based execution:

emitter.on('user *', handler1, 1);      // Lower priority
emitter.on('user login', handler2, 5);  // Higher priority  
emitter.on(/user (.+)/, handler3, 3);   // Medium priority

// Execution order: handler2, handler3, handler1
await emitter.emit('user login', data);

RouteEmitter

Event-driven routing system that extends ExpressEmitter for HTTP-like route handling.

type RouteMap = {
  'GET /users': [Request, Response];
  'POST /users': [Request, Response];
  'GET /users/:id': [Request, Response];
};

const router = new RouteEmitter<Request, Response>();

Properties

The following properties are available when instantiating a RouteEmitter.

Property Type Description
routes Map<string, Route> Map of event names to route definitions
separator string Pattern separator (always '/')
expressions Map<string, EventExpression> Map of event names to regex expressions (inherited)

Methods

The following methods are available when instantiating a RouteEmitter.

Defining Routes

The following example shows how to define HTTP-like routes.

const router = new RouteEmitter();

// Basic routes
router.route('GET', '/users', async (req, res) => {
  // Handle GET /users
});

router.route('POST', '/users', async (req, res) => {
  // Handle POST /users  
});

// Routes with parameters
router.route('GET', '/users/:id', async (req, res) => {
  const userId = req.params.id; // Parameter extraction
});

// Wildcard routes
router.route('GET', '/api/*', async (req, res) => {
  // Handle any GET /api/* route
});

// Any method routes
router.route('ANY', '/health', async (req, res) => {
  // Handle any HTTP method to /health
});

Parameters

Parameter Type Description
method string HTTP method (GET, POST, PUT, DELETE, ANY, etc.)
path string Route path with optional parameters
action RouteAction<R, S> Route handler function
priority number Priority level (default: 0)

Returns

The RouteEmitter instance to allow method chaining.

Using Other RouteEmitters

The following example shows how to merge routes from another router.

const apiRouter = new RouteEmitter();
apiRouter.route('GET', '/api/users', handler);
apiRouter.route('POST', '/api/users', handler);

const mainRouter = new RouteEmitter();
mainRouter.use(apiRouter); // Merges routes and listeners

Parameters

Parameter Type Description
emitter EventEmitter<RouteMap<R, S>> Another router to merge routes from

Returns

The RouteEmitter instance to allow method chaining.

Route Patterns

RouteEmitter supports Express-like route patterns:

Parameter Routes
// Single parameter
router.route('GET', '/users/:id', handler);
// Matches: GET /users/123
// Extracts: { id: '123' }

// Multiple parameters  
router.route('GET', '/users/:userId/posts/:postId', handler);
// Matches: GET /users/123/posts/456
// Extracts: { userId: '123', postId: '456' }
Wildcard Routes
// Single wildcard
router.route('GET', '/files/*', handler);
// Matches: GET /files/document.pdf

// Catch-all wildcard
router.route('GET', '/static/**', handler);  
// Matches: GET /static/css/main.css
Method Handling
// Specific methods
router.route('GET', '/users', getUsers);
router.route('POST', '/users', createUser);
router.route('PUT', '/users/:id', updateUser);
router.route('DELETE', '/users/:id', deleteUser);

// Any method
router.route('ANY', '/health', healthCheck);

Event Generation

RouteEmitter automatically generates event names from routes:

router.route('GET', '/users/:id', handler);
// Generates event: 'GET /users/:id'
// Can be emitted as: router.emit('GET /users/123', req, res)

router.route('ANY', '/api/*', handler);  
// Generates regex pattern for any method
// Matches: 'GET /api/users', 'POST /api/data', etc.

Integration with Router

RouteEmitter is used internally by the Router class but can be used standalone:

const routeEmitter = new RouteEmitter<MyRequest, MyResponse>();

routeEmitter.route('GET', '/api/:resource', async (req, res) => {
  const resource = routeEmitter.event?.data.params.resource;
  // Handle API resource request
});

// Emit route events directly
await routeEmitter.emit('GET /api/users', request, response);

Queue

Priority-based queue implementations for managing items and tasks with FIFO ordering.

When an event is triggered, Stackpress doesn’t just fire off listeners blindly. Instead, it organizes them into a TaskQueue, which then consumes items sequentially by priority.

Because events can be defined anywhere, event priority allows you to structure execution like a series of steps—making the flow of your application predictable and easy to follow.

Even better, the TaskQueue makes the EventEmitter a true plugin system: you can insert new code between existing listeners without rewriting or restructuring what’s already there. This means features, extensions, or third-party modules can seamlessly “hook into” the event pipeline without breaking your core logic.

image

const itemQueue = new ItemQueue<string>();
const taskQueue = new TaskQueue<[number]>();

ItemQueue

An item queue that orders and consumes items sequentially based on priority (FIFO by default).

Properties

The following properties are available when instantiating an ItemQueue.

Property Type Description
queue Item<I>[] The internal queue array (readonly)
size number The number of items in the queue

Methods

The following methods are available when instantiating an ItemQueue.

Adding Items with Priority

The following example shows how to add items with specific priority levels.

const queue = new ItemQueue<string>();
queue.add('medium', 5);
queue.add('high', 10);
queue.add('low', 1);

Parameters

Parameter Type Description
item I The item to add to the queue
priority number Priority level (higher numbers execute first, default: 0)

Returns

The ItemQueue instance to allow method chaining.

Adding Items to Bottom

The following example shows how to add items to the bottom of the queue (lowest priority).

queue.push('bottom-item');

Parameters

Parameter Type Description
item I The item to add to the bottom of the queue

Returns

The ItemQueue instance to allow method chaining.

Adding Items to Top

The following example shows how to add items to the top of the queue (highest priority).

queue.shift('top-item');

Parameters

Parameter Type Description
item I The item to add to the top of the queue

Returns

The ItemQueue instance to allow method chaining.

Consuming Items

The following example shows how to consume items one at a time in priority order.

const item = queue.consume(); // Returns highest priority item
console.log(item); // 'top-item'

Returns

The next item in the queue, or undefined if the queue is empty.

TaskQueue

A task queue that extends ItemQueue specifically for executing functions sequentially.

const queue = new TaskQueue<[number]>();
queue.push(async (x) => console.log(x + 1));
queue.shift(async (x) => console.log(x + 2));
queue.add(async (x) => console.log(x + 3), 10);
await queue.run(5);

Properties

The following properties are available when instantiating a TaskQueue.

Property Type Description
after TaskAction<A> Hook called after each task execution
before TaskAction<A> Hook called before each task execution
queue Item<TaskAction<A>>[] The internal queue array (inherited)
size number The number of tasks in the queue (inherited)

Methods

The following methods are available when instantiating a TaskQueue.

Running Tasks

The following example shows how to execute all tasks in the queue sequentially.

const queue = new TaskQueue<[number]>();
queue.push(async (x) => {
  console.log(x + 1);
  return true; // Continue execution
});
queue.add(async (x) => {
  console.log(x + 2);
  return false; // Abort execution
}, 10);

const result = await queue.run(5);
console.log(result.code); // 309 (ABORT) or 200 (OK)

Parameters

Parameter Type Description
...args A Arguments to pass to each task function

Returns

A promise that resolves to a ResponseStatus indicating success, abort, or not found.

Adding Tasks with Priority

The following example shows how to add tasks with specific priority levels.

queue.add(async (x) => console.log('high priority', x), 10);

Parameters

Parameter Type Description
item TaskAction<A> The task function to add
priority number Priority level (higher numbers execute first, default: 0)

Returns

The TaskQueue instance to allow method chaining.

Adding Tasks to Bottom

The following example shows how to add tasks to the bottom of the queue.

queue.push(async (x) => console.log('low priority', x));

Parameters

Parameter Type Description
item TaskAction<A> The task function to add to the bottom

Returns

The TaskQueue instance to allow method chaining.

Adding Tasks to Top

The following example shows how to add tasks to the top of the queue.

queue.shift(async (x) => console.log('high priority', x));

Parameters

Parameter Type Description
item TaskAction<A> The task function to add to the top

Returns

The TaskQueue instance to allow method chaining.

Setting Hooks

The following example shows how to set before and after hooks for task execution.

queue.before = async (x) => {
  console.log('Before task:', x);
  return true; // Continue execution
};

queue.after = async (x) => {
  console.log('After task:', x);
  return true; // Continue execution
};

Parameters

Parameter Type Description
...args A Arguments passed to the hook function

Returns

For hooks: false to abort execution, any other value to continue.

Task Execution Flow

  1. Before Hook: Called before each task (if set)
  2. Task Execution: The actual task function is called
  3. After Hook: Called after each task (if set)

If any step returns false, execution is aborted and the queue returns Status.ABORT.

Status Codes

  • Status.OK (200): All tasks completed successfully
  • Status.ABORT (309): Execution was aborted by a task or hook
  • Status.NOT_FOUND (404): No tasks in the queue

Routing

Event-driven routing system with generic Request and Response wrappers that work across different platforms (HTTP, terminal, web sockets).

type RequestData = { userId: string };
type ResponseData = { message: string };

const router = new Router<RequestData, ResponseData>();

router.route('GET', '/users/:id', async (req, res, ctx) => {
  const userId = req.data.get('id');
  res.setJSON({ message: `User ${userId}` });
});

Request

Generic request wrapper that works with IncomingMessage and WHATWG (Fetch) Request.

Properties

The following properties are available when instantiating a Request.

Property Type Description
data CallableNest Combined data from query, post, and additional data
headers CallableMap<string, string|string[]> Request headers
query CallableNest URL query parameters
post CallableNest POST body data
session CallableSession Session data
url URL Request URL object
method Method HTTP method
body Body|null Raw request body
loaded boolean Whether the body has been loaded
mimetype string Request body MIME type
resource R Original request resource
type string Type of body content
loader RequestLoader<R> Body loader function

Methods

The following methods are available when instantiating a Request.

Loading Request Body

The following example shows how to load the request body asynchronously.

const req = router.request({ 
  url: 'http://example.com/api',
  method: 'POST'
});

await req.load(); // Loads body using the configured loader
console.log(req.body); // Access the loaded body

Returns

The Request instance to allow method chaining.

Response

Generic response wrapper that works with ServerResponse and WHATWG (Fetch) Response.

Properties

The following properties are available when instantiating a Response.

Property Type Description
headers CallableMap<string, string|string[]> Response headers
session CallableSession Session data
errors CallableNest Validation errors
data CallableNest Response data
body Body|null Response body
code number HTTP status code
error string|undefined Error message
redirected boolean Whether response is a redirect
sent boolean Whether response has been sent
stack Trace[]|undefined Stack trace for errors
status string HTTP status message
total number Total count of results
mimetype string|undefined Response MIME type
resource S Original response resource
type string Type of body content
dispatcher ResponseDispatcher<S> Response dispatcher function

Methods

The following methods are available when instantiating a Response.

Setting JSON Response

The following example shows how to set a JSON response.

res.setJSON({ message: 'Success', data: results });
res.setJSON('{"message": "Success"}', 201, 'Created');

Parameters

Parameter Type Description
body string|NestedObject JSON data or string
code number HTTP status code (default: 200)
status string HTTP status message (optional)

Returns

The Response instance to allow method chaining.

Setting HTML Response

The following example shows how to set an HTML response.

res.setHTML('<h1>Welcome</h1>', 200, 'OK');

Parameters

Parameter Type Description
body string HTML content
code number HTTP status code (default: 200)
status string HTTP status message (optional)

Returns

The Response instance to allow method chaining.

Setting Error Response

The following example shows how to set an error response.

res.setError('Invalid input', { name: 'required' }, [], 400, 'Bad Request');
res.setError({ 
  code: 404, 
  error: 'Not found', 
  errors: { id: 'invalid' } 
});

Parameters

Parameter Type Description
error string|ErrorResponse Error message or response object
errors NestedObject<string|string[]> Validation errors (default: {})
stack Trace[] Stack trace (default: [])
code number HTTP status code (default: 400)
status string HTTP status message (optional)

Returns

The Response instance to allow method chaining.

Setting Results Response

The following example shows how to set a single result response.

res.setResults({ id: 1, name: 'John' });

Parameters

Parameter Type Description
body NestedObject Result object
code number HTTP status code (default: 200)
status string HTTP status message (optional)

Returns

The Response instance to allow method chaining.

Setting Rows Response

The following example shows how to set a collection response with total count.

res.setRows([{ id: 1 }, { id: 2 }], 100); // 2 items out of 100 total

Parameters

Parameter Type Description
body NestedObject[] Array of result objects
total number Total count of possible results (default: 0)
code number HTTP status code (default: 200)
status string HTTP status message (optional)

Returns

The Response instance to allow method chaining.

Redirecting

The following example shows how to redirect the response.

res.redirect('/login', 302, 'Found');
res.redirect('https://example.com'); // Default 302

Parameters

Parameter Type Description
url string Redirect URL
code number HTTP status code (default: 302)
status string HTTP status message (optional)

Returns

The Response instance to allow method chaining.

Dispatching Response

The following example shows how to dispatch the response to the native resource.

const nativeResponse = await res.dispatch();

Returns

The native response resource after dispatching.

Converting to Status Response

The following example shows how to convert the response to a status response object.

const statusResponse = res.toStatusResponse();
// Returns: { code, status, error, errors, stack, results, total }

Returns

A StatusResponse object with all response details.

Router

Event-driven router that extends ExpressEmitter for handling HTTP-like routing.

Properties

The following properties are available when instantiating a Router.

Property Type Description
routes Map<string, Route> Map of event names to route definitions

Methods

The following methods are available when instantiating a Router.

Defining Routes

The following example shows how to define routes with different HTTP methods.

router.route('GET', '/users/:id', async (req, res, ctx) => {
  const userId = req.data.get('id');
  res.setJSON({ id: userId, name: 'John' });
});

router.route('POST', '/users', async (req, res, ctx) => {
  const userData = req.data.get();
  // Create user logic
  res.setJSON(userData, 201, 'Created');
});

Parameters

Parameter Type Description
method string HTTP method (GET, POST, PUT, DELETE, etc.)
path string Route path with optional parameters (:id)
action RouterAction<R, S, X> Route handler function
priority number Priority level (default: 0)

Returns

The Router instance to allow method chaining.

Emitting Route Events

The following example shows how to emit route events directly.

const req = router.request({ url: '/users/123' });
const res = router.response();

const status = await router.emit('GET /users/123', req, res);
console.log(status.code); // 200, 404, etc.

Parameters

Parameter Type Description
event string Route event name (e.g., 'GET /users/123')
req Request<R> Request object
res Response<S> Response object

Returns

A promise that resolves to a Status object indicating success or failure.

Resolving Routes

The following example shows how to resolve routes and get response data.

// Resolve by method and path
const response = await router.resolve('GET', '/users/123', {
  headers: { 'Authorization': 'Bearer token' }
});

// Resolve by event name
const response = await router.resolve('user-created', userData);

Parameters

Parameter Type Description
methodOrEvent string HTTP method or event name
pathOrRequest string|Request<R>|Record<string, any> Route path or request data
requestOrResponse Request<R>|Record<string, any>|Response<S> Request or response object
response Response<S> Response object (optional)

Returns

A promise that resolves to a partial StatusResponse object.

Creating Request Objects

The following example shows how to create request objects.

const req = router.request({
  url: 'http://example.com/api/users',
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  data: { name: 'John', email: '[email protected]' }
});

Parameters

Parameter Type Description
init Partial<RequestOptions<R>> Request initialization options

Returns

A new Request instance.

Creating Response Objects

The following example shows how to create response objects.

const res = router.response({
  headers: { 'Content-Type': 'application/json' },
  data: { message: 'Success' }
});

Parameters

Parameter Type Description
init Partial<ResponseOptions<S>> Response initialization options

Returns

A new Response instance.

Using Other Routers

The following example shows how to merge routes and listeners from another router.

const apiRouter = new Router();
apiRouter.route('GET', '/api/users', handler);

const mainRouter = new Router();
mainRouter.use(apiRouter); // Merges routes and listeners

Parameters

Parameter Type Description
emitter EventEmitter<RouterMap<R, S, X>> Another router or emitter to merge

Returns

The Router instance to allow method chaining.

Route Parameters

Routes support parameter extraction using colon notation:

router.route('GET', '/users/:id/posts/:postId', (req, res) => {
  const userId = req.data.get('id');
  const postId = req.data.get('postId');
  // Parameters are automatically added to req.data
});

Cross-Platform Usage

The Router system is designed to work across different platforms:

  • HTTP Servers: Node.js IncomingMessage/ServerResponse
  • WHATWG Fetch: Browser/Deno Request/Response
  • Terminal Applications: Custom request/response objects
  • WebSockets: Custom message handling
// HTTP Server usage
const httpRouter = new Router<IncomingMessage, ServerResponse>();

// Terminal usage  
const terminalRouter = new Router<TerminalInput, TerminalOutput>();

// WebSocket usage
const wsRouter = new Router<WebSocketMessage, WebSocketResponse>();

File System

Cross-platform file loading utilities for locating, loading, and importing files throughout your project and node_modules.

import { NodeFS, FileLoader } from '@stackpress/lib';

const loader = new FileLoader(new NodeFS());

Properties

The following properties are available when instantiating a FileLoader.

Property Type Description
cwd string Current working directory
fs FileSystem Filesystem interface being used

Methods

The following methods are available when instantiating a FileLoader.

Getting Absolute Paths

The following example shows how to get absolute paths from various path formats.

// Project root paths (@ prefix)
const path1 = await loader.absolute('@/src/index.ts');
// Returns: /project/root/src/index.ts

// Relative paths
const path2 = await loader.absolute('./utils/helper.js');
// Returns: /current/directory/utils/helper.js

// Node modules
const path3 = await loader.absolute('@types/node');
// Returns: /project/node_modules/@types/node

// Absolute paths (unchanged)
const path4 = await loader.absolute('/usr/local/bin');
// Returns: /usr/local/bin

Parameters

Parameter Type Description
pathname string Path to resolve (supports @/, ./, ../, and node_modules)
pwd string Working directory (default: current working directory)

Returns

A promise that resolves to the absolute path string.

Getting Base Paths

The following example shows how to remove file extensions from paths.

const basePath = loader.basepath('/path/to/file.js');
// Returns: '/path/to/file'

const noExt = loader.basepath('/path/to/file');
// Returns: '/path/to/file' (unchanged if no extension)

Parameters

Parameter Type Description
pathname string Path to process

Returns

The path without the file extension.

Locating Node Modules

The following example shows how to locate node_modules directories.

// Find node_modules containing a specific package
const modulesPath = await loader.modules('@types/node');
// Returns: '/project/node_modules'

// Find node_modules from a specific directory
const modulesPath2 = await loader.modules('lodash', '/some/path');
// Returns: '/some/path/node_modules' or traverses up

Parameters

Parameter Type Description
pathname string Package name to locate
pwd string Starting directory (default: current working directory)
meta boolean Use import.meta.resolve if available (default: true)

Returns

A promise that resolves to the node_modules directory path.

Locating Library Path

The following example shows how to locate the @stackpress/lib installation.

const libPath = await loader.lib();
// Returns: '/project/node_modules' (where @stackpress/lib is installed)

const libPath2 = await loader.lib('/custom/path');
// Returns: node_modules path containing @stackpress/lib from custom path

Parameters

Parameter Type Description
pwd string Starting directory (default: current working directory)

Returns

A promise that resolves to the node_modules directory containing @stackpress/lib.

Importing Files

The following example shows how to dynamically import various file types.

// Import JavaScript/TypeScript modules
const module = await loader.import('./utils/helper.js');
console.log(module.default); // Default export
console.log(module.namedExport); // Named export

// Import JSON files
const config = await loader.import('./config.json');
console.log(config); // Parsed JSON object

// Import with default extraction
const defaultExport = await loader.import('./module.js', true);
// Returns only the default export

Parameters

Parameter Type Description
pathname string Path to the file to import
getDefault boolean Return only the default export (default: false)

Returns

A promise that resolves to the imported module or JSON data.

Getting Relative Paths

The following example shows how to get relative paths between files.

// Get relative path from source to target
const relativePath = loader.relative(
  '/project/src/components/Button.js',
  '/project/src/utils/helper.js'
);
// Returns: '../utils/helper'

// Include file extension
const relativeWithExt = loader.relative(
  '/project/src/components/Button.js',
  '/project/src/utils/helper.js',
  true
);
// Returns: '../utils/helper.js'

Parameters

Parameter Type Description
pathname string Source file path
require string Target file path
withExtname boolean Include file extension (default: false)

Returns

The relative path from source to target.

Resolving Paths

The following example shows how to resolve and verify path existence.

// Resolve path (returns null if not found)
const resolved = await loader.resolve('./config.json');
// Returns: '/project/config.json' or null

// Resolve with existence check (throws if not found)
try {
  const resolved = await loader.resolve('./config.json', process.cwd(), true);
  console.log('File exists:', resolved);
} catch (error) {
  console.log('File not found');
}

// Resolve from specific directory
const resolved2 = await loader.resolve('@/src/index.ts', '/custom/pwd');

Parameters

Parameter Type Description
pathname string Path to resolve
pwd string Working directory (default: current working directory)
exists boolean Throw error if path doesn't exist (default: false)

Returns

A promise that resolves to the absolute path or null if not found.

Resolving Files with Extensions

The following example shows how to resolve files with automatic extension detection.

// Try multiple extensions
const file = await loader.resolveFile('./module', ['.js', '.ts', '.json']);
// Tries: ./module.js, ./module.ts, ./module.json, ./module/index.js, etc.

// Resolve with existence check
try {
  const file = await loader.resolveFile('./module', ['.js'], process.cwd(), true);
  console.log('Found file:', file);
} catch (error) {
  console.log('No file found with specified extensions');
}

Parameters

Parameter Type Description
pathname string Path to resolve (without extension)
extnames string[] File extensions to try (default: ['.js', '.json'])
pwd string Working directory (default: current working directory)
exists boolean Throw error if file doesn't exist (default: false)

Returns

A promise that resolves to the resolved file path or null if not found.

Path Resolution Patterns

FileLoader supports several path resolution patterns:

Project Root Paths (@/)

// @ refers to the project root (cwd)
await loader.absolute('@/src/index.ts');
await loader.absolute('@/config/database.json');

Relative Paths

// Standard relative paths
await loader.absolute('./utils/helper.js');
await loader.absolute('../shared/constants.ts');

Node Modules

// Automatically resolves from node_modules
await loader.absolute('lodash');
await loader.absolute('@types/node');
await loader.absolute('@company/private-package');

Absolute Paths

// Absolute paths are returned as-is (with symlink resolution)
await loader.absolute('/usr/local/bin/node');
await loader.absolute('C:\\Program Files\\Node\\node.exe');

File System Abstraction

FileLoader works with any FileSystem implementation:

import { NodeFS } from '@stackpress/lib';

// Node.js filesystem
const nodeLoader = new FileLoader(new NodeFS());

// Custom filesystem (for testing, virtual fs, etc.)
class CustomFS implements FileSystem {
  // Implement required methods...
}
const customLoader = new FileLoader(new CustomFS());

Cross-Platform Compatibility

FileLoader handles platform differences automatically:

  • Path separators: Automatically uses correct separators (/ vs \)
  • Symlinks: Resolves symbolic links to real paths
  • Case sensitivity: Respects filesystem case sensitivity rules
  • Module resolution: Works with both CommonJS and ES modules

Error Handling

FileLoader provides clear error messages for common issues:

try {
  await loader.modules('non-existent-package');
} catch (error) {
  // Error: Cannot find non-existent-package in any node_modules
}

try {
  await loader.resolve('missing-file.js', process.cwd(), true);
} catch (error) {
  // Error: Cannot resolve 'missing-file.js'
}

Usage Examples

Loading Configuration Files

// Load config with fallbacks
const config = await loader.import('@/config.json').catch(() => ({}));

// Load environment-specific config
const env = process.env.NODE_ENV || 'development';
const envConfig = await loader.import(`@/config/${env}.json`);

Module Resolution

// Resolve module paths for bundling
const modulePath = await loader.absolute('lodash');
const relativePath = loader.relative('./src/index.js', modulePath);

// Find all modules in a directory
const modules = await loader.modules('.');

Dynamic Imports

// Dynamically import plugins
const pluginPath = await loader.resolveFile(`./plugins/${name}`, ['.js', '.ts']);
if (pluginPath) {
  const plugin = await loader.import(pluginPath, true);
  // Use plugin.default
}

## Data Structures

Data structures in programming are specialized formats for organizing, processing, storing, and retrieving data within a computer's memory. They define the relationships between data elements and the operations that can be performed on them. The choice of data structure significantly impacts the efficiency and performance of algorithms and overall program execution. 

### Nest

Hierarchical data management utilities for nested objects and arrays.

```typescript
import { nest, Nest, ReadonlyNest } from '@stackpress/lib';

type NestMap = {
  database: Record<string, { host: string, port: number}>
};

const callable = nest<NestMap>({
  database: {
    postgres: {
      host: 'localhost',
      port: 5432
    }
  }
});

const config = new Nest<NestMap>({
  database: {
    postgres: {
      host: 'localhost',
      port: 5432
    }
  }
});

const readonly = new ReadonlyNest<NestMap>({
  database: {
    postgres: {
      host: 'localhost',
      port: 5432
    }
  }
});

Properties

The following properties are available when instantiating a Nest.

Property Type Description
data M Raw nested data structure
size number Total number of top-level keys
withArgs ArgString Parser for terminal args
withFormData FormData Parser for multipart/form-data
withPath PathString Parser for path notations
withQuery QueryString Parser for query string

Methods

The following methods are available when instantiating a Nest.

Retrieving Data

The following example shows how to retrieve data from a nest.

config.get('database', 'postgres', 'port'); //--> 5432
config.get<number>('database', 'postgres', 'port'); //--> 5432

Parameters

Parameter Type Description
...path Key[] A path of object keys leading to the value

Returns

The value given the object key path. If you don't provide a generic, will follow the type map provided.

Setting Data

The following example shows how to add or update data in a nest.

config.set('database', 'postgres', 'port', 5432); //--> Nest
config.set({ foo: 'bar', baz: 'qux' }); //--> Nest

Parameters

Parameter Type Description
...path any[] A path of object keys leading to the value to be set, with the last argument being the value

Returns

The nest object to allow chainability.

Checking For Data

The following example shows how to check if an object key path has been set.

config.has('database', 'postgres', 'port'); //--> true

Parameters

Parameter Type Description
...path Key[] A path of object keys leading to the value to check

Returns

true if the object key path is set, false otherwise.

Deleting Data

The following example shows how to delete a value.

config.delete('database', 'postgres', 'port'); //--> Nest

Parameters

Parameter Type Description
...path Key[] A path of object keys leading to the value to be deleted

Returns

The nest object to allow chainability.

Purging Data

The following example shows how to purge the nest.

config.clear(); //--> Nest

Returns

The nest object to allow chainability.

Getting the Top-Level Keys

The following example shows how to get the top-level keys in a nest.

config.keys(); //--> [ 'database' ]

Returns

An array of object key strings.

Getting the Top-Level Values

The following example shows how to get the top-level values in a nest.

config.values(); //--> [ { postgres: { host: 'localhost', port: 5432 } } ]

Returns

An array of arbitrary values.

Getting the Top-Level Entries

The following example shows how to get the top-level entries in a nest.

config.entries(); //--> [ ['database', { postgres: { host: 'localhost', port: 5432 } }] ]

Returns

An array of key-value pairs.

Retrieving Data Using a Key Path

The following example shows how to retrieve data from a nest using a key dot path.

config.path('database.postgres.port'); //--> 5432
config.path<number>('database.postgres.port'); //--> 5432
config.path('database.mysql.port', 3306); //--> 3306

Parameters

Parameter Type Description
path string An object key path separated by dots leading to the value
defaults TypeOf<T> Default value to return if path doesn't exist

Returns

The value given the object key path. If you don't provide a generic, will follow the type map provided.

Iterating Over Data

The following example shows how to iterate over data at a specific path.

await config.forEach('database', (value, key) => {
  console.log(key, value);
  return true; // continue iteration
});

Parameters

Parameter Type Description
...path any[] A path of object keys leading to the data to iterate, with the last argument being the callback function

Returns

A promise that resolves to true if all iterations completed, false if stopped early.

Converting a Nest to a JSON

The following example shows how to generate a JSON string from a nest.

config.toString(); 
//--> { "database": { "postgres": { "host": "localhost", "port": 5432 } } }
config.toString(false); //--> {"database":{"postgres":{"host":"localhost","port":5432}}}

Parameters

Parameter Type Description
expand boolean Whether to format the JSON with indentation (default: true)
...path Key[] Optional path to convert only a subset of the data

Returns

A JSON string derived from the nest.

Callable Nest

Creates a callable Nest instance that can be invoked as a function to get nested values.

import { nest } from '@stackpress/lib';

const config = nest<NestMap>({
  database: {
    postgres: {
      host: 'localhost',
      port: 5432
    }
  }
});

config('database', 'postgres', 'host'); // 'localhost'
config<string>('database', 'postgres', 'host'); // 'localhost'

Maps

Creates a callable Map instance that can be invoked as a function to get values.

import { map } from '@stackpress/lib';

const userMap = map<string, User>([
  ['john', { name: 'John', age: 30 }],
  ['jane', { name: 'Jane', age: 25 }]
]);

// Use as function to get values
const john = userMap('john'); // { name: 'John', age: 30 }

// Use as Map
userMap.set('bob', { name: 'Bob', age: 35 });
console.log(userMap.size); // 3

Properties

The following properties are available on a callable map.

Property Type Description
size number Number of key-value pairs in the map

Methods

The following methods are available on a callable map.

Direct Invocation

The following example shows how to use the map as a function.

const value = myMap('key'); // Equivalent to myMap.get('key')

Parameters

Parameter Type Description
key K The key to retrieve the value for

Returns

The value associated with the key, or undefined if not found.

Setting Values

The following example shows how to set key-value pairs.

myMap.set('newKey', 'newValue');

Parameters

Parameter Type Description
key K The key to set
value V The value to associate with the key

Returns

The Map instance to allow method chaining.

Getting Values

The following example shows how to get values by key.

const value = myMap.get('key');

Parameters

Parameter Type Description
key K The key to retrieve the value for

Returns

The value associated with the key, or undefined if not found.

Checking Key Existence

The following example shows how to check if a key exists.

const exists = myMap.has('key'); // true or false

Parameters

Parameter Type Description
key K The key to check for existence

Returns

true if the key exists, false otherwise.

Deleting Keys

The following example shows how to delete a key-value pair.

const deleted = myMap.delete('key'); // true if deleted, false if not found

Parameters

Parameter Type Description
key K The key to delete

Returns

true if the key was deleted, false if the key didn't exist.

Clearing All Data

The following example shows how to remove all key-value pairs.

myMap.clear();
console.log(myMap.size); // 0

Returns

undefined

Iterating Over Entries

The following example shows how to iterate over all key-value pairs.

for (const [key, value] of myMap.entries()) {
  console.log(key, value);
}

Returns

An iterator of [key, value] pairs.

Iterating Over Keys

The following example shows how to iterate over all keys.

for (const key of myMap.keys()) {
  console.log(key);
}

Returns

An iterator of keys.

Iterating Over Values

The following example shows how to iterate over all values.

for (const value of myMap.values()) {
  console.log(value);
}

Returns

An iterator of values.

Using forEach

The following example shows how to execute a function for each key-value pair.

myMap.forEach((value, key, map) => {
  console.log(`${key}: ${value}`);
});

Parameters

Parameter Type Description
callback (value: V, key: K, map: Map<K, V>) => void Function to execute for each element

Returns

undefined

Sets

Creates a callable Set instance that can be invoked as a function to get values by index.

import { set } from '@stackpress/lib';

const tags = set(['javascript', 'typescript', 'node.js']);

// Use as function to get by index
const firstTag = tags(0); // 'javascript'
const secondTag = tags(1); // 'typescript'

// Use as Set
tags.add('react');
console.log(tags.size); // 4

Properties

The following properties are available on a callable set.

Property Type Description
size number Number of values in the set

Methods

The following methods are available on a callable set.

Direct Invocation

The following example shows how to use the set as a function to get values by index.

const value = mySet(0); // Get first item
const value2 = mySet(2); // Get third item

Parameters

Parameter Type Description
index number The index of the value to retrieve

Returns

The value at the specified index, or undefined if index is out of bounds.

Getting Values by Index

The following example shows how to get values by index using the index method.

const value = mySet.index(0); // Same as mySet(0)

Parameters

Parameter Type Description
index number The index of the value to retrieve

Returns

The value at the specified index, or undefined if index is out of bounds.

Adding Values

The following example shows how to add values to the set.

mySet.add('newValue');

Parameters

Parameter Type Description
value V The value to add to the set

Returns

The Set instance to allow method chaining.

Checking Value Existence

The following example shows how to check if a value exists.

const exists = mySet.has('value'); // true or false

Parameters

Parameter Type Description
value V The value to check for existence

Returns

true if the value exists, false otherwise.

Deleting Values

The following example shows how to delete a value.

const deleted = mySet.delete('value'); // true if deleted, false if not found

Parameters

Parameter Type Description
value V The value to delete

Returns

true if the value was deleted, false if the value didn't exist.

Clearing All Data

The following example shows how to remove all values.

mySet.clear();
console.log(mySet.size); // 0

Returns

undefined

Iterating Over Entries

The following example shows how to iterate over all value pairs.

for (const [value1, value2] of mySet.entries()) {
  console.log(value1, value2); // Both values are the same in a Set
}

Returns

An iterator of [value, value] pairs.

Iterating Over Values

The following example shows how to iterate over all values.

for (const value of mySet.values()) {
  console.log(value);
}

Returns

An iterator of values.

Using forEach

The following example shows how to execute a function for each value.

mySet.forEach((value1, value2, set) => {
  console.log(value1); // value1 and value2 are the same
});

Parameters

Parameter Type Description
callback (value: V, value2: V, set: Set<V>) => void Function to execute for each element

Returns

undefined

Exception

Enhanced error handling with expressive error reporting and stack trace support.

const exception = new Exception('Invalid Parameters: %s', 400)
  .withErrors({
    name: 'required',
    email: 'invalid format'
  })
  .withPosition(100, 200);

Static Methods

The following methods can be accessed directly from Exception itself.

Creating Exceptions with Templates

The following example shows how to create exceptions with template strings.

throw Exception.for('Something %s is %s', 'good', 'bad');
// Results in: "Something good is bad"

Parameters

Parameter Type Description
message string Template message with %s placeholders
...values unknown[] Values to replace %s placeholders

Returns

A new Exception instance with the formatted message.

Creating Exceptions from Response Objects

The following example shows how to create exceptions from response objects.

const response = { 
  code: 400, 
  error: 'Bad Request', 
  errors: { field: 'required' } 
};
throw Exception.forResponse(response);

Parameters

Parameter Type Description
response Partial<StatusResponse> Response object with error details
message string Fallback message if response.error is not provided

Returns

A new Exception instance configured from the response object.

Creating Exceptions for Validation Errors

The following example shows how to create exceptions for validation errors.

throw Exception.forErrors({
  name: 'required',
  email: 'invalid format'
});

Parameters

Parameter Type Description
errors NestedObject<string> Object containing validation errors

Returns

A new Exception instance with "Invalid Parameters" message and error details.

Requiring Conditions

The following example shows how to assert conditions and throw if they fail.

Exception.require(count > 0, 'Count %s must be positive', count);

Parameters

Parameter Type Description
condition boolean Condition that must be true
message string Error message with %s placeholders
...values any[] Values to replace %s placeholders

Returns

Void if condition is true, throws Exception if false.

Try-Catch Wrapper

The following example shows how to use the synchronous try-catch wrapper.

const result = Exception
  .try(() => riskyOperation())
  .catch((error, kind) => {
    console.log('Error type:', kind);
    return defaultValue;
  });

Parameters

Parameter Type Description
callback () => T Function to execute safely

Returns

An object with a catch method for handling errors.

Upgrading Errors

The following example shows how to upgrade regular errors to exceptions.

try {
  // some operation
} catch (error) {
  throw Exception.upgrade(error, 400);
}

Parameters

Parameter Type Description
error Error The error to upgrade
code number HTTP status code (default: 500)

Returns

An Exception instance (returns original if already an Exception).

Properties

The following properties are available when instantiating an Exception.

Property Type Description
code number HTTP status code
end number Ending character position of the error
errors object Validation errors object
start number Starting character position of the error
type string Exception type name

Methods

The following methods are available when instantiating an Exception.

Setting Error Code

The following example shows how to set the HTTP status code.

exception.withCode(404);

Parameters

Parameter Type Description
code number HTTP status code

Returns

The Exception instance to allow method chaining.

Adding Validation Errors

The following example shows how to add validation errors.

exception.withErrors({
  name: 'required',
  email: ['required', 'invalid format']
});

Parameters

Parameter Type Description
errors NestedObject<string|string[]> Validation errors object

Returns

The Exception instance to allow method chaining.

Setting Position Information

The following example shows how to set character position information.

exception.withPosition(100, 200);

Parameters

Parameter Type Description
start number Starting character position
end number Ending character position

Returns

The Exception instance to allow method chaining.

Converting to Response Object

The following example shows how to convert the exception to a response object.

const response = exception.toResponse();
// Returns: { code, status, error, start, end, stack, errors? }

Parameters

Parameter Type Description
start number Starting index for stack trace (default: 0)
end number Ending index for stack trace (default: 0)

Returns

An ErrorResponse object with all exception details.

Converting to JSON

The following example shows how to convert the exception to JSON.

const json = exception.toJSON();
console.log(json); // Pretty-printed JSON string

Returns

A formatted JSON string representation of the exception.

Getting Stack Trace

The following example shows how to get the parsed stack trace.

const trace = exception.trace();
trace.forEach(frame => {
  console.log(`${frame.method} at ${frame.file}:${frame.line}:${frame.char}`);
});

Parameters

Parameter Type Description
start number Starting index for stack trace (default: 0)
end number Ending index for stack trace (default: 0)

Returns

An array of Trace objects with method, file, line, and char information.

Types

Type definitions for the Stackpress library providing type safety and structure for data manipulation, event handling, routing, and system operations.

import type { 
  NestedObject, 
  EventMap, 
  RouterAction 
} from '@stackpress/lib/types';

General Types

The following types provide general utility for type manipulation and inference.

TypeOf

Utility type that extracts the primitive type from a value, providing type-safe inference for nested object operations.

type StringType = TypeOf<string>; // string
type NumberType = TypeOf<number>; // number
type BooleanType = TypeOf<boolean>; // boolean
type AnyType = TypeOf<undefined>; // any
type NullType = TypeOf<null>; // null

Usage

Used internally by Nest and other data structures to maintain type safety when accessing nested values with generic type parameters.

Data Types

The following types define structures for nested data manipulation and scalar value handling.

NestedObject

Represents a nested object structure where values can be of any type or further nested objects.

type UserConfig = NestedObject<string | number>;
const config: UserConfig = {
  database: {
    host: 'localhost',
    port: 5432
  },
  cache: {
    ttl: 3600
  }
};

Parameters

Parameter Type Description
V unknown The type of values stored in the nested structure

Usage

Used as the foundation for Nest data structures and configuration objects throughout the library.

UnknownNest

Type alias for NestedObject<unknown> representing a nested object with unknown value types.

const data: UnknownNest = {
  user: { name: 'John', age: 30 },
  settings: { theme: 'dark' }
};

Usage

Used as the default type parameter for Nest when specific value types are not known at compile time.

Scalar

Union type representing primitive values that can be stored in nested structures.

const value: Scalar = 'hello'; // string
const count: Scalar = 42; // number
const flag: Scalar = true; // boolean
const empty: Scalar = null; // null

Usage

Used in form data processing and configuration systems where only primitive values are expected.

Hash

Type alias for NestedObject<Scalar> representing a nested object structure containing only scalar values.

const settings: Hash = {
  app: {
    name: 'MyApp',
    version: '1.0.0',
    debug: true
  },
  database: {
    port: 5432,
    ssl: false
  }
};

Usage

Used for configuration objects and form data where complex objects are not allowed, only primitive values.

Cookie Types

The following types define cookie configuration and parsing options.

CookieOptions

Configuration options for HTTP cookies including security and behavior settings.

const cookieConfig: CookieOptions = {
  domain: '.example.com',
  expires: new Date('2024-12-31'),
  httpOnly: true,
  maxAge: 86400,
  path: '/',
  secure: true,
  sameSite: 'strict'
};

Properties

Property Type Description
domain string Cookie domain scope
expires Date Cookie expiration date
httpOnly boolean Restrict cookie to HTTP requests only
maxAge number Cookie lifetime in seconds
path string Cookie path scope
partitioned boolean Enable partitioned cookies
priority `'low' 'medium'
sameSite `boolean 'lax'
secure boolean Require HTTPS for cookie transmission

Usage

Used by the Response class when setting cookies and by cookie parsing utilities in the data processors.

Status Types

The following types define response status structures and error handling.

ResponseStatus

Basic response status structure containing HTTP status code and message.

const status: ResponseStatus = {
  code: 200,
  status: 'OK'
};

Properties

Property Type Description
code number HTTP status code
status string HTTP status message

Usage

Used as the foundation for all response status types and by the Status utility for HTTP status code management.

ErrorResponse

Extended response structure for error conditions including error details and stack traces.

const errorResponse: ErrorResponse = {
  code: 400,
  status: 'Bad Request',
  error: 'Invalid input data',
  errors: {
    email: 'Email is required',
    password: ['Password too short', 'Password must contain numbers']
  },
  stack: [{
    method: 'validateUser',
    file: 'auth.ts',
    line: 42,
    char: 10
  }]
};

Properties

Property Type Description
code number HTTP error status code
status string HTTP error status message
error string Primary error message
errors `NestedObject<string string[]>`
start number Error start position (optional)
end number Error end position (optional)
stack Trace[] Stack trace information (optional)

Usage

Used by the Response class for error responses and by the Exception class for structured error handling.

SuccessResponse

Extended response structure for successful operations including result data and pagination.

const successResponse: SuccessResponse<User[]> = {
  code: 200,
  status: 'OK',
  results: [
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' }
  ],
  total: 150
};

Properties

Property Type Description
code number HTTP success status code
status string HTTP success status message
results T Response data of generic type
total number Total count for pagination (optional)

Usage

Used by the Response class for successful API responses and data retrieval operations.

StatusResponse

Union type combining error and success response structures for flexible response handling.

const response: StatusResponse<User> = {
  code: 200,
  status: 'OK',
  results: { id: 1, name: 'John' }
};

// Or for errors
const errorResponse: StatusResponse = {
  code: 400,
  status: 'Bad Request',
  error: 'Validation failed'
};

Usage

Used by router actions and event handlers that can return either success or error responses. Distinguished from ResponseStatus by including optional error and success fields.

Trace

Stack trace entry providing debugging information for error tracking.

const trace: Trace = {
  method: 'processRequest',
  file: '/src/router/Router.ts',
  line: 125,
  char: 15
};

Properties

Property Type Description
method string Function or method name
file string Source file path
line number Line number in source file
char number Character position on line

Usage

Used in error responses and exception handling to provide detailed debugging information about error origins.

Queue Types

The following types define structures for queue operations and task management.

Item

Generic item wrapper for priority queue operations.

const queueItem: Item<string> = {
  item: 'process-data',
  priority: 10
};

Properties

Property Type Description
item I The queued item of generic type
priority number Priority level for queue ordering

Usage

Used by ItemQueue to wrap items with priority information for ordered processing.

TaskItem

Specialized item type for task functions with priority and arguments.

const taskItem: TaskItem<[string, number]> = {
  item: async (name: string, count: number) => {
    console.log(`Processing ${name} ${count} times`);
    return true;
  },
  priority: 5
};

Usage

Used by TaskQueue to manage executable functions with priority ordering and argument type safety.

TaskAction

Function type for executable tasks with flexible return types.

const taskAction: TaskAction<[User, Options]> = async (user, options) => {
  if (!user.isValid()) {
    return false; // Abort further processing
  }
  await processUser(user, options);
  return true; // Continue processing
};

Returns

Can return boolean, undefined, void, or promises of these types. Returning false aborts queue processing.

Usage

Used to define executable functions in TaskQueue with type-safe arguments and standardized return behavior.

TaskResult

Union type for task function return values.

const result: TaskResult = true; // Continue processing
const result2: TaskResult = false; // Abort processing
const result3: TaskResult = undefined; // Continue processing

Usage

Used internally by TaskQueue to handle different return types from task functions and determine processing flow.

Event Types

The following types define event system structures for type-safe event handling.

Event

Complete event object containing task information and event metadata.

const event: Event<[string, number]> = {
  item: async (message: string, count: number) => {
    console.log(`${message} - ${count}`);
  },
  priority: 1,
  event: 'user.login',
  pattern: 'user.*',
  data: {
    args: ['user', 'login'],
    params: { action: 'login' }
  },
  args: ['Welcome', 5],
  action: async (message: string, count: number) => {
    console.log(`${message} - ${count}`);
  }
};

Properties

Property Type Description
item TaskAction<A> The executable task function
priority number Event priority for ordering
event string The actual event name that was emitted
pattern string The pattern that matched this event
data EventData Parsed event data and parameters
args A Arguments passed to the event handler
action TaskAction<A> The event handler function

Usage

Used internally by EventEmitter to represent complete event objects during emission and processing.

EventMap

Type mapping event names to their argument types for type-safe event emission.

type AppEvents = {
  'user.login': [User, string];
  'user.logout': [User];
  'data.update': [string, any];
};

const emitter = new EventEmitter<AppEvents>();
emitter.on('user.login', (user: User, sessionId: string) => {
  // Type-safe event handler
});

Usage

Used as a generic parameter for EventEmitter to provide compile-time type checking for event names and arguments.

EventName

Utility type extracting valid event names from an event map.

type ValidEvents = EventName<AppEvents>; // 'user.login' | 'user.logout' | 'data.update'

Usage

Used internally by EventEmitter to constrain event names to those defined in the event map.

EventData

Parsed event information containing arguments and extracted parameters.

const eventData: EventData = {
  args: ['user', 'profile', 'update'],
  params: {
    resource: 'user',
    id: 'profile',
    action: 'update'
  }
};

Properties

Property Type Description
args string[] Event name split into components
params Record<string, string> Extracted parameters from pattern matching

Usage

Used by ExpressEmitter and RouteEmitter for pattern-based event matching and parameter extraction.

EventMatch

Event matching result containing pattern information and extracted data.

const match: EventMatch = {
  event: 'user.123.update',
  pattern: 'user.*.update',
  data: {
    args: ['user', '123', 'update'],
    params: { id: '123' }
  }
};

Properties

Property Type Description
event string The actual event name
pattern string The pattern that matched
data EventData Extracted event data

Usage

Used by pattern-matching event emitters to provide information about how events were matched and what parameters were extracted.

EventExpression

Compiled regular expression pattern for event matching.

const expression: EventExpression = {
  pattern: 'user.*.update',
  regexp: /^user\.([^\.]+)\.update$/
};

Properties

Property Type Description
pattern string Original pattern string
regexp RegExp Compiled regular expression

Usage

Used internally by ExpressEmitter to store compiled patterns for efficient event matching.

Payload Types

The following types define request and response payload structures.

Body

Union type for HTTP request/response body content supporting various data formats.

const stringBody: Body = 'Hello World';
const jsonBody: Body = { message: 'Hello' };
const bufferBody: Body = Buffer.from('data');
const streamBody: Body = fs.createReadStream('file.txt');

Usage

Used by Request and Response classes to handle different body types in a type-safe manner.

Headers

HTTP headers structure supporting both object and Map representations.

const headers: Headers = {
  'Content-Type': 'application/json',
  'Authorization': 'Bearer token123'
};

// Or as Map
const headerMap: Headers = new Map([
  ['Content-Type', 'application/json'],
  ['Set-Cookie', ['session=abc', 'csrf=xyz']]
]);

Usage

Used by Request and Response classes for HTTP header management with support for multiple values per header.

Data

Generic data container supporting both Map and object representations.

const data: Data = {
  user: { id: 1, name: 'John' },
  settings: { theme: 'dark' }
};

// Or as Map
const dataMap: Data = new Map([
  ['user', { id: 1, name: 'John' }],
  ['settings', { theme: 'dark' }]
]);

Usage

Used by Request class to store merged data from query parameters, POST data, and additional context.

Query

URL query parameters supporting string, Map, or object representations.

const query: Query = 'page=1&limit=10';
const queryObj: Query = { page: '1', limit: '10' };
const queryMap: Query = new Map([['page', '1'], ['limit', '10']]);

Usage

Used by Request class for URL query parameter handling with flexible input formats.

Post

POST request data supporting object or Map representations.

const postData: Post = {
  username: 'john',
  password: 'secret',
  remember: true
};

const postMap: Post = new Map([
  ['username', 'john'],
  ['password', 'secret']
]);

Usage

Used by Request class for form data and POST body content management.

ResponseOptions

Configuration options for response initialization.

const options: ResponseOptions<User> = {
  body: { id: 1, name: 'John' },
  headers: { 'Content-Type': 'application/json' },
  mimetype: 'application/json',
  data: { timestamp: Date.now() },
  resource: { id: 1, name: 'John' }
};

Properties

Property Type Description
body Body Response body content
headers Headers HTTP response headers
mimetype string Content MIME type
data Data Additional response data
resource S Generic resource object

Usage

Used by Response class constructor for initialization with various content types and metadata.

RequestOptions

Configuration options for request initialization.

const options: RequestOptions<IncomingMessage> = {
  resource: incomingMessage,
  method: 'POST',
  url: 'https://api.example.com/users',
  headers: { 'Content-Type': 'application/json' },
  body: { name: 'John' },
  data: { userId: 123 },
  query: { include: 'profile' },
  post: { action: 'create' },
  session: { token: 'abc123' }
};

Properties

Property Type Description
resource R Generic resource object (required)
body Body Request body content
headers Headers HTTP request headers
mimetype string Content MIME type
data Data Additional request data
method Method HTTP method
query Query URL query parameters
post Post POST request data
session Session Session data
url `string URL`

Usage

Used by Request class constructor for initialization with various request components and metadata.

Session Types

The following types define session management structures.

Session

Session data container supporting object or Map representations.

const session: Session = {
  userId: '123',
  username: 'john',
  role: 'admin'
};

const sessionMap: Session = new Map([
  ['userId', '123'],
  ['username', 'john']
]);

Usage

Used by Request class for session data management and by session processors for cookie-based sessions.

Revision

Session change tracking entry for audit and rollback capabilities.

const revision: Revision = {
  action: 'set',
  value: 'newValue'
};

const removeRevision: Revision = {
  action: 'remove'
};

Properties

Property Type Description
action `'set' 'remove'`
value `string string[]`

Usage

Used by WriteSession class to track session changes for persistence and rollback functionality.

Route Types

The following types define routing structures and HTTP method handling.

Method

HTTP method enumeration supporting all standard HTTP verbs.

const method: Method = 'GET';
const postMethod: Method = 'POST';
const allMethods: Method = 'ALL'; // Matches any method

Usage

Used by Router class for route registration and method-based request handling.

Route

Route definition containing HTTP method and path pattern.

const route: Route = {
  method: 'GET',
  path: '/users/:id'
};

Properties

Property Type Description
method string HTTP method
path string URL path pattern

Usage

Used by Router class to store route definitions and for route lookup operations.

RouteMap

Mapping of route patterns to request/response type pairs.

type AppRoutes = RouteMap<IncomingMessage, ServerResponse>;
const routes: AppRoutes = {
  'GET /users': [incomingMessage, serverResponse],
  'POST /users': [postRequest, postResponse]
};

Usage

Used internally by routing systems to maintain type relationships between routes and their handlers.

RouteAction

Function type for route handlers with request and response parameters.

const routeAction: RouteAction<Request, Response> = async (req, res) => {
  const userId = req.data('id');
  const user = await getUserById(userId);
  res.setResults(user);
  return true;
};

Usage

Used by RouteEmitter for route handler functions with standardized request/response handling.

Router Types

The following types define router context and action structures.

RouterContext

Context type for router operations, defaulting to Router when no custom context is provided.

type DefaultContext = RouterContext<Request, Response, undefined>; // Router<Request, Response>
type CustomContext = RouterContext<Request, Response, MyContext>; // MyContext

Usage

Used by Router class to provide flexible context handling in route handlers.

RouterArgs

Argument tuple for router action functions containing request, response, and context.

type HandlerArgs = RouterArgs<Request, Response, Router>;
// [Request, Response, Router]

Usage

Used internally by Router to define the argument structure for route handler functions.

RouterMap

Mapping of route patterns to router argument tuples.

type AppRouterMap = RouterMap<Request, Response, Router>;
const routerMap: AppRouterMap = {
  'GET /users': [request, response, router],
  'POST /users': [postRequest, postResponse, router]
};

Usage

Used internally by routing systems to maintain type relationships between routes and their argument structures.

RouterAction

Function type for router handlers with request, response, and context parameters.

const routerAction: RouterAction<Request, Response, Router> = async (req, res, router) => {
  const result = await router.resolve('validate-user', req);
  if (result.code !== 200) {
    res.fromStatusResponse(result);
    return false;
  }
  res.setResults({ success: true });
  return true;
};

Usage

Used by Router class for route handler functions with full router context access.

Filesystem Types

The following types define filesystem operation interfaces.

FileSystem

Interface defining filesystem operations for cross-platform compatibility.

const fs: FileSystem = {
  async exists(path: string) {
    try {
      await this.stat(path);
      return true;
    } catch {
      return false;
    }
  },
  
  async readFile(path: string, encoding: BufferEncoding) {
    return await readFileAsync(path, encoding);
  },
  
  async writeFile(path: string, data: string) {
    await writeFileAsync(path, data);
  },
  
  // ... other methods
};

Methods

Method Parameters Returns Description
exists path: string Promise<boolean> Check if file/directory exists
readFile path: string, encoding: BufferEncoding Promise<string> Read file contents
realpath path: string Promise<string> Resolve absolute path
stat path: string Promise<FileStat> Get file statistics
writeFile path: string, data: string Promise<void> Write file contents
mkdir path: string, options?: FileRecursiveOption Promise<void> Create directory
createReadStream path: string FileStream Create readable stream
unlink path: string void Delete file

Usage

Used by FileLoader and NodeFS classes to provide consistent filesystem operations across different environments and platforms.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment