보라! React스러운 이 스크립트를!
저는 yarn build && yarn start
에 질렸고, nodemon
에도 질렸습니다. 좋아 그렇다면 시간도 있겠다, 한 번 이런 거 만들어보자!
일단 전 react-scripts
의 감각이 상당히 좋았으므로 react-scripts
를 뜯기 시작했습니다.
너 뭐야....
모듈화를 착실히 한 결과 깃헙에서 보기 힘들어졌잖아! 라고 하기엔 너무나 긴 코드였다.
시크한 콘솔 제거반
뭐 하여튼 이런 식으로 이것저것 뜯어가며 만들었습니다.
이거, 이거, 이거 만드는 데 들어갔음
react-scripts
가 쓰길래 저도 같이 포트 사용 여부 감지로 썼습니다.execSync('yarn start')
로 바꾸면 되지 않을까요?yarn만 쓰고, express를 쓰는 게 확실한 상황이라서 이렇게 작업했습니다.
require('dotenv').config();
const express = require('express');
const { prepareUrls, clearConsole, categoryToConsole } = require('./utilities');
const chalk = require('chalk');
const chokidar = require('chokidar');
const ts = require('typescript');
const { execSync } = require('child_process');
const detect = require('detect-port-alt');
const fs = require('fs');
const PORT = process.env.PORT || 4321;
const HOST = process.env.HOST || '0.0.0.0';
// TODO: env var HTTPS are no-op
const IS_HTTPS = process.env.HTTPS !== false;
const isInteractive = process.stdout.isTTY;
detect(PORT, HOST)
.then(result => {
if (PORT != result) {
console.log(`Something is already running on port ${PORT}`);
console.log(`Would you try to use port ${result} instead?`);
process.exit(1);
}
})
.catch(err => {
console.log(err);
process.exit(1);
})
.then(main);
function main() {
let lastServer;
const watcher = chokidar.watch('src/**/*', {
ignoreInitial: true,
persistent: true
});
watcher.emit();
if (build().hasError) {
console.log();
console.log(
`Press ${chalk.cyan('A')} key to pause/restart the Automatic Reloading.`
);
console.log(
`Press ${chalk.cyan('Ctrl + C')} key to exit this program immediately.`
);
} else {
lastServer = start();
}
let automaticReloading = true;
watcher.on('all', async (name, path) => {
if (!automaticReloading || !path || name === 'addDir') {
return;
}
console.log();
switch (name) {
case 'change':
console.log(`${chalk.yellow('!')} File Changed: ${path}`);
break;
case 'add':
console.log(`${chalk.yellow('!')} File Added: ${path}`);
break;
case 'unlink':
console.log(`${chalk.yellow('!')} File Removed: ${path}`);
break;
case 'unlinkDir':
console.log(`${chalk.yellow('!')} Folder Removed: ${path}`);
break;
}
const { hasError, updatedFiles } = build();
if (hasError) {
return;
}
for (const file of updatedFiles) {
try {
delete require.cache[require.resolve(file)];
} catch (_e) {
// Not found on require cache. Maybe the file was added ;)
}
}
const startServer = () => {
console.log(chalk.gray('Server closed, Starting development server...'));
lastServer = start();
};
if (!lastServer) {
startServer();
} else {
lastServer.close(startServer);
}
});
const stdin = process.stdin;
// without this, we would only get streams once enter is pressed
stdin.setRawMode(true);
// resume stdin in the parent process (node app won't quit all by itself
// unless an error or process.exit() happens)
stdin.resume();
// i don't want binary, do you?
stdin.setEncoding('utf8');
// on any data into stdin
stdin.on('data', key => {
// ctrl-c ( end of text )
if (key === '\u0003') {
process.exit();
}
if (key === 'A' || key === 'a') {
if (automaticReloading) {
console.log(
chalk.yellow('Automatic Reloading paused. Feel free to edit!')
);
automaticReloading = false;
} else {
console.log(
chalk.green(
'Automatic Reloading started. Edit any file to reload it.'
)
);
automaticReloading = true;
}
}
});
}
const typescriptFile = /.tsx?$/;
const files = fs
.readdirSync(`${process.cwd()}/src`)
.filter(it => typescriptFile.test(it))
.map(it => `src/${it}`);
function build() {
console.log(`${chalk.cyan('i')} Waiting for TypeScript compiling...`);
if (!fs.existsSync(`${process.cwd()}/tsconfig.json`)) {
console.error(`${chalk.red('!')} No 'tsconfig.json' file.`);
return { hasError: true, updatedFiles: [] };
}
const config = require(`${process.cwd()}/tsconfig.json`);
const { options, errors } = ts.convertCompilerOptionsFromJson(
config.compilerOptions,
process.cwd()
);
const program = ts.createProgram(files, {
listEmittedFiles: true,
...options
});
const emitResult = program.emit();
const diagnostics = errors
.concat(ts.getPreEmitDiagnostics(program))
.concat(emitResult.diagnostics);
const errorCount = diagnostics.filter(
it => it.category === ts.DiagnosticCategory.Error
).length;
if (errorCount && isInteractive) {
clearConsole();
console.log(`${chalk.red('!')} Compilation failed!`);
console.log();
}
for (const diagnostic of diagnostics) {
const { color, category } = categoryToConsole(diagnostic.category);
if (!diagnostic.file) {
console.log(
`${color(category)} ${chalk.gray(`TS${diagnostic.code}:`)} ${
diagnostic.messageText
}`
);
continue;
}
const { line, character } = ts.getLineAndCharacterOfPosition(
diagnostic.file,
diagnostic.start
);
const filename = `${chalk.cyan(diagnostic.file.fileName)}:${chalk.yellow(
line + 1
)}:${chalk.yellow(character + 1)}`;
console.log(
`${filename} - ${color(category)} ${chalk.gray(
`TS${diagnostic.code}:`
)} ${diagnostic.messageText}`
);
console.log();
const lineStart = ts.getPositionOfLineAndCharacter(
diagnostic.file,
line,
0
);
console.log(
`${chalk.bgWhite(
chalk.black(` ${line + 1} `)
)} ${diagnostic.file.text.substring(
lineStart,
diagnostic.start + diagnostic.length
)}`
);
console.log(
`${chalk.bgWhite(
(line + 1).toString().replace(/./g, ' ') + ' '
)} ${Array(diagnostic.start - lineStart)
.fill(' ')
.join('')}${color(
Array(diagnostic.length)
.fill('~')
.join('')
)}`
);
}
if (errorCount) {
console.log();
console.log(`Found ${errorCount} error${errorCount != 1 ? 's' : ''}.`);
}
return {
hasError: errorCount > 0,
updatedFiles: emitResult.emittedFiles || []
};
}
function start() {
const app = express();
const { initializeWebServer } = require('../dist/app');
initializeWebServer(app);
return app.listen(PORT, () => {
if (isInteractive) {
clearConsole();
console.log(chalk.green('Compiled Successfully!'));
}
console.log();
console.log(
`${chalk.bold('app-name')} listening on ${chalk.cyan('' + PORT)}!`
);
console.log();
const { lanUrl, localUrl } = prepareUrls({
isHttps: false,
port: PORT
});
console.log(` ${chalk.bold('Local')} : ${localUrl}`);
if (lanUrl) {
console.log(` ${chalk.bold('On Your Network')} : ${lanUrl}`);
}
console.log();
console.log('Note that the development server is not optimized.');
console.log(
`To create a production build, use ${chalk.cyan('yarn build')}.`
);
console.log(
`Press ${chalk.cyan('A')} key to pause/restart the Automatic Reloading.`
);
console.log(
`Press ${chalk.cyan('Ctrl + C')} key to exit this program immediately.`
);
});
}
const address = require('address');
const chalk = require('chalk');
const ts = require('typescript');
function prepareUrls({ isHttps, port }) {
const protocol = isHttps ? 'https' : 'http';
const formatUrl = hostname =>
`${protocol}://${hostname}${port ? `:${port}` : ''}/`;
let lanUrl;
try {
// This can only return an IPv4 address
let lanUrlForConfig = address.ip();
if (lanUrlForConfig) {
// Check if the address is a private ip
// https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
if (
/^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test(
lanUrlForConfig
)
) {
// Address is private, format it for later use
lanUrl = formatUrl(lanUrlForConfig);
}
}
} catch (_e) {
// ignored
}
return {
lanUrl,
localUrl: formatUrl('localhost')
};
}
function clearConsole() {
process.stdout.write(
process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'
);
}
function categoryToConsole(category) {
switch (category) {
case ts.DiagnosticCategory.Error:
return {
color: chalk.red,
category: 'error'
};
case ts.DiagnosticCategory.Message:
return {
color: chalk.blue,
category: 'message'
};
case ts.DiagnosticCategory.Suggestion:
return {
color: chalk.green,
category: 'suggest'
};
case ts.DiagnosticCategory.Warning:
return {
color: chalk.yellow,
category: 'warning'
};
default:
return {
color: s => s,
category: 'unknown'
};
}
}
module.exports = { prepareUrls, clearConsole, categoryToConsole };
이런 거 하지 마요. 이걸로 하루가 날아갔음.
+2019-07-06: 이것저것 메챠쿠챠 더 수정함
백엔드는 잘 모르지만 잘 보고갑니다!! 고생하셨어요~