|
1 | | -import { readFile } from 'node:fs/promises' |
| 1 | +import { readFile, watch } from 'node:fs/promises' |
2 | 2 | import { existsSync, mkdirSync, writeFileSync } from 'node:fs' |
3 | 3 | import { basename, dirname, resolve } from 'node:path' |
4 | 4 |
|
@@ -36,6 +36,22 @@ const COMPILED_FILE = 'add-on.json' |
36 | 36 |
|
37 | 37 | const ASSETS_DIR = 'assets' |
38 | 38 |
|
| 39 | +const ADD_ON_DEV_IGNORE_PREFIXES = [ |
| 40 | + '.add-on/', |
| 41 | + '.git/', |
| 42 | + 'node_modules/', |
| 43 | + '.turbo/', |
| 44 | + 'dist/', |
| 45 | +] |
| 46 | + |
| 47 | +function shouldIgnoreDevPath(path: string) { |
| 48 | + const normalized = path.replace(/\\/g, '/') |
| 49 | + if (normalized === COMPILED_FILE) return true |
| 50 | + return ADD_ON_DEV_IGNORE_PREFIXES.some((prefix) => |
| 51 | + normalized.startsWith(prefix), |
| 52 | + ) |
| 53 | +} |
| 54 | + |
39 | 55 | export function camelCase(str: string) { |
40 | 56 | return str |
41 | 57 | .split(/(\.|-|\/)/) |
@@ -218,6 +234,56 @@ export async function initAddOn(environment: Environment) { |
218 | 234 | await compileAddOn(environment) |
219 | 235 | } |
220 | 236 |
|
| 237 | +export async function devAddOn(environment: Environment) { |
| 238 | + await initAddOn(environment) |
| 239 | + |
| 240 | + environment.info( |
| 241 | + 'Add-on dev mode is running.', |
| 242 | + 'Watching project files and recompiling add-on output on changes. Press Ctrl+C to stop.', |
| 243 | + ) |
| 244 | + |
| 245 | + const abortController = new AbortController() |
| 246 | + let debounceTimeout: ReturnType<typeof setTimeout> | undefined |
| 247 | + |
| 248 | + const rerun = async () => { |
| 249 | + try { |
| 250 | + await updateAddOnInfo(environment) |
| 251 | + await compileAddOn(environment) |
| 252 | + environment.info('Add-on updated.', 'Compiled add-on.json and refreshed .add-on') |
| 253 | + } catch (error) { |
| 254 | + const message = error instanceof Error ? error.message : String(error) |
| 255 | + environment.error('Failed to rebuild add-on.', message) |
| 256 | + } |
| 257 | + } |
| 258 | + |
| 259 | + process.once('SIGINT', () => { |
| 260 | + abortController.abort() |
| 261 | + }) |
| 262 | + |
| 263 | + try { |
| 264 | + for await (const event of watch(process.cwd(), { |
| 265 | + recursive: true, |
| 266 | + signal: abortController.signal, |
| 267 | + })) { |
| 268 | + const file = event.filename?.toString() |
| 269 | + if (!file || shouldIgnoreDevPath(file)) continue |
| 270 | + |
| 271 | + if (debounceTimeout) { |
| 272 | + clearTimeout(debounceTimeout) |
| 273 | + } |
| 274 | + |
| 275 | + debounceTimeout = setTimeout(() => { |
| 276 | + void rerun() |
| 277 | + }, 200) |
| 278 | + } |
| 279 | + } catch (error) { |
| 280 | + const message = error instanceof Error ? error.message : String(error) |
| 281 | + if (!message.includes('aborted')) { |
| 282 | + throw error |
| 283 | + } |
| 284 | + } |
| 285 | +} |
| 286 | + |
221 | 287 | export async function loadRemoteAddOn(url: string): Promise<AddOn> { |
222 | 288 | const response = await fetch(url) |
223 | 289 | const jsonContent = await response.json() |
|
0 commit comments