@c-time/frelio-cli 1.4.8 → 1.4.10

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 (41) hide show
  1. package/README.md +179 -3
  2. package/dist/commands/doctor.d.ts +11 -0
  3. package/dist/commands/doctor.js +71 -0
  4. package/dist/commands/fleet.d.ts +16 -0
  5. package/dist/commands/fleet.js +110 -0
  6. package/dist/commands/init.js +1 -0
  7. package/dist/commands/set-mode.d.ts +10 -0
  8. package/dist/commands/set-mode.js +41 -0
  9. package/dist/commands/update.d.ts +7 -2
  10. package/dist/commands/update.js +72 -19
  11. package/dist/core/config.js +1 -0
  12. package/dist/core/doctor.d.ts +36 -0
  13. package/dist/core/doctor.js +86 -0
  14. package/dist/core/file-generators.d.ts +3 -0
  15. package/dist/core/file-generators.js +13 -3
  16. package/dist/core/fleet.d.ts +62 -0
  17. package/dist/core/fleet.js +172 -0
  18. package/dist/core/migrations/index.d.ts +3 -0
  19. package/dist/core/migrations/index.js +2 -0
  20. package/dist/core/migrations/node20-to-node24.d.ts +15 -0
  21. package/dist/core/migrations/node20-to-node24.js +35 -0
  22. package/dist/core/migrations/registry.d.ts +11 -0
  23. package/dist/core/migrations/registry.js +18 -0
  24. package/dist/core/migrations/types.d.ts +55 -0
  25. package/dist/core/migrations/types.js +11 -0
  26. package/dist/core/site-mode.d.ts +23 -0
  27. package/dist/core/site-mode.js +37 -0
  28. package/dist/core/template-refresh.d.ts +41 -0
  29. package/dist/core/template-refresh.js +148 -0
  30. package/dist/core/template-scaffold.d.ts +11 -0
  31. package/dist/core/template-scaffold.js +12 -2
  32. package/dist/core/types.d.ts +2 -0
  33. package/dist/core/version-check.d.ts +31 -0
  34. package/dist/core/version-check.js +114 -0
  35. package/dist/index.js +41 -3
  36. package/dist/lib/npm-registry.d.ts +12 -0
  37. package/dist/lib/npm-registry.js +30 -0
  38. package/dist/lib/template-renderer.js +6 -1
  39. package/dist/lib/templates.d.ts +36 -0
  40. package/dist/lib/templates.js +171 -0
  41. package/package.json +1 -1
@@ -3,6 +3,26 @@
3
3
  */
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
+ /** 本番ゲートのサイト公開状態(Issue #93) */
7
+ export const SITE_MODES = ['live', 'prelaunch', 'maintenance', 'closed'];
8
+ /** config.siteMode を正規化する(未知値・未指定は 'live') */
9
+ export function resolveSiteMode(config) {
10
+ const m = (config.siteMode ?? 'live');
11
+ return SITE_MODES.includes(m) ? m : 'live';
12
+ }
13
+ /** productionUrl から正規化先ホスト(独自ドメイン)を導出する。無効/空なら空文字。 */
14
+ export function deriveCanonicalHost(config) {
15
+ try {
16
+ return new URL(config.productionUrl).hostname;
17
+ }
18
+ catch {
19
+ return '';
20
+ }
21
+ }
22
+ /** コンテンツ配信 Pages の本番 pages.dev ホストを導出する(例: mysite.pages.dev)。 */
23
+ export function deriveContentPagesDev(config) {
24
+ return config.pagesProjectName ? `${config.pagesProjectName}.pages.dev` : '';
25
+ }
6
26
  /**
7
27
  * ランダムな8文字のハッシュを生成(URL推測を困難にする)
8
28
  */
@@ -73,6 +93,10 @@ export function generateConfigJson(config) {
73
93
  allowedOrigins: buildAllowedOrigins(config),
74
94
  // デプロイ管理機能(Issue #27)の表示用。account_id は非シークレットのため config.json に保持する。
75
95
  cloudflareAccountId: config.cloudflareAccountId ?? '',
96
+ // 保存タイムゾーン(Issue #92)。日時の保存オフセット。
97
+ saveTimezone: config.saveTimezone || '+09:00',
98
+ // 本番ゲートのサイト公開状態(Issue #93)。エッジ実体は wrangler [vars] の SITE_MODE。
99
+ siteMode: resolveSiteMode(config),
76
100
  }, null, 2);
