unrun 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,55 +1,58 @@
1
1
  # unrun
2
2
 
3
- [![NPM version](https://img.shields.io/npm/v/unrun.svg?style=flat)](https://npmjs.com/package/unrun) [![NPM downloads](https://img.shields.io/npm/dm/unrun.svg?style=flat)](https://npmjs.com/package/unrun) [![Build Status](https://img.shields.io/circleci/project/egoist/unrun/master.svg?style=flat)](https://circleci.com/gh/egoist/unrun) [![donate](https://img.shields.io/badge/$-donate-ff69b4.svg?maxAge=2592000&style=flat)](https://github.com/egoist/donate)
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+ [![Unit Test][unit-test-src]][unit-test-href]
4
6
 
5
- ## Features
7
+ unrun is a tool that enables running any module at runtime (TypeScript, ESM, CJS, JSX, etc.) by bundling it with [Rolldown](https://rolldown.rs/).
6
8
 
7
- - Show description for each npm script
8
- - Use `yarn run` instead of `npm run` by default
9
+ It is highly inspired by tools like :
10
+
11
+ - [jiti](https://github.com/unjs/jiti)
12
+ - [bundle-require](https://github.com/egoist/bundle-require)
13
+ - [tsx](https://tsx.is/)
9
14
 
10
15
  ## Install
11
16
 
12
17
  ```bash
13
- $ yarn global add unrun
18
+ npm i unrun
14
19
  ```
15
20
 
16
21
  ## Usage
17
22
 
18
- With following contents in `package.json`:
19
-
20
- ```json
21
- {
22
- "scripts": {
23
- "api": "node server.js",
24
- "stats": "webpack-bundle-analyzer stats.json",
25
- "size": "tree ./dist -L 1 -h"
26
- },
27
- "unrun": {
28
- "api": "Start the API server",
29
- "stats": "Represents bundle content as convenient interactive zoomable treemap",
30
- "size": "Show bundle size"
31
- }
32
- }
33
- ```
23
+ ### Programmatic API
34
24
 
35
- ```bash
36
- $ unrun
25
+ - Async
26
+
27
+ ```ts
28
+ import { unrun } from 'unrun'
29
+
30
+ const { module } = await unrun({
31
+ path: './path/to/file.ts', // Path to the module to load
32
+ })
37
33
  ```
38
34
 
39
- <img src="https://ooo.0o0.ooo/2017/03/18/58cd3c92b6c31.png" width="600" />
35
+ - Sync
40
36
 
41
- ## Contributing
37
+ ```ts
38
+ import { unrunSync } from 'unrun'
42
39
 
43
- 1. Fork it!
44
- 2. Create your feature branch: `git checkout -b my-new-feature`
45
- 3. Commit your changes: `git commit -am 'Add some feature'`
46
- 4. Push to the branch: `git push origin my-new-feature`
47
- 5. Submit a pull request :D
40
+ const { module } = unrunSync({
41
+ path: './path/to/file.ts', // Path to the module to load
42
+ })
43
+ ```
48
44
 
45
+ ### CLI
49
46
 
50
- ## Author
47
+ ```bash
48
+ npx unrun ./path/to/file.ts
49
+ ```
51
50
 
52
- **unrun** © [egoist](https://github.com/egoist), Released under the [MIT](./LICENSE) License.<br>
53
- Authored and maintained by egoist with help from contributors ([list](https://github.com/egoist/unrun/contributors)).
51
+ <!-- Badges -->
54
52
 
55
- > [egoistian.com](https://egoistian.com) · GitHub [@egoist](https://github.com/egoist) · Twitter [@rem_rin_rin](https://twitter.com/rem_rin_rin)
53
+ [npm-version-src]: https://img.shields.io/npm/v/unrun.svg
54
+ [npm-version-href]: https://npmjs.com/package/unrun
55
+ [npm-downloads-src]: https://img.shields.io/npm/dm/unrun
56
+ [npm-downloads-href]: https://www.npmcharts.com/compare/unrun?interval=30
57
+ [unit-test-src]: https://github.com/gugustinette/unrun/actions/workflows/unit-test.yml/badge.svg
58
+ [unit-test-href]: https://github.com/gugustinette/unrun/actions/workflows/unit-test.yml
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.js ADDED
@@ -0,0 +1,103 @@
1
+ import process from "node:process";
2
+
3
+ //#region src/cli.ts
4
+ function parseCLIArguments(argv) {
5
+ let debug = false;
6
+ let preset;
7
+ let filePath;
8
+ const beforeArgs = [];
9
+ const afterArgs = [];
10
+ let expectFilePathAfterDelimiter = false;
11
+ for (let index = 0; index < argv.length; index += 1) {
12
+ const argument = argv[index];
13
+ if (filePath !== void 0) {
14
+ if (argument === "--") {
15
+ afterArgs.push(...argv.slice(index + 1));
16
+ break;
17
+ }
18
+ afterArgs.push(argument);
19
+ continue;
20
+ }
21
+ if (expectFilePathAfterDelimiter) {
22
+ filePath = argument;
23
+ expectFilePathAfterDelimiter = false;
24
+ continue;
25
+ }
26
+ if (argument === "--") {
27
+ expectFilePathAfterDelimiter = true;
28
+ beforeArgs.push(argument);
29
+ continue;
30
+ }
31
+ if (argument === "--debug") {
32
+ debug = true;
33
+ beforeArgs.push(argument);
34
+ continue;
35
+ }
36
+ if (argument === "--no-debug") {
37
+ debug = false;
38
+ beforeArgs.push(argument);
39
+ continue;
40
+ }
41
+ if (argument.startsWith("--preset=")) {
42
+ preset = argument.slice(9);
43
+ beforeArgs.push(argument);
44
+ continue;
45
+ }
46
+ if (argument === "--preset") {
47
+ const presetValue = argv[index + 1];
48
+ if (!presetValue || presetValue.startsWith("-")) throw new Error("[unrun] Missing preset value after --preset");
49
+ preset = presetValue;
50
+ beforeArgs.push(argument, presetValue);
51
+ index += 1;
52
+ continue;
53
+ }
54
+ if (argument.startsWith("-")) {
55
+ beforeArgs.push(argument);
56
+ continue;
57
+ }
58
+ filePath = argument;
59
+ }
60
+ return {
61
+ debug,
62
+ preset,
63
+ filePath,
64
+ beforeArgs,
65
+ afterArgs
66
+ };
67
+ }
68
+ async function runCLI() {
69
+ let parsedArguments;
70
+ try {
71
+ parsedArguments = parseCLIArguments(process.argv.slice(2));
72
+ } catch (error) {
73
+ console.error(error.message);
74
+ process.exit(1);
75
+ }
76
+ if (!parsedArguments.filePath) {
77
+ console.error("[unrun] No input files provided");
78
+ process.exit(1);
79
+ }
80
+ process.argv = [
81
+ process.argv[0],
82
+ parsedArguments.filePath,
83
+ ...parsedArguments.afterArgs
84
+ ];
85
+ try {
86
+ const { unrunCli } = await import("./index.js");
87
+ await unrunCli({
88
+ path: parsedArguments.filePath,
89
+ debug: parsedArguments.debug,
90
+ preset: parsedArguments.preset
91
+ }, parsedArguments.afterArgs);
92
+ } catch (error) {
93
+ console.error(error.message);
94
+ process.exit(1);
95
+ }
96
+ }
97
+ runCLI().catch((error) => {
98
+ console.error(error.message);
99
+ process.exit(1);
100
+ });
101
+
102
+ //#endregion
103
+ export { };
@@ -0,0 +1,75 @@
1
+ import { InputOptions, OutputOptions } from "rolldown";
2
+
3
+ //#region src/options.d.ts
4
+ interface Options {
5
+ /**
6
+ * The path to the file to be imported. Supports filesystem paths, file URLs or URL objects.
7
+ * @default 'index.ts'
8
+ */
9
+ path?: string | URL;
10
+ /**
11
+ * Debug mode.
12
+ * Wether or not to keep temporary files to help with debugging.
13
+ * Temporary files are stored in `node_modules/.cache/unrun/` if possible,
14
+ * otherwise in the OS temporary directory.
15
+ * @default false
16
+ */
17
+ debug?: boolean;
18
+ /**
19
+ * The preset to use for bundling and output format.
20
+ * @default 'none'
21
+ */
22
+ preset?: "none" | "jiti" | "bundle-require";
23
+ /**
24
+ * Additional rolldown input options. These options will be merged with the
25
+ * defaults provided by unrun, with these options always taking precedence.
26
+ */
27
+ inputOptions?: InputOptions;
28
+ /**
29
+ * Additional rolldown output options. These options will be merged with the
30
+ * defaults provided by unrun, with these options always taking precedence.
31
+ */
32
+ outputOptions?: OutputOptions;
33
+ }
34
+ //#endregion
35
+ //#region src/types.d.ts
36
+ interface Result {
37
+ /**
38
+ * The module that was loaded.
39
+ */
40
+ module: any;
41
+ }
42
+ interface CliResult {
43
+ /**
44
+ * The exit code of the CLI execution.
45
+ */
46
+ exitCode: number;
47
+ }
48
+ //#endregion
49
+ //#region src/index.d.ts
50
+ /**
51
+ * Loads a module with JIT transpilation based on the provided options.
52
+ *
53
+ * @param options - The options for loading the module.
54
+ * @returns A promise that resolves to the loaded module.
55
+ */
56
+ declare function unrun(options: Options): Promise<Result>;
57
+ /**
58
+ * Loads a module with JIT transpilation based on the provided options.
59
+ * This function runs synchronously using a worker thread.
60
+ *
61
+ * @param options - The options for loading the module.
62
+ * @returns The loaded module.
63
+ */
64
+ declare function unrunSync(options: Options): Result;
65
+ /**
66
+ * Runs a given module with JIT transpilation based on the provided options.
67
+ * This function does not return the module, as it simply executes it.
68
+ * Corresponds to the CLI behavior.
69
+ *
70
+ * @param options - The options for running the module.
71
+ * @param args - Additional command-line arguments to pass to the module.
72
+ */
73
+ declare function unrunCli(options: Options, args?: string[]): Promise<CliResult>;
74
+ //#endregion
75
+ export { type CliResult, type Options, type Result, unrun, unrunCli, unrunSync };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { n as unrunCli, r as unrunSync, t as unrun } from "./src-CRcGnzTd.js";
2
+
3
+ export { unrun, unrunCli, unrunSync };
@@ -0,0 +1,583 @@
1
+ import { createRequire } from "node:module";
2
+ import process from "node:process";
3
+ import { createSyncFn } from "synckit";
4
+ import path from "node:path";
5
+ import fs, { existsSync } from "node:fs";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
+ import { rolldown } from "rolldown";
8
+ import { Buffer as Buffer$1 } from "node:buffer";
9
+ import { spawn } from "node:child_process";
10
+ import crypto from "node:crypto";
11
+ import { tmpdir } from "node:os";
12
+
13
+ //#region rolldown:runtime
14
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
15
+
16
+ //#endregion
17
+ //#region src/features/preset.ts
18
+ /**
19
+ * Applies preset-specific handling to the loaded module.
20
+ */
21
+ function preset(options, module) {
22
+ if (options.preset === "bundle-require") return module;
23
+ if (options.preset === "jiti") {
24
+ const ext = path.extname(options.path);
25
+ if (module && typeof module === "object" && module[Symbol.toStringTag] === "Module" && Object.keys(module).length === 0) return ext === ".mjs" ? module : {};
26
+ }
27
+ if (module && typeof module === "object" && "default" in module) return module.default;
28
+ return module;
29
+ }
30
+
31
+ //#endregion
32
+ //#region src/utils/normalize-path.ts
33
+ /**
34
+ * Normalize a path-like input to a string path.
35
+ * @param pathLike The path-like input (string or URL).
36
+ * @returns The normalized string path.
37
+ */
38
+ function normalizePath(pathLike) {
39
+ if (!pathLike) return "index.ts";
40
+ if (pathLike instanceof URL) {
41
+ if (pathLike.protocol === "file:") return fileURLToPath(pathLike);
42
+ return pathLike.href;
43
+ }
44
+ if (typeof pathLike === "string") {
45
+ if (!pathLike.startsWith("file:")) return pathLike;
46
+ try {
47
+ return fileURLToPath(pathLike);
48
+ } catch {
49
+ try {
50
+ return fileURLToPath(new URL(pathLike));
51
+ } catch {
52
+ return pathLike;
53
+ }
54
+ }
55
+ }
56
+ return String(pathLike);
57
+ }
58
+
59
+ //#endregion
60
+ //#region src/options.ts
61
+ function resolveOptions(options = {}) {
62
+ const resolvedOptions = {
63
+ path: path.resolve(process.cwd(), normalizePath(options.path)),
64
+ debug: options.debug || false,
65
+ preset: options.preset || "none",
66
+ inputOptions: options.inputOptions,
67
+ outputOptions: options.outputOptions
68
+ };
69
+ if (!fs.existsSync(resolvedOptions.path)) throw new Error(`[unrun] File not found: ${resolvedOptions.path}`);
70
+ if (!new Set([
71
+ "none",
72
+ "jiti",
73
+ "bundle-require"
74
+ ]).has(resolvedOptions.preset)) throw new Error(`[unrun] Invalid preset "${resolvedOptions.preset}" (expected: none | jiti | bundle-require)`);
75
+ return resolvedOptions;
76
+ }
77
+
78
+ //#endregion
79
+ //#region src/plugins/console-output-customizer.ts
80
+ /**
81
+ * Attach a util.inspect customizer to namespace-like module objects produced by rolldown helpers
82
+ * so console.log prints concrete values instead of [Getter], while preserving live bindings.
83
+ *
84
+ * Cleaner strategy: inject a tiny helper at the top of the chunk and minimally augment
85
+ * rolldown helpers (__export and __copyProps) right inside their definitions, before use.
86
+ */
87
+ function createConsoleOutputCustomizer() {
88
+ return {
89
+ name: "unrun-console-output-customizer",
90
+ generateBundle: { handler(_, bundle$1) {
91
+ for (const chunk of Object.values(bundle$1)) {
92
+ if (chunk.type !== "chunk") continue;
93
+ if (!/__unrun__setInspect\b/.test(chunk.code)) {
94
+ const helper = [
95
+ "(function(){",
96
+ " function __unrun__fmt(names, getter, np){",
97
+ " var onlyDefault = names.length === 1 && names[0] === \"default\";",
98
+ " var o = np ? Object.create(null) : {};",
99
+ " for (var i = 0; i < names.length; i++) {",
100
+ " var n = names[i];",
101
+ " try { o[n] = getter(n) } catch {}",
102
+ " }",
103
+ " if (onlyDefault) {",
104
+ " try {",
105
+ " var s = JSON.stringify(o.default);",
106
+ " if (s !== undefined) {",
107
+ " s = s.replace(/\"([^\"]+)\":/g, \"$1: \").replace(/,/g, \", \").replace(/{/g, \"{ \").replace(/}/g, \" }\");",
108
+ " return \"[Module: null prototype] { default: \" + s + \" }\";",
109
+ " }",
110
+ " } catch {}",
111
+ " return \"[Module: null prototype] { default: \" + String(o.default) + \" }\";",
112
+ " }",
113
+ " return o;",
114
+ " }",
115
+ " function __unrun__setInspect(obj, names, getter, np){",
116
+ " try {",
117
+ " var __insp = Symbol.for('nodejs.util.inspect.custom')",
118
+ " Object.defineProperty(obj, __insp, {",
119
+ " value: function(){ return __unrun__fmt(names, getter, np) },",
120
+ " enumerable: false, configurable: true",
121
+ " })",
122
+ " } catch {}",
123
+ " return obj;",
124
+ " }",
125
+ " try { Object.defineProperty(globalThis, \"__unrun__setInspect\", { value: __unrun__setInspect, enumerable: false }) } catch {}",
126
+ "})();"
127
+ ].join("\n");
128
+ if (chunk.code.startsWith("#!")) {
129
+ const nl = chunk.code.indexOf("\n");
130
+ if (nl !== -1) chunk.code = `${chunk.code.slice(0, nl + 1)}${helper}\n${chunk.code.slice(nl + 1)}`;
131
+ else chunk.code = `${helper}\n${chunk.code}`;
132
+ } else chunk.code = `${helper}\n${chunk.code}`;
133
+ }
134
+ chunk.code = chunk.code.replace(/var\s+__export\s*=\s*\(all\)\s*=>\s*\{([\s\S]*?)return\s+target;\s*\}/, (_m, body) => {
135
+ return `var __export = (all) => {\n${[
136
+ body,
137
+ " try {",
138
+ " var __names = Object.keys(all).filter(function(n){ return n !== \"__esModule\" })",
139
+ " __unrun__setInspect(target, __names, function(n){ return all[n]() }, false)",
140
+ " } catch {}",
141
+ " return target;"
142
+ ].join("\n")}\n}`;
143
+ });
144
+ chunk.code = chunk.code.replace(/var\s+__copyProps\s*=\s*\(to,\s*from,\s*except,\s*desc\)\s*=>\s*\{([\s\S]*?)return\s+to;\s*\};/, (_m, body) => {
145
+ return `var __copyProps = (to, from, except, desc) => {\n${[
146
+ body,
147
+ " try {",
148
+ " var __names = Object.keys(to).filter(function(n){ return n !== \"__esModule\" })",
149
+ " __unrun__setInspect(to, __names, function(n){ return to[n] }, true)",
150
+ " } catch {}",
151
+ " return to;"
152
+ ].join("\n")}\n};`;
153
+ });
154
+ }
155
+ } }
156
+ };
157
+ }
158
+
159
+ //#endregion
160
+ //#region src/plugins/json-loader.ts
161
+ /**
162
+ * Minimal JSON loader to mimic jiti/Node behavior expected by tests:
163
+ * - Default export is the parsed JSON object
164
+ * - Also add a self-reference `default` property on the object (so obj.default === obj)
165
+ * - Provide named exports for top-level properties
166
+ */
167
+ function createJsonLoader() {
168
+ return {
169
+ name: "unrun-json-loader",
170
+ resolveId: { handler(source, importer) {
171
+ if (!source.endsWith(".json")) return null;
172
+ const basedir = importer ? path.dirname(importer) : process.cwd();
173
+ const resolved = path.resolve(basedir, source);
174
+ let isRequire = false;
175
+ try {
176
+ if (importer) {
177
+ const src = fs.readFileSync(importer, "utf8");
178
+ const escaped = source.replaceAll(/[.*+?^${}()|[\]\\]/g, (m) => `\\${m}`);
179
+ const pattern = String.raw`\brequire\s*\(\s*['"]${escaped}['"]\s*\)`;
180
+ isRequire = new RegExp(pattern).test(src);
181
+ }
182
+ } catch {}
183
+ return { id: `${resolved}?unrun-json.${isRequire ? "cjs" : "mjs"}` };
184
+ } },
185
+ load: {
186
+ filter: { id: /\?unrun-json\.(?:mjs|cjs)$/ },
187
+ handler(id) {
188
+ try {
189
+ const realId = id.replace(/\?unrun-json\.(?:mjs|cjs)$/, "");
190
+ const src = fs.readFileSync(realId, "utf8");
191
+ const data = JSON.parse(src);
192
+ const jsonLiteral = JSON.stringify(data);
193
+ if (id.endsWith("?unrun-json.cjs")) return { code: `const __data = ${jsonLiteral}\ntry { Object.defineProperty(__data, 'default', { value: __data, enumerable: false, configurable: true }) } catch {}\nmodule.exports = __data\n` };
194
+ const named = Object.keys(data).filter((k) => /^[$A-Z_]\w*$/i.test(k)).map((k) => `export const ${k} = __data[${JSON.stringify(k)}]`).join("\n");
195
+ return { code: [
196
+ `const __data = ${jsonLiteral}`,
197
+ `try { Object.defineProperty(__data, 'default', { value: __data, enumerable: false, configurable: true }) } catch {}`,
198
+ named,
199
+ `export default __data`
200
+ ].filter(Boolean).join("\n") };
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
205
+ }
206
+ };
207
+ }
208
+
209
+ //#endregion
210
+ //#region src/plugins/make-cjs-wrapper-async-friendly.ts
211
+ /**
212
+ * Transforms code strings containing CommonJS wrappers to be async-friendly.
213
+ *
214
+ * Rolldown may wrap CommonJS modules in a `__commonJS` function that uses
215
+ * arrow functions. If the wrapped code contains top-level `await`, this can lead
216
+ * to syntax errors since the callback function won't be marked as `async`.
217
+ * This function scans for such patterns and modifies the arrow functions to
218
+ * be `async` if they contain `await` expressions.
219
+ */
220
+ function createMakeCjsWrapperAsyncFriendlyPlugin() {
221
+ return {
222
+ name: "unrun-make-cjs-wrapper-async-friendly",
223
+ generateBundle: { handler(_outputOptions, bundle$1) {
224
+ for (const chunk of Object.values(bundle$1)) {
225
+ if (chunk.type !== "chunk") continue;
226
+ let code = chunk.code;
227
+ const marker = "__commonJS({";
228
+ if (!code.includes(marker)) return;
229
+ let pos = 0;
230
+ const arrowToken = "(() => {";
231
+ const asyncArrowToken = "(async () => {";
232
+ while (true) {
233
+ const markerIdx = code.indexOf(marker, pos);
234
+ if (markerIdx === -1) break;
235
+ const fnStart = code.indexOf(arrowToken, markerIdx);
236
+ if (fnStart === -1) {
237
+ pos = markerIdx + 12;
238
+ continue;
239
+ }
240
+ const bodyStart = fnStart + 8;
241
+ let i = bodyStart;
242
+ let depth = 1;
243
+ while (i < code.length && depth > 0) {
244
+ const ch = code[i++];
245
+ if (ch === "{") depth++;
246
+ else if (ch === "}") depth--;
247
+ }
248
+ if (depth !== 0) break;
249
+ const bodyEnd = i - 1;
250
+ const body = code.slice(bodyStart, bodyEnd);
251
+ if (/\bawait\b/.test(body) && code.slice(fnStart, fnStart + 14) !== asyncArrowToken) {
252
+ code = `${code.slice(0, fnStart + 1)}async ${code.slice(fnStart + 1)}`;
253
+ pos = fnStart + 1 + 6;
254
+ continue;
255
+ }
256
+ pos = bodyEnd;
257
+ }
258
+ if (code !== chunk.code) chunk.code = code;
259
+ }
260
+ } }
261
+ };
262
+ }
263
+
264
+ //#endregion
265
+ //#region src/plugins/require-resolve-fix.ts
266
+ /**
267
+ * Fix require.resolve calls to use the correct base path.
268
+ * Replace __require.resolve("./relative") with proper resolution from original file location.
269
+ */
270
+ function createRequireResolveFix(options) {
271
+ return {
272
+ name: "unrun-require-resolve-fix",
273
+ generateBundle: { handler(_, bundle$1) {
274
+ for (const chunk of Object.values(bundle$1)) if (chunk.type === "chunk") chunk.code = chunk.code.replaceAll(/__require\.resolve\(["']([^"']+)["']\)/g, (match, id) => {
275
+ if (id.startsWith("./") || id.startsWith("../")) try {
276
+ const baseDir = path.dirname(options.path);
277
+ for (const ext of [
278
+ "",
279
+ ".ts",
280
+ ".js",
281
+ ".mts",
282
+ ".mjs",
283
+ ".cts",
284
+ ".cjs"
285
+ ]) {
286
+ const testPath = path.resolve(baseDir, id + ext);
287
+ if (fs.existsSync(testPath)) return JSON.stringify(testPath);
288
+ }
289
+ const resolvedPath = path.resolve(baseDir, id);
290
+ return JSON.stringify(resolvedPath);
291
+ } catch {
292
+ return match;
293
+ }
294
+ return match;
295
+ });
296
+ } }
297
+ };
298
+ }
299
+
300
+ //#endregion
301
+ //#region src/plugins/require-typeof-fix.ts
302
+ /**
303
+ * Ensure typeof require in ESM stays undefined to match jiti behavior.
304
+ * Replaces typeof __require with typeof require to maintain compatibility.
305
+ */
306
+ function createRequireTypeofFix() {
307
+ return {
308
+ name: "unrun-require-typeof-fix",
309
+ generateBundle: { handler(_, bundle$1) {
310
+ for (const chunk of Object.values(bundle$1)) if (chunk.type === "chunk") chunk.code = chunk.code.replaceAll(/\btypeof\s+__require\b/g, "typeof require");
311
+ } }
312
+ };
313
+ }
314
+
315
+ //#endregion
316
+ //#region src/plugins/source-context-shims.ts
317
+ /**
318
+ * A rolldown plugin that injects source context shims:
319
+ * - Replaces import.meta.resolve calls with resolved file URLs
320
+ * - Injects per-module __filename/__dirname
321
+ * - Replaces import.meta.url with the source file URL
322
+ */
323
+ function createSourceContextShimsPlugin() {
324
+ return {
325
+ name: "unrun-source-context-shims",
326
+ load: {
327
+ filter: { id: /\.(?:m?[jt]s|c?tsx?)(?:$|\?)/ },
328
+ handler(id) {
329
+ let code;
330
+ try {
331
+ code = fs.readFileSync(id, "utf8");
332
+ } catch {
333
+ return null;
334
+ }
335
+ let __MODIFIED_CODE__ = false;
336
+ if (code.includes("import.meta.resolve")) {
337
+ const replaced = code.replaceAll(/import\.meta\.resolve!?\s*\(\s*(["'])([^"']+)\1\s*\)/g, (_m, _q, spec) => {
338
+ const url = pathToFileURL(path.resolve(path.dirname(id), spec)).href;
339
+ return JSON.stringify(url);
340
+ });
341
+ if (replaced !== code) {
342
+ code = replaced;
343
+ __MODIFIED_CODE__ = true;
344
+ }
345
+ }
346
+ if (/__filename|__dirname|import\s*\.\s*meta\s*\.\s*url/.test(code)) {
347
+ const file = id;
348
+ const dir = path.dirname(id);
349
+ const url = pathToFileURL(id).href;
350
+ const prologue = `const __filename = ${JSON.stringify(file)}\nconst __dirname = ${JSON.stringify(dir)}\n`;
351
+ const protectedStrings = [];
352
+ let protectedCode = code.replaceAll(/(["'])[^"']*import\s*\.\s*meta\s*\.\s*url[^"']*\1\s*:/g, (match) => {
353
+ const placeholder = `__PROTECTED_STRING_${protectedStrings.length}__`;
354
+ protectedStrings.push(match);
355
+ return placeholder;
356
+ });
357
+ protectedCode = protectedCode.replaceAll(/\bimport\s*\.\s*meta\s*\.\s*url\b/g, JSON.stringify(url));
358
+ for (const [i, protectedString] of protectedStrings.entries()) protectedCode = protectedCode.replace(`__PROTECTED_STRING_${i}__`, protectedString);
359
+ code = prologue + protectedCode;
360
+ __MODIFIED_CODE__ = true;
361
+ }
362
+ return __MODIFIED_CODE__ ? { code } : null;
363
+ }
364
+ }
365
+ };
366
+ }
367
+
368
+ //#endregion
369
+ //#region src/utils/bundle.ts
370
+ async function bundle(options) {
371
+ const resolvedTsconfigPath = path.resolve(process.cwd(), "tsconfig.json");
372
+ const tsconfig = existsSync(resolvedTsconfigPath) ? resolvedTsconfigPath : void 0;
373
+ const inputOptions = {
374
+ input: options.path,
375
+ platform: "node",
376
+ external: (id) => !id.startsWith(".") && !id.startsWith("/") && !id.startsWith("#"),
377
+ plugins: [
378
+ createMakeCjsWrapperAsyncFriendlyPlugin(),
379
+ createRequireResolveFix(options),
380
+ createSourceContextShimsPlugin(),
381
+ ...options.preset === "jiti" ? [
382
+ createConsoleOutputCustomizer(),
383
+ createJsonLoader(),
384
+ createRequireTypeofFix()
385
+ ] : []
386
+ ],
387
+ transform: { define: {
388
+ __dirname: JSON.stringify(path.dirname(options.path)),
389
+ __filename: JSON.stringify(options.path),
390
+ "import.meta.url": JSON.stringify(pathToFileURL(options.path).href),
391
+ "import.meta.filename": JSON.stringify(options.path),
392
+ "import.meta.dirname": JSON.stringify(path.dirname(options.path)),
393
+ "import.meta.env": "process.env"
394
+ } },
395
+ ...options.inputOptions
396
+ };
397
+ if (tsconfig) inputOptions.tsconfig = tsconfig;
398
+ const bundle$1 = await rolldown(inputOptions);
399
+ const outputOptions = {
400
+ format: "esm",
401
+ inlineDynamicImports: true,
402
+ keepNames: true,
403
+ ...options.outputOptions
404
+ };
405
+ const rolldownOutput = await bundle$1.generate(outputOptions);
406
+ if (!rolldownOutput.output[0]) throw new Error("[unrun] No output chunk found");
407
+ return rolldownOutput.output[0];
408
+ }
409
+
410
+ //#endregion
411
+ //#region src/utils/module/clean-module.ts
412
+ /**
413
+ * Clean the module file at the given URL.
414
+ * Deletes the file if it exists.
415
+ * @param moduleUrl - The URL of the module file to be cleaned.
416
+ */
417
+ function cleanModule(moduleUrl) {
418
+ try {
419
+ if (moduleUrl.startsWith("file://")) {
420
+ const filePath = new URL(moduleUrl);
421
+ fs.unlinkSync(filePath);
422
+ }
423
+ } catch (error) {
424
+ if (error.code !== "ENOENT") throw error;
425
+ }
426
+ }
427
+
428
+ //#endregion
429
+ //#region src/utils/module/exec-module.ts
430
+ /**
431
+ * Execute the module at the given URL, in a separate Node.js process.
432
+ * @param moduleUrl - The URL of the module to execute.
433
+ * @param args - Additional command-line arguments to pass to the Node.js process.
434
+ * @returns A promise that resolves when the module execution is complete.
435
+ */
436
+ function execModule(moduleUrl, args = []) {
437
+ return new Promise((resolve, reject) => {
438
+ const nodePath = process.execPath;
439
+ const spawnArgs = [];
440
+ if (moduleUrl.startsWith("data:")) {
441
+ const commaIndex = moduleUrl.indexOf(",");
442
+ if (commaIndex === -1) {
443
+ reject(/* @__PURE__ */ new Error("[unrun]: Invalid data URL for module execution"));
444
+ return;
445
+ }
446
+ const metadata = moduleUrl.slice(5, commaIndex);
447
+ const payload = moduleUrl.slice(commaIndex + 1);
448
+ const code = metadata.endsWith(";base64") ? Buffer$1.from(payload, "base64").toString("utf8") : decodeURIComponent(payload);
449
+ spawnArgs.push("--input-type=module", "--eval", code);
450
+ } else {
451
+ let modulePath = moduleUrl;
452
+ if (moduleUrl.startsWith("file://")) try {
453
+ modulePath = fileURLToPath(moduleUrl);
454
+ } catch (error) {
455
+ reject(/* @__PURE__ */ new Error(`[unrun]: Failed to resolve module URL ${moduleUrl}: ${error.message}`));
456
+ return;
457
+ }
458
+ spawnArgs.push(modulePath);
459
+ }
460
+ const childProcess = spawn(nodePath, [...spawnArgs, ...args], { stdio: [
461
+ "inherit",
462
+ "inherit",
463
+ "inherit"
464
+ ] });
465
+ childProcess.on("close", (exitCode) => {
466
+ resolve({ exitCode: exitCode ?? 0 });
467
+ });
468
+ childProcess.on("error", (error) => {
469
+ reject(/* @__PURE__ */ new Error(`[unrun]: Failed to start child process: ${error.message}`));
470
+ });
471
+ });
472
+ }
473
+
474
+ //#endregion
475
+ //#region src/utils/module/write-module.ts
476
+ function sanitize(name) {
477
+ return name.replaceAll(/[^\w.-]/g, "_");
478
+ }
479
+ /**
480
+ * Writes a module to the filesystem.
481
+ * @param code - The JavaScript code to be written as a module.
482
+ * @param options - Resolved options.
483
+ * @returns The file URL of the written module.
484
+ */
485
+ function writeModule(code, options) {
486
+ const filenameHint = path.basename(options.path);
487
+ let moduleUrl = "";
488
+ try {
489
+ const randomKey = crypto.randomBytes(16).toString("hex");
490
+ const fname = `${filenameHint ? `${sanitize(filenameHint)}.` : ""}${randomKey}.mjs`;
491
+ const projectNodeModules = path.join(process.cwd(), "node_modules");
492
+ const outDir = path.join(projectNodeModules, ".unrun");
493
+ const outFile = path.join(outDir, fname);
494
+ if (!fs.existsSync(outFile)) try {
495
+ fs.mkdirSync(outDir, { recursive: true });
496
+ fs.writeFileSync(outFile, code, "utf8");
497
+ } catch {
498
+ const fallbackDir = path.join(tmpdir(), "unrun-cache");
499
+ const fallbackFile = path.join(fallbackDir, fname);
500
+ fs.mkdirSync(fallbackDir, { recursive: true });
501
+ fs.writeFileSync(fallbackFile, code, "utf8");
502
+ moduleUrl = pathToFileURL(fallbackFile).href;
503
+ }
504
+ moduleUrl = moduleUrl || pathToFileURL(outFile).href;
505
+ } catch {
506
+ moduleUrl = `data:text/javascript;base64,${Buffer$1.from(code).toString("base64")}`;
507
+ }
508
+ return moduleUrl;
509
+ }
510
+
511
+ //#endregion
512
+ //#region src/utils/module/load-module.ts
513
+ /**
514
+ * Import a JS module from code string.
515
+ * Write ESM code to a temp file (prefer project-local node_modules/.unrun) and import it.
516
+ * @param code - The JavaScript code to be imported as a module.
517
+ * @param options - Resolved options.
518
+ * @returns The imported module.
519
+ */
520
+ async function loadModule(code, options) {
521
+ const moduleUrl = writeModule(code, options);
522
+ let _module;
523
+ try {
524
+ _module = await import(moduleUrl);
525
+ } finally {
526
+ cleanModule(moduleUrl);
527
+ }
528
+ return _module;
529
+ }
530
+
531
+ //#endregion
532
+ //#region src/index.ts
533
+ /**
534
+ * Loads a module with JIT transpilation based on the provided options.
535
+ *
536
+ * @param options - The options for loading the module.
537
+ * @returns A promise that resolves to the loaded module.
538
+ */
539
+ async function unrun(options) {
540
+ const resolvedOptions = resolveOptions(options);
541
+ const outputChunk = await bundle(resolvedOptions);
542
+ let module;
543
+ try {
544
+ module = await loadModule(outputChunk.code, resolvedOptions);
545
+ } catch (error) {
546
+ throw new Error(`[unrun] Import failed (code length: ${outputChunk.code.length}): ${error.message}`);
547
+ }
548
+ return { module: preset(resolvedOptions, module) };
549
+ }
550
+ /**
551
+ * Loads a module with JIT transpilation based on the provided options.
552
+ * This function runs synchronously using a worker thread.
553
+ *
554
+ * @param options - The options for loading the module.
555
+ * @returns The loaded module.
556
+ */
557
+ function unrunSync(options) {
558
+ return createSyncFn(__require.resolve("./sync/worker"), { tsRunner: "node" })(options);
559
+ }
560
+ /**
561
+ * Runs a given module with JIT transpilation based on the provided options.
562
+ * This function does not return the module, as it simply executes it.
563
+ * Corresponds to the CLI behavior.
564
+ *
565
+ * @param options - The options for running the module.
566
+ * @param args - Additional command-line arguments to pass to the module.
567
+ */
568
+ async function unrunCli(options, args = []) {
569
+ const resolvedOptions = resolveOptions(options);
570
+ const outputChunk = await bundle(resolvedOptions);
571
+ const moduleUrl = writeModule(outputChunk.code, resolvedOptions);
572
+ let cliResult;
573
+ try {
574
+ cliResult = await execModule(moduleUrl, args);
575
+ } catch (error) {
576
+ throw new Error(`[unrun] Run failed (code length: ${outputChunk.code.length}): ${error.message}`);
577
+ }
578
+ cleanModule(moduleUrl);
579
+ return cliResult;
580
+ }
581
+
582
+ //#endregion
583
+ export { unrunCli as n, unrunSync as r, unrun as t };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,50 @@
1
+ import { t as unrun } from "../src-CRcGnzTd.js";
2
+ import { runAsWorker } from "synckit";
3
+
4
+ //#region src/sync/worker.ts
5
+ function cloneForTransfer(value, seen = /* @__PURE__ */ new WeakMap()) {
6
+ if (typeof value === "function") throw new TypeError("[unrun] unrunSync cannot return functions");
7
+ if (value === null || typeof value !== "object") return value;
8
+ const objectValue = value;
9
+ if (seen.has(objectValue)) return seen.get(objectValue);
10
+ if (Array.isArray(value)) {
11
+ const clone$1 = [];
12
+ seen.set(objectValue, clone$1);
13
+ for (const item of value) clone$1.push(cloneForTransfer(item, seen));
14
+ return clone$1;
15
+ }
16
+ if (isModuleNamespace(value)) {
17
+ const clone$1 = Object.create(null);
18
+ seen.set(objectValue, clone$1);
19
+ for (const key of Object.keys(value)) {
20
+ const nestedValue = value[key];
21
+ clone$1[key] = cloneForTransfer(nestedValue, seen);
22
+ }
23
+ return clone$1;
24
+ }
25
+ if (typeof structuredClone === "function") try {
26
+ return structuredClone(value);
27
+ } catch (error) {
28
+ if (!isDataCloneError(error)) throw error;
29
+ }
30
+ const clone = {};
31
+ seen.set(objectValue, clone);
32
+ for (const [key, child] of Object.entries(value)) clone[key] = cloneForTransfer(child, seen);
33
+ return clone;
34
+ }
35
+ function isModuleNamespace(value) {
36
+ if (!value || typeof value !== "object") return false;
37
+ return Object.prototype.toString.call(value) === "[object Module]";
38
+ }
39
+ function isDataCloneError(error) {
40
+ if (!error || typeof error !== "object") return false;
41
+ if (!("name" in error)) return false;
42
+ return error.name === "DataCloneError";
43
+ }
44
+ runAsWorker(async (...args) => {
45
+ const options = args[0];
46
+ return cloneForTransfer(await unrun(options));
47
+ });
48
+
49
+ //#endregion
50
+ export { };
package/package.json CHANGED
@@ -1,41 +1,102 @@
1
1
  {
2
2
  "name": "unrun",
3
- "version": "0.1.0",
4
- "description": "Better npm script runner",
3
+ "version": "0.2.1",
4
+ "description": "A tool to load and execute any JavaScript or TypeScript code at runtime.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://gugustinette.github.io/unrun/",
8
+ "bugs": {
9
+ "url": "https://github.com/Gugustinette/unrun/issues"
10
+ },
5
11
  "repository": {
6
- "url": "egoist/unrun",
7
- "type": "git"
12
+ "type": "git",
13
+ "url": "git+https://github.com/Gugustinette/unrun.git"
8
14
  },
15
+ "author": "Augustin Mercier <gugustinette@proton.me>",
16
+ "funding": "https://github.com/sponsors/Gugustinette",
9
17
  "files": [
10
- "bin",
11
- "lib"
18
+ "dist"
12
19
  ],
13
- "bin": "bin/cli.js",
14
- "scripts": {
15
- "test": "npm run lint",
16
- "lint": "xo"
20
+ "main": "./dist/index.js",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": "./dist/index.js",
25
+ "./package.json": "./package.json"
17
26
  },
18
- "author": "egoist <0x142857@gmail.com>",
19
- "license": "MIT",
20
- "jest": {
21
- "testEnvironment": "node"
27
+ "bin": {
28
+ "unrun": "./dist/cli.js"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "dependencies": {
34
+ "@oxc-project/runtime": "^0.95.0",
35
+ "rolldown": "1.0.0-beta.45",
36
+ "synckit": "^0.11.11"
22
37
  },
23
38
  "devDependencies": {
24
- "eslint-config-rem": "^3.0.0",
25
- "jest-cli": "^18.1.0",
26
- "xo": "^0.17.1"
39
+ "@sxzz/eslint-config": "^7.1.4",
40
+ "@sxzz/prettier-config": "^2.2.4",
41
+ "@types/node": "^24.3.0",
42
+ "acorn": "^8.15.0",
43
+ "bumpp": "^10.2.3",
44
+ "bundle-require": "^5.1.0",
45
+ "config": "^4.1.1",
46
+ "consola": "^3.4.2",
47
+ "defu": "^6.1.4",
48
+ "destr": "^2.0.5",
49
+ "esbuild": "^0.25.10",
50
+ "eslint": "^9.34.0",
51
+ "estree-walker": "^3.0.3",
52
+ "etag": "^1.8.1",
53
+ "fast-glob": "^3.3.3",
54
+ "giget": "^2.0.0",
55
+ "is-installed-globally": "^1.0.0",
56
+ "jiti": "^2.6.1",
57
+ "mime": "^4.1.0",
58
+ "moment-timezone": "^0.6.0",
59
+ "nano-jsx": "^0.2.0",
60
+ "preact": "^10.27.2",
61
+ "preact-render-to-string": "^6.6.1",
62
+ "prettier": "^3.6.2",
63
+ "react": "^19.1.1",
64
+ "react-dom": "^19.1.1",
65
+ "reflect-metadata": "^0.2.2",
66
+ "tinyexec": "^1.0.1",
67
+ "tsdown": "^0.15.9",
68
+ "tsx": "^4.20.6",
69
+ "typedoc": "^0.28.14",
70
+ "typedoc-plugin-markdown": "^4.9.0",
71
+ "typedoc-vitepress-theme": "^1.1.2",
72
+ "typescript": "^5.9.3",
73
+ "unconfig": "^7.3.3",
74
+ "vite": "^7.1.7",
75
+ "vitepress": "2.0.0-alpha.12",
76
+ "vitepress-plugin-group-icons": "^1.6.5",
77
+ "vitest": "4.0.2",
78
+ "vue": "^3.5.22",
79
+ "zod": "^4.1.8"
27
80
  },
28
- "xo": {
29
- "extends": "rem",
30
- "esnext": true,
31
- "envs": [
32
- "jest"
33
- ]
81
+ "engines": {
82
+ "node": ">=20.19.0"
34
83
  },
35
- "dependencies": {
36
- "cac": "^3.0.4",
37
- "chalk": "^1.1.3",
38
- "cross-spawn": "^5.1.0",
39
- "inquirer": "^3.0.6"
84
+ "prettier": "@sxzz/prettier-config",
85
+ "scripts": {
86
+ "lint": "eslint --cache .",
87
+ "lint:fix": "pnpm run lint --fix",
88
+ "build": "tsdown",
89
+ "dev": "tsdown --watch",
90
+ "test": "vitest",
91
+ "test:run": "vitest run",
92
+ "test:update-fixtures": "node ./dist/cli.js ./scripts/update-fixtures.ts",
93
+ "benchmark": "tsdown -c benchmark/tsdown.config.ts && node ./benchmark/dist/benchmark.js",
94
+ "typecheck": "tsc --noEmit",
95
+ "format": "prettier --cache --write .",
96
+ "release": "bumpp && unotp pnpm publish",
97
+ "docs:dev": "vitepress dev docs",
98
+ "docs:build": "vitepress build docs",
99
+ "docs:preview": "vitepress preview docs",
100
+ "docs:generate-reference": "node ./dist/cli.js ./docs/.vitepress/scripts/generate-reference.ts"
40
101
  }
41
- }
102
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- The MIT License (MIT)
2
-
3
- Copyright (c) egoist <0x142857@gmail.com> (https://egoistian.com)
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
package/bin/cli.js DELETED
@@ -1,60 +0,0 @@
1
- #!/usr/bin/env node
2
- /* eslint-disable import/no-dynamic-require */
3
- const path = require('path')
4
- const cac = require('cac')
5
- const chalk = require('chalk')
6
- const inq = require('inquirer')
7
- const startProcess = require('../lib/start-process')
8
-
9
- const cli = cac()
10
-
11
- cli.option('npm', 'use `npm run` instead of `yarn run`')
12
-
13
- cli.command('*', 'Run npm script', (input, flags) => {
14
- const bin = flags.npm ? 'npm' : 'yarn'
15
-
16
- if (input[0]) {
17
- startProcess(bin, ['run', input[0]])
18
- return
19
- }
20
-
21
- let pkg
22
-
23
- try {
24
- pkg = require(path.resolve('package.json'))
25
- } catch (err) {
26
- if (err.code !== 'MODULE_NOT_FOUND') {
27
- console.error(chalk.red(err.stack))
28
- process.exit(1)
29
- }
30
- }
31
-
32
- const scripts = pkg && pkg.scripts
33
- if (!scripts) {
34
- console.error(chalk('> no npm scripts existing in current directory'))
35
- process.exit(1)
36
- }
37
-
38
- const unrun = pkg.unrun || pkg.aboutScripts || {}
39
- const choices = Object.keys(scripts).map(value => {
40
- const description = unrun[value]
41
- const script = scripts[value]
42
- const displayScript = chalk.gray(` $ ${script}`)
43
- return {
44
- value,
45
- name: description ? `${value}${displayScript}\n ${chalk.gray(description)}` : value + displayScript,
46
- short: description ? `${value}\n> ${chalk.gray(description)}` : value
47
- }
48
- })
49
-
50
- inq.prompt([{
51
- name: 'script',
52
- type: 'list',
53
- message: 'Choose a script:',
54
- choices
55
- }]).then(({ script }) => {
56
- startProcess(bin, ['run', script])
57
- })
58
- })
59
-
60
- cli.parse()
@@ -1,24 +0,0 @@
1
- /* eslint-disable unicorn/no-process-exit */
2
- const spawn = require('cross-spawn')
3
-
4
- module.exports = function (bin, args) {
5
- const proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] })
6
- proc.on('close', (code, signal) => {
7
- if (code !== null) {
8
- process.exit(code)
9
- }
10
- if (signal) {
11
- if (signal === 'SIGKILL') {
12
- process.exit(137)
13
- }
14
- console.log(`got signal ${signal}, exitting`)
15
- process.exit(1)
16
- }
17
- process.exit(0)
18
- })
19
- proc.on('error', err => {
20
- console.error(err)
21
- process.exit(1)
22
- })
23
- return proc
24
- }