@panagos/jojoscript 0.7.1 → 0.9.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/package.json CHANGED
@@ -1,59 +1,50 @@
1
- {
2
- "name": "@panagos/jojoscript",
3
- "version": "0.7.1",
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
- }
1
+ {
2
+ "name": "@panagos/jojoscript",
3
+ "version": "0.9.0",
4
+ "description": "A tiny JavaScript-compatible language layer with Jojo syntax and pipelines.",
5
+ "license": "Unlicense",
6
+ "type": "module",
7
+ "bin": {
8
+ "jojo": "./bin/jojo.js"
9
+ },
10
+ "exports": {
11
+ ".": {
12
+ "import": "./src/index.js"
13
+ },
14
+ "./runtime": {
15
+ "import": "./src/runtime/index.js"
16
+ }
17
+ },
18
+ "files": [
19
+ "bin/",
20
+ "src/",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "scripts": {
28
+ "test": "node --test test/*.test.js",
29
+ "build:example": "npm --prefix example install && npm --prefix example run build",
30
+ "run:example": "npm --prefix example run start"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/panagos/jojoscript.git"
35
+ },
36
+ "homepage": "https://github.com/panagos/jojoscript",
37
+ "bugs": {
38
+ "url": "https://github.com/panagos/jojoscript/issues"
39
+ },
40
+ "keywords": [
41
+ "jojoscript",
42
+ "javascript",
43
+ "language",
44
+ "compiler",
45
+ "pipeline",
46
+ "functional-programming",
47
+ "iterators",
48
+ "lazy-iteration"
49
+ ]
50
+ }
package/src/compiler.js CHANGED
@@ -1,186 +1,227 @@
1
- import fs from 'node:fs'
2
- import path from 'node:path'
3
- import { fileURLToPath } from 'node:url'
4
-
5
- import {
6
- transformPipelines,
7
- findPipelineStdlibFunctions
8
- } from './pipeline-parser.js'
9
-
10
- import {
11
- transformMutableDeclarations,
12
- transformConstDeclarations,
13
- transformMultiBindingDeclarations,
14
- transformDestructuringDeclarations,
15
- transformFunctionDeclarations,
16
- transformExpressionFunctions,
17
- transformLocalImports
18
- } from './transforms.js'
19
-
20
- import { transformMatchExpressions } from './match-parser.js'
21
- import { transformTypeAnnotations } from './type-annotations.js'
22
-
23
- /*
24
- * The auto-generated runtime import must use whatever name this package is
25
- * actually installed under (e.g. a scoped fork like "@me/jojoscript"), not a
26
- * hardcoded literal, so it's derived from this package's own package.json
27
- * instead of being hardcoded.
28
- */
29
- const PACKAGE_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
30
- const OWN_PACKAGE_NAME = JSON.parse(
31
- fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')
32
- ).name
33
- const JOJO_RUNTIME = `${OWN_PACKAGE_NAME}/runtime`
34
-
35
- export function compile(source) {
36
- let result = source
37
-
38
- /*
39
- * Optional type annotations, stripped and re-expressed as JSDoc before
40
- * anything else touches `fn` declarations.
41
- */
42
- result = transformTypeAnnotations(result)
43
-
44
- /*
45
- * Jojo functions.
46
- */
47
- result = transformExpressionFunctions(result)
48
- result = transformFunctionDeclarations(result)
49
-
50
- /*
51
- * `match` expressions.
52
- */
53
- result = transformMatchExpressions(result)
54
-
55
- /*
56
- * Jojo declarations.
57
- */
58
- result = transformMultiBindingDeclarations(result)
59
- result = transformDestructuringDeclarations(result)
60
- result = transformMutableDeclarations(result)
61
- result = transformConstDeclarations(result)
62
-
63
- /*
64
- * Remember which pipeline stages need the Jojo runtime before
65
- * the pipeline syntax itself is removed.
66
- */
67
- const pipelineStdlibFunctions =
68
- findPipelineStdlibFunctions(result)
69
-
70
- /*
71
- * Pipelines.
72
- */
73
- result = transformPipelines(result)
74
-
75
- /*
76
- * ES module imports.
77
- */
78
- result = transformLocalImports(result)
79
-
80
- /*
81
- * Generated JavaScript imports the runtime as a normal package.
82
- * This makes compiled applications portable and avoids globals. If the
83
- * source already has a manual `jojoscript/runtime` import (e.g. for a
84
- * stdlib function that is only ever called directly, never through a
85
- * pipeline stage), the auto-imported names are merged into it instead
86
- * of creating a second, colliding import declaration.
87
- */
88
- if (pipelineStdlibFunctions.length) {
89
- result = mergeRuntimeImport(result, pipelineStdlibFunctions)
90
- }
91
-
92
- return result.endsWith('\n') ? result : result + '\n'
93
- }
94
-
95
- const RUNTIME_IMPORT_PATTERN = new RegExp(
96
- `import\\s*\\{([^}]*)\\}\\s*from\\s*(["'])${escapeRegExp(JOJO_RUNTIME)}\\2`
97
- )
98
-
99
- function escapeRegExp(value) {
100
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
101
- }
102
-
103
- function mergeRuntimeImport(source, stdlibFunctions) {
104
- const existing = source.match(RUNTIME_IMPORT_PATTERN)
105
-
106
- if (!existing) {
107
- return `import { ${stdlibFunctions.join(', ')} } from "${JOJO_RUNTIME}"\n` + source
108
- }
109
-
110
- const existingNames = existing[1].split(',').map(name => name.trim()).filter(Boolean)
111
- const merged = [...new Set([...existingNames, ...stdlibFunctions])]
112
-
113
- return source.replace(
114
- RUNTIME_IMPORT_PATTERN,
115
- `import { ${merged.join(', ')} } from "${JOJO_RUNTIME}"`
116
- )
117
- }
118
-
119
- export function compileFile(input, outDir = null) {
120
- const src = path.resolve(input)
121
- const dir = outDir
122
- ? path.resolve(outDir)
123
- : path.join(path.dirname(src), 'dist')
124
-
125
- fs.mkdirSync(dir, { recursive: true })
126
-
127
- const out = path.join(
128
- dir,
129
- path.basename(src, path.extname(src)) + '.js'
130
- )
131
-
132
- fs.writeFileSync(
133
- out,
134
- compile(fs.readFileSync(src, 'utf8')),
135
- 'utf8'
136
- )
137
-
138
- return { input: src, output: out }
139
- }
140
-
141
- export function compileDirectory(inputDir, outDir = null) {
142
- const root = path.resolve(inputDir)
143
- const target = outDir
144
- ? path.resolve(outDir)
145
- : path.join(root, 'dist')
146
-
147
- const result = []
148
-
149
- function walk(dir) {
150
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
151
- if (
152
- entry.name === 'node_modules' ||
153
- entry.name === 'dist' ||
154
- entry.name.startsWith('.')
155
- ) {
156
- continue
157
- }
158
-
159
- const full = path.join(dir, entry.name)
160
-
161
- if (entry.isDirectory()) {
162
- walk(full)
163
- continue
164
- }
165
-
166
- if (!entry.name.endsWith('.jojo')) continue
167
-
168
- const rel =
169
- path.relative(root, full).slice(0, -'.jojo'.length) + '.js'
170
-
171
- const out = path.join(target, rel)
172
-
173
- fs.mkdirSync(path.dirname(out), { recursive: true })
174
- fs.writeFileSync(
175
- out,
176
- compile(fs.readFileSync(full, 'utf8')),
177
- 'utf8'
178
- )
179
-
180
- result.push({ input: full, output: out })
181
- }
182
- }
183
-
184
- walk(root)
185
- return result
186
- }
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ import {
6
+ transformPipelines,
7
+ findPipelineStdlibFunctions,
8
+ hasPipelines
9
+ } from './pipeline-parser.js'
10
+
11
+ import {
12
+ transformMutableDeclarations,
13
+ transformConstDeclarations,
14
+ transformMultiBindingDeclarations,
15
+ transformDestructuringDeclarations,
16
+ transformFunctionDeclarations,
17
+ transformExpressionFunctions,
18
+ transformLocalImports
19
+ } from './transforms.js'
20
+
21
+ import { transformMatchExpressions } from './match-parser.js'
22
+ import { transformTypeAnnotations } from './type-annotations.js'
23
+ import {
24
+ transformEffectDeclarations,
25
+ transformHandleBlocks,
26
+ findEffectDeclarations,
27
+ findHandleBlocks
28
+ } from './effect-parser.js'
29
+
30
+ /*
31
+ * The auto-generated runtime import must use whatever name this package is
32
+ * actually installed under (e.g. a scoped fork like "@me/jojoscript"), not a
33
+ * hardcoded literal, so it's derived from this package's own package.json
34
+ * instead of being hardcoded.
35
+ */
36
+ const PACKAGE_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..')
37
+ const OWN_PACKAGE_NAME = JSON.parse(
38
+ fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')
39
+ ).name
40
+ const JOJO_RUNTIME = `${OWN_PACKAGE_NAME}/runtime`
41
+
42
+ export function compile(source, options = {}) {
43
+ let result = source
44
+ const runtimeImport = options.runtimeImport ?? JOJO_RUNTIME
45
+
46
+ /*
47
+ * Optional type annotations, stripped and re-expressed as JSDoc before
48
+ * anything else touches `fn` declarations.
49
+ */
50
+ result = transformTypeAnnotations(result)
51
+
52
+ /*
53
+ * Jojo functions.
54
+ */
55
+ result = transformExpressionFunctions(result)
56
+ result = transformFunctionDeclarations(result)
57
+
58
+ /*
59
+ * `match` expressions.
60
+ */
61
+ result = transformMatchExpressions(result)
62
+
63
+ /*
64
+ * Effects: `effect name(params)` declarations and `handle { ... } { ... }`
65
+ * blocks. Detected before either transform runs (both remove the source
66
+ * syntax they recognize), so the compiler knows afterwards whether the
67
+ * `defineEffect`/`withHandlers` runtime helpers need to be auto-imported.
68
+ */
69
+ const usesEffectDeclarations = findEffectDeclarations(result).length > 0
70
+ const usesHandleBlocks = findHandleBlocks(result).length > 0
71
+
72
+ result = transformEffectDeclarations(result)
73
+ result = transformHandleBlocks(result)
74
+
75
+ /*
76
+ * Jojo declarations.
77
+ */
78
+ result = transformMultiBindingDeclarations(result)
79
+ result = transformDestructuringDeclarations(result)
80
+ result = transformMutableDeclarations(result)
81
+ result = transformConstDeclarations(result)
82
+
83
+ /*
84
+ * Remember which pipeline stages need the Jojo runtime before
85
+ * the pipeline syntax itself is removed.
86
+ */
87
+ const pipelineStdlibFunctions =
88
+ findPipelineStdlibFunctions(result)
89
+
90
+ /*
91
+ * `effect`/`handle` syntax is never written by the user as a runtime
92
+ * function call, so their required runtime helpers are added explicitly,
93
+ * the same way `traceNode` is added below for `jojo profile`.
94
+ */
95
+ if (usesEffectDeclarations && !pipelineStdlibFunctions.includes('defineEffect')) {
96
+ pipelineStdlibFunctions.push('defineEffect')
97
+ }
98
+ if (usesHandleBlocks && !pipelineStdlibFunctions.includes('withHandlers')) {
99
+ pipelineStdlibFunctions.push('withHandlers')
100
+ }
101
+
102
+ /*
103
+ * `jojo profile` compiles with `{ profile: true }`, which wraps every
104
+ * pipeline stage (and the initial source expression) in a call to the
105
+ * `traceNode` runtime helper. That helper is never written by the user,
106
+ * so it must be added to the auto-import list explicitly.
107
+ */
108
+ if (options.profile && hasPipelines(result) && !pipelineStdlibFunctions.includes('traceNode')) {
109
+ pipelineStdlibFunctions.push('traceNode')
110
+ }
111
+
112
+ /*
113
+ * Pipelines.
114
+ */
115
+ result = transformPipelines(result, options)
116
+
117
+ /*
118
+ * ES module imports.
119
+ */
120
+ result = transformLocalImports(result)
121
+
122
+ /*
123
+ * Generated JavaScript imports the runtime as a normal package.
124
+ * This makes compiled applications portable and avoids globals. If the
125
+ * source already has a manual `jojoscript/runtime` import (e.g. for a
126
+ * stdlib function that is only ever called directly, never through a
127
+ * pipeline stage), the auto-imported names are merged into it instead
128
+ * of creating a second, colliding import declaration.
129
+ */
130
+ if (pipelineStdlibFunctions.length) {
131
+ result = mergeRuntimeImport(result, pipelineStdlibFunctions, runtimeImport)
132
+ }
133
+
134
+ return result.endsWith('\n') ? result : result + '\n'
135
+ }
136
+
137
+ function escapeRegExp(value) {
138
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
139
+ }
140
+
141
+ function mergeRuntimeImport(source, stdlibFunctions, runtimeImport) {
142
+ const pattern = new RegExp(
143
+ `import\\s*\\{([^}]*)\\}\\s*from\\s*(["'])([^"']+\\/runtime)\\2`
144
+ )
145
+ const existing = source.match(pattern)
146
+
147
+ if (!existing) {
148
+ return `import { ${stdlibFunctions.join(', ')} } from "${runtimeImport}"\n` + source
149
+ }
150
+
151
+ const existingNames = existing[1].split(',').map(name => name.trim()).filter(Boolean)
152
+ const merged = [...new Set([...existingNames, ...stdlibFunctions])]
153
+
154
+ return source.replace(
155
+ pattern,
156
+ `import { ${merged.join(', ')} } from "${runtimeImport}"`
157
+ )
158
+ }
159
+
160
+ export function compileFile(input, outDir = null, options = {}) {
161
+ const src = path.resolve(input)
162
+ const dir = outDir
163
+ ? path.resolve(outDir)
164
+ : path.join(path.dirname(src), 'dist')
165
+
166
+ fs.mkdirSync(dir, { recursive: true })
167
+
168
+ const out = path.join(
169
+ dir,
170
+ path.basename(src, path.extname(src)) + '.js'
171
+ )
172
+
173
+ fs.writeFileSync(
174
+ out,
175
+ compile(fs.readFileSync(src, 'utf8'), options),
176
+ 'utf8'
177
+ )
178
+
179
+ return { input: src, output: out }
180
+ }
181
+
182
+ export function compileDirectory(inputDir, outDir = null, options = {}) {
183
+ const root = path.resolve(inputDir)
184
+ const target = outDir
185
+ ? path.resolve(outDir)
186
+ : path.join(root, 'dist')
187
+
188
+ const result = []
189
+
190
+ function walk(dir) {
191
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
192
+ if (
193
+ entry.name === 'node_modules' ||
194
+ entry.name === 'dist' ||
195
+ entry.name.startsWith('.')
196
+ ) {
197
+ continue
198
+ }
199
+
200
+ const full = path.join(dir, entry.name)
201
+
202
+ if (entry.isDirectory()) {
203
+ walk(full)
204
+ continue
205
+ }
206
+
207
+ if (!entry.name.endsWith('.jojo')) continue
208
+
209
+ const rel =
210
+ path.relative(root, full).slice(0, -'.jojo'.length) + '.js'
211
+
212
+ const out = path.join(target, rel)
213
+
214
+ fs.mkdirSync(path.dirname(out), { recursive: true })
215
+ fs.writeFileSync(
216
+ out,
217
+ compile(fs.readFileSync(full, 'utf8'), options),
218
+ 'utf8'
219
+ )
220
+
221
+ result.push({ input: full, output: out })
222
+ }
223
+ }
224
+
225
+ walk(root)
226
+ return result
227
+ }