77
101
  }
78
102
  /**
@@ -123,6 +147,12 @@ CONTENT_REPO = "${config.contentRepo}"
123
147
  # CLOUDFLARE_API_TOKEN はシークレットのため wrangler pages secret で管理画面プロジェクトに設定する(ここには置かない)。
124
148
  CLOUDFLARE_ACCOUNT_ID = "${config.cloudflareAccountId ?? ''}"
125
149
  PAGES_PROJECT_NAME = "${config.pagesProjectName}"
150
+ # 本番ゲート(Issue #93): コンテンツ配信の functions/_middleware.ts がエッジで参照する。
151
+ # SITE_MODE: live | prelaunch | maintenance | closed。
152
+ # CANONICAL_HOST / CONTENT_PAGES_DEV は派生値(productionUrl ホスト / <project>.pages.dev)。
153
+ SITE_MODE = "${resolveSiteMode(config)}"
154
+ CANONICAL_HOST = "${deriveCanonicalHost(config)}"
155
+ CONTENT_PAGES_DEV = "${deriveContentPagesDev(config)}"
126
156
  `;
127
157
  }
128
158
  export function generateWorkerWranglerToml(config) {
@@ -156,6 +186,147 @@ export function generateRoutesJson() {
156
186
  exclude: [],
157
187
  }, null, 2);
158
188
  }
189
+ /**
190
+ * コンテンツ配信側(public/)の _routes.json を生成する。
191
+ *
192
+ * 本番ゲート(Issue #93)の functions/_middleware.ts に全リクエストを通すため
193
+ * include を `/*` にする。これにより静的ページもエッジ middleware を経由する
194
+ * (live モードでは即 next() で素通り)。
195
+ */
196
+ export function generatePublicRoutesJson() {
197
+ return JSON.stringify({
198
+ version: 1,
199
+ include: ['/*'],
200
+ exclude: [],
201
+ }, null, 2);
202
+ }
203
+ /**
204
+ * 本番ゲート middleware(Issue #93)を生成する。
205
+ *
206
+ * 注意: この functions/ は管理画面 Pages にも配られる共有資産のため、
207
+ * ホスト判定で「コンテンツのホスト」のみを対象にし、admin / staging /
208
+ * localhost / 不明ホストは素通り(next)して CMS を保護する。
209
+ *
210
+ * テンプレート実ファイル
211
+ * (templates/content-repo/functions/_middleware.ts)と同一文字列を返す。
212
+ * 乖離は sync テストで検出する。
213
+ */
214
+ export function generateGateMiddleware() {
215
+ return GATE_MIDDLEWARE_SOURCE;
216
+ }
217
+ const GATE_MIDDLEWARE_SOURCE = `/**
218
+ * 本番ゲート middleware(Issue #93)
219
+ *
220
+ * コンテンツ配信 Pages のエッジで動作し、サイトの公開状態(siteMode)と
221
+ * URL 正規化(pages.dev → 独自ドメイン 301)を制御する。
222
+ *
223
+ * 重要: この functions/ は管理画面 Pages にも配られる共有資産のため、
224
+ * ホスト判定で「コンテンツのホスト」のみを対象にし、admin / staging /
225
+ * localhost / 不明ホストは素通り(next)して CMS(/api/*)を保護する。
226
+ *
227
+ * 設定は wrangler.toml [vars](context.env)から読む:
228
+ * SITE_MODE live | prelaunch | maintenance | closed(既定 live)
229
+ * CANONICAL_HOST 正規化先ホスト(独自ドメイン。例 example.com)
230
+ * CONTENT_PAGES_DEV コンテンツ Pages の本番ホスト(例 mysite.pages.dev)
231
+ */
232
+
233
+ interface GateEnv {
234
+ SITE_MODE?: string
235
+ CANONICAL_HOST?: string
236
+ CONTENT_PAGES_DEV?: string
237
+ }
238
+
239
+ type GateDecision =
240
+ | { action: 'next' }
241
+ | { action: 'redirect'; location: string }
242
+ | { action: 'block'; status: number }
243
+
244
+ export function decideGate(
245
+ host: string,
246
+ pathname: string,
247
+ search: string,
248
+ env: GateEnv,
249
+ ): GateDecision {
250
+ const canonical = (env.CANONICAL_HOST || '').trim()
251
+ const pagesDev = (env.CONTENT_PAGES_DEV || '').trim()
252
+ const mode = (env.SITE_MODE || 'live').trim()
253
+
254
+ const isCanonical = canonical !== '' && host === canonical
255
+ const isPagesDev = pagesDev !== '' && host === pagesDev
256
+
257
+ // コンテンツのホスト以外(admin / staging / localhost / 不明)は素通り = CMS 保護
258
+ if (!isCanonical && !isPagesDev) return { action: 'next' }
259
+
260
+ // pages.dev → 独自ドメインへ 301(canonical 設定時のみ)
261
+ if (isPagesDev && !isCanonical && canonical !== '') {
262
+ return { action: 'redirect', location: \`https://\${canonical}\${pathname}\${search}\` }
263
+ }
264
+
265
+ if (mode === 'live') return { action: 'next' }
266
+
267
+ const isStorage = pathname === '/storage' || pathname.startsWith('/storage/')
268
+
269
+ // 閉鎖: storage 含め全遮断(キルスイッチ)
270
+ if (mode === 'closed') return { action: 'block', status: 503 }
271
+ // プレ公開: storage 以外を 403(storage は CMS のサムネ/プレビュー用に配信継続)
272
+ if (mode === 'prelaunch') {
273
+ return isStorage ? { action: 'next' } : { action: 'block', status: 403 }
274
+ }
275
+ // メンテナンス: storage 以外を 503
276
+ if (mode === 'maintenance') {
277
+ return isStorage ? { action: 'next' } : { action: 'block', status: 503 }
278
+ }
279
+
280
+ // 未知の値は安全側(素通り)
281
+ return { action: 'next' }
282
+ }
283
+
284
+ function holdingPage(status: number): string {
285
+ const title = status === 403 ? '公開準備中' : 'メンテナンス中'
286
+ const message =
287
+ status === 403
288
+ ? 'このサイトは現在準備中です。公開までしばらくお待ちください。'
289
+ : 'ただいまメンテナンス中です。しばらく経ってから再度アクセスしてください。'
290
+ return \`<!doctype html>
291
+ <html lang="ja">
292
+ <head>
293
+ <meta charset="utf-8">
294
+ <meta name="viewport" content="width=device-width, initial-scale=1">
295
+ <meta name="robots" content="noindex">
296
+ <title>\${title}</title>
297
+ <style>
298
+ body { font-family: system-ui, sans-serif; display: flex; min-height: 100vh; margin: 0; align-items: center; justify-content: center; background: #f5f5f5; color: #333; }
299
+ .box { text-align: center; padding: 2rem; }
300
+ h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
301
+ p { color: #666; }
302
+ </style>
303
+ </head>
304
+ <body>
305
+ <div class="box"><h1>\${title}</h1><p>\${message}</p></div>
306
+ </body>
307
+ </html>
308
+ \`
309
+ }
310
+
311
+ export const onRequest: PagesFunction<GateEnv> = async (context) => {
312
+ const url = new URL(context.request.url)
313
+ const decision = decideGate(url.hostname, url.pathname, url.search, context.env)
314
+
315
+ if (decision.action === 'redirect') {
316
+ return Response.redirect(decision.location, 301)
317
+ }
318
+ if (decision.action === 'block') {
319
+ return new Response(holdingPage(decision.status), {
320
+ status: decision.status,
321
+ headers: {
322
+ 'content-type': 'text/html; charset=utf-8',
323
+ 'cache-control': 'no-store',
324
+ },
325
+ })
326
+ }
327
+ return context.next()
328
+ }
329
+ `;
159
330
  export function generateStorageFunction() {
160
331
  return `interface Env {
161
332
  R2: R2Bucket
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-time/frelio-cli",
3
- "version": "1.4.8",
3
+ "version": "1.4.10",
4
4
  "description": "Frelio CMS setup CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",