functionalscript 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/fs/ci/common/module.f.d.ts +4 -5
  2. package/fs/ci/common/module.f.js +4 -4
  3. package/fs/ci/config/module.f.d.ts +4 -4
  4. package/fs/ci/config/module.f.js +4 -4
  5. package/fs/ci/test.f.js +2 -4
  6. package/fs/dev/module.f.d.ts +3 -3
  7. package/fs/dev/module.f.js +12 -9
  8. package/fs/dev/tf/module.d.ts +1 -0
  9. package/fs/dev/tf/module.f.d.ts +70 -6
  10. package/fs/dev/tf/module.f.js +135 -87
  11. package/fs/dev/tf/module.js +66 -17
  12. package/fs/dev/tf/test.f.d.ts +21 -20
  13. package/fs/dev/tf/test.f.js +249 -31
  14. package/fs/djs/module.f.d.ts +2 -2
  15. package/fs/djs/module.f.js +8 -5
  16. package/fs/djs/test.f.js +5 -6
  17. package/fs/djs/tokenizer-new/test.f.js +126 -78
  18. package/fs/djs/transpiler/module.f.js +2 -2
  19. package/fs/djs/transpiler/test.f.js +11 -12
  20. package/fs/fjs/module.f.d.ts +2 -7
  21. package/fs/fjs/module.f.js +16 -22
  22. package/fs/fjs/module.js +2 -2
  23. package/fs/io/module.d.ts +3 -3
  24. package/fs/io/module.f.d.ts +5 -2
  25. package/fs/io/module.f.js +14 -3
  26. package/fs/io/module.js +38 -17
  27. package/fs/path/module.f.d.ts +6 -0
  28. package/fs/path/module.f.js +6 -0
  29. package/fs/path/test.f.d.ts +3 -5
  30. package/fs/path/test.f.js +67 -49
  31. package/fs/text/sgr/module.f.d.ts +9 -1
  32. package/fs/text/sgr/module.f.js +16 -5
  33. package/fs/types/effects/node/module.f.d.ts +17 -17
  34. package/fs/types/effects/node/module.f.js +20 -2
  35. package/fs/types/effects/node/test.f.js +8 -5
  36. package/fs/types/effects/node/virtual/module.f.d.ts +11 -2
  37. package/fs/types/effects/node/virtual/module.f.js +30 -17
  38. package/fs/types/function/compare/module.f.d.ts +12 -0
  39. package/fs/types/function/compare/module.f.js +33 -0
  40. package/fs/types/function/operator/test.f.d.ts +10 -0
  41. package/fs/types/function/operator/test.f.js +81 -0
  42. package/fs/types/range_map/module.f.js +3 -18
  43. package/fs/types/result/module.f.d.ts +4 -0
  44. package/fs/types/result/module.f.js +4 -0
  45. package/fs/types/result/test.f.d.ts +2 -4
  46. package/fs/types/result/test.f.js +24 -16
  47. package/fs/types/rtti/common/module.f.d.ts +10 -1
  48. package/fs/types/rtti/common/module.f.js +7 -2
  49. package/fs/types/rtti/parse/module.f.js +35 -46
  50. package/fs/types/rtti/validate/module.f.js +9 -12
  51. package/fs/types/sorted_list/module.f.d.ts +1 -2
  52. package/fs/types/sorted_list/module.f.js +8 -21
  53. package/fs/types/sorted_set/module.f.d.ts +1 -3
  54. package/fs/types/ts/test.f.d.ts +18 -0
  55. package/fs/types/ts/test.f.js +111 -0
  56. package/fs/types/uint8array/module.f.js +7 -1
  57. package/fs/types/uint8array/test.f.d.ts +1 -0
  58. package/fs/types/uint8array/test.f.js +5 -1
  59. package/package.json +1 -1
@@ -12,12 +12,12 @@ export type Os = typeof os[number];
12
12
  export declare const architecture: readonly ["intel", "arm"];
13
13
  export type Architecture = typeof architecture[number];
14
14
  export type Image = typeof images[Os][Architecture];
