Skip to content
This repository was archived by the owner on Sep 1, 2024. It is now read-only.

Commit 22e79df

Browse files
committed
feat(cli): seamless watch mode
1 parent 9d2d702 commit 22e79df

6 files changed

Lines changed: 175 additions & 134 deletions

File tree

packages/cli/cli-watch.mjs

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/cli/cli.mjs

Lines changed: 142 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#!/usr/bin/env node --enable-source-maps
22

3+
import { fork } from 'node:child_process'
34
import { register } from 'node:module'
45
import { resolve } from 'node:path'
56
import repl from 'node:repl'
7+
import { fileURLToPath } from 'node:url'
68
import { parseArgs } from 'node:util'
79
import {
810
APP_COMMAND,
@@ -37,138 +39,170 @@ const { values, positionals } = parseArgs({
3739
multiple: true,
3840
default: [],
3941
},
42+
watch: {
43+
type: 'boolean',
44+
multiple: false,
45+
},
4046
},
4147
})
4248

4349
const [command, ...args] = positionals
44-
const { env: envPaths, entry, swc, timeout, ...kwargs } = values
45-
46-
const shutdownTimeout =
47-
(typeof timeout === 'string' ? Number.parseInt(timeout) : undefined) || 1000
48-
49-
for (const env of envPaths) {
50-
if (typeof env === 'string') {
51-
const { error } = config({ path: env })
52-
if (error) console.warn(error)
50+
const { watch, env: envPaths, entry, swc, timeout, ...kwargs } = values
51+
52+
if (watch) {
53+
// spawn the same script with nodejs --watch flag
54+
const forkArgs = process.argv.slice(2).filter((arg) => arg !== '--watch')
55+
fork(fileURLToPath(import.meta.url), forkArgs, {
56+
cwd: process.cwd(),
57+
execArgv: ['--enable-source-maps', '--watch'],
58+
stdio: 'inherit',
59+
env: {
60+
...process.env,
61+
NEEMATA_WATCH: '1',
62+
},
63+
})
64+
} else {
65+
const shutdownTimeout =
66+
(typeof timeout === 'string' ? Number.parseInt(timeout) : undefined) || 1000
67+
68+
for (const env of envPaths) {
69+
if (typeof env === 'string') {
70+
const { error } = config({ path: env })
71+
if (error) console.warn(error)
72+
}
5373
}
54-
}
5574

56-
const entryPath = resolve(
57-
process.env.NEEMATA_ENTRY ||
58-
(typeof entry === 'string' ? entry : swc ? 'index.ts' : 'index.js'),
59-
)
60-
61-
if (swc) {
62-
const url = new URL('./swc-loader.mjs', import.meta.url)
63-
process.env.NEEMATA_SWC = url.toString()
64-
register(url)
65-
}
66-
67-
let exitTimeout
68-
69-
const exitProcess = () => {
70-
if (exitTimeout) clearTimeout(exitTimeout)
71-
process.exit(0)
72-
}
75+
const entryPath = resolve(
76+
process.env.NEEMATA_ENTRY ||
77+
(typeof entry === 'string' ? entry : swc ? 'index.ts' : 'index.js'),
78+
)
7379

74-
const tryExit = async (cb) => {
75-
if (exitTimeout) return
76-
exitTimeout = setTimeout(exitProcess, shutdownTimeout)
77-
try {
78-
await cb()
79-
} catch (error) {
80-
logger.error(error)
81-
} finally {
82-
exitProcess()
80+
if (swc) {
81+
const url = new URL('./swc-loader.mjs', import.meta.url)
82+
process.env.NEEMATA_SWC = url.toString()
83+
register(url)
8384
}
84-
}
8585

86-
const entryApp = await import(entryPath).then((module) => module.default)
86+
let exitTimeout
8787

88-
if (
89-
!(
90-
(NeemataServer && entryApp instanceof NeemataServer.ApplicationServer) ||
91-
entryApp instanceof Application
92-
)
93-
) {
94-
throw new Error(
95-
'Invalid entry module. Must be an instance of Application or ApplicationServer',
96-
)
97-
}
98-
99-
const { logger } = entryApp
88+
const exitProcess = () => {
89+
if (exitTimeout) clearTimeout(exitTimeout)
90+
process.exit(0)
91+
}
10092

101-
process.on('uncaughtException', (error) => logger.error(error))
102-
process.on('unhandledRejection', (error) => logger.error(error))
93+
const tryExit = async (cb) => {
94+
if (exitTimeout) return
95+
exitTimeout = setTimeout(exitProcess, shutdownTimeout)
96+
try {
97+
await cb()
98+
} catch (error) {
99+
logger.error(error)
100+
} finally {
101+
exitProcess()
102+
}
103+
}
103104

104-
const loadApp = async (workerType) => {
105-
/** @type {Application} */
106-
let app
105+
const entryApp = await import(entryPath).then((module) => module.default)
106+
107+
if (
108+
!(
109+
(NeemataServer && entryApp instanceof NeemataServer.ApplicationServer) ||
110+
entryApp instanceof Application
111+
)
112+
) {
113+
throw new Error(
114+
'Invalid entry module. Must be an instance of Application or ApplicationServer',
115+
)
116+
}
107117

108-
if (NeemataServer && entryApp instanceof NeemataServer.ApplicationServer) {
109-
const { applicationPath } = entryApp.options
110-
/** @type {import('@neematajs/server').ApplicationWorkerOptions} */
111-
const options = {
112-
id: 0,
113-
workerType,
114-
isServer: false,
118+
const { logger } = entryApp
119+
120+
process.on('uncaughtException', (error) => logger.error(error))
121+
process.on('unhandledRejection', (error) => logger.error(error))
122+
123+
const loadApp = async (workerType, workerOptions = {}) => {
124+
/** @type {Application} */
125+
let app
126+
127+
if (NeemataServer && entryApp instanceof NeemataServer.ApplicationServer) {
128+
const { applicationPath } = entryApp.options
129+
/** @type {Parameters<typeof NeemataServer.providerWorkerOptions>[0]} */
130+
const options = {
131+
id: 0,
132+
workerType,
133+
isServer: false,
134+
workerOptions,
135+
}
136+
NeemataServer.providerWorkerOptions(options)
137+
app = await importDefault(applicationPath)
138+
} else if (entryApp instanceof Application) {
139+
app = entryApp
115140
}
116-
NeemataServer.providerWorkerOptions(options)
117-
app = await importDefault(applicationPath)
118-
} else if (entryApp instanceof Application) {
119-
app = entryApp
120-
}
121141

122-
return app
123-
}
142+
return app
143+
}
124144

125-
const commands = {
126-
start() {
127-
const terminate = () => tryExit(() => entryApp.stop())
128-
process.on('SIGTERM', terminate)
129-
process.on('SIGINT', terminate)
130-
entryApp.start()
131-
},
132-
async execute() {
133-
const app = await loadApp(WorkerType.Task)
145+
const commands = {
146+
async start() {
147+
const terminate = () => tryExit(() => entryApp.stop())
148+
process.on('SIGTERM', terminate)
149+
process.on('SIGINT', terminate)
150+
if (
151+
process.env.NEEMATA_WATCH &&
152+
entryApp instanceof NeemataServer.ApplicationServer
153+
) {
154+
// start only one api worker in watch mode
155+
const app = await loadApp(
156+
WorkerType.Api,
157+
entryApp.options.apiWorkers[0],
158+
)
159+
await app.start()
160+
} else {
161+
await entryApp.start()
162+
}
163+
},
164+
async execute() {
165+
const app = await loadApp(WorkerType.Task)
134166

135-
const [inputCommand, ...commandArgs] = args
167+
const [inputCommand, ...commandArgs] = args
136168

137-
let [extension, commandName] = inputCommand.split(':')
169+
let [extension, commandName] = inputCommand.split(':')
138170

139-
if (!commandName) {
140-
commandName = extension
141-
extension = undefined
142-
}
171+
if (!commandName) {
172+
commandName = extension
173+
extension = undefined
174+
}
143175

144-
const terminate = () => tryExit(() => defer(() => app.stop()))
176+
const terminate = () => tryExit(() => defer(() => app.stop()))
145177

146-
process.on('SIGTERM', terminate)
147-
process.on('SIGINT', terminate)
178+
process.on('SIGTERM', terminate)
179+
process.on('SIGINT', terminate)
148180

149-
await app.initialize()
181+
await app.initialize()
150182

151-
const command = app.registry.commands
152-
.get(extension ?? APP_COMMAND)
153-
?.get(commandName)
183+
const command = app.registry.commands
184+
.get(extension ?? APP_COMMAND)
185+
?.get(commandName)
154186

155-
if (!command) throw new Error(`Unknown application command: ${commandName}`)
187+
if (!command)
188+
throw new Error(`Unknown application command: ${commandName}`)
156189

157-
try {
158-
await command({ args: commandArgs, kwargs })
159-
} finally {
160-
terminate()
161-
}
162-
},
163-
async repl() {
164-
const app = await loadApp(WorkerType.Api)
165-
await app.initialize()
166-
globalThis.app = app
167-
repl.start({ useGlobal: true })
168-
},
169-
}
190+
try {
191+
await command({ args: commandArgs, kwargs })
192+
} finally {
193+
terminate()
194+
}
195+
},
196+
async repl() {
197+
const app = await loadApp(WorkerType.Api)
198+
await app.initialize()
199+
globalThis.app = app
200+
repl.start({ useGlobal: true })
201+
},
202+
}
170203

171-
if (command in commands === false)
172-
throw new Error(`Unknown CLI command: ${command}`)
204+
if (command in commands === false)
205+
throw new Error(`Unknown CLI command: ${command}`)
173206

174-
commands[command]()
207+
commands[command]()
208+
}

packages/cli/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
"linux"
99
],
1010
"bin": {
11-
"neemata": "./cli.mjs",
12-
"neemata-watch": "./cli-watch.mjs"
11+
"neemata": "./cli.mjs"
1312
},
1413
"exports": {
1514
"./swc": {

packages/server/lib/common.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import EventEmitter from 'node:events'
2-
import { WorkerType } from '@neematajs/application'
2+
import { BasicSubscriptionManager, WorkerType } from '@neematajs/application'
33
import type { ApplicationWorkerOptions } from './worker'
44

55
export const bindPortMessageHandler = (port: EventEmitter) => {
@@ -33,18 +33,21 @@ export const createBroadcastChannel = (name: string) => {
3333

3434
const WORKER_OPTIONS_KEY = Symbol('neemata:workerOptions')
3535

36-
export const providerWorkerOptions = (opts: ApplicationWorkerOptions) => {
37-
globalThis[WORKER_OPTIONS_KEY] = opts
36+
const defaultWorkerOptions = {
37+
id: 0,
38+
workerType: WorkerType.Api,
39+
isServer: false,
40+
subscriptionManager: BasicSubscriptionManager,
41+
}
42+
43+
export const providerWorkerOptions = (
44+
opts: Partial<ApplicationWorkerOptions>,
45+
) => {
46+
globalThis[WORKER_OPTIONS_KEY] = { ...defaultWorkerOptions, ...opts }
3847
}
3948

4049
export const injectWorkerOptions = (): ApplicationWorkerOptions => {
41-
return (
42-
globalThis[WORKER_OPTIONS_KEY] ?? {
43-
id: 0,
44-
workerType: WorkerType.Api,
45-
isServer: false,
46-
}
47-
)
50+
return globalThis[WORKER_OPTIONS_KEY] ?? defaultWorkerOptions
4851
}
4952

5053
export enum WorkerMessageType {

0 commit comments

Comments
 (0)