@xnoxs/flux-lang 3.1.1
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/CHANGELOG.md +103 -0
- package/README.md +1089 -0
- package/bin/flux.js +1397 -0
- package/dist/flux.cjs.js +6664 -0
- package/dist/flux.esm.js +6674 -0
- package/dist/flux.min.js +263 -0
- package/index.d.ts +202 -0
- package/index.js +26 -0
- package/package.json +77 -0
- package/scripts/build.js +76 -0
- package/src/bundler.js +216 -0
- package/src/checker.js +322 -0
- package/src/codegen.js +785 -0
- package/src/css-preprocessor.js +399 -0
- package/src/formatter.js +140 -0
- package/src/jsx.js +480 -0
- package/src/lexer.js +518 -0
- package/src/linter.js +758 -0
- package/src/mangler.js +280 -0
- package/src/parser.js +1671 -0
- package/src/self/bundler.flux +167 -0
- package/src/self/bundler.js +187 -0
- package/src/self/checker.flux +249 -0
- package/src/self/checker.js +338 -0
- package/src/self/codegen.flux +555 -0
- package/src/self/codegen.js +784 -0
- package/src/self/css-preprocessor.flux +373 -0
- package/src/self/css-preprocessor.js +387 -0
- package/src/self/formatter.flux +93 -0
- package/src/self/formatter.js +114 -0
- package/src/self/jsx.flux +430 -0
- package/src/self/jsx.js +396 -0
- package/src/self/lexer.flux +529 -0
- package/src/self/lexer.js +709 -0
- package/src/self/lexer.stage2.js +700 -0
- package/src/self/linter.flux +515 -0
- package/src/self/linter.js +804 -0
- package/src/self/mangler.flux +253 -0
- package/src/self/mangler.js +348 -0
- package/src/self/parser.flux +1146 -0
- package/src/self/parser.js +1571 -0
- package/src/self/sourcemap.flux +66 -0
- package/src/self/sourcemap.js +72 -0
- package/src/self/stdlib.flux +356 -0
- package/src/self/stdlib.js +396 -0
- package/src/self/test-runner.flux +201 -0
- package/src/self/test-runner.js +132 -0
- package/src/self/transpiler.flux +123 -0
- package/src/self/transpiler.js +83 -0
- package/src/self/type-checker.flux +821 -0
- package/src/self/type-checker.js +1106 -0
- package/src/sourcemap.js +82 -0
- package/src/stdlib.js +436 -0
- package/src/test-runner.js +239 -0
- package/src/transpiler.js +172 -0
- package/src/type-checker.js +1206 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flux Lang — TypeScript Definitions
|
|
3
|
+
* Transpiler API for embedding Flux compilation in TypeScript/JavaScript projects.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface TranspileError {
|
|
7
|
+
/** Human-readable error description */
|
|
8
|
+
message: string;
|
|
9
|
+
/** Error category name (e.g. "SyntaxError", "TypeError", "CodeGenError") */
|
|
10
|
+
name?: string;
|
|
11
|
+
/** Compilation stage where the error occurred */
|
|
12
|
+
stage?: 'css' | 'jsx' | 'lexer' | 'parser' | 'checker' | 'typecheck' | 'mangler' | 'codegen';
|
|
13
|
+
/** 1-based line number in the source file */
|
|
14
|
+
line?: number;
|
|
15
|
+
/** 1-based column number in the source file */
|
|
16
|
+
col?: number;
|
|
17
|
+
/** Optional hint for fixing the error */
|
|
18
|
+
hint?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TranspileWarning {
|
|
22
|
+
message: string;
|
|
23
|
+
line?: number;
|
|
24
|
+
col?: number;
|
|
25
|
+
hint?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TranspileOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Generate an inline source map.
|
|
31
|
+
* The map URL is appended as `//# sourceMappingURL=<outputFile>.map`
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
sourcemap?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The name of the source `.flux` file — used in source map metadata.
|
|
38
|
+
* @default 'source.flux'
|
|
39
|
+
*/
|
|
40
|
+
sourceFile?: string;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The name of the output `.js` file — used in source map metadata.
|
|
44
|
+
* @default 'output.js'
|
|
45
|
+
*/
|
|
46
|
+
outputFile?: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Mangle (obfuscate) variable and function names in the output.
|
|
50
|
+
* @default false
|
|
51
|
+
*/
|
|
52
|
+
mangle?: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Enable JSX transformation.
|
|
56
|
+
* @default true
|
|
57
|
+
*/
|
|
58
|
+
jsx?: boolean;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* JSX output target.
|
|
62
|
+
* - `'browser'` — uses `document.createElement` and `appendChild`
|
|
63
|
+
* - `'server'` — produces HTML strings
|
|
64
|
+
* @default 'browser'
|
|
65
|
+
*/
|
|
66
|
+
jsxTarget?: 'browser' | 'server';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Run the static type checker and attach results to `typeErrors` / `typeWarnings`.
|
|
70
|
+
* @default true
|
|
71
|
+
*/
|
|
72
|
+
typecheck?: boolean;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Run the val-immutability checker (ensures `val` bindings are not reassigned).
|
|
76
|
+
* @default false
|
|
77
|
+
*/
|
|
78
|
+
check?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface TranspileResult {
|
|
82
|
+
/** Whether compilation succeeded without errors */
|
|
83
|
+
success: boolean;
|
|
84
|
+
|
|
85
|
+
/** The compiled JavaScript code (empty string on failure) */
|
|
86
|
+
output: string;
|
|
87
|
+
|
|
88
|
+
/** Source map JSON string, or `null` if `sourcemap` option was not set */
|
|
89
|
+
sourceMap: string | null;
|
|
90
|
+
|
|
91
|
+
/** The Abstract Syntax Tree produced by the parser */
|
|
92
|
+
ast: object | null;
|
|
93
|
+
|
|
94
|
+
/** The token stream produced by the lexer */
|
|
95
|
+
tokens: object[] | null;
|
|
96
|
+
|
|
97
|
+
/** Syntax / code generation errors that caused compilation to fail */
|
|
98
|
+
errors: TranspileError[];
|
|
99
|
+
|
|
100
|
+
/** Type errors from the static type checker (does not halt compilation) */
|
|
101
|
+
typeErrors: TranspileError[];
|
|
102
|
+
|
|
103
|
+
/** Type warnings from the static type checker */
|
|
104
|
+
typeWarnings: TranspileWarning[];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The last stage that was executing when an error occurred, or `null` on success.
|
|
108
|
+
*/
|
|
109
|
+
stage: 'css' | 'jsx' | 'lexer' | 'parser' | 'checker' | 'typecheck' | 'mangler' | 'codegen' | null;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Name mangling map — maps original names to mangled names.
|
|
113
|
+
* Only populated when `mangle: true`.
|
|
114
|
+
*/
|
|
115
|
+
nameMap: Record<string, string> | null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Transpile a Flux source string to JavaScript.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* import { transpile } from 'flux-lang';
|
|
124
|
+
*
|
|
125
|
+
* const result = transpile('val x = 42\nprint(x)');
|
|
126
|
+
* if (result.success) {
|
|
127
|
+
* console.log(result.output); // → "const x = 42;\nconsole.log(x);"
|
|
128
|
+
* } else {
|
|
129
|
+
* console.error(result.errors);
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export declare function transpile(source: string, options?: TranspileOptions): TranspileResult;
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
// ── Stdlib types ─────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/** Detect which Flux stdlib symbols are referenced in a JS code string. */
|
|
139
|
+
export declare function detectUsedSymbols(jsCode: string): string[];
|
|
140
|
+
|
|
141
|
+
/** Build the stdlib preamble JavaScript string. */
|
|
142
|
+
export declare function buildStdlib(symbols?: string[]): string;
|
|
143
|
+
|
|
144
|
+
/** All available Flux stdlib symbol names. */
|
|
145
|
+
export declare const STDLIB_SYMBOLS: readonly string[];
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
// ── Formatter ────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Format Flux source code.
|
|
152
|
+
* Normalizes indentation, blank lines, and operator spacing.
|
|
153
|
+
*/
|
|
154
|
+
export declare function format(source: string): string;
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
// ── Lexer ────────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
export interface Token {
|
|
160
|
+
type: string;
|
|
161
|
+
value: string | number | boolean | null;
|
|
162
|
+
line: number;
|
|
163
|
+
col: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export declare class Lexer {
|
|
167
|
+
constructor(source: string);
|
|
168
|
+
tokenize(): Token[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
// ── Parser ───────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
export interface AstNode {
|
|
175
|
+
type: string;
|
|
176
|
+
[key: string]: unknown;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface Program {
|
|
180
|
+
type: 'Program';
|
|
181
|
+
body: AstNode[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export declare class Parser {
|
|
185
|
+
constructor(tokens: Token[]);
|
|
186
|
+
parse(): Program;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
// ── Bundler ──────────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
export interface BundleResult {
|
|
193
|
+
success: boolean;
|
|
194
|
+
code: string;
|
|
195
|
+
modules: number;
|
|
196
|
+
errors: TranspileError[];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Bundle a Flux entry file and all its imports into a single JavaScript file.
|
|
201
|
+
*/
|
|
202
|
+
export declare function bundle(entryPath: string, options?: { minify?: boolean }): BundleResult;
|
package/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flux Lang — Public API
|
|
5
|
+
*
|
|
6
|
+
* const { transpile } = require('flux-lang');
|
|
7
|
+
* import { transpile, format, buildStdlib } from 'flux-lang';
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { transpile } = require('./src/transpiler');
|
|
11
|
+
const { format } = require('./src/formatter');
|
|
12
|
+
const { buildStdlib, detectUsedSymbols, STDLIB_SYMBOLS } = require('./src/stdlib');
|
|
13
|
+
const { Lexer } = require('./src/lexer');
|
|
14
|
+
const { Parser } = require('./src/parser');
|
|
15
|
+
const { bundle } = require('./src/bundler');
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
transpile,
|
|
19
|
+
format,
|
|
20
|
+
buildStdlib,
|
|
21
|
+
detectUsedSymbols,
|
|
22
|
+
STDLIB_SYMBOLS,
|
|
23
|
+
Lexer,
|
|
24
|
+
Parser,
|
|
25
|
+
bundle,
|
|
26
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xnoxs/flux-lang",
|
|
3
|
+
"version": "3.1.1",
|
|
4
|
+
"description": "Flux — A modern language that transpiles to JavaScript. Python-clean syntax, TypeScript-level safety, Rust-inspired pattern matching.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"flux": "./bin/flux.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "node scripts/build.js",
|
|
12
|
+
"prepublishOnly": "npm test && npm run build",
|
|
13
|
+
"test": "node bin/flux.js test tests/",
|
|
14
|
+
"test:file": "node bin/flux.js test",
|
|
15
|
+
"check": "node bin/flux.js check tests/01_basics.test.flux",
|
|
16
|
+
"bench": "node benchmarks/bench.js",
|
|
17
|
+
"start": "node bin/flux.js run app/server.flux",
|
|
18
|
+
"repl": "node bin/flux.js repl",
|
|
19
|
+
"version": "node bin/flux.js version"
|
|
20
|
+
},
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./index.js",
|
|
23
|
+
"./stdlib": "./src/stdlib.js",
|
|
24
|
+
"./formatter":"./src/formatter.js",
|
|
25
|
+
"./dist/cjs": "./dist/flux.cjs.js",
|
|
26
|
+
"./dist/esm": "./dist/flux.esm.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"bin/",
|
|
30
|
+
"src/",
|
|
31
|
+
"dist/",
|
|
32
|
+
"scripts/build.js",
|
|
33
|
+
"index.js",
|
|
34
|
+
"index.d.ts",
|
|
35
|
+
"README.md",
|
|
36
|
+
"CHANGELOG.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"keywords": [
|
|
40
|
+
"transpiler",
|
|
41
|
+
"compiler",
|
|
42
|
+
"language",
|
|
43
|
+
"javascript",
|
|
44
|
+
"flux",
|
|
45
|
+
"flux-lang",
|
|
46
|
+
"async",
|
|
47
|
+
"functional",
|
|
48
|
+
"pattern-matching",
|
|
49
|
+
"type-safe",
|
|
50
|
+
"scripting",
|
|
51
|
+
"frontend",
|
|
52
|
+
"backend",
|
|
53
|
+
"jsx",
|
|
54
|
+
"algebraic-types"
|
|
55
|
+
],
|
|
56
|
+
"author": "Flux Lang Contributors",
|
|
57
|
+
"license": "MIT",
|
|
58
|
+
"preferGlobal": true,
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=16.0.0"
|
|
61
|
+
},
|
|
62
|
+
"repository": {
|
|
63
|
+
"type": "git",
|
|
64
|
+
"url": "https://github.com/flux-lang/flux-lang.git"
|
|
65
|
+
},
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/flux-lang/flux-lang/issues"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://flux-lang.dev",
|
|
70
|
+
"funding": {
|
|
71
|
+
"type": "github",
|
|
72
|
+
"url": "https://github.com/sponsors/flux-lang"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"esbuild": "^0.28.1"
|
|
76
|
+
}
|
|
77
|
+
}
|
package/scripts/build.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Flux Lang — esbuild bundle script
|
|
6
|
+
*
|
|
7
|
+
* Produces:
|
|
8
|
+
* dist/flux.cjs.js — CommonJS bundle (Node, require())
|
|
9
|
+
* dist/flux.esm.js — ESM bundle (import / bundlers)
|
|
10
|
+
* dist/flux.min.js — Minified CJS bundle (browser / CDN)
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node scripts/build.js
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const esbuild = require('esbuild');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
21
|
+
const banner = `/*!
|
|
22
|
+
* flux-lang v${pkg.version}
|
|
23
|
+
* ${pkg.description}
|
|
24
|
+
* (c) ${new Date().getFullYear()} Flux Lang Contributors
|
|
25
|
+
* Released under the ${pkg.license} License
|
|
26
|
+
*/`;
|
|
27
|
+
|
|
28
|
+
const outDir = path.resolve(__dirname, '../dist');
|
|
29
|
+
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
30
|
+
|
|
31
|
+
const shared = {
|
|
32
|
+
entryPoints: ['index.js'],
|
|
33
|
+
bundle: true,
|
|
34
|
+
platform: 'node',
|
|
35
|
+
target: ['node16'],
|
|
36
|
+
external: ['esbuild'], // esbuild stays dev-only
|
|
37
|
+
banner: { js: banner },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
async function build() {
|
|
41
|
+
const start = Date.now();
|
|
42
|
+
|
|
43
|
+
await Promise.all([
|
|
44
|
+
// CJS bundle (for Node require())
|
|
45
|
+
esbuild.build({
|
|
46
|
+
...shared,
|
|
47
|
+
format: 'cjs',
|
|
48
|
+
outfile: 'dist/flux.cjs.js',
|
|
49
|
+
}),
|
|
50
|
+
|
|
51
|
+
// ESM bundle (for modern bundlers / type:"module" projects)
|
|
52
|
+
esbuild.build({
|
|
53
|
+
...shared,
|
|
54
|
+
format: 'esm',
|
|
55
|
+
outfile: 'dist/flux.esm.js',
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
// Minified CJS (CDN / browser via require shim)
|
|
59
|
+
esbuild.build({
|
|
60
|
+
...shared,
|
|
61
|
+
format: 'cjs',
|
|
62
|
+
minify: true,
|
|
63
|
+
outfile: 'dist/flux.min.js',
|
|
64
|
+
}),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const sizes = ['dist/flux.cjs.js', 'dist/flux.esm.js', 'dist/flux.min.js'].map(f => {
|
|
68
|
+
const bytes = fs.statSync(f).size;
|
|
69
|
+
const kb = (bytes / 1024).toFixed(1);
|
|
70
|
+
return ` ${f.padEnd(22)} ${kb} kB`;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
console.log(`\nBuild complete in ${Date.now() - start}ms\n${sizes.join('\n')}\n`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
build().catch(e => { console.error(e); process.exit(1); });
|
package/src/bundler.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { Lexer } = require('./lexer');
|
|
6
|
+
const { Parser } = require('./parser');
|
|
7
|
+
const { CodeGenerator } = require('./codegen');
|
|
8
|
+
|
|
9
|
+
class BundleError extends Error {
|
|
10
|
+
constructor(msg, file) {
|
|
11
|
+
super(file ? `[${path.basename(file)}] ${msg}` : msg);
|
|
12
|
+
this.name = 'BundleError';
|
|
13
|
+
this.file = file;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ── AST transformation helpers ────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
// Hapus node ImportDeclaration dan ExportDeclaration dari body AST
|
|
20
|
+
// Kembalikan: { cleanAst, imports, exports }
|
|
21
|
+
function extractModuleInfo(ast, fromFile) {
|
|
22
|
+
const imports = []; // { names: [], source, absPath }
|
|
23
|
+
const exports = []; // nama yang diekspor
|
|
24
|
+
const body = [];
|
|
25
|
+
const dir = path.dirname(fromFile);
|
|
26
|
+
|
|
27
|
+
for (const node of ast.body) {
|
|
28
|
+
if (node.type === 'ImportDecl') {
|
|
29
|
+
let src = node.source;
|
|
30
|
+
if (!src.endsWith('.flux')) src += '.flux';
|
|
31
|
+
const absPath = path.resolve(dir, src);
|
|
32
|
+
imports.push({ names: node.names, source: node.source, absPath });
|
|
33
|
+
|
|
34
|
+
} else if (node.type === 'ExportDecl') {
|
|
35
|
+
const inner = node.decl;
|
|
36
|
+
if (inner.type === 'FnDecl') exports.push(inner.name);
|
|
37
|
+
if (inner.type === 'ClassDecl') exports.push(inner.name);
|
|
38
|
+
if (inner.type === 'VarDecl') exports.push(inner.name);
|
|
39
|
+
body.push(inner);
|
|
40
|
+
|
|
41
|
+
} else {
|
|
42
|
+
body.push(node);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { cleanAst: { type: 'Program', body }, imports, exports };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Hasilkan kode JS dari clean AST
|
|
50
|
+
function codegenModule(ast) {
|
|
51
|
+
const { code } = new CodeGenerator({ indent: ' ' }).generate(ast);
|
|
52
|
+
// Hapus header boilerplate dari tiap modul (dua baris pertama)
|
|
53
|
+
const lines = code.split('\n');
|
|
54
|
+
// Hapus "// Dihasilkan oleh Flux Transpiler", '"use strict";', dan baris kosong pertama
|
|
55
|
+
const start = lines.findIndex(l => l.trim() !== '' && !l.includes('Dihasilkan') && !l.includes('"use strict"'));
|
|
56
|
+
return lines.slice(start).join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Konversi nama modul (path) ke identifier JS yang aman
|
|
60
|
+
function toModuleId(absPath) {
|
|
61
|
+
const base = path.basename(absPath, '.flux');
|
|
62
|
+
return '_flux_' + base.replace(/[^a-zA-Z0-9]/g, '_');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Bundler class ─────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
class Bundler {
|
|
68
|
+
constructor(entryFile, options = {}) {
|
|
69
|
+
this.entryFile = path.resolve(entryFile);
|
|
70
|
+
this.options = options;
|
|
71
|
+
this.modules = new Map(); // absPath → { ast, imports, exports, source }
|
|
72
|
+
this.order = []; // topological order (dependencies first)
|
|
73
|
+
this.visited = new Set();
|
|
74
|
+
this.inStack = new Set(); // cycle detection
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
bundle() {
|
|
78
|
+
this.collect(this.entryFile);
|
|
79
|
+
return this.link();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Fase 1: Kumpulkan semua modul secara rekursif ─────────────────
|
|
83
|
+
collect(absPath) {
|
|
84
|
+
if (this.visited.has(absPath)) return;
|
|
85
|
+
if (this.inStack.has(absPath)) {
|
|
86
|
+
throw new BundleError(`Dependensi sirkular terdeteksi: ${absPath}`, absPath);
|
|
87
|
+
}
|
|
88
|
+
if (!fs.existsSync(absPath)) {
|
|
89
|
+
throw new BundleError(`File tidak ditemukan: ${absPath}`, absPath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.inStack.add(absPath);
|
|
93
|
+
|
|
94
|
+
const source = fs.readFileSync(absPath, 'utf8');
|
|
95
|
+
|
|
96
|
+
// Lex + Parse
|
|
97
|
+
let ast;
|
|
98
|
+
try {
|
|
99
|
+
const tokens = new Lexer(source).tokenize();
|
|
100
|
+
ast = new Parser(tokens).parse();
|
|
101
|
+
} catch (e) {
|
|
102
|
+
throw new BundleError(`Error saat parsing: ${e.message}`, absPath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { cleanAst, imports, exports } = extractModuleInfo(ast, absPath);
|
|
106
|
+
this.modules.set(absPath, { cleanAst, imports, exports, source, absPath });
|
|
107
|
+
|
|
108
|
+
// Proses dependensi dulu (DFS)
|
|
109
|
+
for (const imp of imports) {
|
|
110
|
+
this.collect(imp.absPath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.inStack.delete(absPath);
|
|
114
|
+
this.visited.add(absPath);
|
|
115
|
+
this.order.push(absPath); // entry ditambahkan terakhir
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Fase 2: Link semua modul menjadi satu file JS ─────────────────
|
|
119
|
+
link() {
|
|
120
|
+
const lines = [];
|
|
121
|
+
const banner = this.options.banner !== false;
|
|
122
|
+
|
|
123
|
+
if (banner) {
|
|
124
|
+
lines.push('// Dihasilkan oleh Flux Bundler');
|
|
125
|
+
lines.push(`// Entry: ${path.basename(this.entryFile)}`);
|
|
126
|
+
lines.push(`// Modul: ${this.order.length}`);
|
|
127
|
+
lines.push('"use strict";');
|
|
128
|
+
lines.push('');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
lines.push('(function() {');
|
|
132
|
+
lines.push('');
|
|
133
|
+
|
|
134
|
+
for (const absPath of this.order) {
|
|
135
|
+
const { cleanAst, imports, exports } = this.modules.get(absPath);
|
|
136
|
+
const isEntry = absPath === this.entryFile;
|
|
137
|
+
const modId = toModuleId(absPath);
|
|
138
|
+
const relName = path.relative(process.cwd(), absPath);
|
|
139
|
+
|
|
140
|
+
lines.push(` // ${'─'.repeat(60)}`);
|
|
141
|
+
lines.push(` // Modul: ${relName}`);
|
|
142
|
+
lines.push(` // ${'─'.repeat(60)}`);
|
|
143
|
+
|
|
144
|
+
if (!isEntry) {
|
|
145
|
+
// Bungkus modul non-entry dalam object exports
|
|
146
|
+
lines.push(` var ${modId} = (function() {`);
|
|
147
|
+
lines.push(` var _exports = {};`);
|
|
148
|
+
lines.push('');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Inject import bindings dari modul lain
|
|
152
|
+
for (const imp of imports) {
|
|
153
|
+
const srcId = toModuleId(imp.absPath);
|
|
154
|
+
if (imp.names.length === 1) {
|
|
155
|
+
// import defaultExport from "./mod"
|
|
156
|
+
lines.push(` var ${imp.names[0]} = ${srcId};`);
|
|
157
|
+
} else {
|
|
158
|
+
// import { a, b } from "./mod"
|
|
159
|
+
for (const name of imp.names) {
|
|
160
|
+
lines.push(` var ${name} = ${srcId}._exports ? ${srcId}._exports.${name} : ${srcId}.${name};`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (imports.length > 0) lines.push('');
|
|
166
|
+
|
|
167
|
+
// Kode modul yang sudah dibersihkan
|
|
168
|
+
const moduleCode = codegenModule(cleanAst);
|
|
169
|
+
lines.push(moduleCode);
|
|
170
|
+
|
|
171
|
+
if (!isEntry) {
|
|
172
|
+
// Registrasikan semua export ke object _exports
|
|
173
|
+
if (exports.length > 0) {
|
|
174
|
+
lines.push('');
|
|
175
|
+
for (const expName of exports) {
|
|
176
|
+
lines.push(` _exports.${expName} = ${expName};`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
lines.push('');
|
|
180
|
+
lines.push(` return _exports;`);
|
|
181
|
+
lines.push(` })()`);
|
|
182
|
+
lines.push(` ${modId}._exports = ${modId};`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
lines.push('');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
lines.push('})();');
|
|
189
|
+
|
|
190
|
+
return { code: lines.join('\n'), modules: this.order.length };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Bundle beberapa file Flux menjadi satu JS.
|
|
196
|
+
* @param {string} entryFile - path ke file .flux utama
|
|
197
|
+
* @param {object} options
|
|
198
|
+
* @returns {{ success, code, modules, errors }}
|
|
199
|
+
*/
|
|
200
|
+
function bundle(entryFile, options = {}) {
|
|
201
|
+
const result = { success: false, code: '', modules: 0, errors: [] };
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const bundler = new Bundler(entryFile, options);
|
|
205
|
+
const { code, modules } = bundler.bundle();
|
|
206
|
+
result.code = code;
|
|
207
|
+
result.modules = modules;
|
|
208
|
+
result.success = true;
|
|
209
|
+
} catch (e) {
|
|
210
|
+
result.errors.push({ message: e.message, name: e.name, file: e.file || null });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = { bundle, Bundler, BundleError };
|