15
- declare const stepSchema: {
15
+ export declare const stepSchema: {
16
16
  readonly run: import("../../types/rtti/module.f.ts").Or<readonly [import("../../types/rtti/module.f.ts").String, undefined]>;
17
17
  readonly uses: import("../../types/rtti/module.f.ts").Or<readonly [import("../../types/rtti/module.f.ts").String, undefined]>;
18
18
  readonly with: import("../../types/rtti/module.f.ts").Or<readonly [import("../../types/rtti/module.f.ts").Type1<"record", import("../../types/rtti/module.f.ts").String>, undefined]>;
19
19
  };
20
- declare const jobSchema: {
20
+ export declare const jobSchema: {
21
21
  readonly 'runs-on': import("../../types/rtti/module.f.ts").String;
22
22
  readonly steps: import("../../types/rtti/module.f.ts").Type1<"array", {
23
23
  readonly run: import("../../types/rtti/module.f.ts").Or<readonly [import("../../types/rtti/module.f.ts").String, undefined]>;
@@ -25,7 +25,7 @@ declare const jobSchema: {
25
25
  readonly with: import("../../types/rtti/module.f.ts").Or<readonly [import("../../types/rtti/module.f.ts").Type1<"record", import("../../types/rtti/module.f.ts").String>, undefined]>;
26
26
  }>;
27
27
  };
28
- declare const jobsSchema: import("../../types/rtti/module.f.ts").Type1<"record", {
28
+ export declare const jobsSchema: import("../../types/rtti/module.f.ts").Type1<"record", {
29
29
  readonly 'runs-on': import("../../types/rtti/module.f.ts").String;
30
30
  readonly steps: import("../../types/rtti/module.f.ts").Type1<"array", {
31
31
  readonly run: import("../../types/rtti/module.f.ts").Or<readonly [import("../../types/rtti/module.f.ts").String, undefined]>;
@@ -33,7 +33,7 @@ declare const jobsSchema: import("../../types/rtti/module.f.ts").Type1<"record",
33
33
  readonly with: import("../../types/rtti/module.f.ts").Or<readonly [import("../../types/rtti/module.f.ts").Type1<"record", import("../../types/rtti/module.f.ts").String>, undefined]>;
34
34
  }>;
35
35
  }>;
36
- declare const gitHubActionSchema: {
36
+ export declare const gitHubActionSchema: {
37
37
  readonly name: import("../../types/rtti/module.f.ts").String;
38
38
  readonly on: {
39
39
  readonly pull_request: import("../../types/rtti/module.f.ts").Or<readonly [{}, undefined]>;
@@ -82,4 +82,3 @@ export declare const clean: (steps: readonly MetaStep[]) => readonly MetaStep[];
82
82
  export declare const toSteps: (m: readonly MetaStep[]) => readonly Step[];
83
83
  export declare const ubuntu: (ms: readonly MetaStep[]) => Job;
84
84
  export declare const findTgz: (v: Os) => "(Get-ChildItem *.tgz).FullName" | "./*.tgz";
85
- export {};
@@ -11,17 +11,17 @@ import {} from "../../types/rtti/ts/module.f.js";
11
11
  import { parse as rttiParse } from "../../types/rtti/parse/module.f.js";
12
12
  export const os = ['ubuntu', 'macos', 'windows'];
13
13
  export const architecture = ['intel', 'arm'];
14
- const stepSchema = {
14
+ export const stepSchema = {
15
15
  run: option(string),
16
16
  uses: option(string),
17
17
  with: option(record(string))
18
18
  };
19
- const jobSchema = {
19
+ export const jobSchema = {
20
20
  'runs-on': string,
21
21
  steps: array(stepSchema)
22
22
  };
23
- const jobsSchema = record(jobSchema);
24
- const gitHubActionSchema = {
23
+ export const jobsSchema = record(jobSchema);
24
+ export const gitHubActionSchema = {
25
25
  name: string,
26
26
  on: { pull_request: option({}) },
27
27
  jobs: jobsSchema
@@ -20,13 +20,13 @@ export declare const images: {
20
20
  };
21
21
  };
22
22
  export declare const bun = "1.3.14";
23
- export declare const deno = "2.7.14";
23
+ export declare const deno = "2.8.0";
24
24
  export declare const playwright = "1.60.0";
25
25
  export declare const rust = "1.95.0";
26
26
  export declare const node: {
27
- readonly default: "26.1.0";
28
- readonly others: readonly ["22.22.3", "24.15.0"];
27
+ readonly default: "26.2.0";
28
+ readonly others: readonly ["22.22.3", "24.16.0"];
29
29
  };
30
30
  export declare const wasmtime = "44.0.1";
31
31
  export declare const wasmer = "7.1.0";
32
- export declare const tsgo = "7.0.0-dev.20260517.1";
32
+ export declare const tsgo = "7.0.0-dev.20260526.1";
@@ -23,19 +23,19 @@ export const images = {
23
23
  // https://bun.sh/
24
24
  export const bun = '1.3.14';
25
25
  // https://deno.com/
26
- export const deno = '2.7.14';
26
+ export const deno = '2.8.0';
27
27
  // https://www.npmjs.com/package/playwright
28
28
  export const playwright = '1.60.0';
29
29
  // https://rust-lang.org/
30
30
  export const rust = '1.95.0';
31
31
  // https://nodejs.org/en/download
32
32
  export const node = {
33
- default: '26.1.0',
34
- others: ['22.22.3', '24.15.0'],
33
+ default: '26.2.0',
34
+ others: ['22.22.3', '24.16.0'],
35
35
  };
36
36
  // https://github.com/bytecodealliance/wasmtime/releases
37
37
  export const wasmtime = '44.0.1';
38
38
  // https://github.com/wasmerio/wasmer/releases
39
39
  export const wasmer = '7.1.0';
40
40
  // https://www.npmjs.com/package/@typescript/native-preview?activeTab=versions
41
- export const tsgo = '7.0.0-dev.20260517.1';
41
+ export const tsgo = '7.0.0-dev.20260526.1';
package/fs/ci/test.f.js CHANGED
@@ -4,10 +4,8 @@ import { isVec } from "../types/bit_vec/module.f.js";
4
4
  import { test, parseGitHubAction } from "./common/module.f.js";
5
5
  import { assert } from "../dev/module.f.js";
6
6
  import { emptyState, virtual } from "../types/effects/node/virtual/module.f.js";
7
- import {} from "../types/rtti/ts/module.f.js";
8
7
  import { parse as jsonParse } from "../json/module.f.js";
9
8
  import { unwrap } from "../types/result/module.f.js";
10
- // type Gha = Ts<typeof gitHubActionSchema>
11
9
  const hasRun = (cmd) => (gha) => Object.values(gha.jobs).some(job => job.steps.some(step => step.run?.includes(cmd)));
12
10
  const hasRunInJob = (jobId, cmd) => (gha) => gha.jobs[jobId]?.steps.some(step => step.run?.includes(cmd)) ?? false;
13
11
  const githubState = {
@@ -18,9 +16,9 @@ const run = (rust, nodeExtra = () => []) => {
18
16
  const [state, result] = virtual(githubState)(ci({ rust, nodeExtra, denoExtra: [], bunExtra: [] }));
19
17
  assert(result === 0, result);
20
18
  const dotGithub = state.root['.github'];
21
- assert(dotGithub !== undefined && !isVec(dotGithub), dotGithub);
19
+ assert(typeof dotGithub === 'object', dotGithub);
22
20
  const workflows = dotGithub['workflows'];
23
- assert(workflows !== undefined && !isVec(workflows), workflows);
21
+ assert(typeof workflows === 'object', workflows);
24
22
  const file = workflows['ci.yml'];
25
23
  assert(isVec(file), file);
26
24
  return unwrap(parseGitHubAction(jsonParse(utf8ToString(file))));
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- import { type Io } from '../io/module.f.ts';
6
+ import type { Io } from '../io/module.f.ts';
7
7
  import { type Access, type All, type Env, type Import, type NodeProgram, type Readdir } from '../types/effects/node/module.f.ts';
8
8
  import { type Effect } from '../types/effects/module.f.ts';
9
9
  export declare const todo: () => never;
@@ -18,6 +18,6 @@ export type ModuleMap = {
18
18
  };
19
19
  export declare const env: (io: Io) => (v: string) => string | undefined;
20
20
  export declare const allFiles: (s: string) => Effect<Readdir | All, readonly string[]>;
21
- export declare const loadModuleMap2: (env: Env) => Effect<Access | Import | All | Readdir, ModuleMap>;
22
- export declare const loadModuleMap: (io: Io) => Promise<ModuleMap>;
21
+ export type LoadModuleOperations = Access | Import | All | Readdir;
22
+ export declare const loadModuleMap: (env: Env) => Effect<LoadModuleOperations, ModuleMap>;
23
23
  export declare const index4: NodeProgram;
@@ -1,9 +1,3 @@
1
- /**
2
- * Development utilities for indexing modules and loading FunctionalScript files.
3
- *
4
- * @module
5
- */
6
- import { fromIo } from "../io/module.f.js";
7
1
  import { updateVersion } from "./version/module.f.js";
8
2
  import { access, all, both, import_, readdir, readFile, writeFile } from "../types/effects/node/module.f.js";
9
3
  import { utf8, utf8ToString } from "../text/module.f.js";
@@ -12,6 +6,7 @@ import { begin, pure } from "../types/effects/module.f.js";
12
6
  import { parse as jsonParse } from "../json/module.f.js";
13
7
  import { record, unknown as rttiUnknown } from "../types/rtti/module.f.js";
14
8
  import { parse as rttiParse } from "../types/rtti/parse/module.f.js";
9
+ import { relativize } from "../path/module.f.js";
15
10
  export const todo = () => { throw 'not implemented'; };
16
11
  export const assert = (v, msg = 'assertion failed') => {
17
12
  if (!v)
@@ -63,14 +58,22 @@ const loadFile = (f) => {
63
58
  }
64
59
  return pure([]);
65
60
  };
66
- export const loadModuleMap2 = (env) => {
61
+ const { fromEntries } = Object;
62
+ export const loadModuleMap = (env) => {
67
63
  const initCwd = env['INIT_CWD'];
68
64
  const s = initCwd === undefined ? '.' : `${initCwd.replaceAll('\\', '/')}`;
65
+ const prefix = s === '.' ? '' : s;
66
+ // TODO: there are multiple `all` effects here,
67
+ // we should consider optimize them by ALIQ technique or something similar.
68
+ // For example, we should be able to write it like `allFiles(s).flatMap(loadFile)`,
69
+ // then an effect runner can batch all file loading operations together.
69
70
  return allFiles(s)
70
71
  .step(files => all(...files.map(loadFile)))
71
- .step(entries => pure(Object.fromEntries(entries.flat().toSorted(cmp))));
72
+ .step(entries => pure(fromEntries(entries
73
+ .flat()
74
+ .map(([k, v]) => [relativize(prefix, k), v])
75
+ .toSorted(cmp))));
72
76
  };
73
- export const loadModuleMap = async (io) => fromIo(io)(loadModuleMap2(io.process.env));
74
77
  const denoJson = './deno.json';
75
78
  const parseDenoJson = rttiParse(record(rttiUnknown));
76
79
  const index2 = begin
@@ -1 +1,2 @@
1
+ export declare const run3: () => Promise<void>;
1
2
  export declare const run: () => Promise<void>;
@@ -1,8 +1,72 @@
1
- import type { Io } from '../../io/module.f.ts';
2
- import type { SandboxResult } from '../../types/effects/node/module.f.ts';
1
+ import { type All, type NodeProgram, type NodeProgramOptions, type Program, type Sandbox, type SandboxResult, type Write } from '../../types/effects/node/module.f.ts';
2
+ import { type Effect, type Operation } from '../../types/effects/module.f.ts';
3
+ import { type LoadModuleOperations, type ModuleMap } from '../module.f.ts';
3
4
  export declare const isTest: (s: string) => boolean;
4
5
  export type Test = () => unknown;
5
- export type TestSet = Test | readonly (readonly [string, unknown])[];
6
- export declare const parseTestSet: (sandbox: <R>(f: () => R) => SandboxResult<R>) => (throws: boolean) => (x: unknown) => TestSet;
7
- export declare const anyLog: (f: (s: string) => void) => (s: string) => <T>(state: T) => T;
8
- export declare const main: (io: Io) => Promise<number>;
6
+ export type TestEntry = {
7
+ readonly fn: Test;
8
+ readonly throws: boolean;
9
+ };
10
+ export type TestSet = TestEntry | readonly (readonly [string, unknown])[];
11
+ export declare const parseTestSet: (throws: boolean, x: unknown) => TestSet;
12
+ /**
13
+ * Recursively collects all leaf tests reachable from `v` as `[path, entry]`
14
+ * pairs, without running anything. Return-value sub-trees are not walked
15
+ * (that requires execution); only the static object/array/function structure
16
+ * is traversed.
17
+ */
18
+ export declare const collectTests: (path: Path, throws: boolean, v: unknown) => readonly (readonly [Path, TestEntry])[];
19
+ /**
20
+ * Receives semantic test-run events. Each method is the runner's notification
21
+ * of an event; the reporter decides how to render it (terminal, GitHub
22
+ * annotations, JSON, node `--test`, etc.). `path` is the chain of object keys
23
+ * leading to the current location; `null` marks a function-call boundary, e.g.
24
+ * `['outer', null, 'inner']` means `outer` was invoked and its return value
25
+ * contained `inner`.
26
+ */
27
+ export type Reporter<O extends Operation> = {
28
+ readonly result: (file: string, path: Path, r: SandboxResult<unknown>) => Effect<O, void>;
29
+ readonly summary: (pass: number, fail: number, time: number) => Effect<O, void>;
30
+ readonly test: (file: string, path: Path, set: TestEntry) => Effect<O, SandboxResult<unknown>>;
31
+ };
32
+ export declare const runModuleMap: <O extends Operation>(reporter: Reporter<O>) => (moduleMap: ModuleMap) => Effect<O | All, number>;
33
+ export declare const test: <O extends Operation>(reporter: Reporter<O>) => Program<O | All | LoadModuleOperations>;
34
+ export type Path = readonly (string | null)[];
35
+ export declare const isInteger: (s: string) => boolean;
36
+ export declare const isIdentifier: (s: string) => boolean;
37
+ /**
38
+ * Renders a key chain as a JS property-access expression: identifier keys use
39
+ * dot notation, integer keys use `[N]`, other strings use `["key"]`, and `null`
40
+ * emits `()` to mark a function-call boundary.
41
+ * E.g. `['math', 'add']` → `.math.add`, `['outer', null, 'inner']` → `.outer().inner`.
42
+ */
43
+ export declare const fmtPath: (path: Path) => string;
44
+ /**
45
+ * Formats a fully-qualified test identifier as a JS-like expression, e.g.
46
+ * `import("./math.test.f.ts").add()` or `import("./a.test.f.ts").users[3].name()`.
47
+ * Self-contained per line — suitable for parallel output and as a CLI filter argument.
48
+ */
49
+ export declare const fmtImport: (file: string, path: Path) => string;
50
+ /**
51
+ * Renders a key chain for terminal output: `| ` per level of depth, followed
52
+ * by the last segment formatted as a bare integer, a bare identifier, or a
53
+ * JSON-quoted string. E.g. `['math', 'add']` → `| | add`,
54
+ * `['a', '0']` → `| | 0`, `['x', 'hello world']` → `| | "hello world"`.
55
+ */
56
+ export declare const fmtTerm: (path: Path) => string;
57
+ /**
58
+ * Percent-encodes characters that GitHub workflow-command property values
59
+ * treat as separators (`%`, `:`, `,`) plus newlines.
60
+ * https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions
61
+ */
62
+ export declare const ghEscape: (s: string) => string;
63
+ export declare const defaultTest: (file: string, path: Path, { fn, throws }: TestEntry) => Effect<Sandbox, SandboxResult<unknown>>;
64
+ /**
65
+ * The terminal/GitHub reporter used by `fjs t`. Output goes through
66
+ * `csiWrite`, so ANSI styles are stripped on non-TTY streams. When
67
+ * `GITHUB_ACTION` is set, failures are emitted as `::error` workflow
68
+ * annotations instead of colored lines. Exported as a factory so the
69
+ * GitHub format path can be exercised directly from tests.
70
+ */
71
+ export declare const defaultReporter: (options: NodeProgramOptions) => Reporter<Write | Sandbox>;
72
+ export declare const main: NodeProgram;
@@ -3,9 +3,11 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- import { fold } from "../../types/list/module.f.js";
7
- import { reset, fgGreen, fgRed, bold, stdio, stderr } from "../../text/sgr/module.f.js";
8
- import { env, loadModuleMap } from "../module.f.js";
6
+ import { reset, fgGreen, fgRed, bold, csiWrite } from "../../text/sgr/module.f.js";
7
+ import { all, sandbox } from "../../types/effects/node/module.f.js";
8
+ import { pure } from "../../types/effects/module.f.js";
9
+ import { loadModuleMap } from "../module.f.js";
10
+ import { invert } from "../../types/result/module.f.js";
9
11
  export const isTest = (s) => s.endsWith('test.f.js') || s.endsWith('test.f.ts');
10
12
  const addPass = (delta) => (ts) => ({ ...ts, time: ts.time + delta, pass: ts.pass + 1 });
11
13
  const addFail = (delta) => (ts) => ({ ...ts, time: ts.time + delta, fail: ts.fail + 1 });
@@ -18,24 +20,12 @@ const timeFormat = (a) => {
18
20
  const e = x.substring(s);
19
21
  return `${b}.${e} ms`;
20
22
  };
21
- export const parseTestSet = (sandbox) => (throws) => (x) => {
23
+ export const parseTestSet = (throws, x) => {
22
24
  switch (typeof x) {
23
25
  case 'function': {
24
26
  if (x.length === 0) {
25
- const xt = x;
26
- if (!throws && xt.name !== 'throw') {
27
- return xt;
28
- }
29
- // Pass-on-throw: the test passes if it throws. Triggered when the
30
- // enclosing tree node is named 'throw' (so any function reference
31
- // works, not only inline ones whose inferred name is 'throw').
32
- return () => {
33
- const { result: [tag, value] } = sandbox(xt);
34
- if (tag === 'ok') {
35
- throw value;
36
- }
37
- return value;
38
- };
27
+ const fn = x;
28
+ return { fn, throws: throws || fn.name === 'throw' };
39
29
  }
40
30
  break;
41
31
  }
@@ -48,77 +38,135 @@ export const parseTestSet = (sandbox) => (throws) => (x) => {
48
38
  }
49
39
  return [];
50
40
  };
51
- const test = async (io) => {
52
- const moduleMap = await loadModuleMap(io);
53
- const log = stdio(io);
54
- const error = stderr(io);
55
- const { sandbox } = io;
56
- const env_ = env(io);
57
- const isGitHub = env_('GITHUB_ACTION') !== undefined;
58
- const parse = parseTestSet(sandbox);
59
- const f = ([k, v]) => {
60
- const test = i => throws => v => ts => {
61
- const next = test(`${i}| `);
62
- const set = parse(throws)(v);
63
- if (typeof set === 'function') {
64
- const { result: [s, r], duration: delta } = sandbox(set);
65
- if (s !== 'ok') {
66
- ts = addFail(delta)(ts);
67
- if (isGitHub) {
68
- // https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions
69
- // https://github.com/OndraM/ci-detector/blob/main/src/Ci/GitHubActions.php
70
- error(`::error file=${k},line=1,title=${i}()::${r}`);
71
- }
72
- else {
73
- error(`${i}() ${fgRed}error${reset}, ${timeFormat(delta)}`);
74
- error(`${fgRed}${r}${reset}`);
41
+ /**
42
+ * Recursively collects all leaf tests reachable from `v` as `[path, entry]`
43
+ * pairs, without running anything. Return-value sub-trees are not walked
44
+ * (that requires execution); only the static object/array/function structure
45
+ * is traversed.
46
+ */
47
+ export const collectTests = (path, throws, v) => {
48
+ const set = parseTestSet(throws, v);
49
+ if (set instanceof Array) {
50
+ return set.flatMap(([ck, cv]) => collectTests([...path, ck], throws || ck === 'throw', cv));
51
+ }
52
+ return [[path, set]];
53
+ };
54
+ const mergeState = (a, b) => ({ time: a.time + b.time, pass: a.pass + b.pass, fail: a.fail + b.fail });
55
+ const zero = { time: 0, pass: 0, fail: 0 };
56
+ const runModule = ({ result, test }) => (k, v) => (ts) => {
57
+ const walk = (path, throws, v) => {
58
+ const effects = collectTests(path, throws, v)
59
+ .map(([testPath, set]) => test(k, testPath, set)
60
+ .step(sr => {
61
+ const { result: [s, r], duration } = sr;
62
+ return result(k, testPath, sr)
63
+ .step(() => {
64
+ if (s === 'ok') {
65
+ if (set.throws) {
66
+ return pure(addPass(duration)(zero));
75
67
  }
68
+ // Walk return-value sub-tree; null marks the call boundary so
69
+ // paths render as e.g. `outer().inner`. throws resets to false.
70
+ return walk([...testPath, null], false, r)
71
+ .step(sub => pure(mergeState(addPass(duration)(zero), sub)));
76
72
  }
77
- else {
78
- ts = addPass(delta)(ts);
79
- log(`${i}() ${fgGreen}ok${reset}, ${timeFormat(delta)}`);
80
- // The result of a function is walked as a fresh sub-tree;
81
- // the parent's `throws` flag does not propagate into it.
82
- ts = next(false)(r)(ts);
83
- }
84
- }
85
- else {
86
- const f = ([k, v]) => ts => {
87
- log(`${i}${k}:`);
88
- ts = next(throws || k === 'throw')(v)(ts);
89
- return ts;
90
- };
91
- ts = fold(f)(ts)(set);
92
- }
93
- return ts;
94
- };
95
- return ts => {
96
- if (isTest(k)) {
97
- log(`testing ${k}`);
98
- ts = test('| ')(false)(v.default)(ts);
99
- // Non-default exports are walked as a sibling test group so
100
- // a test file can spread its tests across multiple named
101
- // exports (see issue 27 in `issues/README.md`). Skip exports
102
- // that parseTestSet would treat as empty (constants, types,
103
- // non-test helpers) to avoid noisy empty entries in output.
104
- const others = Object.fromEntries(Object.entries(v).filter(([key, val]) => key !== 'default' && ((typeof val === 'function' && val.length === 0) ||
105
- (typeof val === 'object' && val !== null))));
106
- if (Object.keys(others).length !== 0) {
107
- ts = test('| ')(false)(others)(ts);
108
- }
109
- }
110
- return ts;
111
- };
73
+ return pure(addFail(duration)(zero));
74
+ });
75
+ }));
76
+ return all(...effects)
77
+ .step(states => pure(states.reduce(mergeState, zero)));
112
78
  };
113
- let ts = { time: 0, pass: 0, fail: 0 };
114
- ts = fold(f)(ts)(Object.entries(moduleMap));
115
- const fgFail = ts.fail === 0 ? fgGreen : fgRed;
116
- log(`${bold}Number of tests: pass: ${fgGreen}${ts.pass}${reset}${bold}, fail: ${fgFail}${ts.fail}${reset}${bold}, total: ${ts.pass + ts.fail}${reset}`);
117
- log(`${bold}Time: ${timeFormat(ts.time)}${reset}`);
118
- return ts.fail !== 0 ? 1 : 0;
79
+ return walk([], false, v)
80
+ .step(delta => pure(mergeState(ts, delta)));
81
+ };
82
+ const { entries } = Object;
83
+ export const runModuleMap = (reporter) => (moduleMap) => {
84
+ const { summary } = reporter;
85
+ const modules = entries(moduleMap).filter(([k]) => isTest(k));
86
+ return modules.reduce((acc, [k, v]) => acc.step(runModule(reporter)(k, v)), pure({ time: 0, pass: 0, fail: 0 }))
87
+ .step(ts => summary(ts.pass, ts.fail, ts.time)
88
+ .step(() => pure(ts.fail !== 0 ? 1 : 0)));
119
89
  };
120
- export const anyLog = (f) => (s) => (state) => {
121
- f(s);
122
- return state;
90
+ export const test = (reporter) => options => loadModuleMap(options.env).step(runModuleMap(reporter));
91
+ const isAlpha = (c) => (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c === '_' || c === '$';
92
+ const isDigit = (c) => c >= '0' && c <= '9';
93
+ export const isInteger = (s) => s.length > 0 && [...s].every(isDigit) && (s === '0' || s[0] !== '0');
94
+ export const isIdentifier = (s) => s.length > 0 && isAlpha(s[0]) && [...s.slice(1)].every(c => isAlpha(c) || isDigit(c));
95
+ const fmtKey = (k) => k === null ? '()'
96
+ : isInteger(k) ? `[${k}]`
97
+ : isIdentifier(k) ? `.${k}`
98
+ : `[${JSON.stringify(k)}]`;
99
+ /**
100
+ * Renders a key chain as a JS property-access expression: identifier keys use
101
+ * dot notation, integer keys use `[N]`, other strings use `["key"]`, and `null`
102
+ * emits `()` to mark a function-call boundary.
103
+ * E.g. `['math', 'add']` → `.math.add`, `['outer', null, 'inner']` → `.outer().inner`.
104
+ */
105
+ export const fmtPath = (path) => path.reduce((acc, k) => acc + fmtKey(k), '');
106
+ /**
107
+ * Formats a fully-qualified test identifier as a JS-like expression, e.g.
108
+ * `import("./math.test.f.ts").add()` or `import("./a.test.f.ts").users[3].name()`.
109
+ * Self-contained per line — suitable for parallel output and as a CLI filter argument.
110
+ */
111
+ export const fmtImport = (file, path) => `import(${JSON.stringify(file)})${fmtPath(path)}()`;
112
+ /**
113
+ * Renders a key chain for terminal output: `| ` per level of depth, followed
114
+ * by the last segment formatted as a bare integer, a bare identifier, or a
115
+ * JSON-quoted string. E.g. `['math', 'add']` → `| | add`,
116
+ * `['a', '0']` → `| | 0`, `['x', 'hello world']` → `| | "hello world"`.
117
+ */
118
+ export const fmtTerm = (path) => {
119
+ const keys = path.flatMap(k => k !== null ? [k] : []);
120
+ const indent = '| '.repeat(keys.length);
121
+ if (keys.length === 0) {
122
+ return `${indent}()`;
123
+ }
124
+ const last = keys[keys.length - 1];
125
+ return `${indent}${isInteger(last) || isIdentifier(last) ? last : JSON.stringify(last)}`;
126
+ };
127
+ /**
128
+ * Percent-encodes characters that GitHub workflow-command property values
129
+ * treat as separators (`%`, `:`, `,`) plus newlines.
130
+ * https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions
131
+ */
132
+ export const ghEscape = (s) => s.replaceAll('%', '%25')
133
+ .replaceAll(':', '%3A')
134
+ .replaceAll(',', '%2C')
135
+ .replaceAll('\r', '%0D')
136
+ .replaceAll('\n', '%0A');
137
+ export const defaultTest = (file, path, { fn, throws }) => sandbox(fn)
138
+ .step(r => pure(throws ? { ...r, result: invert(r.result) } : r));
139
+ const fmtResultLine = (file, path, color, label, duration) => `${fmtImport(file, path)}: ${color}${label}${reset}, ${timeFormat(duration)}`;
140
+ /**
141
+ * The terminal/GitHub reporter used by `fjs t`. Output goes through
142
+ * `csiWrite`, so ANSI styles are stripped on non-TTY streams. When
143
+ * `GITHUB_ACTION` is set, failures are emitted as `::error` workflow
144
+ * annotations instead of colored lines. Exported as a factory so the
145
+ * GitHub format path can be exercised directly from tests.
146
+ */
147
+ export const defaultReporter = (options) => {
148
+ const write = csiWrite(options);
149
+ const line = (w) => {
150
+ const x = write(w);
151
+ return (s) => x(s + '\n');
152
+ };
153
+ const csiLog = line('stdout');
154
+ const csiError = line('stderr');
155
+ const isGitHub = options.env['GITHUB_ACTION'] !== undefined;
156
+ return {
157
+ // https://github.com/OndraM/ci-detector/blob/main/src/Ci/GitHubActions.php
158
+ result: (file, path, { result: [s, v], duration }) => s === 'ok'
159
+ ? csiLog(fmtResultLine(file, path, fgGreen, 'ok', duration))
160
+ : isGitHub
161
+ ? csiError(`::error file=${file},line=1,title=${ghEscape(fmtImport(file, path))}::${ghEscape(String(v))}`)
162
+ : csiError(fmtResultLine(file, path, fgRed, 'error', duration))
163
+ .step(() => csiError(`${fgRed}${v}${reset}`)),
164
+ summary: (pass, fail, time) => {
165
+ const fgFail = fail === 0 ? fgGreen : fgRed;
166
+ return csiLog(`${bold}Number of tests: pass: ${fgGreen}${pass}${reset}${bold}, fail: ${fgFail}${fail}${reset}${bold}, total: ${pass + fail}${reset}`)
167
+ .step(() => csiLog(`${bold}Time: ${timeFormat(time)}${reset}`));
168
+ },
169
+ test: defaultTest,
170
+ };
123
171
  };
124
- export const main = test;
172
+ export const main = options => test(defaultReporter(options))(options);
@@ -1,9 +1,12 @@
1
- import { io } from "../../io/module.js";
2
- import { loadModuleMap2 } from "../module.f.js";
3
- import { isTest, parseTestSet } from "./module.f.js";
1
+ import { io, tryCatch } from "../../io/module.js";
2
+ import { loadModuleMap } from "../module.f.js";
3
+ import { fmtImport, isTest, parseTestSet, runModuleMap } from "./module.f.js";
4
4
  import * as nodeTest from 'node:test';
5
5
  import { asyncImport } from "../../io/module.js";
6
6
  import { fromIo } from "../../io/module.f.js";
7
+ import { pure } from "../../types/effects/module.f.js";
8
+ import { asyncRun } from "../../types/effects/module.js";
9
+ import {} from "../../types/effects/node/module.f.js";
7
10
  const isBun = typeof Bun !== 'undefined';
8
11
  const isPlaywright = typeof process !== 'undefined' && process?.env?.PLAYWRIGHT_TEST !== undefined;
9
12
  const createFramework = (fw) => (prefix, f) => fw.test(prefix, t => f((name, v) => t.test(name, v)));
@@ -16,7 +19,6 @@ const createPlaywrightFramework = async () => {
16
19
  const framework = isPlaywright ? await createPlaywrightFramework() :
17
20
  isBun ? createBunFramework(nodeTest) :
18
21
  createFramework(nodeTest);
19
- const parse = parseTestSet(io.sandbox);
20
22
  const scanModule = (x) => async (subTestRunner) => {
21
23
  let subTests = [x];
22
24
  while (true) {
@@ -27,28 +29,75 @@ const scanModule = (x) => async (subTestRunner) => {
27
29
  subTests = rest;
28
30
  //
29
31
  const [name, value, throws] = first;
30
- const set = parse(throws)(value);
31
- if (typeof set === 'function') {
32
- await subTestRunner(name, () => {
33
- const r = set();
34
- // The result of a function is walked as a fresh sub-tree;
35
- // the parent's `throws` flag does not propagate into it.
36
- subTests = [...subTests, [`${name}()`, r, false]];
37
- });
38
- }
39
- else {
32
+ const set = parseTestSet(throws, value);
33
+ if (set instanceof Array) {
40
34
  for (const [j, y] of set) {
41
35
  const pr = `${name}/${j}`;
42
36
  subTests = [...subTests, [pr, y, throws || j === 'throw']];
43
37
  }
44
38
  }
39
+ else {
40
+ await subTestRunner(name, () => {
41
+ if (set.throws) {
42
+ let threw = false;
43
+ try {
44
+ set.fn();
45
+ }
46
+ catch (_) {
47
+ threw = true;
48
+ }
49
+ if (!threw) {
50
+ throw new Error(`${name}() expected to throw`);
51
+ }
52
+ }
53
+ else {
54
+ const r = set.fn();
55
+ // The result of a function is walked as a fresh sub-tree;
56
+ // the parent's `throws` flag does not propagate into it.
57
+ subTests = [...subTests, [`${name}()`, r, false]];
58
+ }
59
+ });
60
+ }
45
61
  }
46
62
  };
63
+ const noOp = () => pure(undefined);
64
+ const reporter = {
65
+ result: noOp,
66
+ summary: noOp,
67
+ test: (file, path, { throws, fn }) => {
68
+ nodeTest.test(fmtImport(file, path), async (_t) => {
69
+ const [s, r] = tryCatch(fn);
70
+ if (throws === (s === 'ok')) {
71
+ throw r;
72
+ }
73
+ if (!throws) {
74
+ // TODO: add subtests
75
+ }
76
+ });
77
+ return pure({
78
+ result: ['ok', undefined],
79
+ duration: 0,
80
+ });
81
+ }
82
+ };
83
+ const map = {
84
+ // TODO: we use the same algorithm twice. Refactor by creating a `createAll(map)`
85
+ // helper that takes a `map` and returns an `all` function that runs effects
86
+ // according to the map. There could be a problem with circular dependencies,
87
+ // but we can use a lazy function `() => ToAsyncOperationMap<All>` instead od `map`.
88
+ all: async (...effects) => Promise.all(effects.map(asyncRun(map))),
89
+ };
90
+ export const run3 = async () => {
91
+ const fio = fromIo(io);
92
+ const moduleMap = await fio(loadModuleMap(io.process.env));
93
+ const runner = runModuleMap(reporter)(moduleMap);
94
+ await asyncRun(map)(runner);
95
+ };
47
96
  export const run = async () => {
48
- const x = await fromIo(io)(loadModuleMap2(io.process.env));
49
- for (const [i, v] of Object.entries(x)) {
97
+ const moduleMap = await fromIo(io)(loadModuleMap(io.process.env));
98
+ for (const [i, v] of Object.entries(moduleMap)) {
50
99
  if (isTest(i)) {
51
- framework(i, scanModule(['', v.default, false]));
100
+ framework(i, scanModule(['', v, false]));
52
101
  }
53
102
  }
54
103
  };