Skip to content

TypeScript support

sloppyc accepts a focused subset of TypeScript and JavaScript. Sloppy does not require every route to be statically understood. If the compiler can emit runnable JavaScript inside Sloppy's supported runtime boundary, the app can run. Static source gives stronger Plan metadata; dynamic source produces partial metadata and findings.

This page is the high-level shape. The full matrix of error codes and extraction rules lives in reference/supported-syntax.md, and the canonical acceptance source is the fixture suite under compiler/tests/fixtures/.

Editor IntelliSense

@slopware/sloppy ships TypeScript declaration files. The root sloppy import has types, and supported public subpath imports such as sloppy/data, sloppy/fs, sloppy/os, and sloppy/providers/sqlite have types.

Install the package inside each app workspace when you want local editor IntelliSense:

sh
npm install --save-dev @slopware/sloppy@alpha

Templates include editor-friendly TypeScript config where applicable. After the dependency is installed, IntelliSense comes from the normal TypeScript language service used by your editor.

ts
import { Sloppy, Results, schema } from "sloppy";

const app = Sloppy.create();

app.get("/health", () => Results.text("ok"));

Sloppy compiler diagnostics are separate from TypeScript editor diagnostics. sloppyc extracts Sloppy app metadata and transforms supported source; it is not a full TypeScript type checker. A Sloppy language server is not implemented today.

The current declarations provide the public alpha typing surface. Some APIs have basic declarations that will deepen over alpha releases as the public contracts settle.

Inputs

  • File extensions: .js, .mjs, .ts.
  • Web entries import Sloppy from "sloppy", named and unaliased. Program Mode entries can be route-free and do not need Sloppy.
  • Results is file-local: any file with handlers that call Results.* must import Results from "sloppy" in that same file.
  • Compiler-recognized import sources: "sloppy", "sloppy/data", "sloppy/providers/sqlite", "sloppy/providers/postgres", "sloppy/providers/sqlserver", "sloppy/fs", "sloppy/time", "sloppy/crypto", "sloppy/codec", "sloppy/net", "sloppy/os", "sloppy/workers", relative imports rooted in the project, installed pure-JavaScript package imports, and the Node compatibility registry in Program Mode.

Unsupported package shapes, native addons, remote imports, and unsupported Node builtins fail with SLOPPYC_E_UNSUPPORTED_IMPORT_SPECIFIER or SLOPPYC_E_UNSUPPORTED_IMPORT, or a more specific package/Node diagnostic.

What the compiler extracts

The compiler reads supported source and emits Plan metadata for:

  • top-level app.get/post/put/patch/delete calls (and the mapGet/ mapPost/… aliases) on the app and on groups;
  • top-level app.group(...) and group method chains;
  • literal app.mapHealthChecks(...) calls;
  • route options — string literal patterns, route names, tags;
  • top-level app.services and builder.services registrations (addSingleton/addScoped/addTransient with literal token strings and inline non-capturing factories);
  • top-level capability declarations (capabilities.addDatabase("token", { ... }));
  • typed handler parameter bindings: Route<T>, Query<T>, Body<T>, Header<"name">, Service<T>, Config<"KEY">, plus Sqlite<"name">, Postgres<"name">, SqlServer<"name">, WorkQueue<"name">;
  • handler bodies that return Results.* literals or simple computed expressions over the request context;
  • async handlers whose returned Promise settles in the bounded microtask drain;
  • function modules and route-only modules.

Handler bodies and module shapes have their own static rules. If a handler is too dynamic for the extractor, you'll get SLOPPYC_E_UNSUPPORTED_HANDLER, SLOPPYC_E_UNSUPPORTED_HANDLER_PARAMETERS, SLOPPYC_E_UNSUPPORTED_HANDLER_VALUE, SLOPPYC_E_UNSUPPORTED_ASYNC_HANDLER_BODY, or SLOPPYC_E_UNSUPPORTED_TYPESCRIPT_HANDLER.

What gets rejected

Confirmed unsupported or constrained (each has a fixture or diagnostic code):

  • Native Node addons, remote imports, unsupported Node builtins, and package export shapes outside Sloppy's supported resolver subset.
  • Web app source extraction still rejects dynamic import() in route/app extraction paths. Program Mode supports string-literal dynamic imports and computed dynamic imports over explicit moduleInclude graphs.
  • Dynamic route patterns, computed method names, helper-based registration, conditionals, and loops build when the transformed JavaScript remains executable. The Plan records complete metadata for static routes and dynamicRoutes plus SLOPPYC_W_DYNAMIC_ROUTE findings for route metadata the compiler cannot fully infer.
  • Closures over module-level bindings inside handlers (fixtures/unsupported-handler-capture/).
  • TypeScript handler shapes the extractor doesn't model (fixtures/unsupported-typescript-handler/).
  • HTTP methods other than GET/POST/PUT/PATCH/DELETE (fixtures/unsupported-http-method/, SLOPPYC_E_UNSUPPORTED_HTTP_METHOD).
  • Dynamic middleware lookup, dynamic CORS policies, RequestId generator callbacks, dynamic RequestLogging options, Testing/TestServices imports, and dynamic controller mappings fail with specific diagnostics instead of being omitted from the Plan. Static middleware, CORS, RequestId, RequestLogging, and controller subsets are compiler-extracted.
  • Sloppy default imports (fixtures/unsupported-sloppy-default-import/, SLOPPYC_E_UNSUPPORTED_IMPORT_SPECIFIER).

