@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 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
+ }