Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,30 @@ history.

## Unreleased

- **BREAKING CHANGES:** `fjs/media/type` migrates from authored
TypeScript (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`), splitting
`DetectState`, `DetectMeta`, and the internal `_Signature`/`_MagicState`/
`_Utf8Detect` types 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` migrates alongside it
[#1493](https://github.com/functionalscript/functionalscript/pull/1493)
- **BREAKING CHANGES:** `fjs/ci/module.f.ts` migrates from authored
TypeScript (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`), splitting
the `Setup` type 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
[#1493](https://github.com/functionalscript/functionalscript/pull/1493)
- **BREAKING CHANGES:** `fjs/ci/node` migrates from authored
TypeScript (`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`) — no local
types to split. `proof.f.ts` migrates alongside it. Importers must
use the `.f.mjs` specifier
[#1493](https://github.com/functionalscript/functionalscript/pull/1493)
- **BREAKING CHANGES:** `fjs/ci/nix` migrates from authored TypeScript
(`.f.ts`) to JSDoc-typed JavaScript (`.f.mjs`), splitting the
`NixJob` type 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
[#1493](https://github.com/functionalscript/functionalscript/pull/1493)
- **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
Expand Down
2 changes: 1 addition & 1 deletion fjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ cases. See [djs/README.md](djs/README.md) for the accepted subset.
fjs ci
```

`fjs ci` runs the built-in CI generator from `fjs/ci/module.f.ts`, writing
`fjs ci` runs the built-in CI generator from `fjs/ci/module.f.mjs`, writing
`.github/workflows/ci.yml`. It is the standard entry point for projects that want
FunctionalScript's default workflow. Projects with custom CI setup code should keep
using `fjs run <custom-ci-module>`, so their module can call `ci(setup)` with its own
Expand Down
17 changes: 9 additions & 8 deletions fjs/ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@ canonical Node job under `nix/generated/`.

## Files

- `module.f.ts` — the top-level pipeline definition. Exports `ci(setup: Setup)` which
returns an `Effect<NodeOp, number>` that writes the workflow file. Rust support is
detected automatically by checking for `Cargo.toml` at the repository root via the
`access` effect.
- `module.f.mjs` — the top-level pipeline definition. Exports `ci(setup: Setup)`
(`Setup` in `types.ts`) which returns an `Effect<NodeOp, number>` that writes
the workflow file. Rust support is detected automatically by checking for
`Cargo.toml` at the repository root via the `access` effect.
- `proof.f.ts` — property-based proofs for the CI generator (Rust/no-Rust job presence,
per-OS extra steps).
- `common/module.f.ts` — shared RTTI schemas and types (`Step`, `Job`, `Jobs`,
`GitHubAction`, `MetaStep`, `Os`, `Architecture`), and step-builder helpers
(`test`, `install`, `uses`).
- `config/module.f.mjs` — runner image matrix (OS × architecture → GitHub-hosted image name) and pinned tool/package versions, including the FunctionalScript package version used by generated smoke tests and the exact Nixpkgs commit the generated flakes pin.
- `nix/module.f.ts` — writes one self-contained `nix/generated/<job>/flake.nix`
per declared job, using the Nix eDSL in `fjs/media/nix`.
- `node/module.f.ts` — Node.js job steps: platform smoke tests, canonical
- `nix/module.f.mjs` — writes one self-contained `nix/generated/<job>/flake.nix`
per declared job (`NixJob` in `types.ts`), using the Nix eDSL in `fjs/media/nix`.
- `node/module.f.mjs` — Node.js job steps: platform smoke tests, canonical
per-version jobs, coverage, package checks, and the Node flake declarations.
`proof.f.mjs` — its property-based proofs.
- `rust/module.f.mjs` — Rust toolchain setup and `cargo` build/test steps.
- `deno/module.f.mjs` — Deno runtime steps.
- `bun/module.f.mjs` — Bun runtime steps.
Expand All @@ -42,7 +43,7 @@ plain text built from the pinned commit in `config/module.f.mjs`.
### Generated Nix environments

Each canonical Node job declares a system and its Nixpkgs package attribute in
`node/module.f.ts` (`nodeNixJobs`), and `nix/module.f.ts` writes it out as one
`node/module.f.mjs` (`nodeNixJobs`), and `nix/module.f.mjs` writes it out as one
static `flake.nix` exposing `devShells.<system>.default`. Node 22 also declares a
job-local `shellHook` that points `npm install -g` at `$HOME/.npm-global`, so the
installed `fjs` stays on `PATH` for the rest of the same `nix develop` invocation.
Expand Down
50 changes: 24 additions & 26 deletions fjs/ci/module.f.ts → fjs/ci/module.f.mjs
Original file line number Diff line number Diff line change
@@ -1,37 +1,33 @@
/**
* Continuous integration helper commands for repository automation tasks.
*
* See `./types.ts` for the `Setup` type-level API.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update stale CI module paths in the README

After these modules move to .f.mjs and Setup moves to types.ts, fjs/ci/README.md:10-22,45-46 still directs readers to the deleted module.f.ts, node/module.f.ts, and nix/module.f.ts files and omits the new type companion. Anyone following the repository's primary CI documentation therefore reaches nonexistent paths; update those entries as part of the migration.

Useful? React with 👍 / 👎.

*
* @module
*/

import { mapStep, step } from '../effects/module.f.mjs'
import type { Effect } from '../effects/types.ts'
/** @import { Effect } from '../effects/types.ts' */
import { access, writeUtf8File } from '../effects/node/module.f.mjs'
import type { NodeOp } from '../effects/node/types.ts'
/** @import { NodeOp } from '../effects/node/types.ts' */
import { functionalscript, images } from './config/module.f.mjs'
import {
architecture,
os,
toSteps,
ubuntuArm
} from './common/module.f.mjs'
import type {
Architecture,
GitHubAction,
Job,
Jobs,
MetaStep,
Os,
} from './common/types.ts'
/** @import { Architecture, GitHubAction, Job, Jobs, MetaStep, Os } from './common/types.ts' */
import { rustPlatformSteps, rustWasmSteps } from './rust/module.f.mjs'
import { nodeMainSteps, nodeNixJobs, nodeNixVersionSteps, nodeVersionJobs } from './node/module.f.ts'
import { nixFlakes, nixInstall, type NixJob } from './nix/module.f.ts'
import { nodeMainSteps, nodeNixJobs, nodeNixVersionSteps, nodeVersionJobs } from './node/module.f.mjs'
import { nixFlakes, nixInstall } from './nix/module.f.mjs'
/** @import { NixJob } from './nix/types.ts' */
import { bunSteps } from './bun/module.f.mjs'
import { denoSteps } from './deno/module.f.mjs'
/** @import { Setup } from './types.ts' */

const job = (
rust: boolean,
nodeExtra: readonly MetaStep[],
) => (o: Os) => (a: Architecture): readonly [string, Job] => {
/** @type {(rust: boolean, nodeExtra: readonly MetaStep[]) => (o: Os) => (a: Architecture) => readonly [string, Job]} */
const job = (rust, nodeExtra) => o => a => {
const id = `${o}-${a}`
const image = images[o][a]
const result = [
Expand All @@ -42,34 +38,36 @@ const job = (
return [id, { 'runs-on': image, steps: toSteps(result) }]
}

export type Setup = {
readonly nodeExtra: (os: Os) => readonly MetaStep[],
}

// Every generated flake, across all job families that own one.
const nixJobs: readonly NixJob[] = nodeNixJobs
/** @type {readonly NixJob[]} */
const nixJobs = nodeNixJobs

// Temporary: proves the not-yet-migrated flakes still evaluate. Removed once
// the canonical Node jobs check their own flake by running through it.
const nixFlakeJob: Job = ubuntuArm([nixInstall, ...nodeNixVersionSteps])
/** @type {Job} */
const nixFlakeJob = ubuntuArm([nixInstall, ...nodeNixVersionSteps])

const canonicalJobs = (rust: boolean): Jobs => ({
/** @type {(rust: boolean) => Jobs} */
const canonicalJobs = rust => ({
...(rust ? { wasm: ubuntuArm(rustWasmSteps) } : {}),
deno: ubuntuArm(denoSteps(functionalscript)),
bun: ubuntuArm(bunSteps(functionalscript)),
...nodeVersionJobs(functionalscript),
'nix-flakes': nixFlakeJob,
})

export const ci = ({ nodeExtra }: Setup): Effect<NodeOp, number> => step(
/** @type {(setup: Setup) => Effect<NodeOp, number>} */
export const ci = ({ nodeExtra }) => step(
access('Cargo.toml'),
result => {
const rust = result[0] === 'ok'
const jobs: Jobs = {
/** @type {Jobs} */
const jobs = {
...Object.fromEntries(os.flatMap(o => architecture.map(job(rust, nodeExtra(o))(o)))),
...canonicalJobs(rust),
}
const gha: GitHubAction = {
/** @type {GitHubAction} */
const gha = {
name: 'CI',
on: {
pull_request: {},
Expand Down
68 changes: 37 additions & 31 deletions fjs/ci/nix/module.f.ts → fjs/ci/nix/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,33 @@
* readable on purpose: no job selection, no shared Nix modules, no helper
* libraries.
*
* See `./types.ts` for the `NixJob` type-level API.
*
* @module
*/

import { forEachStep, mapStep, pure, step } from '../../effects/module.f.mjs'
import type { Effect } from '../../effects/types.ts'
/** @import { Effect } from '../../effects/types.ts' */
import { mkdir, writeUtf8File } from '../../effects/node/module.f.mjs'
import type { Mkdir, WriteFile } from '../../effects/node/types.ts'
/** @import { Mkdir, WriteFile } from '../../effects/node/types.ts' */
import { nixToString } from '../../media/nix/module.f.mjs'
import type { Expression } from '../../media/nix/types.ts'
/** @import { Expression } from '../../media/nix/types.ts' */
import { fromUndefined, unwrap as unwrapNullable } from '../../types/nullable/module.f.mjs'
import { unwrap } from '../../types/result/module.f.mjs'
import { install, test, uses } from '../common/module.f.mjs'
import type { MetaStep } from '../common/types.ts'
/** @import { MetaStep } from '../common/types.ts' */
import { nixpkgs } from '../config/module.f.mjs'

/** A CI job's development environment, one generated flake each. */
export type NixJob = {
/** Generated directory name under `nix/generated`, matching the CI job id. */
readonly id: string
/** Nix system of the job's runner, e.g. `aarch64-linux`. */
readonly system: string
/** Nixpkgs attribute names made available in the job's shell. */
readonly packages: readonly string[]
/** Job-local shell initialization, when the job needs one. */
readonly shellHook?: string
}
/** @import { NixJob } from './types.ts' */

/** Directory owned by this generator. */
export const generatedDirectory = 'nix/generated' as const
export const generatedDirectory = /** @type {const} */ ('nix/generated')

const { commit } = nixpkgs

const url = `github:NixOS/nixpkgs/${commit}`

const flake = ({ system, packages, shellHook }: NixJob): Expression => ['set',
/** @type {(job: NixJob) => Expression} */
const flake = ({ system, packages, shellHook }) => ['set',
['=', ['inputs', 'nixpkgs', 'url'], url],
['=', ['outputs'], ['lambda',
['open-set-pattern', 'nixpkgs'],
Expand All @@ -54,10 +47,10 @@ const flake = ({ system, packages, shellHook }: NixJob): Expression => ['set',
['apply',
['ref', 'pkgs', 'mkShell'],
['set',
['=', ['packages'], ['list', ...packages.map(p => ['ref', 'pkgs', p] as const)]],
['=', ['packages'], ['list', ...packages.map(p => /** @type {const} */ (['ref', 'pkgs', p]))]],
...(shellHook === undefined
? []
: [['=', ['shellHook'], ['indented-string', shellHook]] as const])
: [/** @type {const} */ (['=', ['shellHook'], ['indented-string', shellHook]])])
]
]
]]
Expand All @@ -72,11 +65,14 @@ const flake = ({ system, packages, shellHook }: NixJob): Expression => ['set',
* is written here — a job only contributes attribute names and strings, which
* are quoted when they are not identifiers. The unwrap is therefore a totality
* assertion, not an input check.
*
* @type {(job: NixJob) => string}
*/
export const flakeText = (job: NixJob): string =>
export const flakeText = job =>
unwrapNullable(fromUndefined(nixToString(flake(job))))

const writeFlake = (job: NixJob): Effect<Mkdir | WriteFile, void> => {
/** @type {(job: NixJob) => Effect<Mkdir | WriteFile, void>} */
const writeFlake = job => {
const directory = `${generatedDirectory}/${job.id}`
const created = mapStep(mkdir(directory, { recursive: true }), unwrap)
const written = step(
Expand All @@ -85,26 +81,32 @@ const writeFlake = (job: NixJob): Effect<Mkdir | WriteFile, void> => {
return mapStep(written, unwrap)
}

/** Writes one generated flake per job. */
export const nixFlakes = (jobs: readonly NixJob[]): Effect<Mkdir | WriteFile, void> =>
/**
* Writes one generated flake per job.
*
* @type {(jobs: readonly NixJob[]) => Effect<Mkdir | WriteFile, void>}
*/
export const nixFlakes = jobs =>
forEachStep(pure(jobs), writeFlake)

/** Path a workflow passes to `nix develop`, for the job of the given id. */
export const flakePath = (id: string): string => `./${generatedDirectory}/${id}`
export const flakePath = /** @type {(id: string) => string} */ (id => `./${generatedDirectory}/${id}`)

/** Installs Nix, with `nix-command` and `flakes` enabled by the action's defaults. */
export const nixInstall: MetaStep = install(uses('cachix/install-nix-action'))
export const nixInstall = install(uses('cachix/install-nix-action'))

/** Runs one command inside a job's generated development shell. */
export const nixDevelop = (id: string, command: string): string =>
`nix develop ${flakePath(id)} --command ${command}`
export const nixDevelop = /** @type {(id: string, command: string) => string} */
((id, command) => `nix develop ${flakePath(id)} --command ${command}`)

/**
* Wraps a string so a POSIX shell reproduces it exactly. Single quotes protect
* every other character, so only the quote itself needs handling: leave the
* literal, reopen it, and escape the quote outside (`'` becomes `'\''`).
*
* @type {(value: string) => string}
*/
const singleQuoted = (value: string): string =>
const singleQuoted = value =>
`'${value.replaceAll("'", "'\\''")}'`

/**
Expand All @@ -114,8 +116,10 @@ const singleQuoted = (value: string): string =>
*
* The commands are a shell script, joined so a failure stops the rest, and are
* quoted as one argument — a command may contain quotes of its own.
*
* @type {(id: string, commands: readonly string[]) => string}
*/
export const nixDevelopAll = (id: string, commands: readonly string[]): string =>
export const nixDevelopAll = (id, commands) =>
nixDevelop(id, `bash -euo pipefail -c ${singleQuoted(commands.join(' && '))}`)

/**
Expand All @@ -124,6 +128,8 @@ export const nixDevelopAll = (id: string, commands: readonly string[]): string =
* already determines the version, so this is the only place the expectation
* is stated — the generated flakes stay declarative instead of carrying an
* `assert` that restates the commit they pin.
*
* @type {(id: string, version: string) => MetaStep}
*/
export const nixVersionCheckStep = (id: string, version: string): MetaStep =>
export const nixVersionCheckStep = (id, version) =>
test({ run: `test "$(${nixDevelop(id, 'node --version')})" = v${version}` })
6 changes: 3 additions & 3 deletions fjs/ci/nix/proof.f.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { step } from '../../effects/module.f.mjs'
import { readUtf8File } from '../../effects/node/module.f.mjs'
import { emptyState, virtual } from '../../effects/node/virtual/module.f.ts'
import { nixpkgs } from '../config/module.f.mjs'
import { nodeNixJobs } from '../node/module.f.ts'
import { nodeNixJobs } from '../node/module.f.mjs'
import {
flakePath,
flakeText,
Expand All @@ -17,8 +17,8 @@ import {
nixDevelopAll,
nixFlakes,
nixInstall,
type NixJob,
} from './module.f.ts'
} from './module.f.mjs'
import type { NixJob } from './types.ts'

const { commit } = nixpkgs

Expand Down
17 changes: 17 additions & 0 deletions fjs/ci/nix/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Types for generated CI Nix flakes.
*
* @module
*/

/** A CI job's development environment, one generated flake each. */
export type NixJob = {
/** Generated directory name under `nix/generated`, matching the CI job id. */
readonly id: string
/** Nix system of the job's runner, e.g. `aarch64-linux`. */
readonly system: string
/** Nixpkgs attribute names made available in the job's shell. */
readonly packages: readonly string[]
/** Job-local shell initialization, when the job needs one. */
readonly shellHook?: string
}
Loading
Loading