What the compiler tolerates outside route-extraction code (helper functions, modules) is broader, but:

  • It does not type-check arbitrary TypeScript. Use tsc in your editor for full type checking; sloppyc parses TS syntax to extract Plan metadata.
  • It does not evaluate arbitrary expressions. Static literals (route patterns, capability tokens, service tokens) are extracted as strong metadata; computed route values become partial/dynamic metadata when the generated app can still run.

If a syntactic feature isn't covered by a fixture, treat it as unverified — file an issue or check the diagnostic if the compiler rejects it.

Editor support and IntelliSense

For editor autocomplete, hover, and go-to-definition in an app workspace, install @slopware/sloppy as a development dependency:

sh
npm install --save-dev @slopware/sloppy@alpha

The package ships TypeScript declarations (.d.ts) for the sloppy entry and supported subpath imports (currently sloppy/data, sloppy/fs, sloppy/os, and sloppy/providers/sqlite). Your editor picks them up through normal TypeScript module resolution against your project's tsconfig.json.

TypeScript IntelliSense in your editor and Sloppy's compiler diagnostics are separate signals:

  • Editor IntelliSense comes from tsc and the TypeScript Language Service using your tsconfig.json. It tells you about TypeScript types.
  • Sloppy diagnostics (SLOPPYC_*) come from sloppyc during sloppy build. They tell you whether the compiler can extract a complete Plan from the source.

A dedicated Sloppy language server is not part of the current alpha. Treat editor IntelliSense as an authoring aid; treat sloppy build diagnostics as the contract.

Imports

ts
// Public surface (everything documented in docs/api/)
import { Sloppy, Results, sql, schema } from "sloppy";

// Provider type wrappers for typed handler injection
import { Sqlite } from "sloppy/providers/sqlite";

// Relative
import { usersModule } from "./users";

// Program Mode package import
import { thing } from "installed-compatible-package";

// Program Mode Node compatibility shim
import path from "node:path";

Subpath imports under "sloppy/..." are reserved for the runtime stdlib; see API for what each subpath exports.

Results imports are not inherited across files. A thin main.ts that only creates the app and registers function modules can import Sloppy alone; each route module imports Results when its own handlers return Results.*.

Async handlers

ts
app.get("/users/{id:int}", async (ctx) => {
    const user = await loadUser(ctx.route.id);
    return user ? Results.ok(user) : Results.notFound();
});

The runtime awaits the returned promise during the owner-thread microtask drain. Long-running awaits aren't supported; if your handler depends on multi-second background work, queue it via WorkQueue and return Results.accepted({ jobId }).

Type-driven handler bindings

Typed handlers let you declare what a handler needs directly in the parameter list:

ts
import { Sloppy, Results, sql } from "sloppy";
import { Route, Query, Body } from "sloppy";
import { Sqlite } from "sloppy/providers/sqlite";

const app = Sloppy.create();

app.get("/users/{id:int}", (
    id:    Route<number>,
    db:    Sqlite<"main">,
) =>
    db.queryOne(sql`SELECT id, name FROM users WHERE id = ${id}`)
);

app.post("/users", (
    body:  Body<{ name: string; email: string }>,
    db:    Sqlite<"main">,
) =>
    db.exec(sql`
        INSERT INTO users (name, email)
        VALUES (${body.name}, ${body.email})
    `)
);

The compiler emits Plan metadata for route bindings, body schemas, provider injections, and service capabilities from these types.

Sqlite<"name">, Postgres<"name">, and SqlServer<"name"> all emit provider metadata and generated typed-injection wrappers. SQLite is the strongest local path. PostgreSQL and SQL Server execution also need the matching connection-string config, active provider bridge, and live service dependencies. SLOPPYC_E_UNSUPPORTED_PROVIDER_BRIDGE is reserved for unsupported static non-SQLite provider handles such as app.provider("postgres:main").

Common gotchas

  • Computed imports need an explicit graph. In Program Mode, import("./plugins/" + name + ".js") works only when matching modules are included with moduleInclude.
  • Static routes produce stronger metadata. Dynamic route registrations, loops, helpers, and conditionals can run, but CLI route metadata and OpenAPI are partial unless the compiler can reduce them safely.
  • No global process.env. Sloppy source has no Node global process. Import node:process only for the partial compatibility module. Use Environment.get(...) from "sloppy/os" if you need env access in module code, or read configuration via ctx.config inside a handler.
  • No top-level await. Initialize lazily inside services or handlers.
  • Config defaults are honored by typed injection. Config<"KEY"> reads the environment first, then falls back to a literal default from app.config.getString("KEY", "default") when the source declares one.

Public alpha. APIs and artifact formats may still change between alpha revisions.