Недели 9-10 · HTML Builder

🧭 ← NFC · Следующая → Async Race

🎯 Что строим

Смена среды: ты выходишь из браузера и заходишь в Node.js. Раньше код жил в DOM, теперь — в процессе ОС с доступом к файловой системе.

Шесть мини-скриптов нарастающей сложности на чистом Node.js (только встроенные модули). Финал — статический site-builder: HTML-шаблон с {{tags}} собирается из компонентов + CSS-бандл + копирование assets/.

📄 Полное задание на GitHub →

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 Рекурсивный обход: readdirstatcopyFile/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.

Self-check:

  1. Что такое V8 и libuv? Кто из них отвечает за event loop?
  2. Чем node script.js отличается от <script src="script.js"> в браузере?
  3. Где живёт global в Node, и почему window не существует?
  4. Почему задание требует именно LTS, а не latest?

2 · Модули — CommonJS (а не ESM) ⭐⭐⭐

Зачем: задание явно требует CommonJS. Если напишешь import fs from 'fs' — будет ошибка Cannot use import statement outside a module или потеря баллов за нарушение конвенции.

  • CommonJS vs ES Modules в Noderequire vs import, синхронность загрузки, module.exports vs export
  • Глобалы 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:

  1. Что вернёт require('./foo') если foo.js экспортирует через module.exports = { a: 1 }?
  2. Зачем существует __dirname? Что он содержит когда я запускаю node 01-read-file из корня репо?
  3. Почему __filename критичен для построения путей в этой задаче?

3 · path — собираем пути правильно ⭐⭐

Зачем: задание запускается с разных машин (cross-check). Хардкод './01-read-file/text.txt' или \\ (Windows-разделители) — сломается. Только path.join(__dirname, ...).

  • pathpath.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:

  1. Чем path.join отличается от строки __dirname + '/text.txt'?
  2. Что вернёт path.extname('Readme')? А path.extname('.gitignore')?
  3. Почему в задаче 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:

  1. В каком порядке выполнятся: setTimeout(fn, 0), Promise.resolve.then(fn), process.nextTick(fn), setImmediate(fn)?
  2. Почему setTimeout запрещён в этой задаче, а setImmediate — нет (но мы и его не используем)?
  3. Что такое microtask queue и почему promises его используют?

5 · process — твой интерфейс к ОС ⭐⭐

Зачем: в задаче 02 нужно слушать Ctrl+C (SIGINT) и красиво выходить. В задаче 03 stdin/stdout управляются через process. Все CLI-аргументы — в process.argv.

  • processprocess.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:

  1. В чём разница между __dirname и process.cwd? Что вернёт каждый при node 01-read-file из корня репо?
  2. Что произойдёт если в задаче 02 не повесить хендлер на SIGINT?
  3. Зачем нужен 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 руками: readdircopyFile/mkdir

withFileTypes: true — критичная опция для задачи 03 и 04: возвращает Dirent с методом dirent.isFile / isDirectory, что быстрее чем отдельный stat на каждый элемент.

Self-check:

  1. Чем fs.promises.readFile(p) отличается от fs.readFile(p, cb)?
  2. Что вернёт await fs.promises.readdir('./styles', { withFileTypes: true })? Чем это отличается от без опции?
  3. Как написать «удали папку рекурсивно и создай заново» одной операцией?
  4. Что произойдёт если вызвать 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:

  1. Зачем читать файл потоком если можно readFile одной командой? (подсказка: файл 10 ГБ)
  2. Что такое backpressure? Почему readable.pipe(writable) решает это автоматически?
  3. Чем отличается режим flowing от paused у Readable stream?
  4. Какие 3 типа stream-событий ты обязательно обрабатываешь?

8 · EventEmitter ⭐⭐

Зачем: streams, readline, process, fs.watch — всё это EventEmitter под капотом. Понимая базовый паттерн, ты понимаешь всю экосистему.

Self-check:

  1. Что произойдёт если повесить 11+ listeners на один эвент? (подсказка: warning)
  2. Чем .on отличается от .once?
  3. Почему 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:

  1. Зачем нужен и rl.on('line') и process.on('SIGINT') — что обрабатывает каждый?
  2. Что вернёт rl.close()?
  3. Чем rl.question(prompt, cb) отличается от rl.on('line', cb)?

10 · Архитектура для финального задания 06

Задача 06 — это композиция 04 + 05 + парсер шаблонов. Разложи на функции:

  1. buildHtml(templatePath, componentsDir, outPath) — читает template, находит все {{name}} через replace(/{{([^}]+)}}/g, ...) (с trim ключа из-за «двух тегов через пробелы»), подставляет содержимое components/<name>.html
  2. buildStyles(stylesDir, outFile) — переиспользуй логику из задачи 05
  3. copyAssets(srcDir, dstDir) — переиспользуй логику из задачи 04
  4. mainmkdir project-dist + параллельный Promise.all([buildHtml, buildStyles, copyAssets])

Ловушки задачи 06:

  • ⚠️ template.html не должен меняться (читай его в память, не пиши обратно в него)
  • ⚠️ Только .html файлы из components/ — остальные игнорить
  • ⚠️ Два тега подряд через пробел {{a}} {{b}} — оба должны заменяться (regex /g это покрывает)
  • ⚠️ При повторном запуске — project-dist/ обновляется (удаляй старое перед записью)
  • ⚠️ assets/ копируется рекурсивно, без fsPromises.cp

Self-check:

  1. Какой regex заменяет все {{component-name}} в шаблоне? Что если в имени пробел или кавычка?
  2. Что произойдёт если components/foo.html отсутствует, а в template есть {{foo}}?
  3. Как обновить project-dist/ при повторном запуске — rm -rf всё или диффать?

11 · Workflow

⚠️ После 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 перед коммитом

  1. Запускается ли каждый subtask из корня репо командой node <folder>? (cross-check будет запускать именно так)
  2. В коде нет ни одного *Sync метода fs? (Grep на Sync( по всему репо — пусто?)
  3. В коде нет setTimeout? (Grep на setTimeout — пусто?)
  4. В коде нет fsPromises.cp / fs.cp в задачах 04 и 06?
  5. В package.json нет ни одной runtime-зависимости кроме того что было в template?
  6. Все пути собраны через path.join(__dirname, ...), без хардкода слешей?
  7. Stream-ошибки обрабатываются (stream.on('error', ...)) везде где stream используется?
  8. Задача 02 корректно ловит и exit, и Ctrl+C?
  9. Задача 04 при повторном запуске удаляет файлы которых больше нет в источнике?
  10. Задача 06 не модифицирует template.html?
  11. PR создан из feature-ветки в main, не смержен?
  12. 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

📚 Внешние ресурсы