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
3 changes: 3 additions & 0 deletions changelog/unreleased/1577.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- **BREAKING CHANGES:** `djs`: `fjs compile` exits with code `1` instead of `0`
when the input cannot be read or fails to parse, so a failed compile is
detectable from the exit status
13 changes: 11 additions & 2 deletions fjs/djs/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,16 @@ import { writeUtf8File, error } from '../effects/node/module.f.mjs'

/** @typedef {ReadFile | WriteFile | Write} _CompileOp */

/** @type {(args: readonly string[]) => Effect<_CompileOp, number>} */
/**
* Compiles the DJS module `args[0]` into `args[1]`, serializing as a JSON tree
* when the output name ends with `.json` and as a module otherwise.
*
* Returns the process exit code: `0` once the output file is written, `1` on
* every failure — too few arguments, a missing input file, or a parse error —
* so a caller can detect a failed compile from the exit status alone.
*
* @type {(args: readonly string[]) => Effect<_CompileOp, number>}
*/
export const compile = args => {
if (args.length < 2) {
return step(
Expand All @@ -35,7 +44,7 @@ export const compile = args => {
const metadata = result[1].metadata
return step(
error(`${metadata?.path}:${metadata?.line}:${metadata?.column} - error: ${result[1].message}`),
() => pure(0))
() => pure(1))
}
const content = outputFileName.endsWith('.json')
? stringifyAsTree(sort)(result[1])
Expand Down
6 changes: 4 additions & 2 deletions fjs/djs/proof.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,15 @@ export const proof = {
},
fileNotFound: () => {
const [state, code] = virtual(emptyState)(compile(['missing.f.js', 'output.f.js']))
assertEq(code, 0)
assertEq(code, 1)
assert(state.stderr.includes('file not found'), state.stderr)
assertEq(state.root['output.f.js'], undefined)
},
parseError: () => {
const root = { 'bad.f.js': [utf8('export default @')] }
const [state, code] = virtual({ ...emptyState, root })(compile(['bad.f.js', 'output.f.js']))
assertEq(code, 0)
assertEq(code, 1)
assert(state.stderr !== '', 'expected error output')
assertEq(state.root['output.f.js'], undefined)
},
}
43 changes: 0 additions & 43 deletions fjs/djs/todo/compile-error-exit-code.md

This file was deleted.

62 changes: 62 additions & 0 deletions fjs/djs/todo/parse-error-location-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
## parse-error-location-format. `compile`: `undefined:undefined:undefined` when a `ParseError` has no metadata

**Priority:** P4
**Status:** open

### Problem

`compile` in `fjs/djs/module.f.mjs` formats every `ParseError` as
`<path>:<line>:<column> - error: <message>`, reading the three fields off
`result[1].metadata`:

```js
const metadata = result[1].metadata
return step(
error(`${metadata?.path}:${metadata?.line}:${metadata?.column} - error: ${result[1].message}`),
() => pure(1))
```

`ParseError.metadata` is `TokenMetadata | null`, and the transpiler raises two
errors with no token to point at — `file not found` (`transpiler/module.f.mjs:41`)
and `circular dependency` (`:80`) — both of which carry `metadata: null`. The
optional chaining then prints the literal string `undefined` three times:

```sh
$ fjs compile nope.f.mjs out.mjs
undefined:undefined:undefined - error: file not found
```

The exit code is correct (`1`); only the location prefix is wrong. It is noise
for a human and a trap for anything parsing the line as `path:line:column`.

### Proposal

Emit the prefix only when there is a location to report:

```js
const { metadata, message } = result[1]
const location = metadata === null
? ''
: `${metadata.path}:${metadata.line}:${metadata.column} - `
return step(error(`${location}error: ${message}`), () => pure(1))
```

A metadata-less error then reads `error: file not found`. An alternative worth
weighing first: give these two errors real metadata — the importing module's
path is known at both sites — which fixes the message *and* tells the user which
import failed. That is the better output but a larger change, since
`TokenMetadata` also wants a line and column.

### Tasks

- [ ] Pick one of the two shapes above and implement it.
- [ ] Assert the exact `stderr` text in `fjs/djs/proof.f.mjs`'s `fileNotFound`,
and cover the metadata-carrying branch too.
- [ ] `npx tsc` clean; `fjs t` passes.

### Related

- `fjs/djs/module.f.mjs` — the formatting site.
- `fjs/djs/transpiler/module.f.mjs:41`, `:80` — the two `metadata: null` errors.
- Split out of the `compile` exit-code issue, which fixed the exit status of
this same branch and left the cosmetic half open.
Loading