@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.
- package/dist/commands/init.js +5 -2
- package/dist/lib/initial-content.js +891 -61
- package/dist/lib/templates.d.ts +1 -0
- package/dist/lib/templates.js +141 -9
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -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="/
|
|
168
|
-
<script type="module" src="/
|
|
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="/
|
|
206
|
-
<script type="module" src="/
|
|
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="/
|
|
282
|
-
<script type="module" src="/
|
|
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="/
|
|
352
|
-
<script type="module" src="/
|
|
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="/
|
|
414
|
-
<script type="module" src="/
|
|
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="/
|
|
480
|
-
<script type="module" src="/
|
|
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
|
|
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
|
|
1295
|
-
npm run build
|
|
1296
|
-
|
|
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
|
-
- \`
|
|
1321
|
-
- 各機能は \`
|
|
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 バケットバインディング
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/lib/templates.js
CHANGED
|
@@ -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
|
-
|
|
150
|
-
index: resolve(
|
|
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: '
|
|
154
|
-
assetFileNames: '
|
|
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
|
-
|
|
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: '
|
|
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: ['
|
|
324
|
+
include: ['scripts/**/*.ts'],
|
|
193
325
|
}, null, 2);
|
|
194
326
|
}
|
|
195
327
|
/**
|