chiwaa 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/chiwaa.js +114 -6
  2. package/package.json +1 -1
package/bin/chiwaa.js CHANGED
@@ -236,11 +236,20 @@ function buildIfNeeded(dir, args) {
236
236
  const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
237
237
  const found = OUTPUTS.find(([n]) => deps[n]);
238
238
  let outName = args.dir || (found ? found[1] : "dist");
239
+ let restore = null;
240
+ if (found && found[0] === "next" && !args.dir) {
241
+ checkNext(dir);
242
+ if (!args["no-build"]) restore = wrapNextConfig(dir);
243
+ }
239
244
  if (args["no-build"]) {
240
245
  console.log(c.dim("빌드는 건너뜁니다(--no-build)."));
241
246
  } else {
242
- if (!fs.existsSync(path.join(dir, "node_modules"))) run("npm install", dir);
243
- run("npm run build", dir);
247
+ try {
248
+ if (!fs.existsSync(path.join(dir, "node_modules"))) run("npm install", dir);
249
+ run("npm run build", dir);
250
+ } finally {
251
+ if (restore) restore();
252
+ }
244
253
  }
245
254
  let out = path.join(dir, outName);
246
255
  if (!fs.existsSync(out)) {
@@ -249,10 +258,108 @@ function buildIfNeeded(dir, args) {
249
258
  out = path.join(dir, alt);
250
259
  }
251
260
  if (found && found[0] === "next" && !fs.existsSync(path.join(out, "index.html")))
252
- fail("Next.js 는 next.config 에 output: 'export' 를 넣어야 올릴 수 있습니다.");
261
+ fail(NEXT_EXPORT_MSG);
253
262
  return { out, built: true };
254
263
  }
255
264
 
265
+ // Next.js: 서버에서 돌아가는 기능(API 주소·middleware·서버 액션·서버 렌더링)은 chiwaa 에서 돌지 않는다 — 빌드 전에 알린다
266
+ // (빌드 서버 chiwaa-deployd 의 next_server_features 와 같은 기준)
267
+ const AI_GUIDE = "안내: https://chiwaa.kr/llms.txt";
268
+ const NEXT_EXPORT_MSG =
269
+ "Next.js 는 next.config 에 output: 'export' 를 넣어야 올릴 수 있습니다(예: const nextConfig = { output: \"export\", images: { unoptimized: true } }).\n" +
270
+ `AI 에게 「이 Next.js 앱을 chiwaa 에 올릴 수 있게 output: 'export' 정적 사이트로 바꿔 줘. ${AI_GUIDE}」라고 말해 보세요.`;
271
+ function nextServerFeatures(dir) {
272
+ const found = [];
273
+ const pat = /['"]use server['"]|getServerSideProps|from\s+['"]next\/headers['"]/;
274
+ (function walk(d, rel) {
275
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
276
+ if (e.isSymbolicLink() || e.name.startsWith(".")) continue;
277
+ const r = rel ? `${rel}/${e.name}` : e.name;
278
+ const full = path.join(d, e.name);
279
+ if (e.isDirectory()) {
280
+ if (!["node_modules", "out", "public"].includes(e.name)) walk(full, r);
281
+ continue;
282
+ }
283
+ const m = e.name.match(/^(.*)\.(js|jsx|ts|tsx|mjs)$/);
284
+ if (!m) continue;
285
+ if ((m[1] === "middleware" && ["", "src"].includes(rel)) ||
286
+ (m[1] === "route" && /(^|\/)app(\/|$)/.test(rel)) ||
287
+ /^(src\/)?pages\/api(\/|$)/.test(rel)) found.push(r);
288
+ else if (fs.statSync(full).size <= 300 * 1024 && pat.test(fs.readFileSync(full, "utf8"))) found.push(r);
289
+ }
290
+ })(dir, "");
291
+ return found;
292
+ }
293
+ function checkNext(dir) {
294
+ const server = nextServerFeatures(dir);
295
+ if (server.length) {
296
+ const shown = server.slice(0, 4).join(", ") + (server.length > 4 ? ` 등 ${server.length}곳` : "");
297
+ fail(`이 Next.js 앱은 서버에서 돌아가는 기능을 씁니다(${shown}). chiwaa 는 화면 파일(HTML·JS)을 올리는 곳이라 이대로는 열리지 않습니다.\n` +
298
+ `AI 에게 「이 Next.js 앱을 chiwaa 에 올릴 수 있게 output: 'export' 정적 사이트로 바꾸고, API·middleware 같은 서버 기능은 맨 위 api/ 폴더의 chiwaa 서버 함수로 옮겨 줘. ${AI_GUIDE}」라고 말해 보세요.`);
299
+ }
300
+ }
301
+
302
+ // 서버 기능이 없는 Next.js 는 next.config 를 그대로 두고 이번 빌드만 정적 내보내기로 덧씌운다(빌드 서버 prepare_next 와 같은 덧씌우기).
303
+ // 빌드가 끝나면(실패·Ctrl+C 포함) 원래 설정으로 되돌린다. 되돌리는 함수, 덧씌울 필요가 없으면 null.
304
+ function wrapNextConfig(dir) {
305
+ const conf = ["next.config.js", "next.config.mjs", "next.config.cjs", "next.config.ts"].find((n) => fs.existsSync(path.join(dir, n)));
306
+ if (conf && /output\s*:\s*['"]export['"]/.test(fs.readFileSync(path.join(dir, conf), "utf8"))) return null;
307
+ const ext = conf ? path.extname(conf) : ".mjs";
308
+ const target = path.join(dir, conf || "next.config.mjs");
309
+ const orig = `next.config.chiwaa-orig${ext}`;
310
+ const body = [
311
+ ' const base = (typeof orig === "function" ? await orig(phase, ctx) : await orig) || {};',
312
+ " // cacheComponents·partialPrefetching(PPR)은 정적 내보내기와 함께 쓸 수 없다(Next 16 기본 설정에 들어 있음)",
313
+ " const { cacheComponents, partialPrefetching, ...rest } = base;",
314
+ ' return { ...rest, output: "export", images: { ...(rest.images || {}), unoptimized: true } };',
315
+ "",
316
+ ].join("\n");
317
+ let text;
318
+ if (!conf) {
319
+ text = 'export default { output: "export", images: { unoptimized: true } };\n';
320
+ } else {
321
+ const pkg = readJSON(path.join(dir, "package.json")) || {};
322
+ const cjs = ext === ".cjs" || (ext === ".js" && pkg.type !== "module");
323
+ const from = ext === ".ts" ? "next.config.chiwaa-orig" : orig;
324
+ text = cjs
325
+ ? `// @ts-nocheck
326
+ const orig = require("./${from}");
327
+ module.exports = async (phase, ctx) => {
328
+ ${body}};
329
+ `
330
+ : `// @ts-nocheck
331
+ import orig from "./${from}";
332
+ export default async function chiwaa(phase, ctx) {
333
+ ${body}}
334
+ `;
335
+ fs.renameSync(target, path.join(dir, orig));
336
+ }
337
+ fs.writeFileSync(target, text);
338
+ let done = false;
339
+ const restore = () => {
340
+ if (done) return;
341
+ done = true;
342
+ fs.rmSync(target, { force: true });
343
+ if (conf) fs.renameSync(path.join(dir, orig), target);
344
+ process.removeListener("SIGINT", onInt);
345
+ };
346
+ const onInt = () => { restore(); process.exit(130); };
347
+ process.on("SIGINT", onInt);
348
+ console.log(c.dim("next.config 는 그대로 두고, 이번 빌드만 정적 사이트로 내보냅니다(output: 'export')."));
349
+ return restore;
350
+ }
351
+
352
+ // 첫 화면(index.html)이 없는 폴더를 올리려 하면, 앱이나 HTML 이 들어 있는 하위 폴더를 알려 주고 멈춘다
353
+ function noIndexMessage(dir) {
354
+ const subs = fs.readdirSync(dir, { withFileTypes: true })
355
+ .filter((e) => e.isDirectory() && !e.name.startsWith(".") && !SKIP.has(e.name) &&
356
+ ["package.json", "index.html"].some((f) => fs.existsSync(path.join(dir, e.name, f))))
357
+ .map((e) => e.name);
358
+ return "첫 화면 파일(index.html)이 없어 올리지 않았습니다(소스가 그대로 공개되지 않게).\n" +
359
+ (subs.length ? `앱이 하위 폴더에 있는 것 같습니다. 그 폴더에서 올려 보세요: ${subs.slice(0, 3).map((s) => `cd ${s} && npx chiwaa deploy`).join(" / ")}\n` : "") +
360
+ `AI 에게 「이 프로젝트를 chiwaa 에 올릴 수 있게 index.html 이 나오는 정적 사이트로 만들어 줘. ${AI_GUIDE}」라고 말해 보세요.`;
361
+ }
362
+
256
363
  // ---------- 사이트 정하기 ----------
257
364
  // 사용자가 직접 정한 이름은 바꾸지 않는다 — 규칙에 안 맞으면 거절한다(대문자만 소문자로)
258
365
  const NAME_RULE = "사이트 이름은 영문 소문자·숫자·하이픈(-)으로 2~30자, 첫 글자는 영문이나 숫자여야 합니다(하이픈 두 개 연속은 안 됨). 예: my-shop, cafe2026";
@@ -381,9 +488,8 @@ async function deploy(args) {
381
488
  files.push({ rel: "api", dir: true }, ...extra.files.map((f) => ({ ...f, rel: `api/${f.rel}` })));
382
489
  }
383
490
  const fnCount = files.filter((f) => !f.dir && /^api\/[^/_][^/]*\.(js|ts|mjs)$/.test(f.rel)).length;
384
- if (fnCount) console.log(c.dim(`서버 함수 ${fnCount}개(api/) 함께 올림 → https://<사이트>/api/<이름>`));
385
- if (!files.some((f) => !f.dir && /^index\.html?$/i.test(f.rel)))
386
- console.log(c.dim("참고: 맨 위에 index.html 이 없습니다. 주소로 열면 첫 화면이 안 보일 수 있습니다."));
491
+ if (fnCount) console.log(c.dim(`서버 함수 ${fnCount}개(api/) 함께 올림 → https://<사이트>/api/<이름> (서버 함수는 1분 안에 켜집니다 — 그 전에는 404 일 수 있음)`));
492
+ if (!files.some((f) => !f.dir && f.rel === "index.html")) fail(noIndexMessage(out));
387
493
  const fileCount = files.filter((f) => !f.dir).length;
388
494
  console.log(c.dim(`올리는 중: ${path.relative(process.cwd(), out) || "."} — 파일 ${fileCount}개, ${(total / 1048576).toFixed(1)}MB`));
389
495
  const gz = makeTarGz(files);
@@ -395,6 +501,8 @@ async function deploy(args) {
395
501
  });
396
502
  if (r.status !== 200 || !r.data.ok) fail(r.data.message || r.data.error || `배포하지 못했습니다 (${r.status}).`);
397
503
  console.log(c.green("✓"), `${preview ? "미리보기 " : ""}배포 완료 ${c.dim(`(${((Date.now() - t0) / 1000).toFixed(1)}초)`)}`);
504
+ const dropped = String(r.data.message || "").indexOf("뺀 파일:");
505
+ if (dropped >= 0) console.log(c.dim(r.data.message.slice(dropped).replace(/\)\s*$/, "")));
398
506
  console.log(`\n ${c.bold(c.cyan(r.data.url))}\n`);
399
507
  if (preview) console.log(c.dim("운영 주소는 그대로입니다. 괜찮으면 --preview 없이 다시 deploy 하세요. (미리보기는 검색엔진에 나오지 않음)\n"));
400
508
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chiwaa",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "chiwaa.kr 에 사이트를 터미널에서 바로 올리는 도구",
5
5
  "bin": {
6
6
  "chiwaa": "bin/chiwaa.js"