@c-time/frelio-cli 0.1.0 → 1.2.1

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.
@@ -8,7 +8,7 @@ import os from 'node:os';
8
8
  import { exec, commandExists, log, logStep, logSuccess, logError } from '../lib/shell.js';
9
9
  import { getLatestRelease, downloadTarball } from '../lib/github-release.js';
10
10
  import { generateInitialContent } from '../lib/initial-content.js';
11
- import { generateConfigJson, generateWranglerToml, generateUsersIndex, generateVersionJson, generateRedirects, generateRoutesJson, generateStorageFunction, generateViteConfig, generatePackageJson, generateTsConfig, generateStagingDomain, writeFile, ensureDir, } from '../lib/templates.js';
11
+ import { generateConfigJson, generateWranglerToml, generateUsersIndex, generateVersionJson, generateRedirects, generateRoutesJson, generateStorageFunction, generateViteConfig, generatePackageJson, generateTsConfig, generateTsConfigNode, generateStagingDomain, writeFile, ensureDir, } from '../lib/templates.js';
12
12
  export async function initCommand(options) {
13
13
  log('');
14
14
  log('🚀 Frelio CMS プロジェクトセットアップ');
@@ -376,10 +376,12 @@ function createContentStructure(projectDir, config) {
376
376
  'frelio-data/site/contents/private',
377
377
  'frelio-data/site/templates/assets/scss',
378
378
  'frelio-data/site/templates/assets/ts',
379
+ 'frelio-data/site/templates/assets/entries',
379
380
  'frelio-data/site/data/data-json',
380
381
  'frelio-data/admin/metadata',
381
382
  'frelio-data/admin/users',
382
383
  'frelio-data/admin/recipes',
384
+ 'scripts',
383
385
  'public',
384
386
  ];
385
387
  for (const dir of dirs) {
@@ -389,9 +391,10 @@ function createContentStructure(projectDir, config) {
389
391
  writeFile(path.join(projectDir, 'frelio-data/admin/users/_index.json'), generateUsersIndex(config));
390
392
  // version.json
391
393
  writeFile(path.join(projectDir, 'version.json'), generateVersionJson());
392
- // vite.config.ts + tsconfig.json + package.json
394
+ // vite.config.ts + tsconfig.json + tsconfig.node.json + package.json
393
395
  writeFile(path.join(projectDir, 'vite.config.ts'), generateViteConfig());
394
396
  writeFile(path.join(projectDir, 'tsconfig.json'), generateTsConfig());
397
+ writeFile(path.join(projectDir, 'tsconfig.node.json'), generateTsConfigNode());
395
398
  writeFile(path.join(projectDir, 'package.json'), generatePackageJson(config));
396
399
  // R2 ファイル配信用 Pages Function (/storage/*)
397
400
  writeFile(path.join(projectDir, 'functions', 'storage', '[[path]].ts'), generateStorageFunction());
@@ -154,6 +154,10 @@ export function generateInitialContent(projectDir) {
154
154
  generateScss(projectDir);
155
155
  // --- TypeScript ---
156
156
  generateTypeScript(projectDir);
157
+ // --- Entry Points ---
158
+ generateEntries(projectDir);
159
+ // --- Build Scripts ---
160
+ generateBuildScripts(projectDir);
157
161
  // --- CLAUDE.md ---
158
162
  generateClaudeMd(projectDir);
159
163
  }
@@ -164,8 +168,8 @@ function generateTemplates(projectDir) {
164
168
  writeFile(t('_parts/head.htm'), `<meta charset="UTF-8">
165
169
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
166
170
  <title data-gen-text="meta.title">Frelio Demo</title>
167
- <link rel="stylesheet" href="/assets/style.css">
168
- <script type="module" src="/assets/index.js"></script>
171
+ <link rel="stylesheet" href="/common/styles/index.css">
172
+ <script type="module" src="/common/scripts/index.js"></script>
169
173
  `);
170
174
  // _parts/header.htm
171
175
  writeFile(t('_parts/header.htm'), `<header class="l-header">
@@ -202,8 +206,8 @@ function generateTemplates(projectDir) {
202
206
  <meta charset="UTF-8">
203
207
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
204
208
  <title>Frelio Demo</title>
205
- <link rel="stylesheet" href="/assets/style.css">
206
- <script type="module" src="/assets/index.js"></script>
209
+ <link rel="stylesheet" href="/home/styles/index.css">
210
+ <script type="module" src="/home/scripts/index.js"></script>
207
211
  </head>
208
212
  <body>
209
213
  <template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
@@ -278,8 +282,8 @@ function generateTemplates(projectDir) {
278
282
  <meta charset="UTF-8">
279
283
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
280
284
  <title>会社概要 | Frelio Demo</title>
281
- <link rel="stylesheet" href="/assets/style.css">
282
- <script type="module" src="/assets/index.js"></script>
285
+ <link rel="stylesheet" href="/about/styles/index.css">
286
+ <script type="module" src="/about/scripts/index.js"></script>
283
287
  </head>
284
288
  <body>
285
289
  <template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
@@ -348,8 +352,8 @@ function generateTemplates(projectDir) {
348
352
  <meta charset="UTF-8">
349
353
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
350
354
  <title>お問い合わせ | Frelio Demo</title>
351
- <link rel="stylesheet" href="/assets/style.css">
352
- <script type="module" src="/assets/index.js"></script>
355
+ <link rel="stylesheet" href="/contact/styles/index.css">
356
+ <script type="module" src="/contact/scripts/index.js"></script>
353
357
  </head>
354
358
  <body>
355
359
  <template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
@@ -410,8 +414,8 @@ function generateTemplates(projectDir) {
410
414
  <meta charset="UTF-8">
411
415
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
412
416
  <title>お知らせ | Frelio Demo</title>
413
- <link rel="stylesheet" href="/assets/style.css">
414
- <script type="module" src="/assets/index.js"></script>
417
+ <link rel="stylesheet" href="/news/styles/index.css">
418
+ <script type="module" src="/news/scripts/index.js"></script>
415
419
  </head>
416
420
  <body>
417
421
  <template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
@@ -476,8 +480,8 @@ function generateTemplates(projectDir) {
476
480
  <meta charset="UTF-8">
477
481
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
478
482
  <title>記事タイトル | Frelio Demo</title>
479
- <link rel="stylesheet" href="/assets/style.css">
480
- <script type="module" src="/assets/index.js"></script>
483
+ <link rel="stylesheet" href="/news/detail/styles/index.css">
484
+ <script type="module" src="/news/detail/scripts/index.js"></script>
481
485
  </head>
482
486
  <body>
483
487
  <template data-gen-scope="" data-gen-include="_parts/header.htm"></template>
@@ -535,34 +539,6 @@ function generateTemplates(projectDir) {
535
539
  // ========== SCSS ==========
536
540
  function generateScss(projectDir) {
537
541
  const s = (...p) => path.join(projectDir, 'frelio-data/site/templates/assets/scss', ...p);
538
- // style.scss (entry point)
539
- writeFile(s('style.scss'), `// Foundation
540
- @use 'foundation/variables' as *;
541
- @use 'foundation/mixins' as *;
542
- @use 'foundation/reset';
543
-
544
- // Layout
545
- @use 'layout/l-header';
546
- @use 'layout/l-footer';
547
- @use 'layout/l-main';
548
-
549
- // Component
550
- @use 'component/c-logo';
551
- @use 'component/c-nav';
552
- @use 'component/c-btn';
553
- @use 'component/c-card';
554
- @use 'component/c-table';
555
-
556
- // Project
557
- @use 'project/p-hero';
558
- @use 'project/p-news-list';
559
- @use 'project/p-article';
560
- @use 'project/p-about';
561
- @use 'project/p-contact';
562
-
563
- // Element
564
- @use 'element/e-heading';
565
- `);
566
542
  // Foundation
567
543
  writeFile(s('foundation/_variables.scss'), `// Colors
568
544
  $color-primary: #0070f3;
@@ -1220,14 +1196,6 @@ button {
1220
1196
  // ========== TypeScript ==========
1221
1197
  function generateTypeScript(projectDir) {
1222
1198
  const ts = (...p) => path.join(projectDir, 'frelio-data/site/templates/assets/ts', ...p);
1223
- writeFile(ts('index.ts'), `import { initMobileNav } from './features/mobile-nav'
1224
- import { initSmoothScroll } from './features/smooth-scroll'
1225
-
1226
- document.addEventListener('DOMContentLoaded', () => {
1227
- initMobileNav()
1228
- initSmoothScroll()
1229
- })
1230
- `);
1231
1199
  writeFile(ts('features/mobile-nav.ts'), `export function initMobileNav(): void {
1232
1200
  const toggle = document.querySelector<HTMLButtonElement>('.c-nav__toggle')
1233
1201
  const list = document.querySelector<HTMLUListElement>('.c-nav__list')
@@ -1268,6 +1236,831 @@ document.addEventListener('DOMContentLoaded', () => {
1268
1236
  }
1269
1237
  `);
1270
1238
  }
1239
+ // ========== Entry Points ==========
1240
+ function generateEntries(projectDir) {
1241
+ const e = (...p) => path.join(projectDir, 'frelio-data/site/templates/assets/entries', ...p);
1242
+ // common
1243
+ writeFile(e('common/styles/index.scss'), `// Foundation
1244
+ @use 'foundation/variables' as *;
1245
+ @use 'foundation/mixins' as *;
1246
+ @use 'foundation/reset';
1247
+
1248
+ // Layout
1249
+ @use 'layout/l-header';
1250
+ @use 'layout/l-footer';
1251
+ @use 'layout/l-main';
1252
+
1253
+ // Component
1254
+ @use 'component/c-logo';
1255
+ @use 'component/c-nav';
1256
+ @use 'component/c-btn';
1257
+ @use 'component/c-card';
1258
+ @use 'component/c-table';
1259
+
1260
+ // Element
1261
+ @use 'element/e-heading';
1262
+ `);
1263
+ writeFile(e('common/scripts/index.ts'), `import '../styles/index.scss'
1264
+ import { initMobileNav } from '@features/mobile-nav'
1265
+ import { initSmoothScroll } from '@features/smooth-scroll'
1266
+
1267
+ document.addEventListener('DOMContentLoaded', () => {
1268
+ initMobileNav()
1269
+ initSmoothScroll()
1270
+ })
1271
+ `);
1272
+ // home
1273
+ writeFile(e('home/styles/index.scss'), `@use 'foundation/variables' as *;
1274
+ @use 'foundation/mixins' as *;
1275
+
1276
+ @use 'project/p-hero';
1277
+ @use 'project/p-news-list';
1278
+ `);
1279
+ writeFile(e('home/scripts/index.ts'), `import '../styles/index.scss'
1280
+ `);
1281
+ // about
1282
+ writeFile(e('about/styles/index.scss'), `@use 'foundation/variables' as *;
1283
+ @use 'foundation/mixins' as *;
1284
+
1285
+ @use 'project/p-about';
1286
+ `);
1287
+ writeFile(e('about/scripts/index.ts'), `import '../styles/index.scss'
1288
+ `);
1289
+ // contact
1290
+ writeFile(e('contact/styles/index.scss'), `@use 'foundation/variables' as *;
1291
+ @use 'foundation/mixins' as *;
1292
+
1293
+ @use 'project/p-contact';
1294
+ `);
1295
+ writeFile(e('contact/scripts/index.ts'), `import '../styles/index.scss'
1296
+ `);
1297
+ // news
1298
+ writeFile(e('news/styles/index.scss'), `@use 'foundation/variables' as *;
1299
+ @use 'foundation/mixins' as *;
1300
+
1301
+ @use 'project/p-news-list';
1302
+ `);
1303
+ writeFile(e('news/scripts/index.ts'), `import '../styles/index.scss'
1304
+ `);
1305
+ // news/detail
1306
+ writeFile(e('news/detail/styles/index.scss'), `@use 'foundation/variables' as *;
1307
+ @use 'foundation/mixins' as *;
1308
+
1309
+ @use 'project/p-article';
1310
+ `);
1311
+ writeFile(e('news/detail/scripts/index.ts'), `import '../styles/index.scss'
1312
+ `);
1313
+ }
1314
+ // ========== Build Scripts ==========
1315
+ function generateBuildScripts(projectDir) {
1316
+ const s = (...p) => path.join(projectDir, 'scripts', ...p);
1317
+ writeFile(s('generate-data-json.ts'), `/**
1318
+ * FrelioDataJson 生成スクリプト
1319
+ *
1320
+ * FrelioBuildDataRecipe に従って FrelioDataJson を生成し、
1321
+ * frelio-data/site/data/data-json/ に出力する。
1322
+ *
1323
+ * @example
1324
+ * # 差分ビルド(デフォルト)
1325
+ * npx tsx scripts/generate-data-json.ts
1326
+ *
1327
+ * # フルリビルド
1328
+ * npx tsx scripts/generate-data-json.ts --full-rebuild
1329
+ *
1330
+ * # ドライラン
1331
+ * npx tsx scripts/generate-data-json.ts --dry-run
1332
+ */
1333
+
1334
+ import {
1335
+ generateDataJson,
1336
+ NodeFileSystem,
1337
+ getGitDiff,
1338
+ type GenerateDataJsonOptions,
1339
+ } from '@c-time/frelio-data-json-generator'
1340
+ import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from 'fs'
1341
+ import { dirname, join } from 'path'
1342
+ import { parseArgs } from 'util'
1343
+
1344
+ const { values } = parseArgs({
1345
+ options: {
1346
+ 'diff-range': { type: 'string', default: 'origin/main...HEAD' },
1347
+ 'full-rebuild': { type: 'boolean', default: false },
1348
+ 'dry-run': { type: 'boolean', default: false },
1349
+ 'log-level': { type: 'string', default: 'info' },
1350
+ },
1351
+ })
1352
+
1353
+ const options: GenerateDataJsonOptions = {
1354
+ fullRebuild: values['full-rebuild'],
1355
+ dryRun: values['dry-run'],
1356
+ logLevel: values['log-level'] as 'debug' | 'info' | 'quiet',
1357
+ }
1358
+
1359
+ const CONTENT_ROOT = 'frelio-data/site'
1360
+ const OUTPUT_ROOT = 'frelio-data/site/data/data-json'
1361
+ const REPORT_PATH = 'frelio-data/site/data/report.json'
1362
+ const RECIPE_PATH = 'frelio-data/admin/recipes/build-data-recipe.json'
1363
+ const DEPENDENCY_MAP_PATH = 'frelio-data/site/data/_dependency-map.json'
1364
+
1365
+ async function main(): Promise<void> {
1366
+ if (!existsSync(RECIPE_PATH)) {
1367
+ console.error(\`Recipe not found: \${RECIPE_PATH}\`)
1368
+ process.exit(1)
1369
+ }
1370
+ const recipe = JSON.parse(readFileSync(RECIPE_PATH, 'utf-8'))
1371
+
1372
+ if (!existsSync(DEPENDENCY_MAP_PATH)) {
1373
+ console.error(\`Dependency map not found: \${DEPENDENCY_MAP_PATH}\`)
1374
+ process.exit(1)
1375
+ }
1376
+ const dependencyMap = JSON.parse(readFileSync(DEPENDENCY_MAP_PATH, 'utf-8'))
1377
+
1378
+ const gitDiff = options.fullRebuild
1379
+ ? { added: [], modified: [], deleted: [] }
1380
+ : await getGitDiff(values['diff-range']!)
1381
+
1382
+ if (options.logLevel !== 'quiet') {
1383
+ console.log(\`Mode: \${options.fullRebuild ? 'full-rebuild' : 'incremental'}\`)
1384
+ console.log(\`Dry run: \${options.dryRun}\`)
1385
+ }
1386
+
1387
+ const result = await generateDataJson({
1388
+ recipe,
1389
+ dependencyMap,
1390
+ gitDiff,
1391
+ fileSystem: new NodeFileSystem(),
1392
+ contentRootPath: CONTENT_ROOT,
1393
+ outputRootPath: OUTPUT_ROOT,
1394
+ options,
1395
+ })
1396
+
1397
+ if (!options.dryRun) {
1398
+ mkdirSync(OUTPUT_ROOT, { recursive: true })
1399
+ for (const output of result.outputs) {
1400
+ if (output.content.type === 'delete') {
1401
+ const fullPath = join(OUTPUT_ROOT, output.path)
1402
+ if (existsSync(fullPath)) unlinkSync(fullPath)
1403
+ }
1404
+ }
1405
+ for (const output of result.outputs) {
1406
+ if (output.content.type !== 'delete') {
1407
+ const fullPath = join(OUTPUT_ROOT, output.path)
1408
+ mkdirSync(dirname(fullPath), { recursive: true })
1409
+ writeFileSync(fullPath, JSON.stringify(output.content, null, 2))
1410
+ }
1411
+ }
1412
+ }
1413
+
1414
+ mkdirSync(dirname(REPORT_PATH), { recursive: true })
1415
+ writeFileSync(REPORT_PATH, JSON.stringify(result.report, null, 2))
1416
+
1417
+ const { stats } = result.report
1418
+ console.log(\`\\n=== Summary ===\`)
1419
+ console.log(\`Updated: \${stats.updated}\`)
1420
+ console.log(\`Deleted: \${stats.deleted}\`)
1421
+ console.log(\`Skipped: \${stats.skipped}\`)
1422
+ console.log(\`Errors: \${stats.errors}\`)
1423
+ }
1424
+
1425
+ main().catch((error) => {
1426
+ console.error('Fatal error:', error)
1427
+ process.exit(1)
1428
+ })
1429
+ `);
1430
+ writeFile(s('generate-html.ts'), `/**
1431
+ * HTML 生成スクリプト
1432
+ *
1433
+ * FrelioDataJson → HTML の変換(ビルドパイプライン Phase 2)
1434
+ *
1435
+ * @example
1436
+ * npx tsx scripts/generate-html.ts
1437
+ * npx tsx scripts/generate-html.ts --dry-run
1438
+ */
1439
+
1440
+ import {
1441
+ generateHtml,
1442
+ NodeFileSystem,
1443
+ type GenerateHtmlOptions,
1444
+ } from '@c-time/frelio-gentl'
1445
+ import type { FrelioDataJson } from '@c-time/frelio-data-json'
1446
+ import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync, readdirSync } from 'fs'
1447
+ import { dirname, join, extname } from 'path'
1448
+ import { parseArgs } from 'util'
1449
+
1450
+ const { values } = parseArgs({
1451
+ options: {
1452
+ 'dry-run': { type: 'boolean', default: false },
1453
+ 'log-level': { type: 'string', default: 'info' },
1454
+ },
1455
+ })
1456
+
1457
+ const options: GenerateHtmlOptions = {
1458
+ logLevel: values['log-level'] as 'debug' | 'info' | 'quiet',
1459
+ }
1460
+
1461
+ const DATA_JSON_ROOT = 'frelio-data/site/data/data-json'
1462
+ const TEMPLATE_ROOT = 'frelio-data/site/templates'
1463
+ const OUTPUT_ROOT = 'public'
1464
+ const REPORT_PATH = 'frelio-data/site/data/html-report.json'
1465
+
1466
+ function collectJsonFiles(dir: string): string[] {
1467
+ if (!existsSync(dir)) return []
1468
+ const files: string[] = []
1469
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1470
+ const fullPath = join(dir, entry.name)
1471
+ if (entry.isDirectory()) {
1472
+ files.push(...collectJsonFiles(fullPath))
1473
+ } else if (extname(entry.name) === '.json') {
1474
+ files.push(fullPath)
1475
+ }
1476
+ }
1477
+ return files
1478
+ }
1479
+
1480
+ async function main(): Promise<void> {
1481
+ if (!existsSync(DATA_JSON_ROOT)) {
1482
+ console.error(\`Data JSON directory not found: \${DATA_JSON_ROOT}\`)
1483
+ process.exit(1)
1484
+ }
1485
+ if (!existsSync(TEMPLATE_ROOT)) {
1486
+ console.error(\`Template directory not found: \${TEMPLATE_ROOT}\`)
1487
+ process.exit(1)
1488
+ }
1489
+
1490
+ const jsonFiles = collectJsonFiles(DATA_JSON_ROOT)
1491
+ if (jsonFiles.length === 0) {
1492
+ console.log('No data JSON files found. Nothing to generate.')
1493
+ return
1494
+ }
1495
+
1496
+ const dataJsons: FrelioDataJson[] = jsonFiles.map(filePath => {
1497
+ return JSON.parse(readFileSync(filePath, 'utf-8'))
1498
+ })
1499
+
1500
+ console.log(\`Found \${dataJsons.length} data JSON files\`)
1501
+
1502
+ const result = await generateHtml({
1503
+ dataJsons,
1504
+ templateRootPath: TEMPLATE_ROOT,
1505
+ fileSystem: new NodeFileSystem(),
1506
+ options,
1507
+ })
1508
+
1509
+ if (!values['dry-run']) {
1510
+ mkdirSync(OUTPUT_ROOT, { recursive: true })
1511
+ for (const output of result.outputs) {
1512
+ if (output.status === 'deleted') {
1513
+ const fullPath = join(OUTPUT_ROOT, output.outputPath)
1514
+ if (existsSync(fullPath)) unlinkSync(fullPath)
1515
+ }
1516
+ }
1517
+ for (const output of result.outputs) {
1518
+ if (output.status === 'created') {
1519
+ const fullPath = join(OUTPUT_ROOT, output.outputPath)
1520
+ mkdirSync(dirname(fullPath), { recursive: true })
1521
+ writeFileSync(fullPath, output.html)
1522
+ }
1523
+ }
1524
+ }
1525
+
1526
+ mkdirSync(dirname(REPORT_PATH), { recursive: true })
1527
+ writeFileSync(REPORT_PATH, JSON.stringify(result.report, null, 2))
1528
+
1529
+ const { stats } = result.report
1530
+ console.log(\`\\n=== Summary ===\`)
1531
+ console.log(\`Created: \${stats.created}\`)
1532
+ console.log(\`Deleted: \${stats.deleted}\`)
1533
+ console.log(\`Errors: \${stats.errors}\`)
1534
+ }
1535
+
1536
+ main().catch((error) => {
1537
+ console.error('Fatal error:', error)
1538
+ process.exit(1)
1539
+ })
1540
+ `);
1541
+ writeFile(s('generate-sitemap.ts'), `/**
1542
+ * sitemap.xml 生成スクリプト
1543
+ *
1544
+ * public/ 配下の HTML ファイルを走査して sitemap.xml を生成する。
1545
+ *
1546
+ * @example
1547
+ * npx tsx scripts/generate-sitemap.ts --base-url https://example.com
1548
+ * npx tsx scripts/generate-sitemap.ts --full-rebuild --base-url https://example.com
1549
+ */
1550
+
1551
+ import { execSync } from 'child_process'
1552
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs'
1553
+ import { dirname, join } from 'path'
1554
+ import { parseArgs } from 'util'
1555
+
1556
+ const { values } = parseArgs({
1557
+ options: {
1558
+ 'base-url': { type: 'string' },
1559
+ 'diff-range': { type: 'string', default: 'origin/main...HEAD' },
1560
+ 'full-rebuild': { type: 'boolean', default: false },
1561
+ 'dry-run': { type: 'boolean', default: false },
1562
+ 'log-level': { type: 'string', default: 'info' },
1563
+ },
1564
+ })
1565
+
1566
+ const baseUrl = (values['base-url'] || process.env.SITE_BASE_URL || '').replace(/\\/$/, '')
1567
+ if (!baseUrl) {
1568
+ console.error('Error: --base-url or SITE_BASE_URL is required.')
1569
+ process.exit(1)
1570
+ }
1571
+
1572
+ const logLevel = values['log-level'] as 'debug' | 'info' | 'quiet'
1573
+
1574
+ const HTML_ROOT = 'public'
1575
+ const OUTPUT_PATH = 'public/sitemap.xml'
1576
+
1577
+ interface UrlEntry {
1578
+ loc: string
1579
+ lastmod: string | null
1580
+ }
1581
+
1582
+ function htmlPathToUrlPath(htmlPath: string): string {
1583
+ const relative = htmlPath
1584
+ .replace(/\\\\\\\\/g, '/')
1585
+ .replace(/^public\\//, '')
1586
+ .replace(/\\/index\\.html$/, '/')
1587
+ return relative === 'index.html' ? '/' : '/' + relative
1588
+ }
1589
+
1590
+ function hasNoindex(htmlPath: string): boolean {
1591
+ const html = readFileSync(htmlPath, 'utf-8')
1592
+ return /<meta\\s[^>]*name\\s*=\\s*["']robots["'][^>]*content\\s*=\\s*["'][^"']*noindex[^"']*["'][^>]*>/i.test(html)
1593
+ || /<meta\\s[^>]*content\\s*=\\s*["'][^"']*noindex[^"']*["'][^>]*name\\s*=\\s*["']robots["'][^>]*>/i.test(html)
1594
+ }
1595
+
1596
+ function getLastmod(filePath: string): string | null {
1597
+ try {
1598
+ const result = execSync(\`git log -1 --format=%aI -- "\${filePath}"\`, {
1599
+ encoding: 'utf-8',
1600
+ stdio: ['pipe', 'pipe', 'pipe'],
1601
+ }).trim()
1602
+ return result || null
1603
+ } catch {
1604
+ return null
1605
+ }
1606
+ }
1607
+
1608
+ function collectHtmlFiles(dir: string): string[] {
1609
+ if (!existsSync(dir)) return []
1610
+ const files: string[] = []
1611
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1612
+ const fullPath = join(dir, entry.name)
1613
+ if (entry.isDirectory()) {
1614
+ files.push(...collectHtmlFiles(fullPath))
1615
+ } else if (entry.name === 'index.html') {
1616
+ files.push(fullPath)
1617
+ }
1618
+ }
1619
+ return files
1620
+ }
1621
+
1622
+ function parseSitemap(xml: string): Map<string, UrlEntry> {
1623
+ const entries = new Map<string, UrlEntry>()
1624
+ const urlBlockRe = /<url>([\\s\\S]*?)<\\/url>/g
1625
+ const locRe = /<loc>([\\s\\S]*?)<\\/loc>/
1626
+ const lastmodRe = /<lastmod>([\\s\\S]*?)<\\/lastmod>/
1627
+ let match: RegExpExecArray | null
1628
+ while ((match = urlBlockRe.exec(xml)) !== null) {
1629
+ const block = match[1]
1630
+ const locMatch = locRe.exec(block)
1631
+ if (!locMatch) continue
1632
+ const loc = locMatch[1].trim()
1633
+ const lastmodMatch = lastmodRe.exec(block)
1634
+ entries.set(loc, { loc, lastmod: lastmodMatch ? lastmodMatch[1].trim() : null })
1635
+ }
1636
+ return entries
1637
+ }
1638
+
1639
+ function buildSitemapXml(entries: Map<string, UrlEntry>): string {
1640
+ const sorted = [...entries.values()].sort((a, b) => a.loc.localeCompare(b.loc))
1641
+ const urlElements = sorted.map((entry) => {
1642
+ const lastmodLine = entry.lastmod ? \`\\n <lastmod>\${entry.lastmod}</lastmod>\` : ''
1643
+ return \` <url>\\n <loc>\${entry.loc}</loc>\${lastmodLine}\\n </url>\`
1644
+ })
1645
+ return [
1646
+ '<?xml version="1.0" encoding="UTF-8"?>',
1647
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
1648
+ ...urlElements,
1649
+ '</urlset>',
1650
+ '',
1651
+ ].join('\\n')
1652
+ }
1653
+
1654
+ function createEntry(htmlPath: string): UrlEntry | null {
1655
+ if (hasNoindex(htmlPath)) return null
1656
+ return { loc: baseUrl + htmlPathToUrlPath(htmlPath), lastmod: getLastmod(htmlPath) }
1657
+ }
1658
+
1659
+ function main(): void {
1660
+ if (logLevel !== 'quiet') {
1661
+ console.log(\`Mode: \${values['full-rebuild'] ? 'full-rebuild' : 'incremental'}\`)
1662
+ console.log(\`Dry run: \${values['dry-run']}\`)
1663
+ }
1664
+
1665
+ const entries = new Map<string, UrlEntry>()
1666
+ const htmlFiles = collectHtmlFiles(HTML_ROOT)
1667
+ for (const htmlPath of htmlFiles) {
1668
+ const entry = createEntry(htmlPath)
1669
+ if (entry) entries.set(entry.loc, entry)
1670
+ }
1671
+
1672
+ const xml = buildSitemapXml(entries)
1673
+
1674
+ if (!values['dry-run']) {
1675
+ mkdirSync(dirname(OUTPUT_PATH), { recursive: true })
1676
+ writeFileSync(OUTPUT_PATH, xml)
1677
+ }
1678
+
1679
+ if (logLevel !== 'quiet') {
1680
+ console.log(\`Total URLs: \${entries.size}\`)
1681
+ console.log(\`Output: \${OUTPUT_PATH}\`)
1682
+ }
1683
+ }
1684
+
1685
+ main()
1686
+ `);
1687
+ writeFile(s('generate-dependency-map.ts'), `/**
1688
+ * 依存マップ生成スクリプト
1689
+ *
1690
+ * FrelioBuildDataRecipe から FrelioDependencyMap を生成する。
1691
+ *
1692
+ * @example
1693
+ * npx tsx scripts/generate-dependency-map.ts
1694
+ */
1695
+
1696
+ import { convertRecipeToDependencyMap } from '@c-time/frelio-data-json-recipe-to-dependency-map'
1697
+ import { validateSiteRecipe, formatZodErrors } from '@c-time/frelio-types/schemas'
1698
+ import { isFrelioDependencyMap } from '@c-time/frelio-dependency-map'
1699
+ import type { FrelioBuildDataRecipe } from '@c-time/frelio-data-json-recipe'
1700
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs'
1701
+ import { dirname } from 'path'
1702
+
1703
+ const RECIPE_PATH = 'frelio-data/admin/recipes/build-data-recipe.json'
1704
+ const OUTPUT_PATH = 'frelio-data/site/data/_dependency-map.json'
1705
+
1706
+ const raw = JSON.parse(readFileSync(RECIPE_PATH, 'utf-8'))
1707
+
1708
+ const result = validateSiteRecipe(raw)
1709
+ if (!result.success) {
1710
+ console.error('Invalid recipe:', formatZodErrors(result.errors))
1711
+ process.exit(1)
1712
+ }
1713
+
1714
+ const recipe: FrelioBuildDataRecipe = result.data
1715
+ const dependencyMap = convertRecipeToDependencyMap(recipe)
1716
+
1717
+ if (!isFrelioDependencyMap(dependencyMap)) {
1718
+ console.error('Generated dependency map is invalid')
1719
+ process.exit(1)
1720
+ }
1721
+
1722
+ mkdirSync(dirname(OUTPUT_PATH), { recursive: true })
1723
+ writeFileSync(OUTPUT_PATH, JSON.stringify(dependencyMap, null, 2))
1724
+ console.log(\`Dependency map written to \${OUTPUT_PATH}\`)
1725
+ `);
1726
+ writeFile(s('rebuild-indexes.ts'), `/**
1727
+ * インデックス・ダッシュボードメタデータ一括再構築スクリプト
1728
+ *
1729
+ * @example
1730
+ * npx tsx scripts/rebuild-indexes.ts
1731
+ * npx tsx scripts/rebuild-indexes.ts --dry-run
1732
+ */
1733
+
1734
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs'
1735
+ import { dirname } from 'path'
1736
+ import { parseArgs } from 'util'
1737
+ import {
1738
+ rebuildAllIndexes,
1739
+ calcMetadataOnSave,
1740
+ SITE_CONTENTS,
1741
+ CONTENT_TYPE_LIST_PATH,
1742
+ DASHBOARD_METADATA_PATH,
1743
+ type ContentStorePort,
1744
+ type ContentData,
1745
+ type FileChange,
1746
+ type BasePath,
1747
+ } from '@c-time/frelio-content-ops'
1748
+ import type { DashboardMetadata } from '@c-time/frelio-types'
1749
+
1750
+ const { values } = parseArgs({
1751
+ options: {
1752
+ 'dry-run': { type: 'boolean', default: false },
1753
+ 'log-level': { type: 'string', default: 'info' },
1754
+ },
1755
+ })
1756
+
1757
+ const DRY_RUN = values['dry-run']!
1758
+ const LOG_LEVEL = values['log-level'] as 'debug' | 'info' | 'quiet'
1759
+
1760
+ function logInfo(...args: unknown[]) {
1761
+ if (LOG_LEVEL !== 'quiet') console.log('[info]', ...args)
1762
+ }
1763
+
1764
+ function createFsContentStore(): ContentStorePort {
1765
+ return {
1766
+ async readJson<T>(path: string): Promise<T | null> {
1767
+ if (!existsSync(path)) return null
1768
+ try {
1769
+ return JSON.parse(readFileSync(path, 'utf-8')) as T
1770
+ } catch {
1771
+ return null
1772
+ }
1773
+ },
1774
+ }
1775
+ }
1776
+
1777
+ function applyFileChanges(changes: FileChange[]) {
1778
+ for (const change of changes) {
1779
+ mkdirSync(dirname(change.path), { recursive: true })
1780
+ writeFileSync(change.path, change.content + '\\n')
1781
+ }
1782
+ }
1783
+
1784
+ async function main() {
1785
+ const store = createFsContentStore()
1786
+ const listRaw = await store.readJson<{ contentTypes: { id: string }[] }>(CONTENT_TYPE_LIST_PATH)
1787
+ if (!listRaw) {
1788
+ console.error(\`Content type list not found: \${CONTENT_TYPE_LIST_PATH}\`)
1789
+ process.exit(1)
1790
+ }
1791
+ const contentTypeIds = listRaw.contentTypes.map((ct) => ct.id)
1792
+ logInfo(\`Content types: \${contentTypeIds.join(', ')}\`)
1793
+
1794
+ const allChanges: FileChange[] = []
1795
+ let dashboardMetadata: DashboardMetadata = { contentTypes: {} }
1796
+ const now = new Date().toISOString()
1797
+
1798
+ for (const contentTypeId of contentTypeIds) {
1799
+ const allContent: { basePath: BasePath; content: ContentData }[] = []
1800
+ for (const basePath of ['published', 'private'] as const) {
1801
+ const dir = \`\${SITE_CONTENTS}/\${basePath}/\${contentTypeId}\`
1802
+ if (!existsSync(dir)) continue
1803
+ const files = readdirSync(dir).filter((f) => f.endsWith('.json') && !f.startsWith('_'))
1804
+ for (const file of files) {
1805
+ const content = await store.readJson<ContentData>(\`\${dir}/\${file}\`)
1806
+ if (content) allContent.push({ basePath, content })
1807
+ }
1808
+ }
1809
+ logInfo(\`\${contentTypeId}: \${allContent.length} content(s) found\`)
1810
+ const indexChanges = await rebuildAllIndexes(store, { contentTypeId, allContent })
1811
+ allChanges.push(...indexChanges)
1812
+ for (const { content } of allContent) {
1813
+ dashboardMetadata = calcMetadataOnSave(
1814
+ dashboardMetadata, contentTypeId, content.updatedBy,
1815
+ null, content.status, now,
1816
+ )
1817
+ }
1818
+ }
1819
+
1820
+ allChanges.push({
1821
+ path: DASHBOARD_METADATA_PATH,
1822
+ content: JSON.stringify(dashboardMetadata, null, 2),
1823
+ })
1824
+
1825
+ logInfo(\`Files to write: \${allChanges.length}\`)
1826
+ if (DRY_RUN) {
1827
+ logInfo('Dry run — no files written.')
1828
+ return
1829
+ }
1830
+ applyFileChanges(allChanges)
1831
+ logInfo(\`Done. \${allChanges.length} file(s) written.\`)
1832
+ }
1833
+
1834
+ main().catch((error) => {
1835
+ console.error('Fatal error:', error)
1836
+ process.exit(1)
1837
+ })
1838
+ `);
1839
+ writeFile(s('watch-content.ts'), `/**
1840
+ * コンテンツ変更監視スクリプト
1841
+ *
1842
+ * frelio-data/ 内のコンテンツ JSON の変更を検知し、
1843
+ * ビューインデックスとダッシュボードメタデータを自動更新する。
1844
+ *
1845
+ * @example
1846
+ * npx tsx scripts/watch-content.ts
1847
+ * npx tsx scripts/watch-content.ts --log-level debug
1848
+ */
1849
+
1850
+ import { watch, readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync, readdirSync } from 'fs'
1851
+ import { dirname } from 'path'
1852
+ import { parseArgs } from 'util'
1853
+ import {
1854
+ computeViewIndexUpsert,
1855
+ computeViewIndexRemoval,
1856
+ computeDashboardMetadataOnSave,
1857
+ computeDashboardMetadataOnDelete,
1858
+ rebuildAllIndexes,
1859
+ SITE_CONTENTS,
1860
+ ADMIN_CONTENT_TYPES,
1861
+ type ContentStorePort,
1862
+ type ContentData,
1863
+ type FileChange,
1864
+ type BasePath,
1865
+ } from '@c-time/frelio-content-ops'
1866
+
1867
+ const { values } = parseArgs({
1868
+ options: {
1869
+ 'log-level': { type: 'string', default: 'info' },
1870
+ 'debounce-ms': { type: 'string', default: '300' },
1871
+ },
1872
+ })
1873
+
1874
+ const LOG_LEVEL = values['log-level'] as 'debug' | 'info' | 'quiet'
1875
+ const DEBOUNCE_MS = Number(values['debounce-ms'])
1876
+
1877
+ function logDebug(...args: unknown[]) {
1878
+ if (LOG_LEVEL === 'debug') console.log('[debug]', ...args)
1879
+ }
1880
+ function logInfo(...args: unknown[]) {
1881
+ if (LOG_LEVEL !== 'quiet') console.log('[info]', ...args)
1882
+ }
1883
+
1884
+ function createFsContentStore(): ContentStorePort {
1885
+ return {
1886
+ async readJson<T>(path: string): Promise<T | null> {
1887
+ if (!existsSync(path)) return null
1888
+ try {
1889
+ return JSON.parse(readFileSync(path, 'utf-8')) as T
1890
+ } catch {
1891
+ return null
1892
+ }
1893
+ },
1894
+ }
1895
+ }
1896
+
1897
+ type ContentChangeEvent = {
1898
+ kind: 'content-save' | 'content-delete'
1899
+ basePath: BasePath
1900
+ contentTypeId: string
1901
+ contentId: string
1902
+ }
1903
+
1904
+ type ViewsChangeEvent = {
1905
+ kind: 'views-change'
1906
+ contentTypeId: string
1907
+ }
1908
+
1909
+ type ChangeEvent = ContentChangeEvent | ViewsChangeEvent
1910
+
1911
+ function classifyChange(filePath: string): ChangeEvent | null {
1912
+ const normalized = filePath.replace(/\\\\\\\\/g, '/')
1913
+ const contentMatch = normalized.match(
1914
+ /^frelio-data\\/site\\/contents\\/(published|private)\\/([^/]+)\\/([^/]+)\\.json$/,
1915
+ )
1916
+ if (contentMatch) {
1917
+ const [, basePath, contentTypeId, fileName] = contentMatch
1918
+ if (fileName.startsWith('_')) return null
1919
+ const exists = existsSync(filePath)
1920
+ return {
1921
+ kind: exists ? 'content-save' : 'content-delete',
1922
+ basePath: basePath as BasePath,
1923
+ contentTypeId,
1924
+ contentId: fileName,
1925
+ }
1926
+ }
1927
+ const viewsMatch = normalized.match(
1928
+ /^frelio-data\\/admin\\/content_types\\/([^/]+)\\.views\\.json$/,
1929
+ )
1930
+ if (viewsMatch) {
1931
+ return { kind: 'views-change', contentTypeId: viewsMatch[1] }
1932
+ }
1933
+ return null
1934
+ }
1935
+
1936
+ function applyFileChanges(changes: FileChange[]) {
1937
+ for (const change of changes) {
1938
+ if (change.delete) {
1939
+ if (existsSync(change.path)) unlinkSync(change.path)
1940
+ } else {
1941
+ mkdirSync(dirname(change.path), { recursive: true })
1942
+ writeFileSync(change.path, change.content + '\\n')
1943
+ }
1944
+ }
1945
+ }
1946
+
1947
+ const store = createFsContentStore()
1948
+ const suppressedPaths = new Set<string>()
1949
+
1950
+ function applyFileChangesWithSuppress(changes: FileChange[]) {
1951
+ for (const change of changes) {
1952
+ const normalized = change.path.replace(/\\\\\\\\/g, '/')
1953
+ suppressedPaths.add(normalized)
1954
+ setTimeout(() => suppressedPaths.delete(normalized), DEBOUNCE_MS + 200)
1955
+ }
1956
+ applyFileChanges(changes)
1957
+ }
1958
+
1959
+ async function handleContentSave(event: ContentChangeEvent) {
1960
+ const { basePath, contentTypeId, contentId } = event
1961
+ const contentPath = \`\${SITE_CONTENTS}/\${basePath}/\${contentTypeId}/\${contentId}.json\`
1962
+ const content = await store.readJson<ContentData>(contentPath)
1963
+ if (!content) return
1964
+
1965
+ const viewChanges = await computeViewIndexUpsert(store, { basePath, contentTypeId, content })
1966
+ const metaChange = await computeDashboardMetadataOnSave(store, {
1967
+ contentTypeId, updatedBy: content.updatedBy,
1968
+ oldStatus: null, newStatus: content.status,
1969
+ })
1970
+ applyFileChangesWithSuppress([...viewChanges, metaChange])
1971
+ logInfo(\`Content saved: \${basePath}/\${contentTypeId}/\${contentId}\`)
1972
+ }
1973
+
1974
+ async function handleContentDelete(event: ContentChangeEvent) {
1975
+ const { basePath, contentTypeId, contentId } = event
1976
+ const viewChanges = await computeViewIndexRemoval(store, { basePath, contentTypeId, contentId })
1977
+ const metaChange = await computeDashboardMetadataOnDelete(store, {
1978
+ contentTypeId, deletedBy: 'unknown', deletedStatus: 'draft' as ContentData['status'],
1979
+ })
1980
+ applyFileChangesWithSuppress([...viewChanges, metaChange])
1981
+ logInfo(\`Content deleted: \${basePath}/\${contentTypeId}/\${contentId}\`)
1982
+ }
1983
+
1984
+ async function handleViewsChange(event: ViewsChangeEvent) {
1985
+ const { contentTypeId } = event
1986
+ logInfo(\`Views changed for: \${contentTypeId}, rebuilding indexes...\`)
1987
+ const allContent: { basePath: BasePath; content: ContentData }[] = []
1988
+ for (const basePath of ['published', 'private'] as const) {
1989
+ const dir = \`\${SITE_CONTENTS}/\${basePath}/\${contentTypeId}\`
1990
+ if (!existsSync(dir)) continue
1991
+ const files = readdirSync(dir).filter((f) => f.endsWith('.json') && !f.startsWith('_'))
1992
+ for (const file of files) {
1993
+ const content = await store.readJson<ContentData>(\`\${dir}/\${file}\`)
1994
+ if (content) allContent.push({ basePath, content })
1995
+ }
1996
+ }
1997
+ const changes = await rebuildAllIndexes(store, { contentTypeId, allContent })
1998
+ applyFileChangesWithSuppress(changes)
1999
+ logInfo(\`Rebuilt \${changes.length} index file(s) for \${contentTypeId}\`)
2000
+ }
2001
+
2002
+ const pending = new Map<string, NodeJS.Timeout>()
2003
+ let processing = Promise.resolve()
2004
+
2005
+ function enqueue(fn: () => Promise<void>) {
2006
+ processing = processing.then(fn).catch((err) => console.error('Handler error:', err))
2007
+ }
2008
+
2009
+ function scheduleHandler(filePath: string, handler: () => Promise<void>) {
2010
+ const existing = pending.get(filePath)
2011
+ if (existing) clearTimeout(existing)
2012
+ pending.set(filePath, setTimeout(() => {
2013
+ pending.delete(filePath)
2014
+ enqueue(handler)
2015
+ }, DEBOUNCE_MS))
2016
+ }
2017
+
2018
+ async function main() {
2019
+ logInfo('Starting content watcher...')
2020
+ const watchTargets = [
2021
+ { path: SITE_CONTENTS, label: 'contents' },
2022
+ { path: ADMIN_CONTENT_TYPES, label: 'admin/content_types' },
2023
+ ]
2024
+
2025
+ for (const target of watchTargets) {
2026
+ if (!existsSync(target.path)) {
2027
+ console.error(\`Watch target not found: \${target.path}\`)
2028
+ process.exit(1)
2029
+ }
2030
+ watch(target.path, { recursive: true }, (_eventType, fileName) => {
2031
+ if (!fileName) return
2032
+ const fullPath = \`\${target.path}/\${fileName.replace(/\\\\\\\\/g, '/')}\`
2033
+ if (!fullPath.endsWith('.json')) return
2034
+ if (suppressedPaths.has(fullPath)) return
2035
+ const event = classifyChange(fullPath)
2036
+ if (!event) return
2037
+ scheduleHandler(fullPath, async () => {
2038
+ const latestEvent = classifyChange(fullPath)
2039
+ if (!latestEvent) return
2040
+ switch (latestEvent.kind) {
2041
+ case 'content-save': await handleContentSave(latestEvent); break
2042
+ case 'content-delete': await handleContentDelete(latestEvent); break
2043
+ case 'views-change': await handleViewsChange(latestEvent); break
2044
+ }
2045
+ })
2046
+ })
2047
+ logInfo(\`Watching: \${target.path}\`)
2048
+ }
2049
+
2050
+ logInfo('Ready. Press Ctrl+C to stop.')
2051
+ process.on('SIGINT', () => {
2052
+ logInfo('\\nShutting down...')
2053
+ for (const timer of pending.values()) clearTimeout(timer)
2054
+ process.exit(0)
2055
+ })
2056
+ }
2057
+
2058
+ main().catch((error) => {
2059
+ console.error('Fatal error:', error)
2060
+ process.exit(1)
2061
+ })
2062
+ `);
2063
+ }
1271
2064
  // ========== CLAUDE.md ==========
1272
2065
  function generateClaudeMd(projectDir) {
1273
2066
  writeFile(path.join(projectDir, 'CLAUDE.md'), `# CLAUDE.md
@@ -1277,26 +2070,61 @@ Frelio(ヘッドレス CMS)で構築されたサイトリポジトリ。
1277
2070
 
1278
2071
  ## プロジェクト構成
1279
2072
 
1280
- - \`admin/\` — CMS 管理画面(ビルド済み、\`config.json\` で設定)
1281
- - \`public/\` — 公開サイトルート(SSG 出力先)
1282
- - \`functions/\` — Cloudflare Pages Functions
1283
- - \`api/auth/\` — OAuth 認証
1284
- - \`api/storage/\` — CMS ファイルアップロード API
1285
- - \`storage/[[path]].ts\` — R2 ファイル配信(/storage/*)
1286
2073
  - \`frelio-data/\` — CMS データ(コンテンツタイプ、コンテンツ、テンプレート、レシピ)
1287
- - \`site/templates/assets/scss/\` — SCSS ソース(FLOCSS 亜種)
1288
- - \`site/templates/assets/ts/\` — TypeScript ソース
2074
+ - \`site/templates/assets/scss/\` — 共有 SCSS パーシャル(FLOCSS 亜種)
2075
+ - \`site/templates/assets/ts/\` — 共有 TypeScript(features/)
2076
+ - \`site/templates/assets/entries/\` — ページ別エントリーポイント
2077
+ - \`public/\` — SSG 出力(HTML + ビルド済みアセット)
2078
+ - \`functions/storage/\` — R2 ファイル配信(/storage/*)
2079
+ - \`scripts/\` — ビルドスクリプト(tsx)
1289
2080
  - \`public/images/\` — 静的ファイル(画像等、git 追跡対象)
1290
2081
 
2082
+ CMS 管理画面関連(\`admin/\`, \`functions/api/\`, \`workers/\`, \`wrangler.toml\`, \`_redirects\`)は
2083
+ \`npx @frelio/cli update\` で追加・更新される。
2084
+
1291
2085
  ## よく使うコマンド
1292
2086
 
1293
2087
  \`\`\`bash
1294
- npm run dev # Vite dev server(テンプレートプレビュー)
1295
- npm run build # SCSS/TS ビルド
1296
- npx @frelio/cli update # CMS Admin バンドル更新
2088
+ npm run dev # Vite dev server(テンプレートプレビュー + コンテンツ監視)
2089
+ npm run build # SCSS/TS ビルド(ページ別エントリー)
2090
+ npm run generate # data-json 生成(差分ビルド)
2091
+ npm run generate:full # data-json 生成(フルリビルド)
2092
+ npm run generate:html # HTML 生成(data-json → public/)
2093
+ npm run generate:sitemap # sitemap.xml 生成
2094
+ npm run generate:dep-map # 依存マップ生成
2095
+ npm run watch:content # コンテンツ変更監視(インデックス自動更新)
2096
+ npm run rebuild:indexes # インデックス一括再構築
2097
+ npx @frelio/cli update # CMS Admin バンドル更新
1297
2098
  npx @frelio/cli add-staging # カスタムステージング追加
1298
2099
  \`\`\`
1299
2100
 
2101
+ ## ビルドパイプライン
2102
+
2103
+ \`\`\`
2104
+ 1. Recipe → 依存マップ (npm run generate:dep-map)
2105
+ 2. コンテンツ → data-json (npm run generate)
2106
+ 3. data-json → HTML (npm run generate:html)
2107
+ 4. SCSS/TS → CSS/JS (npm run build)
2108
+ 5. sitemap.xml 生成 (npm run generate:sitemap)
2109
+ \`\`\`
2110
+
2111
+ ## ページ別エントリーポイント
2112
+
2113
+ アセットはページ単位でコード分割される。各ページに \`styles/index.scss\` と \`scripts/index.ts\` がある。
2114
+
2115
+ \`\`\`
2116
+ assets/entries/
2117
+ ├── common/ — 全ページ共通(foundation, layout, component, element + JS初期化)
2118
+ ├── home/ — トップページ(p-hero, p-news-list)
2119
+ ├── about/ — 会社概要(p-about)
2120
+ ├── contact/ — お問い合わせ(p-contact)
2121
+ ├── news/ — お知らせ一覧(p-news-list)
2122
+ └── news/detail/ — 記事詳細(p-article)
2123
+ \`\`\`
2124
+
2125
+ - \`_parts/head.htm\` で common の CSS/JS を読み込み
2126
+ - 各ページテンプレートでページ固有の CSS/JS を読み込み
2127
+
1300
2128
  ## CSS 記法ルール(FLOCSS 亜種・厳格)
1301
2129
 
1302
2130
  - **プレフィックス**: \`l-\`(layout)、\`c-\`(component)、\`p-\`(project)、\`e-\`(element)のみ
@@ -1317,8 +2145,9 @@ npx @frelio/cli add-staging # カスタムステージング追加
1317
2145
 
1318
2146
  ## TypeScript ルール
1319
2147
 
1320
- - \`frelio-data/site/templates/assets/ts/index.ts\` はオーケストレーション専用(import + init 呼び出しのみ)
1321
- - 各機能は \`frelio-data/site/templates/assets/ts/features/\` にファイル分離
2148
+ - 共通の初期化ロジックは \`entries/common/scripts/index.ts\` に集約
2149
+ - 各機能は \`assets/ts/features/\` にファイル分離
2150
+ - ページ固有の JS が必要な場合は \`entries/{page}/scripts/index.ts\` に追加
1322
2151
 
1323
2152
  ## テンプレート規約
1324
2153
 
@@ -1329,6 +2158,7 @@ npx @frelio/cli add-staging # カスタムステージング追加
1329
2158
 
1330
2159
  ## Cloudflare Pages 構成
1331
2160
 
2161
+ \`npx @frelio/cli update\` 実行後に以下が配置される:
1332
2162
  - \`_redirects\`: \`/admin/*\` → SPA、\`/*\` → \`/public/:splat\`
1333
2163
  - \`_routes.json\`: \`/api/*\`, \`/storage/*\` → Functions
1334
2164
  - \`wrangler.toml\`: R2 バケットバインディング
@@ -33,6 +33,7 @@ export declare function generateStorageFunction(): string;
33
33
  export declare function generateViteConfig(): string;
34
34
  export declare function generatePackageJson(config: ProjectConfig): string;
35
35
  export declare function generateTsConfig(): string;
36
+ export declare function generateTsConfigNode(): string;
36
37
  /**
37
38
  * ファイルを書き込む(ディレクトリがなければ作成)
38
39
  */
@@ -133,25 +133,121 @@ export const onRequest: PagesFunction<Env> = async (context) => {
133
133
  `;
134
134
  }
135
135
  export function generateViteConfig() {
136
- return `import { defineConfig } from 'vite'
136
+ return `import { defineConfig, type Plugin } from 'vite'
137
137
  import { resolve } from 'path'
138
+ import { spawn, type ChildProcess } from 'child_process'
138
139
 
139
140
  const templateAssets = resolve(__dirname, 'frelio-data/site/templates/assets')
141
+ const entries = resolve(templateAssets, 'entries')
142
+
143
+ /** dev server 起動時にコンテンツ変更監視を自動開始する Vite プラグイン */
144
+ function contentWatcherPlugin(): Plugin {
145
+ let watcher: ChildProcess | null = null
146
+ return {
147
+ name: 'content-watcher',
148
+ apply: 'serve',
149
+ configureServer(server) {
150
+ watcher = spawn('npx', ['tsx', 'scripts/watch-content.ts'], {
151
+ cwd: resolve(__dirname),
152
+ stdio: 'inherit',
153
+ shell: true,
154
+ })
155
+ watcher.on('error', (err) => {
156
+ console.error('[content-watcher] Failed to start:', err.message)
157
+ })
158
+ server.httpServer?.on('close', () => {
159
+ watcher?.kill()
160
+ })
161
+ },
162
+ buildEnd() {
163
+ watcher?.kill()
164
+ watcher = null
165
+ },
166
+ }
167
+ }
168
+
169
+ /** ビルド時に CSS を対応する JS チャンクのパスに合わせて styles/ 配下へ配置する */
170
+ function cssRelocatePlugin(): Plugin {
171
+ return {
172
+ name: 'css-relocate',
173
+ apply: 'build',
174
+ enforce: 'post',
175
+ generateBundle(_, bundle) {
176
+ const renames: Array<{ chunkName: string; oldCss: string; newCss: string }> = []
177
+
178
+ for (const chunk of Object.values(bundle)) {
179
+ if (chunk.type !== 'chunk' || !chunk.isEntry) continue
180
+ const meta = (chunk as any).viteMetadata as
181
+ | { importedCss?: Set<string> }
182
+ | undefined
183
+ if (!meta?.importedCss?.size) continue
184
+
185
+ const newCss = chunk.name.replace('/scripts/', '/styles/') + '.css'
186
+ for (const oldCss of meta.importedCss) {
187
+ if (oldCss !== newCss) {
188
+ renames.push({ chunkName: chunk.name, oldCss, newCss })
189
+ }
190
+ }
191
+ }
192
+
193
+ for (const { oldCss, newCss } of renames) {
194
+ const asset = bundle[oldCss]
195
+ if (!asset) continue
196
+ delete bundle[oldCss]
197
+ asset.fileName = newCss
198
+ bundle[newCss] = asset
199
+ }
200
+
201
+ for (const chunk of Object.values(bundle)) {
202
+ if (chunk.type !== 'chunk' || !chunk.isEntry) continue
203
+ const meta = (chunk as any).viteMetadata as
204
+ | { importedCss?: Set<string> }
205
+ | undefined
206
+ if (!meta?.importedCss) continue
207
+ for (const { oldCss, newCss } of renames) {
208
+ if (meta.importedCss.has(oldCss)) {
209
+ meta.importedCss.delete(oldCss)
210
+ meta.importedCss.add(newCss)
211
+ }
212
+ }
213
+ }
214
+ },
215
+ }
216
+ }
140
217
 
141
218
  export default defineConfig(({ command }) => ({
142
219
  root: 'frelio-data/site/templates',
143
220
  publicDir: command === 'serve' ? resolve(__dirname, 'public') : false,
221
+ plugins: [contentWatcherPlugin(), cssRelocatePlugin()],
222
+ resolve: {
223
+ alias: {
224
+ '@features': resolve(templateAssets, 'ts/features'),
225
+ },
226
+ },
227
+ css: {
228
+ preprocessorOptions: {
229
+ scss: {
230
+ api: 'modern-compiler',
231
+ loadPaths: [resolve(templateAssets, 'scss')],
232
+ },
233
+ },
234
+ },
144
235
  build: {
145
236
  outDir: resolve(__dirname, 'public'),
146
237
  emptyOutDir: false,
238
+ cssCodeSplit: true,
147
239
  rollupOptions: {
148
240
  input: {
149
- style: resolve(templateAssets, 'scss/style.scss'),
150
- index: resolve(templateAssets, 'ts/index.ts'),
241
+ 'common/scripts/index': resolve(entries, 'common/scripts/index.ts'),
242
+ 'home/scripts/index': resolve(entries, 'home/scripts/index.ts'),
243
+ 'about/scripts/index': resolve(entries, 'about/scripts/index.ts'),
244
+ 'contact/scripts/index': resolve(entries, 'contact/scripts/index.ts'),
245
+ 'news/scripts/index': resolve(entries, 'news/scripts/index.ts'),
246
+ 'news/detail/scripts/index': resolve(entries, 'news/detail/scripts/index.ts'),
151
247
  },
152
248
  output: {
153
- entryFileNames: 'assets/[name].js',
154
- assetFileNames: 'assets/[name].[ext]',
249
+ entryFileNames: '[name].js',
250
+ assetFileNames: '[name].[ext]',
155
251
  },
156
252
  },
157
253
  },
@@ -170,11 +266,31 @@ export function generatePackageJson(config) {
170
266
  dev: 'vite',
171
267
  build: 'vite build',
172
268
  preview: 'vite preview',
269
+ 'generate:dep-map': 'tsx scripts/generate-dependency-map.ts',
270
+ generate: 'tsx scripts/generate-data-json.ts',
271
+ 'generate:full': 'tsx scripts/generate-data-json.ts --full-rebuild',
272
+ 'generate:dry-run': 'tsx scripts/generate-data-json.ts --dry-run',
273
+ 'generate:html': 'tsx scripts/generate-html.ts',
274
+ 'generate:html:dry-run': 'tsx scripts/generate-html.ts --dry-run',
275
+ 'generate:sitemap': 'tsx scripts/generate-sitemap.ts',
276
+ 'generate:sitemap:full': 'tsx scripts/generate-sitemap.ts --full-rebuild',
277
+ 'watch:content': 'tsx scripts/watch-content.ts',
278
+ 'rebuild:indexes': 'tsx scripts/rebuild-indexes.ts',
279
+ 'rebuild:indexes:dry-run': 'tsx scripts/rebuild-indexes.ts --dry-run',
173
280
  },
174
281
  devDependencies: {
175
- vite: '^6.0.0',
282
+ '@c-time/frelio-content-ops': '^0.1.0',
283
+ '@c-time/frelio-data-json-generator': '*',
284
+ '@c-time/frelio-data-json-recipe': '^1.2.0',
285
+ '@c-time/frelio-data-json-recipe-to-dependency-map': '*',
286
+ '@c-time/frelio-dependency-map': '*',
287
+ '@c-time/frelio-gentl': '*',
288
+ '@c-time/frelio-types': '^0.1.0',
176
289
  sass: '^1.80.0',
290
+ tsx: '^4.0.0',
177
291
  typescript: '^5.7.0',
292
+ vite: '^6.0.0',
293
+ zod: '^3.25.0',
178
294
  },
179
295
  }, null, 2);
180
296
  }
@@ -183,13 +299,29 @@ export function generateTsConfig() {
183
299
  compilerOptions: {
184
300
  target: 'ES2022',
185
301
  module: 'ESNext',
186
- moduleResolution: 'Bundler',
187
- lib: ['ES2022', 'DOM', 'DOM.Iterable'],
302
+ moduleResolution: 'bundler',
188
303
  strict: true,
304
+ esModuleInterop: true,
189
305
  skipLibCheck: true,
306
+ resolveJsonModule: true,
307
+ outDir: 'dist',
308
+ },
309
+ include: ['frelio-data/site/templates/assets/ts/**/*.ts', 'vite.config.ts'],
310
+ }, null, 2);
311
+ }
312
+ export function generateTsConfigNode() {
313
+ return JSON.stringify({
314
+ compilerOptions: {
315
+ target: 'ES2022',
316
+ module: 'NodeNext',
317
+ moduleResolution: 'NodeNext',
318
+ strict: true,
190
319
  esModuleInterop: true,
320
+ skipLibCheck: true,
321
+ resolveJsonModule: true,
322
+ outDir: 'dist',
191
323
  },
192
- include: ['frelio-data/site/templates/assets/ts'],
324
+ include: ['scripts/**/*.ts'],
193
325
  }, null, 2);
194
326
  }
195
327
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c-time/frelio-cli",
3
- "version": "0.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "Frelio CMS setup CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",