@ugo-studio/jspp 0.3.0 → 0.3.2
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 +25 -25
- package/README.md +20 -12
- package/dist/cli/args.js +22 -0
- package/dist/cli/compiler.js +53 -0
- package/dist/cli/index.js +43 -107
- package/dist/cli/pch.js +71 -0
- package/dist/cli/runner.js +23 -0
- package/dist/cli/spinner.js +27 -11
- package/dist/cli/transpiler.js +20 -0
- package/dist/cli/utils.js +59 -0
- package/dist/cli/wasm.js +70 -0
- package/dist/index.js +17 -6
- package/dist/{analysis → interpreter/analysis}/scope.js +38 -3
- package/dist/{analysis → interpreter/analysis}/typeAnalyzer.js +563 -28
- package/dist/{core → interpreter/core}/codegen/class-handlers.js +1 -1
- package/dist/{core → interpreter/core}/codegen/control-flow-handlers.js +12 -11
- package/dist/{core → interpreter/core}/codegen/declaration-handlers.js +28 -9
- package/dist/{core → interpreter/core}/codegen/destructuring-handlers.js +9 -4
- package/dist/{core → interpreter/core}/codegen/expression-handlers.js +82 -88
- package/dist/{core → interpreter/core}/codegen/function-handlers.js +159 -46
- package/dist/{core → interpreter/core}/codegen/helpers.js +170 -25
- package/dist/interpreter/core/codegen/index.js +156 -0
- package/dist/{core → interpreter/core}/codegen/literal-handlers.js +9 -0
- package/dist/{core → interpreter/core}/codegen/statement-handlers.js +47 -7
- package/package.json +6 -4
- package/scripts/precompile-headers.ts +293 -50
- package/scripts/setup-compiler.ts +63 -63
- package/scripts/setup-emsdk.ts +114 -0
- package/src/prelude/any_value.cpp +888 -0
- package/src/prelude/any_value.hpp +29 -24
- package/src/prelude/{exception_helpers.hpp → exception.cpp} +53 -53
- package/src/prelude/exception.hpp +27 -27
- package/src/prelude/iterator_instantiations.hpp +10 -0
- package/src/prelude/{index.hpp → jspp.hpp} +13 -17
- package/src/prelude/library/array.cpp +191 -0
- package/src/prelude/library/array.hpp +5 -178
- package/src/prelude/library/boolean.cpp +30 -0
- package/src/prelude/library/boolean.hpp +14 -0
- package/src/prelude/library/console.cpp +125 -0
- package/src/prelude/library/console.hpp +9 -97
- package/src/prelude/library/error.cpp +100 -0
- package/src/prelude/library/error.hpp +8 -108
- package/src/prelude/library/function.cpp +69 -0
- package/src/prelude/library/function.hpp +6 -5
- package/src/prelude/library/global.cpp +98 -0
- package/src/prelude/library/global.hpp +12 -28
- package/src/prelude/library/global_usings.hpp +15 -0
- package/src/prelude/library/math.cpp +261 -0
- package/src/prelude/library/math.hpp +8 -288
- package/src/prelude/library/object.cpp +379 -0
- package/src/prelude/library/object.hpp +5 -267
- package/src/prelude/library/performance.cpp +21 -0
- package/src/prelude/library/performance.hpp +5 -20
- package/src/prelude/library/process.cpp +38 -0
- package/src/prelude/library/process.hpp +3 -31
- package/src/prelude/library/promise.cpp +131 -0
- package/src/prelude/library/promise.hpp +5 -116
- package/src/prelude/library/symbol.cpp +56 -0
- package/src/prelude/library/symbol.hpp +5 -46
- package/src/prelude/library/timer.cpp +88 -0
- package/src/prelude/library/timer.hpp +11 -87
- package/src/prelude/runtime.cpp +19 -0
- package/src/prelude/types.hpp +26 -20
- package/src/prelude/utils/access.hpp +123 -32
- package/src/prelude/utils/assignment_operators.hpp +119 -99
- package/src/prelude/utils/log_any_value/array.hpp +61 -40
- package/src/prelude/utils/log_any_value/function.hpp +39 -39
- package/src/prelude/utils/log_any_value/log_any_value.hpp +1 -1
- package/src/prelude/utils/log_any_value/object.hpp +60 -3
- package/src/prelude/utils/log_any_value/primitives.hpp +1 -1
- package/src/prelude/utils/operators.hpp +109 -94
- package/src/prelude/utils/operators_native.hpp +349 -0
- package/src/prelude/utils/well_known_symbols.hpp +24 -24
- package/src/prelude/values/array.cpp +1399 -0
- package/src/prelude/values/array.hpp +4 -0
- package/src/prelude/values/async_iterator.cpp +251 -0
- package/src/prelude/values/async_iterator.hpp +60 -32
- package/src/prelude/values/boolean.cpp +64 -0
- package/src/prelude/values/function.cpp +262 -0
- package/src/prelude/values/function.hpp +10 -30
- package/src/prelude/values/iterator.cpp +309 -0
- package/src/prelude/values/iterator.hpp +33 -64
- package/src/prelude/values/number.cpp +221 -0
- package/src/prelude/values/object.cpp +200 -0
- package/src/prelude/values/object.hpp +4 -0
- package/src/prelude/values/promise.cpp +479 -0
- package/src/prelude/values/promise.hpp +9 -2
- package/src/prelude/values/prototypes/array.hpp +46 -1348
- package/src/prelude/values/prototypes/async_iterator.hpp +19 -61
- package/src/prelude/values/prototypes/boolean.hpp +24 -0
- package/src/prelude/values/prototypes/function.hpp +7 -46
- package/src/prelude/values/prototypes/iterator.hpp +15 -191
- package/src/prelude/values/prototypes/number.hpp +30 -210
- package/src/prelude/values/prototypes/object.hpp +7 -23
- package/src/prelude/values/prototypes/promise.hpp +8 -186
- package/src/prelude/values/prototypes/string.hpp +28 -553
- package/src/prelude/values/prototypes/symbol.hpp +9 -70
- package/src/prelude/values/shape.hpp +52 -52
- package/src/prelude/values/string.cpp +485 -0
- package/src/prelude/values/symbol.cpp +89 -0
- package/src/prelude/values/symbol.hpp +101 -101
- package/dist/cli/file-utils.js +0 -20
- package/dist/cli-utils/args.js +0 -59
- package/dist/cli-utils/colors.js +0 -9
- package/dist/cli-utils/file-utils.js +0 -20
- package/dist/cli-utils/spinner.js +0 -55
- package/dist/cli.js +0 -153
- package/dist/core/codegen/index.js +0 -86
- package/src/prelude/any_value_access.hpp +0 -170
- package/src/prelude/any_value_defines.hpp +0 -190
- package/src/prelude/any_value_helpers.hpp +0 -374
- package/src/prelude/utils/operators_primitive.hpp +0 -337
- package/src/prelude/values/helpers/array.hpp +0 -199
- package/src/prelude/values/helpers/async_iterator.hpp +0 -275
- package/src/prelude/values/helpers/function.hpp +0 -109
- package/src/prelude/values/helpers/iterator.hpp +0 -145
- package/src/prelude/values/helpers/object.hpp +0 -104
- package/src/prelude/values/helpers/promise.hpp +0 -254
- package/src/prelude/values/helpers/string.hpp +0 -37
- package/src/prelude/values/helpers/symbol.hpp +0 -21
- /package/dist/{ast → interpreter/ast}/symbols.js +0 -0
- /package/dist/{ast → interpreter/ast}/types.js +0 -0
- /package/dist/{core → interpreter/core}/codegen/visitor.js +0 -0
- /package/dist/{core → interpreter/core}/constants.js +0 -0
- /package/dist/{core → interpreter/core}/error.js +0 -0
- /package/dist/{core → interpreter/core}/parser.js +0 -0
- /package/dist/{core → interpreter/core}/traverser.js +0 -0
package/LICENSE
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
# License
|
|
2
|
-
|
|
3
|
-
JSPP is licensed under the [MIT License](#mit-license).
|
|
4
|
-
|
|
5
|
-
## MIT License
|
|
6
|
-
|
|
7
|
-
Copyright (c) 2025 Emmanuel
|
|
8
|
-
|
|
9
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
-
in the Software without restriction, including without limitation the rights
|
|
12
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
-
furnished to do so, subject to the following conditions:
|
|
15
|
-
|
|
16
|
-
The above copyright notice and this permission notice shall be included in all
|
|
17
|
-
copies or substantial portions of the Software.
|
|
18
|
-
|
|
19
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
-
SOFTWARE.
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
JSPP is licensed under the [MIT License](#mit-license).
|
|
4
|
+
|
|
5
|
+
## MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2025 Emmanuel
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -11,7 +11,11 @@ The primary goal of this project is to achieve a near-perfect translation of Jav
|
|
|
11
11
|
|
|
12
12
|
JavaScript is flexible and dynamic; C++ is performant and type-safe. JSPP aims to offer the best of both worlds. By transpiling JS/TS to C++, we can potentially run JavaScript logic in environments where C++ is native, with significant performance gains and opportunities for low-level interoperability.
|
|
13
13
|
|
|
14
|
-
This project serves as a deep dive into compiler design, language semantics, and the expressive power of modern C++. The
|
|
14
|
+
This project serves as a deep dive into compiler design, language semantics, and the expressive power of modern C++. The architecture is designed for performance, utilizing:
|
|
15
|
+
- **Fast Runtime Library:** Core JavaScript logic is implemented in a static C++ library (`libjspp.a`), precompiled for speed.
|
|
16
|
+
- **Precompiled Headers (PCH):** Common headers are precompiled to drastically reduce the front-end parsing time of the C++ compiler.
|
|
17
|
+
- **NaN-Boxing:** An efficient 64-bit value representation (NaN-boxing) is used to replicate JavaScript's dynamic typing with minimal overhead.
|
|
18
|
+
- **Modern C++23:** Leverages the latest language features, including coroutines for `async/await` and generators.
|
|
15
19
|
|
|
16
20
|
## Features
|
|
17
21
|
|
|
@@ -67,8 +71,10 @@ To contribute to JSPP or run its test suite, follow these steps:
|
|
|
67
71
|
### Prerequisites
|
|
68
72
|
|
|
69
73
|
- **Bun:** This project uses [Bun](https://bun.sh/) for package management, script execution, and testing.
|
|
70
|
-
- **C++ Compiler:** A compiler with support for C++23 is required.
|
|
71
|
-
-
|
|
74
|
+
- **C++ Compiler:** A compiler with support for C++23 is required (e.g., `g++` 13+ or `clang` 17+).
|
|
75
|
+
- **Windows:** [MSYS2](https://www.msys2.org/) with `mingw-w64-x86_64-gcc` is recommended.
|
|
76
|
+
- **Linux:** `g++-14` or equivalent.
|
|
77
|
+
- **macOS:** `brew install gcc`.
|
|
72
78
|
|
|
73
79
|
### Setup
|
|
74
80
|
|
|
@@ -76,10 +82,11 @@ To contribute to JSPP or run its test suite, follow these steps:
|
|
|
76
82
|
```sh
|
|
77
83
|
git clone https://github.com/ugo-studio/jspp.git
|
|
78
84
|
```
|
|
79
|
-
2. Install dependencies:
|
|
85
|
+
2. Install dependencies and build the runtime:
|
|
80
86
|
```sh
|
|
81
87
|
bun install
|
|
82
88
|
```
|
|
89
|
+
*Note: The `postinstall` script will automatically check for your C++ compiler and precompile the runtime headers and library.*
|
|
83
90
|
|
|
84
91
|
## Usage
|
|
85
92
|
|
|
@@ -99,11 +106,9 @@ jspp my-code/test.ts
|
|
|
99
106
|
|
|
100
107
|
The transpiled C++ file and executable will be generated in the same directory as the input file and cleaned up after execution (unless `--keep-cpp` is used).
|
|
101
108
|
|
|
102
|
-
|
|
109
|
+
### Timing and Reports
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
bun test
|
|
106
|
-
```
|
|
111
|
+
In debug mode (default), JSPP provides a compilation time report using GCC's `-ftime-report`. This helps track the performance of the transpilation and compilation phases.
|
|
107
112
|
|
|
108
113
|
## Roadmap
|
|
109
114
|
|
|
@@ -115,7 +120,7 @@ This project is ambitious, and there is a long and exciting road ahead. Here is
|
|
|
115
120
|
|
|
116
121
|
This phase focuses on building a solid foundation that correctly models JavaScript's core runtime behavior.
|
|
117
122
|
|
|
118
|
-
- [x] Dynamic Variables & Primitives
|
|
123
|
+
- [x] Dynamic Variables & Primitives (NaN-boxing)
|
|
119
124
|
- [x] Function Declarations & Arrow Functions
|
|
120
125
|
- [x] Correct Hoisting for Variables and Functions
|
|
121
126
|
- [x] Closures & Lexical Scoping
|
|
@@ -139,16 +144,19 @@ This phase broadens the range of supported JavaScript syntax and features.
|
|
|
139
144
|
|
|
140
145
|
This phase focuses on building out the standard library and enabling modular code.
|
|
141
146
|
|
|
142
|
-
- [
|
|
143
|
-
- [
|
|
147
|
+
- [x] **JS Standard Library:** Core implementation of `Math`, `Symbol`, `Error`, `String`, `Array`, `Object`, `Timer`.
|
|
148
|
+
- [ ] **Expanded Library:** `Date`, `Temporal`, `Map`, `Set`, `JSON`, `RegExp`.
|
|
149
|
+
- [x] **Asynchronous Operations:** Event loop, `Promise`, `async/await`.
|
|
144
150
|
- [ ] **Module System:** Support for `import` and `export` to transpile multi-file projects.
|
|
145
151
|
|
|
146
152
|
### **Phase 4: Optimization & Advanced Features**
|
|
147
153
|
|
|
148
154
|
With a feature-complete transpiler, the focus will shift to performance and advanced capabilities.
|
|
149
155
|
|
|
156
|
+
- [x] **Architecture Optimization:** Static library runtime and Precompiled Headers.
|
|
150
157
|
- [ ] **Performance Benchmarking:** Create a suite to compare transpiled C++ performance against V8.
|
|
151
|
-
- [ ] **
|
|
158
|
+
- [ ] **Linker Optimization:** Support for `LLD` or `Mold` linkers.
|
|
159
|
+
- [ ] **C++ Interoperability:** Define a clear API for calling C++ functions from JavaScript.
|
|
152
160
|
|
|
153
161
|
## Contributing
|
|
154
162
|
|
package/dist/cli/args.js
CHANGED
|
@@ -7,6 +7,7 @@ export function parseArgs(rawArgs) {
|
|
|
7
7
|
let keepCpp = false;
|
|
8
8
|
let outputExePath = null;
|
|
9
9
|
let scriptArgs = [];
|
|
10
|
+
let target = "native";
|
|
10
11
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
11
12
|
const arg = rawArgs[i];
|
|
12
13
|
if (!arg)
|
|
@@ -21,6 +22,26 @@ export function parseArgs(rawArgs) {
|
|
|
21
22
|
else if (arg === "--keep-cpp") {
|
|
22
23
|
keepCpp = true;
|
|
23
24
|
}
|
|
25
|
+
else if (arg === "-t" || arg === "--target") {
|
|
26
|
+
if (i + 1 < rawArgs.length) {
|
|
27
|
+
const targetValue = rawArgs[i + 1]?.toLowerCase();
|
|
28
|
+
if (targetValue === "wasm") {
|
|
29
|
+
target = "wasm";
|
|
30
|
+
}
|
|
31
|
+
else if (targetValue === "native") {
|
|
32
|
+
target = "native";
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.error(`${COLORS.red}Error: Invalid target '${targetValue}'. Supported targets: native, wasm.${COLORS.reset}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
i++;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
console.error(`${COLORS.red}Error: --target requires a value (native or wasm).${COLORS.reset}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
24
45
|
else if (arg === "-o" || arg === "--output") {
|
|
25
46
|
if (i + 1 < rawArgs.length) {
|
|
26
47
|
outputExePath = rawArgs[i + 1] ?? null;
|
|
@@ -55,5 +76,6 @@ export function parseArgs(rawArgs) {
|
|
|
55
76
|
? path.resolve(process.cwd(), outputExePath)
|
|
56
77
|
: null,
|
|
57
78
|
scriptArgs,
|
|
79
|
+
target,
|
|
58
80
|
};
|
|
59
81
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { COLORS } from "./colors.js";
|
|
5
|
+
import { Spinner } from "./spinner.js";
|
|
6
|
+
import { msToHumanReadable } from "./utils.js";
|
|
7
|
+
export async function compileCpp(cppFilePath, exeFilePath, pchDir, preludePath, isWasm, flags, emsdkEnv, spinner) {
|
|
8
|
+
spinner.text = `Compiling binary...`;
|
|
9
|
+
spinner.start();
|
|
10
|
+
// Ensure output directory exists
|
|
11
|
+
await fs.mkdir(path.dirname(exeFilePath), { recursive: true });
|
|
12
|
+
const runtimeLibPath = path.join(pchDir, "libjspp.a");
|
|
13
|
+
const compiler = isWasm ? "em++" : "g++";
|
|
14
|
+
const compileArgs = [
|
|
15
|
+
"-std=c++23",
|
|
16
|
+
...flags,
|
|
17
|
+
"-include",
|
|
18
|
+
"jspp.hpp",
|
|
19
|
+
cppFilePath,
|
|
20
|
+
runtimeLibPath,
|
|
21
|
+
"-o",
|
|
22
|
+
exeFilePath,
|
|
23
|
+
"-I",
|
|
24
|
+
pchDir,
|
|
25
|
+
"-I",
|
|
26
|
+
preludePath,
|
|
27
|
+
];
|
|
28
|
+
if (!isWasm) {
|
|
29
|
+
compileArgs.splice(1, 0, "-Winvalid-pch");
|
|
30
|
+
}
|
|
31
|
+
const compileStartTime = performance.now();
|
|
32
|
+
const compile = spawn(compiler, compileArgs, {
|
|
33
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
34
|
+
env: emsdkEnv,
|
|
35
|
+
shell: process.platform === "win32",
|
|
36
|
+
});
|
|
37
|
+
const compileStderrChunks = [];
|
|
38
|
+
if (compile.stderr) {
|
|
39
|
+
compile.stderr.on("data", (chunk) => compileStderrChunks.push(chunk));
|
|
40
|
+
}
|
|
41
|
+
const compileExitCode = await new Promise((resolve) => {
|
|
42
|
+
compile.on("close", (code) => resolve(code ?? 1));
|
|
43
|
+
});
|
|
44
|
+
if (compileExitCode !== 0) {
|
|
45
|
+
const stderr = Buffer.concat(compileStderrChunks).toString();
|
|
46
|
+
spinner.fail(`Compilation failed`);
|
|
47
|
+
console.error(stderr);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
const compileEndTime = performance.now();
|
|
51
|
+
const compileTime = msToHumanReadable(compileEndTime - compileStartTime);
|
|
52
|
+
spinner.succeed(`Compiled to ${COLORS.green}${COLORS.bold}${path.basename(exeFilePath)}${COLORS.reset} ${COLORS.dim}[${compileTime}]${COLORS.reset}`);
|
|
53
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -1,128 +1,75 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { spawn } from "child_process";
|
|
3
2
|
import fs from "fs/promises";
|
|
4
3
|
import path from "path";
|
|
5
4
|
import pkg from "../../package.json" with { type: "json" };
|
|
6
|
-
import { CompilerError } from "../core/error.js";
|
|
7
|
-
import { Interpreter } from "../index.js";
|
|
5
|
+
import { CompilerError } from "../interpreter/core/error.js";
|
|
8
6
|
import { parseArgs } from "./args.js";
|
|
9
7
|
import { COLORS } from "./colors.js";
|
|
10
|
-
import {
|
|
8
|
+
import { compileCpp } from "./compiler.js";
|
|
9
|
+
import { checkAndRebuildPCH } from "./pch.js";
|
|
10
|
+
import { runOutput } from "./runner.js";
|
|
11
11
|
import { Spinner } from "./spinner.js";
|
|
12
|
+
import { transpile } from "./transpiler.js";
|
|
13
|
+
import { postProcessWasm, setupEmsdk } from "./wasm.js";
|
|
12
14
|
const pkgDir = path.dirname(path.dirname(import.meta.dirname));
|
|
15
|
+
const emsdkEnv = {
|
|
16
|
+
...process.env,
|
|
17
|
+
PATH: `${path.join(pkgDir, ".emsdk")}${path.delimiter}${path.join(pkgDir, ".emsdk", "upstream", "emscripten")}${path.delimiter}${process.env.PATH}`,
|
|
18
|
+
};
|
|
13
19
|
async function main() {
|
|
14
|
-
const { jsFilePath, isRelease, keepCpp, outputExePath, scriptArgs } = parseArgs(process.argv.slice(2));
|
|
20
|
+
const { jsFilePath, isRelease, keepCpp, outputExePath, scriptArgs, target, } = parseArgs(process.argv.slice(2));
|
|
21
|
+
const isWasm = target === "wasm";
|
|
15
22
|
const ext = path.extname(jsFilePath);
|
|
16
23
|
const jsFileName = path.basename(jsFilePath, ext);
|
|
17
24
|
const sourceDir = path.dirname(jsFilePath);
|
|
18
|
-
// Intermediate C++ file goes alongside the source JS file
|
|
19
|
-
|
|
25
|
+
// Intermediate C++ file goes alongside the source JS file if `--output` is not set
|
|
26
|
+
let cppFilePath = path.join(sourceDir, `${jsFileName}.cpp`);
|
|
20
27
|
// Determine output executable path
|
|
21
28
|
let exeFilePath;
|
|
22
29
|
if (outputExePath) {
|
|
23
30
|
exeFilePath = outputExePath;
|
|
31
|
+
cppFilePath = path.join(path.dirname(outputExePath), `${jsFileName}.cpp`);
|
|
24
32
|
}
|
|
25
33
|
else {
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
if (isWasm) {
|
|
35
|
+
exeFilePath = path.join(sourceDir, `${jsFileName}.js`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const ext = process.platform === "win32" ? ".exe" : "";
|
|
39
|
+
exeFilePath = path.join(sourceDir, `${jsFileName}${ext}`);
|
|
40
|
+
}
|
|
28
41
|
}
|
|
29
42
|
// Mode Configuration
|
|
30
|
-
const mode = isRelease ? "release" : "debug";
|
|
43
|
+
const mode = isWasm ? "wasm" : (isRelease ? "release" : "debug");
|
|
44
|
+
const modeNote = isRelease
|
|
45
|
+
? `${COLORS.dim}(optimized)${COLORS.reset}`
|
|
46
|
+
: `${COLORS.dim}(debug)${COLORS.reset}\n${COLORS.dim}NOTE: Use "--release" for an optimized build for production${COLORS.reset}`;
|
|
31
47
|
console.log(`${COLORS.bold}JSPP Compiler${COLORS.reset} ${COLORS.dim}v${pkg.version}${COLORS.reset}`);
|
|
32
|
-
console.log(`Mode: ${isRelease ? COLORS.green : COLORS.yellow}${mode.toUpperCase()}${COLORS.reset}\n`);
|
|
33
|
-
const flags = isRelease ? ["-O3", "-DNDEBUG"] : ["-
|
|
34
|
-
if (
|
|
48
|
+
console.log(`Target: ${isWasm ? COLORS.cyan : COLORS.green}${target.toUpperCase()}${COLORS.reset} | Mode: ${isRelease ? COLORS.green : COLORS.yellow}${mode.toUpperCase()}${COLORS.reset} ${modeNote}\n`);
|
|
49
|
+
const flags = isRelease ? ["-O3", "-DNDEBUG"] : ["-Og"];
|
|
50
|
+
if (isWasm) {
|
|
51
|
+
flags.push("-sASYNCIFY", "-sALLOW_MEMORY_GROWTH=1", "-sWASM=1", "-sEXPORT_ES6=1", "-sMODULARIZE=1", "-sEXPORT_NAME=jsppModule");
|
|
52
|
+
}
|
|
53
|
+
else if (process.platform === "win32") {
|
|
35
54
|
flags.push("-Wa,-mbig-obj");
|
|
36
55
|
}
|
|
37
56
|
const pchDir = path.resolve(pkgDir, "prelude-build", mode);
|
|
38
57
|
const spinner = new Spinner("Initializing...");
|
|
39
58
|
try {
|
|
59
|
+
if (isWasm) {
|
|
60
|
+
await setupEmsdk(pkgDir, spinner);
|
|
61
|
+
}
|
|
40
62
|
spinner.start();
|
|
41
|
-
// 1.
|
|
42
|
-
|
|
43
|
-
const jsCode = await fs.readFile(jsFilePath, "utf-8");
|
|
44
|
-
spinner.update("Transpiling to C++...");
|
|
45
|
-
const interpreter = new Interpreter();
|
|
46
|
-
const { cppCode, preludePath } = interpreter.interpret(jsCode, jsFilePath);
|
|
47
|
-
// Ensure directory for cpp file exists (should exist as it's source dir, but for safety if we change logic)
|
|
48
|
-
await fs.mkdir(path.dirname(cppFilePath), { recursive: true });
|
|
49
|
-
await fs.writeFile(cppFilePath, cppCode);
|
|
50
|
-
spinner.succeed(`Generated cpp`);
|
|
63
|
+
// 1. Transpilation Phase
|
|
64
|
+
const { preludePath, wasmExports } = await transpile(jsFilePath, cppFilePath, target, spinner);
|
|
51
65
|
// 2. Precompiled Header Check
|
|
52
|
-
|
|
53
|
-
spinner.start();
|
|
54
|
-
const pchFile = path.join(pchDir, "index.hpp.gch");
|
|
55
|
-
let shouldRebuild = false;
|
|
56
|
-
try {
|
|
57
|
-
const pchStats = await fs.stat(pchFile);
|
|
58
|
-
const sourceMtime = await getLatestMtime(preludePath);
|
|
59
|
-
if (sourceMtime > pchStats.mtimeMs) {
|
|
60
|
-
shouldRebuild = true;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
catch (e) {
|
|
64
|
-
shouldRebuild = true;
|
|
65
|
-
}
|
|
66
|
-
if (shouldRebuild) {
|
|
67
|
-
spinner.update("Rebuilding precompiled headers (this may take a while)...");
|
|
68
|
-
// Use spawn (async) instead of spawnSync to keep spinner alive
|
|
69
|
-
const rebuild = spawn("bun", [
|
|
70
|
-
"run",
|
|
71
|
-
"scripts/precompile-headers.ts",
|
|
72
|
-
], {
|
|
73
|
-
cwd: pkgDir,
|
|
74
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
75
|
-
});
|
|
76
|
-
const stderrChunks = [];
|
|
77
|
-
if (rebuild.stderr) {
|
|
78
|
-
rebuild.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
79
|
-
}
|
|
80
|
-
const exitCode = await new Promise((resolve) => {
|
|
81
|
-
rebuild.on("close", (code) => resolve(code ?? 1));
|
|
82
|
-
});
|
|
83
|
-
if (exitCode !== 0) {
|
|
84
|
-
const stderr = Buffer.concat(stderrChunks).toString();
|
|
85
|
-
spinner.fail("Failed to rebuild precompiled headers");
|
|
86
|
-
console.error(stderr);
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
spinner.succeed("Precompiled headers updated");
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
spinner.succeed("Precompiled headers");
|
|
93
|
-
}
|
|
66
|
+
await checkAndRebuildPCH(pkgDir, pchDir, mode, preludePath, emsdkEnv, spinner);
|
|
94
67
|
// 3. Compilation Phase
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const compile = spawn("g++", [
|
|
100
|
-
"-std=c++23",
|
|
101
|
-
...flags,
|
|
102
|
-
cppFilePath,
|
|
103
|
-
"-o",
|
|
104
|
-
exeFilePath,
|
|
105
|
-
"-I",
|
|
106
|
-
pchDir,
|
|
107
|
-
"-I",
|
|
108
|
-
preludePath,
|
|
109
|
-
], {
|
|
110
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
111
|
-
});
|
|
112
|
-
const compileStderrChunks = [];
|
|
113
|
-
if (compile.stderr) {
|
|
114
|
-
compile.stderr.on("data", (chunk) => compileStderrChunks.push(chunk));
|
|
68
|
+
await compileCpp(cppFilePath, exeFilePath, pchDir, preludePath, isWasm, flags, emsdkEnv, spinner);
|
|
69
|
+
// 3.5 Post-processing for Wasm (Exports)
|
|
70
|
+
if (isWasm) {
|
|
71
|
+
await postProcessWasm(exeFilePath, wasmExports);
|
|
115
72
|
}
|
|
116
|
-
const compileExitCode = await new Promise((resolve) => {
|
|
117
|
-
compile.on("close", (code) => resolve(code ?? 1));
|
|
118
|
-
});
|
|
119
|
-
if (compileExitCode !== 0) {
|
|
120
|
-
const stderr = Buffer.concat(compileStderrChunks).toString();
|
|
121
|
-
spinner.fail(`Compilation failed`);
|
|
122
|
-
console.error(stderr);
|
|
123
|
-
process.exit(1);
|
|
124
|
-
}
|
|
125
|
-
spinner.succeed(`Compiled to ${COLORS.green}${COLORS.bold}${path.basename(exeFilePath)}${COLORS.reset}`);
|
|
126
73
|
// Clean up C++ file if not requested to keep
|
|
127
74
|
if (!keepCpp) {
|
|
128
75
|
try {
|
|
@@ -133,18 +80,7 @@ async function main() {
|
|
|
133
80
|
}
|
|
134
81
|
}
|
|
135
82
|
// 4. Execution Phase
|
|
136
|
-
|
|
137
|
-
const run = spawn(exeFilePath, scriptArgs, {
|
|
138
|
-
stdio: "inherit",
|
|
139
|
-
});
|
|
140
|
-
const runExitCode = await new Promise((resolve) => {
|
|
141
|
-
run.on("close", (code) => resolve(code ?? 1));
|
|
142
|
-
});
|
|
143
|
-
console.log(`${COLORS.cyan}----------------------${COLORS.reset}\n`);
|
|
144
|
-
if (runExitCode !== 0) {
|
|
145
|
-
console.error(`${COLORS.red}Execution failed with exit code ${runExitCode}${COLORS.reset}`);
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}
|
|
83
|
+
await runOutput(exeFilePath, scriptArgs, isWasm);
|
|
148
84
|
}
|
|
149
85
|
catch (error) {
|
|
150
86
|
if (error instanceof CompilerError) {
|
package/dist/cli/pch.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { COLORS } from "./colors.js";
|
|
5
|
+
import { Spinner } from "./spinner.js";
|
|
6
|
+
import { getLatestMtime, msToHumanReadable } from "./utils.js";
|
|
7
|
+
export async function checkAndRebuildPCH(pkgDir, pchDir, mode, preludePath, emsdkEnv, spinner) {
|
|
8
|
+
spinner.text = "Checking precompiled headers...";
|
|
9
|
+
spinner.start();
|
|
10
|
+
const pchFile = path.join(pchDir, "jspp.hpp.gch");
|
|
11
|
+
const runtimeLibPath = path.join(pchDir, "libjspp.a");
|
|
12
|
+
let shouldRebuildPCH = false;
|
|
13
|
+
try {
|
|
14
|
+
const pchStats = await fs.stat(pchFile);
|
|
15
|
+
const libStats = await fs.stat(runtimeLibPath);
|
|
16
|
+
// 1. Check if any header is newer than the PCH
|
|
17
|
+
const latestHeaderMtime = await getLatestMtime(preludePath, (name) => name.endsWith(".hpp") || name.endsWith(".h"));
|
|
18
|
+
// 2. Check if any CPP file is newer than the library
|
|
19
|
+
const latestCppMtime = await getLatestMtime(preludePath, (name) => name.endsWith(".cpp"));
|
|
20
|
+
if (latestHeaderMtime > pchStats.mtimeMs ||
|
|
21
|
+
latestCppMtime > libStats.mtimeMs ||
|
|
22
|
+
pchStats.mtimeMs > libStats.mtimeMs) {
|
|
23
|
+
shouldRebuildPCH = true;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
shouldRebuildPCH = true;
|
|
28
|
+
}
|
|
29
|
+
if (shouldRebuildPCH) {
|
|
30
|
+
spinner.update("Rebuilding precompiled headers (this may take a while)...");
|
|
31
|
+
const pchStartTime = performance.now();
|
|
32
|
+
const rebuild = spawn("bun", [
|
|
33
|
+
"run",
|
|
34
|
+
"scripts/precompile-headers.ts",
|
|
35
|
+
"--jspp-cli-is-parent",
|
|
36
|
+
"--mode",
|
|
37
|
+
mode,
|
|
38
|
+
], {
|
|
39
|
+
cwd: pkgDir,
|
|
40
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
41
|
+
env: emsdkEnv,
|
|
42
|
+
});
|
|
43
|
+
if (rebuild.stdout) {
|
|
44
|
+
rebuild.stdout.on("data", (chunk) => {
|
|
45
|
+
spinner.pause();
|
|
46
|
+
process.stdout.write(chunk);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const stderrChunks = [];
|
|
50
|
+
if (rebuild.stderr) {
|
|
51
|
+
rebuild.stderr.on("data", (chunk) => stderrChunks.push(chunk));
|
|
52
|
+
}
|
|
53
|
+
const exitCode = await new Promise((resolve) => {
|
|
54
|
+
rebuild.on("close", (code) => {
|
|
55
|
+
spinner.resume();
|
|
56
|
+
resolve(code ?? 1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
if (exitCode !== 0) {
|
|
60
|
+
const stderr = Buffer.concat(stderrChunks).toString();
|
|
61
|
+
spinner.fail("Failed to rebuild precompiled headers");
|
|
62
|
+
console.error(stderr);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
const pchTime = msToHumanReadable(performance.now() - pchStartTime);
|
|
66
|
+
spinner.succeed(`Precompiled headers updated ${COLORS.dim}[${pchTime}]${COLORS.reset}`);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
spinner.stop();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { COLORS } from "./colors.js";
|
|
4
|
+
export async function runOutput(exeFilePath, scriptArgs, isWasm) {
|
|
5
|
+
if (isWasm) {
|
|
6
|
+
console.log(`\n${COLORS.cyan}Compilation finished. To run the output, use node or a browser:${COLORS.reset}`);
|
|
7
|
+
console.log(`${COLORS.bold} node ${path.basename(exeFilePath)}${COLORS.reset}\n`);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
console.log(`\n${COLORS.cyan}--- Running Output ---${COLORS.reset}`);
|
|
11
|
+
const run = spawn(exeFilePath, scriptArgs, {
|
|
12
|
+
stdio: "inherit",
|
|
13
|
+
});
|
|
14
|
+
const runExitCode = await new Promise((resolve) => {
|
|
15
|
+
run.on("close", (code) => resolve(code ?? 1));
|
|
16
|
+
});
|
|
17
|
+
console.log(`${COLORS.cyan}----------------------${COLORS.reset}\n`);
|
|
18
|
+
if (runExitCode !== 0) {
|
|
19
|
+
console.error(`${COLORS.red}Execution failed with exit code ${runExitCode}${COLORS.reset}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/cli/spinner.js
CHANGED
|
@@ -1,32 +1,42 @@
|
|
|
1
1
|
import { COLORS } from "./colors.js";
|
|
2
2
|
export class Spinner {
|
|
3
3
|
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
4
|
-
interval = null;
|
|
4
|
+
interval = null; // Note: You may need NodeJS.Timeout depending on your environment
|
|
5
5
|
frameIndex = 0;
|
|
6
6
|
text;
|
|
7
7
|
constructor(text) {
|
|
8
8
|
this.text = text;
|
|
9
9
|
}
|
|
10
10
|
start() {
|
|
11
|
+
if (this.interval)
|
|
12
|
+
return; // Prevent multiple intervals
|
|
11
13
|
process.stdout.write("\x1b[?25l"); // Hide cursor
|
|
12
14
|
this.frameIndex = 0;
|
|
13
15
|
this.render();
|
|
14
|
-
this.
|
|
15
|
-
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
16
|
-
this.render();
|
|
17
|
-
}, 80);
|
|
18
|
-
}
|
|
19
|
-
update(text) {
|
|
20
|
-
this.text = text;
|
|
21
|
-
this.render();
|
|
16
|
+
this.startInterval();
|
|
22
17
|
}
|
|
23
|
-
|
|
18
|
+
pause() {
|
|
24
19
|
if (this.interval) {
|
|
25
20
|
clearInterval(this.interval);
|
|
26
21
|
this.interval = null;
|
|
27
22
|
}
|
|
23
|
+
}
|
|
24
|
+
resume() {
|
|
25
|
+
if (this.interval)
|
|
26
|
+
return; // Already running
|
|
27
|
+
process.stdout.write("\x1b[?25l"); // Ensure cursor stays hidden
|
|
28
|
+
this.startInterval();
|
|
29
|
+
}
|
|
30
|
+
update(text) {
|
|
31
|
+
this.text = text;
|
|
32
|
+
this.render();
|
|
33
|
+
}
|
|
34
|
+
stop(symbol, color = COLORS.reset) {
|
|
35
|
+
this.pause(); // Reuse pause logic to clear the interval
|
|
28
36
|
this.clearLine();
|
|
29
|
-
|
|
37
|
+
if (symbol) {
|
|
38
|
+
process.stdout.write(`${color}${symbol} ${COLORS.reset} ${this.text}\n`);
|
|
39
|
+
}
|
|
30
40
|
process.stdout.write("\x1b[?25h"); // Show cursor
|
|
31
41
|
}
|
|
32
42
|
succeed(text) {
|
|
@@ -44,6 +54,12 @@ export class Spinner {
|
|
|
44
54
|
this.text = text;
|
|
45
55
|
this.stop("ℹ", COLORS.cyan);
|
|
46
56
|
}
|
|
57
|
+
startInterval() {
|
|
58
|
+
this.interval = setInterval(() => {
|
|
59
|
+
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
60
|
+
this.render();
|
|
61
|
+
}, 80);
|
|
62
|
+
}
|
|
47
63
|
render() {
|
|
48
64
|
this.clearLine();
|
|
49
65
|
const frame = this.frames[this.frameIndex];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { Interpreter } from "../index.js";
|
|
4
|
+
import { COLORS } from "./colors.js";
|
|
5
|
+
import { Spinner } from "./spinner.js";
|
|
6
|
+
import { msToHumanReadable } from "./utils.js";
|
|
7
|
+
export async function transpile(jsFilePath, cppFilePath, target, spinner) {
|
|
8
|
+
spinner.update(`Reading ${path.basename(jsFilePath)}...`);
|
|
9
|
+
const jsCode = await fs.readFile(jsFilePath, "utf-8");
|
|
10
|
+
spinner.update("Transpiling to C++...");
|
|
11
|
+
const transpileStartTime = performance.now();
|
|
12
|
+
const interpreter = new Interpreter();
|
|
13
|
+
const { cppCode, preludePath, wasmExports } = interpreter.interpret(jsCode, jsFilePath, target);
|
|
14
|
+
const transpileTime = msToHumanReadable(performance.now() - transpileStartTime);
|
|
15
|
+
// Ensure directory for cpp file exists
|
|
16
|
+
await fs.mkdir(path.dirname(cppFilePath), { recursive: true });
|
|
17
|
+
await fs.writeFile(cppFilePath, cppCode);
|
|
18
|
+
spinner.succeed(`Generated cpp ${COLORS.dim}[${transpileTime}]${COLORS.reset}`);
|
|
19
|
+
return { preludePath, wasmExports };
|
|
20
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export async function getLatestMtime(dirPath, filter) {
|
|
4
|
+
let maxMtime = 0;
|
|
5
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
6
|
+
for (const entry of entries) {
|
|
7
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
8
|
+
if (entry.isDirectory()) {
|
|
9
|
+
const nestedMtime = await getLatestMtime(fullPath, filter);
|
|
10
|
+
if (nestedMtime > maxMtime)
|
|
11
|
+
maxMtime = nestedMtime;
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
if (filter && !filter(entry.name))
|
|
15
|
+
continue;
|
|
16
|
+
const stats = await fs.stat(fullPath);
|
|
17
|
+
if (stats.mtimeMs > maxMtime)
|
|
18
|
+
maxMtime = stats.mtimeMs;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return maxMtime;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Converts milliseconds to a single-unit, human-readable decimal format.
|
|
25
|
+
* @param ms - The time in milliseconds
|
|
26
|
+
* @returns A formatted string (e.g., "2.52s", "1.5h")
|
|
27
|
+
*/
|
|
28
|
+
export function msToHumanReadable(ms) {
|
|
29
|
+
if (ms < 0) {
|
|
30
|
+
throw new Error("Time cannot be negative");
|
|
31
|
+
}
|
|
32
|
+
if (ms === 0) {
|
|
33
|
+
return "0ms";
|
|
34
|
+
}
|
|
35
|
+
const MS_PER_SECOND = 1000;
|
|
36
|
+
const MS_PER_MINUTE = 60 * MS_PER_SECOND;
|
|
37
|
+
const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
38
|
+
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
39
|
+
// Helper to safely truncate to 2 decimal places without rounding up
|
|
40
|
+
const formatValue = (val) => {
|
|
41
|
+
// toFixed(3) resolves float math errors, slice(0,-1) enforces truncation (e.g. 2.528 -> 2.52)
|
|
42
|
+
let str = val.toFixed(3).slice(0, -1);
|
|
43
|
+
// Remove trailing zeroes and lingering decimal points (e.g., "2.00" -> "2")
|
|
44
|
+
return str.replace(/0+$/, "").replace(/\.$/, "");
|
|
45
|
+
};
|
|
46
|
+
if (ms >= MS_PER_DAY) {
|
|
47
|
+
return formatValue(ms / MS_PER_DAY) + "d";
|
|
48
|
+
}
|
|
49
|
+
if (ms >= MS_PER_HOUR) {
|
|
50
|
+
return formatValue(ms / MS_PER_HOUR) + "h";
|
|
51
|
+
}
|
|
52
|
+
if (ms >= MS_PER_MINUTE) {
|
|
53
|
+
return formatValue(ms / MS_PER_MINUTE) + "m";
|
|
54
|
+
}
|
|
55
|
+
if (ms >= MS_PER_SECOND) {
|
|
56
|
+
return formatValue(ms / MS_PER_SECOND) + "s";
|
|
57
|
+
}
|
|
58
|
+
return formatValue(ms) + "ms";
|
|
59
|
+
}
|