diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d689b8b..38d11dc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,11 +9,11 @@ on: jobs: lint: - runs-on: ${{ matrix.os }} # runs a test on Ubuntu, Windows and macOS + runs-on: ${{ matrix.os }} strategy: matrix: - deno: ["v1.42.x"] + deno: ["v2.4.4"] os: [ubuntu-latest] steps: @@ -21,19 +21,20 @@ jobs: uses: actions/checkout@v4 - name: Setup Deno - uses: denoland/setup-deno@v1 + uses: denoland/setup-deno@v2 with: - deno-version: ${{ matrix.deno }} # tests across multiple Deno versions + deno-version: ${{ matrix.deno }} + cache: true - name: Run Deno lint run: deno lint test: - runs-on: ${{ matrix.os }} # runs a test on Ubuntu, Windows and macOS + runs-on: ${{ matrix.os }} strategy: matrix: - deno: ["v1.42.0", "v1.38.0"] + deno: ["v2.4.4", "v2.3.7"] os: [macOS-latest, windows-latest, ubuntu-latest] steps: @@ -41,12 +42,10 @@ jobs: uses: actions/checkout@v4 - name: Setup Deno - uses: denoland/setup-deno@v1 + uses: denoland/setup-deno@v2 with: - deno-version: ${{ matrix.deno }} # tests across multiple Deno versions - - - name: Cache Dependencies - run: deno cache deps.ts + deno-version: ${{ matrix.deno }} + cache: true - name: Run Tests run: deno test -A diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c0118d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Dnit manifest files (build state) +.manifest.json +*/.manifest.json + +# Test directories +.test/ diff --git a/ADLMap.ts b/ADLMap.ts deleted file mode 100644 index 7d1c168..0000000 --- a/ADLMap.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type * as sysTypes from "./adl-gen/runtime/sys/types.ts"; - -export class ADLMap { - constructor( - public data: sysTypes.Map, - private isEqual: (k1: K, k2: K) => boolean, - ) { - } - has(k: K): boolean { - return this.findIndex(k) !== -1; - } - get(k: K): V | undefined { - const ind = this.findIndex(k); - if (ind === -1) { - return undefined; - } - return this.data[ind].v2; - } - getOrInsert(k: K, v: V): V { - const existing = this.get(k); - if (existing === undefined) { - this.set(k, v); - return v; - } - return existing; - } - set(k: K, v: V) { - const ind = this.findIndex(k); - if (ind === -1) { - this.data.push({ v1: k, v2: v }); - } - this.data[ind] = { v1: k, v2: v }; - return this; - } - keys(): K[] { - return this.data.map((p) => p.v1); - } - values(): V[] { - return this.data.map((p) => p.v2); - } - entries(): [K, V][] { - return this.data.map((p) => [p.v1, p.v2]); - } - toData() { - return this.data; - } - - findIndex(k: K) { - return this.data.findIndex((p) => this.isEqual(p.v1, k)); - } -} diff --git a/README.md b/README.md index 6e688e9..4c5071f 100644 --- a/README.md +++ b/README.md @@ -9,37 +9,36 @@ across many files or shared between projects. ### Pre-Requisites - [Deno](https://deno.land/#installation) -- Requires deno v1.16.4 or greater +- Requires Deno 2.x (tested on recent versions in CI) ### Install It is recommended to use `deno install` to install the tool, which provides a convenient entrypoint script and aliases the permission flags. -``` -deno install --global --allow-read --allow-write --allow-run -f --name dnit https://deno.land/x/dnit@dnit-v1.14.4/main.ts -``` - Install from source checkout: ``` -deno install --global --allow-read --allow-write --allow-run -f --name dnit ./main.ts +deno install --global --allow-read --allow-write --allow-run -f --name dnit --config deno.json ./main.ts ``` +(Install instructions from JSR will be added pending final release) + - Read, Write and Run permissions are required in order to operate on files and execute tasks. +## Example + +See the [example/](./example/) directory for a complete working hello world +example. + ## Sample Usage ```ts -import { - file, - main, - task, -} from "https://deno.land/x/dnit@dnit-v1.14.4/dnit.ts"; +import { main, task, trackFile } from "jsr:@dnit/dnit@2.0.0"; /// A file to be tracked as a target and dependency: -export const msg = file({ +export const msg = trackFile({ path: "./msg.txt", }); @@ -48,12 +47,12 @@ export const helloWorld = task({ name: "helloWorld", description: "foo", action: async () => { /// Actions are typescript async ()=> Promise functions. - await Deno.run({ - cmd: ["./writeMsg.sh"], - }).status(); + const command = new Deno.Command("./writeMsg.sh"); + const { code } = await command.output(); + if (code !== 0) throw new Error(`Command failed with code ${code}`); }, deps: [ - file({ + trackFile({ path: "./writeMsg.sh", }), ], @@ -112,7 +111,7 @@ In verbose mode the tool logs to stderr (fd #2) ## Tasks and Files in Detail Files are tracked by the exported -`export function file(fileParams: FileParams) : TrackedFile` +`export function trackFile(fileParams: FileParams) : TrackedFile` ```ts /** User params for a tracked file */ @@ -122,7 +121,10 @@ export type FileParams = { /// Optional function for how to hash the file. Defaults to the sha1 hash of the file contents. /// A file is out of date if the file timestamp and the hash are different than that in the task manifest - gethash?: GetFileHash; + getHash?: GetFileHash; + + /// Optional function for how to get the file timestamp. Defaults to the actual file timestamp + getTimestamp?: GetFileTimestamp; }; ``` @@ -134,7 +136,7 @@ Tasks are created by the exported `function task(taskParams: TaskParams): Task` /** User definition of a task */ export type TaskParams = { /// Name: (string) - The key used to initiate a task - name: A.TaskName; + name: TaskName; /// Description (string) - Freeform text description shown on help description?: string; @@ -142,14 +144,8 @@ export type TaskParams = { /// Action executed on execution of the task (async or sync) action: Action; - /// Optional list of explicit task dependencies - task_deps?: Task[]; - - /// Optional list of explicit file dependencies - file_deps?: TrackedFile[]; - /// Optional list of task or file dependencies - deps?: (Task | TrackedFile)[]; + deps?: Dep[]; /// Targets (files which will be produced by execution of this task) targets?: TrackedFile[]; @@ -157,15 +153,18 @@ export type TaskParams = { /// Custom up-to-date definition - Can be used to make a task *less* up to date. Eg; use uptodate: runAlways to run always on request regardless of dependencies being up to date. uptodate?: IsUpToDate; }; + +/// The kinds of supported dependencies. +export type Dep = Task | TrackedFile | TrackedFilesAsync; ``` Tasks are passed to the exported -`export async function exec(cliArgs: string[], tasks: Task[]) : Promise` -This exposes the tasks for execution by the CLI and executes them according to -the `cliArgs` passed in. +`export function main(cliArgs: string[], tasks: Task[]) : Promise` This +exposes the tasks for execution by the CLI and executes them according to the +`cliArgs` passed in. ```ts -exec(Deno.args, tasks); +main(Deno.args, tasks); ``` ## Larger Scale use of tasks @@ -180,8 +179,10 @@ definitions across projects. place to have a (deno) typescript tree for the task scripting, which encourages tasks to be separated into modules and generally organised as a typescript project tree. -- User scripts can have an `import_map.json` file in order to import tasks and - utils more flexibly. +- User scripts can use a `deno.json` file in the `dnit` directory for + configuration (import maps, TypeScript options, etc). For legacy + compatibility, standalone `import_map.json` or `.import_map.json` files are + also supported. - The main `dnit` tool can be executed on its own (see section on [Installation](#Installation) above) @@ -195,8 +196,10 @@ The `dnit` tool searches for a user script to execute, in order to support the - It starts from the current working directory and runs `findUserSource` - `findUserSource` looks for subdirectory `dnit` and looks for sources `main.ts` or `dnit.ts` - - It optionally looks for `import_map.json` or `.import_map.json` to use as - the import map. + - Deno will automatically discover and use any `deno.json` file in the `dnit` + directory or parent directories + - For legacy compatibility, it also looks for `import_map.json` or + `.import_map.json` to use as the import map - If found then it changes working directory and executes the user script. - If not found then it recurses into `findUserSource` in the parent directory. @@ -206,7 +209,7 @@ Eg: with a file layout: repo dnit main.ts - import_map.json + deno.json src project.ts package.json diff --git a/adl-gen/.manifest b/adl-gen/.manifest deleted file mode 100644 index a9d3a0b..0000000 --- a/adl-gen/.manifest +++ /dev/null @@ -1,11 +0,0 @@ -# manifest @generated by the adl compiler -dnit/manifest.ts -resolver.ts -runtime/adl.ts -runtime/dynamic.ts -runtime/json.ts -runtime/sys/adlast.ts -runtime/sys/dynamic.ts -runtime/sys/types.ts -runtime/utils.ts -sys/types.ts diff --git a/adl-gen/dnit/manifest.ts b/adl-gen/dnit/manifest.ts deleted file mode 100644 index 36823c6..0000000 --- a/adl-gen/dnit/manifest.ts +++ /dev/null @@ -1,395 +0,0 @@ -/* @generated from adl module dnit.manifest */ -// deno-lint-ignore-file - -import type * as ADL from "./../runtime/adl.ts"; -import type * as sys_types from "./../sys/types.ts"; - -export type TaskName = ADL.Flavored0; - -const TaskName_AST: ADL.ScopedDecl = { - "moduleName": "dnit.manifest", - "decl": { - "annotations": [], - "type_": { - "kind": "newtype_", - "value": { - "typeParams": [], - "default": { "kind": "nothing" }, - "typeExpr": { - "typeRef": { "kind": "primitive", "value": "String" }, - "parameters": [], - }, - }, - }, - "name": "TaskName", - "version": { "kind": "nothing" }, - }, -}; - -export const snTaskName: ADL.ScopedName = { - moduleName: "dnit.manifest", - name: "TaskName", -}; - -export function texprTaskName(): ADL.ATypeExpr { - return { - value: { - typeRef: { kind: "reference", value: snTaskName }, - parameters: [], - }, - }; -} - -export type TrackedFileName = ADL.Flavored0; - -const TrackedFileName_AST: ADL.ScopedDecl = { - "moduleName": "dnit.manifest", - "decl": { - "annotations": [], - "type_": { - "kind": "newtype_", - "value": { - "typeParams": [], - "default": { "kind": "nothing" }, - "typeExpr": { - "typeRef": { "kind": "primitive", "value": "String" }, - "parameters": [], - }, - }, - }, - "name": "TrackedFileName", - "version": { "kind": "nothing" }, - }, -}; - -export const snTrackedFileName: ADL.ScopedName = { - moduleName: "dnit.manifest", - name: "TrackedFileName", -}; - -export function texprTrackedFileName(): ADL.ATypeExpr { - return { - value: { - typeRef: { kind: "reference", value: snTrackedFileName }, - parameters: [], - }, - }; -} - -export type TrackedFileHash = ADL.Flavored0; - -const TrackedFileHash_AST: ADL.ScopedDecl = { - "moduleName": "dnit.manifest", - "decl": { - "annotations": [], - "type_": { - "kind": "newtype_", - "value": { - "typeParams": [], - "default": { "kind": "nothing" }, - "typeExpr": { - "typeRef": { "kind": "primitive", "value": "String" }, - "parameters": [], - }, - }, - }, - "name": "TrackedFileHash", - "version": { "kind": "nothing" }, - }, -}; - -export const snTrackedFileHash: ADL.ScopedName = { - moduleName: "dnit.manifest", - name: "TrackedFileHash", -}; - -export function texprTrackedFileHash(): ADL.ATypeExpr { - return { - value: { - typeRef: { kind: "reference", value: snTrackedFileHash }, - parameters: [], - }, - }; -} - -export type Timestamp = ADL.Flavored0; - -const Timestamp_AST: ADL.ScopedDecl = { - "moduleName": "dnit.manifest", - "decl": { - "annotations": [], - "type_": { - "kind": "newtype_", - "value": { - "typeParams": [], - "default": { "kind": "nothing" }, - "typeExpr": { - "typeRef": { "kind": "primitive", "value": "String" }, - "parameters": [], - }, - }, - }, - "name": "Timestamp", - "version": { "kind": "nothing" }, - }, -}; - -export const snTimestamp: ADL.ScopedName = { - moduleName: "dnit.manifest", - name: "Timestamp", -}; - -export function texprTimestamp(): ADL.ATypeExpr { - return { - value: { - typeRef: { kind: "reference", value: snTimestamp }, - parameters: [], - }, - }; -} - -export interface TaskData { - lastExecution: Timestamp | null; - trackedFiles: sys_types.Map; -} - -export function makeTaskData( - input: { - lastExecution?: Timestamp | null; - trackedFiles: sys_types.Map; - }, -): TaskData { - return { - lastExecution: input.lastExecution === undefined - ? null - : input.lastExecution, - trackedFiles: input.trackedFiles, - }; -} - -const TaskData_AST: ADL.ScopedDecl = { - "moduleName": "dnit.manifest", - "decl": { - "annotations": [], - "type_": { - "kind": "struct_", - "value": { - "typeParams": [], - "fields": [{ - "annotations": [], - "serializedName": "lastExecution", - "default": { "kind": "just", "value": null }, - "name": "lastExecution", - "typeExpr": { - "typeRef": { "kind": "primitive", "value": "Nullable" }, - "parameters": [{ - "typeRef": { - "kind": "reference", - "value": { "moduleName": "dnit.manifest", "name": "Timestamp" }, - }, - "parameters": [], - }], - }, - }, { - "annotations": [], - "serializedName": "trackedFiles", - "default": { "kind": "nothing" }, - "name": "trackedFiles", - "typeExpr": { - "typeRef": { - "kind": "reference", - "value": { "moduleName": "sys.types", "name": "Map" }, - }, - "parameters": [{ - "typeRef": { - "kind": "reference", - "value": { - "moduleName": "dnit.manifest", - "name": "TrackedFileName", - }, - }, - "parameters": [], - }, { - "typeRef": { - "kind": "reference", - "value": { - "moduleName": "dnit.manifest", - "name": "TrackedFileData", - }, - }, - "parameters": [], - }], - }, - }], - }, - }, - "name": "TaskData", - "version": { "kind": "nothing" }, - }, -}; - -export const snTaskData: ADL.ScopedName = { - moduleName: "dnit.manifest", - name: "TaskData", -}; - -export function texprTaskData(): ADL.ATypeExpr { - return { - value: { - typeRef: { kind: "reference", value: snTaskData }, - parameters: [], - }, - }; -} - -export interface TrackedFileData { - hash: TrackedFileHash; - timestamp: Timestamp; -} - -export function makeTrackedFileData( - input: { - hash: TrackedFileHash; - timestamp: Timestamp; - }, -): TrackedFileData { - return { - hash: input.hash, - timestamp: input.timestamp, - }; -} - -const TrackedFileData_AST: ADL.ScopedDecl = { - "moduleName": "dnit.manifest", - "decl": { - "annotations": [], - "type_": { - "kind": "struct_", - "value": { - "typeParams": [], - "fields": [{ - "annotations": [], - "serializedName": "hash", - "default": { "kind": "nothing" }, - "name": "hash", - "typeExpr": { - "typeRef": { - "kind": "reference", - "value": { - "moduleName": "dnit.manifest", - "name": "TrackedFileHash", - }, - }, - "parameters": [], - }, - }, { - "annotations": [], - "serializedName": "timestamp", - "default": { "kind": "nothing" }, - "name": "timestamp", - "typeExpr": { - "typeRef": { - "kind": "reference", - "value": { "moduleName": "dnit.manifest", "name": "Timestamp" }, - }, - "parameters": [], - }, - }], - }, - }, - "name": "TrackedFileData", - "version": { "kind": "nothing" }, - }, -}; - -export const snTrackedFileData: ADL.ScopedName = { - moduleName: "dnit.manifest", - name: "TrackedFileData", -}; - -export function texprTrackedFileData(): ADL.ATypeExpr { - return { - value: { - typeRef: { kind: "reference", value: snTrackedFileData }, - parameters: [], - }, - }; -} - -export interface Manifest { - tasks: sys_types.Map; -} - -export function makeManifest( - input: { - tasks?: sys_types.Map; - }, -): Manifest { - return { - tasks: input.tasks === undefined ? [] : input.tasks, - }; -} - -const Manifest_AST: ADL.ScopedDecl = { - "moduleName": "dnit.manifest", - "decl": { - "annotations": [], - "type_": { - "kind": "struct_", - "value": { - "typeParams": [], - "fields": [{ - "annotations": [], - "serializedName": "tasks", - "default": { "kind": "just", "value": [] }, - "name": "tasks", - "typeExpr": { - "typeRef": { - "kind": "reference", - "value": { "moduleName": "sys.types", "name": "Map" }, - }, - "parameters": [{ - "typeRef": { - "kind": "reference", - "value": { "moduleName": "dnit.manifest", "name": "TaskName" }, - }, - "parameters": [], - }, { - "typeRef": { - "kind": "reference", - "value": { "moduleName": "dnit.manifest", "name": "TaskData" }, - }, - "parameters": [], - }], - }, - }], - }, - }, - "name": "Manifest", - "version": { "kind": "nothing" }, - }, -}; - -export const snManifest: ADL.ScopedName = { - moduleName: "dnit.manifest", - name: "Manifest", -}; - -export function texprManifest(): ADL.ATypeExpr { - return { - value: { - typeRef: { kind: "reference", value: snManifest }, - parameters: [], - }, - }; -} - -export const _AST_MAP: { [key: string]: ADL.ScopedDecl } = { - "dnit.manifest.TaskName": TaskName_AST, - "dnit.manifest.TrackedFileName": TrackedFileName_AST, - "dnit.manifest.TrackedFileHash": TrackedFileHash_AST, - "dnit.manifest.Timestamp": Timestamp_AST, - "dnit.manifest.TaskData": TaskData_AST, - "dnit.manifest.TrackedFileData": TrackedFileData_AST, - "dnit.manifest.Manifest": Manifest_AST, -}; diff --git a/adl-gen/resolver.ts b/adl-gen/resolver.ts deleted file mode 100644 index 2015aa7..0000000 --- a/adl-gen/resolver.ts +++ /dev/null @@ -1,12 +0,0 @@ -// deno-lint-ignore-file -/* @generated from adl */ -import { declResolver, ScopedDecl } from "./runtime/adl.ts"; -import { _AST_MAP as dnit_manifest } from "./dnit/manifest.ts"; -import { _AST_MAP as sys_types } from "./sys/types.ts"; - -export const ADL: { [key: string]: ScopedDecl } = { - ...dnit_manifest, - ...sys_types, -}; - -export const RESOLVER = declResolver(ADL); diff --git a/adl-gen/runtime/adl.ts b/adl-gen/runtime/adl.ts deleted file mode 100644 index 405aa37..0000000 --- a/adl-gen/runtime/adl.ts +++ /dev/null @@ -1,129 +0,0 @@ -//deno-lint-ignore-file -import type * as AST from "./sys/adlast.ts"; -import type * as utils from "./utils.ts"; - -export type ScopedName = AST.ScopedName; -export type ScopedDecl = AST.ScopedDecl; -export type ATypeRef<_T> = { value: AST.TypeRef }; -export type ATypeExpr<_T> = { value: AST.TypeExpr }; - -/** - * A function to obtain details on a declared type. - */ -export interface DeclResolver { - (decl: AST.ScopedName): AST.ScopedDecl; -} - -export function declResolver( - ...astMaps: ({ [key: string]: AST.ScopedDecl })[] -) { - const astMap: { [key: string]: AST.ScopedDecl } = {}; - for (let map of astMaps) { - for (let scopedName in map) { - astMap[scopedName] = map[scopedName]; - } - } - - function resolver(scopedName: AST.ScopedName): AST.ScopedDecl { - const scopedNameStr = scopedName.moduleName + "." + scopedName.name; - const result = astMap[scopedNameStr]; - if (result === undefined) { - throw new Error("Unable to resolve ADL type " + scopedNameStr); - } - return result; - } - - return resolver; -} - -type Unknown = {} | null; -type Json = {} | null; - -/* Type expressions for primitive types */ - -function texprPrimitive(ptype: string): ATypeExpr { - return { - value: { - typeRef: { kind: "primitive", value: ptype }, - parameters: [], - }, - }; -} - -function texprPrimitive1( - ptype: string, - etype: ATypeExpr, -): ATypeExpr { - return { - value: { - typeRef: { kind: "primitive", value: ptype }, - parameters: [etype.value], - }, - }; -} - -export function texprVoid(): ATypeExpr { - return texprPrimitive("Void"); -} -export function texprBool(): ATypeExpr { - return texprPrimitive("Bool"); -} -export function texprInt8(): ATypeExpr { - return texprPrimitive("Int8"); -} -export function texprInt16(): ATypeExpr { - return texprPrimitive("Int16"); -} -export function texprInt32(): ATypeExpr { - return texprPrimitive("Int32"); -} -export function texprInt64(): ATypeExpr { - return texprPrimitive("Int64"); -} -export function texprWord8(): ATypeExpr { - return texprPrimitive("Word8"); -} -export function texprWord16(): ATypeExpr { - return texprPrimitive("Word16"); -} -export function texprWord32(): ATypeExpr { - return texprPrimitive("Word32"); -} -export function texprWord64(): ATypeExpr { - return texprPrimitive("Word64"); -} -export function texprFloat(): ATypeExpr { - return texprPrimitive("Float"); -} -export function texprDouble(): ATypeExpr { - return texprPrimitive("Double"); -} -export function texprJson(): ATypeExpr { - return texprPrimitive("Json"); -} -export function texprByteVector(): ATypeExpr { - return texprPrimitive("ByteVector"); -} -export function texprString(): ATypeExpr { - return texprPrimitive("String"); -} - -export function texprVector(etype: ATypeExpr): ATypeExpr { - return texprPrimitive1("Vector", etype); -} - -export function texprStringMap( - etype: ATypeExpr, -): ATypeExpr<{ [key: string]: T }> { - return texprPrimitive1("StringMap", etype); -} - -export function texprNullable(etype: ATypeExpr): ATypeExpr { - return texprPrimitive1("Nullable", etype); -} -// "Flavoured" nominal typing. -// https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/ -export type Flavored0 = utils.Flavored0; -export type Flavored1 = utils.Flavored1; -export type Flavored2 = utils.Flavored2; -export type Flavored3 = utils.Flavored3; diff --git a/adl-gen/runtime/dynamic.ts b/adl-gen/runtime/dynamic.ts deleted file mode 100644 index db880ba..0000000 --- a/adl-gen/runtime/dynamic.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { typeExprsEqual } from "./utils.ts"; -import { JsonBinding } from "./json.ts"; -import { Dynamic } from "./sys/dynamic.ts"; - -/** - * Convert an ADL value to a dynamically typed value - */ -export function toDynamic(jsonBinding: JsonBinding, value: T): Dynamic { - return { typeExpr: jsonBinding.typeExpr, value: jsonBinding.toJson(value) }; -} - -/** - * Convert an ADL value to a dynamically typed value - */ -export function fromDynamic( - jsonBinding: JsonBinding, - dynamic: Dynamic, -): T | null { - if (typeExprsEqual(jsonBinding.typeExpr, dynamic.typeExpr)) { - return jsonBinding.fromJson(dynamic.value); - } - return null; -} diff --git a/adl-gen/runtime/json.ts b/adl-gen/runtime/json.ts deleted file mode 100644 index 1854f5c..0000000 --- a/adl-gen/runtime/json.ts +++ /dev/null @@ -1,671 +0,0 @@ -// deno-lint-ignore-file - -import type { ATypeExpr, DeclResolver } from "./adl.ts"; -import type * as AST from "./sys/adlast.ts"; -//import * as b64 from 'base64-js'; -import { isEnum, isVoid, scopedNamesEqual } from "./utils.ts"; - -/** A type for json serialised values */ - -export type Json = {} | null; -export type JsonObject = { [member: string]: Json }; -export type JsonArray = Json[]; - -function asJsonObject(jv: Json): JsonObject | undefined { - if (jv instanceof Object && !(jv instanceof Array)) { - return jv as JsonObject; - } - return undefined; -} - -function asJsonArray(jv: Json): JsonArray | undefined { - if (jv instanceof Array) { - return jv as JsonArray; - } - return undefined; -} - -/** A type alias for values of an Unknown type */ -type Unknown = {} | null; - -/** - * A JsonBinding is a de/serialiser for a give ADL type - */ -export interface JsonBinding { - typeExpr: AST.TypeExpr; - - // Convert a value of type T to Json - toJson(t: T): Json; - - // Parse a json blob into a value of type T. Throws - // JsonParseExceptions on failure. - fromJson(json: Json): T; - - // Variant of fromJson that throws Errors on failure - fromJsonE(json: Json): T; -} /** - * Construct a JsonBinding for an arbitrary type expression - */ - -export function createJsonBinding( - dresolver: DeclResolver, - texpr: ATypeExpr, -): JsonBinding { - const jb0 = buildJsonBinding(dresolver, texpr.value, {}) as JsonBinding0; - function fromJsonE(json: Json): T { - try { - return jb0.fromJson(json); - } catch (e) { - throw mapJsonException(e); - } - } - return { - typeExpr: texpr.value, - toJson: jb0.toJson, - fromJson: jb0.fromJson, - fromJsonE, - }; -} /** - * Interface for json parsing exceptions. - * Any implementation should properly show the parse error tree. - * - * @interface JsonParseException - */ - -export interface JsonParseException { - kind: "JsonParseException"; - getMessage(): string; - pushField(fieldName: string): void; - pushIndex(index: number): void; - toString(): string; -} - -// Map a JsonException to an Error value -export function mapJsonException(exception: {}): {} { - if ( - exception && (exception as { kind: string })["kind"] == "JsonParseException" - ) { - const jserr: JsonParseException = exception as JsonParseException; - return new Error(jserr.getMessage()); - } else { - return exception; - } -} - -/** Convenience function for generating a json parse exception. - * @param {string} message - Exception message. - */ -export function jsonParseException(message: string): JsonParseException { - const context: string[] = []; - let createContextString: () => string = () => { - const rcontext: string[] = context.slice(0); - rcontext.push("$"); - rcontext.reverse(); - return rcontext.join("."); - }; - return { - kind: "JsonParseException", - getMessage(): string { - return message + " at " + createContextString(); - }, - pushField(fieldName: string): void { - context.push(fieldName); - }, - pushIndex(index: number): void { - context.push("[" + index + "]"); - }, - toString(): string { - return this.getMessage(); - }, - }; -} - -/** - * Check if a javascript error is of the json parse exception type. - * @param exception The exception to check. - */ -export function isJsonParseException( - exception: {}, -): exception is JsonParseException { - return ( exception).kind === "JsonParseException"; -} - -interface JsonBinding0 { - toJson(t: T): Json; - fromJson(json: Json): T; -} - -interface BoundTypeParams { - [key: string]: JsonBinding0; -} - -function buildJsonBinding( - dresolver: DeclResolver, - texpr: AST.TypeExpr, - boundTypeParams: BoundTypeParams, -): JsonBinding0 { - if (texpr.typeRef.kind === "primitive") { - return primitiveJsonBinding( - dresolver, - texpr.typeRef.value, - texpr.parameters, - boundTypeParams, - ); - } else if (texpr.typeRef.kind === "reference") { - const ast = dresolver(texpr.typeRef.value); - if (ast.decl.type_.kind === "struct_") { - return structJsonBinding( - dresolver, - ast.decl.type_.value, - texpr.parameters, - boundTypeParams, - ); - } else if (ast.decl.type_.kind === "union_") { - const union = ast.decl.type_.value; - if (isEnum(union)) { - return enumJsonBinding( - dresolver, - union, - texpr.parameters, - boundTypeParams, - ); - } else { - return unionJsonBinding( - dresolver, - union, - texpr.parameters, - boundTypeParams, - ); - } - } else if (ast.decl.type_.kind === "newtype_") { - return newtypeJsonBinding( - dresolver, - ast.decl.type_.value, - texpr.parameters, - boundTypeParams, - ); - } else if (ast.decl.type_.kind === "type_") { - return typedefJsonBinding( - dresolver, - ast.decl.type_.value, - texpr.parameters, - boundTypeParams, - ); - } - } else if (texpr.typeRef.kind === "typeParam") { - return boundTypeParams[texpr.typeRef.value]; - } - throw new Error("buildJsonBinding : unimplemented ADL type"); -} - -function primitiveJsonBinding( - dresolver: DeclResolver, - ptype: string, - params: AST.TypeExpr[], - boundTypeParams: BoundTypeParams, -): JsonBinding0 { - if (ptype === "String") { - return identityJsonBinding("a string", (v) => typeof (v) === "string"); - } else if (ptype === "Int8") { - return identityJsonBinding("a number", (v) => typeof (v) === "number"); - } else if (ptype === "Void") { - return identityJsonBinding("a null", (v) => v === null); - } else if (ptype === "Bool") { - return identityJsonBinding("a bool", (v) => typeof (v) === "boolean"); - } else if (ptype === "Int8") { - return identityJsonBinding("a number", (v) => typeof (v) === "number"); - } else if (ptype === "Int16") { - return identityJsonBinding("a number", (v) => typeof (v) === "number"); - } else if (ptype === "Int32") { - return identityJsonBinding("a number", (v) => typeof (v) === "number"); - } else if (ptype === "Int64") { - return identityJsonBinding("a number", (v) => typeof (v) === "number"); - } else if (ptype === "Word8") { - return identityJsonBinding("a number", (v) => typeof (v) === "number"); - } else if (ptype === "Word16") { - return identityJsonBinding("a number", (v) => typeof (v) === "number"); - } else if (ptype === "Word32") { - return identityJsonBinding("a number", (v) => typeof (v) === "number"); - } else if (ptype === "Word64") { - return identityJsonBinding("a number", (v) => typeof (v) === "number"); - } else if (ptype === "Float") { - return identityJsonBinding("a number", (v) => typeof (v) === "number"); - } else if (ptype === "Double") { - return identityJsonBinding("a number", (v) => typeof (v) === "number"); - } else if (ptype === "Json") { - return identityJsonBinding("a json value", (_v) => true); - } else if (ptype === "Bytes") return bytesJsonBinding(); - else if (ptype === "Vector") { - return vectorJsonBinding(dresolver, params[0], boundTypeParams); - } else if (ptype === "StringMap") { - return stringMapJsonBinding(dresolver, params[0], boundTypeParams); - } else if (ptype === "Nullable") { - return nullableJsonBinding(dresolver, params[0], boundTypeParams); - } else throw new Error("Unimplemented json binding for primitive " + ptype); -} - -function identityJsonBinding( - expected: string, - predicate: (json: Json) => boolean, -): JsonBinding0 { - function toJson(v: T): Json { - return (v as Unknown as Json); - } - - function fromJson(json: Json): T { - if (!predicate(json)) { - throw jsonParseException("expected " + expected); - } - return json as Unknown as T; - } - - return { toJson, fromJson }; -} - -function bytesJsonBinding(): JsonBinding0 { - function toJson(v: Uint8Array): Json { - //return b64.fromByteArray(v); - throw new Error("bytesJsonBinding not implemented"); - } - - function fromJson(json: Json): Uint8Array { - if (typeof (json) != "string") { - throw jsonParseException("expected a string"); - } - //return b64.toByteArray(json); - throw new Error("bytesJsonBinding not implemented"); - } - - return { toJson, fromJson }; -} - -function vectorJsonBinding( - dresolver: DeclResolver, - texpr: AST.TypeExpr, - boundTypeParams: BoundTypeParams, -): JsonBinding0 { - const elementBinding = once(() => - buildJsonBinding(dresolver, texpr, boundTypeParams) - ); - - function toJson(v: Unknown[]): Json { - return v.map(elementBinding().toJson); - } - - function fromJson(json: Json): Unknown[] { - const jarr = asJsonArray(json); - if (jarr == undefined) { - throw jsonParseException("expected an array"); - } - let result: Unknown[] = []; - jarr.forEach((eljson: Json, i: number) => { - try { - result.push(elementBinding().fromJson(eljson)); - } catch (e) { - if (isJsonParseException(e)) { - e.pushIndex(i); - } - throw e; - } - }); - return result; - } - - return { toJson, fromJson }; -} - -type StringMap = { [key: string]: T }; - -function stringMapJsonBinding( - dresolver: DeclResolver, - texpr: AST.TypeExpr, - boundTypeParams: BoundTypeParams, -): JsonBinding0> { - const elementBinding = once(() => - buildJsonBinding(dresolver, texpr, boundTypeParams) - ); - - function toJson(v: StringMap): Json { - const result: JsonObject = {}; - for (let k in v) { - result[k] = elementBinding().toJson(v[k]); - } - return result; - } - - function fromJson(json: Json): StringMap { - const jobj = asJsonObject(json); - if (!jobj) { - throw jsonParseException("expected an object"); - } - let result: JsonObject = {}; - for (let k in jobj) { - try { - result[k] = elementBinding().fromJson(jobj[k]); - } catch (e) { - if (isJsonParseException(e)) { - e.pushField(k); - } - } - } - return result; - } - - return { toJson, fromJson }; -} - -function nullableJsonBinding( - dresolver: DeclResolver, - texpr: AST.TypeExpr, - boundTypeParams: BoundTypeParams, -): JsonBinding0 { - const elementBinding = once(() => - buildJsonBinding(dresolver, texpr, boundTypeParams) - ); - - function toJson(v: Unknown): Json { - if (v === null) { - return null; - } - return elementBinding().toJson(v); - } - - function fromJson(json: Json): Unknown { - if (json === null) { - return null; - } - return elementBinding().fromJson(json); - } - - return { toJson, fromJson }; -} - -interface StructFieldDetails { - field: AST.Field; - jsonBinding: () => JsonBinding0; - buildDefault: () => { value: Unknown } | null; -} - -function structJsonBinding( - dresolver: DeclResolver, - struct: AST.Struct, - params: AST.TypeExpr[], - boundTypeParams: BoundTypeParams, -): JsonBinding0 { - const newBoundTypeParams = createBoundTypeParams( - dresolver, - struct.typeParams, - params, - boundTypeParams, - ); - const fieldDetails: StructFieldDetails[] = []; - struct.fields.forEach((field) => { - let buildDefault = once(() => { - if (field.default.kind === "just") { - const json = field.default.value; - return { - "value": buildJsonBinding( - dresolver, - field.typeExpr, - newBoundTypeParams, - ).fromJson(json), - }; - } else { - return null; - } - }); - - fieldDetails.push({ - field: field, - jsonBinding: once(() => - buildJsonBinding(dresolver, field.typeExpr, newBoundTypeParams) - ), - buildDefault: buildDefault, - }); - }); - - function toJson(v0: Unknown): Json { - const v = v0 as { [key: string]: Unknown }; - const json: JsonObject = {}; - fieldDetails.forEach((fd) => { - json[fd.field.serializedName] = fd.jsonBinding().toJson( - v && v[fd.field.name], - ); - }); - return json; - } - - function fromJson(json: Json): Unknown { - const jobj = asJsonObject(json); - if (!jobj) { - throw jsonParseException("expected an object"); - } - - const v: { [member: string]: Unknown } = {}; - fieldDetails.forEach((fd) => { - if (jobj[fd.field.serializedName] === undefined) { - const defaultv = fd.buildDefault(); - if (defaultv === null) { - throw jsonParseException( - "missing struct field " + fd.field.serializedName, - ); - } else { - v[fd.field.name] = defaultv.value; - } - } else { - try { - v[fd.field.name] = fd.jsonBinding().fromJson( - jobj[fd.field.serializedName], - ); - } catch (e) { - if (isJsonParseException(e)) { - e.pushField(fd.field.serializedName); - } - throw e; - } - } - }); - return v; - } - - return { toJson, fromJson }; -} - -function enumJsonBinding( - _dresolver: DeclResolver, - union: AST.Union, - _params: AST.TypeExpr[], - _boundTypeParams: BoundTypeParams, -): JsonBinding0 { - const fieldSerializedNames: string[] = []; - const fieldNumbers: { [key: string]: number } = {}; - union.fields.forEach((field, i) => { - fieldSerializedNames.push(field.serializedName); - fieldNumbers[field.serializedName] = i; - }); - - function toJson(v: Unknown): Json { - return fieldSerializedNames[v as number]; - } - - function fromJson(json: Json): Unknown { - if (typeof (json) !== "string") { - throw jsonParseException("expected a string for enum"); - } - const result = fieldNumbers[json as string]; - if (result === undefined) { - throw jsonParseException("invalid string for enum: " + json); - } - return result; - } - - return { toJson, fromJson }; -} - -interface FieldDetails { - field: AST.Field; - isVoid: boolean; - jsonBinding: () => JsonBinding0; -} - -function unionJsonBinding( - dresolver: DeclResolver, - union: AST.Union, - params: AST.TypeExpr[], - boundTypeParams: BoundTypeParams, -): JsonBinding0 { - const newBoundTypeParams = createBoundTypeParams( - dresolver, - union.typeParams, - params, - boundTypeParams, - ); - const detailsByName: { [key: string]: FieldDetails } = {}; - const detailsBySerializedName: { [key: string]: FieldDetails } = {}; - union.fields.forEach((field) => { - const details = { - field: field, - isVoid: isVoid(field.typeExpr), - jsonBinding: once(() => - buildJsonBinding(dresolver, field.typeExpr, newBoundTypeParams) - ), - }; - detailsByName[field.name] = details; - detailsBySerializedName[field.serializedName] = details; - }); - - function toJson(v0: Unknown): Json { - const v = v0 as { kind: string; value: Unknown }; - const details = detailsByName[v.kind]; - if (details.isVoid) { - return details.field.serializedName; - } else { - const result: JsonObject = {}; - result[details.field.serializedName] = details.jsonBinding().toJson( - v.value, - ); - return result; - } - } - - function lookupDetails(serializedName: string) { - let details = detailsBySerializedName[serializedName]; - if (details === undefined) { - throw jsonParseException("invalid union field " + serializedName); - } - return details; - } - - function fromJson(json: Json): Unknown { - if (typeof (json) === "string") { - let details = lookupDetails(json); - if (!details.isVoid) { - throw jsonParseException( - "union field " + json + "needs an associated value", - ); - } - return { kind: details.field.name }; - } - const jobj = asJsonObject(json); - if (jobj) { - for (let k in jobj) { - let details = lookupDetails(k); - try { - return { - kind: details.field.name, - value: details.jsonBinding().fromJson(jobj[k]), - }; - } catch (e) { - if (isJsonParseException(e)) { - e.pushField(k); - } - throw e; - } - } - throw jsonParseException("union without a property"); - } else { - throw jsonParseException("expected an object or string"); - } - } - - return { toJson, fromJson }; -} - -function newtypeJsonBinding( - dresolver: DeclResolver, - newtype: AST.NewType, - params: AST.TypeExpr[], - boundTypeParams: BoundTypeParams, -): JsonBinding0 { - const newBoundTypeParams = createBoundTypeParams( - dresolver, - newtype.typeParams, - params, - boundTypeParams, - ); - return buildJsonBinding(dresolver, newtype.typeExpr, newBoundTypeParams); -} - -function typedefJsonBinding( - dresolver: DeclResolver, - typedef: AST.TypeDef, - params: AST.TypeExpr[], - boundTypeParams: BoundTypeParams, -): JsonBinding0 { - const newBoundTypeParams = createBoundTypeParams( - dresolver, - typedef.typeParams, - params, - boundTypeParams, - ); - return buildJsonBinding(dresolver, typedef.typeExpr, newBoundTypeParams); -} - -function createBoundTypeParams( - dresolver: DeclResolver, - paramNames: string[], - paramTypes: AST.TypeExpr[], - boundTypeParams: BoundTypeParams, -): BoundTypeParams { - let result: BoundTypeParams = {}; - paramNames.forEach((paramName, i) => { - result[paramName] = buildJsonBinding( - dresolver, - paramTypes[i], - boundTypeParams, - ); - }); - return result; -} - -/** - * Helper function that takes a thunk, and evaluates it only on the first call. Subsequent - * calls return the previous value - */ -function once(run: () => T): () => T { - let result: T | null = null; - return () => { - if (result === null) { - result = run(); - } - return result; - }; -} - -/** - * Get the value of an annotation of type T - */ -export function getAnnotation( - jb: JsonBinding, - annotations: AST.Annotations, -): T | undefined { - if (jb.typeExpr.typeRef.kind != "reference") { - return undefined; - } - const annScopedName: AST.ScopedName = jb.typeExpr.typeRef.value; - const ann = annotations.find((el) => scopedNamesEqual(el.v1, annScopedName)); - if (ann === undefined) { - return undefined; - } - return jb.fromJsonE(ann.v2); -} diff --git a/adl-gen/runtime/sys/adlast.ts b/adl-gen/runtime/sys/adlast.ts deleted file mode 100644 index 31d07ef..0000000 --- a/adl-gen/runtime/sys/adlast.ts +++ /dev/null @@ -1,294 +0,0 @@ -// deno-lint-ignore-file - -/* @generated from adl module sys.adlast */ -import type * as sys_types from "./types.ts"; - -export type ModuleName = string; - -export type Ident = string; - -export type Annotations = sys_types.Map; - -export interface ScopedName { - moduleName: ModuleName; - name: Ident; -} - -export function makeScopedName( - input: { - moduleName: ModuleName; - name: Ident; - }, -): ScopedName { - return { - moduleName: input.moduleName, - name: input.name, - }; -} - -export interface TypeRef_Primitive { - kind: "primitive"; - value: Ident; -} -export interface TypeRef_TypeParam { - kind: "typeParam"; - value: Ident; -} -export interface TypeRef_Reference { - kind: "reference"; - value: ScopedName; -} - -export type TypeRef = TypeRef_Primitive | TypeRef_TypeParam | TypeRef_Reference; - -export interface TypeRefOpts { - primitive: Ident; - typeParam: Ident; - reference: ScopedName; -} - -export function makeTypeRef( - kind: K, - value: TypeRefOpts[K], -) { - return { kind, value }; -} - -export interface TypeExpr { - typeRef: TypeRef; - parameters: TypeExpr[]; -} - -export function makeTypeExpr( - input: { - typeRef: TypeRef; - parameters: TypeExpr[]; - }, -): TypeExpr { - return { - typeRef: input.typeRef, - parameters: input.parameters, - }; -} - -export interface Field { - name: Ident; - serializedName: Ident; - typeExpr: TypeExpr; - default: sys_types.Maybe<{} | null>; - annotations: Annotations; -} - -export function makeField( - input: { - name: Ident; - serializedName: Ident; - typeExpr: TypeExpr; - default: sys_types.Maybe<{} | null>; - annotations: Annotations; - }, -): Field { - return { - name: input.name, - serializedName: input.serializedName, - typeExpr: input.typeExpr, - default: input.default, - annotations: input.annotations, - }; -} - -export interface Struct { - typeParams: Ident[]; - fields: Field[]; -} - -export function makeStruct( - input: { - typeParams: Ident[]; - fields: Field[]; - }, -): Struct { - return { - typeParams: input.typeParams, - fields: input.fields, - }; -} - -export interface Union { - typeParams: Ident[]; - fields: Field[]; -} - -export function makeUnion( - input: { - typeParams: Ident[]; - fields: Field[]; - }, -): Union { - return { - typeParams: input.typeParams, - fields: input.fields, - }; -} - -export interface TypeDef { - typeParams: Ident[]; - typeExpr: TypeExpr; -} - -export function makeTypeDef( - input: { - typeParams: Ident[]; - typeExpr: TypeExpr; - }, -): TypeDef { - return { - typeParams: input.typeParams, - typeExpr: input.typeExpr, - }; -} - -export interface NewType { - typeParams: Ident[]; - typeExpr: TypeExpr; - default: sys_types.Maybe<{} | null>; -} - -export function makeNewType( - input: { - typeParams: Ident[]; - typeExpr: TypeExpr; - default: sys_types.Maybe<{} | null>; - }, -): NewType { - return { - typeParams: input.typeParams, - typeExpr: input.typeExpr, - default: input.default, - }; -} - -export interface DeclType_Struct_ { - kind: "struct_"; - value: Struct; -} -export interface DeclType_Union_ { - kind: "union_"; - value: Union; -} -export interface DeclType_Type_ { - kind: "type_"; - value: TypeDef; -} -export interface DeclType_Newtype_ { - kind: "newtype_"; - value: NewType; -} - -export type DeclType = - | DeclType_Struct_ - | DeclType_Union_ - | DeclType_Type_ - | DeclType_Newtype_; - -export interface DeclTypeOpts { - struct_: Struct; - union_: Union; - type_: TypeDef; - newtype_: NewType; -} - -export function makeDeclType( - kind: K, - value: DeclTypeOpts[K], -) { - return { kind, value }; -} - -export interface Decl { - name: Ident; - version: sys_types.Maybe; - type_: DeclType; - annotations: Annotations; -} - -export function makeDecl( - input: { - name: Ident; - version: sys_types.Maybe; - type_: DeclType; - annotations: Annotations; - }, -): Decl { - return { - name: input.name, - version: input.version, - type_: input.type_, - annotations: input.annotations, - }; -} - -export interface ScopedDecl { - moduleName: ModuleName; - decl: Decl; -} - -export function makeScopedDecl( - input: { - moduleName: ModuleName; - decl: Decl; - }, -): ScopedDecl { - return { - moduleName: input.moduleName, - decl: input.decl, - }; -} - -export type DeclVersions = Decl[]; - -export interface Import_ModuleName { - kind: "moduleName"; - value: ModuleName; -} -export interface Import_ScopedName { - kind: "scopedName"; - value: ScopedName; -} - -export type Import = Import_ModuleName | Import_ScopedName; - -export interface ImportOpts { - moduleName: ModuleName; - scopedName: ScopedName; -} - -export function makeImport( - kind: K, - value: ImportOpts[K], -) { - return { kind, value }; -} - -export interface Module { - name: ModuleName; - imports: Import[]; - decls: { [key: string]: Decl }; - annotations: Annotations; -} - -export function makeModule( - input: { - name: ModuleName; - imports: Import[]; - decls: { [key: string]: Decl }; - annotations: Annotations; - }, -): Module { - return { - name: input.name, - imports: input.imports, - decls: input.decls, - annotations: input.annotations, - }; -} diff --git a/adl-gen/runtime/sys/dynamic.ts b/adl-gen/runtime/sys/dynamic.ts deleted file mode 100644 index 070571b..0000000 --- a/adl-gen/runtime/sys/dynamic.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* @generated from adl module sys.dynamic */ -//deno-lint-ignore-file -import * as sys_adlast from "./adlast.ts"; - -/** - * A serialised value along with its type - */ -export interface Dynamic { - typeExpr: sys_adlast.TypeExpr; - value: {} | null; -} - -export function makeDynamic( - input: { - typeExpr: sys_adlast.TypeExpr; - value: {} | null; - }, -): Dynamic { - return { - typeExpr: input.typeExpr, - value: input.value, - }; -} diff --git a/adl-gen/runtime/sys/types.ts b/adl-gen/runtime/sys/types.ts deleted file mode 100644 index d8cfa44..0000000 --- a/adl-gen/runtime/sys/types.ts +++ /dev/null @@ -1,108 +0,0 @@ -// deno-lint-ignore-file - -/* @generated from adl module sys.types */ -export interface Pair { - v1: T1; - v2: T2; -} - -export function makePair( - input: { - v1: T1; - v2: T2; - }, -): Pair { - return { - v1: input.v1, - v2: input.v2, - }; -} - -export interface Either_Left { - kind: "left"; - value: T1; -} -export interface Either_Right<_T1, T2> { - kind: "right"; - value: T2; -} - -export type Either = Either_Left | Either_Right; - -export interface EitherOpts { - left: T1; - right: T2; -} - -export function makeEither>( - kind: K, - value: EitherOpts[K], -) { - return { kind, value }; -} - -export interface Maybe_Nothing<_T> { - kind: "nothing"; -} -export interface Maybe_Just { - kind: "just"; - value: T; -} - -export type Maybe = Maybe_Nothing | Maybe_Just; - -export interface MaybeOpts { - nothing: null; - just: T; -} - -export function makeMaybe>( - kind: K, - value: MaybeOpts[K], -) { - return { kind, value }; -} - -export interface Error_Value { - kind: "value"; - value: T; -} -export interface Error_Error<_T> { - kind: "error"; - value: string; -} - -export type Error = Error_Value | Error_Error; - -export interface ErrorOpts { - value: T; - error: string; -} - -export function makeError>( - kind: K, - value: ErrorOpts[K], -) { - return { kind, value }; -} - -export interface MapEntry { - key: K; - value: V; -} - -export function makeMapEntry( - input: { - key: K; - value: V; - }, -): MapEntry { - return { - key: input.key, - value: input.value, - }; -} - -export type Map = Pair[]; - -export type Set = T[]; diff --git a/adl-gen/runtime/utils.ts b/adl-gen/runtime/utils.ts deleted file mode 100644 index e61e70b..0000000 --- a/adl-gen/runtime/utils.ts +++ /dev/null @@ -1,123 +0,0 @@ -// deno-lint-ignore-file -import type * as AST from "./sys/adlast.ts"; - -export function isEnum(union: AST.Union): boolean { - for (let field of union.fields) { - if (!isVoid(field.typeExpr)) { - return false; - } - } - return true; -} - -export function isVoid(texpr: AST.TypeExpr): boolean { - if (texpr.typeRef.kind === "primitive") { - return texpr.typeRef.value === "Void"; - } - return false; -} - -export function typeExprsEqual( - texpr1: AST.TypeExpr, - texpr2: AST.TypeExpr, -): boolean { - if (!typeRefsEqual(texpr1.typeRef, texpr2.typeRef)) { - return false; - } - if (texpr1.parameters.length != texpr2.parameters.length) { - return false; - } - for (let i = 0; i < texpr1.parameters.length; i++) { - if (!typeExprsEqual(texpr1.parameters[i], texpr2.parameters[i])) { - return false; - } - } - return true; -} - -export function typeRefsEqual(tref1: AST.TypeRef, tref2: AST.TypeRef): boolean { - if (tref1.kind === "primitive" && tref2.kind === "primitive") { - return tref1.value === tref2.value; - } else if (tref1.kind === "typeParam" && tref2.kind === "typeParam") { - return tref1.value === tref2.value; - } else if (tref1.kind === "reference" && tref2.kind === "reference") { - return scopedNamesEqual(tref1.value, tref2.value); - } - return false; -} - -export function scopedNamesEqual( - sn1: AST.ScopedName, - sn2: AST.ScopedName, -): boolean { - return sn1.moduleName === sn2.moduleName && sn1.name === sn2.name; -} - -function typeExprToStringImpl( - te: AST.TypeExpr, - withScopedNames: boolean, -): string { - let result = ""; - if (te.typeRef.kind == "primitive") { - result = te.typeRef.value; - } else if (te.typeRef.kind == "typeParam") { - result = te.typeRef.value; - } else if (te.typeRef.kind == "reference") { - result = withScopedNames - ? te.typeRef.value.moduleName + "." + te.typeRef.value.name - : te.typeRef.value.name; - } - if (te.parameters.length > 0) { - result = result + "<" + te.parameters.map((p) => - typeExprToStringImpl(p, withScopedNames) - ) + ">"; - } - return result; -} - -/* Convert a type expression to a string, with fully scoped names */ - -export function typeExprToString(te: AST.TypeExpr): string { - return typeExprToStringImpl(te, true); -} - -/* Convert a type expression to a string, with unscoped names */ - -export function typeExprToStringUnscoped(te: AST.TypeExpr): string { - return typeExprToStringImpl(te, false); -} - -// "Flavoured" nominal typing. -// https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/ -const symS = Symbol(); -const symT = Symbol(); -const symU = Symbol(); -const symV = Symbol(); - -/// Zero ADL type params - literal string type Name (fully scoped module name) -/// eg for 'newtype X = string' -> 'type X = Flavouring0<"X">;' -type Flavoring0 = { - readonly [symS]?: Name; -}; - -/// 1 ADL type param -/// eg for 'newtype X = string' -> 'type X = Flavouring1<"X",T>;' -type Flavoring1 = Flavoring0 & { - readonly [symT]?: T; -}; - -/// 2 ADL type params -/// eg for 'newtype X = string' -> 'type X = Flavouring2<"X",T,U>;' -type Flavoring2 = Flavoring1 & { - readonly [symU]?: U; -}; - -/// 3 ADL type params -/// eg for 'newtype X = string' -> 'type X = Flavouring3<"X",T,U,V>;' -type Flavoring3 = Flavoring2 & { - readonly [symV]?: V; -}; -export type Flavored0 = A & Flavoring0; -export type Flavored1 = A & Flavoring1; -export type Flavored2 = A & Flavoring2; -export type Flavored3 = A & Flavoring3; diff --git a/adl-gen/sys/types.ts b/adl-gen/sys/types.ts deleted file mode 100644 index 4b3d8bf..0000000 --- a/adl-gen/sys/types.ts +++ /dev/null @@ -1,472 +0,0 @@ -// deno-lint-ignore-file - -/* @generated from adl module sys.types */ - -import type * as ADL from "./../runtime/adl.ts"; - -export interface Pair { - v1: T1; - v2: T2; -} - -export function makePair( - input: { - v1: T1; - v2: T2; - }, -): Pair { - return { - v1: input.v1, - v2: input.v2, - }; -} - -const Pair_AST: ADL.ScopedDecl = { - "moduleName": "sys.types", - "decl": { - "annotations": [], - "type_": { - "kind": "struct_", - "value": { - "typeParams": ["T1", "T2"], - "fields": [{ - "annotations": [], - "serializedName": "v1", - "default": { "kind": "nothing" }, - "name": "v1", - "typeExpr": { - "typeRef": { "kind": "typeParam", "value": "T1" }, - "parameters": [], - }, - }, { - "annotations": [], - "serializedName": "v2", - "default": { "kind": "nothing" }, - "name": "v2", - "typeExpr": { - "typeRef": { "kind": "typeParam", "value": "T2" }, - "parameters": [], - }, - }], - }, - }, - "name": "Pair", - "version": { "kind": "nothing" }, - }, -}; - -export const snPair: ADL.ScopedName = { moduleName: "sys.types", name: "Pair" }; - -export function texprPair( - texprT1: ADL.ATypeExpr, - texprT2: ADL.ATypeExpr, -): ADL.ATypeExpr> { - return { - value: { - typeRef: { - kind: "reference", - value: { moduleName: "sys.types", name: "Pair" }, - }, - parameters: [texprT1.value, texprT2.value], - }, - }; -} - -export interface Either_Left { - kind: "left"; - value: T1; -} -export interface Either_Right<_T1, T2> { - kind: "right"; - value: T2; -} - -export type Either = Either_Left | Either_Right; - -export interface EitherOpts { - left: T1; - right: T2; -} - -export function makeEither>( - kind: K, - value: EitherOpts[K], -) { - return { kind, value }; -} - -const Either_AST: ADL.ScopedDecl = { - "moduleName": "sys.types", - "decl": { - "annotations": [], - "type_": { - "kind": "union_", - "value": { - "typeParams": ["T1", "T2"], - "fields": [{ - "annotations": [], - "serializedName": "left", - "default": { "kind": "nothing" }, - "name": "left", - "typeExpr": { - "typeRef": { "kind": "typeParam", "value": "T1" }, - "parameters": [], - }, - }, { - "annotations": [], - "serializedName": "right", - "default": { "kind": "nothing" }, - "name": "right", - "typeExpr": { - "typeRef": { "kind": "typeParam", "value": "T2" }, - "parameters": [], - }, - }], - }, - }, - "name": "Either", - "version": { "kind": "nothing" }, - }, -}; - -export const snEither: ADL.ScopedName = { - moduleName: "sys.types", - name: "Either", -}; - -export function texprEither( - texprT1: ADL.ATypeExpr, - texprT2: ADL.ATypeExpr, -): ADL.ATypeExpr> { - return { - value: { - typeRef: { - kind: "reference", - value: { moduleName: "sys.types", name: "Either" }, - }, - parameters: [texprT1.value, texprT2.value], - }, - }; -} - -export interface Maybe_Nothing<_T> { - kind: "nothing"; -} -export interface Maybe_Just { - kind: "just"; - value: T; -} - -export type Maybe = Maybe_Nothing | Maybe_Just; - -export interface MaybeOpts { - nothing: null; - just: T; -} - -export function makeMaybe>( - kind: K, - value: MaybeOpts[K], -) { - return { kind, value }; -} - -const Maybe_AST: ADL.ScopedDecl = { - "moduleName": "sys.types", - "decl": { - "annotations": [], - "type_": { - "kind": "union_", - "value": { - "typeParams": ["T"], - "fields": [{ - "annotations": [], - "serializedName": "nothing", - "default": { "kind": "nothing" }, - "name": "nothing", - "typeExpr": { - "typeRef": { "kind": "primitive", "value": "Void" }, - "parameters": [], - }, - }, { - "annotations": [], - "serializedName": "just", - "default": { "kind": "nothing" }, - "name": "just", - "typeExpr": { - "typeRef": { "kind": "typeParam", "value": "T" }, - "parameters": [], - }, - }], - }, - }, - "name": "Maybe", - "version": { "kind": "nothing" }, - }, -}; - -export const snMaybe: ADL.ScopedName = { - moduleName: "sys.types", - name: "Maybe", -}; - -export function texprMaybe( - texprT: ADL.ATypeExpr, -): ADL.ATypeExpr> { - return { - value: { - typeRef: { - kind: "reference", - value: { moduleName: "sys.types", name: "Maybe" }, - }, - parameters: [texprT.value], - }, - }; -} - -export interface Error_Value { - kind: "value"; - value: T; -} -export interface Error_Error<_T> { - kind: "error"; - value: string; -} - -export type Error = Error_Value | Error_Error; - -export interface ErrorOpts { - value: T; - error: string; -} - -export function makeError>( - kind: K, - value: ErrorOpts[K], -) { - return { kind, value }; -} - -const Error_AST: ADL.ScopedDecl = { - "moduleName": "sys.types", - "decl": { - "annotations": [], - "type_": { - "kind": "union_", - "value": { - "typeParams": ["T"], - "fields": [{ - "annotations": [], - "serializedName": "value", - "default": { "kind": "nothing" }, - "name": "value", - "typeExpr": { - "typeRef": { "kind": "typeParam", "value": "T" }, - "parameters": [], - }, - }, { - "annotations": [], - "serializedName": "error", - "default": { "kind": "nothing" }, - "name": "error", - "typeExpr": { - "typeRef": { "kind": "primitive", "value": "String" }, - "parameters": [], - }, - }], - }, - }, - "name": "Error", - "version": { "kind": "nothing" }, - }, -}; - -export const snError: ADL.ScopedName = { - moduleName: "sys.types", - name: "Error", -}; - -export function texprError( - texprT: ADL.ATypeExpr, -): ADL.ATypeExpr> { - return { - value: { - typeRef: { - kind: "reference", - value: { moduleName: "sys.types", name: "Error" }, - }, - parameters: [texprT.value], - }, - }; -} - -export interface MapEntry { - key: K; - value: V; -} - -export function makeMapEntry( - input: { - key: K; - value: V; - }, -): MapEntry { - return { - key: input.key, - value: input.value, - }; -} - -const MapEntry_AST: ADL.ScopedDecl = { - "moduleName": "sys.types", - "decl": { - "annotations": [], - "type_": { - "kind": "struct_", - "value": { - "typeParams": ["K", "V"], - "fields": [{ - "annotations": [], - "serializedName": "k", - "default": { "kind": "nothing" }, - "name": "key", - "typeExpr": { - "typeRef": { "kind": "typeParam", "value": "K" }, - "parameters": [], - }, - }, { - "annotations": [], - "serializedName": "v", - "default": { "kind": "nothing" }, - "name": "value", - "typeExpr": { - "typeRef": { "kind": "typeParam", "value": "V" }, - "parameters": [], - }, - }], - }, - }, - "name": "MapEntry", - "version": { "kind": "nothing" }, - }, -}; - -export const snMapEntry: ADL.ScopedName = { - moduleName: "sys.types", - name: "MapEntry", -}; - -export function texprMapEntry( - texprK: ADL.ATypeExpr, - texprV: ADL.ATypeExpr, -): ADL.ATypeExpr> { - return { - value: { - typeRef: { - kind: "reference", - value: { moduleName: "sys.types", name: "MapEntry" }, - }, - parameters: [texprK.value, texprV.value], - }, - }; -} - -export type Map = Pair[]; - -const Map_AST: ADL.ScopedDecl = { - "moduleName": "sys.types", - "decl": { - "annotations": [], - "type_": { - "kind": "newtype_", - "value": { - "typeParams": ["K", "V"], - "default": { "kind": "nothing" }, - "typeExpr": { - "typeRef": { "kind": "primitive", "value": "Vector" }, - "parameters": [{ - "typeRef": { - "kind": "reference", - "value": { "moduleName": "sys.types", "name": "Pair" }, - }, - "parameters": [{ - "typeRef": { "kind": "typeParam", "value": "K" }, - "parameters": [], - }, { - "typeRef": { "kind": "typeParam", "value": "V" }, - "parameters": [], - }], - }], - }, - }, - }, - "name": "Map", - "version": { "kind": "nothing" }, - }, -}; - -export const snMap: ADL.ScopedName = { moduleName: "sys.types", name: "Map" }; - -export function texprMap( - texprK: ADL.ATypeExpr, - texprV: ADL.ATypeExpr, -): ADL.ATypeExpr> { - return { - value: { - typeRef: { - kind: "reference", - value: { moduleName: "sys.types", name: "Map" }, - }, - parameters: [texprK.value, texprV.value], - }, - }; -} - -export type Set = T[]; - -const Set_AST: ADL.ScopedDecl = { - "moduleName": "sys.types", - "decl": { - "annotations": [], - "type_": { - "kind": "newtype_", - "value": { - "typeParams": ["T"], - "default": { "kind": "nothing" }, - "typeExpr": { - "typeRef": { "kind": "primitive", "value": "Vector" }, - "parameters": [{ - "typeRef": { "kind": "typeParam", "value": "T" }, - "parameters": [], - }], - }, - }, - }, - "name": "Set", - "version": { "kind": "nothing" }, - }, -}; - -export const snSet: ADL.ScopedName = { moduleName: "sys.types", name: "Set" }; - -export function texprSet(texprT: ADL.ATypeExpr): ADL.ATypeExpr> { - return { - value: { - typeRef: { - kind: "reference", - value: { moduleName: "sys.types", name: "Set" }, - }, - parameters: [texprT.value], - }, - }; -} - -export const _AST_MAP: { [key: string]: ADL.ScopedDecl } = { - "sys.types.Pair": Pair_AST, - "sys.types.Either": Either_AST, - "sys.types.Maybe": Maybe_AST, - "sys.types.Error": Error_AST, - "sys.types.MapEntry": MapEntry_AST, - "sys.types.Map": Map_AST, - "sys.types.Set": Set_AST, -}; diff --git a/adl/manifest.adl b/adl/manifest.adl deleted file mode 100644 index 5b4e214..0000000 --- a/adl/manifest.adl +++ /dev/null @@ -1,22 +0,0 @@ -module dnit.manifest { - import sys.types.Map; - - newtype TaskName = String; - newtype TrackedFileName = String; - newtype TrackedFileHash = String; - newtype Timestamp = String; - - struct TaskData { - Nullable lastExecution = null; - Map trackedFiles; - }; - - struct TrackedFileData { - TrackedFileHash hash; - Timestamp timestamp; - }; - - struct Manifest { - Map tasks = []; - }; -}; diff --git a/cli/builtinTasks.ts b/cli/builtinTasks.ts new file mode 100644 index 0000000..a5c215a --- /dev/null +++ b/cli/builtinTasks.ts @@ -0,0 +1,52 @@ +import { runAlways, type Task, task } from "../core/task.ts"; +import type { TaskContext } from "../core/TaskContext.ts"; +import { echoBashCompletionScript, showTaskList } from "./utils.ts"; + +export const builtinTasks: Task[] = [ + task({ + name: "clean", + description: "Clean tracked files", + action: async (ctx: TaskContext) => { + const positionalArgs = ctx.args["_"]; + + const affectedTasks = positionalArgs.length > 1 + ? positionalArgs.map((arg: unknown) => + ctx.exec.taskRegister.get(String(arg)) + ) + .filter((task) => task !== undefined) + : Array.from(ctx.exec.taskRegister.values()); + if (affectedTasks.length > 0) { + ctx.exec.cliLogger.info("Clean tasks:"); + /// Reset tasks + await Promise.all( + affectedTasks.map((t) => { + ctx.exec.cliLogger.info(` ${t.name}`); + return ctx.exec.schedule(() => t.reset(ctx.exec)); + }), + ); + // await ctx.exec.manifest.save(); + } + }, + uptodate: runAlways, + }), + + task({ + name: "list", + description: "List tasks", + action: (ctx: TaskContext) => { + showTaskList(ctx.exec, ctx.args); + }, + uptodate: runAlways, + }), + + task({ + name: "tabcompletion", + description: "Generate shell completion script", + action: (ctx: TaskContext) => { + // todo: detect shell type and generate appropriate script + // or add args for shell type + echoBashCompletionScript(ctx.exec); + }, + uptodate: runAlways, + }), +]; diff --git a/cli/cli.ts b/cli/cli.ts new file mode 100644 index 0000000..a9d3416 --- /dev/null +++ b/cli/cli.ts @@ -0,0 +1,157 @@ +import { type Args, parseArgs } from "@std/cli/parse-args"; +import { Manifest } from "../manifest.ts"; +import { ExecContext } from "../core/execContext.ts"; +import type { Task } from "../core/task.ts"; +import { builtinTasks } from "./builtinTasks.ts"; +import { createConsoleLoggers } from "./logging.ts"; +import type { ILoggers } from "../interfaces/core/ICoreInterfaces.ts"; +import { showHelp } from "./utils.ts"; + +export type ExecResult = { + success: boolean; +}; + +// Initialize execution context with logging, manifest, and registered tasks. +export async function execContextInit( + args: Args, + tasks: Task[], + overrides?: Partial, +): Promise { + /// directory of user's entrypoint source as discovered by 'launch' util: + const dnitDir = args["dnitDir"] || "./dnit"; + + const manifest = new Manifest(dnitDir); + + const ctx = await execContextInitBasicArgs(args, tasks, manifest, overrides); + return ctx; +} + +// Execute a specific task by name, handling manifest load/save and error reporting. +export async function executeRequestedTask( + ctx: ExecContext, + requestedTaskName: string, +) { + try { + /// Load manifest (dependency tracking data) + await ctx.manifest.load(); + + /// Find the requested task: + const requestedTask = ctx.taskRegister.get(requestedTaskName); + if (requestedTask !== undefined) { + /// Execute the requested task: + await requestedTask.exec(ctx); + /// Save manifest (dependency tracking data) + await ctx.manifest.save(); + return { success: true }; + } else { + ctx.taskLogger.error(`Task ${requestedTaskName} not found`); + return { success: false }; + } + } catch (err) { + ctx.taskLogger.error("Error", err); + throw err; + } +} + +// get requested task name from args +export function getRequestedTaskName(args: Args) { + const positionalArgs = args["_"]; + if (positionalArgs.length > 0) { + return `${positionalArgs[0]}`; + } + + // default to show the list for no args + return "list"; +} + +/** Execute given commandline args and array of items (task & trackedfile) */ +export async function execCli( + cliArgs: string[], + tasks: Task[], + overrides?: Partial, +): Promise { + const args = parseArgs(cliArgs); + + // Handle --help flag early + if (args["help"]) { + const ctx = await execContextInit(args, tasks, overrides); + showHelp(ctx); + return { success: true }; + } + + const ctx = await execContextInit(args, tasks, overrides); + + const requestedTaskName: string = getRequestedTaskName(args); + const result = await executeRequestedTask(ctx, requestedTaskName); + return result; +} + +// Create execution context from parsed args with tasks registered and setup methods called. +export async function execContextInitBasicArgs( + args: Args, + tasks: Task[], + manifest: Manifest, + overrides?: Partial, +): Promise { + // Extract loggers and other overrides + const defaultLoggers = createConsoleLoggers(); + const { + internalLogger = defaultLoggers.internalLogger, + taskLogger = defaultLoggers.taskLogger, + userLogger = defaultLoggers.userLogger, + cliLogger = defaultLoggers.cliLogger, + ...otherOverrides + } = overrides || {}; + + const loggers: ILoggers = { + internalLogger, + taskLogger, + userLogger, + cliLogger, + }; + const ctx = new ExecContext(manifest, args, loggers); + + // Apply other overrides if any + Object.assign(ctx, otherOverrides); + + // register given tasks: + tasks.forEach((t) => ctx.taskRegister.set(t.name, t)); + + /// register built-in tasks: + for (const t of builtinTasks) { + ctx.taskRegister.set(t.name, t); + } + + // execute setup on all tasks: + await Promise.all( + Array.from(ctx.taskRegister.values()).map((t) => + ctx.schedule(() => t.setup(ctx)) + ), + ); + return ctx; +} + +/// No-frills setup of an ExecContext (mainly for testing) +export async function execContextInitBasic( + cliArgs: string[], + tasks: Task[], + manifest: Manifest, + overrides?: Partial, +): Promise { + const args = parseArgs(cliArgs); + const ctx = await execContextInitBasicArgs(args, tasks, manifest, overrides); + return ctx; +} + +/// main function for use in dnit scripts +export function main( + cliArgs: string[], + tasks: Task[], +): void { + execCli(cliArgs, tasks) + .then(() => Deno.exit(0)) + .catch((err) => { + console.error("error in main", err); + Deno.exit(1); + }); +} diff --git a/cli/logging.ts b/cli/logging.ts new file mode 100644 index 0000000..018b94f --- /dev/null +++ b/cli/logging.ts @@ -0,0 +1,33 @@ +import * as log from "@std/log"; +import type { ILoggers } from "../interfaces/core/ICoreInterfaces.ts"; + +/// StdErr plaintext handler (no color codes) +class StdErrPlainHandler extends log.BaseHandler { + constructor(levelName: log.LevelName) { + super(levelName, { + formatter: (rec) => rec.msg, + }); + } + + override log(msg: string): void { + Deno.stderr.writeSync(new TextEncoder().encode(msg + "\n")); + } +} + +export function createConsoleLoggers(): ILoggers { + const stderrHandler = new StdErrPlainHandler("DEBUG"); + + return { + internalLogger: new log.Logger("internal", "WARN", { + handlers: [stderrHandler], + }), + taskLogger: new log.Logger("task", "INFO", { handlers: [stderrHandler] }), + userLogger: new log.Logger("user", "INFO", { handlers: [stderrHandler] }), + cliLogger: new log.Logger("cli", "INFO", { handlers: [stderrHandler] }), + }; +} + +/** Convenience access to a setup logger for tasks */ +export function getLogger(): log.Logger { + return log.getLogger("user"); +} diff --git a/cli/utils.ts b/cli/utils.ts new file mode 100644 index 0000000..77830f3 --- /dev/null +++ b/cli/utils.ts @@ -0,0 +1,102 @@ +import type { Args } from "@std/cli/parse-args"; +import { plainTextTable } from "../utils/textTable.ts"; +import type { IExecContext } from "../interfaces/core/ICoreInterfaces.ts"; + +export function showTaskList(ctx: IExecContext, args: Args) { + if (args["quiet"]) { + Array.from(ctx.taskRegister.values()).map((task) => + ctx.cliLogger.info(task.name) + ); + } else { + ctx.cliLogger.info( + plainTextTable( + ["Name", "Description"], + Array.from(ctx.taskRegister.values()).map((t) => [ + t.name, + t.description || "", + ]), + ), + ); + } +} + +function showHelpCommon(logger: { info: (msg: string) => void }) { + logger.info("dnit - A TypeScript-based task runner for Deno\n"); + + logger.info("USAGE:"); + logger.info(" dnit [FLAGS] [TASK] [ARGS...]\n"); + + logger.info("FLAGS:"); + logger.info(" --help Show this help message"); + logger.info(" --version Show version information"); + logger.info(" --verbose Enable verbose logging"); + logger.info(" --quiet Enable quiet mode (minimal output)\n"); + + logger.info("Run 'dnit' without arguments to see available tasks.\n"); +} + +function helpFooter(logger: { info: (msg: string) => void }) { + logger.info("\nFor more information: https://github.com/PaulThompson/dnit"); +} + +export function showHelp(ctx: IExecContext) { + showHelpCommon(ctx.cliLogger); + + ctx.cliLogger.info("AVAILABLE TASKS:"); + const tasks = Array.from(ctx.taskRegister.values()).map((t) => [ + t.name, + t.description || "", + ]); + + if (tasks.length > 0) { + ctx.cliLogger.info( + plainTextTable( + ["Name", "Description"], + tasks, + ), + ); + } else { + ctx.cliLogger.info(" No tasks found"); + } + + helpFooter(ctx.cliLogger); +} + +export function showHelpBasic(logger: { info: (msg: string) => void }) { + showHelpCommon(logger); + + logger.info("ERROR:"); + logger.info( + " No dnit/ directory found. Create dnit/main.ts with your task definitions", + ); + logger.info(" to get started."); + + helpFooter(logger); +} + +export function echoBashCompletionScript(ctx: IExecContext) { + ctx.cliLogger.info( + "# bash completion for dnit\n" + + "# auto-generate by `dnit tabcompletion`\n" + + "\n" + + "# to activate it you need to 'source' the generated script\n" + + "# $ source <(dnit tabcompletion)\n" + + "\n" + + "_dnit() \n" + + "{\n" + + " local cur prev words cword basetask sub_cmds tasks i dodof\n" + + " COMPREPLY=() # contains list of words with suitable completion\n" + + " _get_comp_words_by_ref -n : cur prev words cword\n" + + " # list of sub-commands\n" + + ' sub_cmds="list"\n' + + "\n" + + " tasks=$(dnit list --quiet 2>/dev/null)\n" + + "\n" + + ' COMPREPLY=( $(compgen -W "${sub_cmds} ${tasks}" -- ${cur}) )\n' + + " return 0\n" + + "}\n" + + "\n" + + "\n" + + "complete -o filenames -F _dnit dnit \n", + ); +} diff --git a/core/TaskContext.ts b/core/TaskContext.ts new file mode 100644 index 0000000..e724025 --- /dev/null +++ b/core/TaskContext.ts @@ -0,0 +1,22 @@ +import type { Args } from "@std/cli/parse-args"; +import type * as log from "@std/log"; +import type { + IExecContext, + ITask, +} from "../interfaces/core/ICoreInterfaces.ts"; + +export interface TaskContext { + logger: log.Logger; + task: ITask; + args: Args; + exec: IExecContext; +} + +export function taskContext(ctx: IExecContext, task: ITask): TaskContext { + return { + logger: ctx.taskLogger, + task, + args: ctx.args, + exec: ctx, + }; +} diff --git a/core/execContext.ts b/core/execContext.ts new file mode 100644 index 0000000..541da58 --- /dev/null +++ b/core/execContext.ts @@ -0,0 +1,77 @@ +import type { Args } from "@std/cli/parse-args"; +import type * as log from "@std/log"; +import { version } from "../version.ts"; +import { AsyncQueue } from "../utils/asyncQueue.ts"; +import type { Manifest } from "../manifest.ts"; +import type { + TaskName, + TrackedFileName, +} from "../interfaces/core/IManifestTypes.ts"; +import type { + IExecContext, + ILoggers, + ITask, +} from "../interfaces/core/ICoreInterfaces.ts"; + +export class ExecContext implements IExecContext { + /// All tasks by name + taskRegister: Map = new Map(); + + /// Tasks by target + targetRegister: Map = new Map< + TrackedFileName, + ITask + >(); + + /// Done or up-to-date tasks + doneTasks: Set = new Set(); + + /// In progress tasks + inprogressTasks: Set = new Set(); + + /// Queue for scheduling async work with specified number allowable concurrently. + asyncQueue: AsyncQueue; + + readonly internalLogger: log.Logger; + readonly taskLogger: log.Logger; + readonly userLogger: log.Logger; + readonly cliLogger: log.Logger; + + constructor( + /// loaded hash manifest + readonly manifest: Manifest, + /// commandline args + readonly args: Args, + /// loggers + loggers: ILoggers, + ) { + this.internalLogger = loggers.internalLogger; + this.taskLogger = loggers.taskLogger; + this.userLogger = loggers.userLogger; + this.cliLogger = loggers.cliLogger; + if (args["verbose"] !== undefined) { + this.internalLogger.levelName = "INFO"; + } + + const concurrency = args["concurrency"] || 4; + this.asyncQueue = new AsyncQueue(concurrency); + + this.internalLogger.info(`Starting ExecContext version: ${version}`); + } + + getTaskByName(name: TaskName): ITask | undefined { + return this.taskRegister.get(name); + } + + schedule(action: () => Promise): Promise { + return this.asyncQueue.schedule(action); + } + + get concurrency(): number { + return this.asyncQueue.concurrency || 4; + } + + get verbose(): boolean { + return this.args["verbose"] as boolean || false; + } +} diff --git a/core/file/TrackedFile.ts b/core/file/TrackedFile.ts new file mode 100644 index 0000000..d63e13d --- /dev/null +++ b/core/file/TrackedFile.ts @@ -0,0 +1,205 @@ +import * as log from "@std/log"; +import * as path from "@std/path"; +import type { + Timestamp, + TrackedFileData, + TrackedFileHash, + TrackedFileName, +} from "../../interfaces/core/IManifestTypes.ts"; +import { + deletePath, + getFileSha1Sum, + getFileTimestamp, + statPath, + type StatResult, +} from "../../utils/filesystem.ts"; +import type { + IExecContext, + ITask, +} from "../../interfaces/core/ICoreInterfaces.ts"; + +export type GetFileHash = ( + filename: TrackedFileName, + stat: Deno.FileInfo, +) => Promise | TrackedFileHash; + +export type GetFileTimestamp = ( + filename: TrackedFileName, + stat: Deno.FileInfo, +) => Promise | Timestamp; + +/** User params for a tracked file */ +export type FileParams = { + /// File path + path: string; + + /// Optional function for how to hash the file. Defaults to the sha1 hash of the file contents. + /// A file is out of date if the file timestamp and the hash are different than that in the task manifest + getHash?: GetFileHash; + + /// Optional function for how to get the file timestamp. Defaults to the actual file timestamp + getTimestamp?: GetFileTimestamp; +}; + +export class TrackedFile { + path: TrackedFileName = ""; + #getHash: GetFileHash; + #getTimestamp: GetFileTimestamp; + + fromTask: ITask | null = null; + + constructor(fileParams: FileParams) { + this.path = path.resolve(fileParams.path); + this.#getHash = fileParams.getHash || getFileSha1Sum; + this.#getTimestamp = fileParams.getTimestamp || getFileTimestamp; + } + + private async stat(): Promise { + log.getLogger("internal").info(`checking file ${this.path}`); + return await statPath(this.path); + } + + async delete(): Promise { + await deletePath(this.path); + } + + async exists(statInput?: StatResult): Promise { + let statResult = statInput; + if (statResult === undefined) { + statResult = await this.stat(); + } + return statResult.kind === "fileInfo"; + } + + async getHash(statInput?: StatResult): Promise { + let statResult = statInput; + if (statResult === undefined) { + statResult = await this.stat(); + } + if (statResult.kind !== "fileInfo") { + return ""; + } + + log.getLogger("internal").info(`checking hash on ${this.path}`); + return this.#getHash(this.path, statResult.fileInfo); + } + + async getTimestamp(statInput?: StatResult): Promise { + let statResult = statInput; + if (statResult === undefined) { + statResult = await this.stat(); + } + if (statResult.kind !== "fileInfo") { + return ""; + } + return this.#getTimestamp(this.path, statResult.fileInfo); + } + + /// whether this is up to date w.r.t. the given TrackedFileData + async isUpToDate( + tData: TrackedFileData | undefined, + statInput?: StatResult, + ): Promise { + if (tData === undefined) { + return false; + } + + let statResult = statInput; + if (statResult === undefined) { + statResult = await this.stat(); + } + + // File is up-to-date only if BOTH hash and timestamp match + // On Windows, check hash first since timestamp caching can be unreliable + if (Deno.build.os === "windows") { + const hash = await this.getHash(statResult); + if (hash !== tData.hash) { + return false; + } + // If hash matches, check timestamp as well + const mtime = await this.getTimestamp(statResult); + return mtime === tData.timestamp; + } + + // On other platforms, check timestamp first (faster) + const mtime = await this.getTimestamp(statResult); + if (mtime !== tData.timestamp) { + return false; + } + const hash = await this.getHash(statResult); + return hash === tData.hash; + } + + /// Recalculate timestamp and hash data + async getFileData( + statInput?: StatResult, + ): Promise { + let statResult = statInput; + if (statResult === undefined) { + statResult = await this.stat(); + } + return { + hash: await this.getHash(statResult), + timestamp: await this.getTimestamp(statResult), + }; + } + + /// return given tData if up to date or re-calculate + async getFileDataOrCached( + _ctx: IExecContext, + tData: TrackedFileData | undefined, + statInput?: StatResult, + ): Promise<{ + tData: TrackedFileData; + upToDate: boolean; + }> { + let statResult = statInput; + if (statResult === undefined) { + statResult = await this.stat(); + } + + if (tData !== undefined && await this.isUpToDate(tData, statResult)) { + return { + tData, + upToDate: true, + }; + } + return { + tData: await this.getFileData(statResult), + upToDate: false, + }; + } + + setTask(t: ITask) { + if (this.fromTask === null) { + this.fromTask = t; + } else { + throw new Error( + "Duplicate tasks generating TrackedFile as target - " + this.path, + ); + } + } + + getTask(): ITask | null { + return this.fromTask; + } +} + +/** Generate a trackedfile for tracking */ +export function trackFile(fileParams: FileParams | string): TrackedFile { + if (typeof fileParams === "string") { + return new TrackedFile({ path: fileParams }); + } + return new TrackedFile(fileParams); +} + +/** @deprecated Use trackFile() instead */ +export function file(fileParams: FileParams | string): TrackedFile { + return trackFile(fileParams); +} + +export function isTrackedFile( + dep: unknown, +): dep is TrackedFile { + return dep instanceof TrackedFile; +} diff --git a/core/file/TrackedFilesAsync.ts b/core/file/TrackedFilesAsync.ts new file mode 100644 index 0000000..f0343e8 --- /dev/null +++ b/core/file/TrackedFilesAsync.ts @@ -0,0 +1,24 @@ +import type { TrackedFile } from "./TrackedFile.ts"; + +export type GenTrackedFiles = () => Promise | TrackedFile[]; + +export class TrackedFilesAsync { + kind: "trackedfilesasync" = "trackedfilesasync"; + + constructor(public gen: GenTrackedFiles) { + } + + async getTrackedFiles(): Promise { + return await this.gen(); + } +} + +export function asyncFiles(gen: GenTrackedFiles): TrackedFilesAsync { + return new TrackedFilesAsync(gen); +} + +export function isTrackedFileAsync( + dep: unknown, +): dep is TrackedFilesAsync { + return dep instanceof TrackedFilesAsync; +} diff --git a/core/manifestSchemas.ts b/core/manifestSchemas.ts new file mode 100644 index 0000000..6ac4162 --- /dev/null +++ b/core/manifestSchemas.ts @@ -0,0 +1,47 @@ +import { z } from "zod"; + +// Zod schemas for manifest type validation and inference +export const TaskNameSchema: z.ZodString = z.string(); +export const TrackedFileNameSchema: z.ZodString = z.string(); +export const TrackedFileHashSchema: z.ZodString = z.string(); +export const TimestampSchema: z.ZodString = z.string(); + +export const TrackedFileDataSchema: z.ZodObject<{ + hash: z.ZodString; + timestamp: z.ZodString; +}> = z.object({ + hash: TrackedFileHashSchema, + timestamp: TimestampSchema, +}); + +export const TaskDataSchema: z.ZodObject<{ + lastExecution: z.ZodNullable; + trackedFiles: z.ZodRecord< + z.ZodString, + z.ZodObject<{ + hash: z.ZodString; + timestamp: z.ZodString; + }> + >; +}> = z.object({ + lastExecution: TimestampSchema.nullable(), + trackedFiles: z.record(TrackedFileNameSchema, TrackedFileDataSchema), +}); + +export const ManifestSchema: z.ZodObject<{ + tasks: z.ZodRecord< + z.ZodString, + z.ZodObject<{ + lastExecution: z.ZodNullable; + trackedFiles: z.ZodRecord< + z.ZodString, + z.ZodObject<{ + hash: z.ZodString; + timestamp: z.ZodString; + }> + >; + }> + >; +}> = z.object({ + tasks: z.record(TaskNameSchema, TaskDataSchema), +}); diff --git a/core/task.ts b/core/task.ts new file mode 100644 index 0000000..c298ccc --- /dev/null +++ b/core/task.ts @@ -0,0 +1,309 @@ +import type { TaskName } from "../interfaces/core/IManifestTypes.ts"; +import { TaskManifest } from "./taskManifest.ts"; +import type { + IExecContext, + ITask, + ITaskContext, +} from "../interfaces/core/ICoreInterfaces.ts"; +import { taskContext } from "./TaskContext.ts"; +import { isTrackedFile, type TrackedFile } from "./file/TrackedFile.ts"; +import { + isTrackedFileAsync, + type TrackedFilesAsync, +} from "./file/TrackedFilesAsync.ts"; + +export type Action = (ctx: ITaskContext) => Promise | void; +export type IsUpToDate = (ctx: ITaskContext) => Promise | boolean; + +/** User definition of a task */ +export type TaskParams = { + /// Name: (string) - The key used to initiate a task + name: TaskName; + + /// Description (string) - Freeform text description shown on help + description?: string; + + /// Action executed on execution of the task (async or sync) + action: Action; + + /// Optional list of task or file dependencies + deps?: Dep[]; + + /// Targets (files which will be produced by execution of this task) + targets?: TrackedFile[]; + + /// Custom up-to-date definition - Can be used to make a task *less* up to date. Eg; use uptodate: runAlways to run always on request regardless of dependencies being up to date. + uptodate?: IsUpToDate; +}; + +/// The kinds of supported dependencies. +export type Dep = Task | TrackedFile | TrackedFilesAsync; + +/// Convenience function: an up to date always false to run always +export const runAlways: IsUpToDate = () => false; + +/** Result of circular dependency detection */ +export type CircularDependency = { + cycle: Task[]; +}; + +/** Detect circular dependencies in task dependency graph using iterative DFS */ +export function detectCircularDependencies( + startTask: Task, +): CircularDependency | null { + const visited = new Set(); + const stack: { task: Task; path: Task[] }[] = [{ task: startTask, path: [] }]; + + while (stack.length > 0) { + const { task, path } = stack.pop()!; + + // Check if task is already in the current path (circular dependency) + if (path.includes(task)) { + const cycleStart = path.indexOf(task); + const cycle = path.slice(cycleStart).concat([task]); + return { cycle }; + } + + if (visited.has(task)) continue; + + visited.add(task); + const newPath = [...path, task]; + + // Add all task dependencies to stack + for (const dep of task.task_deps) { + stack.push({ task: dep, path: newPath }); + } + } + + return null; +} + +function isTask(dep: Task | TrackedFile | TrackedFilesAsync): dep is Task { + return dep instanceof Task; +} + +export class Task implements ITask { + public name: TaskName; + public description?: string; + public action: Action; + public task_deps: Set; + public file_deps: Set; + public async_files_deps: Set; + public targets: Set; + + public taskManifest: TaskManifest | null = null; + public uptodate?: IsUpToDate; + + constructor(taskParams: TaskParams) { + this.name = taskParams.name; + this.action = taskParams.action; + this.description = taskParams.description; + this.task_deps = new Set( + this.getTaskDeps(taskParams.deps || []), + ); + this.file_deps = new Set( + this.getTrackedFiles(taskParams.deps || []), + ); + this.async_files_deps = new Set( + this.getTrackedFilesAsync(taskParams.deps || []), + ); + this.targets = new Set(taskParams.targets || []); + this.uptodate = taskParams.uptodate; + + for (const f of this.targets) { + f.setTask(this); + } + } + + private getTaskDeps( + deps: (Task | TrackedFile | TrackedFilesAsync)[], + ): Task[] { + return deps.filter(isTask); + } + private getTrackedFiles( + deps: (Task | TrackedFile | TrackedFilesAsync)[], + ): TrackedFile[] { + return deps.filter(isTrackedFile); + } + private getTrackedFilesAsync( + deps: (Task | TrackedFile | TrackedFilesAsync)[], + ): TrackedFilesAsync[] { + return deps.filter(isTrackedFileAsync); + } + + async setup(ctx: IExecContext): Promise { + if (this.taskManifest === null) { + for (const t of this.targets) { + ctx.targetRegister.set(t.path, this); + } + + this.taskManifest = this.getOrCreateTaskManifest(ctx); + + // ensure preceding tasks are setup too + for (const taskDep of this.task_deps) { + await taskDep.setup(ctx); + } + for (const fDep of this.file_deps) { + const fDepTask = fDep.getTask(); + if (fDepTask !== null) { + await fDepTask.setup(ctx); + } + } + } + } + + async exec(ctx: IExecContext): Promise { + if (ctx.doneTasks.has(this)) { + return; + } + if (ctx.inprogressTasks.has(this)) { + return; + } + + ctx.inprogressTasks.add(this); + + // evaluate async file_deps (useful if task depends on a glob of the filesystem) + for (const afd of this.async_files_deps) { + const fileDeps = await afd.getTrackedFiles(); + for (const fd of fileDeps) { + this.file_deps.add(fd); + } + } + + // add task dep on the task that makes the file if its a target + for (const fd of this.file_deps) { + const t = ctx.targetRegister.get(fd.path); + if (t !== undefined && t instanceof Task) { + this.task_deps.add(t); + } + } + + // detect circular dependencies after all dynamic dependencies are resolved + const circularDep = detectCircularDependencies(this); + if (circularDep) { + throw new Error( + `Circular dependency detected: ${ + circularDep.cycle.map((t) => t.name).join(" -> ") + }`, + ); + } + + await this.execDependencies(ctx); + + let actualUpToDate = true; + + actualUpToDate = actualUpToDate && await this.checkFileDeps(ctx); + ctx.internalLogger.info(`${this.name} checkFileDeps ${actualUpToDate}`); + + actualUpToDate = actualUpToDate && await this.targetsExist(ctx); + ctx.internalLogger.info(`${this.name} targetsExist ${actualUpToDate}`); + + if (this.uptodate !== undefined) { + actualUpToDate = actualUpToDate && + await this.uptodate(taskContext(ctx, this)); + } + ctx.internalLogger.info(`${this.name} uptodate ${actualUpToDate}`); + + if (actualUpToDate) { + ctx.taskLogger.info(`--- ${this.name}`); + } else { + // suppress logging the task "{-- name --}" for the list task + const logTaskScope = this.name !== "list"; + if (logTaskScope) ctx.taskLogger.info(`{-- ${this.name}`); + await this.action(taskContext(ctx, this)); + if (logTaskScope) ctx.taskLogger.info(`--} ${this.name}`); + + { + /// recalc & save data of deps: + this.taskManifest?.setExecutionTimestamp(); + const promisesInProgress: Promise[] = []; + for (const fdep of this.file_deps) { + promisesInProgress.push( + ctx.schedule(async () => { + const trackedFileData = await fdep.getFileData(); + this.taskManifest?.setFileData(fdep.path, trackedFileData); + }), + ); + } + await Promise.all(promisesInProgress); + } + } + + ctx.doneTasks.add(this); + ctx.inprogressTasks.delete(this); + } + + async reset(ctx: IExecContext): Promise { + await this.cleanTargets(ctx); + } + + private async cleanTargets(ctx: IExecContext): Promise { + await Promise.all( + Array.from(this.targets).map(async (tf) => { + try { + await ctx.schedule(() => tf.delete()); + } catch (err) { + ctx.taskLogger.error(`Error scheduling deletion of ${tf.path}`, err); + } + }), + ); + } + + private async targetsExist(ctx: IExecContext): Promise { + const tex = await Promise.all( + Array.from(this.targets).map((tf) => ctx.schedule(() => tf.exists())), + ); + // all exist: NOT some NOT exist + return !tex.some((t) => !t); + } + + private async checkFileDeps(ctx: IExecContext): Promise { + let fileDepsUpToDate = true; + let promisesInProgress: Promise[] = []; + + const taskManifest = this.taskManifest; + if (taskManifest === null) { + throw new Error(`Invalid null taskManifest on ${this.name}`); + } + + for (const fdep of this.file_deps) { + promisesInProgress.push( + ctx.schedule(async () => { + const r = await fdep.getFileDataOrCached( + ctx, + taskManifest.getFileData(fdep.path), + ); + taskManifest.setFileData(fdep.path, r.tData); + fileDepsUpToDate = fileDepsUpToDate && r.upToDate; + }), + ); + } + await Promise.all(promisesInProgress); + promisesInProgress = []; + return fileDepsUpToDate; + } + + private getOrCreateTaskManifest(ctx: IExecContext): TaskManifest { + if (!ctx.manifest.tasks[this.name]) { + ctx.manifest.tasks[this.name] = new TaskManifest({ + lastExecution: null, + trackedFiles: {}, + }); + } + return ctx.manifest.tasks[this.name]; + } + + private async execDependencies(ctx: IExecContext) { + for (const dep of this.task_deps) { + if (!ctx.doneTasks.has(dep) && !ctx.inprogressTasks.has(dep)) { + await dep.exec(ctx); + } + } + } +} + +/** Generate a task */ +export function task(taskParams: TaskParams): Task { + const task = new Task(taskParams); + return task; +} diff --git a/core/taskManifest.ts b/core/taskManifest.ts new file mode 100644 index 0000000..b05f727 --- /dev/null +++ b/core/taskManifest.ts @@ -0,0 +1,36 @@ +import type { + TaskData, + Timestamp, + TrackedFileData, + TrackedFileName, +} from "../interfaces/core/IManifestTypes.ts"; +import type { ITaskManifest } from "../interfaces/core/IManifest.ts"; + +export class TaskManifest implements ITaskManifest { + public lastExecution: Timestamp | null = null; + trackedFiles: Record = {}; + + constructor(data: TaskData) { + this.trackedFiles = data.trackedFiles; + this.lastExecution = data.lastExecution; + } + + getFileData(fn: TrackedFileName): TrackedFileData | undefined { + return this.trackedFiles[fn]; + } + + setFileData(fn: TrackedFileName, d: TrackedFileData) { + this.trackedFiles[fn] = d; + } + + setExecutionTimestamp() { + this.lastExecution = (new Date()).toISOString(); + } + + toData(): TaskData { + return { + lastExecution: this.lastExecution, + trackedFiles: this.trackedFiles, + }; + } +} diff --git a/deno.json b/deno.json index b60dac7..a9730e3 100644 --- a/deno.json +++ b/deno.json @@ -1,7 +1,33 @@ { - "fmt": { + "name": "@dnit/dnit", + "version": "2.0.0", + "description": "A TypeScript (Deno) based task runner for complex projects", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/PaulThompson/dnit.git" + }, + "exports": "./mod.ts", + "publish": { "exclude": [ - "adl-gen/" + ".claude", + ".vscode", + ".github", + "CLAUDE.md", + "dnit", + "tests" ] + }, + "fmt": {}, + "imports": { + "@std/assert": "jsr:@std/assert@^1.0.13", + "@std/cli": "jsr:@std/cli@^1.0.21", + "@std/crypto": "jsr:@std/crypto@^1.0.5", + "@std/encoding": "jsr:@std/encoding@^1.0.10", + "@std/fs": "jsr:@std/fs@^1.0.19", + "@std/log": "jsr:@std/log@^0.224.14", + "@std/path": "jsr:@std/path@^1.1.1", + "@std/semver": "jsr:@std/semver@^1.0.5", + "zod": "npm:zod@^4.0.16" } } diff --git a/deno.lock b/deno.lock index f8dbbd0..cdbb1c1 100644 --- a/deno.lock +++ b/deno.lock @@ -1,9 +1,125 @@ { - "version": "3", + "version": "5", + "specifiers": { + "jsr:@std/assert@^1.0.13": "1.0.13", + "jsr:@std/cli@^1.0.21": "1.0.21", + "jsr:@std/crypto@^1.0.5": "1.0.5", + "jsr:@std/encoding@^1.0.10": "1.0.10", + "jsr:@std/fmt@^1.0.5": "1.0.6", + "jsr:@std/fs@*": "1.0.15", + "jsr:@std/fs@1.0.15": "1.0.15", + "jsr:@std/fs@^1.0.11": "1.0.15", + "jsr:@std/fs@^1.0.15": "1.0.15", + "jsr:@std/fs@^1.0.19": "1.0.19", + "jsr:@std/internal@^1.0.6": "1.0.10", + "jsr:@std/internal@^1.0.9": "1.0.10", + "jsr:@std/io@~0.225.2": "0.225.2", + "jsr:@std/log@*": "0.224.14", + "jsr:@std/log@0.224.14": "0.224.14", + "jsr:@std/log@~0.224.14": "0.224.14", + "jsr:@std/path@*": "1.0.8", + "jsr:@std/path@1.0.8": "1.0.8", + "jsr:@std/path@^1.0.8": "1.0.8", + "jsr:@std/path@^1.1.1": "1.1.1", + "jsr:@std/semver@^1.0.5": "1.0.5", + "npm:zod@^4.0.16": "4.0.16" + }, + "jsr": { + "@std/assert@1.0.13": { + "integrity": "ae0d31e41919b12c656c742b22522c32fb26ed0cba32975cb0de2a273cb68b29", + "dependencies": [ + "jsr:@std/internal@^1.0.6" + ] + }, + "@std/cli@1.0.21": { + "integrity": "cd25b050bdf6282e321854e3822bee624f07aca7636a3a76d95f77a3a919ca2a" + }, + "@std/crypto@1.0.5": { + "integrity": "0dcfbb319fe0bba1bd3af904ceb4f948cde1b92979ec1614528380ed308a3b40" + }, + "@std/encoding@1.0.10": { + "integrity": "8783c6384a2d13abd5e9e87a7ae0520a30e9f56aeeaa3bdf910a3eaaf5c811a1" + }, + "@std/fmt@1.0.6": { + "integrity": "a2c56a69a2369876ddb3ad6a500bb6501b5bad47bb3ea16bfb0c18974d2661fc" + }, + "@std/fs@1.0.15": { + "integrity": "c083fb479889d6440d768e498195c3fc499d426fbf9a6592f98f53884d1d3f41", + "dependencies": [ + "jsr:@std/path@^1.0.8" + ] + }, + "@std/fs@1.0.19": { + "integrity": "051968c2b1eae4d2ea9f79a08a3845740ef6af10356aff43d3e2ef11ed09fb06", + "dependencies": [ + "jsr:@std/internal@^1.0.9", + "jsr:@std/path@^1.1.1" + ] + }, + "@std/internal@1.0.10": { + "integrity": "e3be62ce42cab0e177c27698e5d9800122f67b766a0bea6ca4867886cbde8cf7" + }, + "@std/io@0.225.2": { + "integrity": "3c740cd4ee4c082e6cfc86458f47e2ab7cb353dc6234d5e9b1f91a2de5f4d6c7" + }, + "@std/log@0.224.14": { + "integrity": "257f7adceee3b53bb2bc86c7242e7d1bc59729e57d4981c4a7e5b876c808f05e", + "dependencies": [ + "jsr:@std/fmt", + "jsr:@std/fs@^1.0.11", + "jsr:@std/io" + ] + }, + "@std/path@1.0.8": { + "integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be" + }, + "@std/path@1.1.1": { + "integrity": "fe00026bd3a7e6a27f73709b83c607798be40e20c81dde655ce34052fd82ec76", + "dependencies": [ + "jsr:@std/internal@^1.0.9" + ] + }, + "@std/semver@1.0.5": { + "integrity": "529f79e83705714c105ad0ba55bec0f9da0f24d2f726b6cc1c15e505cc2c0624" + } + }, + "npm": { + "zod@4.0.16": { + "integrity": "sha512-Djo/cM339grjI7/HmN+ixYO2FzEMcWr/On50UlQ/RjrWK1I/hPpWhpC76heCptnRFpH0LMwrEbUY50HDc0V8wg==" + } + }, "remote": { + "https://deno.land/std@0.221.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", + "https://deno.land/std@0.221.0/assert/_diff.ts": "4bf42969aa8b1a33aaf23eb8e478b011bfaa31b82d85d2ff4b5c4662d8780d2b", + "https://deno.land/std@0.221.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4", "https://deno.land/std@0.221.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", + "https://deno.land/std@0.221.0/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f", + "https://deno.land/std@0.221.0/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1", + "https://deno.land/std@0.221.0/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e", "https://deno.land/std@0.221.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9", + "https://deno.land/std@0.221.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769", + "https://deno.land/std@0.221.0/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c", + "https://deno.land/std@0.221.0/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219", + "https://deno.land/std@0.221.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444", + "https://deno.land/std@0.221.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2", + "https://deno.land/std@0.221.0/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005", + "https://deno.land/std@0.221.0/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0", + "https://deno.land/std@0.221.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1", + "https://deno.land/std@0.221.0/assert/assert_not_equals.ts": "ac86413ab70ffb14fdfc41740ba579a983fe355ba0ce4a9ab685e6b8e7f6a250", + "https://deno.land/std@0.221.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931", + "https://deno.land/std@0.221.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f", + "https://deno.land/std@0.221.0/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be", + "https://deno.land/std@0.221.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49", + "https://deno.land/std@0.221.0/assert/assert_rejects.ts": "5206ac37d883797d9504e3915a0c7b692df6efcdefff3889cc14bb5a325641dd", + "https://deno.land/std@0.221.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366", + "https://deno.land/std@0.221.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7", + "https://deno.land/std@0.221.0/assert/assert_throws.ts": "31f3c061338aec2c2c33731973d58ccd4f14e42f355501541409ee958d2eb8e5", "https://deno.land/std@0.221.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", + "https://deno.land/std@0.221.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2", + "https://deno.land/std@0.221.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c", + "https://deno.land/std@0.221.0/assert/mod.ts": "7e41449e77a31fef91534379716971bebcfc12686e143d38ada5438e04d4a90e", + "https://deno.land/std@0.221.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd", + "https://deno.land/std@0.221.0/assert/unreachable.ts": "3670816a4ab3214349acb6730e3e6f5299021234657eefe05b48092f3848c270", "https://deno.land/std@0.221.0/crypto/_wasm/lib/deno_std_wasm_crypto.generated.mjs": "f65ea775c52c5641f0154d98d6059e261ca3dc917a8856209d60bc6cb406e699", "https://deno.land/std@0.221.0/crypto/_wasm/mod.ts": "e89fbbc3c4722602ff975dd85f18273c7741ec766a9b68f6de4fd1d9876409f8", "https://deno.land/std@0.221.0/crypto/crypto.ts": "7ccd24e766d026d92ee1260b5a1639624775e94456d2a95c3a42fd3d49df78ab", @@ -128,6 +244,102 @@ "https://deno.land/std@0.221.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", "https://deno.land/std@0.221.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", "https://deno.land/std@0.221.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c", + "https://deno.land/std@0.221.0/testing/asserts.ts": "0cb9c745d9b157bed062a4aa8647168d2221f6456c385a548b0ca24de9e0f3ca", + "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", + "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", + "https://deno.land/std@0.224.0/fs/_create_walk_entry.ts": "5d9d2aaec05bcf09a06748b1684224d33eba7a4de24cf4cf5599991ca6b5b412", + "https://deno.land/std@0.224.0/fs/_to_path_string.ts": "29bfc9c6c112254961d75cbf6ba814d6de5349767818eb93090cecfa9665591e", + "https://deno.land/std@0.224.0/fs/walk.ts": "cddf87d2705c0163bff5d7767291f05b0f46ba10b8b28f227c3849cace08d303", + "https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", + "https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", + "https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c", + "https://deno.land/std@0.224.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", + "https://deno.land/std@0.224.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.224.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b", + "https://deno.land/std@0.224.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", + "https://deno.land/std@0.224.0/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d", + "https://deno.land/std@0.224.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.224.0/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3", + "https://deno.land/std@0.224.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607", + "https://deno.land/std@0.224.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", + "https://deno.land/std@0.224.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883", + "https://deno.land/std@0.224.0/path/_interface.ts": "8dfeb930ca4a772c458a8c7bbe1e33216fe91c253411338ad80c5b6fa93ddba0", + "https://deno.land/std@0.224.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", + "https://deno.land/std@0.224.0/path/basename.ts": "7ee495c2d1ee516ffff48fb9a93267ba928b5a3486b550be73071bc14f8cc63e", + "https://deno.land/std@0.224.0/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643", + "https://deno.land/std@0.224.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36", + "https://deno.land/std@0.224.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", + "https://deno.land/std@0.224.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441", + "https://deno.land/std@0.224.0/path/format.ts": "6ce1779b0980296cf2bc20d66436b12792102b831fd281ab9eb08fa8a3e6f6ac", + "https://deno.land/std@0.224.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", + "https://deno.land/std@0.224.0/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972", + "https://deno.land/std@0.224.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", + "https://deno.land/std@0.224.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", + "https://deno.land/std@0.224.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", + "https://deno.land/std@0.224.0/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0", + "https://deno.land/std@0.224.0/path/mod.ts": "f6bd79cb08be0e604201bc9de41ac9248582699d1b2ee0ab6bc9190d472cf9cd", + "https://deno.land/std@0.224.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", + "https://deno.land/std@0.224.0/path/normalize_glob.ts": "cc89a77a7d3b1d01053b9dcd59462b75482b11e9068ae6c754b5cf5d794b374f", + "https://deno.land/std@0.224.0/path/parse.ts": "77ad91dcb235a66c6f504df83087ce2a5471e67d79c402014f6e847389108d5a", + "https://deno.land/std@0.224.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", + "https://deno.land/std@0.224.0/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0", + "https://deno.land/std@0.224.0/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.224.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1", + "https://deno.land/std@0.224.0/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00", + "https://deno.land/std@0.224.0/path/posix/extname.ts": "e398c1d9d1908d3756a7ed94199fcd169e79466dd88feffd2f47ce0abf9d61d2", + "https://deno.land/std@0.224.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1", + "https://deno.land/std@0.224.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", + "https://deno.land/std@0.224.0/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f", + "https://deno.land/std@0.224.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", + "https://deno.land/std@0.224.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.224.0/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63", + "https://deno.land/std@0.224.0/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.224.0/path/posix/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.224.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", + "https://deno.land/std@0.224.0/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.224.0/path/posix/parse.ts": "09dfad0cae530f93627202f28c1befa78ea6e751f92f478ca2cc3b56be2cbb6a", + "https://deno.land/std@0.224.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c", + "https://deno.land/std@0.224.0/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf", + "https://deno.land/std@0.224.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf", + "https://deno.land/std@0.224.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0", + "https://deno.land/std@0.224.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add", + "https://deno.land/std@0.224.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", + "https://deno.land/std@0.224.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b", + "https://deno.land/std@0.224.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40", + "https://deno.land/std@0.224.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", + "https://deno.land/std@0.224.0/path/windows/basename.ts": "6bbc57bac9df2cec43288c8c5334919418d784243a00bc10de67d392ab36d660", + "https://deno.land/std@0.224.0/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.224.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5", + "https://deno.land/std@0.224.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", + "https://deno.land/std@0.224.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef", + "https://deno.land/std@0.224.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6", + "https://deno.land/std@0.224.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", + "https://deno.land/std@0.224.0/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8", + "https://deno.land/std@0.224.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", + "https://deno.land/std@0.224.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.224.0/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf", + "https://deno.land/std@0.224.0/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.224.0/path/windows/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.224.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", + "https://deno.land/std@0.224.0/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.224.0/path/windows/parse.ts": "08804327b0484d18ab4d6781742bf374976de662f8642e62a67e93346e759707", + "https://deno.land/std@0.224.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", + "https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", + "https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", + "https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c", "https://deno.land/x/semver@v1.4.1/mod.ts": "0b79c87562eb8a1f008ab0d98f8bb60076dd65bc06f1f8fdfac2d2dab162c27b" + }, + "workspace": { + "dependencies": [ + "jsr:@std/assert@^1.0.13", + "jsr:@std/cli@^1.0.21", + "jsr:@std/crypto@^1.0.5", + "jsr:@std/encoding@^1.0.10", + "jsr:@std/fs@^1.0.19", + "jsr:@std/log@~0.224.14", + "jsr:@std/path@^1.1.1", + "jsr:@std/semver@^1.0.5", + "npm:zod@^4.0.16" + ] } } diff --git a/deps.ts b/deps.ts deleted file mode 100644 index 3b31d86..0000000 --- a/deps.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as flags from "https://deno.land/std@0.221.0/flags/mod.ts"; -import * as path from "https://deno.land/std@0.221.0/path/mod.ts"; -import * as log from "https://deno.land/std@0.221.0/log/mod.ts"; -import * as fs from "https://deno.land/std@0.221.0/fs/mod.ts"; -import { crypto } from "https://deno.land/std@0.221.0/crypto/mod.ts"; -import * as semver from "https://deno.land/x/semver@v1.4.1/mod.ts"; - -export { crypto, flags, fs, log, path, semver }; diff --git a/dnit.ts b/dnit.ts deleted file mode 100644 index fbd8c9d..0000000 --- a/dnit.ts +++ /dev/null @@ -1,812 +0,0 @@ -import { crypto, flags, log, path } from "./deps.ts"; -import { version } from "./version.ts"; - -import { textTable } from "./textTable.ts"; - -import type * as A from "./adl-gen/dnit/manifest.ts"; -import { Manifest, TaskManifest } from "./manifest.ts"; - -import { AsyncQueue } from "./asyncQueue.ts"; - -class ExecContext { - /// All tasks by name - taskRegister = new Map(); - - /// Tasks by target - targetRegister = new Map(); - - /// Done or up-to-date tasks - doneTasks = new Set(); - - /// In progress tasks - inprogressTasks = new Set(); - - /// Queue for scheduling async work with specified number allowable concurrently. - // deno-lint-ignore no-explicit-any - asyncQueue: AsyncQueue; - - internalLogger = log.getLogger("internal"); - taskLogger = log.getLogger("task"); - userLogger = log.getLogger("user"); - - constructor( - /// loaded hash manifest - readonly manifest: Manifest, - /// commandline args - readonly args: flags.Args, - ) { - if (args["verbose"] !== undefined) { - this.internalLogger.levelName = "INFO"; - } - - const concurrency = args["concurrency"] || 4; - this.asyncQueue = new AsyncQueue(concurrency); - - this.internalLogger.info(`Starting ExecContext version: ${version}`); - } - - getTaskByName(name: A.TaskName): Task | undefined { - return this.taskRegister.get(name); - } -} - -export interface TaskContext { - logger: log.Logger; - task: Task; - args: flags.Args; - exec: ExecContext; -} - -function taskContext(ctx: ExecContext, task: Task): TaskContext { - return { - logger: ctx.taskLogger, - task, - args: ctx.args, - exec: ctx, - }; -} - -export type Action = (ctx: TaskContext) => Promise | void; - -export type IsUpToDate = (ctx: TaskContext) => Promise | boolean; -export type GetFileHash = ( - filename: A.TrackedFileName, - stat: Deno.FileInfo, -) => Promise | A.TrackedFileHash; -export type GetFileTimestamp = ( - filename: A.TrackedFileName, - stat: Deno.FileInfo, -) => Promise | A.Timestamp; - -/** User definition of a task */ -export type TaskParams = { - /// Name: (string) - The key used to initiate a task - name: A.TaskName; - - /// Description (string) - Freeform text description shown on help - description?: string; - - /// Action executed on execution of the task (async or sync) - action: Action; - - /// Optional list of task or file dependencies - deps?: Dep[]; - - /// Targets (files which will be produced by execution of this task) - targets?: TrackedFile[]; - - /// Custom up-to-date definition - Can be used to make a task *less* up to date. Eg; use uptodate: runAlways to run always on request regardless of dependencies being up to date. - uptodate?: IsUpToDate; -}; - -/// The kinds of supported dependencies. -export type Dep = Task | TrackedFile | TrackedFilesAsync; - -/// Convenience function: an up to date always false to run always -export const runAlways: IsUpToDate = () => false; - -function isTask(dep: Task | TrackedFile | TrackedFilesAsync): dep is Task { - return dep instanceof Task; -} -function isTrackedFile( - dep: Task | TrackedFile | TrackedFilesAsync, -): dep is TrackedFile { - return dep instanceof TrackedFile; -} -function isTrackedFileAsync( - dep: Task | TrackedFile | TrackedFilesAsync, -): dep is TrackedFilesAsync { - return dep instanceof TrackedFilesAsync; -} - -type StatResult = - | { - kind: "fileInfo"; - fileInfo: Deno.FileInfo; - } - | { - kind: "nonExistent"; - }; - -async function statPath(path: A.TrackedFileName): Promise { - try { - const fileInfo = await Deno.stat(path); - return { - kind: "fileInfo", - fileInfo, - }; - } catch (err) { - if (err instanceof Deno.errors.NotFound) { - return { - kind: "nonExistent", - }; - } - throw err; - } -} - -async function deletePath(path: A.TrackedFileName): Promise { - try { - await Deno.remove(path, { recursive: true }); - } catch (err) { - // Ignore NotFound errors - if (!(err instanceof Deno.errors.NotFound)) { - console.log("Error deleting path: ", path, err); - } - } -} - -export class Task { - public name: A.TaskName; - public description?: string; - public action: Action; - public task_deps: Set; - public file_deps: Set; - public async_files_deps: Set; - public targets: Set; - - public taskManifest: TaskManifest | null = null; - public uptodate?: IsUpToDate; - - constructor(taskParams: TaskParams) { - this.name = taskParams.name; - this.action = taskParams.action; - this.description = taskParams.description; - this.task_deps = new Set( - this.getTaskDeps(taskParams.deps || []), - ); - this.file_deps = new Set( - this.getTrackedFiles(taskParams.deps || []), - ); - this.async_files_deps = new Set( - this.getTrackedFilesAsync(taskParams.deps || []), - ); - this.targets = new Set(taskParams.targets || []); - this.uptodate = taskParams.uptodate; - - for (const f of this.targets) { - f.setTask(this); - } - } - - private getTaskDeps( - deps: (Task | TrackedFile | TrackedFilesAsync)[], - ): Task[] { - return deps.filter(isTask); - } - private getTrackedFiles( - deps: (Task | TrackedFile | TrackedFilesAsync)[], - ): TrackedFile[] { - return deps.filter(isTrackedFile); - } - private getTrackedFilesAsync( - deps: (Task | TrackedFile | TrackedFilesAsync)[], - ): TrackedFilesAsync[] { - return deps.filter(isTrackedFileAsync); - } - - async setup(ctx: ExecContext): Promise { - if (this.taskManifest === null) { - for (const t of this.targets) { - ctx.targetRegister.set(t.path, this); - } - - this.taskManifest = ctx.manifest.tasks.getOrInsert( - this.name, - new TaskManifest({ - lastExecution: null, - trackedFiles: [], - }), - ); - - // ensure preceding tasks are setup too - for (const taskDep of this.task_deps) { - await taskDep.setup(ctx); - } - for (const fDep of this.file_deps) { - const fDepTask = fDep.getTask(); - if (fDepTask !== null) { - await fDepTask.setup(ctx); - } - } - } - } - - async exec(ctx: ExecContext): Promise { - if (ctx.doneTasks.has(this)) { - return; - } - if (ctx.inprogressTasks.has(this)) { - return; - } - - ctx.inprogressTasks.add(this); - - // evaluate async file_deps (useful if task depends on a glob of the filesystem) - for (const afd of this.async_files_deps) { - const fileDeps = await afd.getTrackedFiles(); - for (const fd of fileDeps) { - this.file_deps.add(fd); - } - } - - // add task dep on the task that makes the file if its a target - for (const fd of this.file_deps) { - const t = ctx.targetRegister.get(fd.path); - if (t !== undefined) { - this.task_deps.add(t); - } - } - - await this.execDependencies(ctx); - - let actualUpToDate = true; - - actualUpToDate = actualUpToDate && await this.checkFileDeps(ctx); - ctx.internalLogger.info(`${this.name} checkFileDeps ${actualUpToDate}`); - - actualUpToDate = actualUpToDate && await this.targetsExist(ctx); - ctx.internalLogger.info(`${this.name} targetsExist ${actualUpToDate}`); - - if (this.uptodate !== undefined) { - actualUpToDate = actualUpToDate && - await this.uptodate(taskContext(ctx, this)); - } - ctx.internalLogger.info(`${this.name} uptodate ${actualUpToDate}`); - - if (actualUpToDate) { - ctx.taskLogger.info(`--- ${this.name}`); - } else { - // suppress logging the task "{-- name --}" for the list task - const logTaskScope = this.name !== "list"; - if (logTaskScope) ctx.taskLogger.info(`{-- ${this.name}`); - await this.action(taskContext(ctx, this)); - if (logTaskScope) ctx.taskLogger.info(`--} ${this.name}`); - - { - /// recalc & save data of deps: - this.taskManifest?.setExecutionTimestamp(); - const promisesInProgress: Promise[] = []; - for (const fdep of this.file_deps) { - promisesInProgress.push( - ctx.asyncQueue.schedule(async () => { - const trackedFileData = await fdep.getFileData(ctx); - this.taskManifest?.setFileData(fdep.path, trackedFileData); - }), - ); - } - await Promise.all(promisesInProgress); - } - } - - ctx.doneTasks.add(this); - ctx.inprogressTasks.delete(this); - } - - async reset(ctx: ExecContext): Promise { - await this.cleanTargets(ctx); - } - - private async cleanTargets(ctx: ExecContext): Promise { - await Promise.all( - Array.from(this.targets).map(async (tf) => { - try { - await ctx.asyncQueue.schedule(() => tf.delete()); - } catch (err) { - ctx.taskLogger.error(`Error scheduling deletion of ${tf.path}`, err); - } - }), - ); - } - - private async targetsExist(ctx: ExecContext): Promise { - const tex = await Promise.all( - Array.from(this.targets).map((tf) => - ctx.asyncQueue.schedule(() => tf.exists()) - ), - ); - // all exist: NOT some NOT exist - return !tex.some((t) => !t); - } - - private async checkFileDeps(ctx: ExecContext): Promise { - let fileDepsUpToDate = true; - let promisesInProgress: Promise[] = []; - - const taskManifest = this.taskManifest; - if (taskManifest === null) { - throw new Error(`Invalid null taskManifest on ${this.name}`); - } - - for (const fdep of this.file_deps) { - promisesInProgress.push( - ctx.asyncQueue.schedule(async () => { - const r = await fdep.getFileDataOrCached( - ctx, - taskManifest.getFileData(fdep.path), - ); - taskManifest.setFileData(fdep.path, r.tData); - fileDepsUpToDate = fileDepsUpToDate && r.upToDate; - }), - ); - } - await Promise.all(promisesInProgress); - promisesInProgress = []; - return fileDepsUpToDate; - } - - private async execDependencies(ctx: ExecContext) { - for (const dep of this.task_deps) { - if (!ctx.doneTasks.has(dep) && !ctx.inprogressTasks.has(dep)) { - await dep.exec(ctx); - } - } - } -} - -export class TrackedFile { - path: A.TrackedFileName = ""; - #getHash: GetFileHash; - #getTimestamp: GetFileTimestamp; - - fromTask: Task | null = null; - - constructor(fileParams: FileParams) { - this.path = path.resolve(fileParams.path); - this.#getHash = fileParams.getHash || getFileSha1Sum; - this.#getTimestamp = fileParams.getTimestamp || getFileTimestamp; - } - - private async stat(): Promise { - log.getLogger("internal").info(`checking file ${this.path}`); - return await statPath(this.path); - } - - async delete(): Promise { - await deletePath(this.path); - } - - async exists(statInput?: StatResult): Promise { - let statResult = statInput; - if (statResult === undefined) { - statResult = await this.stat(); - } - return statResult.kind === "fileInfo"; - } - - async getHash(statInput?: StatResult) { - let statResult = statInput; - if (statResult === undefined) { - statResult = await this.stat(); - } - if (statResult.kind !== "fileInfo") { - return ""; - } - - log.getLogger("internal").info(`checking hash on ${this.path}`); - return this.#getHash(this.path, statResult.fileInfo); - } - - async getTimestamp(statInput?: StatResult) { - let statResult = statInput; - if (statResult === undefined) { - statResult = await this.stat(); - } - if (statResult.kind !== "fileInfo") { - return ""; - } - return this.#getTimestamp(this.path, statResult.fileInfo); - } - - /// whether this is up to date w.r.t. the given TrackedFileData - async isUpToDate( - _ctx: ExecContext, - tData: A.TrackedFileData | undefined, - statInput?: StatResult, - ): Promise { - if (tData === undefined) { - return false; - } - - let statResult = statInput; - if (statResult === undefined) { - statResult = await this.stat(); - } - - const mtime = await this.getTimestamp(statResult); - if (mtime === tData.timestamp) { - return true; - } - const hash = await this.getHash(statResult); - return hash === tData.hash; - } - - /// Recalculate timestamp and hash data - async getFileData( - _ctx: ExecContext, - statInput?: StatResult, - ): Promise { - let statResult = statInput; - if (statResult === undefined) { - statResult = await this.stat(); - } - return { - hash: await this.getHash(statResult), - timestamp: await this.getTimestamp(statResult), - }; - } - - /// return given tData if up to date or re-calculate - async getFileDataOrCached( - ctx: ExecContext, - tData: A.TrackedFileData | undefined, - statInput?: StatResult, - ): Promise<{ - tData: A.TrackedFileData; - upToDate: boolean; - }> { - let statResult = statInput; - if (statResult === undefined) { - statResult = await this.stat(); - } - - if (tData !== undefined && await this.isUpToDate(ctx, tData, statResult)) { - return { - tData, - upToDate: true, - }; - } - return { - tData: await this.getFileData(ctx, statResult), - upToDate: false, - }; - } - - setTask(t: Task) { - if (this.fromTask === null) { - this.fromTask = t; - } else { - throw new Error( - "Duplicate tasks generating TrackedFile as target - " + this.path, - ); - } - } - - getTask(): Task | null { - return this.fromTask; - } -} - -export type GenTrackedFiles = () => Promise | TrackedFile[]; - -export class TrackedFilesAsync { - kind: "trackedfilesasync" = "trackedfilesasync"; - - constructor(public gen: GenTrackedFiles) { - } - - async getTrackedFiles(): Promise { - return await this.gen(); - } -} - -export async function getFileSha1Sum( - filename: string, -): Promise { - const data = await Deno.readFile(filename); - const hashBuffer = await crypto.subtle.digest("SHA-1", data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join( - "", - ); - return hashHex; -} - -export function getFileTimestamp( - _filename: string, - stat: Deno.FileInfo, -): A.Timestamp { - const mtime = stat.mtime; - return mtime?.toISOString() || ""; -} - -/** User params for a tracked file */ -export type FileParams = { - /// File path - path: string; - - /// Optional function for how to hash the file. Defaults to the sha1 hash of the file contents. - /// A file is out of date if the file timestamp and the hash are different than that in the task manifest - getHash?: GetFileHash; - - /// Optional function for how to get the file timestamp. Defaults to the actual file timestamp - getTimestamp?: GetFileTimestamp; -}; - -/** Generate a trackedfile for tracking */ -export function file(fileParams: FileParams | string): TrackedFile { - if (typeof fileParams === "string") { - return new TrackedFile({ path: fileParams }); - } - return new TrackedFile(fileParams); -} -export function trackFile(fileParams: FileParams | string): TrackedFile { - return file(fileParams); -} - -export function asyncFiles(gen: GenTrackedFiles): TrackedFilesAsync { - return new TrackedFilesAsync(gen); -} - -/** Generate a task */ -export function task(taskParams: TaskParams): Task { - const task = new Task(taskParams); - return task; -} - -function showTaskList(ctx: ExecContext, args: flags.Args) { - if (args["quiet"]) { - Array.from(ctx.taskRegister.values()).map((task) => console.log(task.name)); - } else { - console.log( - textTable( - ["Name", "Description"], - Array.from(ctx.taskRegister.values()).map((t) => [ - t.name, - t.description || "", - ]), - ), - ); - } -} - -function echoBashCompletionScript() { - console.log( - "# bash completion for dnit\n" + - "# auto-generate by `dnit tabcompletion`\n" + - "\n" + - "# to activate it you need to 'source' the generated script\n" + - "# $ source <(dnit tabcompletion)\n" + - "\n" + - "_dnit() \n" + - "{\n" + - " local cur prev words cword basetask sub_cmds tasks i dodof\n" + - " COMPREPLY=() # contains list of words with suitable completion\n" + - " _get_comp_words_by_ref -n : cur prev words cword\n" + - " # list of sub-commands\n" + - ' sub_cmds="list"\n' + - "\n" + - " tasks=$(dnit list --quiet 2>/dev/null)\n" + - "\n" + - ' COMPREPLY=( $(compgen -W "${sub_cmds} ${tasks}" -- ${cur}) )\n' + - " return 0\n" + - "}\n" + - "\n" + - "\n" + - "complete -o filenames -F _dnit dnit \n", - ); -} - -/// StdErr plaintext handler (no color codes) -class StdErrPlainHandler extends log.BaseHandler { - constructor(levelName: log.LevelName) { - super(levelName, { - formatter: (rec) => rec.msg, - }); - } - - log(msg: string): void { - Deno.stderr.writeSync(new TextEncoder().encode(msg + "\n")); - } -} - -/// StdErr handler on top of ConsoleHandler (which uses colors) -class StdErrHandler extends log.ConsoleHandler { - log(msg: string): void { - Deno.stderr.writeSync(new TextEncoder().encode(msg + "\n")); - } -} - -export async function setupLogging() { - await log.setup({ - handlers: { - stderr: new StdErrHandler("DEBUG"), - stderrPlain: new StdErrPlainHandler("DEBUG"), - }, - - loggers: { - // internals of dnit tooling - internal: { - level: "WARN", - handlers: ["stderrPlain"], - }, - - // basic events eg start of task or task already up to date - task: { - level: "INFO", - handlers: ["stderrPlain"], - }, - - // for user to use within task actions - user: { - level: "INFO", - handlers: ["stderrPlain"], - }, - }, - }); -} - -/** Convenience access to a setup logger for tasks */ -export function getLogger(): log.Logger { - return log.getLogger("user"); -} - -export type ExecResult = { - success: boolean; -}; - -const builtinTasks = [ - task({ - name: "clean", - description: "Clean tracked files", - action: async (ctx: TaskContext) => { - const positionalArgs = ctx.args["_"]; - - const affectedTasks: Task[] = positionalArgs.length > 1 - ? positionalArgs.map((arg) => ctx.exec.taskRegister.get(String(arg))) - .filter((task) => task !== undefined) as Task[] - : Array.from(ctx.exec.taskRegister.values()); - if (affectedTasks.length > 0) { - console.log("Clean tasks:"); - /// Reset tasks - await Promise.all( - affectedTasks.map((t) => { - console.log(` ${t.name}`); - ctx.exec.asyncQueue.schedule(() => t.reset(ctx.exec)); - }), - ); - // await ctx.exec.manifest.save(); - } - }, - uptodate: runAlways, - }), - - task({ - name: "list", - description: "List tasks", - action: (ctx: TaskContext) => { - showTaskList(ctx.exec, ctx.args); - }, - uptodate: runAlways, - }), - - task({ - name: "tabcompletion", - description: "Generate shell completion script", - action: () => { - // todo: detect shell type and generate appropriate script - // or add args for shell type - echoBashCompletionScript(); - }, - uptodate: runAlways, - }), -]; - -/** Execute given commandline args and array of items (task & trackedfile) */ -export async function execCli( - cliArgs: string[], - tasks: Task[], -): Promise { - const args = flags.parse(cliArgs); - - await setupLogging(); - - /// directory of user's entrypoint source as discovered by 'launch' util: - const dnitDir = args["dnitDir"] || "./dnit"; - delete args["dnitDir"]; - - const ctx = new ExecContext(new Manifest(dnitDir), args); - - /// register tasks as provided by user's source: - tasks.forEach((t) => ctx.taskRegister.set(t.name, t)); - - /// register built-in tasks: - for (const t of builtinTasks) { - ctx.taskRegister.set(t.name, t); - } - - let requestedTaskName: string | null = null; - const positionalArgs = args["_"]; - if (positionalArgs.length > 0) { - requestedTaskName = `${positionalArgs[0]}`; - } - - if (requestedTaskName === null) { - requestedTaskName = "list"; - } - - try { - /// Load manifest (dependency tracking data) - await ctx.manifest.load(); - - /// Run async setup on all tasks: - await Promise.all( - Array.from(ctx.taskRegister.values()).map((t) => - ctx.asyncQueue.schedule(() => t.setup(ctx)) - ), - ); - - /// Find the requested task: - const requestedTask = ctx.taskRegister.get(requestedTaskName); - if (requestedTask !== undefined) { - /// Execute the requested task: - await requestedTask.exec(ctx); - } else { - ctx.taskLogger.error(`Task ${requestedTaskName} not found`); - } - - /// Save manifest (dependency tracking data) - await ctx.manifest.save(); - - return { success: true }; - } catch (err) { - ctx.taskLogger.error("Error", err); - throw err; - } -} - -/// No-frills setup of an ExecContext (mainly for testing) -export async function execBasic( - cliArgs: string[], - tasks: Task[], - manifest: Manifest, -): Promise { - const args = flags.parse(cliArgs); - const ctx = new ExecContext(manifest, args); - tasks.forEach((t) => ctx.taskRegister.set(t.name, t)); - - /// register built-in tasks: - for (const t of builtinTasks) { - ctx.taskRegister.set(t.name, t); - } - - await Promise.all( - Array.from(ctx.taskRegister.values()).map((t) => - ctx.asyncQueue.schedule(() => t.setup(ctx)) - ), - ); - return ctx; -} - -/// main function for use in dnit scripts -export function main( - cliArgs: string[], - tasks: Task[], -): void { - execCli(cliArgs, tasks) - .then(() => Deno.exit(0)) - .catch((err) => { - console.error("error in main", err); - Deno.exit(1); - }); -} diff --git a/dnit/.denoversion b/dnit/.denoversion index ba7c934..a0d4707 100644 --- a/dnit/.denoversion +++ b/dnit/.denoversion @@ -1 +1 @@ ->=1.16.4 <=1.42.0 +>=2.1 <3 diff --git a/dnit/deps.ts b/dnit/deps.ts deleted file mode 100644 index cce7ddc..0000000 --- a/dnit/deps.ts +++ /dev/null @@ -1,11 +0,0 @@ -// refer to own sources for ease of development -import { file, main, task } from "../dnit.ts"; -import * as utils from "../utils.ts"; - -import * as flags from "https://deno.land/std@0.221.0/flags/mod.ts"; -import * as path from "https://deno.land/std@0.221.0/path/mod.ts"; -import * as log from "https://deno.land/std@0.221.0/log/mod.ts"; -import * as fs from "https://deno.land/std@0.221.0/fs/mod.ts"; -import * as semver from "https://deno.land/x/semver@v1.4.1/mod.ts"; - -export { file, flags, fs, log, main, path, semver, task, utils }; diff --git a/dnit/main.ts b/dnit/main.ts index 975a5bb..6d330cc 100644 --- a/dnit/main.ts +++ b/dnit/main.ts @@ -1,5 +1,6 @@ -import { flags, log, semver, task, utils } from "./deps.ts"; -import { file, main, runAlways, TaskContext } from "../dnit.ts"; +import { main, runAlways, task, type TaskContext } from "../mod.ts"; +import * as semver from "@std/semver"; +import type { Args as CliArgs } from "@std/cli/parse-args"; import { fetchTags, @@ -7,12 +8,12 @@ import { gitLatestTag, requireCleanGit, } from "../utils/git.ts"; -import { fs } from "../deps.ts"; +import * as fs from "@std/fs"; import { runConsole } from "../utils.ts"; const tagPrefix = "dnit-v"; -async function getNextTagVersion(args: flags.Args): Promise { +async function getNextTagVersion(args: CliArgs): Promise { const current = await gitLatestTag(tagPrefix); type Args = { @@ -24,7 +25,9 @@ async function getNextTagVersion(args: flags.Args): Promise { const increment: "major" | "minor" | "patch" = args.major ? "major" : (xargs.minor ? "minor" : ("patch")); - const next = semver.inc(current, increment); + const next = semver.format( + semver.increment(semver.parse(current), increment), + ); return next; } @@ -32,8 +35,6 @@ const tag = task({ name: "tag", description: "Run git tag", action: async (ctx: TaskContext) => { - const current = await gitLatestTag(tagPrefix); - type Args = { "major"?: true; "minor"?: true; @@ -42,11 +43,13 @@ const tag = task({ "origin"?: string; "dry-run"?: true; }; + + const next = await getNextTagVersion(ctx.args); + const args: Args = ctx.args as Args; const increment: "major" | "minor" | "patch" = args.major ? "major" : (args.minor ? "minor" : ("patch")); - const next = semver.inc(current, increment); const tagMessage = args.message || `Tag ${increment} to ${next}`; const tagName = `${tagPrefix}${next}`; @@ -58,16 +61,17 @@ const tag = task({ console.log("Last commit: " + gitLastCommit); const conf = confirm( - `Git tag and push ${tagMessage} tagName?`, + `Git tag and push ${tagName} with message: ${tagMessage}?`, ); if (conf) { const cmds = dryRun ? ["echo"] : []; - await utils.runConsole( + await runConsole( cmds.concat(["git", "tag", "-a", "-m", tagMessage, tagName]), ); - await utils.runConsole(cmds.concat(["git", "push", origin, tagName])); - log.info( + await runConsole(cmds.concat(["git", "push", origin, tagName])); + + ctx.logger.info( `${ dryRun ? "(dry-run) " : "" }Git tagged and pushed ${tagPrefix}${next}`, @@ -91,7 +95,7 @@ const push = task({ name: "push", description: "Run git push", action: async () => { - await utils.runConsole(["git", "push", "origin", "main"]); + await runConsole(["git", "push", "origin", "main"]); }, deps: [ requireCleanGit, @@ -169,66 +173,17 @@ const release = task({ uptodate: runAlways, }); -const genadl = task({ - name: "genadl", - description: "Code generate from ADL definition", - action: async () => { - await utils.runConsole(["./tools/gen-adl.sh"]); - await utils.runConsole( - ["git", "apply", "./tools/0001-Revert-non-desired-gen-adl-edits.patch"], - ); - }, - deps: [ - file({ path: "./adl/manifest.adl" }), - file({ path: "./tools/0001-Revert-non-desired-gen-adl-edits.patch" }), - ], -}); - -const updategenadlfix = task({ - name: "updategenadlfix", - description: "Update the patch that fixes the generated code", - action: async () => { - await utils.runConsole(["./tools/gen-adl.sh"]); - await utils.runConsole(["git", "commit", "-am", "Generated adl"]); - await utils.runConsole(["git", "revert", "HEAD", "--no-edit"]); - await utils.runConsole([ - "git", - "commit", - "--amend", - "-m", - "Revert non desired gen-adl edits", - ]); - await utils.runConsole(["git", "format-patch", "-1", "HEAD"]); - await utils.runConsole([ - "mv", - "0001-Revert-non-desired-gen-adl-edits.patch", - "./tools", - ]); - await utils.runConsole([ - "git", - "commit", - "-am", - "Updated gen-adl fix patch", - ]); - }, - deps: [ - requireCleanGit, - ], - uptodate: runAlways, -}); - const test = task({ name: "test", description: "Run local unit tests", action: async () => { - await utils.runConsole([ + await runConsole([ "deno", "test", "--allow-read", "--allow-write", - ], { - cwd: "./tests", - }); + "--allow-run", + ]); }, deps: [], uptodate: runAlways, @@ -238,7 +193,7 @@ const killTest = task({ name: "killTest", description: "Test what happens when killing via signals", action: async () => { - await utils.runConsole([ + await runConsole([ "bash", "-c", "echo $$; trap '' 2; echo helloworld; sleep 30s; echo done", @@ -248,15 +203,64 @@ const killTest = task({ uptodate: runAlways, }); +const sourceCheckEntryPoints: string[] = [ + "launch.ts", + "mod.ts", + "dnit/main.ts", +]; + +const check = task({ + name: "check", + description: "Run local checks", + action: async () => { + await Promise.all(sourceCheckEntryPoints.map(async (path) => { + await runConsole([ + "deno", + "check", + path, + ]); + })); + }, + deps: [], + uptodate: runAlways, +}); + +const lint = task({ + name: "lint", + description: "Run local lint", + action: async () => { + await runConsole([ + "deno", + "lint", + ]); + }, + deps: [], + uptodate: runAlways, +}); + +const fmt = task({ + name: "fmt", + description: "Run local fmt", + action: async () => { + await runConsole([ + "deno", + "fmt", + ]); + }, + deps: [], + uptodate: runAlways, +}); + const tasks = [ test, - genadl, tag, push, - updategenadlfix, makeReleaseEdits, release, killTest, + check, + lint, + fmt, ]; main(Deno.args, tasks); diff --git a/example/.gitignore b/example/.gitignore deleted file mode 100644 index 721b360..0000000 --- a/example/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/dnit/.manifest.json -/msg.txt diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..ba64938 --- /dev/null +++ b/example/README.md @@ -0,0 +1,46 @@ +# Hello World Example + +This is a simple example demonstrating basic dnit usage. + +## Setup + +From the dnit project root, navigate to this example: + +```bash +cd example +``` + +## Usage + +If you have dnit installed globally: + +```bash +dnit list +dnit hello +dnit show +dnit cleanup +``` + +Or run directly with Deno: + +```bash +deno run --allow-read --allow-write --allow-run ../main.ts list +deno run --allow-read --allow-write --allow-run ../main.ts hello +deno run --allow-read --allow-write --allow-run ../main.ts show +deno run --allow-read --allow-write --allow-run ../main.ts cleanup +``` + +## What This Demonstrates + +- **Task definition**: How to create tasks with `task()` +- **File tracking**: Using `trackFile()` to track dependencies and targets +- **Task dependencies**: The `show` task depends on `hello.txt` existing +- **File I/O**: Reading and writing files in task actions +- **Error handling**: Graceful handling of missing files in cleanup + +## Key Concepts + +1. **Tasks** are defined with names, descriptions, and actions +2. **Dependencies** ensure tasks run in the correct order +3. **Targets** are files that tasks produce +4. **Tracking** helps dnit determine when tasks need to re-run diff --git a/example/dnit/.manifest.json b/example/dnit/.manifest.json new file mode 100644 index 0000000..ad88893 --- /dev/null +++ b/example/dnit/.manifest.json @@ -0,0 +1,33 @@ +{ + "tasks": { + "hello": { + "lastExecution": "2025-08-04T09:38:33.314Z", + "trackedFiles": {} + }, + "show": { + "lastExecution": "2025-08-04T09:38:40.612Z", + "trackedFiles": { + "/home/pt/pt/dnit/example/hello.txt": { + "hash": "60fde9c2310b0d4cad4dab8d126b04387efba289", + "timestamp": "2025-08-04T09:38:33.312Z" + } + } + }, + "cleanup": { + "lastExecution": null, + "trackedFiles": {} + }, + "clean": { + "lastExecution": null, + "trackedFiles": {} + }, + "list": { + "lastExecution": "2025-08-09T07:52:09.004Z", + "trackedFiles": {} + }, + "tabcompletion": { + "lastExecution": null, + "trackedFiles": {} + } + } +} \ No newline at end of file diff --git a/example/dnit/deps.ts b/example/dnit/deps.ts deleted file mode 100644 index 46ba351..0000000 --- a/example/dnit/deps.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { - file, - main, - task, -} from "https://deno.land/x/dnit@dnit-v1.14.4/dnit.ts"; -import * as flags from "https://deno.land/std@0.221.0/flags/mod.ts"; -import * as path from "https://deno.land/std@0.221.0/path/mod.ts"; -import * as log from "https://deno.land/std@0.221.0/log/mod.ts"; -import * as fs from "https://deno.land/std@0.221.0/fs/mod.ts"; - -export { file, flags, fs, log, main, path, task }; diff --git a/example/dnit/goodBye.ts b/example/dnit/goodBye.ts deleted file mode 100644 index f407d23..0000000 --- a/example/dnit/goodBye.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { task } from "./deps.ts"; -import { msg } from "./helloWorld.ts"; - -//import { red } from "fmt/colors.ts"; - -//console.log(red("hello world")); - -export const goodbye = task({ - name: "goodbye", - action: async () => { - // use ordinary typescript idiomatically if several actions are required - const actions = [ - async () => { - const txt = await Deno.readTextFile(msg.path); - console.log(txt); - }, - ]; - for (const action of actions) { - await action(); - } - }, - deps: [msg], -}); diff --git a/example/dnit/helloWorld.ts b/example/dnit/helloWorld.ts deleted file mode 100644 index 5bffba1..0000000 --- a/example/dnit/helloWorld.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { file, task } from "./deps.ts"; - -export const msg = file({ - path: "./msg.txt", -}); - -export const helloWorld = task({ - name: "helloWorld", - description: "foo", - action: async () => { - const cmd = new Deno.Command("sh", { - args: ["./writeMsg.sh"], - }); - await cmd.output(); - }, - deps: [ - file({ - path: "./writeMsg.sh", - }), - ], - targets: [ - msg, - ], -}); diff --git a/example/dnit/import_map.json b/example/dnit/import_map.json deleted file mode 100644 index 6842e29..0000000 --- a/example/dnit/import_map.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "imports": { - "fmt/": "https://deno.land/std@0.221.0/fmt/" - } -} diff --git a/example/dnit/main.ts b/example/dnit/main.ts index 20bfa27..eff4c1c 100644 --- a/example/dnit/main.ts +++ b/example/dnit/main.ts @@ -1,10 +1,48 @@ -import { main } from "./deps.ts"; -import { helloWorld } from "./helloWorld.ts"; -import { goodbye } from "./goodBye.ts"; +import { main, task, trackFile } from "../../mod.ts"; -const tasks = [ - helloWorld, - goodbye, -]; +// A simple task that creates a hello world message +const helloWorld = task({ + name: "hello", + description: "Create a hello world message", + action: async () => { + await Deno.writeTextFile("hello.txt", "Hello, World!\n"); + console.log("Created hello.txt with greeting!"); + }, + targets: [ + trackFile({ path: "hello.txt" }), + ], +}); -main(Deno.args, tasks); +// A task that reads and displays the message +const showMessage = task({ + name: "show", + description: "Display the hello world message", + action: async () => { + const message = await Deno.readTextFile("hello.txt"); + console.log("Message contents:", message.trim()); + }, + deps: [ + trackFile({ path: "hello.txt" }), + ], +}); + +// A task that cleans up +const cleanup = task({ + name: "cleanup", + description: "Remove the hello.txt file", + action: async () => { + try { + await Deno.remove("hello.txt"); + console.log("Cleaned up hello.txt"); + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + console.log("hello.txt already removed"); + } else { + throw error; + } + } + }, +}); + +// Register tasks with dnit +main(Deno.args, [helloWorld, showMessage, cleanup]); diff --git a/example/hello.txt b/example/hello.txt new file mode 100644 index 0000000..8ab686e --- /dev/null +++ b/example/hello.txt @@ -0,0 +1 @@ +Hello, World! diff --git a/example/somedir/.gitignore b/example/somedir/.gitignore deleted file mode 100644 index e69de29..0000000 diff --git a/example/writeMsg.sh b/example/writeMsg.sh deleted file mode 100755 index bdb0155..0000000 --- a/example/writeMsg.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# an example task - -cd "$( dirname "${BASH_SOURCE[0]}" )" - -echo "writing msg.txt" -echo helloworld > msg.txt diff --git a/interfaces/core/ICoreInterfaces.ts b/interfaces/core/ICoreInterfaces.ts new file mode 100644 index 0000000..a2bfe92 --- /dev/null +++ b/interfaces/core/ICoreInterfaces.ts @@ -0,0 +1,63 @@ +import type { Args } from "@std/cli/parse-args"; +import type * as log from "@std/log"; +import type { TaskName, TrackedFileName } from "./IManifestTypes.ts"; +import type { IManifest } from "./IManifest.ts"; + +// Main task execution interface +export interface ITask { + name: TaskName; + description?: string; + exec(ctx: IExecContext): Promise; + setup(ctx: IExecContext): Promise; + reset(ctx: IExecContext): Promise; +} + +// Execution context interface +export interface ILoggers { + readonly internalLogger: log.Logger; + readonly taskLogger: log.Logger; + readonly userLogger: log.Logger; + readonly cliLogger: log.Logger; +} + +export interface IExecContext { + // Task registry + readonly taskRegister: Map; + readonly targetRegister: Map; + + // Task tracking + readonly doneTasks: Set; + readonly inprogressTasks: Set; + + // Logging + readonly internalLogger: log.Logger; + readonly taskLogger: log.Logger; + readonly userLogger: log.Logger; + readonly cliLogger: log.Logger; + + // Configuration + readonly concurrency: number; + readonly verbose: boolean; + + // Data + readonly manifest: IManifest; + readonly args: Args; + + // Methods + getTaskByName(name: TaskName): ITask | undefined; + schedule(action: () => Promise): Promise; +} + +// Task execution context passed to actions +export interface ITaskContext { + logger: log.Logger; + task: ITask; + args: Args; + exec: IExecContext; +} + +// Task action function type +export type IAction = (ctx: ITaskContext) => Promise | void; + +// Task up-to-date check function type +export type IIsUpToDate = (ctx: ITaskContext) => Promise | boolean; diff --git a/interfaces/core/IManifest.ts b/interfaces/core/IManifest.ts new file mode 100644 index 0000000..a02de52 --- /dev/null +++ b/interfaces/core/IManifest.ts @@ -0,0 +1,27 @@ +import type { + TaskData, + TaskName, + Timestamp, + TrackedFileData, + TrackedFileName, +} from "./IManifestTypes.ts"; + +// Manifest persistence interface +export interface IManifest { + readonly filename: string; + tasks: Record; + + load(): Promise; + save(): Promise; +} + +// Task manifest interface +export interface ITaskManifest { + lastExecution: Timestamp | null; + trackedFiles: Record; + + getFileData(fn: TrackedFileName): TrackedFileData | undefined; + setFileData(fn: TrackedFileName, d: TrackedFileData): void; + setExecutionTimestamp(): void; + toData(): TaskData; +} diff --git a/interfaces/core/IManifestTypes.ts b/interfaces/core/IManifestTypes.ts new file mode 100644 index 0000000..a8661e1 --- /dev/null +++ b/interfaces/core/IManifestTypes.ts @@ -0,0 +1,64 @@ +import type { Flavored } from "../utils/IFlavoring.ts"; + +/** + * Core manifest type definitions for dnit's persistence layer. + * + * These types define the structure of data that gets serialized to and from + * the manifest file (.manifest.json) that tracks task execution state and + * file dependencies. + */ + +/** + * A unique identifier for a task. + * Flavored to prevent mixing with other string types. + */ +export type TaskName = Flavored; + +/** + * A file path used for tracking file dependencies. + * Flavored to prevent mixing with other string types. + */ +export type TrackedFileName = Flavored; + +/** + * A hash value representing the content of a tracked file. + * Flavored to prevent mixing with other string types. + */ +export type TrackedFileHash = Flavored; + +/** + * An ISO timestamp string representing when something occurred. + * Flavored to prevent mixing with other string types. + */ +export type Timestamp = Flavored; + +/** + * Data about a tracked file at a specific point in time. + * Used to determine if a file has changed since the last task execution. + */ +export interface TrackedFileData { + /** Hash of the file content (usually SHA-1) */ + hash: TrackedFileHash; + /** Timestamp when the file was last modified */ + timestamp: Timestamp; +} + +/** + * Execution data for a single task. + * Contains information about when the task last ran and what files it depends on. + */ +export interface TaskData { + /** ISO timestamp of when this task was last executed, or null if never run */ + lastExecution: Timestamp | null; + /** Map of file paths to their tracked data for this task's dependencies */ + trackedFiles: Record; +} + +/** + * Root manifest structure that gets serialized to/from .manifest.json. + * Contains execution state for all tasks in the project. + */ +export interface Manifest { + /** Map of task names to their execution data */ + tasks: Record; +} diff --git a/interfaces/core/ITrackedFile.ts b/interfaces/core/ITrackedFile.ts new file mode 100644 index 0000000..cc1b414 --- /dev/null +++ b/interfaces/core/ITrackedFile.ts @@ -0,0 +1,41 @@ +import type { + Timestamp, + TrackedFileData, + TrackedFileHash, + TrackedFileName, +} from "./IManifestTypes.ts"; +import type { IExecContext, ITask } from "./ICoreInterfaces.ts"; + +// File tracking interface +export interface ITrackedFile { + path: TrackedFileName; + + // File operations + delete(): Promise; + exists(): Promise; + getHash(): Promise; + getTimestamp(): Promise; + + // Tracking operations + isUpToDate( + ctx: IExecContext, + tData: TrackedFileData | undefined, + ): Promise; + getFileData(ctx: IExecContext): Promise; + getFileDataOrCached( + ctx: IExecContext, + tData: TrackedFileData | undefined, + ): Promise<{ + tData: TrackedFileData; + upToDate: boolean; + }>; + + // Task association + setTask(t: ITask): void; + getTask(): ITask | null; +} + +// Async file generator interface +export interface ITrackedFilesAsync { + getTrackedFiles(): Promise; +} diff --git a/interfaces/utils/IFlavoring.ts b/interfaces/utils/IFlavoring.ts new file mode 100644 index 0000000..a36db41 --- /dev/null +++ b/interfaces/utils/IFlavoring.ts @@ -0,0 +1,31 @@ +/** + * "Flavoured" nominal typing utilities. + * + * Based on the pattern from: + * https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/ + * + * We use a symbol for the hidden field to ensure uniqueness across different + * flavored types while maintaining runtime compatibility with the base type. + */ + +const sym: unique symbol = Symbol(); + +/** + * Creates a "flavored" nominal type that is structurally identical to the base type + * but nominally distinct, preventing accidental type mixing. + * + * @template T - The base string type to flavor + * @template Name - A unique string literal to distinguish this flavored type + * + * @example + * ```typescript + * type UserId = Flavored; + * type ProductId = Flavored; + * + * const userId: UserId = "user123" as UserId; + * const productId: ProductId = userId; // ❌ Type error - prevents mixing + * ``` + */ +export type Flavored = T & { + readonly [sym]?: Name; +}; diff --git a/launch.ts b/launch.ts index 88bdba1..03b49e0 100644 --- a/launch.ts +++ b/launch.ts @@ -1,6 +1,10 @@ /// Convenience util to launch a user's dnit.ts -import { fs, log, path, semver } from "./deps.ts"; +import * as fs from "@std/fs"; +import type * as log from "@std/log"; +import * as path from "@std/path"; +import { type Args, parseArgs } from "@std/cli/parse-args"; +import { showHelpBasic } from "./cli/utils.ts"; type UserSource = { baseDir: string; @@ -21,7 +25,7 @@ function findUserSourceContext(dir: string): FindUserSourceContext { }; } -function findUserSource( +export function findUserSource( dir: string, startCtxArg: FindUserSourceContext | null, ): UserSource | null { @@ -86,14 +90,6 @@ function findUserSource( return findUserSource(path.join(dir, ".."), startCtx); } -export async function parseDotDenoVersionFile(fname: string): Promise { - const contents = await Deno.readTextFile(fname); - const trimmed = contents.split("\n").map((l) => l.trim()).filter((l) => - l.length > 0 - ).join("\n"); - return trimmed; -} - export async function getDenoVersion(): Promise { const cmd = new Deno.Command(Deno.execPath(), { args: [ @@ -111,13 +107,6 @@ export async function getDenoVersion(): Promise { throw new Error("Invalid parse of deno version output"); } -export function checkValidDenoVersion( - denoVersion: string, - denoReqSemverRange: string, -): boolean { - return semver.satisfies(denoVersion, denoReqSemverRange); -} - export async function launch(logger: log.Logger): Promise { const userSource = findUserSource(Deno.cwd(), null); if (userSource !== null) { @@ -129,18 +118,6 @@ export async function launch(logger: log.Logger): Promise { const denoVersion = await getDenoVersion(); logger.info("deno version:" + denoVersion); - const dotDenoVersionFile = path.join(userSource.dnitDir, ".denoversion"); - if (fs.existsSync(dotDenoVersionFile)) { - const reqDenoVerStr = await parseDotDenoVersionFile(dotDenoVersionFile); - const validDenoVer = checkValidDenoVersion(denoVersion, reqDenoVerStr); - if (!validDenoVer) { - throw new Error( - `Note that ${dotDenoVersionFile} requires version(s) ${reqDenoVerStr}. The current version is ${denoVersion}. Consider editing the .denoversion file and try again`, - ); - } - logger.info("deno version ok:" + denoVersion + " for " + reqDenoVerStr); - } - Deno.chdir(userSource.baseDir); const permissions = [ @@ -155,7 +132,7 @@ export async function launch(logger: log.Logger): Promise { ]; const importmap = userSource.importmap ? [ - "--importmap", + "--import-map", userSource.importmap, ] : []; @@ -192,6 +169,16 @@ export async function launch(logger: log.Logger): Promise { signal, }; } else { + const args: Args = parseArgs(Deno.args); + if (args["help"] === true) { + showHelpBasic({ info: console.log }); + return { + success: true, + code: 0, + signal: null, + }; + } + logger.error("No dnit.ts or dnit directory found"); return { success: false, diff --git a/main.ts b/main.ts index 603f727..d9dc64b 100644 --- a/main.ts +++ b/main.ts @@ -1,26 +1,25 @@ -import { flags, log, setupLogging } from "./mod.ts"; +import { createConsoleLoggers } from "./cli/logging.ts"; +import { type Args, parseArgs } from "@std/cli/parse-args"; import { launch } from "./launch.ts"; import { version } from "./version.ts"; export async function main() { - const args = flags.parse(Deno.args); + const args: Args = parseArgs(Deno.args); if (args["version"] === true) { console.log(`dnit ${version}`); Deno.exit(0); } - await setupLogging(); - const internalLogger = log.getLogger("internal"); + const loggers = createConsoleLoggers(); if (args["verbose"] !== undefined) { - internalLogger.levelName = "INFO"; + loggers.internalLogger.levelName = "INFO"; } - internalLogger.info(`starting dnit launch using version: ${version}`); + loggers.internalLogger.info(`starting dnit launch using version: ${version}`); - launch(internalLogger).then((st) => { - Deno.exit(st.code); - }); + const st = await launch(loggers.internalLogger); + Deno.exit(st.code); } -main(); +await main(); diff --git a/manifest.ts b/manifest.ts index bdc182e..8440130 100644 --- a/manifest.ts +++ b/manifest.ts @@ -1,70 +1,58 @@ -import { fs, path } from "./deps.ts"; +import * as fs from "@std/fs"; +import * as path from "@std/path"; +import * as log from "@std/log"; +import { TaskManifest } from "./core/taskManifest.ts"; +import type { IManifest } from "./interfaces/core/IManifest.ts"; -import * as A from "./adl-gen/dnit/manifest.ts"; -import * as J from "./adl-gen/runtime/json.ts"; +import type { TaskData, TaskName } from "./interfaces/core/IManifestTypes.ts"; +import { ManifestSchema } from "./core/manifestSchemas.ts"; -import { RESOLVER } from "./adl-gen/resolver.ts"; -import { ADLMap } from "./ADLMap.ts"; -export class Manifest { +export class Manifest implements IManifest { readonly filename: string; - readonly jsonBinding = J.createJsonBinding(RESOLVER, A.texprManifest()); - tasks: ADLMap = new ADLMap( - [], - (k1, k2) => k1 === k2, - ); + tasks: Record = {}; constructor(dir: string, filename: string = ".manifest.json") { this.filename = path.join(dir, filename); } async load() { if (await fs.exists(this.filename)) { - const json: J.Json = JSON.parse( - await Deno.readTextFile(this.filename), - ) as J.Json; - const mdata = this.jsonBinding.fromJson(json); - for (const p of mdata.tasks) { - const taskName: A.TaskName = p.v1; - const taskData: A.TaskData = p.v2; - this.tasks.set(taskName, new TaskManifest(taskData)); + try { + const jsonText = await Deno.readTextFile(this.filename); + const json = JSON.parse(jsonText); + const result = ManifestSchema.safeParse(json); + + if (result.success) { + for ( + const [taskName, taskData] of Object.entries(result.data.tasks) + ) { + this.tasks[taskName] = new TaskManifest(taskData); + } + } else { + log.getLogger("internal").warn( + `Manifest file ${this.filename} has invalid schema, creating fresh manifest`, + ); + await this.save(); + } + } catch (error) { + const errorMessage = error instanceof Error + ? error.message + : String(error); + log.getLogger("internal").warn( + `Failed to parse manifest file ${this.filename}: ${errorMessage}, creating fresh manifest`, + ); + await this.save(); } } } async save() { if (!await fs.exists(path.dirname(this.filename))) { - await Deno.mkdir(path.dirname(this.filename)); + await Deno.mkdir(path.dirname(this.filename), { recursive: true }); } - const mdata: A.Manifest = { - tasks: this.tasks.entries().map((p) => ({ v1: p[0], v2: p[1].toData() })), - }; - const jsonval = this.jsonBinding.toJson(mdata); - await Deno.writeTextFile(this.filename, JSON.stringify(jsonval, null, 2)); - } -} -export class TaskManifest { - public lastExecution: A.Timestamp | null = null; - trackedFiles: ADLMap = new ADLMap( - [], - (k1, k2) => k1 === k2, - ); - constructor(data: A.TaskData) { - this.trackedFiles = new ADLMap(data.trackedFiles, (k1, k2) => k1 === k2); - this.lastExecution = data.lastExecution; - } - - getFileData(fn: A.TrackedFileName): A.TrackedFileData | undefined { - return this.trackedFiles.get(fn); - } - setFileData(fn: A.TrackedFileName, d: A.TrackedFileData) { - this.trackedFiles.set(fn, d); - } - setExecutionTimestamp() { - this.lastExecution = (new Date()).toISOString(); - } - - toData(): A.TaskData { - return { - lastExecution: this.lastExecution, - trackedFiles: this.trackedFiles.toData(), - }; + const tasks: Record = {}; + for (const [taskName, taskManifest] of Object.entries(this.tasks)) { + tasks[taskName] = taskManifest.toData(); + } + const mdata = { tasks }; + await Deno.writeTextFile(this.filename, JSON.stringify(mdata, null, 2)); } } diff --git a/mod.ts b/mod.ts index fe4a6b3..6483204 100644 --- a/mod.ts +++ b/mod.ts @@ -1,2 +1,77 @@ -export * from "./dnit.ts"; -export * from "./deps.ts"; +// Main dnit module - clean exports organized by category + +// Core types +export * from "./interfaces/core/IManifestTypes.ts"; +export * from "./interfaces/utils/IFlavoring.ts"; +export type { + IAction, + IExecContext, + IIsUpToDate, + ITask, + ITaskContext, +} from "./interfaces/core/ICoreInterfaces.ts"; +export type { IManifest, ITaskManifest } from "./interfaces/core/IManifest.ts"; +export type { + ITrackedFile, + ITrackedFilesAsync, +} from "./interfaces/core/ITrackedFile.ts"; + +// Core implementations +export { ExecContext } from "./core/execContext.ts"; +export { + type Action, + type Dep, + type IsUpToDate, + runAlways, + Task, + task, + type TaskParams, +} from "./core/task.ts"; +export { + /** @deprecated Use trackFile() instead */ + file, + type FileParams, + type GetFileHash, + type GetFileTimestamp, + isTrackedFile, + TrackedFile, + trackFile, +} from "./core/file/TrackedFile.ts"; +export { + asyncFiles, + type GenTrackedFiles, + isTrackedFileAsync, + TrackedFilesAsync, +} from "./core/file/TrackedFilesAsync.ts"; +export { TaskManifest } from "./core/taskManifest.ts"; + +// Task context utilities +// TaskInterface now exported as ITask above +export { type TaskContext, taskContext } from "./core/TaskContext.ts"; + +// CLI utilities +export { + execCli, + execContextInitBasic as execBasic, + type ExecResult, + main, +} from "./cli/cli.ts"; +export { getLogger } from "./cli/logging.ts"; + +// Manifest handling +export { Manifest } from "./manifest.ts"; + +// Utilities +export * from "./utils/filesystem.ts"; + +// Git utilities +export { + fetchTags, + gitIsClean, + gitLastCommitMessage, + gitLatestTag, + requireCleanGit, +} from "./utils/git.ts"; + +// Process utilities +export { run, runConsole } from "./utils/process.ts"; diff --git a/tests/.gitignore b/tests/.gitignore deleted file mode 100644 index e49a891..0000000 --- a/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/.test diff --git a/tests/asyncQueue.test.ts b/tests/asyncQueue.test.ts index 34559a3..b95a7ac 100644 --- a/tests/asyncQueue.test.ts +++ b/tests/asyncQueue.test.ts @@ -1,29 +1,25 @@ -import { AsyncQueue } from "../asyncQueue.ts"; +import { AsyncQueue } from "../utils/asyncQueue.ts"; -import { assert } from "https://deno.land/std@0.221.0/testing/asserts.ts"; +import { assertLessOrEqual } from "@std/assert"; -class TestHelperCtx { +class TestConcurrency { numInProgress = 0; maxInProgress = 0; -} -class TestHelper { - started = false; - completed = false; + start() { + this.numInProgress += 1; + this.maxInProgress = Math.max(this.maxInProgress, this.numInProgress); + } - constructor(public ctx: TestHelperCtx) {} + finish() { + this.numInProgress -= 1; + } action = () => { - this.started = true; - this.ctx.numInProgress += 1; - this.ctx.maxInProgress = Math.max( - this.ctx.maxInProgress, - this.ctx.numInProgress, - ); + this.start(); return new Promise((resolve) => { setTimeout(() => { - this.completed = true; - this.ctx.numInProgress -= 1; + this.finish(); resolve(); }, 10); }); @@ -32,25 +28,16 @@ class TestHelper { Deno.test("async queue", async () => { for (let concurrency = 1; concurrency <= 32; concurrency *= 2) { - const ctx: TestHelperCtx = new TestHelperCtx(); + const ctx = new TestConcurrency(); const numTasks = concurrency * 10; - const testHelpers: TestHelper[] = []; - for (let i = 0; i < numTasks; ++i) { - testHelpers.push(new TestHelper(ctx)); - } - - // deno-lint-ignore no-explicit-any - const asyncQueue: AsyncQueue = new AsyncQueue(concurrency); + const asyncQueue = new AsyncQueue(concurrency); const promises: Promise[] = []; for (let i = 0; i < numTasks; ++i) { - const th = testHelpers[i]; - promises.push(asyncQueue.schedule(th.action)); - //promises.push(th.action()); // equivalent code but without the asyncQueue (runs them all in parallel) + promises.push(asyncQueue.schedule(ctx.action)); } await Promise.all(promises); - console.log(`ctx.maxInProgress: ${ctx.maxInProgress}`); - assert(ctx.maxInProgress <= concurrency); + assertLessOrEqual(ctx.maxInProgress, concurrency); } }); diff --git a/tests/basic.test.ts b/tests/basic.test.ts index 7ba73df..746cb6d 100644 --- a/tests/basic.test.ts +++ b/tests/basic.test.ts @@ -3,23 +3,22 @@ import { execBasic, runAlways, task, - TrackedFile, + type TrackedFile, trackFile, -} from "../dnit.ts"; +} from "../mod.ts"; -import { assertEquals } from "https://deno.land/std@0.221.0/testing/asserts.ts"; +import { assert, assertFalse } from "@std/assert"; import { Manifest } from "../manifest.ts"; -import { path } from "../deps.ts"; +import * as path from "@std/path"; +import { createTempDir } from "./utils.ts"; -Deno.test("basic test", async () => { +Deno.test("basic test - two tasks with dependency", async () => { const tasksDone: { [key: string]: boolean } = {}; const taskA = task({ name: "taskA", - description: "taskA", action: () => { - console.log("taskA"); tasksDone["taskA"] = true; }, uptodate: runAlways, @@ -27,9 +26,7 @@ Deno.test("basic test", async () => { const taskB = task({ name: "taskB", - description: "taskB", action: () => { - console.log("taskB"); tasksDone["taskB"] = true; }, deps: [taskA], @@ -37,76 +34,78 @@ Deno.test("basic test", async () => { }); const ctx = await execBasic(["taskB"], [taskA, taskB], new Manifest("")); + + // execute starting from taskB await ctx.getTaskByName("taskB")?.exec(ctx); - assertEquals(tasksDone["taskA"], true); - assertEquals(tasksDone["taskB"], true); + // assert that both A and B are done: + assert(tasksDone["taskA"]); + assert(tasksDone["taskB"]); }); -/* Something flaky with this test Deno.test("task up to date", async () => { - const testDir = path.join(".test", uuid.v4.generate()); - await Deno.mkdir(testDir, { recursive: true }); - + const { dirPath, cleanup } = await createTempDir(); const tasksDone: { [key: string]: boolean } = {}; - const testFile: TrackedFile = file({ - path: path.join(testDir, "testFile.txt"), - }); - await Deno.writeTextFile(testFile.path, uuid.v4.generate()); + const testFile: TrackedFile = trackFile(path.join(dirPath, "testFile.txt")); + + const initialContent = "initial-content-" + crypto.randomUUID(); + await Deno.writeTextFile(testFile.path, initialContent); const taskA = task({ name: "taskA", - description: "taskA", action: () => { - console.log("taskA"); tasksDone["taskA"] = true; }, - deps: [ - testFile, - ], + deps: [testFile], }); // Setup: const manifest = new Manifest(""); // share manifest to simulate independent runs: + // === FIRST RUN (setup) === { const ctx = await execBasic([], [taskA], manifest); // run once beforehand to setup manifest await ctx.getTaskByName("taskA")?.exec(ctx); - assertEquals(tasksDone["taskA"], true); + assert(tasksDone["taskA"]); tasksDone["taskA"] = false; // clear to reset } + // === SECOND RUN (should be up to date) === { const ctx = await execBasic([], [taskA], manifest); // Test: Run taskA again await ctx.getTaskByName("taskA")?.exec(ctx); - assertEquals(tasksDone["taskA"], false); // didn't run because of up-to-date + assertFalse(tasksDone["taskA"]); // didn't run because of up-to-date } + // === THIRD RUN (after file modification) === { /// Test: make not-up-to-date again tasksDone["taskA"] = false; - await Deno.writeTextFile(testFile.path, uuid.v4.generate()); + assertFalse(tasksDone["taskA"]); + + const newContent = "modified-content-" + crypto.randomUUID(); + await Deno.writeTextFile(testFile.path, newContent); const ctx = await execBasic([], [taskA], manifest); // Test: Run taskA again await ctx.getTaskByName("taskA")?.exec(ctx); - assertEquals(tasksDone["taskA"], true); // runs because of not up-to-date + + assert(tasksDone["taskA"]); // ran because of not up-to-date } - await Deno.remove(testDir, { recursive: true }); + await cleanup(); }); -*/ Deno.test("async file deps test", async () => { function genTrackedFiles(): Promise { return new Promise((resolve) => { setTimeout(() => { resolve([]); - }, 1000); + }, 10); }); } @@ -114,7 +113,6 @@ Deno.test("async file deps test", async () => { const taskA = task({ name: "taskA", - description: "taskA", action: () => { console.log("taskA"); tasksDone["taskA"] = true; @@ -124,7 +122,6 @@ Deno.test("async file deps test", async () => { const taskB = task({ name: "taskB", - description: "taskB", action: () => { console.log("taskB"); tasksDone["taskB"] = true; @@ -136,17 +133,15 @@ Deno.test("async file deps test", async () => { const ctx = await execBasic(["taskB"], [taskA, taskB], new Manifest("")); await ctx.getTaskByName("taskB")?.exec(ctx); - assertEquals(tasksDone["taskA"], true); - assertEquals(tasksDone["taskB"], true); + assert(tasksDone["taskA"]); + assert(tasksDone["taskB"]); }); Deno.test("tasks with target and clean", async () => { - const tempDir = await Deno.makeTempDir(); - - console.log("tempDir", tempDir); + const { dirPath, cleanup } = await createTempDir(); const exampleTarget1 = trackFile({ - path: path.join(tempDir, "exampleTarget1.txt"), + path: path.join(dirPath, "exampleTarget1.txt"), }); const testTask1 = task({ name: "testTask1", @@ -158,7 +153,7 @@ Deno.test("tasks with target and clean", async () => { }); const exampleTarget2 = trackFile({ - path: path.join(tempDir, "exampleTarget2.txt"), + path: path.join(dirPath, "exampleTarget2.txt"), }); const testTask2 = task({ name: "testTask2", @@ -170,26 +165,26 @@ Deno.test("tasks with target and clean", async () => { }); // precheck nonexists - assertEquals(await exampleTarget1.exists(), false); - assertEquals(await exampleTarget2.exists(), false); + assertFalse(await exampleTarget1.exists()); + assertFalse(await exampleTarget2.exists()); // setup exec ctx const ctx = await execBasic([], [testTask1, testTask2], new Manifest("")); // run test tasks await ctx.getTaskByName("testTask1")?.exec(ctx); - assertEquals(await exampleTarget1.exists(), true); + assert(await exampleTarget1.exists()); await ctx.getTaskByName("testTask2")?.exec(ctx); - assertEquals(await exampleTarget2.exists(), true); + assert(await exampleTarget2.exists()); // clean await ctx.getTaskByName("clean")?.exec(ctx); // check nonexists - assertEquals(await exampleTarget1.exists(), false); - assertEquals(await exampleTarget2.exists(), false); + assertFalse(await exampleTarget1.exists()); + assertFalse(await exampleTarget2.exists()); // clean tempdir - await Deno.remove(tempDir, { recursive: true }); + await cleanup(); }); diff --git a/tests/cli.test.ts b/tests/cli.test.ts new file mode 100644 index 0000000..0418805 --- /dev/null +++ b/tests/cli.test.ts @@ -0,0 +1,235 @@ +import { + assert, + assertEquals, + assertExists, + assertFalse, + assertStringIncludes, +} from "@std/assert"; +import { execCli, runAlways, task } from "../mod.ts"; +import { createTestLoggers } from "./testLogging.ts"; +import type { Args } from "@std/cli/parse-args"; + +Deno.test("CLI - execCli executes the requested task", async () => { + let taskRun = false; + + const testTask = task({ + name: "testTask", + description: "A test task", + action: () => { + taskRun = true; + }, + uptodate: runAlways, + }); + + const result = await execCli(["testTask"], [testTask]); + + assert(result.success); + assert(taskRun); +}); + +Deno.test("CLI - execCli defaults to list task when no args", async () => { + const testTask = task({ + name: "myTask", + description: "My test task", + action: () => {}, + }); + + // Setup test logging to capture output + const logCapture = createTestLoggers(); + + // run cli with no arg to test the 'list' feature on no args. + await execCli([], [testTask], logCapture.loggers); + + const output = logCapture.stdout.output.join("\n"); + + assertStringIncludes(output, "myTask"); + assertStringIncludes(output, "My test task"); + + // check for all builtin tasks in the list + assertStringIncludes(output, "clean"); + assertStringIncludes(output, "Clean tracked files"); + + assertStringIncludes(output, "list"); + assertStringIncludes(output, "List tasks"); + + assertStringIncludes(output, "tabcompletion"); + assertStringIncludes(output, "Generate shell completion script"); +}); + +Deno.test("CLI - execCli handles non-existent task", async () => { + // Setup test logging to capture output + const logCapture = createTestLoggers(); + + const result = await execCli(["nonExistentTask"], [], logCapture.loggers); + + assertFalse(result.success); + + const errorOutput = logCapture.stderr.output.join("\n"); + assertStringIncludes(errorOutput, "Task nonExistentTask not found"); +}); + +Deno.test("CLI - execCli handles task execution errors", async () => { + // Setup test logging to capture output + const logCapture = createTestLoggers(); + + const failingTask = task({ + name: "failingTask", + description: "A task that throws an error", + action: () => { + throw new Error("Task execution failed"); + }, + uptodate: runAlways, + }); + + try { + await execCli(["failingTask"], [failingTask], logCapture.loggers); + // Should not reach here - execCli should throw + assertEquals(false, true, "execCli should have thrown an error"); + } catch (error) { + // Verify the error was thrown as expected + assertStringIncludes((error as Error).message, "Task execution failed"); + + // Verify error was logged to stderr + const errorOutput = logCapture.stderr.output.join("\n"); + assertStringIncludes(errorOutput, "Error"); + } +}); + +Deno.test("CLI - task receives command-line arguments", async () => { + let receivedArgs: Args | null = null; + + const testTask = task({ + name: "argTest", + description: "Test task for arguments", + action: (ctx) => { + receivedArgs = ctx.args; + }, + uptodate: runAlways, + }); + + await execCli(["argTest", "pos1", "pos2", "--flag", "value"], [testTask]); + + assertExists(receivedArgs); + // Positional args include the task name and additional positional arguments + assertEquals(receivedArgs["_"], ["argTest", "pos1", "pos2"]); + assertEquals(receivedArgs["flag"], "value"); +}); + +Deno.test("CLI - task receives named flags", async () => { + let receivedArgs: Args | null = null; + + const testTask = task({ + name: "flagTest", + description: "Test task for named flags", + action: (ctx) => { + receivedArgs = ctx.args; + }, + uptodate: runAlways, + }); + + await execCli([ + "flagTest", + "--verbose", + "--dry-run", + "--output", + "file.txt", + "--count", + "42", + ], [testTask]); + + assertExists(receivedArgs); + assertEquals(receivedArgs["_"], ["flagTest"]); + assert(receivedArgs["verbose"]); + assert(receivedArgs["dry-run"]); + assertEquals(receivedArgs["output"], "file.txt"); + assertEquals(receivedArgs["count"], 42); // parseArgs converts numeric strings to numbers +}); + +Deno.test("CLI - task receives mixed positional and named arguments", async () => { + let receivedArgs: Args | null = null; + + const testTask = task({ + name: "mixedTest", + description: "Test task for mixed arguments", + action: (ctx) => { + receivedArgs = ctx.args; + }, + uptodate: runAlways, + }); + + await execCli([ + "mixedTest", + "file1.txt", + "--verbose", + "file2.txt", + "--output", + "result.txt", + "file3.txt", + ], [testTask]); + + assertExists(receivedArgs); + // parseArgs treats "file2.txt" as the value for --verbose flag + assertEquals(receivedArgs["_"], ["mixedTest", "file1.txt", "file3.txt"]); + assertEquals(receivedArgs["verbose"], "file2.txt"); // Gets value assigned to flag + assertEquals(receivedArgs["output"], "result.txt"); +}); + +Deno.test("CLI - task receives arguments with special characters", async () => { + let receivedArgs: Args | null = null; + + const testTask = task({ + name: "specialTest", + description: "Test task for special character arguments", + action: (ctx) => { + receivedArgs = ctx.args; + }, + uptodate: runAlways, + }); + + await execCli([ + "specialTest", + "file with spaces.txt", + "--message", + "Hello, World!", + "--path", + "/usr/local/bin", + "another-file.txt", + ], [testTask]); + + assertExists(receivedArgs); + assertEquals(receivedArgs["_"], [ + "specialTest", + "file with spaces.txt", + "another-file.txt", + ]); + assertEquals(receivedArgs["message"], "Hello, World!"); + assertEquals(receivedArgs["path"], "/usr/local/bin"); +}); + +Deno.test("CLI - task receives boolean flags correctly", async () => { + let receivedArgs: Args | null = null; + + const testTask = task({ + name: "boolTest", + description: "Test task for boolean flags", + action: (ctx) => { + receivedArgs = ctx.args; + }, + uptodate: runAlways, + }); + + await execCli([ + "boolTest", + "--enable", + "--no-cache", + "--verbose", + "false", // This will be string "false", not boolean + ], [testTask]); + + assertExists(receivedArgs); + assertEquals(receivedArgs["_"], ["boolTest"]); + assert(receivedArgs["enable"]); + assert(receivedArgs["no-cache"]); + // When a value follows a flag, it's treated as the flag's value + assertEquals(receivedArgs["verbose"], "false"); +}); diff --git a/tests/discovery.test.ts b/tests/discovery.test.ts new file mode 100644 index 0000000..04458fc --- /dev/null +++ b/tests/discovery.test.ts @@ -0,0 +1,187 @@ +import { assertEquals, assertInstanceOf } from "@std/assert"; +import * as path from "@std/path"; +import { findUserSource } from "../launch.ts"; +import { createFileInDir, createTempDir } from "./utils.ts"; + +Deno.test("Discovery - finds main.ts in dnit subdirectory", async () => { + const { dirPath, cleanup } = await createTempDir(); + + // Create dnit/main.ts + const dnitDir = path.join(dirPath, "dnit"); + await Deno.mkdir(dnitDir); + await createFileInDir(dnitDir, "main.ts", 'console.log("test");'); + + const result = findUserSource(dirPath, null); + + assertEquals(result?.baseDir, path.resolve(dirPath)); + assertEquals(result?.dnitDir, path.resolve(dnitDir)); + assertEquals(result?.mainSrc, path.resolve(path.join(dnitDir, "main.ts"))); + assertEquals(result?.importmap, null); + + await cleanup(); +}); + +Deno.test("Discovery - finds dnit.ts when no main.ts exists", async () => { + const { dirPath, cleanup } = await createTempDir(); + + // Create dnit/dnit.ts (no main.ts) + const dnitDir = path.join(dirPath, "dnit"); + await Deno.mkdir(dnitDir); + await createFileInDir(dnitDir, "dnit.ts", 'console.log("test");'); + + const result = findUserSource(dirPath, null); + + assertEquals(result?.baseDir, path.resolve(dirPath)); + assertEquals(result?.dnitDir, path.resolve(dnitDir)); + assertEquals(result?.mainSrc, path.resolve(path.join(dnitDir, "dnit.ts"))); + assertEquals(result?.importmap, null); + + await cleanup(); +}); + +Deno.test("Discovery - finds source in alternative deno/dnit path", async () => { + const { dirPath, cleanup } = await createTempDir(); + + // Create deno/dnit/main.ts + const denoDir = path.join(dirPath, "deno"); + const dnitDir = path.join(denoDir, "dnit"); + await Deno.mkdir(denoDir); + await Deno.mkdir(dnitDir); + await createFileInDir(dnitDir, "main.ts", 'console.log("test");'); + + const result = findUserSource(dirPath, null); + + assertEquals(result?.baseDir, path.resolve(dirPath)); + assertEquals(result?.dnitDir, path.resolve(dnitDir)); + assertEquals(result?.mainSrc, path.resolve(path.join(dnitDir, "main.ts"))); + assertEquals(result?.importmap, null); + + await cleanup(); +}); + +Deno.test("Discovery - prefers main.ts over dnit.ts", async () => { + const { dirPath, cleanup } = await createTempDir(); + + // Create both main.ts and dnit.ts + const dnitDir = path.join(dirPath, "dnit"); + await Deno.mkdir(dnitDir); + await createFileInDir(dnitDir, "main.ts", 'console.log("main");'); + await createFileInDir(dnitDir, "dnit.ts", 'console.log("dnit");'); + + const result = findUserSource(dirPath, null); + + // Should prefer main.ts + assertEquals(result?.mainSrc, path.resolve(path.join(dnitDir, "main.ts"))); + + await cleanup(); +}); + +Deno.test("Discovery - prefers dnit/ over deno/dnit/ path", async () => { + const { dirPath, cleanup } = await createTempDir(); + + // Create both dnit/main.ts and deno/dnit/main.ts + const dnitDir = path.join(dirPath, "dnit"); + await Deno.mkdir(dnitDir); + await createFileInDir(dnitDir, "main.ts", 'console.log("dnit");'); + + const denoDir = path.join(dirPath, "deno"); + const denoDnitDir = path.join(denoDir, "dnit"); + await Deno.mkdir(denoDir); + await Deno.mkdir(denoDnitDir); + await createFileInDir(denoDnitDir, "main.ts", 'console.log("deno/dnit");'); + + const result = findUserSource(dirPath, null); + + // Should prefer dnit/ over deno/dnit/ + assertEquals(result?.dnitDir, path.resolve(dnitDir)); + assertEquals(result?.mainSrc, path.resolve(path.join(dnitDir, "main.ts"))); + + await cleanup(); +}); + +Deno.test("Discovery - searches parent directories", async () => { + const { dirPath, cleanup } = await createTempDir(); + + // Create dnit/main.ts in root + const dnitDir = path.join(dirPath, "dnit"); + await Deno.mkdir(dnitDir); + await createFileInDir(dnitDir, "main.ts", 'console.log("test");'); + + // Create a nested subdirectory + const subDir = path.join(dirPath, "subdir"); + await Deno.mkdir(subDir); + + // Search from subdirectory - should find dnit source in parent + const result = findUserSource(subDir, null); + + assertEquals(result?.baseDir, path.resolve(dirPath)); + assertEquals(result?.dnitDir, path.resolve(dnitDir)); + assertEquals(result?.mainSrc, path.resolve(path.join(dnitDir, "main.ts"))); + + await cleanup(); +}); + +Deno.test("Discovery - searches multiple parent levels", async () => { + const { dirPath, cleanup } = await createTempDir(); + + // Create dnit/main.ts in root + const dnitDir = path.join(dirPath, "dnit"); + await Deno.mkdir(dnitDir); + await createFileInDir(dnitDir, "main.ts", 'console.log("test");'); + + // Create deeply nested subdirectory + const deepDir = path.join(dirPath, "a", "b", "c"); + await Deno.mkdir(deepDir, { recursive: true }); + + // Search from deep subdirectory - should find dnit source in ancestor + const result = findUserSource(deepDir, null); + + assertEquals(result?.baseDir, path.resolve(dirPath)); + assertEquals(result?.dnitDir, path.resolve(dnitDir)); + assertEquals(result?.mainSrc, path.resolve(path.join(dnitDir, "main.ts"))); + + await cleanup(); +}); + +Deno.test("Discovery - returns null when no dnit source found", async () => { + const { dirPath, cleanup } = await createTempDir(); + + // Create empty directory structure without any dnit sources + const subDir = path.join(dirPath, "subdir"); + await Deno.mkdir(subDir); + + const result = findUserSource(subDir, null); + + assertEquals(result, null); + + await cleanup(); +}); + +Deno.test("Discovery - returns null when directory doesn't exist", () => { + const nonExistentDir = "/path/that/does/not/exist"; + + // Should handle non-existent directory gracefully + try { + const result = findUserSource(nonExistentDir, null); + // If it doesn't throw, it should return null + assertEquals(result, null); + } catch (error) { + // It's also acceptable to throw an error for non-existent paths + assertInstanceOf(error, Deno.errors.NotFound); + } +}); + +Deno.test("Discovery - handles directory with no source files", async () => { + const { dirPath, cleanup } = await createTempDir(); + + // Create dnit directory but no source files + const dnitDir = path.join(dirPath, "dnit"); + await Deno.mkdir(dnitDir); + await createFileInDir(dnitDir, "README.md", "# No source files here"); + + const result = findUserSource(dirPath, null); + + assertEquals(result, null); + + await cleanup(); +}); diff --git a/tests/filesystem.test.ts b/tests/filesystem.test.ts new file mode 100644 index 0000000..25fb19f --- /dev/null +++ b/tests/filesystem.test.ts @@ -0,0 +1,238 @@ +import { + assert, + assertEquals, + assertFalse, + assertInstanceOf, + assertNotInstanceOf, + assertRejects, +} from "@std/assert"; +import * as path from "@std/path"; +import { + deletePath, + getFileSha1Sum, + getFileTimestamp, + statPath, +} from "../utils/filesystem.ts"; +import type { TrackedFileName } from "../interfaces/core/IManifestTypes.ts"; +import { createFileInDir, createTempDir } from "./utils.ts"; + +Deno.test("filesystem utilities", async (t) => { + const { dirPath: testDir, cleanup } = await createTempDir(); + + await t.step("statPath - file exists", async () => { + const testFile = await createFileInDir(testDir, "test.txt", "test content"); + + const result = await statPath(testFile); + assertEquals(result.kind, "fileInfo"); + if (result.kind === "fileInfo") { + assert(result.fileInfo.isFile); + } + }); + + await t.step("statPath - file does not exist", async () => { + const nonExistentFile = path.join( + testDir, + "nonexistent.txt", + ); + + const result = await statPath(nonExistentFile); + assertEquals(result.kind, "nonExistent"); + }); + + await t.step("statPath - directory exists", async () => { + const testSubDir = path.join(testDir, "subdir"); + await Deno.mkdir(testSubDir); + + const result = await statPath(testSubDir); + assertEquals(result.kind, "fileInfo"); + if (result.kind === "fileInfo") { + assert(result.fileInfo.isDirectory); + } + }); + + await t.step("statPath - permission error propagates", async () => { + // Test that permission errors are properly propagated (not converted to NotFound) + // Use platform-appropriate restricted paths + + let restrictedPath: TrackedFileName; + if (Deno.build.os === "windows") { + // Windows: Use a system file that typically requires elevated privileges + restrictedPath = "C:\\Windows\\System32\\config\\SAM"; + } else { + // Unix-like: Use a common restricted directory + restrictedPath = "/root/.ssh/id_rsa"; + } + + try { + await statPath(restrictedPath); + // If we reach here, the path was accessible (running with high privileges) + // This is not an error, just means we can't test permission errors + } catch (err) { + // Should throw an error, and it should NOT be NotFound + // (it should be a permission error instead) + assertInstanceOf(err, Error); + if (err instanceof Deno.errors.NotFound) { + // This is fine - the path doesn't exist, which is also a valid test case + // since it confirms statPath handles Deno.errors.NotFound properly + } else { + // This is what we're testing for - non-NotFound errors should propagate + assertNotInstanceOf(err, Deno.errors.NotFound); + } + } + }); + + await t.step("deletePath - file exists", async () => { + const testFile = await createFileInDir( + testDir, + "to_delete.txt", + "delete me", + ); + + // Verify file exists + const beforeStat = await statPath(testFile); + assertEquals(beforeStat.kind, "fileInfo"); + + // Delete it + await deletePath(testFile); + + // Verify it's gone + const afterStat = await statPath(testFile); + assertEquals(afterStat.kind, "nonExistent"); + }); + + await t.step("deletePath - directory with contents", async () => { + const testSubDir = path.join(testDir, "dir_to_delete"); + await Deno.mkdir(testSubDir); + const fileInDir = path.join(testSubDir, "file.txt"); + await Deno.writeTextFile(fileInDir, "content"); + + // Verify directory exists + const beforeStat = await statPath(testSubDir); + assertEquals(beforeStat.kind, "fileInfo"); + + // Delete it recursively + await deletePath(testSubDir); + + // Verify it's gone + const afterStat = await statPath(testSubDir); + assertEquals(afterStat.kind, "nonExistent"); + }); + + await t.step("deletePath - file does not exist (no error)", async () => { + const nonExistentFile = path.join( + testDir, + "never_existed.txt", + ); + + // Should not throw + await deletePath(nonExistentFile); + }); + + await t.step("getFileSha1Sum - text file", async () => { + const content = "Hello, World!"; + const testFile = await createFileInDir(testDir, "hash_test.txt", content); + + const hash = await getFileSha1Sum(testFile); + + // SHA-1 of "Hello, World!" should be consistent + assertEquals(typeof hash, "string"); + assertEquals(hash.length, 40); // SHA-1 is 40 hex characters + assertEquals(hash, "0a0a9f2a6772942557ab5355d76af442f8f65e01"); + }); + + await t.step("getFileSha1Sum - binary file", async () => { + const testFile = path.join(testDir, "binary_test.bin"); + const binaryData = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0xFF]); + await Deno.writeFile(testFile, binaryData); + + const hash = await getFileSha1Sum(testFile); + + assertEquals(typeof hash, "string"); + assertEquals(hash.length, 40); + // Should be deterministic for the same binary content + const hash2 = await getFileSha1Sum(testFile); + assertEquals(hash, hash2); + }); + + await t.step("getFileSha1Sum - empty file", async () => { + const testFile = await createFileInDir(testDir, "empty_test.txt", ""); + + const hash = await getFileSha1Sum(testFile); + + assertEquals(hash, "da39a3ee5e6b4b0d3255bfef95601890afd80709"); // SHA-1 of empty string + }); + + await t.step("getFileSha1Sum - large file", async () => { + const largeContent = "A".repeat(100000); // 100KB of 'A's + const testFile = await createFileInDir( + testDir, + "large_test.txt", + largeContent, + ); + + const hash = await getFileSha1Sum(testFile); + + assertEquals(typeof hash, "string"); + assertEquals(hash.length, 40); + // Should handle large files without issue + }); + + await t.step("getFileSha1Sum - nonexistent file throws", async () => { + const nonExistentFile = path.join(testDir, "does_not_exist.txt"); + + await assertRejects( + () => getFileSha1Sum(nonExistentFile), + Deno.errors.NotFound, + ); + }); + + await t.step("getFileTimestamp - valid file", async () => { + const testFile = await createFileInDir( + testDir, + "timestamp_test.txt", + "timestamp content", + ); + + const fileInfo = await Deno.stat(testFile); + const timestamp = getFileTimestamp(testFile, fileInfo); + + assertEquals(typeof timestamp, "string"); + // Should be a valid ISO string + const date = new Date(timestamp); + assertFalse(isNaN(date.getTime())); + + // Should match file's mtime + if (fileInfo.mtime) { + assertEquals(timestamp, fileInfo.mtime.toISOString()); + } + }); + + await t.step("getFileTimestamp - file with no mtime", async () => { + const testFile = path.join(testDir, "no_mtime_test.txt"); + await Deno.writeTextFile(testFile, "content"); + + // Create a mock FileInfo with no mtime + const mockFileInfo = { mtime: null } as Deno.FileInfo; + + const timestamp = getFileTimestamp(testFile, mockFileInfo); + assertEquals(timestamp, ""); + }); + + await t.step("special characters in paths", async () => { + const specialFile = await createFileInDir( + testDir, + "file with spaces & symbols!.txt", + "special content", + ); + + const result = await statPath(specialFile); + assertEquals(result.kind, "fileInfo"); + + const hash = await getFileSha1Sum(specialFile); + assertEquals(typeof hash, "string"); + assertEquals(hash.length, 40); + }); + + // Cleanup test directory + await cleanup(); +}); diff --git a/tests/task.test.ts b/tests/task.test.ts new file mode 100644 index 0000000..5702d46 --- /dev/null +++ b/tests/task.test.ts @@ -0,0 +1,753 @@ +import { + assert, + assertEquals, + assertExists, + assertFalse, + assertInstanceOf, + assertRejects, + assertThrows, +} from "@std/assert"; +import { + execBasic, + file, + Task, + task, + TrackedFile, + TrackedFilesAsync, +} from "../mod.ts"; +import { detectCircularDependencies } from "../core/task.ts"; +import { Manifest } from "../manifest.ts"; +import { type Action, type IsUpToDate, runAlways } from "../core/task.ts"; +import { type TaskContext, taskContext } from "../core/TaskContext.ts"; +import { createFileInDir, createTempDir } from "./utils.ts"; + +Deno.test("Task - basic task creation", () => { + const testAction: Action = () => {}; + + const testTask = new Task({ + name: "testTask", + description: "A test task", + action: testAction, + }); + + assertEquals(testTask.name, "testTask"); + assertEquals(testTask.description, "A test task"); + assertEquals(testTask.action, testAction); + assertEquals(testTask.task_deps.size, 0); + assertEquals(testTask.file_deps.size, 0); + assertEquals(testTask.async_files_deps.size, 0); + assertEquals(testTask.targets.size, 0); +}); + +Deno.test("Task - task() function", () => { + const testAction: Action = () => {}; + + const testTask = task({ + name: "testTask", + description: "A test task", + action: testAction, + }); + + assertInstanceOf(testTask, Task); + assertEquals(testTask.name, "testTask"); + assertEquals(testTask.description, "A test task"); +}); + +Deno.test("Task - task with dependencies", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "dependency content", + ); + const trackedFile = new TrackedFile({ path: tempFile }); + + const depTask = new Task({ + name: "depTask", + action: () => {}, + }); + + const mainTask = new Task({ + name: "mainTask", + action: () => {}, + deps: [depTask, trackedFile], + }); + + assertEquals(mainTask.task_deps.size, 1); + assertEquals(mainTask.file_deps.size, 1); + assert(mainTask.task_deps.has(depTask)); + assert(mainTask.file_deps.has(trackedFile)); + + await cleanup(); +}); + +Deno.test("Task - task with targets", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "target content", + ); + const targetFile = new TrackedFile({ path: tempFile }); + + const testTask = new Task({ + name: "testTask", + action: () => {}, + targets: [targetFile], + }); + + assertEquals(testTask.targets.size, 1); + assert(testTask.targets.has(targetFile)); + + // Target should have task assigned + assertEquals(targetFile.getTask(), testTask); + + await cleanup(); +}); + +Deno.test("Task - task with TrackedFilesAsync dependencies", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "async content", + ); + + const generator = async () => { + // await something to make it actually async + await new Promise((resolve) => queueMicrotask(resolve)); + return [file(tempFile)]; + }; + + const asyncFiles = new TrackedFilesAsync(generator); + + const testTask = new Task({ + name: "testTask", + action: () => {}, + deps: [asyncFiles], + }); + + assertEquals(testTask.async_files_deps.size, 1); + assert(testTask.async_files_deps.has(asyncFiles)); + + await cleanup(); +}); + +Deno.test("Task - task with custom uptodate function", () => { + let uptodateCalled = false; + const customUptodate: IsUpToDate = () => { + uptodateCalled = true; + return false; + }; + + const testTask = new Task({ + name: "testTask", + action: () => {}, + uptodate: customUptodate, + }); + + assertEquals(testTask.uptodate, customUptodate); + assertFalse(uptodateCalled); // Should not be called during task creation +}); + +Deno.test("Task - runAlways uptodate helper", () => { + // Create a mock TaskContext to pass to runAlways + const mockTaskContext = {} as TaskContext; + const result = runAlways(mockTaskContext); + assertFalse(result); +}); + +Deno.test("Task - empty task name is allowed", () => { + const testTask = new Task({ + name: "", + action: () => {}, + }); + + assertEquals(testTask.name, ""); +}); + +Deno.test("Task - duplicate target assignment throws error", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "shared target", + ); + const sharedTarget = new TrackedFile({ path: tempFile }); + + const _task1 = new Task({ + name: "task1", + action: () => {}, + targets: [sharedTarget], + }); + + // Second task trying to use same target should throw + assertThrows( + () => + new Task({ + name: "task2", + action: () => {}, + targets: [sharedTarget], + }), + Error, + "Duplicate tasks generating TrackedFile as target", + ); + + await cleanup(); +}); + +Deno.test("Task - setup registers targets", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "target content", + ); + const targetFile = new TrackedFile({ path: tempFile }); + const manifest = new Manifest(""); + + const testTask = new Task({ + name: "testTask", + action: () => {}, + targets: [targetFile], + }); + + const ctx = await execBasic([], [testTask], manifest); + + assertEquals(ctx.targetRegister.get(targetFile.path), testTask); + assertExists(testTask.taskManifest); + + await cleanup(); +}); + +Deno.test("Task - setup with task dependencies", async () => { + const manifest = new Manifest(""); + + const depTask = new Task({ + name: "depTask", + action: () => {}, + }); + + const mainTask = new Task({ + name: "mainTask", + action: () => {}, + deps: [depTask], + }); + + await execBasic([], [mainTask, depTask], manifest); + + // Both tasks should be set up + assertExists(mainTask.taskManifest); + assertExists(depTask.taskManifest); +}); + +Deno.test("Task - exec marks task as done", async () => { + const manifest = new Manifest(""); + let actionCalled = false; + + const testTask = new Task({ + name: "testTask", + action: () => { + actionCalled = true; + }, + uptodate: runAlways, // Force it to run + }); + + const ctx = await execBasic([], [testTask], manifest); + await testTask.exec(ctx); + + assert(actionCalled); + assert(ctx.doneTasks.has(testTask)); + assertFalse(ctx.inprogressTasks.has(testTask)); +}); + +Deno.test("Task - exec skips already done tasks", async () => { + const manifest = new Manifest(""); + let actionCallCount = 0; + + const testTask = new Task({ + name: "testTask", + action: () => { + actionCallCount++; + }, + uptodate: runAlways, // Force it to run + }); + + const ctx = await execBasic([], [testTask], manifest); + await testTask.exec(ctx); + await testTask.exec(ctx); // Second call should be skipped + + assertEquals(actionCallCount, 1); + assert(ctx.doneTasks.has(testTask)); +}); + +Deno.test("Task - exec skips in-progress tasks", async () => { + const manifest = new Manifest(""); + let actionCallCount = 0; + + const testTask = new Task({ + name: "testTask", + action: () => { + actionCallCount++; + }, + }); + + const ctx = await execBasic([], [testTask], manifest); + + // Manually mark as in-progress + ctx.inprogressTasks.add(testTask); + + await testTask.exec(ctx); + + assertEquals(actionCallCount, 0); +}); + +Deno.test("Task - exec with async action", async () => { + const manifest = new Manifest(""); + let actionCompleted = false; + + const testTask = new Task({ + name: "testTask", + action: async () => { + await new Promise((resolve) => queueMicrotask(resolve)); + actionCompleted = true; + }, + uptodate: runAlways, // Force it to run + }); + + const ctx = await execBasic([], [testTask], manifest); + await testTask.exec(ctx); + + assert(actionCompleted); + assert(ctx.doneTasks.has(testTask)); +}); + +Deno.test("Task - exec with uptodate check", async () => { + const manifest = new Manifest(""); + let actionCalled = false; + let uptodateCalled = false; + + const testTask = new Task({ + name: "testTask", + action: () => { + actionCalled = true; + }, + uptodate: () => { + uptodateCalled = true; + return true; // Task is up-to-date + }, + }); + + const ctx = await execBasic([], [testTask], manifest); + await testTask.exec(ctx); + + assert(uptodateCalled); + assertFalse(actionCalled); // Should not run action if up-to-date +}); + +Deno.test("Task - exec with runAlways", async () => { + const manifest = new Manifest(""); + let actionCalled = false; + + const testTask = new Task({ + name: "testTask", + action: () => { + actionCalled = true; + }, + uptodate: runAlways, + }); + + const ctx = await execBasic([], [testTask], manifest); + await testTask.exec(ctx); + + assert(actionCalled); // Should always run +}); + +Deno.test("Task - reset cleans targets", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "target content", + ); + const targetFile = new TrackedFile({ path: tempFile }); + const manifest = new Manifest(""); + + const testTask = new Task({ + name: "testTask", + action: () => {}, + targets: [targetFile], + }); + + const ctx = await execBasic([], [testTask], manifest); + + // Verify file exists + assert(await targetFile.exists()); + + await testTask.reset(ctx); + + // File should be deleted + assertFalse(await targetFile.exists()); + + await cleanup(); +}); + +Deno.test("Task - taskContext creation", async () => { + const manifest = new Manifest(""); + + const testTask = new Task({ + name: "testTask", + action: () => {}, + }); + + const ctx = await execBasic([], [testTask], manifest); + const tCtx = taskContext(ctx, testTask); + + assertEquals(tCtx.logger, ctx.taskLogger); + assertEquals(tCtx.task, testTask); + assertEquals(tCtx.args, ctx.args); + assertEquals(tCtx.exec, ctx); +}); + +Deno.test("Task - action receives TaskContext", async () => { + const manifest = new Manifest(""); + let receivedContext: TaskContext | null = null; + + const testTask = new Task({ + name: "testTask", + action: (taskCtx) => { + receivedContext = taskCtx; + }, + uptodate: runAlways, // Force it to run + }); + + const ctx = await execBasic([], [testTask], manifest); + await testTask.exec(ctx); + + assertExists(receivedContext); + const context = receivedContext as TaskContext; + assertEquals(context.task, testTask); + assertEquals(context.exec, ctx); +}); + +Deno.test("Task - exec with file dependencies updates manifest", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "dependency content", + ); + const trackedFile = new TrackedFile({ path: tempFile }); + const manifest = new Manifest(""); + + const testTask = new Task({ + name: "testTask", + action: () => {}, + deps: [trackedFile], + }); + + const ctx = await execBasic([], [testTask], manifest); + await testTask.exec(ctx); + + // Manifest should have file data + const fileData = testTask.taskManifest?.getFileData(trackedFile.path); + assertExists(fileData); + assertEquals(typeof fileData.hash, "string"); + assertEquals(typeof fileData.timestamp, "string"); + + await cleanup(); +}); + +Deno.test("Task - task with mixed dependency types", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "mixed dep content", + ); + const trackedFile = new TrackedFile({ path: tempFile }); + + const depTask = new Task({ + name: "depTask", + action: () => {}, + }); + + const generator = () => { + return [file(tempFile)]; + }; + const asyncFiles = new TrackedFilesAsync(generator); + + const mainTask = new Task({ + name: "mainTask", + action: () => {}, + deps: [depTask, trackedFile, asyncFiles], + }); + + assertEquals(mainTask.task_deps.size, 1); + assertEquals(mainTask.file_deps.size, 1); + assertEquals(mainTask.async_files_deps.size, 1); + + await cleanup(); +}); + +Deno.test("Task - description is optional", () => { + const testTask = new Task({ + name: "testTask", + action: () => {}, + }); + + assertEquals(testTask.description, undefined); +}); + +Deno.test("Task - circular dependency detection A->B->C->A", async () => { + const manifest = new Manifest(""); + + const taskA = new Task({ + name: "taskA", + action: () => console.log("Running task A"), + }); + + const taskB = new Task({ + name: "taskB", + action: () => console.log("Running task B"), + deps: [taskA], + }); + + const taskC = new Task({ + name: "taskC", + action: () => console.log("Running task C"), + deps: [taskB], + }); + + // Create circular dependency: A depends on C + taskA.task_deps.add(taskC); + + // Try to execute taskA which should trigger circular dependency + const ctx = await execBasic([], [taskA, taskB, taskC], manifest); + + await assertRejects( + () => taskA.exec(ctx), + Error, + "Circular dependency detected: taskA -> taskC -> taskB -> taskA", + ); +}); + +Deno.test("Task - self-referencing task", async () => { + const manifest = new Manifest(""); + + const selfTask = new Task({ + name: "selfTask", + action: () => console.log("Running self task"), + }); + + // Make task depend on itself + selfTask.task_deps.add(selfTask); + + const ctx = await execBasic([], [selfTask], manifest); + + await assertRejects( + () => selfTask.exec(ctx), + Error, + "Circular dependency detected: selfTask -> selfTask", + ); +}); + +Deno.test("Task - circular dependency A->B->A", async () => { + const manifest = new Manifest(""); + + const taskA = new Task({ + name: "taskA", + action: () => console.log("Running task A"), + }); + + const taskB = new Task({ + name: "taskB", + action: () => console.log("Running task B"), + deps: [taskA], + }); + + // Create circular dependency: A depends on B + taskA.task_deps.add(taskB); + + const ctx = await execBasic([], [taskA, taskB], manifest); + + await assertRejects( + () => taskA.exec(ctx), + Error, + "Circular dependency detected: taskA -> taskB -> taskA", + ); +}); + +// Direct tests for detectCircularDependencies function + +Deno.test("detectCircularDependencies - no circular dependency", () => { + const taskA = new Task({ + name: "taskA", + action: () => {}, + }); + + const taskB = new Task({ + name: "taskB", + action: () => {}, + deps: [taskA], + }); + + const result = detectCircularDependencies(taskB); + assertEquals(result, null); +}); + +Deno.test("detectCircularDependencies - self-referencing task", () => { + const taskA = new Task({ + name: "taskA", + action: () => {}, + }); + + // Make task depend on itself + taskA.task_deps.add(taskA); + + const result = detectCircularDependencies(taskA); + assertExists(result); + assertEquals(result!.cycle.length, 2); + assertEquals(result!.cycle[0].name, "taskA"); + assertEquals(result!.cycle[1].name, "taskA"); +}); + +Deno.test("detectCircularDependencies - simple A->B->A cycle", () => { + const taskA = new Task({ + name: "taskA", + action: () => {}, + }); + + const taskB = new Task({ + name: "taskB", + action: () => {}, + deps: [taskA], + }); + + // Create circular dependency: A depends on B + taskA.task_deps.add(taskB); + + const result = detectCircularDependencies(taskA); + assertExists(result); + assertEquals(result!.cycle.length, 3); + assertEquals(result!.cycle[0].name, "taskA"); + assertEquals(result!.cycle[1].name, "taskB"); + assertEquals(result!.cycle[2].name, "taskA"); +}); + +Deno.test("detectCircularDependencies - complex A->B->C->A cycle", () => { + const taskA = new Task({ + name: "taskA", + action: () => {}, + }); + + const taskB = new Task({ + name: "taskB", + action: () => {}, + deps: [taskA], + }); + + const taskC = new Task({ + name: "taskC", + action: () => {}, + deps: [taskB], + }); + + // Create circular dependency: A depends on C + taskA.task_deps.add(taskC); + + const result = detectCircularDependencies(taskA); + assertExists(result); + assertEquals(result!.cycle.length, 4); + assertEquals(result!.cycle[0].name, "taskA"); + assertEquals(result!.cycle[1].name, "taskC"); + assertEquals(result!.cycle[2].name, "taskB"); + assertEquals(result!.cycle[3].name, "taskA"); +}); + +Deno.test("detectCircularDependencies - task with no dependencies", () => { + const taskA = new Task({ + name: "taskA", + action: () => {}, + }); + + const result = detectCircularDependencies(taskA); + assertEquals(result, null); +}); + +Deno.test("detectCircularDependencies - linear chain no cycle", () => { + const taskA = new Task({ + name: "taskA", + action: () => {}, + }); + + const taskB = new Task({ + name: "taskB", + action: () => {}, + deps: [taskA], + }); + + const taskC = new Task({ + name: "taskC", + action: () => {}, + deps: [taskB], + }); + + const taskD = new Task({ + name: "taskD", + action: () => {}, + deps: [taskC], + }); + + const result = detectCircularDependencies(taskD); + assertEquals(result, null); +}); + +Deno.test("detectCircularDependencies - multiple dependencies no cycle", () => { + const taskA = new Task({ + name: "taskA", + action: () => {}, + }); + + const taskB = new Task({ + name: "taskB", + action: () => {}, + }); + + const taskC = new Task({ + name: "taskC", + action: () => {}, + deps: [taskA, taskB], + }); + + const result = detectCircularDependencies(taskC); + assertEquals(result, null); +}); + +Deno.test("detectCircularDependencies - diamond dependency no cycle", () => { + const taskA = new Task({ + name: "taskA", + action: () => {}, + }); + + const taskB = new Task({ + name: "taskB", + action: () => {}, + deps: [taskA], + }); + + const taskC = new Task({ + name: "taskC", + action: () => {}, + deps: [taskA], + }); + + const taskD = new Task({ + name: "taskD", + action: () => {}, + deps: [taskB, taskC], + }); + + const result = detectCircularDependencies(taskD); + assertEquals(result, null); +}); diff --git a/tests/testLogging.ts b/tests/testLogging.ts new file mode 100644 index 0000000..c9796fe --- /dev/null +++ b/tests/testLogging.ts @@ -0,0 +1,43 @@ +import * as log from "@std/log"; +import type { ILoggers } from "../interfaces/core/ICoreInterfaces.ts"; + +/// Test capture handler that stores output in an array +class TestCaptureHandler extends log.BaseHandler { + public output: string[] = []; + + constructor(levelName: log.LevelName = "DEBUG") { + super(levelName, { + formatter: (rec) => rec.msg, + }); + } + + override log(msg: string): void { + this.output.push(msg); + } +} + +export interface TestLogCapture { + stdout: TestCaptureHandler; + stderr: TestCaptureHandler; + loggers: ILoggers; +} + +export function createTestLoggers(): TestLogCapture { + const testStdOut = new TestCaptureHandler(); + const testStdErr = new TestCaptureHandler(); + + const loggers: ILoggers = { + internalLogger: new log.Logger("internal", "WARN", { + handlers: [testStdErr], + }), + taskLogger: new log.Logger("task", "INFO", { handlers: [testStdErr] }), + userLogger: new log.Logger("user", "INFO", { handlers: [testStdOut] }), + cliLogger: new log.Logger("cli", "INFO", { handlers: [testStdOut] }), + }; + + return { + stdout: testStdOut, + stderr: testStdErr, + loggers, + }; +} diff --git a/tests/textTable.test.ts b/tests/textTable.test.ts new file mode 100644 index 0000000..c1c5e7a --- /dev/null +++ b/tests/textTable.test.ts @@ -0,0 +1,295 @@ +import { + assertEquals, + assertFalse, + assertGreater, + assertStringIncludes, +} from "@std/assert"; +import { plainTextTable, textTable } from "../utils/textTable.ts"; + +Deno.test("textTable utilities", async (t) => { + await t.step("basic table with single row", () => { + const headings = ["Name", "Age"]; + const cells = [["John", "30"]]; + const result = textTable(headings, cells); + + // Should contain proper box drawing characters + assertEquals(typeof result, "string"); + assertStringIncludes(result, "┌"); + assertStringIncludes(result, "┐"); + assertStringIncludes(result, "└"); + assertStringIncludes(result, "┘"); + assertStringIncludes(result, "│"); + assertStringIncludes(result, "─"); + + // Should contain the data + assertStringIncludes(result, "Name"); + assertStringIncludes(result, "Age"); + assertStringIncludes(result, "John"); + assertStringIncludes(result, "30"); + }); + + await t.step("empty table with headers only", () => { + const headings = ["Column1", "Column2"]; + const cells: string[][] = []; + const result = textTable(headings, cells); + + assertEquals(typeof result, "string"); + assertStringIncludes(result, "Column1"); + assertStringIncludes(result, "Column2"); + // Should still have proper table structure + assertStringIncludes(result, "┌"); + assertStringIncludes(result, "┐"); + }); + + await t.step("multiple rows with varying lengths", () => { + const headings = ["Short", "Very Long Header"]; + const cells = [ + ["A", "Short"], + ["Very Long Content", "B"], + ]; + const result = textTable(headings, cells); + + assertEquals(typeof result, "string"); + assertStringIncludes(result, "Short"); + assertStringIncludes(result, "Very Long Header"); + assertStringIncludes(result, "Very Long Content"); + + // Should handle alignment properly + const lines = result.split("\n"); + assertGreater(lines.length, 3); // At least headers, separator, and rows + }); + + await t.step("single column table", () => { + const headings = ["Status"]; + const cells = [["Active"], ["Inactive"], ["Pending"]]; + const result = textTable(headings, cells); + + assertEquals(typeof result, "string"); + assertStringIncludes(result, "Status"); + assertStringIncludes(result, "Active"); + assertStringIncludes(result, "Inactive"); + assertStringIncludes(result, "Pending"); + }); + + await t.step("table with special characters", () => { + const headings = ["Symbols", "Unicode"]; + const cells = [ + ["!@#$%", "αβγδε"], + ["^&*()", "中文测试"], + ]; + const result = textTable(headings, cells); + + assertEquals(typeof result, "string"); + assertStringIncludes(result, "!@#$%"); + assertStringIncludes(result, "αβγδε"); + assertStringIncludes(result, "^&*()"); + assertStringIncludes(result, "中文测试"); + }); + + await t.step("table with empty cells", () => { + const headings = ["Name", "Value"]; + const cells = [ + ["Item1", ""], + ["", "Value2"], + ["Item3", "Value3"], + ]; + const result = textTable(headings, cells); + + assertEquals(typeof result, "string"); + assertStringIncludes(result, "Item1"); + assertStringIncludes(result, "Value2"); + assertStringIncludes(result, "Item3"); + assertStringIncludes(result, "Value3"); + }); + + await t.step("large table structure", () => { + const headings = ["A", "B", "C", "D", "E"]; + const cells = [ + ["1", "2", "3", "4", "5"], + ["6", "7", "8", "9", "10"], + ["11", "12", "13", "14", "15"], + ]; + const result = textTable(headings, cells); + + assertEquals(typeof result, "string"); + + // Check all numbers are present + for (let i = 1; i <= 15; i++) { + assertStringIncludes(result, i.toString()); + } + + // Check all headers are present + ["A", "B", "C", "D", "E"].forEach((header) => { + assertStringIncludes(result, header); + }); + }); + + await t.step("column alignment and spacing", () => { + const headings = ["ID", "Description"]; + const cells = [ + ["1", "Short"], + ["123", "This is a much longer description"], + ]; + const result = textTable(headings, cells); + const lines = result.split("\n"); + + // All lines should have same length (proper alignment) + const firstLineLength = lines[0].length; + lines.forEach((line) => { + assertEquals(line.length, firstLineLength); + }); + + // Should contain proper spacing around content + assertStringIncludes(result, " ID "); + assertStringIncludes(result, " Description "); + }); + + await t.step("table with numbers and mixed content", () => { + const headings = ["Index", "Name", "Score", "Active"]; + const cells = [ + ["0", "Alice", "95.5", "true"], + ["1", "Bob", "87.2", "false"], + ["2", "Charlie", "92.8", "true"], + ]; + const result = textTable(headings, cells); + + assertEquals(typeof result, "string"); + assertStringIncludes(result, "Alice"); + assertStringIncludes(result, "95.5"); + assertStringIncludes(result, "false"); + + // Check that the table has proper structure + const lines = result.split("\n"); + assertEquals(lines.length, 7); // Top, header, separator, 3 data rows, bottom = 7 lines + }); + + await t.step("consistent table formatting", () => { + // Test that identical tables produce identical output + const headings = ["X", "Y"]; + const cells = [["a", "b"]]; + + const result1 = textTable(headings, cells); + const result2 = textTable(headings, cells); + + assertEquals(result1, result2); + }); + + await t.step("table line structure", () => { + const headings = ["Test"]; + const cells = [["Data"]]; + const result = textTable(headings, cells); + const lines = result.split("\n"); + + // Should have: top border, header row, separator, data row, bottom border + assertEquals(lines.length, 5); + + // First and last lines should be borders + assertStringIncludes(lines[0], "┌"); + assertStringIncludes(lines[0], "┐"); + assertStringIncludes(lines[lines.length - 1], "└"); + assertStringIncludes(lines[lines.length - 1], "┘"); + + // Middle separator should contain cross characters + assertStringIncludes(lines[2], "├"); + assertStringIncludes(lines[2], "┤"); + }); +}); + +Deno.test("plainTextTable utilities", async (t) => { + await t.step("basic plain text table with single row", () => { + const headings = ["Name", "Age"]; + const cells = [["John", "30"]]; + const result = plainTextTable(headings, cells); + + assertEquals(typeof result, "string"); + assertStringIncludes(result, "John"); + assertStringIncludes(result, "30"); + + // Should not contain box drawing characters + assertFalse(result.includes("┌")); + assertFalse(result.includes("│")); + assertFalse(result.includes("─")); + + // Should not contain header names + assertFalse(result.includes("Name")); + assertFalse(result.includes("Age")); + }); + + await t.step("plain text table with multiple rows", () => { + const headings = ["Task", "Description"]; + const cells = [ + ["test", "Run local unit tests"], + ["lint", "Run local lint"], + ["fmt", "Run local fmt"], + ]; + const result = plainTextTable(headings, cells); + + const lines = result.split("\n"); + assertEquals(lines.length, 3); // 3 data rows only (no header) + + // Check data rows + assertStringIncludes(lines[0], "test"); + assertStringIncludes(lines[0], "Run local unit tests"); + assertStringIncludes(lines[1], "lint"); + assertStringIncludes(lines[1], "Run local lint"); + assertStringIncludes(lines[2], "fmt"); + assertStringIncludes(lines[2], "Run local fmt"); + + // Should not contain header names + assertFalse(result.includes("Task")); + assertFalse(result.includes("Description")); + }); + + await t.step("plain text table alignment", () => { + const headings = ["Short", "Very Long Header"]; + const cells = [ + ["A", "Short"], + ["Very Long Content", "B"], + ]; + const result = plainTextTable(headings, cells); + + const lines = result.split("\n"); + assertEquals(lines.length, 2); // 2 data rows only (no header) + + // Check that content is present and properly aligned + assertStringIncludes(lines[0], "A"); + assertStringIncludes(lines[0], "Short"); + assertStringIncludes(lines[1], "Very Long Content"); + assertStringIncludes(lines[1], "B"); + + // Check that columns start at consistent positions + const aPos = lines[0].indexOf("A"); + const shortPos = lines[0].indexOf("Short"); + assertEquals(aPos, 0); + assertGreater(shortPos, aPos + 5); + }); + + await t.step("plain text table with empty cells", () => { + const headings = ["Name", "Value"]; + const cells = [ + ["Item1", ""], + ["", "Value2"], + ]; + const result = plainTextTable(headings, cells); + + assertStringIncludes(result, "Item1"); + assertStringIncludes(result, "Value2"); + + const lines = result.split("\n"); + assertEquals(lines.length, 2); // 2 data rows only (no header) + }); + + await t.step("plain text table with single column", () => { + const headings = ["Status"]; + const cells = [["Active"], ["Inactive"]]; + const result = plainTextTable(headings, cells); + + const lines = result.split("\n"); + assertEquals(lines.length, 2); // 2 data rows only (no header) + assertEquals(lines[0].trim(), "Active"); + assertEquals(lines[1].trim(), "Inactive"); + + // Should not contain header name + assertFalse(result.includes("Status")); + }); +}); diff --git a/tests/types.ts b/tests/types.ts new file mode 100644 index 0000000..6be73f9 --- /dev/null +++ b/tests/types.ts @@ -0,0 +1,92 @@ +import type { z } from "zod"; +import type { + Manifest, + TaskData, + TaskName, + Timestamp, + TrackedFileData, + TrackedFileHash, + TrackedFileName, +} from "../interfaces/core/IManifestTypes.ts"; +import type { + ManifestSchema, + TaskDataSchema, + TaskNameSchema, + TimestampSchema, + TrackedFileDataSchema, + TrackedFileHashSchema, + TrackedFileNameSchema, +} from "../core/manifestSchemas.ts"; + +/** + * Compile-time type assertions to ensure Zod schemas match their respective TypeScript interfaces. + * These checks will cause TypeScript compilation to fail if the schemas diverge from the types. + */ + +// Utility types for bidirectional checking +type And = A extends true + ? B extends true ? true : false + : false; +type AllOf = T[number] extends true ? true + : false; +type Equivalent = And< + A extends B ? true : false, + B extends A ? true : false +>; + +// Type checks using utility types +type TaskNameCheck = Equivalent, TaskName>; +type TrackedFileNameCheck = Equivalent< + z.infer, + TrackedFileName +>; +type TrackedFileHashCheck = Equivalent< + z.infer, + TrackedFileHash +>; +type TimestampCheck = Equivalent, Timestamp>; +type TrackedFileDataCheck = Equivalent< + z.infer, + TrackedFileData +>; +type TaskDataCheck = Equivalent, TaskData>; +type ManifestCheck = Equivalent, Manifest>; + +// Type-level check - will cause compile error if any check fails +type AllChecks = { + taskName: TaskNameCheck; + trackedFileName: TrackedFileNameCheck; + trackedFileHash: TrackedFileHashCheck; + timestamp: TimestampCheck; + trackedFileData: TrackedFileDataCheck; + taskData: TaskDataCheck; + manifest: ManifestCheck; +}; + +const allChecks: AllChecks = { + taskName: true, + trackedFileName: true, + trackedFileHash: true, + timestamp: true, + trackedFileData: true, + taskData: true, + manifest: true, +}; + +// Ensure all checks pass using AllOf +type AllChecksPass = AllOf<[AllChecks[keyof AllChecks]]>; +const passed: AllChecksPass = true as const; + +Deno.test("type checks pass at runtime", () => { + // Verify all type checks passed at compile time + if (!passed) { + throw new Error("Type checks failed"); + } + + // Verify all checks in the object are true + for (const [key, value] of Object.entries(allChecks)) { + if (value !== true) { + throw new Error(`Type check failed for ${key}`); + } + } +}); diff --git a/tests/uptodate.test.ts b/tests/uptodate.test.ts new file mode 100644 index 0000000..6b3e79d --- /dev/null +++ b/tests/uptodate.test.ts @@ -0,0 +1,885 @@ +import { assert, assertEquals } from "@std/assert"; +import * as path from "@std/path"; +import { execBasic, Task, TrackedFile } from "../mod.ts"; +import { Manifest } from "../manifest.ts"; +import { runAlways } from "../core/task.ts"; +import { createFileInDir, createTempDir } from "./utils.ts"; +import type { TaskContext } from "../core/TaskContext.ts"; + +Deno.test("UpToDate - file modification detection by hash", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "original content", + ); + const trackedFile = new TrackedFile({ path: tempFile }); + const manifest = new Manifest(""); + + let taskRunCount = 0; + + const task = new Task({ + name: "hashTestTask", + action: () => { + taskRunCount++; + }, + deps: [trackedFile], + }); + + const ctx = await execBasic(["hashTestTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("hashTestTask"); + + // First run - should execute because no previous manifest data + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Reset done tasks to allow re-execution + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Second run - should skip because file hasn't changed + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); // Should not increment + + // Modify file content + await Deno.writeTextFile(tempFile, "modified content"); + + // Reset done tasks to allow re-execution + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Third run - should execute because file content changed + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 2); // Should increment + + await cleanup(); +}); + +Deno.test("UpToDate - timestamp-based change detection", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "timestamp test", + ); + + // Create a TrackedFile with a custom hash function that includes timestamp + const timestampBasedHash = (_filePath: string, stat: Deno.FileInfo) => { + // Use timestamp as the "hash" for change detection + return stat.mtime?.toISOString() || "no-mtime"; + }; + + const trackedFile = new TrackedFile({ + path: tempFile, + getHash: timestampBasedHash, + }); + + const manifest = new Manifest(""); + let taskRunCount = 0; + + const task = new Task({ + name: "timestampTestTask", + action: () => { + taskRunCount++; + }, + deps: [trackedFile], + }); + + // Use execBasic for proper task setup + const ctx = await execBasic(["timestampTestTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("timestampTestTask"); + + // First run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Get the current file data + const initialFileData = await trackedFile.getFileData(); + + // Reset done tasks to allow re-execution + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Second run with no changes - should not run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); // Should not increment + + // Rewrite the same content but this will change the timestamp + await new Promise((resolve) => setTimeout(resolve, 10)); // Wait for timestamp to change + await Deno.writeTextFile(tempFile, "timestamp test"); // Same content, new timestamp + + // Reset done tasks to allow re-execution + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Should detect timestamp change via custom hash function + const newFileData = await trackedFile.getFileData(); + assert(initialFileData.hash !== newFileData.hash); // Different timestamp-based "hash" + + // Task should run due to timestamp change + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 2); + + await cleanup(); +}); + +Deno.test("UpToDate - custom uptodate function execution", async () => { + const manifest = new Manifest(""); + let taskRunCount = 0; + let uptodateCallCount = 0; + + const customUptodate = () => { + uptodateCallCount++; + return uptodateCallCount <= 2; // Return true first two times, false after + }; + + const task = new Task({ + name: "customUptodateTask", + action: () => { + taskRunCount++; + }, + uptodate: customUptodate, + }); + + // Use execBasic for proper task setup + const ctx = await execBasic(["customUptodateTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("customUptodateTask"); + + // First run - custom uptodate returns true, so task should not run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(uptodateCallCount, 1); + assertEquals(taskRunCount, 0); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Second run - custom uptodate returns true again + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(uptodateCallCount, 2); + assertEquals(taskRunCount, 0); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Third run - custom uptodate returns false, so task should run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(uptodateCallCount, 3); + assertEquals(taskRunCount, 1); +}); + +Deno.test("UpToDate - runAlways behavior", async () => { + const manifest = new Manifest(""); + let taskRunCount = 0; + + const task = new Task({ + name: "runAlwaysTask", + action: () => { + taskRunCount++; + }, + uptodate: runAlways, + }); + + // Use execBasic for proper task setup + const ctx = await execBasic(["runAlwaysTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("runAlwaysTask"); + + // First run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Second run - should always run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 2); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Third run - should always run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 3); +}); + +Deno.test("UpToDate - task execution skipping when up-to-date", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "skip test content", + ); + const trackedFile = new TrackedFile({ path: tempFile }); + const targetFile = await createFileInDir( + dirPath, + "target_file.txt", + "target content", + ); + const target = new TrackedFile({ path: targetFile }); + const manifest = new Manifest(""); + + let taskRunCount = 0; + + const task = new Task({ + name: "skipTestTask", + action: () => { + taskRunCount++; + }, + deps: [trackedFile], + targets: [target], + }); + + // Use execBasic for proper task setup + const ctx = await execBasic(["skipTestTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("skipTestTask"); + + // First run - should execute + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Second run - should skip because: + // 1. File dependencies haven't changed + // 2. Targets still exist + // 3. No custom uptodate function forcing re-run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); // Should not increment + + await cleanup(); +}); + +Deno.test("UpToDate - task runs when target is deleted", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "target deletion test", + ); + const trackedFile = new TrackedFile({ path: tempFile }); + const targetFile = await createFileInDir( + dirPath, + "target_file.txt", + "target to delete", + ); + const target = new TrackedFile({ path: targetFile }); + const manifest = new Manifest(""); + + let taskRunCount = 0; + + const task = new Task({ + name: "targetDeletionTask", + action: () => { + taskRunCount++; + // Recreate the target file + Deno.writeTextFileSync(targetFile, "recreated target"); + }, + deps: [trackedFile], + targets: [target], + }); + + // Use execBasic for proper task setup + const ctx = await execBasic(["targetDeletionTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("targetDeletionTask"); + + // First run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Delete the target file + await Deno.remove(targetFile); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Second run - should execute because target was deleted + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 2); + + await cleanup(); +}); + +Deno.test("UpToDate - cross-run manifest state consistency", async () => { + const tempDir = await Deno.makeTempDir({ prefix: "dnit_manifest_test_" }); + const tempFile = path.join(tempDir, "consistency_test.txt"); + await Deno.writeTextFile(tempFile, "consistency test"); + + const trackedFile = new TrackedFile({ path: tempFile }); + + let taskRunCount = 0; + + const taskFactory = () => + new Task({ + name: "consistencyTask", + action: () => { + taskRunCount++; + }, + deps: [trackedFile], + }); + + // First run with first manifest + const manifest1 = new Manifest(tempDir); + await manifest1.load(); + const task1 = taskFactory(); + + const ctx1 = await execBasic(["consistencyTask"], [task1], manifest1); + const requestedTask1 = ctx1.taskRegister.get("consistencyTask"); + if (requestedTask1) { + await requestedTask1.exec(ctx1); + } + assertEquals(taskRunCount, 1); + + // Save manifest state + await manifest1.save(); + + // Second run with new manifest (simulating new process) + const manifest2 = new Manifest(tempDir); + await manifest2.load(); + const task2 = taskFactory(); + + const ctx2 = await execBasic(["consistencyTask"], [task2], manifest2); + const requestedTask2 = ctx2.taskRegister.get("consistencyTask"); + if (requestedTask2) { + await requestedTask2.exec(ctx2); + } + + // Should not run again because manifest shows file is unchanged + assertEquals(taskRunCount, 1); + + // Modify file + await Deno.writeTextFile(tempFile, "modified consistency test"); + + // Reset done tasks and run again + ctx2.doneTasks.clear(); + ctx2.inprogressTasks.clear(); + + if (requestedTask2) { + await requestedTask2.exec(ctx2); + } + assertEquals(taskRunCount, 2); + + await Deno.remove(tempDir, { recursive: true }); +}); + +Deno.test("UpToDate - multiple file dependencies change detection", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile1 = await createFileInDir( + dirPath, + "file1.txt", + "file 1 content", + ); + const tempFile2 = await createFileInDir( + dirPath, + "file2.txt", + "file 2 content", + ); + const trackedFile1 = new TrackedFile({ path: tempFile1 }); + const trackedFile2 = new TrackedFile({ path: tempFile2 }); + const manifest = new Manifest(""); + + let taskRunCount = 0; + + const task = new Task({ + name: "multiFileTask", + action: () => { + taskRunCount++; + }, + deps: [trackedFile1, trackedFile2], + }); + + // Use execBasic for proper task setup + const ctx = await execBasic(["multiFileTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("multiFileTask"); + + // First run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Second run - no changes, should not run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Modify only first file + await Deno.writeTextFile(tempFile1, "modified file 1"); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Third run - should run because first file changed + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 2); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Fourth run - should not run again + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 2); + + // Modify second file + await Deno.writeTextFile(tempFile2, "modified file 2"); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Fifth run - should run because second file changed + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 3); + + await cleanup(); +}); + +Deno.test("UpToDate - task with no dependencies always up-to-date", async () => { + const manifest = new Manifest(""); + let taskRunCount = 0; + + const task = new Task({ + name: "noDepsTask", + action: () => { + taskRunCount++; + }, + // No deps, no targets, no custom uptodate + }); + + // Use execBasic for proper task setup + const ctx = await execBasic(["noDepsTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("noDepsTask"); + + // First run - should not run because it's considered up-to-date + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 0); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Second run - still should not run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 0); +}); + +Deno.test("UpToDate - task with targets but no dependencies", async () => { + const { dirPath, cleanup } = await createTempDir(); + const targetFile = await createFileInDir( + dirPath, + "target_file.txt", + "target only content", + ); + const target = new TrackedFile({ path: targetFile }); + const manifest = new Manifest(""); + + let taskRunCount = 0; + + const task = new Task({ + name: "targetOnlyTask", + action: () => { + taskRunCount++; + }, + targets: [target], + }); + + // Use execBasic for proper task setup + const ctx = await execBasic(["targetOnlyTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("targetOnlyTask"); + + // First run - should not run because target exists + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 0); + + // Delete target + await Deno.remove(targetFile); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Second run - should run because target was deleted + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + await cleanup(); +}); + +Deno.test("UpToDate - custom uptodate with task context access", async () => { + const manifest = new Manifest(""); + let taskRunCount = 0; + let contextReceived = false; + + const customUptodate = (taskCtx: TaskContext): boolean => { + contextReceived = true; + // Verify we have access to task context + return !!(taskCtx.task.name === "contextTask" && taskCtx.logger && + taskCtx.exec); + }; + + const task = new Task({ + name: "contextTask", + action: () => { + taskRunCount++; + }, + uptodate: customUptodate, + }); + + // Use execBasic for proper task setup + const ctx = await execBasic(["contextTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("contextTask"); + + if (requestedTask) { + await requestedTask.exec(ctx); + } + + assert(contextReceived); + assertEquals(taskRunCount, 0); // Should NOT run because uptodate returned true (up-to-date) +}); + +Deno.test("UpToDate - custom hash function based on file size", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "size_hash_test.txt", + "initial content", + ); + + // Custom hash function that uses file size as the "hash" + const sizeBasedHash = (_filePath: string, stat: Deno.FileInfo) => { + return stat.size?.toString() || "0"; + }; + + // Custom timestamp that always returns the same value + const constantTimestamp = (_filePath: string, _stat: Deno.FileInfo) => { + return "2023-01-01T00:00:00.000Z"; + }; + + const trackedFile = new TrackedFile({ + path: tempFile, + getHash: sizeBasedHash, + getTimestamp: constantTimestamp, + }); + + const manifest = new Manifest(""); + let taskRunCount = 0; + + const task = new Task({ + name: "sizeHashTask", + action: () => { + taskRunCount++; + }, + deps: [trackedFile], + }); + + const ctx = await execBasic(["sizeHashTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("sizeHashTask"); + + // First run - should execute to initialize + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Write different content but same length as "initial content" (15 chars) + await Deno.writeTextFile(tempFile, "different conte"); + + // Should NOT run because size-based hash is same AND timestamp is constant + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Change to different size + await Deno.writeTextFile(tempFile, "much longer content than before"); + + // Should run because file size changed + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 2); + + await cleanup(); +}); + +Deno.test("UpToDate - custom timestamp from file content", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "timestamp_test.txt", + "# Timestamp: 2023-01-01T00:00:00.000Z\nsome content here", + ); + + // Custom timestamp function that extracts timestamp from file content + const extractTimestamp = async (filePath: string, _stat: Deno.FileInfo) => { + try { + const content = await Deno.readTextFile(filePath); + const match = content.match(/# Timestamp: (.+)/); + return match ? match[1] : "2000-01-01T00:00:00.000Z"; + } catch { + return "2000-01-01T00:00:00.000Z"; + } + }; + + // Custom hash that always returns the same value + const constantHash = (_filePath: string, _stat: Deno.FileInfo) => { + return "constant-hash"; + }; + + const trackedFile = new TrackedFile({ + path: tempFile, + getHash: constantHash, + getTimestamp: extractTimestamp, + }); + + const manifest = new Manifest(""); + let taskRunCount = 0; + + const task = new Task({ + name: "timestampTask", + action: () => { + taskRunCount++; + }, + deps: [trackedFile], + }); + + const ctx = await execBasic(["timestampTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("timestampTask"); + + // First run - should execute to initialize + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Change content but keep same timestamp in file + await Deno.writeTextFile( + tempFile, + "# Timestamp: 2023-01-01T00:00:00.000Z\ndifferent content here", + ); + + // Should NOT run because custom hash is constant and timestamp didn't change + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Keep same content but change timestamp in file + await Deno.writeTextFile( + tempFile, + "# Timestamp: 2023-12-31T23:59:59.999Z\ndifferent content here", + ); + + // Should run because timestamp changed (now both hash AND timestamp must match) + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 2); + + await cleanup(); +}); + +Deno.test("UpToDate - combined custom hash and timestamp functions", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "combined_test.txt", + "version: 1\ndata: some content", + ); + + // Custom hash based on version line + const versionHash = async (filePath: string, _stat: Deno.FileInfo) => { + try { + const content = await Deno.readTextFile(filePath); + const match = content.match(/version: (\d+)/); + return match ? `v${match[1]}` : "v0"; + } catch { + return "v0"; + } + }; + + // Custom timestamp from file size (simple demonstration) + const sizeTimestamp = (_filePath: string, stat: Deno.FileInfo) => { + return `2023-01-01T00:${stat.size || 0}:00.000Z`; + }; + + const trackedFile = new TrackedFile({ + path: tempFile, + getHash: versionHash, + getTimestamp: sizeTimestamp, + }); + + const manifest = new Manifest(""); + let taskRunCount = 0; + + const task = new Task({ + name: "combinedTask", + action: () => { + taskRunCount++; + }, + deps: [trackedFile], + }); + + const ctx = await execBasic(["combinedTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get("combinedTask"); + + // First run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Change version (hash changes) but keep same size (timestamp same) + await Deno.writeTextFile(tempFile, "version: 2\ndata: some content"); + + // Should run because hash changed (version: 1 → version: 2) + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 2); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Change size (timestamp changes) but keep same version (hash same) + await Deno.writeTextFile( + tempFile, + "version: 2\ndata: different content here", + ); + + // Should run because timestamp changed (file size changed) + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 3); + + await cleanup(); +}); + +Deno.test("UpToDate - file disappears after initial tracking", async () => { + const { dirPath, cleanup } = await createTempDir(); + const tempFile = await createFileInDir( + dirPath, + "test_file.txt", + "file to disappear", + ); + const trackedFile = new TrackedFile({ path: tempFile }); + const manifest = new Manifest(""); + + let taskRunCount = 0; + + const task = new Task({ + name: "disappearingFileTask", + action: () => { + taskRunCount++; + }, + deps: [trackedFile], + }); + + // Use execBasic for proper task setup + const ctx = await execBasic(["disappearingFileTask"], [task], manifest); + const requestedTask = ctx.taskRegister.get( + "disappearingFileTask", + ); + + // First run - file exists + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 1); + + // Delete the file + await Deno.remove(tempFile); + + // Reset done tasks + ctx.doneTasks.clear(); + ctx.inprogressTasks.clear(); + + // Second run - file is gone, should trigger re-run + if (requestedTask) { + await requestedTask.exec(ctx); + } + assertEquals(taskRunCount, 2); + + await cleanup(); +}); diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 0000000..5618866 --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,31 @@ +import * as path from "@std/path"; + +export async function createTempDir(): Promise< + { dirPath: string; cleanup: () => Promise } +> { + const tempDir = await Deno.makeTempDir({ prefix: "dnit_test_" }); + + return { + dirPath: tempDir, + cleanup: async () => { + try { + await Deno.remove(tempDir, { recursive: true }); + } catch (err) { + // Ignore NotFound errors - directory may already be cleaned up + if (!(err instanceof Deno.errors.NotFound)) { + throw err; + } + } + }, + }; +} + +export async function createFileInDir( + dirPath: string, + fileName: string, + content: string, +): Promise { + const filePath = path.join(dirPath, fileName); + await Deno.writeTextFile(filePath, content); + return filePath; +} diff --git a/tools/0001-Revert-non-desired-gen-adl-edits.patch b/tools/0001-Revert-non-desired-gen-adl-edits.patch deleted file mode 100644 index 3528aa6..0000000 --- a/tools/0001-Revert-non-desired-gen-adl-edits.patch +++ /dev/null @@ -1,2607 +0,0 @@ -From 02828841c7ae2492d0b3885e23317ce2a454a9b9 Mon Sep 17 00:00:00 2001 -From: Paul Thompson -Date: Thu, 25 Feb 2021 18:48:52 +1100 -Subject: [PATCH] Revert non desired gen-adl edits - ---- - adl-gen/dnit/manifest.ts | 376 ++++++++++++++++++---- - adl-gen/resolver.ts | 1 + - adl-gen/runtime/adl.ts | 111 +++++-- - adl-gen/runtime/dynamic.ts | 15 +- - adl-gen/runtime/json.ts | 562 ++++++++++++++++++++++----------- - adl-gen/runtime/sys/adlast.ts | 134 ++++---- - adl-gen/runtime/sys/dynamic.ts | 10 +- - adl-gen/runtime/sys/types.ts | 49 ++- - adl-gen/runtime/utils.ts | 68 +++- - adl-gen/sys/types.ts | 421 ++++++++++++++++++++---- - 10 files changed, 1312 insertions(+), 435 deletions(-) - -diff --git a/adl-gen/dnit/manifest.ts b/adl-gen/dnit/manifest.ts -index 8741f6e..8b8a7f4 100644 ---- a/adl-gen/dnit/manifest.ts -+++ b/adl-gen/dnit/manifest.ts -@@ -1,76 +1,245 @@ - /* @generated from adl module dnit.manifest */ -+// deno-lint-ignore-file -+ -+import type * as ADL from "./../runtime/adl.ts"; -+import type * as sys_types from "./../sys/types.ts"; -+ -+export type TaskName = ADL.Flavored0; -+ -+const TaskName_AST: ADL.ScopedDecl = { -+ "moduleName": "dnit.manifest", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "newtype_", -+ "value": { -+ "typeParams": [], -+ "default": { "kind": "nothing" }, -+ "typeExpr": { -+ "typeRef": { "kind": "primitive", "value": "String" }, -+ "parameters": [], -+ }, -+ }, -+ }, -+ "name": "TaskName", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --import * as ADL from "./../runtime/adl.ts"; --import * as sys_types from "./../sys/types.ts"; -- --export type TaskName = string; -- --const TaskName_AST : ADL.ScopedDecl = -- {"moduleName":"dnit.manifest","decl":{"annotations":[],"type_":{"kind":"newtype_","value":{"typeParams":[],"default":{"kind":"nothing"},"typeExpr":{"typeRef":{"kind":"primitive","value":"String"},"parameters":[]}}},"name":"TaskName","version":{"kind":"nothing"}}}; -- --export const snTaskName: ADL.ScopedName = {moduleName:"dnit.manifest", name:"TaskName"}; -+export const snTaskName: ADL.ScopedName = { -+ moduleName: "dnit.manifest", -+ name: "TaskName", -+}; - - export function texprTaskName(): ADL.ATypeExpr { -- return {value : {typeRef : {kind: "reference", value : snTaskName}, parameters : []}}; -+ return { -+ value: { -+ typeRef: { kind: "reference", value: snTaskName }, -+ parameters: [], -+ }, -+ }; - } - --export type TrackedFileName = string; -- --const TrackedFileName_AST : ADL.ScopedDecl = -- {"moduleName":"dnit.manifest","decl":{"annotations":[],"type_":{"kind":"newtype_","value":{"typeParams":[],"default":{"kind":"nothing"},"typeExpr":{"typeRef":{"kind":"primitive","value":"String"},"parameters":[]}}},"name":"TrackedFileName","version":{"kind":"nothing"}}}; -+export type TrackedFileName = ADL.Flavored0; -+ -+const TrackedFileName_AST: ADL.ScopedDecl = { -+ "moduleName": "dnit.manifest", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "newtype_", -+ "value": { -+ "typeParams": [], -+ "default": { "kind": "nothing" }, -+ "typeExpr": { -+ "typeRef": { "kind": "primitive", "value": "String" }, -+ "parameters": [], -+ }, -+ }, -+ }, -+ "name": "TrackedFileName", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snTrackedFileName: ADL.ScopedName = {moduleName:"dnit.manifest", name:"TrackedFileName"}; -+export const snTrackedFileName: ADL.ScopedName = { -+ moduleName: "dnit.manifest", -+ name: "TrackedFileName", -+}; - - export function texprTrackedFileName(): ADL.ATypeExpr { -- return {value : {typeRef : {kind: "reference", value : snTrackedFileName}, parameters : []}}; -+ return { -+ value: { -+ typeRef: { kind: "reference", value: snTrackedFileName }, -+ parameters: [], -+ }, -+ }; - } - --export type TrackedFileHash = string; -- --const TrackedFileHash_AST : ADL.ScopedDecl = -- {"moduleName":"dnit.manifest","decl":{"annotations":[],"type_":{"kind":"newtype_","value":{"typeParams":[],"default":{"kind":"nothing"},"typeExpr":{"typeRef":{"kind":"primitive","value":"String"},"parameters":[]}}},"name":"TrackedFileHash","version":{"kind":"nothing"}}}; -+export type TrackedFileHash = ADL.Flavored0; -+ -+const TrackedFileHash_AST: ADL.ScopedDecl = { -+ "moduleName": "dnit.manifest", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "newtype_", -+ "value": { -+ "typeParams": [], -+ "default": { "kind": "nothing" }, -+ "typeExpr": { -+ "typeRef": { "kind": "primitive", "value": "String" }, -+ "parameters": [], -+ }, -+ }, -+ }, -+ "name": "TrackedFileHash", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snTrackedFileHash: ADL.ScopedName = {moduleName:"dnit.manifest", name:"TrackedFileHash"}; -+export const snTrackedFileHash: ADL.ScopedName = { -+ moduleName: "dnit.manifest", -+ name: "TrackedFileHash", -+}; - - export function texprTrackedFileHash(): ADL.ATypeExpr { -- return {value : {typeRef : {kind: "reference", value : snTrackedFileHash}, parameters : []}}; -+ return { -+ value: { -+ typeRef: { kind: "reference", value: snTrackedFileHash }, -+ parameters: [], -+ }, -+ }; - } - --export type Timestamp = string; -- --const Timestamp_AST : ADL.ScopedDecl = -- {"moduleName":"dnit.manifest","decl":{"annotations":[],"type_":{"kind":"newtype_","value":{"typeParams":[],"default":{"kind":"nothing"},"typeExpr":{"typeRef":{"kind":"primitive","value":"String"},"parameters":[]}}},"name":"Timestamp","version":{"kind":"nothing"}}}; -+export type Timestamp = ADL.Flavored0; -+ -+const Timestamp_AST: ADL.ScopedDecl = { -+ "moduleName": "dnit.manifest", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "newtype_", -+ "value": { -+ "typeParams": [], -+ "default": { "kind": "nothing" }, -+ "typeExpr": { -+ "typeRef": { "kind": "primitive", "value": "String" }, -+ "parameters": [], -+ }, -+ }, -+ }, -+ "name": "Timestamp", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snTimestamp: ADL.ScopedName = {moduleName:"dnit.manifest", name:"Timestamp"}; -+export const snTimestamp: ADL.ScopedName = { -+ moduleName: "dnit.manifest", -+ name: "Timestamp", -+}; - - export function texprTimestamp(): ADL.ATypeExpr { -- return {value : {typeRef : {kind: "reference", value : snTimestamp}, parameters : []}}; -+ return { -+ value: { -+ typeRef: { kind: "reference", value: snTimestamp }, -+ parameters: [], -+ }, -+ }; - } - - export interface TaskData { -- lastExecution: (Timestamp|null); -+ lastExecution: (Timestamp | null); - trackedFiles: sys_types.Map; - } - - export function makeTaskData( - input: { -- lastExecution?: (Timestamp|null), -- trackedFiles: sys_types.Map, -- } -+ lastExecution?: (Timestamp | null); -+ trackedFiles: sys_types.Map; -+ }, - ): TaskData { - return { -- lastExecution: input.lastExecution === undefined ? null : input.lastExecution, -+ lastExecution: input.lastExecution === undefined -+ ? null -+ : input.lastExecution, - trackedFiles: input.trackedFiles, - }; - } - --const TaskData_AST : ADL.ScopedDecl = -- {"moduleName":"dnit.manifest","decl":{"annotations":[],"type_":{"kind":"struct_","value":{"typeParams":[],"fields":[{"annotations":[],"serializedName":"lastExecution","default":{"kind":"just","value":null},"name":"lastExecution","typeExpr":{"typeRef":{"kind":"primitive","value":"Nullable"},"parameters":[{"typeRef":{"kind":"reference","value":{"moduleName":"dnit.manifest","name":"Timestamp"}},"parameters":[]}]}},{"annotations":[],"serializedName":"trackedFiles","default":{"kind":"nothing"},"name":"trackedFiles","typeExpr":{"typeRef":{"kind":"reference","value":{"moduleName":"sys.types","name":"Map"}},"parameters":[{"typeRef":{"kind":"reference","value":{"moduleName":"dnit.manifest","name":"TrackedFileName"}},"parameters":[]},{"typeRef":{"kind":"reference","value":{"moduleName":"dnit.manifest","name":"TrackedFileData"}},"parameters":[]}]}}]}},"name":"TaskData","version":{"kind":"nothing"}}}; -+const TaskData_AST: ADL.ScopedDecl = { -+ "moduleName": "dnit.manifest", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "struct_", -+ "value": { -+ "typeParams": [], -+ "fields": [{ -+ "annotations": [], -+ "serializedName": "lastExecution", -+ "default": { "kind": "just", "value": null }, -+ "name": "lastExecution", -+ "typeExpr": { -+ "typeRef": { "kind": "primitive", "value": "Nullable" }, -+ "parameters": [{ -+ "typeRef": { -+ "kind": "reference", -+ "value": { "moduleName": "dnit.manifest", "name": "Timestamp" }, -+ }, -+ "parameters": [], -+ }], -+ }, -+ }, { -+ "annotations": [], -+ "serializedName": "trackedFiles", -+ "default": { "kind": "nothing" }, -+ "name": "trackedFiles", -+ "typeExpr": { -+ "typeRef": { -+ "kind": "reference", -+ "value": { "moduleName": "sys.types", "name": "Map" }, -+ }, -+ "parameters": [{ -+ "typeRef": { -+ "kind": "reference", -+ "value": { -+ "moduleName": "dnit.manifest", -+ "name": "TrackedFileName", -+ }, -+ }, -+ "parameters": [], -+ }, { -+ "typeRef": { -+ "kind": "reference", -+ "value": { -+ "moduleName": "dnit.manifest", -+ "name": "TrackedFileData", -+ }, -+ }, -+ "parameters": [], -+ }], -+ }, -+ }], -+ }, -+ }, -+ "name": "TaskData", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snTaskData: ADL.ScopedName = {moduleName:"dnit.manifest", name:"TaskData"}; -+export const snTaskData: ADL.ScopedName = { -+ moduleName: "dnit.manifest", -+ name: "TaskData", -+}; - - export function texprTaskData(): ADL.ATypeExpr { -- return {value : {typeRef : {kind: "reference", value : snTaskData}, parameters : []}}; -+ return { -+ value: { -+ typeRef: { kind: "reference", value: snTaskData }, -+ parameters: [], -+ }, -+ }; - } - - export interface TrackedFileData { -@@ -80,9 +249,9 @@ export interface TrackedFileData { - - export function makeTrackedFileData( - input: { -- hash: TrackedFileHash, -- timestamp: Timestamp, -- } -+ hash: TrackedFileHash; -+ timestamp: Timestamp; -+ }, - ): TrackedFileData { - return { - hash: input.hash, -@@ -90,13 +259,61 @@ export function makeTrackedFileData( - }; - } - --const TrackedFileData_AST : ADL.ScopedDecl = -- {"moduleName":"dnit.manifest","decl":{"annotations":[],"type_":{"kind":"struct_","value":{"typeParams":[],"fields":[{"annotations":[],"serializedName":"hash","default":{"kind":"nothing"},"name":"hash","typeExpr":{"typeRef":{"kind":"reference","value":{"moduleName":"dnit.manifest","name":"TrackedFileHash"}},"parameters":[]}},{"annotations":[],"serializedName":"timestamp","default":{"kind":"nothing"},"name":"timestamp","typeExpr":{"typeRef":{"kind":"reference","value":{"moduleName":"dnit.manifest","name":"Timestamp"}},"parameters":[]}}]}},"name":"TrackedFileData","version":{"kind":"nothing"}}}; -+const TrackedFileData_AST: ADL.ScopedDecl = { -+ "moduleName": "dnit.manifest", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "struct_", -+ "value": { -+ "typeParams": [], -+ "fields": [{ -+ "annotations": [], -+ "serializedName": "hash", -+ "default": { "kind": "nothing" }, -+ "name": "hash", -+ "typeExpr": { -+ "typeRef": { -+ "kind": "reference", -+ "value": { -+ "moduleName": "dnit.manifest", -+ "name": "TrackedFileHash", -+ }, -+ }, -+ "parameters": [], -+ }, -+ }, { -+ "annotations": [], -+ "serializedName": "timestamp", -+ "default": { "kind": "nothing" }, -+ "name": "timestamp", -+ "typeExpr": { -+ "typeRef": { -+ "kind": "reference", -+ "value": { "moduleName": "dnit.manifest", "name": "Timestamp" }, -+ }, -+ "parameters": [], -+ }, -+ }], -+ }, -+ }, -+ "name": "TrackedFileData", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snTrackedFileData: ADL.ScopedName = {moduleName:"dnit.manifest", name:"TrackedFileData"}; -+export const snTrackedFileData: ADL.ScopedName = { -+ moduleName: "dnit.manifest", -+ name: "TrackedFileData", -+}; - - export function texprTrackedFileData(): ADL.ATypeExpr { -- return {value : {typeRef : {kind: "reference", value : snTrackedFileData}, parameters : []}}; -+ return { -+ value: { -+ typeRef: { kind: "reference", value: snTrackedFileData }, -+ parameters: [], -+ }, -+ }; - } - - export interface Manifest { -@@ -105,29 +322,74 @@ export interface Manifest { - - export function makeManifest( - input: { -- tasks?: sys_types.Map, -- } -+ tasks?: sys_types.Map; -+ }, - ): Manifest { - return { - tasks: input.tasks === undefined ? [] : input.tasks, - }; - } - --const Manifest_AST : ADL.ScopedDecl = -- {"moduleName":"dnit.manifest","decl":{"annotations":[],"type_":{"kind":"struct_","value":{"typeParams":[],"fields":[{"annotations":[],"serializedName":"tasks","default":{"kind":"just","value":[]},"name":"tasks","typeExpr":{"typeRef":{"kind":"reference","value":{"moduleName":"sys.types","name":"Map"}},"parameters":[{"typeRef":{"kind":"reference","value":{"moduleName":"dnit.manifest","name":"TaskName"}},"parameters":[]},{"typeRef":{"kind":"reference","value":{"moduleName":"dnit.manifest","name":"TaskData"}},"parameters":[]}]}}]}},"name":"Manifest","version":{"kind":"nothing"}}}; -+const Manifest_AST: ADL.ScopedDecl = { -+ "moduleName": "dnit.manifest", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "struct_", -+ "value": { -+ "typeParams": [], -+ "fields": [{ -+ "annotations": [], -+ "serializedName": "tasks", -+ "default": { "kind": "just", "value": [] }, -+ "name": "tasks", -+ "typeExpr": { -+ "typeRef": { -+ "kind": "reference", -+ "value": { "moduleName": "sys.types", "name": "Map" }, -+ }, -+ "parameters": [{ -+ "typeRef": { -+ "kind": "reference", -+ "value": { "moduleName": "dnit.manifest", "name": "TaskName" }, -+ }, -+ "parameters": [], -+ }, { -+ "typeRef": { -+ "kind": "reference", -+ "value": { "moduleName": "dnit.manifest", "name": "TaskData" }, -+ }, -+ "parameters": [], -+ }], -+ }, -+ }], -+ }, -+ }, -+ "name": "Manifest", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snManifest: ADL.ScopedName = {moduleName:"dnit.manifest", name:"Manifest"}; -+export const snManifest: ADL.ScopedName = { -+ moduleName: "dnit.manifest", -+ name: "Manifest", -+}; - - export function texprManifest(): ADL.ATypeExpr { -- return {value : {typeRef : {kind: "reference", value : snManifest}, parameters : []}}; -+ return { -+ value: { -+ typeRef: { kind: "reference", value: snManifest }, -+ parameters: [], -+ }, -+ }; - } - - export const _AST_MAP: { [key: string]: ADL.ScopedDecl } = { -- "dnit.manifest.TaskName" : TaskName_AST, -- "dnit.manifest.TrackedFileName" : TrackedFileName_AST, -- "dnit.manifest.TrackedFileHash" : TrackedFileHash_AST, -- "dnit.manifest.Timestamp" : Timestamp_AST, -- "dnit.manifest.TaskData" : TaskData_AST, -- "dnit.manifest.TrackedFileData" : TrackedFileData_AST, -- "dnit.manifest.Manifest" : Manifest_AST -+ "dnit.manifest.TaskName": TaskName_AST, -+ "dnit.manifest.TrackedFileName": TrackedFileName_AST, -+ "dnit.manifest.TrackedFileHash": TrackedFileHash_AST, -+ "dnit.manifest.Timestamp": Timestamp_AST, -+ "dnit.manifest.TaskData": TaskData_AST, -+ "dnit.manifest.TrackedFileData": TrackedFileData_AST, -+ "dnit.manifest.Manifest": Manifest_AST, - }; -diff --git a/adl-gen/resolver.ts b/adl-gen/resolver.ts -index 7218d79..2015aa7 100644 ---- a/adl-gen/resolver.ts -+++ b/adl-gen/resolver.ts -@@ -1,3 +1,4 @@ -+// deno-lint-ignore-file - /* @generated from adl */ - import { declResolver, ScopedDecl } from "./runtime/adl.ts"; - import { _AST_MAP as dnit_manifest } from "./dnit/manifest.ts"; -diff --git a/adl-gen/runtime/adl.ts b/adl-gen/runtime/adl.ts -index 4b44aff..405aa37 100644 ---- a/adl-gen/runtime/adl.ts -+++ b/adl-gen/runtime/adl.ts -@@ -1,26 +1,30 @@ --import * as AST from "./sys/adlast.ts"; -+//deno-lint-ignore-file -+import type * as AST from "./sys/adlast.ts"; -+import type * as utils from "./utils.ts"; - - export type ScopedName = AST.ScopedName; - export type ScopedDecl = AST.ScopedDecl; --export type ATypeRef<_T> = {value: AST.TypeRef}; --export type ATypeExpr<_T> = {value : AST.TypeExpr}; -+export type ATypeRef<_T> = { value: AST.TypeRef }; -+export type ATypeExpr<_T> = { value: AST.TypeExpr }; - - /** - * A function to obtain details on a declared type. - */ - export interface DeclResolver { -- (decl : AST.ScopedName): AST.ScopedDecl; --}; -+ (decl: AST.ScopedName): AST.ScopedDecl; -+} - --export function declResolver(...astMaps : ({[key:string] : AST.ScopedDecl})[]) { -- const astMap : {[key:string] : AST.ScopedDecl} = {}; -+export function declResolver( -+ ...astMaps: ({ [key: string]: AST.ScopedDecl })[] -+) { -+ const astMap: { [key: string]: AST.ScopedDecl } = {}; - for (let map of astMaps) { - for (let scopedName in map) { - astMap[scopedName] = map[scopedName]; - } - } - -- function resolver(scopedName : AST.ScopedName) : AST.ScopedDecl { -+ function resolver(scopedName: AST.ScopedName): AST.ScopedDecl { - const scopedNameStr = scopedName.moduleName + "." + scopedName.name; - const result = astMap[scopedNameStr]; - if (result === undefined) { -@@ -41,44 +45,85 @@ function texprPrimitive(ptype: string): ATypeExpr { - return { - value: { - typeRef: { kind: "primitive", value: ptype }, -- parameters: [] -- } -+ parameters: [], -+ }, - }; --}; -+} - --function texprPrimitive1(ptype: string, etype: ATypeExpr): ATypeExpr { -+function texprPrimitive1( -+ ptype: string, -+ etype: ATypeExpr, -+): ATypeExpr { - return { - value: { - typeRef: { kind: "primitive", value: ptype }, -- parameters: [etype.value] -- } -+ parameters: [etype.value], -+ }, - }; --}; -+} - --export function texprVoid() : ATypeExpr {return texprPrimitive("Void");} --export function texprBool() : ATypeExpr {return texprPrimitive("Bool");} --export function texprInt8() : ATypeExpr {return texprPrimitive("Int8");} --export function texprInt16() : ATypeExpr {return texprPrimitive("Int16");} --export function texprInt32() : ATypeExpr {return texprPrimitive("Int32");} --export function texprInt64() : ATypeExpr {return texprPrimitive("Int64");} --export function texprWord8() : ATypeExpr {return texprPrimitive("Word8");} --export function texprWord16() : ATypeExpr {return texprPrimitive("Word16");} --export function texprWord32() : ATypeExpr {return texprPrimitive("Word32");} --export function texprWord64() : ATypeExpr {return texprPrimitive("Word64");} --export function texprFloat() : ATypeExpr {return texprPrimitive("Float");} --export function texprDouble() : ATypeExpr {return texprPrimitive("Double");} --export function texprJson() : ATypeExpr {return texprPrimitive("Json");} --export function texprByteVector() : ATypeExpr {return texprPrimitive("ByteVector");} --export function texprString() : ATypeExpr {return texprPrimitive("String");} -+export function texprVoid(): ATypeExpr { -+ return texprPrimitive("Void"); -+} -+export function texprBool(): ATypeExpr { -+ return texprPrimitive("Bool"); -+} -+export function texprInt8(): ATypeExpr { -+ return texprPrimitive("Int8"); -+} -+export function texprInt16(): ATypeExpr { -+ return texprPrimitive("Int16"); -+} -+export function texprInt32(): ATypeExpr { -+ return texprPrimitive("Int32"); -+} -+export function texprInt64(): ATypeExpr { -+ return texprPrimitive("Int64"); -+} -+export function texprWord8(): ATypeExpr { -+ return texprPrimitive("Word8"); -+} -+export function texprWord16(): ATypeExpr { -+ return texprPrimitive("Word16"); -+} -+export function texprWord32(): ATypeExpr { -+ return texprPrimitive("Word32"); -+} -+export function texprWord64(): ATypeExpr { -+ return texprPrimitive("Word64"); -+} -+export function texprFloat(): ATypeExpr { -+ return texprPrimitive("Float"); -+} -+export function texprDouble(): ATypeExpr { -+ return texprPrimitive("Double"); -+} -+export function texprJson(): ATypeExpr { -+ return texprPrimitive("Json"); -+} -+export function texprByteVector(): ATypeExpr { -+ return texprPrimitive("ByteVector"); -+} -+export function texprString(): ATypeExpr { -+ return texprPrimitive("String"); -+} - --export function texprVector(etype: ATypeExpr) : ATypeExpr { -+export function texprVector(etype: ATypeExpr): ATypeExpr { - return texprPrimitive1("Vector", etype); - } - --export function texprStringMap(etype: ATypeExpr) : ATypeExpr<{[key:string]:T}> { -+export function texprStringMap( -+ etype: ATypeExpr, -+): ATypeExpr<{ [key: string]: T }> { - return texprPrimitive1("StringMap", etype); - } - --export function texprNullable(etype: ATypeExpr) : ATypeExpr { -+export function texprNullable(etype: ATypeExpr): ATypeExpr { - return texprPrimitive1("Nullable", etype); - } -+// "Flavoured" nominal typing. -+// https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/ -+export type Flavored0 = utils.Flavored0; -+export type Flavored1 = utils.Flavored1; -+export type Flavored2 = utils.Flavored2; -+export type Flavored3 = utils.Flavored3; -diff --git a/adl-gen/runtime/dynamic.ts b/adl-gen/runtime/dynamic.ts -index 2f38342..bd27593 100644 ---- a/adl-gen/runtime/dynamic.ts -+++ b/adl-gen/runtime/dynamic.ts -@@ -1,18 +1,21 @@ --import {typeExprsEqual} from "./utils.ts"; --import {JsonBinding} from "./json.ts"; --import {Dynamic} from "./sys/dynamic.ts"; -+import { typeExprsEqual } from "./utils.ts"; -+import { JsonBinding } from "./json.ts"; -+import { Dynamic } from "./sys/dynamic.ts"; - - /** - * Convert an ADL value to a dynamically typed value - */ --export function toDynamic(jsonBinding : JsonBinding, value : T) : Dynamic { -- return {typeExpr: jsonBinding.typeExpr, value : jsonBinding.toJson(value)}; -+export function toDynamic(jsonBinding: JsonBinding, value: T): Dynamic { -+ return { typeExpr: jsonBinding.typeExpr, value: jsonBinding.toJson(value) }; - } - - /** - * Convert an ADL value to a dynamically typed value - */ --export function fromDynamic(jsonBinding : JsonBinding, dynamic : Dynamic) : (T|null) { -+export function fromDynamic( -+ jsonBinding: JsonBinding, -+ dynamic: Dynamic, -+): (T | null) { - if (typeExprsEqual(jsonBinding.typeExpr, dynamic.typeExpr)) { - return jsonBinding.fromJson(dynamic.value); - } -diff --git a/adl-gen/runtime/json.ts b/adl-gen/runtime/json.ts -index a551879..372d84a 100644 ---- a/adl-gen/runtime/json.ts -+++ b/adl-gen/runtime/json.ts -@@ -1,7 +1,9 @@ --import {DeclResolver,ATypeExpr} from "./adl.ts"; --import * as AST from "./sys/adlast.ts"; --import * as b64 from "base64-js.ts"; --import {isVoid, isEnum, scopedNamesEqual} from "./utils.ts"; -+// deno-lint-ignore-file -+ -+import type { ATypeExpr, DeclResolver } from "./adl.ts"; -+import type * as AST from "./sys/adlast.ts"; -+//import * as b64 from 'base64-js'; -+import { isEnum, isVoid, scopedNamesEqual } from "./utils.ts"; - - /** A type for json serialised values */ - -@@ -16,56 +18,62 @@ function asJsonObject(jv: Json): JsonObject | undefined { - return undefined; - } - --function asJsonArray(jv: Json): JsonArray | undefined{ -- if(jv instanceof Array) { -+function asJsonArray(jv: Json): JsonArray | undefined { -+ if (jv instanceof Array) { - return jv as JsonArray; - } - return undefined; - } - - /** A type alias for values of an Unknown type */ --type Unknown = {}|null; -+type Unknown = {} | null; - - /** - * A JsonBinding is a de/serialiser for a give ADL type - */ - export interface JsonBinding { -- typeExpr : AST.TypeExpr; -+ typeExpr: AST.TypeExpr; - - // Convert a value of type T to Json -- toJson (t : T): Json; -+ toJson(t: T): Json; - - // Parse a json blob into a value of type T. Throws - // JsonParseExceptions on failure. -- fromJson(json : Json) : T; -+ fromJson(json: Json): T; - - // Variant of fromJson that throws Errors on failure -- fromJsonE(json : Json) : T; --}; -- --/** -+ fromJsonE(json: Json): T; -+}/** - * Construct a JsonBinding for an arbitrary type expression - */ --export function createJsonBinding(dresolver : DeclResolver, texpr : ATypeExpr) : JsonBinding { -+ -+export function createJsonBinding( -+ dresolver: DeclResolver, -+ texpr: ATypeExpr, -+): JsonBinding { - const jb0 = buildJsonBinding(dresolver, texpr.value, {}) as JsonBinding0; -- function fromJsonE(json :Json): T { -+ function fromJsonE(json: Json): T { - try { - return jb0.fromJson(json); - } catch (e) { - throw mapJsonException(e); - } - } -- return {typeExpr : texpr.value, toJson:jb0.toJson, fromJson:jb0.fromJson, fromJsonE}; --}; -- --/** -+ return { -+ typeExpr: texpr.value, -+ toJson: jb0.toJson, -+ fromJson: jb0.fromJson, -+ fromJsonE, -+ }; -+}/** - * Interface for json parsing exceptions. - * Any implementation should properly show the parse error tree. - * - * @interface JsonParseException - */ -+ - export interface JsonParseException { -- kind: 'JsonParseException'; -+ kind: "JsonParseException"; - getMessage(): string; - pushField(fieldName: string): void; - pushIndex(index: number): void; -@@ -73,8 +81,10 @@ export interface JsonParseException { - } - - // Map a JsonException to an Error value --export function mapJsonException(exception:{}): {} { -- if (exception && (exception as {kind:string})['kind'] == "JsonParseException") { -+export function mapJsonException(exception: {}): {} { -+ if ( -+ exception && (exception as { kind: string })["kind"] == "JsonParseException" -+ ) { - const jserr: JsonParseException = exception as JsonParseException; - return new Error(jserr.getMessage()); - } else { -@@ -89,24 +99,24 @@ export function jsonParseException(message: string): JsonParseException { - const context: string[] = []; - let createContextString: () => string = () => { - const rcontext: string[] = context.slice(0); -- rcontext.push('$'); -+ rcontext.push("$"); - rcontext.reverse(); -- return rcontext.join('.'); -+ return rcontext.join("."); - }; - return { -- kind: 'JsonParseException', -+ kind: "JsonParseException", - getMessage(): string { -- return message + ' at ' + createContextString(); -+ return message + " at " + createContextString(); - }, - pushField(fieldName: string): void { - context.push(fieldName); - }, - pushIndex(index: number): void { -- context.push('[' + index + ']'); -+ context.push("[" + index + "]"); - }, - toString(): string { - return this.getMessage(); -- } -+ }, - }; - } - -@@ -114,133 +124,223 @@ export function jsonParseException(message: string): JsonParseException { - * Check if a javascript error is of the json parse exception type. - * @param exception The exception to check. - */ --export function isJsonParseException(exception: {}): exception is JsonParseException { -- return ( exception).kind === 'JsonParseException'; -+export function isJsonParseException( -+ exception: {}, -+): exception is JsonParseException { -+ return ( exception).kind === "JsonParseException"; - } - - interface JsonBinding0 { -- toJson (t : T): Json; -- fromJson(json : Json) : T; --}; -+ toJson(t: T): Json; -+ fromJson(json: Json): T; -+} - - interface BoundTypeParams { - [key: string]: JsonBinding0; - } - --function buildJsonBinding(dresolver : DeclResolver, texpr : AST.TypeExpr, boundTypeParams : BoundTypeParams) : JsonBinding0 { -+function buildJsonBinding( -+ dresolver: DeclResolver, -+ texpr: AST.TypeExpr, -+ boundTypeParams: BoundTypeParams, -+): JsonBinding0 { - if (texpr.typeRef.kind === "primitive") { -- return primitiveJsonBinding(dresolver, texpr.typeRef.value, texpr.parameters, boundTypeParams); -+ return primitiveJsonBinding( -+ dresolver, -+ texpr.typeRef.value, -+ texpr.parameters, -+ boundTypeParams, -+ ); - } else if (texpr.typeRef.kind === "reference") { - const ast = dresolver(texpr.typeRef.value); - if (ast.decl.type_.kind === "struct_") { -- return structJsonBinding(dresolver, ast.decl.type_.value, texpr.parameters, boundTypeParams); -+ return structJsonBinding( -+ dresolver, -+ ast.decl.type_.value, -+ texpr.parameters, -+ boundTypeParams, -+ ); - } else if (ast.decl.type_.kind === "union_") { - const union = ast.decl.type_.value; - if (isEnum(union)) { -- return enumJsonBinding(dresolver, union, texpr.parameters, boundTypeParams); -+ return enumJsonBinding( -+ dresolver, -+ union, -+ texpr.parameters, -+ boundTypeParams, -+ ); - } else { -- return unionJsonBinding(dresolver, union, texpr.parameters, boundTypeParams); -+ return unionJsonBinding( -+ dresolver, -+ union, -+ texpr.parameters, -+ boundTypeParams, -+ ); - } - } else if (ast.decl.type_.kind === "newtype_") { -- return newtypeJsonBinding(dresolver, ast.decl.type_.value, texpr.parameters, boundTypeParams); -+ return newtypeJsonBinding( -+ dresolver, -+ ast.decl.type_.value, -+ texpr.parameters, -+ boundTypeParams, -+ ); - } else if (ast.decl.type_.kind === "type_") { -- return typedefJsonBinding(dresolver, ast.decl.type_.value, texpr.parameters, boundTypeParams); -+ return typedefJsonBinding( -+ dresolver, -+ ast.decl.type_.value, -+ texpr.parameters, -+ boundTypeParams, -+ ); - } - } else if (texpr.typeRef.kind === "typeParam") { - return boundTypeParams[texpr.typeRef.value]; - } - throw new Error("buildJsonBinding : unimplemented ADL type"); --}; -- --function primitiveJsonBinding(dresolver : DeclResolver, ptype : string, params : AST.TypeExpr[], boundTypeParams : BoundTypeParams ) : JsonBinding0 { -- if (ptype === "String") { return identityJsonBinding("a string", (v) => typeof(v) === 'string'); } -- else if (ptype === "Int8") { return identityJsonBinding("a number", (v) => typeof(v) === 'number'); } -- else if (ptype === "Void") { return identityJsonBinding("a null", (v) => v === null); } -- else if (ptype === "Bool") { return identityJsonBinding("a bool", (v) => typeof(v) === 'boolean'); } -- else if (ptype === "Int8") { return identityJsonBinding("a number", (v) => typeof(v) === 'number'); } -- else if (ptype === "Int16") { return identityJsonBinding("a number", (v) => typeof(v) === 'number'); } -- else if (ptype === "Int32") { return identityJsonBinding("a number", (v) => typeof(v) === 'number'); } -- else if (ptype === "Int64") { return identityJsonBinding("a number", (v) => typeof(v) === 'number'); } -- else if (ptype === "Word8") { return identityJsonBinding("a number", (v) => typeof(v) === 'number'); } -- else if (ptype === "Word16") { return identityJsonBinding("a number", (v) => typeof(v) === 'number'); } -- else if (ptype === "Word32") { return identityJsonBinding("a number", (v) => typeof(v) === 'number'); } -- else if (ptype === "Word64") { return identityJsonBinding("a number", (v) => typeof(v) === 'number'); } -- else if (ptype === "Float") { return identityJsonBinding("a number", (v) => typeof(v) === 'number'); } -- else if (ptype === "Double") { return identityJsonBinding("a number", (v) => typeof(v) === 'number'); } -- else if (ptype === "Json") { return identityJsonBinding("a json value", (_v) => true); } -- else if (ptype === "Bytes") { return bytesJsonBinding(); } -- else if (ptype === "Vector") { return vectorJsonBinding(dresolver, params[0], boundTypeParams); } -- else if (ptype === "StringMap") { return stringMapJsonBinding(dresolver, params[0], boundTypeParams); } -- else if (ptype === "Nullable") { return nullableJsonBinding(dresolver, params[0], boundTypeParams); } -- else throw new Error("Unimplemented json binding for primitive " + ptype); --}; -- --function identityJsonBinding(expected : string, predicate : (json : Json) => boolean) : JsonBinding0{ -- -- function toJson(v : T) : Json { -+} -+ -+function primitiveJsonBinding( -+ dresolver: DeclResolver, -+ ptype: string, -+ params: AST.TypeExpr[], -+ boundTypeParams: BoundTypeParams, -+): JsonBinding0 { -+ if (ptype === "String") { -+ return identityJsonBinding("a string", (v) => -+ typeof (v) === "string"); -+ } else if (ptype === "Int8") { -+ return identityJsonBinding("a number", (v) => -+ typeof (v) === "number"); -+ } else if (ptype === "Void") { -+ return identityJsonBinding("a null", (v) => -+ v === null); -+ } else if (ptype === "Bool") { -+ return identityJsonBinding("a bool", (v) => -+ typeof (v) === "boolean"); -+ } else if (ptype === "Int8") { -+ return identityJsonBinding("a number", (v) => -+ typeof (v) === "number"); -+ } else if (ptype === "Int16") { -+ return identityJsonBinding("a number", (v) => -+ typeof (v) === "number"); -+ } else if (ptype === "Int32") { -+ return identityJsonBinding("a number", (v) => -+ typeof (v) === "number"); -+ } else if (ptype === "Int64") { -+ return identityJsonBinding("a number", (v) => -+ typeof (v) === "number"); -+ } else if (ptype === "Word8") { -+ return identityJsonBinding("a number", (v) => -+ typeof (v) === "number"); -+ } else if (ptype === "Word16") { -+ return identityJsonBinding("a number", (v) => -+ typeof (v) === "number"); -+ } else if (ptype === "Word32") { -+ return identityJsonBinding("a number", (v) => -+ typeof (v) === "number"); -+ } else if (ptype === "Word64") { -+ return identityJsonBinding("a number", (v) => -+ typeof (v) === "number"); -+ } else if (ptype === "Float") { -+ return identityJsonBinding("a number", (v) => -+ typeof (v) === "number"); -+ } else if (ptype === "Double") { -+ return identityJsonBinding("a number", (v) => -+ typeof (v) === "number"); -+ } else if (ptype === "Json") { -+ return identityJsonBinding("a json value", (_v) => -+ true); -+ } else if (ptype === "Bytes") return bytesJsonBinding(); -+ else if (ptype === "Vector") { -+ return vectorJsonBinding(dresolver, params[0], boundTypeParams); -+ } else if (ptype === "StringMap") { -+ return stringMapJsonBinding(dresolver, params[0], boundTypeParams); -+ } else if (ptype === "Nullable") { -+ return nullableJsonBinding(dresolver, params[0], boundTypeParams); -+ } else throw new Error("Unimplemented json binding for primitive " + ptype); -+} -+ -+function identityJsonBinding( -+ expected: string, -+ predicate: (json: Json) => boolean, -+): JsonBinding0 { -+ function toJson(v: T): Json { - return (v as Unknown as Json); - } - -- function fromJson(json : Json) : T { -- if( !predicate(json)) { -+ function fromJson(json: Json): T { -+ if (!predicate(json)) { - throw jsonParseException("expected " + expected); - } - return json as Unknown as T; - } - -- return {toJson, fromJson}; -+ return { toJson, fromJson }; - } - --function bytesJsonBinding() : JsonBinding0 { -- function toJson(v : Uint8Array) : Json { -- return b64.fromByteArray(v); -+function bytesJsonBinding(): JsonBinding0 { -+ function toJson(v: Uint8Array): Json { -+ //return b64.fromByteArray(v); -+ throw new Error("bytesJsonBinding not implemented"); - } - -- function fromJson(json : Json) : Uint8Array { -- if (typeof(json) != 'string') { -- throw jsonParseException('expected a string'); -+ function fromJson(json: Json): Uint8Array { -+ if (typeof (json) != "string") { -+ throw jsonParseException("expected a string"); - } -- return b64.toByteArray(json); -+ //return b64.toByteArray(json); -+ throw new Error("bytesJsonBinding not implemented"); - } - -- return {toJson, fromJson}; -+ return { toJson, fromJson }; - } - --function vectorJsonBinding(dresolver : DeclResolver, texpr : AST.TypeExpr, boundTypeParams : BoundTypeParams) : JsonBinding0 { -- const elementBinding = once(() => buildJsonBinding(dresolver, texpr, boundTypeParams)); -+function vectorJsonBinding( -+ dresolver: DeclResolver, -+ texpr: AST.TypeExpr, -+ boundTypeParams: BoundTypeParams, -+): JsonBinding0 { -+ const elementBinding = once(() => -+ buildJsonBinding(dresolver, texpr, boundTypeParams) -+ ); - -- function toJson(v : Unknown[]) : Json { -+ function toJson(v: Unknown[]): Json { - return v.map(elementBinding().toJson); - } - -- function fromJson(json : Json) : Unknown[] { -- const jarr = asJsonArray(json); -- if (jarr == undefined) { -- throw jsonParseException('expected an array'); -- } -- let result : Unknown[] = []; -- jarr.forEach( (eljson:Json,i:number) => { -- try { -- result.push(elementBinding().fromJson(eljson)); -- } catch(e) { -- if (isJsonParseException(e)) { -- e.pushIndex(i); -- } -- throw e; -+ function fromJson(json: Json): Unknown[] { -+ const jarr = asJsonArray(json); -+ if (jarr == undefined) { -+ throw jsonParseException("expected an array"); -+ } -+ let result: Unknown[] = []; -+ jarr.forEach((eljson: Json, i: number) => { -+ try { -+ result.push(elementBinding().fromJson(eljson)); -+ } catch (e) { -+ if (isJsonParseException(e)) { -+ e.pushIndex(i); - } -- }); -+ throw e; -+ } -+ }); - return result; - } - -- return {toJson, fromJson}; -+ return { toJson, fromJson }; - } - --type StringMap = {[key:string]: T}; -+type StringMap = { [key: string]: T }; - --function stringMapJsonBinding(dresolver : DeclResolver, texpr : AST.TypeExpr, boundTypeParams : BoundTypeParams) : JsonBinding0> { -- const elementBinding = once(() => buildJsonBinding(dresolver, texpr, boundTypeParams)); -+function stringMapJsonBinding( -+ dresolver: DeclResolver, -+ texpr: AST.TypeExpr, -+ boundTypeParams: BoundTypeParams, -+): JsonBinding0> { -+ const elementBinding = once(() => -+ buildJsonBinding(dresolver, texpr, boundTypeParams) -+ ); - -- function toJson(v : StringMap) : Json { -+ function toJson(v: StringMap): Json { - const result: JsonObject = {}; - for (let k in v) { - result[k] = elementBinding().toJson(v[k]); -@@ -248,16 +348,16 @@ function stringMapJsonBinding(dresolver : DeclResolver, texpr : AST.TypeExpr, bo - return result; - } - -- function fromJson(json : Json) : StringMap { -+ function fromJson(json: Json): StringMap { - const jobj = asJsonObject(json); - if (!jobj) { -- throw jsonParseException('expected an object'); -+ throw jsonParseException("expected an object"); - } -- let result: JsonObject = {}; -+ let result: JsonObject = {}; - for (let k in jobj) { - try { - result[k] = elementBinding().fromJson(jobj[k]); -- } catch(e) { -+ } catch (e) { - if (isJsonParseException(e)) { - e.pushField(k); - } -@@ -266,60 +366,86 @@ function stringMapJsonBinding(dresolver : DeclResolver, texpr : AST.TypeExpr, bo - return result; - } - -- return {toJson, fromJson}; -+ return { toJson, fromJson }; - } - --function nullableJsonBinding(dresolver : DeclResolver, texpr : AST.TypeExpr, boundTypeParams : BoundTypeParams) : JsonBinding0 { -- const elementBinding = once(() => buildJsonBinding(dresolver, texpr, boundTypeParams)); -+function nullableJsonBinding( -+ dresolver: DeclResolver, -+ texpr: AST.TypeExpr, -+ boundTypeParams: BoundTypeParams, -+): JsonBinding0 { -+ const elementBinding = once(() => -+ buildJsonBinding(dresolver, texpr, boundTypeParams) -+ ); - -- function toJson(v : Unknown) : Json { -+ function toJson(v: Unknown): Json { - if (v === null) { - return null; - } - return elementBinding().toJson(v); - } - -- function fromJson(json : Json) : Unknown { -+ function fromJson(json: Json): Unknown { - if (json === null) { - return null; - } - return elementBinding().fromJson(json); - } - -- return {toJson,fromJson}; -+ return { toJson, fromJson }; - } - - interface StructFieldDetails { -- field : AST.Field, -- jsonBinding : () => JsonBinding0, -- buildDefault : () => { value : Unknown } | null --}; -- --function structJsonBinding(dresolver : DeclResolver, struct : AST.Struct, params : AST.TypeExpr[], boundTypeParams : BoundTypeParams ) : JsonBinding0 { -- const newBoundTypeParams = createBoundTypeParams(dresolver, struct.typeParams, params, boundTypeParams); -- const fieldDetails : StructFieldDetails[] = []; -- struct.fields.forEach( (field) => { -- let buildDefault = once( () => { -- if (field.default.kind === "just") { -+ field: AST.Field; -+ jsonBinding: () => JsonBinding0; -+ buildDefault: () => { value: Unknown } | null; -+} -+ -+function structJsonBinding( -+ dresolver: DeclResolver, -+ struct: AST.Struct, -+ params: AST.TypeExpr[], -+ boundTypeParams: BoundTypeParams, -+): JsonBinding0 { -+ const newBoundTypeParams = createBoundTypeParams( -+ dresolver, -+ struct.typeParams, -+ params, -+ boundTypeParams, -+ ); -+ const fieldDetails: StructFieldDetails[] = []; -+ struct.fields.forEach((field) => { -+ let buildDefault = once(() => { -+ if (field.default.kind === "just") { - const json = field.default.value; -- return { 'value' : buildJsonBinding(dresolver, field.typeExpr, newBoundTypeParams).fromJson(json)}; -+ return { -+ "value": buildJsonBinding( -+ dresolver, -+ field.typeExpr, -+ newBoundTypeParams, -+ ).fromJson(json), -+ }; - } else { - return null; - } - }); - -- fieldDetails.push( { -- field : field, -- jsonBinding : once(() => buildJsonBinding(dresolver, field.typeExpr, newBoundTypeParams)), -- buildDefault : buildDefault, -+ fieldDetails.push({ -+ field: field, -+ jsonBinding: once(() => -+ buildJsonBinding(dresolver, field.typeExpr, newBoundTypeParams) -+ ), -+ buildDefault: buildDefault, - }); - }); - -- function toJson(v0: Unknown) : Json { -- const v = v0 as {[key:string]:Unknown}; -+ function toJson(v0: Unknown): Json { -+ const v = v0 as { [key: string]: Unknown }; - const json: JsonObject = {}; -- fieldDetails.forEach( (fd) => { -- json[fd.field.serializedName] = fd.jsonBinding().toJson(v && v[fd.field.name]); -+ fieldDetails.forEach((fd) => { -+ json[fd.field.serializedName] = fd.jsonBinding().toJson( -+ v && v[fd.field.name], -+ ); - }); - return json; - } -@@ -330,19 +456,23 @@ function structJsonBinding(dresolver : DeclResolver, struct : AST.Struct, params - throw jsonParseException("expected an object"); - } - -- const v : {[member:string]: Unknown} = {}; -- fieldDetails.forEach( (fd) => { -+ const v: { [member: string]: Unknown } = {}; -+ fieldDetails.forEach((fd) => { - if (jobj[fd.field.serializedName] === undefined) { - const defaultv = fd.buildDefault(); -- if (defaultv === null) { -- throw jsonParseException("missing struct field " + fd.field.serializedName ); -+ if (defaultv === null) { -+ throw jsonParseException( -+ "missing struct field " + fd.field.serializedName, -+ ); - } else { - v[fd.field.name] = defaultv.value; - } - } else { - try { -- v[fd.field.name] = fd.jsonBinding().fromJson(jobj[fd.field.serializedName]); -- } catch(e) { -+ v[fd.field.name] = fd.jsonBinding().fromJson( -+ jobj[fd.field.serializedName], -+ ); -+ } catch (e) { - if (isJsonParseException(e)) { - e.pushField(fd.field.serializedName); - } -@@ -353,23 +483,28 @@ function structJsonBinding(dresolver : DeclResolver, struct : AST.Struct, params - return v; - } - -- return {toJson, fromJson}; -+ return { toJson, fromJson }; - } - --function enumJsonBinding(_dresolver : DeclResolver, union : AST.Union, _params : AST.TypeExpr[], _boundTypeParams : BoundTypeParams ) : JsonBinding0 { -- const fieldSerializedNames : string[] = []; -- const fieldNumbers : {[key:string]:number} = {}; -- union.fields.forEach( (field,i) => { -+function enumJsonBinding( -+ _dresolver: DeclResolver, -+ union: AST.Union, -+ _params: AST.TypeExpr[], -+ _boundTypeParams: BoundTypeParams, -+): JsonBinding0 { -+ const fieldSerializedNames: string[] = []; -+ const fieldNumbers: { [key: string]: number } = {}; -+ union.fields.forEach((field, i) => { - fieldSerializedNames.push(field.serializedName); - fieldNumbers[field.serializedName] = i; - }); - -- function toJson(v :Unknown) : Json { -+ function toJson(v: Unknown): Json { - return fieldSerializedNames[v as number]; - } - -- function fromJson(json : Json) : Unknown { -- if (typeof(json) !== 'string') { -+ function fromJson(json: Json): Unknown { -+ if (typeof (json) !== "string") { - throw jsonParseException("expected a string for enum"); - } - const result = fieldNumbers[json as string]; -@@ -379,44 +514,56 @@ function enumJsonBinding(_dresolver : DeclResolver, union : AST.Union, _params : - return result; - } - -- return {toJson, fromJson}; -+ return { toJson, fromJson }; - } - - interface FieldDetails { -- field : AST.Field; -- isVoid : boolean; -- jsonBinding : () => JsonBinding0; --}; -- --function unionJsonBinding(dresolver : DeclResolver, union : AST.Union, params : AST.TypeExpr[], boundTypeParams : BoundTypeParams ) : JsonBinding0 { -- -+ field: AST.Field; -+ isVoid: boolean; -+ jsonBinding: () => JsonBinding0; -+} - -- const newBoundTypeParams = createBoundTypeParams(dresolver, union.typeParams, params, boundTypeParams); -- const detailsByName : {[key: string]: FieldDetails} = {}; -- const detailsBySerializedName : {[key: string]: FieldDetails} = {}; -- union.fields.forEach( (field) => { -+function unionJsonBinding( -+ dresolver: DeclResolver, -+ union: AST.Union, -+ params: AST.TypeExpr[], -+ boundTypeParams: BoundTypeParams, -+): JsonBinding0 { -+ const newBoundTypeParams = createBoundTypeParams( -+ dresolver, -+ union.typeParams, -+ params, -+ boundTypeParams, -+ ); -+ const detailsByName: { [key: string]: FieldDetails } = {}; -+ const detailsBySerializedName: { [key: string]: FieldDetails } = {}; -+ union.fields.forEach((field) => { - const details = { -- field : field, -- isVoid : isVoid(field.typeExpr), -- jsonBinding : once(() => buildJsonBinding(dresolver, field.typeExpr, newBoundTypeParams)) -+ field: field, -+ isVoid: isVoid(field.typeExpr), -+ jsonBinding: once(() => -+ buildJsonBinding(dresolver, field.typeExpr, newBoundTypeParams) -+ ), - }; - detailsByName[field.name] = details; - detailsBySerializedName[field.serializedName] = details; - }); - -- function toJson(v0 : Unknown) : Json { -- const v = v0 as {kind:string, value:Unknown}; -+ function toJson(v0: Unknown): Json { -+ const v = v0 as { kind: string; value: Unknown }; - const details = detailsByName[v.kind]; - if (details.isVoid) { - return details.field.serializedName; - } else { - const result: JsonObject = {}; -- result[details.field.serializedName] = details.jsonBinding().toJson(v.value); -+ result[details.field.serializedName] = details.jsonBinding().toJson( -+ v.value, -+ ); - return result; - } - } - -- function lookupDetails(serializedName : string) { -+ function lookupDetails(serializedName: string) { - let details = detailsBySerializedName[serializedName]; - if (details === undefined) { - throw jsonParseException("invalid union field " + serializedName); -@@ -424,13 +571,15 @@ function unionJsonBinding(dresolver : DeclResolver, union : AST.Union, params : - return details; - } - -- function fromJson(json : Json) : Unknown { -- if (typeof(json) === "string") { -+ function fromJson(json: Json): Unknown { -+ if (typeof (json) === "string") { - let details = lookupDetails(json); - if (!details.isVoid) { -- throw jsonParseException("union field " + json + "needs an associated value"); -+ throw jsonParseException( -+ "union field " + json + "needs an associated value", -+ ); - } -- return { kind : details.field.name }; -+ return { kind: details.field.name }; - } - const jobj = asJsonObject(json); - if (jobj) { -@@ -438,10 +587,10 @@ function unionJsonBinding(dresolver : DeclResolver, union : AST.Union, params : - let details = lookupDetails(k); - try { - return { -- kind : details.field.name, -- value : details.jsonBinding().fromJson(jobj[k]) -- } -- } catch(e) { -+ kind: details.field.name, -+ value: details.jsonBinding().fromJson(jobj[k]), -+ }; -+ } catch (e) { - if (isJsonParseException(e)) { - e.pushField(k); - } -@@ -454,24 +603,52 @@ function unionJsonBinding(dresolver : DeclResolver, union : AST.Union, params : - } - } - -- return {toJson, fromJson}; -+ return { toJson, fromJson }; - } - --function newtypeJsonBinding(dresolver : DeclResolver, newtype : AST.NewType, params : AST.TypeExpr[], boundTypeParams : BoundTypeParams ) : JsonBinding0 { -- const newBoundTypeParams = createBoundTypeParams(dresolver, newtype.typeParams, params, boundTypeParams); -+function newtypeJsonBinding( -+ dresolver: DeclResolver, -+ newtype: AST.NewType, -+ params: AST.TypeExpr[], -+ boundTypeParams: BoundTypeParams, -+): JsonBinding0 { -+ const newBoundTypeParams = createBoundTypeParams( -+ dresolver, -+ newtype.typeParams, -+ params, -+ boundTypeParams, -+ ); - return buildJsonBinding(dresolver, newtype.typeExpr, newBoundTypeParams); - } - --function typedefJsonBinding(dresolver : DeclResolver, typedef : AST.TypeDef, params : AST.TypeExpr[], boundTypeParams : BoundTypeParams ) : JsonBinding0 { -- const newBoundTypeParams = createBoundTypeParams(dresolver, typedef.typeParams, params, boundTypeParams); -+function typedefJsonBinding( -+ dresolver: DeclResolver, -+ typedef: AST.TypeDef, -+ params: AST.TypeExpr[], -+ boundTypeParams: BoundTypeParams, -+): JsonBinding0 { -+ const newBoundTypeParams = createBoundTypeParams( -+ dresolver, -+ typedef.typeParams, -+ params, -+ boundTypeParams, -+ ); - return buildJsonBinding(dresolver, typedef.typeExpr, newBoundTypeParams); - } - --function createBoundTypeParams(dresolver : DeclResolver, paramNames : string[], paramTypes : AST.TypeExpr[], boundTypeParams : BoundTypeParams) : BoundTypeParams --{ -- let result : BoundTypeParams = {}; -- paramNames.forEach( (paramName,i) => { -- result[paramName] = buildJsonBinding(dresolver,paramTypes[i], boundTypeParams); -+function createBoundTypeParams( -+ dresolver: DeclResolver, -+ paramNames: string[], -+ paramTypes: AST.TypeExpr[], -+ boundTypeParams: BoundTypeParams, -+): BoundTypeParams { -+ let result: BoundTypeParams = {}; -+ paramNames.forEach((paramName, i) => { -+ result[paramName] = buildJsonBinding( -+ dresolver, -+ paramTypes[i], -+ boundTypeParams, -+ ); - }); - return result; - } -@@ -480,10 +657,10 @@ function createBoundTypeParams(dresolver : DeclResolver, paramNames : string[], - * Helper function that takes a thunk, and evaluates it only on the first call. Subsequent - * calls return the previous value - */ --function once(run : () => T) : () => T { -- let result : T | null = null; -+function once(run: () => T): () => T { -+ let result: T | null = null; - return () => { -- if(result === null) { -+ if (result === null) { - result = run(); - } - return result; -@@ -493,12 +670,15 @@ function once(run : () => T) : () => T { - /** - * Get the value of an annotation of type T - */ --export function getAnnotation(jb: JsonBinding, annotations: AST.Annotations): T | undefined { -- if (jb.typeExpr.typeRef.kind != 'reference') { -+export function getAnnotation( -+ jb: JsonBinding, -+ annotations: AST.Annotations, -+): T | undefined { -+ if (jb.typeExpr.typeRef.kind != "reference") { - return undefined; - } -- const annScopedName :AST.ScopedName = jb.typeExpr.typeRef.value; -- const ann = annotations.find(el => scopedNamesEqual(el.v1, annScopedName)); -+ const annScopedName: AST.ScopedName = jb.typeExpr.typeRef.value; -+ const ann = annotations.find((el) => scopedNamesEqual(el.v1, annScopedName)); - if (ann === undefined) { - return undefined; - } -diff --git a/adl-gen/runtime/sys/adlast.ts b/adl-gen/runtime/sys/adlast.ts -index 2e6aac5..31d07ef 100644 ---- a/adl-gen/runtime/sys/adlast.ts -+++ b/adl-gen/runtime/sys/adlast.ts -@@ -1,12 +1,13 @@ --/* @generated from adl module sys.adlast */ -+// deno-lint-ignore-file - --import * as sys_types from "./types.ts"; -+/* @generated from adl module sys.adlast */ -+import type * as sys_types from "./types.ts"; - - export type ModuleName = string; - - export type Ident = string; - --export type Annotations = sys_types.Map; -+export type Annotations = sys_types.Map; - - export interface ScopedName { - moduleName: ModuleName; -@@ -15,9 +16,9 @@ export interface ScopedName { - - export function makeScopedName( - input: { -- moduleName: ModuleName, -- name: Ident, -- } -+ moduleName: ModuleName; -+ name: Ident; -+ }, - ): ScopedName { - return { - moduleName: input.moduleName, -@@ -26,15 +27,15 @@ export function makeScopedName( - } - - export interface TypeRef_Primitive { -- kind: 'primitive'; -+ kind: "primitive"; - value: Ident; - } - export interface TypeRef_TypeParam { -- kind: 'typeParam'; -+ kind: "typeParam"; - value: Ident; - } - export interface TypeRef_Reference { -- kind: 'reference'; -+ kind: "reference"; - value: ScopedName; - } - -@@ -46,7 +47,12 @@ export interface TypeRefOpts { - reference: ScopedName; - } - --export function makeTypeRef(kind: K, value: TypeRefOpts[K]) { return {kind, value}; } -+export function makeTypeRef( -+ kind: K, -+ value: TypeRefOpts[K], -+) { -+ return { kind, value }; -+} - - export interface TypeExpr { - typeRef: TypeRef; -@@ -55,9 +61,9 @@ export interface TypeExpr { - - export function makeTypeExpr( - input: { -- typeRef: TypeRef, -- parameters: TypeExpr[], -- } -+ typeRef: TypeRef; -+ parameters: TypeExpr[]; -+ }, - ): TypeExpr { - return { - typeRef: input.typeRef, -@@ -69,18 +75,18 @@ export interface Field { - name: Ident; - serializedName: Ident; - typeExpr: TypeExpr; -- default: sys_types.Maybe<{}|null>; -+ default: sys_types.Maybe<{} | null>; - annotations: Annotations; - } - - export function makeField( - input: { -- name: Ident, -- serializedName: Ident, -- typeExpr: TypeExpr, -- default: sys_types.Maybe<{}|null>, -- annotations: Annotations, -- } -+ name: Ident; -+ serializedName: Ident; -+ typeExpr: TypeExpr; -+ default: sys_types.Maybe<{} | null>; -+ annotations: Annotations; -+ }, - ): Field { - return { - name: input.name, -@@ -98,9 +104,9 @@ export interface Struct { - - export function makeStruct( - input: { -- typeParams: Ident[], -- fields: Field[], -- } -+ typeParams: Ident[]; -+ fields: Field[]; -+ }, - ): Struct { - return { - typeParams: input.typeParams, -@@ -115,9 +121,9 @@ export interface Union { - - export function makeUnion( - input: { -- typeParams: Ident[], -- fields: Field[], -- } -+ typeParams: Ident[]; -+ fields: Field[]; -+ }, - ): Union { - return { - typeParams: input.typeParams, -@@ -132,9 +138,9 @@ export interface TypeDef { - - export function makeTypeDef( - input: { -- typeParams: Ident[], -- typeExpr: TypeExpr, -- } -+ typeParams: Ident[]; -+ typeExpr: TypeExpr; -+ }, - ): TypeDef { - return { - typeParams: input.typeParams, -@@ -145,15 +151,15 @@ export function makeTypeDef( - export interface NewType { - typeParams: Ident[]; - typeExpr: TypeExpr; -- default: sys_types.Maybe<{}|null>; -+ default: sys_types.Maybe<{} | null>; - } - - export function makeNewType( - input: { -- typeParams: Ident[], -- typeExpr: TypeExpr, -- default: sys_types.Maybe<{}|null>, -- } -+ typeParams: Ident[]; -+ typeExpr: TypeExpr; -+ default: sys_types.Maybe<{} | null>; -+ }, - ): NewType { - return { - typeParams: input.typeParams, -@@ -163,23 +169,27 @@ export function makeNewType( - } - - export interface DeclType_Struct_ { -- kind: 'struct_'; -+ kind: "struct_"; - value: Struct; - } - export interface DeclType_Union_ { -- kind: 'union_'; -+ kind: "union_"; - value: Union; - } - export interface DeclType_Type_ { -- kind: 'type_'; -+ kind: "type_"; - value: TypeDef; - } - export interface DeclType_Newtype_ { -- kind: 'newtype_'; -+ kind: "newtype_"; - value: NewType; - } - --export type DeclType = DeclType_Struct_ | DeclType_Union_ | DeclType_Type_ | DeclType_Newtype_; -+export type DeclType = -+ | DeclType_Struct_ -+ | DeclType_Union_ -+ | DeclType_Type_ -+ | DeclType_Newtype_; - - export interface DeclTypeOpts { - struct_: Struct; -@@ -188,7 +198,12 @@ export interface DeclTypeOpts { - newtype_: NewType; - } - --export function makeDeclType(kind: K, value: DeclTypeOpts[K]) { return {kind, value}; } -+export function makeDeclType( -+ kind: K, -+ value: DeclTypeOpts[K], -+) { -+ return { kind, value }; -+} - - export interface Decl { - name: Ident; -@@ -199,11 +214,11 @@ export interface Decl { - - export function makeDecl( - input: { -- name: Ident, -- version: sys_types.Maybe, -- type_: DeclType, -- annotations: Annotations, -- } -+ name: Ident; -+ version: sys_types.Maybe; -+ type_: DeclType; -+ annotations: Annotations; -+ }, - ): Decl { - return { - name: input.name, -@@ -220,9 +235,9 @@ export interface ScopedDecl { - - export function makeScopedDecl( - input: { -- moduleName: ModuleName, -- decl: Decl, -- } -+ moduleName: ModuleName; -+ decl: Decl; -+ }, - ): ScopedDecl { - return { - moduleName: input.moduleName, -@@ -233,11 +248,11 @@ export function makeScopedDecl( - export type DeclVersions = Decl[]; - - export interface Import_ModuleName { -- kind: 'moduleName'; -+ kind: "moduleName"; - value: ModuleName; - } - export interface Import_ScopedName { -- kind: 'scopedName'; -+ kind: "scopedName"; - value: ScopedName; - } - -@@ -248,22 +263,27 @@ export interface ImportOpts { - scopedName: ScopedName; - } - --export function makeImport(kind: K, value: ImportOpts[K]) { return {kind, value}; } -+export function makeImport( -+ kind: K, -+ value: ImportOpts[K], -+) { -+ return { kind, value }; -+} - - export interface Module { - name: ModuleName; - imports: Import[]; -- decls: {[key: string]: Decl}; -+ decls: { [key: string]: Decl }; - annotations: Annotations; - } - - export function makeModule( - input: { -- name: ModuleName, -- imports: Import[], -- decls: {[key: string]: Decl}, -- annotations: Annotations, -- } -+ name: ModuleName; -+ imports: Import[]; -+ decls: { [key: string]: Decl }; -+ annotations: Annotations; -+ }, - ): Module { - return { - name: input.name, -diff --git a/adl-gen/runtime/sys/dynamic.ts b/adl-gen/runtime/sys/dynamic.ts -index 0047acc..070571b 100644 ---- a/adl-gen/runtime/sys/dynamic.ts -+++ b/adl-gen/runtime/sys/dynamic.ts -@@ -1,5 +1,5 @@ - /* @generated from adl module sys.dynamic */ -- -+//deno-lint-ignore-file - import * as sys_adlast from "./adlast.ts"; - - /** -@@ -7,14 +7,14 @@ import * as sys_adlast from "./adlast.ts"; - */ - export interface Dynamic { - typeExpr: sys_adlast.TypeExpr; -- value: {}|null; -+ value: {} | null; - } - - export function makeDynamic( - input: { -- typeExpr: sys_adlast.TypeExpr, -- value: {}|null, -- } -+ typeExpr: sys_adlast.TypeExpr; -+ value: {} | null; -+ }, - ): Dynamic { - return { - typeExpr: input.typeExpr, -diff --git a/adl-gen/runtime/sys/types.ts b/adl-gen/runtime/sys/types.ts -index 42b5599..d8cfa44 100644 ---- a/adl-gen/runtime/sys/types.ts -+++ b/adl-gen/runtime/sys/types.ts -@@ -1,6 +1,6 @@ --/* @generated from adl module sys.types */ -- -+// deno-lint-ignore-file - -+/* @generated from adl module sys.types */ - export interface Pair { - v1: T1; - v2: T2; -@@ -8,9 +8,9 @@ export interface Pair { - - export function makePair( - input: { -- v1: T1, -- v2: T2, -- } -+ v1: T1; -+ v2: T2; -+ }, - ): Pair { - return { - v1: input.v1, -@@ -19,11 +19,11 @@ export function makePair( - } - - export interface Either_Left { -- kind: 'left'; -+ kind: "left"; - value: T1; - } - export interface Either_Right<_T1, T2> { -- kind: 'right'; -+ kind: "right"; - value: T2; - } - -@@ -34,13 +34,18 @@ export interface EitherOpts { - right: T2; - } - --export function makeEither>(kind: K, value: EitherOpts[K]) { return {kind, value}; } -+export function makeEither>( -+ kind: K, -+ value: EitherOpts[K], -+) { -+ return { kind, value }; -+} - - export interface Maybe_Nothing<_T> { -- kind: 'nothing'; -+ kind: "nothing"; - } - export interface Maybe_Just { -- kind: 'just'; -+ kind: "just"; - value: T; - } - -@@ -51,14 +56,19 @@ export interface MaybeOpts { - just: T; - } - --export function makeMaybe>(kind: K, value: MaybeOpts[K]) { return {kind, value}; } -+export function makeMaybe>( -+ kind: K, -+ value: MaybeOpts[K], -+) { -+ return { kind, value }; -+} - - export interface Error_Value { -- kind: 'value'; -+ kind: "value"; - value: T; - } - export interface Error_Error<_T> { -- kind: 'error'; -+ kind: "error"; - value: string; - } - -@@ -69,7 +79,12 @@ export interface ErrorOpts { - error: string; - } - --export function makeError>(kind: K, value: ErrorOpts[K]) { return {kind, value}; } -+export function makeError>( -+ kind: K, -+ value: ErrorOpts[K], -+) { -+ return { kind, value }; -+} - - export interface MapEntry { - key: K; -@@ -78,9 +93,9 @@ export interface MapEntry { - - export function makeMapEntry( - input: { -- key: K, -- value: V, -- } -+ key: K; -+ value: V; -+ }, - ): MapEntry { - return { - key: input.key, -diff --git a/adl-gen/runtime/utils.ts b/adl-gen/runtime/utils.ts -index 6d2eacb..e61e70b 100644 ---- a/adl-gen/runtime/utils.ts -+++ b/adl-gen/runtime/utils.ts -@@ -1,6 +1,7 @@ --import * as AST from "./sys/adlast.ts"; -+// deno-lint-ignore-file -+import type * as AST from "./sys/adlast.ts"; - --export function isEnum(union : AST.Union) : boolean { -+export function isEnum(union: AST.Union): boolean { - for (let field of union.fields) { - if (!isVoid(field.typeExpr)) { - return false; -@@ -9,14 +10,17 @@ export function isEnum(union : AST.Union) : boolean { - return true; - } - --export function isVoid(texpr : AST.TypeExpr) : boolean { -+export function isVoid(texpr: AST.TypeExpr): boolean { - if (texpr.typeRef.kind === "primitive") { - return texpr.typeRef.value === "Void"; - } - return false; - } - --export function typeExprsEqual(texpr1 : AST.TypeExpr, texpr2 : AST.TypeExpr) : boolean { -+export function typeExprsEqual( -+ texpr1: AST.TypeExpr, -+ texpr2: AST.TypeExpr, -+): boolean { - if (!typeRefsEqual(texpr1.typeRef, texpr2.typeRef)) { - return false; - } -@@ -24,14 +28,14 @@ export function typeExprsEqual(texpr1 : AST.TypeExpr, texpr2 : AST.TypeExpr) : b - return false; - } - for (let i = 0; i < texpr1.parameters.length; i++) { -- if(!typeExprsEqual(texpr1.parameters[i], texpr2.parameters[i])) { -+ if (!typeExprsEqual(texpr1.parameters[i], texpr2.parameters[i])) { - return false; - } - } - return true; - } - --export function typeRefsEqual(tref1 : AST.TypeRef, tref2 : AST.TypeRef) : boolean { -+export function typeRefsEqual(tref1: AST.TypeRef, tref2: AST.TypeRef): boolean { - if (tref1.kind === "primitive" && tref2.kind === "primitive") { - return tref1.value === tref2.value; - } else if (tref1.kind === "typeParam" && tref2.kind === "typeParam") { -@@ -42,11 +46,17 @@ export function typeRefsEqual(tref1 : AST.TypeRef, tref2 : AST.TypeRef) : boolea - return false; - } - --export function scopedNamesEqual(sn1: AST.ScopedName, sn2: AST.ScopedName): boolean { -+export function scopedNamesEqual( -+ sn1: AST.ScopedName, -+ sn2: AST.ScopedName, -+): boolean { - return sn1.moduleName === sn2.moduleName && sn1.name === sn2.name; - } - --function typeExprToStringImpl(te: AST.TypeExpr, withScopedNames: boolean) : string { -+function typeExprToStringImpl( -+ te: AST.TypeExpr, -+ withScopedNames: boolean, -+): string { - let result = ""; - if (te.typeRef.kind == "primitive") { - result = te.typeRef.value; -@@ -58,20 +68,56 @@ function typeExprToStringImpl(te: AST.TypeExpr, withScopedNames: boolean) : stri - : te.typeRef.value.name; - } - if (te.parameters.length > 0) { -- result = result + "<" + te.parameters.map(p => typeExprToStringImpl(p, withScopedNames)) + ">"; -+ result = result + "<" + te.parameters.map((p) => -+ typeExprToStringImpl(p, withScopedNames) -+ ) + ">"; - } - return result; - } - - /* Convert a type expression to a string, with fully scoped names */ - --export function typeExprToString(te: AST.TypeExpr) : string { -+export function typeExprToString(te: AST.TypeExpr): string { - return typeExprToStringImpl(te, true); - } - - /* Convert a type expression to a string, with unscoped names */ - --export function typeExprToStringUnscoped(te: AST.TypeExpr) : string { -+export function typeExprToStringUnscoped(te: AST.TypeExpr): string { - return typeExprToStringImpl(te, false); - } - -+// "Flavoured" nominal typing. -+// https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/ -+const symS = Symbol(); -+const symT = Symbol(); -+const symU = Symbol(); -+const symV = Symbol(); -+ -+/// Zero ADL type params - literal string type Name (fully scoped module name) -+/// eg for 'newtype X = string' -> 'type X = Flavouring0<"X">;' -+type Flavoring0 = { -+ readonly [symS]?: Name; -+}; -+ -+/// 1 ADL type param -+/// eg for 'newtype X = string' -> 'type X = Flavouring1<"X",T>;' -+type Flavoring1 = Flavoring0 & { -+ readonly [symT]?: T; -+}; -+ -+/// 2 ADL type params -+/// eg for 'newtype X = string' -> 'type X = Flavouring2<"X",T,U>;' -+type Flavoring2 = Flavoring1 & { -+ readonly [symU]?: U; -+}; -+ -+/// 3 ADL type params -+/// eg for 'newtype X = string' -> 'type X = Flavouring3<"X",T,U,V>;' -+type Flavoring3 = Flavoring2 & { -+ readonly [symV]?: V; -+}; -+export type Flavored0 = A & Flavoring0; -+export type Flavored1 = A & Flavoring1; -+export type Flavored2 = A & Flavoring2; -+export type Flavored3 = A & Flavoring3; -diff --git a/adl-gen/sys/types.ts b/adl-gen/sys/types.ts -index ba23476..4b3d8bf 100644 ---- a/adl-gen/sys/types.ts -+++ b/adl-gen/sys/types.ts -@@ -1,6 +1,8 @@ -+// deno-lint-ignore-file -+ - /* @generated from adl module sys.types */ - --import * as ADL from "./../runtime/adl.ts"; -+import type * as ADL from "./../runtime/adl.ts"; - - export interface Pair { - v1: T1; -@@ -9,9 +11,9 @@ export interface Pair { - - export function makePair( - input: { -- v1: T1, -- v2: T2, -- } -+ v1: T1; -+ v2: T2; -+ }, - ): Pair { - return { - v1: input.v1, -@@ -19,21 +21,63 @@ export function makePair( - }; - } - --const Pair_AST : ADL.ScopedDecl = -- {"moduleName":"sys.types","decl":{"annotations":[],"type_":{"kind":"struct_","value":{"typeParams":["T1","T2"],"fields":[{"annotations":[],"serializedName":"v1","default":{"kind":"nothing"},"name":"v1","typeExpr":{"typeRef":{"kind":"typeParam","value":"T1"},"parameters":[]}},{"annotations":[],"serializedName":"v2","default":{"kind":"nothing"},"name":"v2","typeExpr":{"typeRef":{"kind":"typeParam","value":"T2"},"parameters":[]}}]}},"name":"Pair","version":{"kind":"nothing"}}}; -+const Pair_AST: ADL.ScopedDecl = { -+ "moduleName": "sys.types", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "struct_", -+ "value": { -+ "typeParams": ["T1", "T2"], -+ "fields": [{ -+ "annotations": [], -+ "serializedName": "v1", -+ "default": { "kind": "nothing" }, -+ "name": "v1", -+ "typeExpr": { -+ "typeRef": { "kind": "typeParam", "value": "T1" }, -+ "parameters": [], -+ }, -+ }, { -+ "annotations": [], -+ "serializedName": "v2", -+ "default": { "kind": "nothing" }, -+ "name": "v2", -+ "typeExpr": { -+ "typeRef": { "kind": "typeParam", "value": "T2" }, -+ "parameters": [], -+ }, -+ }], -+ }, -+ }, -+ "name": "Pair", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snPair: ADL.ScopedName = {moduleName:"sys.types", name:"Pair"}; -+export const snPair: ADL.ScopedName = { moduleName: "sys.types", name: "Pair" }; - --export function texprPair(texprT1 : ADL.ATypeExpr, texprT2 : ADL.ATypeExpr): ADL.ATypeExpr> { -- return {value : {typeRef : {kind: "reference", value : {moduleName : "sys.types",name : "Pair"}}, parameters : [texprT1.value, texprT2.value]}}; -+export function texprPair( -+ texprT1: ADL.ATypeExpr, -+ texprT2: ADL.ATypeExpr, -+): ADL.ATypeExpr> { -+ return { -+ value: { -+ typeRef: { -+ kind: "reference", -+ value: { moduleName: "sys.types", name: "Pair" }, -+ }, -+ parameters: [texprT1.value, texprT2.value], -+ }, -+ }; - } - - export interface Either_Left { -- kind: 'left'; -+ kind: "left"; - value: T1; - } - export interface Either_Right<_T1, T2> { -- kind: 'right'; -+ kind: "right"; - value: T2; - } - -@@ -44,22 +88,72 @@ export interface EitherOpts { - right: T2; - } - --export function makeEither>(kind: K, value: EitherOpts[K]) { return {kind, value}; } -+export function makeEither>( -+ kind: K, -+ value: EitherOpts[K], -+) { -+ return { kind, value }; -+} - --const Either_AST : ADL.ScopedDecl = -- {"moduleName":"sys.types","decl":{"annotations":[],"type_":{"kind":"union_","value":{"typeParams":["T1","T2"],"fields":[{"annotations":[],"serializedName":"left","default":{"kind":"nothing"},"name":"left","typeExpr":{"typeRef":{"kind":"typeParam","value":"T1"},"parameters":[]}},{"annotations":[],"serializedName":"right","default":{"kind":"nothing"},"name":"right","typeExpr":{"typeRef":{"kind":"typeParam","value":"T2"},"parameters":[]}}]}},"name":"Either","version":{"kind":"nothing"}}}; -+const Either_AST: ADL.ScopedDecl = { -+ "moduleName": "sys.types", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "union_", -+ "value": { -+ "typeParams": ["T1", "T2"], -+ "fields": [{ -+ "annotations": [], -+ "serializedName": "left", -+ "default": { "kind": "nothing" }, -+ "name": "left", -+ "typeExpr": { -+ "typeRef": { "kind": "typeParam", "value": "T1" }, -+ "parameters": [], -+ }, -+ }, { -+ "annotations": [], -+ "serializedName": "right", -+ "default": { "kind": "nothing" }, -+ "name": "right", -+ "typeExpr": { -+ "typeRef": { "kind": "typeParam", "value": "T2" }, -+ "parameters": [], -+ }, -+ }], -+ }, -+ }, -+ "name": "Either", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snEither: ADL.ScopedName = {moduleName:"sys.types", name:"Either"}; -+export const snEither: ADL.ScopedName = { -+ moduleName: "sys.types", -+ name: "Either", -+}; - --export function texprEither(texprT1 : ADL.ATypeExpr, texprT2 : ADL.ATypeExpr): ADL.ATypeExpr> { -- return {value : {typeRef : {kind: "reference", value : {moduleName : "sys.types",name : "Either"}}, parameters : [texprT1.value, texprT2.value]}}; -+export function texprEither( -+ texprT1: ADL.ATypeExpr, -+ texprT2: ADL.ATypeExpr, -+): ADL.ATypeExpr> { -+ return { -+ value: { -+ typeRef: { -+ kind: "reference", -+ value: { moduleName: "sys.types", name: "Either" }, -+ }, -+ parameters: [texprT1.value, texprT2.value], -+ }, -+ }; - } - - export interface Maybe_Nothing<_T> { -- kind: 'nothing'; -+ kind: "nothing"; - } - export interface Maybe_Just { -- kind: 'just'; -+ kind: "just"; - value: T; - } - -@@ -70,23 +164,72 @@ export interface MaybeOpts { - just: T; - } - --export function makeMaybe>(kind: K, value: MaybeOpts[K]) { return {kind, value}; } -+export function makeMaybe>( -+ kind: K, -+ value: MaybeOpts[K], -+) { -+ return { kind, value }; -+} - --const Maybe_AST : ADL.ScopedDecl = -- {"moduleName":"sys.types","decl":{"annotations":[],"type_":{"kind":"union_","value":{"typeParams":["T"],"fields":[{"annotations":[],"serializedName":"nothing","default":{"kind":"nothing"},"name":"nothing","typeExpr":{"typeRef":{"kind":"primitive","value":"Void"},"parameters":[]}},{"annotations":[],"serializedName":"just","default":{"kind":"nothing"},"name":"just","typeExpr":{"typeRef":{"kind":"typeParam","value":"T"},"parameters":[]}}]}},"name":"Maybe","version":{"kind":"nothing"}}}; -+const Maybe_AST: ADL.ScopedDecl = { -+ "moduleName": "sys.types", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "union_", -+ "value": { -+ "typeParams": ["T"], -+ "fields": [{ -+ "annotations": [], -+ "serializedName": "nothing", -+ "default": { "kind": "nothing" }, -+ "name": "nothing", -+ "typeExpr": { -+ "typeRef": { "kind": "primitive", "value": "Void" }, -+ "parameters": [], -+ }, -+ }, { -+ "annotations": [], -+ "serializedName": "just", -+ "default": { "kind": "nothing" }, -+ "name": "just", -+ "typeExpr": { -+ "typeRef": { "kind": "typeParam", "value": "T" }, -+ "parameters": [], -+ }, -+ }], -+ }, -+ }, -+ "name": "Maybe", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snMaybe: ADL.ScopedName = {moduleName:"sys.types", name:"Maybe"}; -+export const snMaybe: ADL.ScopedName = { -+ moduleName: "sys.types", -+ name: "Maybe", -+}; - --export function texprMaybe(texprT : ADL.ATypeExpr): ADL.ATypeExpr> { -- return {value : {typeRef : {kind: "reference", value : {moduleName : "sys.types",name : "Maybe"}}, parameters : [texprT.value]}}; -+export function texprMaybe( -+ texprT: ADL.ATypeExpr, -+): ADL.ATypeExpr> { -+ return { -+ value: { -+ typeRef: { -+ kind: "reference", -+ value: { moduleName: "sys.types", name: "Maybe" }, -+ }, -+ parameters: [texprT.value], -+ }, -+ }; - } - - export interface Error_Value { -- kind: 'value'; -+ kind: "value"; - value: T; - } - export interface Error_Error<_T> { -- kind: 'error'; -+ kind: "error"; - value: string; - } - -@@ -97,15 +240,64 @@ export interface ErrorOpts { - error: string; - } - --export function makeError>(kind: K, value: ErrorOpts[K]) { return {kind, value}; } -+export function makeError>( -+ kind: K, -+ value: ErrorOpts[K], -+) { -+ return { kind, value }; -+} - --const Error_AST : ADL.ScopedDecl = -- {"moduleName":"sys.types","decl":{"annotations":[],"type_":{"kind":"union_","value":{"typeParams":["T"],"fields":[{"annotations":[],"serializedName":"value","default":{"kind":"nothing"},"name":"value","typeExpr":{"typeRef":{"kind":"typeParam","value":"T"},"parameters":[]}},{"annotations":[],"serializedName":"error","default":{"kind":"nothing"},"name":"error","typeExpr":{"typeRef":{"kind":"primitive","value":"String"},"parameters":[]}}]}},"name":"Error","version":{"kind":"nothing"}}}; -+const Error_AST: ADL.ScopedDecl = { -+ "moduleName": "sys.types", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "union_", -+ "value": { -+ "typeParams": ["T"], -+ "fields": [{ -+ "annotations": [], -+ "serializedName": "value", -+ "default": { "kind": "nothing" }, -+ "name": "value", -+ "typeExpr": { -+ "typeRef": { "kind": "typeParam", "value": "T" }, -+ "parameters": [], -+ }, -+ }, { -+ "annotations": [], -+ "serializedName": "error", -+ "default": { "kind": "nothing" }, -+ "name": "error", -+ "typeExpr": { -+ "typeRef": { "kind": "primitive", "value": "String" }, -+ "parameters": [], -+ }, -+ }], -+ }, -+ }, -+ "name": "Error", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snError: ADL.ScopedName = {moduleName:"sys.types", name:"Error"}; -+export const snError: ADL.ScopedName = { -+ moduleName: "sys.types", -+ name: "Error", -+}; - --export function texprError(texprT : ADL.ATypeExpr): ADL.ATypeExpr> { -- return {value : {typeRef : {kind: "reference", value : {moduleName : "sys.types",name : "Error"}}, parameters : [texprT.value]}}; -+export function texprError( -+ texprT: ADL.ATypeExpr, -+): ADL.ATypeExpr> { -+ return { -+ value: { -+ typeRef: { -+ kind: "reference", -+ value: { moduleName: "sys.types", name: "Error" }, -+ }, -+ parameters: [texprT.value], -+ }, -+ }; - } - - export interface MapEntry { -@@ -115,9 +307,9 @@ export interface MapEntry { - - export function makeMapEntry( - input: { -- key: K, -- value: V, -- } -+ key: K; -+ value: V; -+ }, - ): MapEntry { - return { - key: input.key, -@@ -125,43 +317,156 @@ export function makeMapEntry( - }; - } - --const MapEntry_AST : ADL.ScopedDecl = -- {"moduleName":"sys.types","decl":{"annotations":[],"type_":{"kind":"struct_","value":{"typeParams":["K","V"],"fields":[{"annotations":[],"serializedName":"k","default":{"kind":"nothing"},"name":"key","typeExpr":{"typeRef":{"kind":"typeParam","value":"K"},"parameters":[]}},{"annotations":[],"serializedName":"v","default":{"kind":"nothing"},"name":"value","typeExpr":{"typeRef":{"kind":"typeParam","value":"V"},"parameters":[]}}]}},"name":"MapEntry","version":{"kind":"nothing"}}}; -+const MapEntry_AST: ADL.ScopedDecl = { -+ "moduleName": "sys.types", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "struct_", -+ "value": { -+ "typeParams": ["K", "V"], -+ "fields": [{ -+ "annotations": [], -+ "serializedName": "k", -+ "default": { "kind": "nothing" }, -+ "name": "key", -+ "typeExpr": { -+ "typeRef": { "kind": "typeParam", "value": "K" }, -+ "parameters": [], -+ }, -+ }, { -+ "annotations": [], -+ "serializedName": "v", -+ "default": { "kind": "nothing" }, -+ "name": "value", -+ "typeExpr": { -+ "typeRef": { "kind": "typeParam", "value": "V" }, -+ "parameters": [], -+ }, -+ }], -+ }, -+ }, -+ "name": "MapEntry", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snMapEntry: ADL.ScopedName = {moduleName:"sys.types", name:"MapEntry"}; -+export const snMapEntry: ADL.ScopedName = { -+ moduleName: "sys.types", -+ name: "MapEntry", -+}; - --export function texprMapEntry(texprK : ADL.ATypeExpr, texprV : ADL.ATypeExpr): ADL.ATypeExpr> { -- return {value : {typeRef : {kind: "reference", value : {moduleName : "sys.types",name : "MapEntry"}}, parameters : [texprK.value, texprV.value]}}; -+export function texprMapEntry( -+ texprK: ADL.ATypeExpr, -+ texprV: ADL.ATypeExpr, -+): ADL.ATypeExpr> { -+ return { -+ value: { -+ typeRef: { -+ kind: "reference", -+ value: { moduleName: "sys.types", name: "MapEntry" }, -+ }, -+ parameters: [texprK.value, texprV.value], -+ }, -+ }; - } - - export type Map = Pair[]; - --const Map_AST : ADL.ScopedDecl = -- {"moduleName":"sys.types","decl":{"annotations":[],"type_":{"kind":"newtype_","value":{"typeParams":["K","V"],"default":{"kind":"nothing"},"typeExpr":{"typeRef":{"kind":"primitive","value":"Vector"},"parameters":[{"typeRef":{"kind":"reference","value":{"moduleName":"sys.types","name":"Pair"}},"parameters":[{"typeRef":{"kind":"typeParam","value":"K"},"parameters":[]},{"typeRef":{"kind":"typeParam","value":"V"},"parameters":[]}]}]}}},"name":"Map","version":{"kind":"nothing"}}}; -+const Map_AST: ADL.ScopedDecl = { -+ "moduleName": "sys.types", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "newtype_", -+ "value": { -+ "typeParams": ["K", "V"], -+ "default": { "kind": "nothing" }, -+ "typeExpr": { -+ "typeRef": { "kind": "primitive", "value": "Vector" }, -+ "parameters": [{ -+ "typeRef": { -+ "kind": "reference", -+ "value": { "moduleName": "sys.types", "name": "Pair" }, -+ }, -+ "parameters": [{ -+ "typeRef": { "kind": "typeParam", "value": "K" }, -+ "parameters": [], -+ }, { -+ "typeRef": { "kind": "typeParam", "value": "V" }, -+ "parameters": [], -+ }], -+ }], -+ }, -+ }, -+ }, -+ "name": "Map", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snMap: ADL.ScopedName = {moduleName:"sys.types", name:"Map"}; -+export const snMap: ADL.ScopedName = { moduleName: "sys.types", name: "Map" }; - --export function texprMap(texprK : ADL.ATypeExpr, texprV : ADL.ATypeExpr): ADL.ATypeExpr> { -- return {value : {typeRef : {kind: "reference", value : {moduleName : "sys.types",name : "Map"}}, parameters : [texprK.value, texprV.value]}}; -+export function texprMap( -+ texprK: ADL.ATypeExpr, -+ texprV: ADL.ATypeExpr, -+): ADL.ATypeExpr> { -+ return { -+ value: { -+ typeRef: { -+ kind: "reference", -+ value: { moduleName: "sys.types", name: "Map" }, -+ }, -+ parameters: [texprK.value, texprV.value], -+ }, -+ }; - } - - export type Set = T[]; - --const Set_AST : ADL.ScopedDecl = -- {"moduleName":"sys.types","decl":{"annotations":[],"type_":{"kind":"newtype_","value":{"typeParams":["T"],"default":{"kind":"nothing"},"typeExpr":{"typeRef":{"kind":"primitive","value":"Vector"},"parameters":[{"typeRef":{"kind":"typeParam","value":"T"},"parameters":[]}]}}},"name":"Set","version":{"kind":"nothing"}}}; -+const Set_AST: ADL.ScopedDecl = { -+ "moduleName": "sys.types", -+ "decl": { -+ "annotations": [], -+ "type_": { -+ "kind": "newtype_", -+ "value": { -+ "typeParams": ["T"], -+ "default": { "kind": "nothing" }, -+ "typeExpr": { -+ "typeRef": { "kind": "primitive", "value": "Vector" }, -+ "parameters": [{ -+ "typeRef": { "kind": "typeParam", "value": "T" }, -+ "parameters": [], -+ }], -+ }, -+ }, -+ }, -+ "name": "Set", -+ "version": { "kind": "nothing" }, -+ }, -+}; - --export const snSet: ADL.ScopedName = {moduleName:"sys.types", name:"Set"}; -+export const snSet: ADL.ScopedName = { moduleName: "sys.types", name: "Set" }; - --export function texprSet(texprT : ADL.ATypeExpr): ADL.ATypeExpr> { -- return {value : {typeRef : {kind: "reference", value : {moduleName : "sys.types",name : "Set"}}, parameters : [texprT.value]}}; -+export function texprSet(texprT: ADL.ATypeExpr): ADL.ATypeExpr> { -+ return { -+ value: { -+ typeRef: { -+ kind: "reference", -+ value: { moduleName: "sys.types", name: "Set" }, -+ }, -+ parameters: [texprT.value], -+ }, -+ }; - } - - export const _AST_MAP: { [key: string]: ADL.ScopedDecl } = { -- "sys.types.Pair" : Pair_AST, -- "sys.types.Either" : Either_AST, -- "sys.types.Maybe" : Maybe_AST, -- "sys.types.Error" : Error_AST, -- "sys.types.MapEntry" : MapEntry_AST, -- "sys.types.Map" : Map_AST, -- "sys.types.Set" : Set_AST -+ "sys.types.Pair": Pair_AST, -+ "sys.types.Either": Either_AST, -+ "sys.types.Maybe": Maybe_AST, -+ "sys.types.Error": Error_AST, -+ "sys.types.MapEntry": MapEntry_AST, -+ "sys.types.Map": Map_AST, -+ "sys.types.Set": Set_AST, - }; --- -2.20.1 - diff --git a/tools/adlc b/tools/adlc deleted file mode 100755 index d889b32..0000000 --- a/tools/adlc +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -# -# script that downloads and caches the adl compiler if necessary, and then -# runs it. - -set -e - -adlversion=0.13.4 - -if [ "$(uname)" == "Darwin" ]; then - platform=osx - cachedir=$HOME/Library/Caches/adl -elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then - platform=linux - cachedir=$HOME/.cache/adl -else - echo "Unable to download ADL for platform" - exit 1 -fi - -downloads=$cachedir/downloads -release=https://github.com/timbod7/adl/releases/download/v$adlversion/adl-bindist-$adlversion-$platform.zip - -if [ ! -d "$cachedir/$adlversion" ]; then - echo "fetching $release ..." 1>@2 - mkdir -p $downloads - (cd $downloads; wget -q $release || (echo "download failed"; exit 1)) - mkdir -p $cachedir/$adlversion - (cd $cachedir/$adlversion; unzip -q $downloads/$(basename $release)) -fi - -exec $cachedir/$adlversion/bin/adlc "$@" - diff --git a/tools/gen-adl.sh b/tools/gen-adl.sh deleted file mode 100755 index b7fa41b..0000000 --- a/tools/gen-adl.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -set -euo pipefail -IFS=$'\n\t' - -set -x - -SCRIPT_DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -ROOT=$SCRIPT_DIR/.. -ADLC=$SCRIPT_DIR/adlc - -APP_ADL_DIR=$ROOT/adl -APP_ADL_FILES=`find $APP_ADL_DIR -iname '*.adl'` - -ADL_STDLIB_DIR=`$ADLC show --adlstdlib` -ADL_STDLIB_SYS_FILES=`find ${ADL_STDLIB_DIR} -name '*.adl'` - -# Generate Typescript for unit testing code -OUTPUT_DIR=$ROOT/adl-gen -$ADLC typescript \ - --searchdir $APP_ADL_DIR \ - --outputdir $OUTPUT_DIR \ - --manifest=$OUTPUT_DIR/.manifest \ - --include-rt \ - --include-resolver \ - --runtime-dir runtime \ - ${ADL_STDLIB_DIR}/sys/types.adl \ - ${APP_ADL_FILES} - -cd adl-gen -ADLFILES=$(find . -type f -name '*.ts') -for file in ${ADLFILES}; do - sed --in-place -r -e 's/import (.*) from "(.*)";/import \1 from "\2.ts";/g' ${file} - sed --in-place -r -e "s/import (.*) from '(.*)';/import \1 from \"\2.ts\";/g" ${file} -done diff --git a/utils.ts b/utils.ts index afb9e4d..a09918a 100644 --- a/utils.ts +++ b/utils.ts @@ -1,2 +1,4 @@ +// Convenience re-exports for backward compatibility and internal use +// These utilities are also available through the main module exports export * from "./utils/process.ts"; export * from "./utils/git.ts"; diff --git a/asyncQueue.ts b/utils/asyncQueue.ts similarity index 78% rename from asyncQueue.ts rename to utils/asyncQueue.ts index 52bf5ed..5249c66 100644 --- a/asyncQueue.ts +++ b/utils/asyncQueue.ts @@ -1,14 +1,14 @@ export type Action = () => Promise; // based on https://medium.com/@karenmarkosyan/how-to-manage-promises-into-dynamic-queue-with-vanilla-javascript-9d0d1f8d4df5 -export class AsyncQueue { +export class AsyncQueue { inProgress = 0; concurrency: number; queue: { - action: Action; - resolve: (t: T) => void; - reject: (err: E) => void; + action: Action; + resolve: (t: unknown) => void; + reject: (err: unknown) => void; }[] = []; constructor(concurrency: number) { @@ -17,11 +17,11 @@ export class AsyncQueue { /// Schedule an action for start later. Immediately returns a Promise but actual /// work of the original action->promise starts later - schedule(t: Action): Promise { + schedule(action: Action): Promise { return new Promise((resolve, reject) => { this.queue.push({ - action: t, - resolve, + action: action as Action, + resolve: resolve as (t: unknown) => void, reject, }); this.startQueuedItem(); @@ -41,7 +41,7 @@ export class AsyncQueue { this.inProgress += 1; item.action() - .then((val: T) => { + .then((val: unknown) => { item.resolve(val); }) .catch((err) => { diff --git a/utils/filesystem.ts b/utils/filesystem.ts new file mode 100644 index 0000000..4cff957 --- /dev/null +++ b/utils/filesystem.ts @@ -0,0 +1,60 @@ +import { crypto } from "@std/crypto/crypto"; +import { encodeHex } from "@std/encoding"; +import type { + Timestamp, + TrackedFileHash, + TrackedFileName, +} from "../interfaces/core/IManifestTypes.ts"; + +export type StatResult = + | { + kind: "fileInfo"; + fileInfo: Deno.FileInfo; + } + | { + kind: "nonExistent"; + }; + +export async function statPath(path: TrackedFileName): Promise { + try { + const fileInfo = await Deno.stat(path); + return { + kind: "fileInfo", + fileInfo, + }; + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + return { + kind: "nonExistent", + }; + } + throw err; + } +} + +export async function deletePath(path: TrackedFileName): Promise { + try { + await Deno.remove(path, { recursive: true }); + } catch (err) { + // Ignore NotFound errors + if (!(err instanceof Deno.errors.NotFound)) { + throw err; + } + } +} + +export async function getFileSha1Sum( + filename: string, +): Promise { + const file = await Deno.open(filename, { read: true }); + const hashBuffer = await crypto.subtle.digest("SHA-1", file.readable); + return encodeHex(hashBuffer); +} + +export function getFileTimestamp( + _filename: string, + stat: Deno.FileInfo, +): Timestamp { + const mtime = stat.mtime; + return mtime?.toISOString() || ""; +} diff --git a/utils/git.ts b/utils/git.ts index ea7def6..65f2660 100644 --- a/utils/git.ts +++ b/utils/git.ts @@ -1,7 +1,8 @@ import { run, runConsole } from "./process.ts"; -import { task, TaskContext } from "../dnit.ts"; +import { type Task, task } from "../core/task.ts"; +import type { TaskContext } from "../core/TaskContext.ts"; -export async function gitLatestTag(tagPrefix: string) { +export async function gitLatestTag(tagPrefix: string): Promise { const describeStr = await run( ["git", "describe", "--tags", "--match", `${tagPrefix}*`, "--abbrev=0"], ); @@ -13,12 +14,12 @@ export function gitLastCommitMessage(): Promise { return run(["git", "log", "--pretty=oneline", "--abbrev-commit", "-1"]); } -export async function gitIsClean() { +export async function gitIsClean(): Promise { const gitStatus = await run(["git", "status", "--porcelain"]); return gitStatus.length === 0; } -export const fetchTags = task({ +export const fetchTags: Task = task({ name: "fetch-tags", description: "Git remote fetch tags", action: async () => { @@ -27,7 +28,7 @@ export const fetchTags = task({ uptodate: () => false, }); -export const requireCleanGit = task({ +export const requireCleanGit: Task = task({ name: "git-is-clean", description: "Check git status is clean", action: async (ctx: TaskContext) => { diff --git a/utils/process.test.ts b/utils/process.test.ts deleted file mode 100644 index 00a2e31..0000000 --- a/utils/process.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { assertEquals } from "https://deno.land/std@0.221.0/assert/mod.ts"; - -import { run } from "./process.ts"; - -Deno.test("Process - run", async () => { - const str = await run(["echo", "hello world"]); - assertEquals(str.trim(), "hello world"); -}); diff --git a/utils/process.ts b/utils/process.ts index 5f4022a..c1fe5b1 100644 --- a/utils/process.ts +++ b/utils/process.ts @@ -27,5 +27,10 @@ export async function runConsole( stdout: "inherit", }); - await dcmd.output(); + const result = await dcmd.output(); + if (!result.success) { + throw new Error( + `Command failed with exit code ${result.code}: ${cmd.join(" ")}`, + ); + } } diff --git a/textTable.ts b/utils/textTable.ts similarity index 80% rename from textTable.ts rename to utils/textTable.ts index eec7f3f..0a7789d 100644 --- a/textTable.ts +++ b/utils/textTable.ts @@ -1,3 +1,26 @@ +export function plainTextTable(headings: string[], cells: string[][]): string { + const maxWidths: number[] = headings.map((t) => t.length); + for (const row of cells) { + let colInd = 0; + for (const col of row) { + maxWidths[colInd] = Math.max(maxWidths[colInd], col.length); + ++colInd; + } + } + + const output: string[] = []; + + // Add data rows only (no header) + for (const row of cells) { + const dataRow = row.map((cell, i) => { + return cell.padEnd(maxWidths[i]); + }).join(" "); + output.push(dataRow); + } + + return output.join("\n"); +} + export function textTable(headings: string[], cells: string[][]): string { const corners = [["┌", "┐"], ["└", "┘"]]; const hbar = "─"; diff --git a/version.ts b/version.ts index 00628f4..7f1cf46 100644 --- a/version.ts +++ b/version.ts @@ -1 +1 @@ -export const version = "1.14.4"; +export const version = "2.0.0-pre.0";