Недели 9-10 · HTML Builder
🎯 Что строим
Смена среды: ты выходишь из браузера и заходишь в Node.js. Раньше код жил в DOM, теперь — в процессе ОС с доступом к файловой системе.
Шесть мини-скриптов нарастающей сложности на чистом Node.js (только встроенные модули). Финал — статический site-builder: HTML-шаблон с {{tags}} собирается из компонентов + CSS-бандл + копирование assets/.
Subtasks (запуск из корня репо node <папка>):
| # | Папка | Баллы | Что делает |
|---|---|---|---|
| 01 | 01-read-file |
40 | Печатает text.txt в консоль через ReadStream |
| 02 | 02-write-file |
50 | Принимает stdin через readline, appendFile в файл, exit/Ctrl+C — farewell |
| 03 | 03-files-in-folder |
50 | fs.readdir + fs.stat, формат name - ext - size |
| 04 | 04-copy-directory |
70 | Копирует папку, удаляя файлы которых нет в источнике. БЕЗ fsPromises.cp |
| 05 | 05-merge-styles |
45 | Конкат всех .css из styles/ → bundle.css |
| 06 | 06-build-page |
135 | template.html + {{component}} + бандл CSS + копия assets/ → project-dist/ |
🏷 Required Skills (как заявлено в задании)
Node.js · fs / fs.promises · streams · events · readline · path module · process · CommonJS modules · command-line scripts
🚫 Запреты (нарушишь — штрафы драконовские)
| ❌ | Что нельзя | Штраф | Что вместо этого |
|---|---|---|---|
| 🔴 | Любой npm-пакет в любом subtask | -390 (вся задача в 0) | Только встроенные модули Node.js |
| 🔴 | Репо не публичный на момент cross-check | -390 | Public на GitHub |
| 🔴 | Синхронные fs.*Sync (readFileSync, statSync, writeFileSync…) |
-40 за subtask (до -240) | fs.promises.* или callback API |
| 🔴 | setTimeout в любом виде |
-30 | События/promises/streams |
| 🔴 | fsPromises.cp в задаче 04 |
-40 | Рекурсивный обход: readdir → stat → copyFile/mkdir |
| 🔴 | fsPromises.cp в задаче 06 |
-40 | То же |
| 🟡 | ESM (import/export) |
— | Только CommonJS: require/module.exports |
| 🟡 | Не-LTS Node.js | — | LTS-версия |
💡 Разрешено в
package.json: ESLint, Prettier и@types/nodeкак devDependencies (они идут в template из коробки и не вызываются твоим кодом).
⛰ Compound: что ты несёшь из прошлого
⚠️ Это первая Node.js-задача bootcamp. Среда меняется фундаментально.
Из всего что было раньше — работает только JS-синтаксис (переменные, функции, async/await, классы, деструктуризация). Всё остальное (DOM,
window,localStorage,fetchкак глобал в браузере,<script>, css, html в рантайме) — здесь не существует.Новый мир:
- Нет DOM — есть stdin/stdout/stderr и файловая система
- Нет
window— естьglobalиprocess- Нет браузера — есть процесс операционной системы с PID, env-переменными, exit codes
- Нет CSS — текст это просто строки в файлах, ты их склеиваешь руками
- Нет fetch-as-global — нужен
require('node:http')или встроенныйfetch(с Node 18+)Что переносится:
- async/await, Promise, then/catch
- Замыкания, callback-паттерны
- JSON, регулярки, методы строк/массивов
- Понимание Event Loop (но в Node он шире, см. блок 4)
📚 Что изучить (по порядку)
⚠️ Идти строго по порядку. Сначала «что такое Node вообще», потом модули, потом stream-механика. Если прыгнуть сразу в streams — застрянешь, потому что не поймёшь как они связаны с EventEmitter.
📥 Что должен знать ДО старта
- JS-синтаксис ES2020+ (async/await, деструктуризация, spread, optional chaining)
- Promise (chains,
Promise.all, error handling) - Базовый CLI: cd/ls/mkdir, как запустить
node script.js - Git basics (commits, branches, PR)
1 · Что такое Node.js — смена среды ⭐⭐⭐
Зачем: прежде чем писать код, нужно понять что ты вообще запускаешь. Node — это не браузер, это V8 + libuv + биндинги к OS.
- Что такое Node.js — runtime, V8, libuv, разница с браузером
- Установка и nvm — почему LTS, как переключать версии
- REPL и запуск скриптов —
node,node script.js, как читать stdin - Структура Node.js проекта и слои — где
index.js, где модули, что такое entry point
Self-check:
- Что такое V8 и libuv? Кто из них отвечает за event loop?
- Чем
node script.jsотличается от<script src="script.js">в браузере? - Где живёт
globalв Node, и почемуwindowне существует? - Почему задание требует именно LTS, а не latest?
2 · Модули — CommonJS (а не ESM) ⭐⭐⭐
Зачем: задание явно требует CommonJS. Если напишешь import fs from 'fs' — будет ошибка Cannot use import statement outside a module или потеря баллов за нарушение конвенции.
- CommonJS vs ES Modules в Node —
requirevsimport, синхронность загрузки,module.exportsvsexport - Глобалы CJS:
__dirname,__filename,module,exports,require
Шпаргалка:
| Что | CommonJS | ESM |
|---|---|---|
| Импорт | const fs = require('fs') |
import fs from 'fs' |
| Экспорт | module.exports = ... |
export default ... |
| Путь к файлу | __dirname, __filename |
import.meta.url |
| Загрузка | синхронная | асинхронная |
| Расширение | .js (если "type" не module) |
.mjs или "type": "module" |
Self-check:
- Что вернёт
require('./foo')еслиfoo.jsэкспортирует черезmodule.exports = { a: 1 }? - Зачем существует
__dirname? Что он содержит когда я запускаюnode 01-read-fileиз корня репо? - Почему
__filenameкритичен для построения путей в этой задаче?
3 · path — собираем пути правильно ⭐⭐
Зачем: задание запускается с разных машин (cross-check). Хардкод './01-read-file/text.txt' или \\ (Windows-разделители) — сломается. Только path.join(__dirname, ...).
- path —
path.join,path.resolve,path.parse,path.extname,path.basename
Шпаргалка:
const path = require('node:path');
path.join(__dirname, 'text.txt'); // абсолютный путь к файлу рядом со скриптом
path.extname('style.css'); // '.css' (полезно в 03 и 05)
path.parse('/foo/bar/style.css'); // { dir, base, name, ext, root }
Self-check:
- Чем
path.joinотличается от строки__dirname + '/text.txt'? - Что вернёт
path.extname('Readme')? Аpath.extname('.gitignore')? - Почему в задаче 05 фильтр
path.extname(file) === '.css'надёжнее чемfile.endsWith('.css')?
4 · Event Loop в Node — это не браузерный ⭐⭐⭐
Зачем: все асинхронные операции (fs, readline, streams) живут на event loop. В Node он состоит из фаз (timers, pending, poll, check, close) — это шире чем в браузере. Без понимания механики невозможно отлаживать гонки и зависшие промисы.
- Event Loop в Node — фазы (timers, poll, check, close callbacks)
- microtasks vs macrotasks:
process.nextTick> Promise > setImmediate > setTimeout
Self-check:
- В каком порядке выполнятся:
setTimeout(fn, 0),Promise.resolve.then(fn),process.nextTick(fn),setImmediate(fn)? - Почему
setTimeoutзапрещён в этой задаче, аsetImmediate— нет (но мы и его не используем)? - Что такое microtask queue и почему promises его используют?
5 · process — твой интерфейс к ОС ⭐⭐
Зачем: в задаче 02 нужно слушать Ctrl+C (SIGINT) и красиво выходить. В задаче 03 stdin/stdout управляются через process. Все CLI-аргументы — в process.argv.
- process —
process.stdin,process.stdout,process.argv,process.exit, обработчики сигналов
Шпаргалка:
| Что | Где |
|---|---|
| Аргументы команды | process.argv (массив) |
| stdin (что ввёл user) | process.stdin (Readable stream) |
stdout (console.log под капотом) |
process.stdout (Writable stream) |
| Завершение процесса | process.exit(0) (success) / process.exit(1) (error) |
| Ctrl+C | process.on('SIGINT', handler) |
| Текущая папка запуска | process.cwd (НЕ __dirname) |
Self-check:
- В чём разница между
__dirnameиprocess.cwd? Что вернёт каждый приnode 01-read-fileиз корня репо? - Что произойдёт если в задаче 02 не повесить хендлер на
SIGINT? - Зачем нужен
process.exit(0)после farewell message?
6 · fs / fs.promises — файловая система ⭐⭐⭐
Зачем: это сердце задачи. Все 6 subtasks так или иначе работают с файлами. Запрет на синхронные методы — главная ловушка.
- fs — Promises API (
fs.promises.readFile,readdir,stat,mkdir,copyFile,unlink,rm,writeFile,appendFile)
Шпаргалка асинхронных альтернатив:
| ❌ Запрещено | ✅ Используй |
|---|---|
fs.readFileSync(p) |
await fs.promises.readFile(p, 'utf-8') |
fs.writeFileSync(p, data) |
await fs.promises.writeFile(p, data) |
fs.appendFileSync(p, data) |
await fs.promises.appendFile(p, data) |
fs.statSync(p) |
await fs.promises.stat(p) |
fs.readdirSync(p) |
await fs.promises.readdir(p, { withFileTypes: true }) |
fs.mkdirSync(p, opts) |
await fs.promises.mkdir(p, { recursive: true }) |
fs.rmSync(p, opts) |
await fs.promises.rm(p, { recursive: true, force: true }) |
fsPromises.cp(src, dst) ⛔ задачи 04/06 |
руками: readdir → copyFile/mkdir |
withFileTypes: true — критичная опция для задачи 03 и 04: возвращает Dirent с методом dirent.isFile / isDirectory, что быстрее чем отдельный stat на каждый элемент.
Self-check:
- Чем
fs.promises.readFile(p)отличается отfs.readFile(p, cb)? - Что вернёт
await fs.promises.readdir('./styles', { withFileTypes: true })? Чем это отличается от без опции? - Как написать «удали папку рекурсивно и создай заново» одной операцией?
- Что произойдёт если вызвать
fs.promises.mkdir('a/b/c')безrecursive: trueкогдаa/bещё не существует?
7 · Streams ⭐⭐⭐⭐ — главная новая концепция
Зачем: задача 01 требует ReadStream (за это +20 баллов). Streams — это способ работать с данными по частям (chunks), не загружая весь файл в память. Это совершенно новая ментальная модель: вместо data = await readFile ты подписываешься на события data и склеиваешь куски.
- Stream API -- потоки — Readable / Writable / Duplex / Transform,
.pipe, события (data,end,error,close) - Stream -- backpressure — что делать когда writer медленнее reader; зачем
.pipeэто решает автоматически - Stream -- async iterators — современный способ:
for await (const chunk of readable) {}вместо.on('data', ...)
Минимальный пример для задачи 01:
const fs = require('node:fs');
const path = require('node:path');
const stream = fs.createReadStream(path.join(__dirname, 'text.txt'));
stream.pipe(process.stdout);
// или:
stream.on('data', (chunk) => process.stdout.write(chunk));
stream.on('error', (err) => console.error(err));
Self-check:
- Зачем читать файл потоком если можно
readFileодной командой? (подсказка: файл 10 ГБ) - Что такое backpressure? Почему
readable.pipe(writable)решает это автоматически? - Чем отличается режим
flowingотpausedу Readable stream? - Какие 3 типа stream-событий ты обязательно обрабатываешь?
8 · EventEmitter ⭐⭐
Зачем: streams, readline, process, fs.watch — всё это EventEmitter под капотом. Понимая базовый паттерн, ты понимаешь всю экосистему.
- events -- EventEmitter —
emitter.on,.once,.emit,.off, утечки слушателей
Self-check:
- Что произойдёт если повесить 11+ listeners на один эвент? (подсказка: warning)
- Чем
.onотличается от.once? - Почему
stream.on('error', ...)обязателен — что случится без него?
9 · readline — для задачи 02 ⭐⭐
Зачем: задача 02 требует читать ввод пользователя построчно. readline — встроенный модуль для этого. Альтернатива (читать process.stdin напрямую как stream) сложнее.
- Официальные доки: https://nodejs.org/api/readline.html
- Сценарий:
readline.createInterface({ input: process.stdin, output: process.stdout })→rl.question(prompt, cb)илиrl.on('line', line => ...) - Закрытие:
rl.close()→ событиеclose
Шаблон под subtask 02:
const readline = require('node:readline');
const fs = require('node:fs');
const path = require('node:path');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const filePath = path.join(__dirname, 'output.txt');
rl.write('Hello! Type something. Type "exit" or Ctrl+C to quit.\n');
rl.on('line', async (line) => {
if (line.trim() === 'exit') return rl.close();
await fs.promises.appendFile(filePath, line + '\n');
});
rl.on('close', () => {
console.log('Bye!');
process.exit(0);
});
process.on('SIGINT', () => rl.close());
Self-check:
- Зачем нужен и
rl.on('line')иprocess.on('SIGINT')— что обрабатывает каждый? - Что вернёт
rl.close()? - Чем
rl.question(prompt, cb)отличается отrl.on('line', cb)?
10 · Архитектура для финального задания 06
Задача 06 — это композиция 04 + 05 + парсер шаблонов. Разложи на функции:
buildHtml(templatePath, componentsDir, outPath)— читает template, находит все{{name}}черезreplace(/{{([^}]+)}}/g, ...)(с trim ключа из-за «двух тегов через пробелы»), подставляет содержимоеcomponents/<name>.htmlbuildStyles(stylesDir, outFile)— переиспользуй логику из задачи 05copyAssets(srcDir, dstDir)— переиспользуй логику из задачи 04main—mkdir project-dist+ параллельныйPromise.all([buildHtml, buildStyles, copyAssets])
- Структура Node.js проекта и слои — как организовать модули внутри одного скрипта
Ловушки задачи 06:
- ⚠️
template.htmlне должен меняться (читай его в память, не пиши обратно в него) - ⚠️ Только
.htmlфайлы изcomponents/— остальные игнорить - ⚠️ Два тега подряд через пробел
{{a}} {{b}}— оба должны заменяться (regex/gэто покрывает) - ⚠️ При повторном запуске —
project-dist/обновляется (удаляй старое перед записью) - ⚠️
assets/копируется рекурсивно, безfsPromises.cp
Self-check:
- Какой regex заменяет все
{{component-name}}в шаблоне? Что если в имени пробел или кавычка? - Что произойдёт если
components/foo.htmlотсутствует, а в template есть{{foo}}? - Как обновить
project-dist/при повторном запуске —rm -rfвсё или диффать?
11 · Workflow
- Repo Workflow для bootcamp — Use this template на HTML-builder, public репо
- Git Commit Convention —
feat:/fix:/refactor:+ timestamp в скобках - PR Description Schema — PR из ветки в
main, не мержить, описание по схеме - Cross-Check процесс — после deadline 3 дня на проверку чужих работ
⚠️ После
npm installв template подтянутся ESLint, Prettier,@types/node— это devDependencies, они не считаются «third-party модулями». Их использовать в коде нельзя, они только для линта.
✅ Чек-лист критериев (390 баллов)
01 · Read a file · 40
node 01-read-fileпечатает содержимоеtext.txt(+20)- Используется ReadStream (
createReadStream), без sync (+20)
02 · Write to file · 50
- При запуске создаётся файл и печатается prompt (
+10) - Каждая строка ввода дозаписывается в файл (предыдущее сохраняется) (
+15) - После ввода процесс ждёт следующего ввода, не падает (
+5) exit→ farewell + завершение (+10)Ctrl+C→ farewell + завершение (+10)
03 · Files in folder · 50
- Перечисляются файлы из
secret-folder(+15) - Формат
<name> - <ext> - <size>(+20) - Подпапки не перечисляются (
+15)
04 · Copy directory · 70
files-copyсуществует и точно зеркалитfiles(+30)- При повторном запуске после добавления/изменения — обновляется (
+20) - При повторном запуске после удаления — удалённые файлы исчезают из копии (
+20) - БЕЗ
fsPromises.cp(-40если использован)
05 · Merge styles · 45
bundle.cssсуществует, содержит конкат всех.css(+20)- Не-CSS файлы и подпапки игнорятся (
+10) - Повторный запуск перезаписывает bundle актуальным контентом (
+15)
06 · Build page · 135
project-dist/сindex.html,style.css,assets/(+20)index.htmlсобирается из template + components (+35)style.css— bundle изstyles/(+20)assets/— точная копия исходной (+20)template.htmlне изменяется скриптом (+10)- Два тега подряд
{{a}} {{b}}обрабатываются корректно (+10) - Повторный запуск обновляет index/style/assets (
+20) - БЕЗ
fsPromises.cp(-40если использован)
Submission & общие правила
- Репо public на GitHub (иначе
-390) - Только встроенные модули Node (иначе
-390) - Ни одной sync-функции
fs(иначе-40за subtask) - Никаких
setTimeout(иначе-30) - Все импорты — CommonJS (
require) - LTS Node.js
- PR в
main, не смержен - Ссылка засабмичена в rs app → Cross-Check: Submit
🧠 Self-check перед коммитом
- Запускается ли каждый subtask из корня репо командой
node <folder>? (cross-check будет запускать именно так) - В коде нет ни одного
*Syncметодаfs? (Grep наSync(по всему репо — пусто?) - В коде нет
setTimeout? (Grep наsetTimeout— пусто?) - В коде нет
fsPromises.cp/fs.cpв задачах 04 и 06? - В
package.jsonнет ни одной runtime-зависимости кроме того что было в template? - Все пути собраны через
path.join(__dirname, ...), без хардкода слешей? - Stream-ошибки обрабатываются (
stream.on('error', ...)) везде где stream используется? - Задача 02 корректно ловит и
exit, иCtrl+C? - Задача 04 при повторном запуске удаляет файлы которых больше нет в источнике?
- Задача 06 не модифицирует
template.html? - PR создан из feature-ветки в
main, не смержен? - PR-description содержит список протестированных subtasks?
➡️ Что переходит в следующие задачи (compound forward)
После HTML Builder в Async Race:
- Назад в браузер: эта задача — единственная в bootcamp где ты пишешь Node, дальше снова DOM. Но опыт работы с
EventEmitter, async-паттернами и stream-мышлением останется.
В дальнейшей карьере backend:
- Блок 2 (CommonJS) — каркас понимания модулей, дальше будет важно когда перейдёшь на ESM/TypeScript
- Блок 4 (Event Loop) — фундамент для отладки производительности backend
- Блок 5 (process) — нужен для любых CLI-утилит и Docker-контейнеров
- Блок 6 (fs.promises) — основа любой работы с файлами в backend
- Блок 7 (Streams) — критично для NestJS file upload, prisma streaming queries, обработки больших данных
- Блок 8 (EventEmitter) — фундамент NestJS Event Module, Socket.io, EventBus
- Блок 10 (архитектура) — первый шаг к слоистой архитектуре которую увидишь в NestJS
📚 Внешние ресурсы
- 📄 Полное задание
- 📦 Template репозиторий
- Node.js File System (
fs) — official docs - Node.js Path module — official docs
- Node.js Streams — official docs
- Understanding Streams in Node.js — NodeSource
- Node.js Events — official docs
- Understanding Node.js Event-Driven Architecture — freeCodeCamp
- Node.js Readline module — official docs
- Node.js Process — official docs
- Working with folders in Node.js — official docs
- CommonJS modules — Node.js docs
- The Node.js Event Loop, Timers, and
process.nextTick