@panagos/jojoscript 0.7.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.
- package/LICENSE +24 -0
- package/README.md +418 -0
- package/bin/jojo.js +170 -0
- package/package.json +59 -0
- package/src/compiler.js +169 -0
- package/src/index.js +19 -0
- package/src/lexer.js +178 -0
- package/src/match-parser.js +215 -0
- package/src/pipeline-parser.js +418 -0
- package/src/runtime/collections.js +327 -0
- package/src/runtime/index.js +29 -0
- package/src/transforms.js +281 -0
- package/src/type-annotations.js +191 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
This is free and unencumbered software released into the public domain.
|
|
2
|
+
|
|
3
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
4
|
+
distribute this software, either in source code form or as a compiled
|
|
5
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
6
|
+
means.
|
|
7
|
+
|
|
8
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
9
|
+
of this software dedicate any and all copyright interest in the
|
|
10
|
+
software to the public domain. We make this dedication for the benefit
|
|
11
|
+
of the public at large and to the detriment of our heirs and
|
|
12
|
+
successors. We intend this dedication to be an overt act of
|
|
13
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
14
|
+
software under copyright law.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
17
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
19
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
20
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
21
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
23
|
+
|
|
24
|
+
For more information, please refer to <https://unlicense.org>
|
package/README.md
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# JojoScript 0.7.0
|
|
2
|
+
|
|
3
|
+
JojoScript is a deliberately small source-to-source language that adds a
|
|
4
|
+
handful of ergonomic constructs on top of JavaScript while keeping ordinary
|
|
5
|
+
JavaScript valid. The compiler is a regex/token-based transpiler (not a full
|
|
6
|
+
AST), so it stays tiny and every feature composes with plain JavaScript
|
|
7
|
+
around it.
|
|
8
|
+
|
|
9
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history and
|
|
10
|
+
[example/](example/) for a complete runnable application that exercises
|
|
11
|
+
every feature described below.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install jojoscript
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## CLI
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
jojo build <file.jojo|directory> [--out <directory>] [--watch]
|
|
23
|
+
jojo run <file.jojo> [--out <directory>]
|
|
24
|
+
jojo --version
|
|
25
|
+
jojo --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- `jojo build src` compiles every `.jojo` file under `src` to `src/dist/*.js` (or `--out <dir>`).
|
|
29
|
+
- `jojo build src --watch` rebuilds whenever a `.jojo` file under `src` changes.
|
|
30
|
+
- `jojo run app.jojo` compiles a single file and immediately runs it with Node — useful for a fast dev loop.
|
|
31
|
+
- Compiler and filesystem errors are reported as a single friendly line (`jojo: <message>`, exit code 1) instead of a raw stack trace. Unknown flags are rejected.
|
|
32
|
+
|
|
33
|
+
## Language features
|
|
34
|
+
|
|
35
|
+
### Immutable declaration
|
|
36
|
+
|
|
37
|
+
```jojo
|
|
38
|
+
users := getUsers()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
becomes:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
const users = getUsers()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Mutable declaration
|
|
48
|
+
|
|
49
|
+
```jojo
|
|
50
|
+
mutable count := 0
|
|
51
|
+
count++
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
becomes:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
let count = 0
|
|
58
|
+
count++
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Existing JavaScript `const`, `let`, and `var` declarations are untouched.
|
|
62
|
+
|
|
63
|
+
### Destructuring declarations
|
|
64
|
+
|
|
65
|
+
Object and array patterns are supported with `:=` and `mutable :=`. Only the
|
|
66
|
+
declaration head is rewritten — the right-hand side is left completely
|
|
67
|
+
untouched and may span multiple lines.
|
|
68
|
+
|
|
69
|
+
```jojo
|
|
70
|
+
{ id, name } := getUser()
|
|
71
|
+
[head, ...rest] := items
|
|
72
|
+
mutable { count } := state
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
becomes:
|
|
76
|
+
|
|
77
|
+
```js
|
|
78
|
+
const { id, name } = getUser()
|
|
79
|
+
const [head, ...rest] = items
|
|
80
|
+
let { count } = state
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Multi-binding (tuple) declarations
|
|
84
|
+
|
|
85
|
+
A comma-separated list of names before `:=` destructures a comma-separated
|
|
86
|
+
list of values on the right-hand side (single line only):
|
|
87
|
+
|
|
88
|
+
```jojo
|
|
89
|
+
a, b := 1, 2
|
|
90
|
+
mutable x, y := 10, 20
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
becomes:
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
const [a, b] = [1, 2]
|
|
97
|
+
let [x, y] = [10, 20]
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This is a distinct construct from bracket destructuring: use `a, b := 1, 2`
|
|
101
|
+
to bind a fixed list of values, and `[a, b] := getPair()` to destructure a
|
|
102
|
+
single array-producing expression.
|
|
103
|
+
|
|
104
|
+
### Functions
|
|
105
|
+
|
|
106
|
+
Block functions:
|
|
107
|
+
|
|
108
|
+
```jojo
|
|
109
|
+
fn add(a, b) {
|
|
110
|
+
return a + b
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Expression functions:
|
|
115
|
+
|
|
116
|
+
```jojo
|
|
117
|
+
fn double(value) = value * 2
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Async functions:
|
|
121
|
+
|
|
122
|
+
```jojo
|
|
123
|
+
async fn loadUsers() {
|
|
124
|
+
return await getUsers()
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Optional type annotations
|
|
129
|
+
|
|
130
|
+
Parameters and return values can be annotated. Annotations are **never
|
|
131
|
+
checked** — they are stripped from the emitted JavaScript and re-expressed
|
|
132
|
+
as a JSDoc block above the function, so editors/tools that understand JSDoc
|
|
133
|
+
get parameter and return type information without JojoScript needing its own
|
|
134
|
+
type checker.
|
|
135
|
+
|
|
136
|
+
```jojo
|
|
137
|
+
fn add(a: number, b: number): number { return a + b }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
becomes:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
/**
|
|
144
|
+
* @param {number} a
|
|
145
|
+
* @param {number} b
|
|
146
|
+
* @returns {number}
|
|
147
|
+
*/
|
|
148
|
+
function add(a, b) { return a + b; }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Functions with no annotations are left completely untouched.
|
|
152
|
+
|
|
153
|
+
### `match` expression
|
|
154
|
+
|
|
155
|
+
`match` compares a subject against each pattern with `===`, in order, and
|
|
156
|
+
requires a default `_` arm as the last arm. Arms are single expressions
|
|
157
|
+
(not statement blocks).
|
|
158
|
+
|
|
159
|
+
```jojo
|
|
160
|
+
label := match status {
|
|
161
|
+
200 => "OK",
|
|
162
|
+
404 => "Not Found",
|
|
163
|
+
_ => "Unknown"
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
becomes:
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
const label = (() => {
|
|
171
|
+
const __match = status;
|
|
172
|
+
if (__match === (200)) return "OK";
|
|
173
|
+
if (__match === (404)) return "Not Found";
|
|
174
|
+
return "Unknown";
|
|
175
|
+
})()
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Patterns can be arbitrary expressions, which enables range-style matching
|
|
179
|
+
against `true`:
|
|
180
|
+
|
|
181
|
+
```jojo
|
|
182
|
+
category := match true {
|
|
183
|
+
value > 100 => "large",
|
|
184
|
+
value > 10 => "medium",
|
|
185
|
+
_ => "small"
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`match` is a value-equality construct, not structural pattern matching:
|
|
190
|
+
patterns are plain expressions compared with `===`.
|
|
191
|
+
|
|
192
|
+
### Pipelines
|
|
193
|
+
|
|
194
|
+
```jojo
|
|
195
|
+
users
|
|
196
|
+
|> filter(isAdult)
|
|
197
|
+
|> map(normalize)
|
|
198
|
+
|> sort(byName)
|
|
199
|
+
|> take(10)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
becomes nested JavaScript calls:
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
take(sort(map(filter(users, isAdult), normalize), byName), 10)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
A stage can have no arguments (`users |> unique`) or arguments
|
|
209
|
+
(`users |> take(10)`), and normal JavaScript expressions can be pipeline
|
|
210
|
+
inputs, including `await` expressions and pipelines inside `return`
|
|
211
|
+
statements, function bodies, and object literals.
|
|
212
|
+
|
|
213
|
+
#### Placeholder argument
|
|
214
|
+
|
|
215
|
+
By default the piped value is inserted as the stage's first argument. The
|
|
216
|
+
`_` placeholder lets you put it anywhere in the argument list instead:
|
|
217
|
+
|
|
218
|
+
```jojo
|
|
219
|
+
value |> divide(100, _)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
becomes:
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
divide(100, value)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### `catch` / `orElse` control stages
|
|
229
|
+
|
|
230
|
+
`catch` and `orElse` provide inline error handling without leaving pipeline
|
|
231
|
+
syntax. Both wrap the preceding pipeline in a try/catch, compiled as an
|
|
232
|
+
immediately-invoked arrow function:
|
|
233
|
+
|
|
234
|
+
```jojo
|
|
235
|
+
risky() |> catch(error => fallbackFor(error))
|
|
236
|
+
risky() |> orElse(0)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`catch(handler)` calls `handler(error)` and returns its result; `orElse(fallback)`
|
|
240
|
+
returns `fallback` directly. Both require exactly one argument.
|
|
241
|
+
|
|
242
|
+
#### `await` control stage
|
|
243
|
+
|
|
244
|
+
`|> await` unwraps a promise at the end of a pipeline, so an async pipeline
|
|
245
|
+
still reads as a single chain:
|
|
246
|
+
|
|
247
|
+
```jojo
|
|
248
|
+
return ids
|
|
249
|
+
|> mapAsync(id => repository.findById(id))
|
|
250
|
+
|> toArrayAsync
|
|
251
|
+
|> await
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`await`, `catch`, and `orElse` are resolved at compile time and are never
|
|
255
|
+
added to the automatic `jojoscript/runtime` import.
|
|
256
|
+
|
|
257
|
+
### Standard pipeline runtime
|
|
258
|
+
|
|
259
|
+
The following pipeline stages are supplied by `jojoscript/runtime`. The
|
|
260
|
+
compiler automatically generates only the runtime imports that are actually
|
|
261
|
+
used through pipeline syntax (custom/user-defined stages are never
|
|
262
|
+
auto-imported):
|
|
263
|
+
|
|
264
|
+
| Stage | Kind | Description |
|
|
265
|
+
|---|---|---|
|
|
266
|
+
| `map` | lazy | Transform each item |
|
|
267
|
+
| `filter` | lazy | Keep items matching a predicate |
|
|
268
|
+
| `take` | lazy | Keep the first N items |
|
|
269
|
+
| `skip` | lazy | Discard the first N items |
|
|
270
|
+
| `unique` | lazy | Remove duplicates (by `===`) |
|
|
271
|
+
| `flatMap` | lazy | Map then flatten one level |
|
|
272
|
+
| `distinctBy` | lazy | Remove duplicates by a derived key |
|
|
273
|
+
| `tap` | lazy | Observe each item without changing the pipeline (side effects/debugging) |
|
|
274
|
+
| `chunk` | lazy | Group items into fixed-size arrays |
|
|
275
|
+
| `window` | lazy | Yield a sliding view of size N |
|
|
276
|
+
| `zip` | lazy | Pair items from two iterables, stopping at the shorter one |
|
|
277
|
+
| `zipWith` | lazy | Combine items from two iterables with an arity-2 combining function, stopping at the shorter one |
|
|
278
|
+
| `scan` | lazy | Yield a running accumulation (like `reduce`, but yields every step) |
|
|
279
|
+
| `mapAsync` | lazy, async | Like `map`, awaiting an async transform |
|
|
280
|
+
| `filterAsync` | lazy, async | Like `filter`, awaiting an async predicate |
|
|
281
|
+
| `sort` | eager | Materializes the input; sorting needs the complete input |
|
|
282
|
+
| `sortBy` | eager | Like `sort`, ordered by a derived key |
|
|
283
|
+
| `groupBy` | eager | Groups items into a `Map` keyed by a derived key |
|
|
284
|
+
| `partition` | eager | Splits into `[matching, nonMatching]` arrays |
|
|
285
|
+
| `reduce` | terminal | Accumulate to a single value |
|
|
286
|
+
| `toArray` | terminal | Materialize an iterator into an array |
|
|
287
|
+
| `toArrayAsync` | terminal, async | Materialize an async iterator into an array |
|
|
288
|
+
| `count` | terminal | Count items |
|
|
289
|
+
| `first` | terminal | First item, or `undefined` |
|
|
290
|
+
| `find` | terminal | First matching item, or `undefined` |
|
|
291
|
+
| `some` | terminal | Whether any item matches (short-circuits) |
|
|
292
|
+
| `every` | terminal | Whether every item matches (short-circuits) |
|
|
293
|
+
|
|
294
|
+
See [ITERATOR_PROPOSAL.md](ITERATOR_PROPOSAL.md) and [ITERATORS.md](ITERATORS.md)
|
|
295
|
+
for the full laziness/memory model.
|
|
296
|
+
|
|
297
|
+
## JavaScript compatibility
|
|
298
|
+
|
|
299
|
+
JojoScript intentionally leaves ordinary JavaScript alone. You can use:
|
|
300
|
+
|
|
301
|
+
- normal `const`, `let`, and `var`
|
|
302
|
+
- arrow functions
|
|
303
|
+
- classes
|
|
304
|
+
- destructuring (as plain JS assignment, not a `:=` declaration)
|
|
305
|
+
- spread/rest
|
|
306
|
+
- optional chaining
|
|
307
|
+
- nullish coalescing
|
|
308
|
+
- template literals
|
|
309
|
+
- loops
|
|
310
|
+
- try/catch
|
|
311
|
+
- array methods
|
|
312
|
+
- async/await
|
|
313
|
+
- built-in Node modules
|
|
314
|
+
- npm/bare imports
|
|
315
|
+
|
|
316
|
+
Local relative imports receive `.js` after compilation:
|
|
317
|
+
|
|
318
|
+
```jojo
|
|
319
|
+
import { users } from "./users"
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
becomes:
|
|
323
|
+
|
|
324
|
+
```js
|
|
325
|
+
import { users } from "./users.js"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Bare imports remain untouched:
|
|
329
|
+
|
|
330
|
+
```jojo
|
|
331
|
+
import axios from "axios"
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## Runtime package
|
|
335
|
+
|
|
336
|
+
Generated JavaScript imports the runtime using the package subpath:
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
import { map, take } from "jojoscript/runtime"
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
The package exposes this through `package.json` `exports`, so consumers
|
|
343
|
+
install `jojoscript` as a normal dependency. If a file both manually imports
|
|
344
|
+
from `jojoscript/runtime` (for a stage that's only ever called directly,
|
|
345
|
+
never through `|>`) and also uses pipeline syntax with overlapping names,
|
|
346
|
+
the compiler merges the auto-imported names into the existing import
|
|
347
|
+
instead of emitting a second, colliding import declaration.
|
|
348
|
+
|
|
349
|
+
## Example
|
|
350
|
+
|
|
351
|
+
See [example/](example/). It is a complete runnable project covering every
|
|
352
|
+
language feature above plus JavaScript compatibility: a Node/SQLite
|
|
353
|
+
application that fetches posts from a public API, stores them, and prints a
|
|
354
|
+
report, alongside one small focused module (and test) per feature. Every
|
|
355
|
+
module is imported and invoked from `example/src/main.jojo`, so both
|
|
356
|
+
`npm start` and `npm test` exercise the whole example.
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
cd example
|
|
360
|
+
npm install
|
|
361
|
+
npm run build
|
|
362
|
+
npm start
|
|
363
|
+
npm test
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Iterator pipelines
|
|
367
|
+
|
|
368
|
+
Collection pipeline stages are lazy by default. `map`, `filter`, `take`,
|
|
369
|
+
`skip`, `unique`, `flatMap`, `distinctBy`, `tap`, `chunk`, `window`, `zip`,
|
|
370
|
+
`zipWith`, `scan`, `mapAsync`, and `filterAsync` return iterators (or async iterators)
|
|
371
|
+
instead of allocating an intermediate array. Use `toArray`/`toArrayAsync`
|
|
372
|
+
when an array is actually required.
|
|
373
|
+
|
|
374
|
+
```jojo
|
|
375
|
+
const total = users
|
|
376
|
+
|> filter(user => user.active)
|
|
377
|
+
|> map(user => user.age)
|
|
378
|
+
|> reduce((sum, age) => sum + age, 0)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
`sort`, `sortBy`, `groupBy`, and `partition` are intentionally materialization
|
|
382
|
+
boundaries because they need the complete input. See
|
|
383
|
+
[ITERATORS.md](ITERATORS.md) for the complete proposal and memory model.
|
|
384
|
+
|
|
385
|
+
## Editor support
|
|
386
|
+
|
|
387
|
+
See [editors/vscode](editors/vscode) for a VS Code extension providing syntax
|
|
388
|
+
highlighting and a `Format Document` provider for `.jojo` files. It is not
|
|
389
|
+
published to the marketplace; package it with `vsce package` and install the
|
|
390
|
+
resulting `.vsix` locally.
|
|
391
|
+
|
|
392
|
+
## Known limitations
|
|
393
|
+
|
|
394
|
+
- **No source maps.** Stack traces from compiled `dist/*.js` point at the
|
|
395
|
+
generated JavaScript, not the original `.jojo` source.
|
|
396
|
+
- **No `jojo fmt` CLI command.** Formatting is only available through the
|
|
397
|
+
VS Code extension's `Format Document` command, not the `jojo` CLI.
|
|
398
|
+
- **No regex-literal awareness in the lexer.** A regex containing `{`, `}`,
|
|
399
|
+
`(`, or `)` can confuse brace/paren-depth-sensitive transforms (pipelines,
|
|
400
|
+
`match`, destructuring, type annotations). Prefer `new RegExp(...)` in code
|
|
401
|
+
paths that also use these constructs on the same line, or keep such regexes
|
|
402
|
+
simple.
|
|
403
|
+
- **Pipeline statement boundaries need a blank line (or a semicolon) between
|
|
404
|
+
consecutive `:=` statements that each end in a bare pipeline stage**, e.g.:
|
|
405
|
+
|
|
406
|
+
```jojo
|
|
407
|
+
a := x |> map(f)
|
|
408
|
+
b := y |> map(g) // may be parsed as a continuation of the line above
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Add a blank line (or a trailing `;`) between them.
|
|
412
|
+
- **`match` subject expressions should be simple** (a single identifier,
|
|
413
|
+
member access, or a fully-parenthesized expression) and appear on the same
|
|
414
|
+
statement as the `{ ... }` block.
|
|
415
|
+
- **Multi-binding declarations (`a, b := 1, 2`) are single-line only** and
|
|
416
|
+
always wrap the right-hand side in `[...]`; use `[a, b] := expr()` to
|
|
417
|
+
destructure a single expression instead.
|
|
418
|
+
|
package/bin/jojo.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { spawn } from 'node:child_process'
|
|
7
|
+
|
|
8
|
+
import { compileFile, compileDirectory } from '../src/compiler.js'
|
|
9
|
+
|
|
10
|
+
const PACKAGE_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
11
|
+
|
|
12
|
+
function packageVersion() {
|
|
13
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'))
|
|
14
|
+
return pkg.version
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function usage() {
|
|
18
|
+
console.log(`JojoScript CLI v${packageVersion()}
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
jojo build <file.jojo|directory> [--out <directory>] [--watch]
|
|
22
|
+
jojo run <file.jojo> [--out <directory>]
|
|
23
|
+
jojo --version
|
|
24
|
+
jojo --help
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
jojo build src
|
|
28
|
+
jojo build src/main.jojo
|
|
29
|
+
jojo build src --out dist
|
|
30
|
+
jojo build src --watch
|
|
31
|
+
jojo run src/main.jojo
|
|
32
|
+
`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
* All CLI-facing failures (bad arguments, missing files, compiler syntax
|
|
37
|
+
* errors) are surfaced as a single friendly `jojo: <message>` line instead
|
|
38
|
+
* of a raw stack trace.
|
|
39
|
+
*/
|
|
40
|
+
function fail(message) {
|
|
41
|
+
throw new Error(message)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseFlags(args, startIndex) {
|
|
45
|
+
let out = null
|
|
46
|
+
let watch = false
|
|
47
|
+
|
|
48
|
+
for (let i = startIndex; i < args.length; i++) {
|
|
49
|
+
const arg = args[i]
|
|
50
|
+
|
|
51
|
+
if (arg === '--out') {
|
|
52
|
+
const value = args[++i]
|
|
53
|
+
if (!value) fail('--out requires a directory')
|
|
54
|
+
out = path.resolve(value)
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (arg === '--watch') {
|
|
59
|
+
watch = true
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fail(`Unknown option: ${arg}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { out, watch }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function build(input, out) {
|
|
70
|
+
const resolvedInput = path.resolve(input)
|
|
71
|
+
|
|
72
|
+
if (!fs.existsSync(resolvedInput)) {
|
|
73
|
+
fail(`Not found: ${resolvedInput}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const isDirectory = fs.statSync(resolvedInput).isDirectory()
|
|
77
|
+
|
|
78
|
+
const result = isDirectory
|
|
79
|
+
? compileDirectory(resolvedInput, out)
|
|
80
|
+
: [compileFile(resolvedInput, out)]
|
|
81
|
+
|
|
82
|
+
for (const item of result) {
|
|
83
|
+
console.log(`Compiled ${item.input} -> ${item.output}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function watch(input, out) {
|
|
90
|
+
const resolvedInput = path.resolve(input)
|
|
91
|
+
console.log(`Watching ${resolvedInput} for changes (Ctrl+C to stop)...`)
|
|
92
|
+
|
|
93
|
+
const rebuild = () => {
|
|
94
|
+
try {
|
|
95
|
+
build(input, out)
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error(`jojo: ${error.message}`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
rebuild()
|
|
102
|
+
|
|
103
|
+
fs.watch(resolvedInput, { recursive: true }, (_, filename) => {
|
|
104
|
+
if (filename && !filename.endsWith('.jojo')) return
|
|
105
|
+
console.log(`\nChange detected${filename ? `: ${filename}` : ''}, rebuilding...`)
|
|
106
|
+
rebuild()
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function run(input, out) {
|
|
111
|
+
const resolvedInput = path.resolve(input)
|
|
112
|
+
|
|
113
|
+
if (!fs.existsSync(resolvedInput) || fs.statSync(resolvedInput).isDirectory()) {
|
|
114
|
+
fail('`jojo run` requires a single .jojo file, not a directory')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const [{ output }] = build(input, out)
|
|
118
|
+
|
|
119
|
+
const child = spawn(process.execPath, [output], { stdio: 'inherit' })
|
|
120
|
+
child.on('exit', code => process.exit(code ?? 0))
|
|
121
|
+
child.on('error', error => fail(error.message))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function main() {
|
|
125
|
+
const args = process.argv.slice(2)
|
|
126
|
+
|
|
127
|
+
if (!args.length || args[0] === '--help' || args[0] === '-h') {
|
|
128
|
+
usage()
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (args[0] === '--version' || args[0] === '-v') {
|
|
133
|
+
console.log(packageVersion())
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const command = args[0]
|
|
138
|
+
|
|
139
|
+
if (command !== 'build' && command !== 'run') {
|
|
140
|
+
usage()
|
|
141
|
+
fail(`Unknown command: ${command}`)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!args[1] || args[1].startsWith('-')) {
|
|
145
|
+
fail(`\`jojo ${command}\` requires a file or directory argument`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const input = args[1]
|
|
149
|
+
const { out, watch: shouldWatch } = parseFlags(args, 2)
|
|
150
|
+
|
|
151
|
+
if (command === 'run') {
|
|
152
|
+
if (shouldWatch) fail('--watch is not supported with `jojo run`, use `jojo build --watch` instead')
|
|
153
|
+
run(input, out)
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (shouldWatch) {
|
|
158
|
+
watch(input, out)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
build(input, out)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
main()
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(`jojo: ${error.message}`)
|
|
169
|
+
process.exit(1)
|
|
170
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@panagos/jojoscript",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "A tiny JavaScript-compatible language layer with Jojo syntax and pipelines.",
|
|
5
|
+
"license": "Unlicense",
|
|
6
|
+
"type": "module",
|
|
7
|
+
|
|
8
|
+
"bin": {
|
|
9
|
+
"jojo": "./bin/jojo.js"
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./runtime": {
|
|
17
|
+
"import": "./src/runtime/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
"files": [
|
|
22
|
+
"bin/",
|
|
23
|
+
"src/",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18"
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
"scripts": {
|
|
33
|
+
"test": "node --test test/*.test.js",
|
|
34
|
+
"build:example": "npm --prefix example install && npm --prefix example run build",
|
|
35
|
+
"run:example": "npm --prefix example run start"
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/panagos/jojoscript.git"
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
"homepage": "https://github.com/panagos/jojoscript",
|
|
44
|
+
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/panagos/jojoscript/issues"
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
"keywords": [
|
|
50
|
+
"jojoscript",
|
|
51
|
+
"javascript",
|
|
52
|
+
"language",
|
|
53
|
+
"compiler",
|
|
54
|
+
"pipeline",
|
|
55
|
+
"functional-programming",
|
|
56
|
+
"iterators",
|
|
57
|
+
"lazy-iteration"
|
|
58
|
+
]
|
|
59
|
+
}
|