Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: fast refresh #35

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
"devDependencies": {
"prettier": "^3.0.3",
"turbo": "^1.10.13"
}
},
"packageManager": "[email protected]+sha256.5858806c3b292cbec89b5533662168a957358e2bbd86431516d441dc1aface89"
}
24 changes: 20 additions & 4 deletions packages/commandkit/bin/common.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ export function findPackageJSON() {
return JSON.parse(fs.readFileSync(target, 'utf8'));
}

export function findTsconfig() {
const cwd = process.cwd();
const target = join(cwd, 'tsconfig.json');

if (!fs.existsSync(target)) {
return null;
}

return target;
}

const possibleFileNames = [
'commandkit.json',
'commandkit.config.json',
Expand All @@ -69,24 +80,29 @@ const possibleFileNames = [
'commandkit.config.cjs',
];

export async function findCommandKitConfig(src) {

export async function findCommandKitConfig(src, skipLoad = false) {
const cwd = process.cwd();
const locations = src ? [join(cwd, src)] : possibleFileNames.map((name) => join(cwd, name));

for (const location of locations) {
try {
return await loadConfigInner(location);
const conf = await loadConfigInner(location, skipLoad);
if (!conf) continue;
return conf;
} catch (e) {
continue;
}
}

if (skipLoad) return null;
panic('Could not locate commandkit config.');
}

async function loadConfigInner(target) {
async function loadConfigInner(target, skipLoad = false) {
const isJSON = target.endsWith('.json');

if (skipLoad) return fs.existsSync(target) ? target : null;

/**
* @type {import('..').CommandKitConfig}
*/
Expand Down
267 changes: 182 additions & 85 deletions packages/commandkit/bin/development.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,35 @@ import { parseEnv } from './parse-env.mjs';
import child_process from 'node:child_process';
import ora from 'ora';
import { injectShims } from './build.mjs';
import { CommandKit, CommandKitSignalType } from '../dist';
import { watchFiles } from './watcher.mjs';

const RESTARTING_MSG_PATTERN = /^Restarting '|".+'|"\n?$/;
const FAILED_RUNNING_PATTERN = /^Failed running '.+'|"\n?$/;

function readEnv(envExtra) {
const processEnv = {};
const env = dotenv({
path: join(process.cwd(), '.env'),
// @ts-expect-error
processEnv,
});

if (envExtra) {
parseEnv(processEnv);
}

if (env.error) {
write(Colors.yellow(`[DOTENV] Warning: ${env.error.message}`));
}

if (env.parsed) {
write(Colors.blue('[DOTENV] Loaded .env file!'));
}

return processEnv;
}

export async function bootstrapDevelopmentServer(opts) {
const {
src,
Expand All @@ -35,18 +60,34 @@ export async function bootstrapDevelopmentServer(opts) {
const status = ora(Colors.green('Starting a development server...\n')).start();
const start = performance.now();

if (watchMode && !nodeOptions.includes('--watch')) {
nodeOptions.push('--watch');
} else if (!watchMode && nodeOptions.includes('--watch')) {
nodeOptions.splice(nodeOptions.indexOf('--watch'), 1);
}
// if (watchMode && !nodeOptions.includes('--watch')) {
// nodeOptions.push('--watch');
// } else if (!watchMode && nodeOptions.includes('--watch')) {
// nodeOptions.splice(nodeOptions.indexOf('--watch'), 1);
// }

if (!nodeOptions.includes('--enable-source-maps')) {
nodeOptions.push('--enable-source-maps');
}

erase('.commandkit');

let watchEmitted = false, serverProcess = null, watching = false;
const watchModeMetadata = {
didEventsChange: false,
didCommandsChange: false,
didValidatorsChange: false,
didOthersChange: false,
};

const knownPaths = {
commands: '',
events: '',
validators: '',
source: join(process.cwd(), src),
config: await findCommandKitConfig(opts.config, true)
};

try {
await build({
clean: true,
Expand All @@ -61,107 +102,163 @@ export async function bootstrapDevelopmentServer(opts) {
silent: true,
entry: [src, '!dist', '!.commandkit', `!${outDir}`].filter(Boolean),
watch: watchMode,
});
async onSuccess() {
await injectShims('.commandkit', main, false, requirePolyfill);
status.succeed(
Colors.green(`Dev server started in ${(performance.now() - start).toFixed(2)}ms!\n`),
);
const processEnv = readEnv(envExtra);
if (watchMode) {
if (!watchEmitted) { write(Colors.cyan('Watching for file changes...\n')); watchEmitted = true }
if (!watching) {
watchFiles(Object.values(knownPaths).filter(Boolean), (path) => {
watchModeMetadata.didCommandsChange = path === knownPaths.commands;
watchModeMetadata.didEventsChange = path === knownPaths.events;
watchModeMetadata.didValidatorsChange = path === knownPaths.validators;

await injectShims('.commandkit', main, false, requirePolyfill);

status.succeed(
Colors.green(`Dev server started in ${(performance.now() - start).toFixed(2)}ms!\n`),
);

if (watchMode) write(Colors.cyan('Watching for file changes...\n'));

const processEnv = {};
watchModeMetadata.didOthersChange = [
watchModeMetadata.didCommandsChange,
watchModeMetadata.didEventsChange,
watchModeMetadata.didValidatorsChange,
].every(p => !p);
});
watching = true;
}
}

const env = dotenv({
path: join(process.cwd(), '.env'),
// @ts-expect-error
processEnv,
serverProcess = triggerRestart({
serverProcess,
processEnv,
main,
nodeOptions,
clearRestartLogs,
...watchModeMetadata,
});
}
});
} catch (e) {
status.fail(`Error occurred after ${(performance.now() - start).toFixed(2)}ms!\n`);
panic(e.stack ?? e);
}
}

if (envExtra) {
parseEnv(processEnv);
async function triggerRestart({
serverProcess,
processEnv,
main,
nodeOptions,
clearRestartLogs,
didEventsChange,
didCommandsChange,
didValidatorsChange,
didOthersChange
}) {
if (didOthersChange && serverProcess) {
serverProcess.kill();
} else if (!didOthersChange && serverProcess) {
/**
* @type {import('node:child_process').ChildProcessWithoutNullStreams}
*/
const commandkit = serverProcess;
if (didEventsChange) {
commandkit.send(CommandKitSignalType.ReloadEvents);
write(Colors.cyan('⌀ Reloading events...'));
}

if (env.error) {
write(Colors.yellow(`[DOTENV] Warning: ${env.error.message}`));
if (didCommandsChange) {
commandkit.send(CommandKitSignalType.ReloadCommands);
write(Colors.cyan('⌀ Reloading commands...'));
}

if (env.parsed) {
write(Colors.blue('[DOTENV] Loaded .env file!'));
if (didValidatorsChange) {
commandkit.send(CommandKitSignalType.ReloadValidations);
write(Colors.cyan('⌀ Reloading validators...'));
}

/**
* @type {child_process.ChildProcessWithoutNullStreams}
*/
const ps = child_process.spawn(
'node',
[...nodeOptions, join(process.cwd(), '.commandkit', main)],
{
env: {
...process.env,
...processEnv,
NODE_ENV: 'development',
// @ts-expect-error
COMMANDKIT_DEV: true,
// @ts-expect-error
COMMANDKIT_PRODUCTION: false,
},
cwd: process.cwd(),
return serverProcess;
}

return bootstrapNodeServer({
main,
nodeOptions,
processEnv,
clearRestartLogs,
});
}

function bootstrapNodeServer({
main,
nodeOptions,
processEnv,
clearRestartLogs,
}) {
/**
* @type {child_process.ChildProcessWithoutNullStreams}
*/
const ps = child_process.spawn(
'node',
[...nodeOptions.filter(o => o !== '--watch'), join(process.cwd(), '.commandkit', main)],
{
env: {
...process.env,
...processEnv,
NODE_ENV: 'development',
COMMANDKIT_DEV: true,
COMMANDKIT_PRODUCTION: false,
},
);
cwd: process.cwd(),
},
);

let isLastLogRestarting = false,
hasStarted = false;
let isLastLogRestarting = false,
hasStarted = false;

ps.stdout.on('data', (data) => {
const message = data.toString();
ps.stdout.on('data', (data) => {
const message = data.toString();

if (FAILED_RUNNING_PATTERN.test(message)) {
write(Colors.cyan('Failed running the bot, waiting for changes...'));
isLastLogRestarting = false;
if (FAILED_RUNNING_PATTERN.test(message)) {
write(Colors.cyan('Failed running the bot, waiting for changes...'));
isLastLogRestarting = false;
if (!hasStarted) hasStarted = true;
return;
}

if (clearRestartLogs && !RESTARTING_MSG_PATTERN.test(message)) {
write(message);
isLastLogRestarting = false;
} else {
if (isLastLogRestarting || !hasStarted) {
if (!hasStarted) hasStarted = true;
return;
}
write(Colors.cyan('⌀ Restarting the bot...'));
isLastLogRestarting = true;
}

if (clearRestartLogs && !RESTARTING_MSG_PATTERN.test(message)) {
write(message);
isLastLogRestarting = false;
} else {
if (isLastLogRestarting || !hasStarted) {
if (!hasStarted) hasStarted = true;
return;
}
write(Colors.cyan('⌀ Restarting the bot...'));
isLastLogRestarting = true;
}
if (!hasStarted) hasStarted = true;
});

if (!hasStarted) hasStarted = true;
});

ps.stderr.on('data', (data) => {
const message = data.toString();
ps.stderr.on('data', (data) => {
const message = data.toString();

if (
message.includes(
'ExperimentalWarning: Watch mode is an experimental feature and might change at any time',
)
if (
message.includes(
'ExperimentalWarning: Watch mode is an experimental feature and might change at any time',
)
return;
)
return;

write(Colors.red(message));
});
write(Colors.red(message));
});

ps.on('close', (code) => {
write('\n');
process.exit(code ?? 0);
});
ps.on('close', (code) => {
write('\n');
process.exit(code ?? 0);
});

ps.on('error', (err) => {
panic(err);
});
} catch (e) {
status.fail(`Error occurred after ${(performance.now() - start).toFixed(2)}ms!\n`);
panic(e.stack ?? e);
}
}
ps.on('error', (err) => {
panic(err);
});

return ps;
}
9 changes: 9 additions & 0 deletions packages/commandkit/bin/watcher.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @ts-check

import { watch } from 'chokidar'

export function watchFiles(target, callback) {
const watcher = watch(target);

return watcher.on('all', callback);
}
Loading