diff --git a/CHANGELOG.md b/CHANGELOG.md index 51c6ebb43..87611de93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,22 @@ history. ## Unreleased +- **BREAKING CHANGES:** `fjs/cli` migrates from authored TypeScript + (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`), splitting its + type-level API into a sibling `types.ts` — importers must use the + `.f.mjs` specifier for runtime values and the `types.ts` specifier + for types. `proof.f.ts` stays TypeScript for now + [#1491](https://github.com/functionalscript/functionalscript/pull/1491) +- **BREAKING CHANGES:** `fjs/website` migrates from authored + TypeScript (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`) — importers + must use the `.f.mjs` specifier. `proof.f.ts` stays TypeScript for + now + [#1491](https://github.com/functionalscript/functionalscript/pull/1491) +- **BREAKING CHANGES:** `fjs/dev/update` migrates from authored + TypeScript (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`) — importers + must use the `.f.mjs` specifier. `proof.f.ts` stays TypeScript for + now + [#1491](https://github.com/functionalscript/functionalscript/pull/1491) - **BREAKING CHANGES:** `fjs/text/sgr` migrates from authored TypeScript (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`), splitting its type-level API into a sibling `types.ts` — importers must use diff --git a/fjs/README.md b/fjs/README.md index 67da724f2..94a10c281 100644 --- a/fjs/README.md +++ b/fjs/README.md @@ -106,5 +106,5 @@ fjs run ./my-tool.f.ts foo bar # options.args === ['foo', 'bar'] ``` fjs/module.ts — Node.js entry point (runs main via the node runner) fjs/module.f.ts — FunctionalScript command dispatcher (Commands list + dispatch) -fjs/cli/module.f.ts — generic Command/Commands/dispatch primitives +fjs/cli/module.f.mjs — generic dispatch primitive (Command/Commands types in fjs/cli/types.ts) ``` diff --git a/fjs/cas/cli/module.f.ts b/fjs/cas/cli/module.f.ts index 613e54b1a..e6463c62c 100644 --- a/fjs/cas/cli/module.f.ts +++ b/fjs/cas/cli/module.f.ts @@ -8,7 +8,8 @@ import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.mjs' import { forEachStep, pure, step } from '../../effects/module.f.mjs' import { errorExit, log, writeFromStream } from '../../effects/node/module.f.mjs' import type { All, Read, Write, WriteFile } from '../../effects/node/types.ts' -import { dispatch, type Commands } from '../../cli/module.f.ts' +import { dispatch } from '../../cli/module.f.mjs' +import type { Commands } from '../../cli/types.ts' import type { MemOp } from '../../effects/memory/types.ts' import { casAddFile, fileCas, type FileCasOperation } from '../module.f.ts' diff --git a/fjs/cas/cli/proof.f.ts b/fjs/cas/cli/proof.f.ts index db5e5babf..8b3cb926b 100644 --- a/fjs/cas/cli/proof.f.ts +++ b/fjs/cas/cli/proof.f.ts @@ -3,7 +3,7 @@ import { computeSync, sha256 } from '../../crypto/sha2/module.f.mjs' import { maxLength, vec, vec8 } from '../../types/bit_vec/module.f.mjs' import { defaultNodeProgramOptions, emptyState, virtual } from '../../effects/node/virtual/module.f.ts' import type { NodeProgramOptions } from '../../effects/node/types.ts' -import { dispatch } from '../../cli/module.f.ts' +import { dispatch } from '../../cli/module.f.mjs' import { vecToCBase32 } from '../../basen/cbase32/module.f.mjs' import { assert, assertEq } from '../../asserts/module.f.mjs' diff --git a/fjs/cli/module.f.ts b/fjs/cli/module.f.mjs similarity index 71% rename from fjs/cli/module.f.ts rename to fjs/cli/module.f.mjs index 80e59b75c..fc476ea75 100644 --- a/fjs/cli/module.f.ts +++ b/fjs/cli/module.f.mjs @@ -1,22 +1,22 @@ +/** + * CLI command dispatch table. + * + * See `./types.ts` for the type-level API. + * + * @module + */ + import { errorExit, log } from '../effects/node/module.f.mjs' -import type { NodeOp, NodeProgramOptions, Write } from '../effects/node/types.ts' +/** @import { NodeOp, NodeProgramOptions, Write } from '../effects/node/types.ts' */ import { pure, step } from '../effects/module.f.mjs' -import type { Effect } from '../effects/types.ts' +/** @import { Effect } from '../effects/types.ts' */ import { at, fromEntries } from '../types/object/module.f.mjs' - -type Handler = (options: NodeProgramOptions) => Effect - -export type Command = { - readonly names: readonly string[] - readonly description: string - readonly handler: Handler | Commands -} - -export type Commands = readonly Command[] +/** @import { Commands } from './types.ts' */ const helpMeta = { names: ['help', 'h', '?'], description: 'Print this help message' } -export const dispatch = (commands: Commands) => (options: NodeProgramOptions): Effect => { +/** @type {(commands: Commands) => (options: NodeProgramOptions) => Effect} */ +export const dispatch = commands => options => { const [cmd, ...rest] = options.args const rows = [...commands, helpMeta] const nameCol = rows.map(({names}) => names.join(', ')) @@ -25,7 +25,7 @@ export const dispatch = (commands: Commands) => (options: N 'Available commands:', ...rows.map(({description}, i) => ` ${nameCol[i].padEnd(width)} ${description}`) ].join('\n') - const map = fromEntries(commands.flatMap(c => c.names.map(n => [n, c] as const))) + const map = fromEntries(commands.flatMap(c => c.names.map(n => /** @type {const} */ ([n, c])))) if (cmd === undefined) { return errorExit(`Error: command is required.\n${helpText}`) } diff --git a/fjs/cli/proof.f.ts b/fjs/cli/proof.f.ts index 9adbeef00..8a917b8d6 100644 --- a/fjs/cli/proof.f.ts +++ b/fjs/cli/proof.f.ts @@ -1,7 +1,8 @@ import { pure } from '../effects/module.f.mjs' import type { NodeOp, NodeProgramOptions } from '../effects/node/types.ts' import { defaultNodeProgramOptions, emptyState, virtual } from '../effects/node/virtual/module.f.ts' -import { dispatch, type Commands } from './module.f.ts' +import { dispatch } from './module.f.mjs' +import type { Commands } from './types.ts' import { assert, assertEq } from '../asserts/module.f.mjs' const makeOptions = (args: readonly string[]): NodeProgramOptions => diff --git a/fjs/cli/todo/dispatch-help-rendering.md b/fjs/cli/todo/dispatch-help-rendering.md index c63022390..1f29e93a6 100644 --- a/fjs/cli/todo/dispatch-help-rendering.md +++ b/fjs/cli/todo/dispatch-help-rendering.md @@ -5,7 +5,7 @@ ### Problem -`dispatch` (`fjs/cli/module.f.ts:17-46`) interleaves three distinct concerns — +`dispatch` (`fjs/cli/module.f.mjs:18-50`) interleaves three distinct concerns — building the name→command lookup map, rendering the aligned help table, and routing — and computes the first two eagerly at the top of every call: @@ -22,9 +22,9 @@ const map = fromEntries(commands.flatMap(c => c.names.map(n => [n, c] as const)) ``` `helpText` — the column-measuring and padding work — is consumed only by the -three error/help branches (`:28`, `:38`, `:42`); on the common success path +three error/help branches (`:29`, `:39-41`, `:45`); on the common success path it is dead work. And because `dispatch` recurses for nested command groups -(`:35`, `:45`), the table and the map are rebuilt at every level of the +(`:36`, `:48`), the table and the map are rebuilt at every level of the descent even when neither is used. Per AGENTS.md, separation of concerns is "always appropriate" even with a @@ -34,7 +34,7 @@ its own named, independently testable function. ### Proposal -Extract a module-scope pure helper in `fjs/cli/module.f.ts` (the layer that +Extract a module-scope pure helper in `fjs/cli/module.f.mjs` (the layer that owns `Command`/`Commands`): ```ts @@ -64,6 +64,6 @@ help rendering is the clear win. ### Related -- `fjs/cli/module.f.ts:17-46` — current `dispatch`. +- `fjs/cli/module.f.mjs:18-50` — current `dispatch`. - [positional-arity-check](./positional-arity-check.md) — separate concern (argument validation), independent of this change. diff --git a/fjs/cli/todo/options-edsl.md b/fjs/cli/todo/options-edsl.md index 232e4439d..bd5d66a91 100644 --- a/fjs/cli/todo/options-edsl.md +++ b/fjs/cli/todo/options-edsl.md @@ -5,7 +5,7 @@ ### Problem -`Command` (`fjs/cli/module.f.ts`) hands every handler a raw +`Command` (`fjs/cli/types.ts`) hands every handler a raw `args: readonly string[]` and has no concept of a named option. Any command that wants a flag has to hand-parse it: diff --git a/fjs/cli/todo/positional-arity-check.md b/fjs/cli/todo/positional-arity-check.md index ef3380ff1..3996eab36 100644 --- a/fjs/cli/todo/positional-arity-check.md +++ b/fjs/cli/todo/positional-arity-check.md @@ -25,8 +25,8 @@ handler: ({ home, args: [hashCBase32, path, ...rest] }) => { ### Proposal -A combinator in `fjs/cli/module.f.ts`, the layer that already owns -`Command`/`dispatch`: +A combinator in `fjs/cli/module.f.mjs`, the layer that already owns +`dispatch` (`Command` is defined in `fjs/cli/types.ts`): ```ts export const exact = (name: string, n: number) => diff --git a/fjs/cli/types.ts b/fjs/cli/types.ts new file mode 100644 index 000000000..eae90dedb --- /dev/null +++ b/fjs/cli/types.ts @@ -0,0 +1,18 @@ +/** + * Types for the CLI command dispatch table. + * + * @module + */ + +import type { NodeOp, NodeProgramOptions } from '../effects/node/types.ts' +import type { Effect } from '../effects/types.ts' + +type Handler = (options: NodeProgramOptions) => Effect + +export type Command = { + readonly names: readonly string[] + readonly description: string + readonly handler: Handler | Commands +} + +export type Commands = readonly Command[] diff --git a/fjs/dev/update/module.f.ts b/fjs/dev/update/module.f.mjs similarity index 51% rename from fjs/dev/update/module.f.ts rename to fjs/dev/update/module.f.mjs index e9b704a21..d49727c9f 100644 --- a/fjs/dev/update/module.f.ts +++ b/fjs/dev/update/module.f.mjs @@ -3,18 +3,23 @@ * * @module */ + import { history, historyStep, mapStep, step } from '../../effects/module.f.mjs' -import type { Effect } from '../../effects/types.ts' +/** @import { Effect } from '../../effects/types.ts' */ import { mkdir, readUtf8File, writeUtf8File } from '../../effects/node/module.f.mjs' -import type { Mkdir, NodeProgram, ReadFile, WriteFile } from '../../effects/node/types.ts' +/** @import { Mkdir, NodeProgram, ReadFile, WriteFile } from '../../effects/node/types.ts' */ import { unwrap } from '../../types/result/module.f.mjs' -const source = '.copilot/mcp.json' as const -const targetDirectory = '.vscode' as const -const target = '.vscode/mcp.json' as const +const source = /** @type {const} */ ('.copilot/mcp.json') +const targetDirectory = /** @type {const} */ ('.vscode') +const target = /** @type {const} */ ('.vscode/mcp.json') -/** Regenerates VS Code's local MCP configuration from the canonical Copilot configuration. */ -export const syncMcp = (): Effect => { +/** + * Regenerates VS Code's local MCP configuration from the canonical Copilot configuration. + * + * @type {() => Effect} + */ +export const syncMcp = () => { const sourceText = history(mapStep(readUtf8File(source), unwrap)) const targetDirectoryReady = historyStep( sourceText, @@ -23,5 +28,9 @@ export const syncMcp = (): Effect => { return mapStep(targetWritten, unwrap) } -/** Runs all local development configuration generators. */ -export const main: NodeProgram = () => mapStep(syncMcp(), () => 0) +/** + * Runs all local development configuration generators. + * + * @type {NodeProgram} + */ +export const main = () => mapStep(syncMcp(), () => 0) diff --git a/fjs/dev/update/proof.f.ts b/fjs/dev/update/proof.f.ts index c64d4ef0b..cdcfcc01d 100644 --- a/fjs/dev/update/proof.f.ts +++ b/fjs/dev/update/proof.f.ts @@ -7,7 +7,7 @@ import { assert, assertEq } from '../../asserts/module.f.mjs' import { utf8 } from '../../text/module.f.mjs' import { readUtf8File } from '../../effects/node/module.f.mjs' import { defaultNodeProgramOptions, emptyState, virtual } from '../../effects/node/virtual/module.f.ts' -import { main, syncMcp } from './module.f.ts' +import { main, syncMcp } from './module.f.mjs' import { step } from '../../effects/module.f.mjs' const mcp = '{"servers":{}}' as const diff --git a/fjs/effects/todo/map-step-combinator.md b/fjs/effects/todo/map-step-combinator.md index ee3030b63..db321be0e 100644 --- a/fjs/effects/todo/map-step-combinator.md +++ b/fjs/effects/todo/map-step-combinator.md @@ -39,13 +39,13 @@ Also `fjs/dev/module.f.ts`, `fjs/cas/evo/module.f.ts`, yield an exit code" shape of a `NodeProgram`: ```ts -// fjs/cli/module.f.ts:38-40 +// fjs/cli/module.f.mjs:39-41 return step( log(helpText), () => pure(0)) -// fjs/website/module.f.ts:17-19 -const program: Effect = step( +// fjs/website/module.f.mjs:19-21 +const program = step( writeFile('index.html', html), () => pure(0)) ``` diff --git a/fjs/module.f.ts b/fjs/module.f.ts index 4872ab465..9d0333b8a 100644 --- a/fjs/module.f.ts +++ b/fjs/module.f.ts @@ -9,7 +9,8 @@ import { commands as casCommands } from './cas/cli/module.f.ts' import { main as ciMain } from './ci/module.f.ts' import { import_ } from './effects/node/module.f.mjs' import type { NodeOp, NodeProgram } from './effects/node/types.ts' -import { dispatch, type Commands } from './cli/module.f.ts' +import { dispatch } from './cli/module.f.mjs' +import type { Commands } from './cli/types.ts' import { casMcpServer } from './mcp/module.f.ts' import { pure, step } from './effects/module.f.mjs' import { unwrap } from './types/result/module.f.mjs' diff --git a/fjs/todo/66g-fjs-run-commands.md b/fjs/todo/66g-fjs-run-commands.md index 17939d94e..a8db0bf63 100644 --- a/fjs/todo/66g-fjs-run-commands.md +++ b/fjs/todo/66g-fjs-run-commands.md @@ -25,7 +25,7 @@ runtime. Every downstream caller (`fjs run`, bin scripts) already goes through export type NodeMain = NodeProgram | Commands ``` -Widen `dispatch` in `fjs/cli/module.f.ts` to accept either a `Commands` array +Widen `dispatch` in `fjs/cli/module.f.mjs` to accept either a `Commands` array or a `Program` function, and short-circuit to the function when it receives one: ```ts @@ -61,4 +61,5 @@ wrapper in `fjs/cas/module.f.ts` simplifies to `export const main = commands`. - `fjs/module.f.ts` — the `run` handler at line 39. - `fjs/cas/module.f.ts` — the `main = dispatch(commands)` boilerplate this issue eliminates. -- `fjs/cli/module.f.ts` — `dispatch` and `Commands` used by the new branch. +- `fjs/cli/module.f.mjs` — `dispatch` used by the new branch; `Commands` + is defined in `fjs/cli/types.ts`. diff --git a/fjs/website/module.f.ts b/fjs/website/module.f.mjs similarity index 65% rename from fjs/website/module.f.ts rename to fjs/website/module.f.mjs index 4759c381f..81e40f76c 100644 --- a/fjs/website/module.f.ts +++ b/fjs/website/module.f.mjs @@ -3,20 +3,21 @@ * * @module */ + import { htmlUtf8 } from '../media/html/module.f.mjs' import { writeFile } from '../effects/node/module.f.mjs' -import type { WriteFile } from '../effects/node/types.ts' +/** @import { WriteFile } from '../effects/node/types.ts' */ import { pure, step } from '../effects/module.f.mjs' -import type { Effect } from '../effects/types.ts' -import type { Vec } from '../types/bit_vec/types.ts' +/** @import { Effect } from '../effects/types.ts' */ -const html: Vec = htmlUtf8()( +const html = htmlUtf8()( ['a', { href: 'https://github.com/functionalscript/functionalscript' }, 'GitHub Repository' ]) -const program: Effect = step( +/** @type {Effect} */ +const program = step( writeFile('index.html', html), () => pure(0)) diff --git a/fjs/website/proof.f.ts b/fjs/website/proof.f.ts index 8346e01cd..218c4023f 100644 --- a/fjs/website/proof.f.ts +++ b/fjs/website/proof.f.ts @@ -1,4 +1,4 @@ -import { main } from './module.f.ts' +import { main } from './module.f.mjs' import { emptyState, virtual } from '../effects/node/virtual/module.f.ts' import { assertEq, assertNotNullish } from '../asserts/module.f.mjs' diff --git a/package.json b/package.json index e2bf73233..cfd6266cd 100644 --- a/package.json +++ b/package.json @@ -15,9 +15,9 @@ "cov": "node --test --experimental-test-coverage --test-coverage-include=**/module.f.ts --test-coverage-include=**/module.f.mjs", "start": "node ./fjs/module.ts", "ci-update": "node ./fjs/module.ts ci", - "dev-update": "node ./fjs/module.ts r ./fjs/dev/update/module.f.ts", + "dev-update": "node ./fjs/module.ts r ./fjs/dev/update/module.f.mjs", "update": "npm run ci-update && npm install && deno install && bun install", - "index-html": "node ./fjs/module.ts r ./fjs/website/module.f.ts", + "index-html": "node ./fjs/module.ts r ./fjs/website/module.f.mjs", "website": "npm run prepack && npm run index-html" }, "engines": {