@cordisjs/cli-cordis 0.0.0 → 2.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # @cordisjs/cli-cordis
package/lib/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { Context } from 'cordis';
2
+ import type { Config } from './worker/index.ts';
3
+ export declare const name = "cli-cordis";
4
+ export declare const inject: string[];
5
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,105 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/index.ts
5
+ import { fork } from "node:child_process";
6
+ import { extname, resolve } from "node:path";
7
+ import { createRequire } from "node:module";
8
+ import { fileURLToPath } from "node:url";
9
+ import kleur from "kleur";
10
+ var name = "cli-cordis";
11
+ var inject = ["cli"];
12
+ function apply(ctx, config) {
13
+ ctx.cli.command("", "Meta-framework of spatiotemporal composability").option("-v, --version", "Show version").action(({ options }) => {
14
+ if (options.version) {
15
+ const require2 = createRequire(import.meta.url);
16
+ const { version } = require2("cordis/package.json");
17
+ return `cordis ${version}`;
18
+ }
19
+ });
20
+ ctx.cli.command("run [url]", "Start a Cordis application").option("-d, --daemon", "Run as daemon", { default: config.daemon.enabled }).action(async ({ args, options }) => {
21
+ const workerConfig = { ...config };
22
+ workerConfig.baseUrl ??= ctx.baseUrl;
23
+ if (args[0]) {
24
+ workerConfig.path = args[0];
25
+ }
26
+ if (options.daemon) {
27
+ createWorker(workerConfig);
28
+ } else {
29
+ const { start } = await import("./worker/index.js");
30
+ await start(workerConfig);
31
+ }
32
+ });
33
+ }
34
+ __name(apply, "apply");
35
+ var child;
36
+ function createWorker(config) {
37
+ let timer;
38
+ let started = false;
39
+ let exiting = false;
40
+ process.env.CORDIS_SHARED = JSON.stringify({
41
+ startTime: Date.now()
42
+ });
43
+ const filename = fileURLToPath(import.meta.url);
44
+ child = fork(resolve(filename, `../worker/main${extname(filename)}`), [], {
45
+ execArgv: [
46
+ ...process.execArgv,
47
+ ...config.execArgv || [],
48
+ "--expose-internals"
49
+ ],
50
+ env: {
51
+ ...process.env,
52
+ CORDIS_LOADER_OPTIONS: JSON.stringify(config)
53
+ }
54
+ });
55
+ child.on("message", (message) => {
56
+ if (message.type === "start") {
57
+ started = true;
58
+ timer = config.daemon?.heartbeatTimeout && setTimeout(() => {
59
+ console.log(kleur.red("daemon: heartbeat timeout"));
60
+ child.kill("SIGKILL");
61
+ }, config.daemon?.heartbeatTimeout);
62
+ } else if (message.type === "shared") {
63
+ process.env.CORDIS_SHARED = message.body;
64
+ } else if (message.type === "heartbeat") {
65
+ if (timer) timer.refresh();
66
+ } else if (message.type === "exit") {
67
+ exiting = true;
68
+ }
69
+ });
70
+ const signals = [
71
+ "SIGABRT",
72
+ "SIGBREAK",
73
+ "SIGBUS",
74
+ "SIGFPE",
75
+ "SIGHUP",
76
+ "SIGILL",
77
+ "SIGINT",
78
+ "SIGKILL",
79
+ "SIGSEGV",
80
+ "SIGSTOP",
81
+ "SIGTERM"
82
+ ];
83
+ function shouldExit(code, signal) {
84
+ if (!started) return true;
85
+ if (exiting) return true;
86
+ if (code === 0) return true;
87
+ if (signals.includes(signal)) return true;
88
+ if (code === 51) return false;
89
+ if (code === 52) return true;
90
+ return !config.daemon?.autoRestart;
91
+ }
92
+ __name(shouldExit, "shouldExit");
93
+ child.on("exit", (code, signal) => {
94
+ if (shouldExit(code, signal)) {
95
+ process.exit(code);
96
+ }
97
+ createWorker(config);
98
+ });
99
+ }
100
+ __name(createWorker, "createWorker");
101
+ export {
102
+ apply,
103
+ inject,
104
+ name
105
+ };
@@ -0,0 +1,10 @@
1
+ import { Context } from 'cordis';
2
+ export interface Config {
3
+ enabled?: boolean;
4
+ autoRestart?: boolean;
5
+ heartbeatInterval?: number;
6
+ heartbeatTimeout?: number;
7
+ exitTimeout?: number;
8
+ }
9
+ export declare const name = "daemon";
10
+ export declare function apply(ctx: Context, config?: Config): Generator<() => void, void, unknown>;
@@ -0,0 +1,10 @@
1
+ import { EntryOptions } from '@cordisjs/loader';
2
+ import * as daemon from './daemon.ts';
3
+ export interface Config {
4
+ baseUrl?: string;
5
+ execArgv?: string[];
6
+ path: string;
7
+ daemon: daemon.Config;
8
+ prelude?: EntryOptions[];
9
+ }
10
+ export declare function start(config: Config): Promise<void>;
@@ -0,0 +1,103 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+ var __export = (target, all) => {
4
+ for (var name2 in all)
5
+ __defProp(target, name2, { get: all[name2], enumerable: true });
6
+ };
7
+
8
+ // src/worker/index.ts
9
+ import { Context } from "cordis";
10
+ import { noop } from "cosmokit";
11
+ import Fs from "@cordisjs/fs";
12
+ import NodeFs from "@cordisjs/fs-node";
13
+ import { Loader } from "@cordisjs/loader";
14
+
15
+ // src/worker/daemon.ts
16
+ var daemon_exports = {};
17
+ __export(daemon_exports, {
18
+ apply: () => apply,
19
+ name: () => name
20
+ });
21
+ var name = "daemon";
22
+ var closedErrors = ["EPIPE", "ECONNRESET", "ERR_IPC_CHANNEL_CLOSED"];
23
+ function* apply(ctx, config = {}) {
24
+ let exiting = false;
25
+ function send(message) {
26
+ process.send?.(message, (error) => {
27
+ if (!error || closedErrors.includes(error.code)) return;
28
+ ctx.logger.warn(error);
29
+ });
30
+ }
31
+ __name(send, "send");
32
+ function exit(signal, reason) {
33
+ if (exiting) return;
34
+ exiting = true;
35
+ setTimeout(() => process.exit(1), config.exitTimeout ?? 1e4).unref();
36
+ ctx.logger.info(reason);
37
+ ctx.parallel("exit", signal).finally(() => process.exit());
38
+ }
39
+ __name(exit, "exit");
40
+ function handleSignal(signal) {
41
+ process.stderr.write("\x1B[2K\r");
42
+ send({ type: "exit" });
43
+ exit(signal, `terminated by ${signal}`);
44
+ }
45
+ __name(handleSignal, "handleSignal");
46
+ function handleDisconnect() {
47
+ exit("SIGHUP", "parent process disconnected");
48
+ }
49
+ __name(handleDisconnect, "handleDisconnect");
50
+ process.on("SIGINT", handleSignal);
51
+ yield () => process.off("SIGINT", handleSignal);
52
+ process.on("SIGTERM", handleSignal);
53
+ yield () => process.off("SIGTERM", handleSignal);
54
+ process.on("disconnect", handleDisconnect);
55
+ yield () => process.off("disconnect", handleDisconnect);
56
+ if (process.send && !process.connected) {
57
+ handleDisconnect();
58
+ return;
59
+ }
60
+ send({ type: "start", body: config });
61
+ if (config.heartbeatInterval) {
62
+ const timer = setInterval(() => {
63
+ send({ type: "heartbeat" });
64
+ }, config.heartbeatInterval);
65
+ yield () => clearInterval(timer);
66
+ }
67
+ }
68
+ __name(apply, "apply");
69
+
70
+ // src/worker/index.ts
71
+ async function start(config) {
72
+ const ctx = new Context();
73
+ process.stdout.on("error", noop);
74
+ process.stderr.on("error", noop);
75
+ process.on("uncaughtException", (error) => {
76
+ ctx.logger.error(error);
77
+ process.exitCode = 1;
78
+ });
79
+ process.on("unhandledRejection", (error) => {
80
+ ctx.logger.warn(error);
81
+ });
82
+ ctx.baseUrl = config.baseUrl;
83
+ if (config.daemon.enabled) {
84
+ await ctx.use(daemon_exports, config.daemon);
85
+ }
86
+ await ctx.use(Fs);
87
+ await ctx.use(NodeFs);
88
+ await ctx.use(Loader);
89
+ for (const options of config.prelude ?? []) {
90
+ await ctx.loader.create(options);
91
+ }
92
+ await ctx.loader.create({
93
+ name: "@cordisjs/include",
94
+ config: {
95
+ path: config.path,
96
+ enableLogs: true
97
+ }
98
+ });
99
+ }
100
+ __name(start, "start");
101
+ export {
102
+ start
103
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ // src/worker/main.ts
2
+ import assert from "node:assert";
3
+ import { start } from "./index.js";
4
+ assert.ok(process.env.CORDIS_LOADER_OPTIONS);
5
+ start(JSON.parse(process.env.CORDIS_LOADER_OPTIONS));
package/package.json CHANGED
@@ -1 +1,59 @@
1
- { "name": "@cordisjs/cli-cordis", "version": "0.0.0" }
1
+ {
2
+ "name": "@cordisjs/cli-cordis",
3
+ "version": "2.0.0-rc.0",
4
+ "description": "Commands for running Cordis applications",
5
+ "main": "lib/index.js",
6
+ "types": "lib/index.d.ts",
7
+ "sideEffects": [
8
+ "./lib/worker/main.js",
9
+ "./src/worker/main.ts"
10
+ ],
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./lib/index.d.ts",
15
+ "default": "./lib/index.js"
16
+ },
17
+ "./worker": {
18
+ "types": "./lib/worker/index.d.ts",
19
+ "default": "./lib/worker/index.js"
20
+ },
21
+ "./main": "./lib/worker/main.js",
22
+ "./src/*": "./src/*",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "lib",
27
+ "src"
28
+ ],
29
+ "author": "Shigma <shigma10826@gmail.com>",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/cordiverse/cordis.git",
34
+ "directory": "packages/cli-cordis"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/cordiverse/cordis/issues"
38
+ },
39
+ "homepage": "https://github.com/cordiverse/cordis",
40
+ "peerDependencies": {
41
+ "@cordisjs/cli": "^4.0.0",
42
+ "@cordisjs/fs": "^2.0.0-rc.0",
43
+ "@cordisjs/fs-node": "^0.1.0",
44
+ "@cordisjs/loader": "^1.0.0-rc.10",
45
+ "cordis": "^4.0.0-rc.13"
46
+ },
47
+ "devDependencies": {
48
+ "@cordisjs/cli": "^4.0.0",
49
+ "@cordisjs/fs": "^2.0.0-rc.0",
50
+ "@cordisjs/fs-node": "^0.1.0",
51
+ "@cordisjs/loader": "^1.0.0-rc.10",
52
+ "@cordisjs/logger-console": "^1.0.1",
53
+ "cordis": "^4.0.0-rc.13"
54
+ },
55
+ "dependencies": {
56
+ "cosmokit": "^1.8.1",
57
+ "kleur": "^4.1.5"
58
+ }
59
+ }
package/src/index.ts ADDED
@@ -0,0 +1,112 @@
1
+ import { ChildProcess, fork } from 'node:child_process'
2
+ import { extname, resolve } from 'node:path'
3
+ import { createRequire } from 'node:module'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { Context } from 'cordis'
6
+ import type {} from '@cordisjs/cli'
7
+ import type { Config } from './worker/index.ts'
8
+ import kleur from 'kleur'
9
+
10
+ export const name = 'cli-cordis'
11
+ export const inject = ['cli']
12
+
13
+ interface Event {
14
+ type: 'start' | 'shared' | 'heartbeat' | 'exit'
15
+ body?: string
16
+ }
17
+
18
+ export function apply(ctx: Context, config: Config) {
19
+ ctx.cli
20
+ .command('', 'Meta-framework of spatiotemporal composability')
21
+ .option('-v, --version', 'Show version')
22
+ .action(({ options }) => {
23
+ if (options.version) {
24
+ const require = createRequire(import.meta.url)
25
+ const { version } = require('cordis/package.json')
26
+ return `cordis ${version}`
27
+ }
28
+ })
29
+
30
+ ctx.cli
31
+ .command('run [url]', 'Start a Cordis application')
32
+ .option('-d, --daemon', 'Run as daemon', { default: config.daemon.enabled })
33
+ .action(async ({ args, options }) => {
34
+ const workerConfig: Config = { ...config }
35
+ workerConfig.baseUrl ??= ctx.baseUrl
36
+ if (args[0]) {
37
+ workerConfig.path = args[0]
38
+ }
39
+
40
+ if (options.daemon) {
41
+ createWorker(workerConfig)
42
+ } else {
43
+ // Direct mode: load in the same process
44
+ const { start } = await import('./worker/index.ts')
45
+ await start(workerConfig)
46
+ }
47
+ })
48
+ }
49
+
50
+ let child: ChildProcess
51
+
52
+ function createWorker(config: Config) {
53
+ let timer: 0 | NodeJS.Timeout | undefined
54
+ let started = false
55
+ let exiting = false
56
+
57
+ process.env.CORDIS_SHARED = JSON.stringify({
58
+ startTime: Date.now(),
59
+ })
60
+
61
+ const filename = fileURLToPath(import.meta.url)
62
+ child = fork(resolve(filename, `../worker/main${extname(filename)}`), [], {
63
+ execArgv: [
64
+ ...process.execArgv,
65
+ ...config.execArgv || [],
66
+ '--expose-internals',
67
+ ],
68
+ env: {
69
+ ...process.env,
70
+ CORDIS_LOADER_OPTIONS: JSON.stringify(config),
71
+ },
72
+ })
73
+
74
+ child.on('message', (message: Event) => {
75
+ if (message.type === 'start') {
76
+ started = true
77
+ timer = config.daemon?.heartbeatTimeout && setTimeout(() => {
78
+ // eslint-disable-next-line no-console
79
+ console.log(kleur.red('daemon: heartbeat timeout'))
80
+ child.kill('SIGKILL')
81
+ }, config.daemon?.heartbeatTimeout)
82
+ } else if (message.type === 'shared') {
83
+ process.env.CORDIS_SHARED = message.body
84
+ } else if (message.type === 'heartbeat') {
85
+ if (timer) timer.refresh()
86
+ } else if (message.type === 'exit') {
87
+ exiting = true
88
+ }
89
+ })
90
+
91
+ const signals: NodeJS.Signals[] = [
92
+ 'SIGABRT', 'SIGBREAK', 'SIGBUS', 'SIGFPE', 'SIGHUP',
93
+ 'SIGILL', 'SIGINT', 'SIGKILL', 'SIGSEGV', 'SIGSTOP', 'SIGTERM',
94
+ ]
95
+
96
+ function shouldExit(code: number, signal: NodeJS.Signals) {
97
+ if (!started) return true
98
+ if (exiting) return true
99
+ if (code === 0) return true
100
+ if (signals.includes(signal)) return true
101
+ if (code === 51) return false
102
+ if (code === 52) return true
103
+ return !config.daemon?.autoRestart
104
+ }
105
+
106
+ child.on('exit', (code, signal) => {
107
+ if (shouldExit(code!, signal!)) {
108
+ process.exit(code!)
109
+ }
110
+ createWorker(config)
111
+ })
112
+ }
@@ -0,0 +1,72 @@
1
+ import { Context } from 'cordis'
2
+
3
+ export interface Config {
4
+ enabled?: boolean
5
+ autoRestart?: boolean
6
+ heartbeatInterval?: number
7
+ heartbeatTimeout?: number
8
+ exitTimeout?: number
9
+ }
10
+
11
+ export const name = 'daemon'
12
+
13
+ // errors raised by writing to an IPC channel whose peer has gone
14
+ const closedErrors = ['EPIPE', 'ECONNRESET', 'ERR_IPC_CHANNEL_CLOSED']
15
+
16
+ export function* apply(ctx: Context, config: Config = {}) {
17
+ let exiting = false
18
+
19
+ // `process.send` is undefined when the worker is not forked with an IPC channel
20
+ function send(message: any) {
21
+ // A callback receives write errors that would otherwise be emitted on `process`.
22
+ // `process.connected` may still be true when the peer has already closed.
23
+ process.send?.(message, (error: any) => {
24
+ if (!error || closedErrors.includes(error.code)) return
25
+ ctx.logger.warn(error)
26
+ })
27
+ }
28
+
29
+ function exit(signal: NodeJS.Signals, reason: string) {
30
+ if (exiting) return
31
+ exiting = true
32
+ // an `exit` listener that never settles must not keep the worker alive
33
+ setTimeout(() => process.exit(1), config.exitTimeout ?? 10000).unref()
34
+ ctx.logger.info(reason)
35
+ ctx.parallel('exit', signal).finally(() => process.exit())
36
+ }
37
+
38
+ function handleSignal(signal: NodeJS.Signals) {
39
+ process.stderr.write('\x1b[2K\r')
40
+ // the parent process must not restart a worker that is exiting on request,
41
+ // regardless of the exit code produced by the cleanup
42
+ send({ type: 'exit' })
43
+ exit(signal, `terminated by ${signal}`)
44
+ }
45
+
46
+ // the parent process has exited, so no one supervises or restarts this worker
47
+ function handleDisconnect() {
48
+ exit('SIGHUP', 'parent process disconnected')
49
+ }
50
+
51
+ process.on('SIGINT', handleSignal)
52
+ yield () => process.off('SIGINT', handleSignal)
53
+ process.on('SIGTERM', handleSignal)
54
+ yield () => process.off('SIGTERM', handleSignal)
55
+ process.on('disconnect', handleDisconnect)
56
+ yield () => process.off('disconnect', handleDisconnect)
57
+
58
+ // `disconnect` is not emitted again for a channel that closed before the
59
+ // listener was registered, such as during module loading or preloading
60
+ if (process.send && !process.connected) {
61
+ handleDisconnect()
62
+ return
63
+ }
64
+
65
+ send({ type: 'start', body: config })
66
+ if (config.heartbeatInterval) {
67
+ const timer = setInterval(() => {
68
+ send({ type: 'heartbeat' })
69
+ }, config.heartbeatInterval)
70
+ yield () => clearInterval(timer)
71
+ }
72
+ }
@@ -0,0 +1,51 @@
1
+ import { Context } from 'cordis'
2
+ import { noop } from 'cosmokit'
3
+ import Fs from '@cordisjs/fs'
4
+ import NodeFs from '@cordisjs/fs-node'
5
+ import { EntryOptions, Loader } from '@cordisjs/loader'
6
+ import * as daemon from './daemon.ts'
7
+
8
+ export interface Config {
9
+ baseUrl?: string
10
+ execArgv?: string[]
11
+ path: string
12
+ daemon: daemon.Config
13
+ prelude?: EntryOptions[]
14
+ }
15
+
16
+ export async function start(config: Config) {
17
+ const ctx = new Context()
18
+
19
+ // Output to stdio is best-effort. A write error, such as EPIPE after the
20
+ // reader has gone, would otherwise become an uncaught exception, whose log
21
+ // entry is written to the same stream and raises the next one.
22
+ process.stdout.on('error', noop)
23
+ process.stderr.on('error', noop)
24
+
25
+ process.on('uncaughtException', (error) => {
26
+ ctx.logger.error(error)
27
+ process.exitCode = 1
28
+ })
29
+
30
+ process.on('unhandledRejection', (error) => {
31
+ ctx.logger.warn(error)
32
+ })
33
+
34
+ ctx.baseUrl = config.baseUrl
35
+ if (config.daemon.enabled) {
36
+ await ctx.use(daemon, config.daemon)
37
+ }
38
+ await ctx.use(Fs)
39
+ await ctx.use(NodeFs)
40
+ await ctx.use(Loader)
41
+ for (const options of config.prelude ?? []) {
42
+ await ctx.loader.create(options)
43
+ }
44
+ await ctx.loader.create({
45
+ name: '@cordisjs/include',
46
+ config: {
47
+ path: config.path,
48
+ enableLogs: true,
49
+ },
50
+ })
51
+ }
@@ -0,0 +1,6 @@
1
+ import assert from 'node:assert'
2
+ import { start } from './index.ts'
3
+
4
+ assert.ok(process.env.CORDIS_LOADER_OPTIONS)
5
+
6
+ start(JSON.parse(process.env.CORDIS_LOADER_OPTIONS))