Periksa YAML
Periksa YAML
Section titled “Periksa YAML”Pengganti check-config.mjs tanpa paket yaml.
Berkas: alat/hari-3/Praktik 11/tools/periksa-yaml.mjs
#!/usr/bin/env node// =====================================================================// Praktik 11 - periksa-yaml.mjs//// Pengganti scripts/check-config.mjs dari modul, yang TIDAK BISA JALAN// pada project apa adanya.//// Masalahnya: check-config.mjs memuat paket `yaml`// const YAML = createRequire(import.meta.url)('yaml');// tetapi `yaml` tidak ada di daftar dependencies package.json project.// Modul Praktik 11 meminta menjalankan// node scripts/check-config.mjs// dan mengharapkan keluaran tertentu, padahal perintah itu berhenti dengan// Error: Cannot find module 'yaml'// kecuali peserta lebih dahulu menjalankan `npm install yaml` — langkah// yang tidak pernah disebutkan di modul.//// Skrip ini memeriksa hal yang sama tanpa dependensi apa pun.//// Pakai:// node periksa-yaml.mjs docker-compose.yml cloudbuild.yaml// node periksa-yaml.mjs # memakai dua nama berkas bawaan// =====================================================================
import { readFileSync, existsSync } from 'node:fs';import { pathToFileURL } from 'node:url';
// ---------------------------------------------------------------------// Parser YAML minimal: cukup untuk struktur docker-compose.yml dan// cloudbuild.yaml. Menangani pemetaan bersarang lewat indentasi, daftar// "- item", dan blok skalar (| dan >). Tidak menangani anchor/alias,// tag, atau aliran JSON satu baris yang rumit — dan memang tidak perlu.// ---------------------------------------------------------------------export function parseYaml(teks) { const akar = {}; const tumpukan = [{ indentasi: -1, objek: akar, jenis: 'map' }]; const barisSemua = teks.replace(/\r\n?/g, '\n').split('\n'); const masalah = [];
for (let i = 0; i < barisSemua.length; i++) { const mentah = barisSemua[i]; if (!mentah.trim() || mentah.trim().startsWith('#')) continue; if (mentah.trim() === '---') continue;
const indentasi = mentah.length - mentah.trimStart().length; const isi = mentah.trim();
// Naik ke induk yang sesuai. while (tumpukan.length > 1 && indentasi <= tumpukan[tumpukan.length - 1].indentasi) { tumpukan.pop(); } const simpul = tumpukan[tumpukan.length - 1];
// Item daftar. if (isi.startsWith('- ') || isi === '-') { const nilai = isi.slice(1).trim();
// "- |" atau "- >": item daftar berupa blok skalar. Ini bentuk yang // dipakai cloudbuild.yaml untuk args, dan tanpa penanganan khusus // seluruh isi perintah SSH akan dianggap sebagai YAML yang rusak. if (/^[|>][-+]?$/.test(nilai)) { const lipat = nilai.startsWith('>'); const potongan = []; let j = i + 1; let indentasiBlok = -1; while (j < barisSemua.length) { const b = barisSemua[j]; if (!b.trim()) { if (indentasiBlok > -1) potongan.push(''); j++; continue; } const ind = b.length - b.trimStart().length; if (ind <= indentasi) break; if (indentasiBlok === -1) indentasiBlok = ind; potongan.push(b.slice(indentasiBlok)); j++; } i = j - 1; const teksBlok = lipat ? potongan.join(' ') : potongan.join('\n'); if (Array.isArray(simpul.objek)) simpul.objek.push(teksBlok); continue; }
if (!Array.isArray(simpul.objek)) { masalah.push(`baris ${i + 1}: item daftar di luar konteks daftar`); continue; } if (nilai === '') { const wadah = {}; simpul.objek.push(wadah); tumpukan.push({ indentasi, objek: wadah, jenis: 'map' }); } else if (nilai.includes(': ')) { // "- key: value" -> objek di dalam daftar const pisah = nilai.indexOf(':'); const wadah = { [nilai.slice(0, pisah).trim()]: nilai.slice(pisah + 1).trim().replace(/^["']|["']$/g, '') }; simpul.objek.push(wadah); tumpukan.push({ indentasi, objek: wadah, jenis: 'map' }); } else { simpul.objek.push(nilai.replace(/^["']|["']$/g, '')); } continue; }
// "key:" atau "key: value" const pisah = isi.indexOf(':'); if (pisah === -1) { masalah.push(`baris ${i + 1}: bukan pemetaan maupun item daftar -> ${isi.slice(0, 40)}`); continue; } if (Array.isArray(simpul.objek)) { masalah.push(`baris ${i + 1}: kunci di dalam daftar tanpa tanda "-"`); continue; }
const kunci = isi.slice(0, pisah).trim().replace(/^["']|["']$/g, ''); let nilai = isi.slice(pisah + 1).trim();
// Komentar di akhir baris (hanya bila tidak di dalam tanda kutip). if (!/^["']/.test(nilai)) { const tandaKomentar = nilai.indexOf(' #'); if (tandaKomentar > -1) nilai = nilai.slice(0, tandaKomentar).trim(); }
if (nilai === '|' || nilai === '>' || nilai === '|-' || nilai === '>-' || nilai === '|+') { const lipat = nilai.startsWith('>'); const potongan = []; let j = i + 1; let indentasiBlok = -1; while (j < barisSemua.length) { const b = barisSemua[j]; // Baris kosong selalu bagian dari blok. if (!b.trim()) { if (indentasiBlok > -1) potongan.push(''); j++; continue; } const ind = b.length - b.trimStart().length; if (ind <= indentasi) break; if (indentasiBlok === -1) indentasiBlok = ind; potongan.push(b.slice(indentasiBlok)); j++; } i = j - 1; simpul.objek[kunci] = lipat ? potongan.join(' ') : potongan.join('\n'); continue; }
if (nilai === '') { // Tentukan apakah anaknya daftar atau pemetaan. let j = i + 1; let jenisAnak = null; while (j < barisSemua.length) { const b = barisSemua[j]; if (!b.trim() || b.trim().startsWith('#')) { j++; continue; } const ind = b.length - b.trimStart().length; if (ind <= indentasi) break; jenisAnak = b.trim().startsWith('- ') || b.trim() === '-' ? 'list' : 'map'; break; } const wadah = jenisAnak === 'list' ? [] : {}; simpul.objek[kunci] = wadah; if (jenisAnak) tumpukan.push({ indentasi, objek: wadah, jenis: jenisAnak }); continue; }
// Buang tanda kutip pembungkus. if ((nilai.startsWith('"') && nilai.endsWith('"')) || (nilai.startsWith("'") && nilai.endsWith("'"))) { nilai = nilai.slice(1, -1); } simpul.objek[kunci] = nilai; }
return { data: akar, masalah };}
// ---------------------------------------------------------------------// Bagian CLI//// Berkas ini juga diimpor oleh periksa-deploy.mjs, jadi blok CLI hanya// boleh berjalan bila berkas ini dieksekusi langsung. Perbandingannya// memakai path yang sudah di-resolve, bukan nama berkas, supaya import// dari folder lain tidak salah dianggap sebagai eksekusi langsung.// ---------------------------------------------------------------------const dijalankanLangsung = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
if (dijalankanLangsung) { const berkas = process.argv.slice(2).filter((a) => !a.startsWith('--')); const daftar = berkas.length ? berkas : ['docker-compose.yml', 'cloudbuild.yaml'];
let gagal = false; for (const nama of daftar) { if (!existsSync(nama)) { console.error(`GAGAL ${nama}: berkas tidak ditemukan`); gagal = true; continue; } try { const { data, masalah } = parseYaml(readFileSync(nama, 'utf8')); console.log(`OK ${nama} -> ${Object.keys(data).join(', ')}`);
if (masalah.length) { for (const m of masalah) console.error(` ${m}`); gagal = true; }
if (nama === 'docker-compose.yml') { const layanan = Object.keys(data.services ?? {}); console.log(` service: ${layanan.join(', ')}`); if (layanan.length !== 3) { console.error(` HARUS 3 service, ditemukan ${layanan.length}`); gagal = true; } // Cek tambahan: variabel yang dipakai harus ada di .env.example. if (existsSync('.env.example')) { const envContoh = readFileSync('.env.example', 'utf8'); const dipakai = [...readFileSync(nama, 'utf8').matchAll(/\$\{([A-Z_][A-Z0-9_]*)/g)] .map((m) => m[1]); const hilang = [...new Set(dipakai)].filter((v) => !new RegExp(`^${v}=`, 'm').test(envContoh)); if (hilang.length) { console.error(` variabel tanpa nilai di .env.example: ${hilang.join(', ')}`); gagal = true; } } } } catch (e) { console.error(`GAGAL ${nama}: ${e.message}`); gagal = true; } } process.exit(gagal ? 1 : 0);}