Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions apps/site/src/config/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,24 +118,16 @@ export const stackLabels: Record<Stack, string> = {
astro: 'Astro',
next: 'Next.js',
vite: 'Vite + React',
html: 'Static HTML',
};

export const stackOrder: Stack[] = ['astro', 'html', 'vite', 'next'];
export const stackOrder: Stack[] = ['astro', 'vite', 'next'];

function hasSourcePath(tpl: TemplateDef): tpl is Extract<TemplateDef, { sourcePath: string }> {
return 'sourcePath' in tpl;
}

function thumbFileName(tpl: Extract<TemplateDef, { sourcePath: string }>): string {
function thumbFileName(tpl: TemplateDef): string {
return tpl.kind === 'single-project-variant' ? `${tpl.variant}.png` : 'top.png';
}

/** 規約ベースで thumb を解決(解決できなければビルド時に失敗させる) */
function resolveThumb(tpl: TemplateDef): ImageMetadata {
if (!hasSourcePath(tpl)) {
throw new Error(`[templates] thumb resolution unsupported for kind "${tpl.kind}" (slug: ${tpl.slug})`);
}
const key = `../../../../templates/${tpl.sourcePath}/screenshots/${thumbFileName(tpl)}`;
const mod = screenshotModules[key];
if (!mod) {
Expand All @@ -146,7 +138,6 @@ function resolveThumb(tpl: TemplateDef): ImageMetadata {

/** en 用 thumb を解決(`screenshots/en/` 未撮影なら undefined → 呼び出し側で ja にフォールバック) */
function resolveThumbEn(tpl: TemplateDef): ImageMetadata | undefined {
if (!hasSourcePath(tpl)) return undefined;
const key = `../../../../templates/${tpl.sourcePath}/screenshots/en/${thumbFileName(tpl)}`;
return screenshotModules[key]?.default;
}
Expand Down
18 changes: 17 additions & 1 deletion docs/decisions.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
基準日: 2026-09-30・コミット4dc696023
基準日: 2026-09-30・コミットcd06232fa

# 意思決定の記録

## 2026-09-30: テンプレ種別`base-overlay`を削除し、variant違いは`single-project-variant`で持つ

#366では、`lp`と`web`のAstro版を「共通base + variantごとの差分(overlay)」で配る設計だった。`base-overlay`型はそのための型。しかし#372でLPには採用されず、`single-project-variant`型が作られた。variant間で構造の違い(独自のkeyframesやclass prefix)が大きく、共通baseにする利点が薄いため。以後、この型を使うテンプレは1つも無かった。

- 決定: `base-overlay`型と、CLIの取得・マージ・package名の書き換え処理、テストを削除する。
- 決定: variant違いのテンプレは`single-project-variant`型で持つ。今後`web`カテゴリを作るときもこれに倣う。プレビューも1サイトにまとめてビルドできる。
- 却下: 将来の`web`カテゴリ用に残す案。LPで却下した理由は、業種ごとに構造が違う`web`にも当てはまる。

## 2026-09-30: LPテンプレのHTML版はCLIで配らない

#375では、LPテンプレ(`templates/lp/astro/`)のビルド結果をHTML版としてCLIから配る予定だった。`static-html`型とCLIの分岐だけが先に実装されていた。調べると、Astroの出力はminify済みで手編集に向かない。JSは`type="module"`のため、`file://`で開くと動かない。さらにCLIはgigetでGitHubから取得するため、生成物をコミットし、sourceとのずれをCIで検査し続ける必要がある。

- 決定: HTML版は配らない。`static-html`型とstackの`html`、関連するCLIの分岐・文言・テストを削除する。
- 却下: #375の計画どおり`templates/lp/html/_generated/`を生成してコミットする案。整形・スクリプト変換・ずれ検査を保守し続ける手間に、利用価値が見合わない。
- 受容: HTMLが欲しい利用者には、Astro版を`npm run build`して`dist/`を使ってもらう。

## 2026-09-30: `@lism-css/mcp`はlism-cssと同じバージョンで出す

同日の決定「1.0.0以降、3つのバージョンは揃えない」のうち、mcpの部分を覆す。mcpはapps/siteから作ったdocs-indexと`skills/lism-css-guide`をビルド時に同梱して配るため、coreのリリースに合わせて出し直さないと古い情報を返し続ける。v0.26.0〜v0.30.0もcoreと同じバージョンで出していたが`/release`に書かれておらず、v0.31.0では出し忘れかけた。
Expand Down
2 changes: 0 additions & 2 deletions documents/cli-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ nr publish:cli # build → lism-cli publish → create-lism publish
### 規約

- 各テンプレの`package.json`に必ず`"private": true`を付ける(npmへの誤公開防止)。階層の深さは問わない(`scripts/check-templates-private.mjs`が`package.json`まで再帰して検出する)。
- `base-overlay`型(現在の`TEMPLATES`に登録なし)を追加する場合、overlay側に`package.json`を置かない。CLIはbaseの`package.json`を採用し、overlayは差分ファイルだけ上書きする。置くと`name`の書き換えが二重化し、`workspace:*`置換も崩れる。共通設定はbaseに集約する。
- `single-project-variant`型(例: `templates/lp/astro/`)は`src/pages/{variant}/`を並べる構成。CLIは選択variantの`index.astro`を`src/pages/index.astro`へ持ち上げ、他variantを削除する。variant追加は`src/pages/{variant}/index.astro`を作り、`TEMPLATES`にslugを足す。
- `templates/lp/html/_generated/`(未実装、#375で追加予定)はsourceからの生成物置き場。追加後は手編集禁止で、修正はgenerator側で行う。

### 言語

Expand Down
98 changes: 0 additions & 98 deletions packages/lism-cli/src/commands/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,54 +295,6 @@ describe('runCreate', () => {
});
});

it('base-overlay型はbase取得後にoverlayをmergeし、package.json.nameをslugにする', async () => {
const templates: Parameters<typeof runCreateWithTemplates>[1] = [
{
slug: 'lp-astro-saas',
kind: 'base-overlay',
category: 'lp',
stack: 'astro',
variant: 'saas',
variantLabel: { ja: 'SaaS', en: 'SaaS' },
basePath: 'lp/astro/_base',
overlayPath: 'lp/astro/_variants/saas',
title: { ja: 'LP SaaS', en: 'LP SaaS' },
description: { ja: 'SaaS LP', en: 'SaaS landing page' },
},
];
vi.mocked(downloadTemplate).mockImplementation((source, options) => {
const dir = (options as { dir: string }).dir;
fs.mkdirSync(dir, { recursive: true });
if (String(source).includes('/_base#')) {
writePackageJson(dir, { name: 'lp-astro-base', dependencies: { 'lism-css': 'workspace:*' } });
writeFile(path.join(dir, 'src/pages/index.astro'), 'base');
} else {
writeFile(path.join(dir, 'src/pages/index.astro'), 'overlay');
writeFile(path.join(dir, 'src/styles/variant.css'), '.hero{}');
}
return Promise.resolve({} as Awaited<ReturnType<typeof downloadTemplate>>);
});

await runCreateWithTemplates({ template: 'lp-astro-saas', targetDir: 'lp-app', force: true }, templates);

const outDir = path.join(tmpDir, 'lp-app');
expect(downloadTemplate).toHaveBeenCalledTimes(2);
expect(downloadTemplate).toHaveBeenNthCalledWith(1, 'github:lism-css/core/templates/lp/astro/_base#main', {
dir: outDir,
force: true,
forceClean: true,
});
expect(downloadTemplate).toHaveBeenNthCalledWith(2, 'github:lism-css/core/templates/lp/astro/_variants/saas#main', {
dir: expect.any(String),
force: true,
forceClean: true,
});
expect(fs.readFileSync(path.join(outDir, 'src/pages/index.astro'), 'utf-8')).toBe('overlay');
expect(fs.existsSync(path.join(outDir, 'src/styles/variant.css'))).toBe(true);
const pkg = JSON.parse(fs.readFileSync(path.join(outDir, 'package.json'), 'utf-8')) as { name: string };
expect(pkg.name).toBe('lp-astro-saas');
});

it('project型はlangOverlaysに要求言語があればbase取得後にoverlayをmergeし、.langを除去する', async () => {
const templates: Parameters<typeof runCreateWithTemplates>[1] = [
{
Expand Down Expand Up @@ -841,34 +793,6 @@ describe('runCreate', () => {
expect(fs.existsSync(path.join(outDir, 'src/pages/en'))).toBe(false);
});

it('static-html型はpackage.jsonなしでもindex.htmlがあれば完了し、HTML向けNext stepsを出す', async () => {
const templates: Parameters<typeof runCreateWithTemplates>[1] = [
{
slug: 'lp-html-saas',
kind: 'static-html',
category: 'lp',
stack: 'html',
variant: 'saas',
variantLabel: { ja: 'SaaS', en: 'SaaS' },
sourcePath: 'lp/html/_generated/saas',
title: { ja: 'LP SaaS HTML', en: 'LP SaaS HTML' },
description: { ja: 'SaaS LP HTML', en: 'SaaS landing page HTML' },
},
];
vi.mocked(downloadTemplate).mockImplementation((_source, options) => {
const dir = (options as { dir: string }).dir;
fs.mkdirSync(dir, { recursive: true });
writeFile(path.join(dir, 'index.html'), '<!doctype html>');
return Promise.resolve({} as Awaited<ReturnType<typeof downloadTemplate>>);
});

await runCreateWithTemplates({ template: 'lp-html-saas', targetDir: 'html-app', force: true }, templates);

const outDir = path.join(tmpDir, 'html-app');
expect(fs.existsSync(path.join(outDir, 'package.json'))).toBe(false);
expect(console.log).toHaveBeenCalledWith(' Open index.html in your browser');
});

it('--templateにカテゴリslugを指定するとstack選択へ降りる', async () => {
const templates: Parameters<typeof runCreateWithTemplates>[1] = [
{
Expand Down Expand Up @@ -932,28 +856,6 @@ describe('runCreate', () => {
await expect(runCreate({ template: 'minimal-astro', targetDir: 'broken', force: true })).rejects.toThrow('package.json');
});

it('static-html型でindex.htmlがない場合はtemplateIndexMissingを返す', async () => {
const templates: Parameters<typeof runCreateWithTemplates>[1] = [
{
slug: 'lp-html-empty',
kind: 'static-html',
category: 'lp',
stack: 'html',
sourcePath: 'lp/html/_generated/empty',
title: { ja: 'LP Empty', en: 'LP Empty' },
description: { ja: 'empty', en: 'empty' },
},
];
vi.mocked(downloadTemplate).mockImplementation((_source, options) => {
fs.mkdirSync((options as { dir: string }).dir, { recursive: true });
return Promise.resolve({} as Awaited<ReturnType<typeof downloadTemplate>>);
});

await expect(runCreateWithTemplates({ template: 'lp-html-empty', targetDir: 'empty-html', force: true }, templates)).rejects.toThrow(
'index.html'
);
});

it('上書き確認でnoなら中断してdownloadしない', async () => {
const outDir = path.join(tmpDir, 'occupied');
fs.mkdirSync(outDir, { recursive: true });
Expand Down
50 changes: 4 additions & 46 deletions packages/lism-cli/src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ const STACK_LABELS: Record<TemplateStack, LocalizedText> = {
astro: { ja: 'Astro', en: 'Astro' },
next: { ja: 'Next.js', en: 'Next.js' },
vite: { ja: 'Vite + React', en: 'Vite + React' },
html: { ja: 'Static HTML', en: 'Static HTML' },
};

const CATEGORIES: CategoryDef[] = [
Expand Down Expand Up @@ -114,7 +113,7 @@ export async function runCreateWithTemplates(
}

logger.info(t('create.fetching', { name: tpl.slug, ref }));
await downloadTemplateSource(tpl, outDir, ref, force);
await downloadTemplatePath(tpl.sourcePath, outDir, ref, force);

await applyLangOverlay(tpl, outDir, ref, resolvedLang);

Expand All @@ -123,7 +122,7 @@ export async function runCreateWithTemplates(
await postProcessTemplate(outDir, tpl, resolvedLang);

logger.success(t('create.created', { dir: outDir }));
printNextSteps(outDir, tpl);
printNextSteps(outDir);
}

export async function createCommand(targetDir: string | undefined, options: CreateOptions, command?: CommandLike): Promise<void> {
Expand Down Expand Up @@ -229,32 +228,9 @@ async function resolveTargetDir(provided: string | undefined, templateName: stri
}

function ensureTemplateDownloaded(projectDir: string, tpl: TemplateDef): void {
if (tpl.kind === 'static-html') {
const indexPath = path.join(projectDir, 'index.html');
if (fs.existsSync(indexPath)) return;
throw new Error(t('create.templateIndexMissing', { name: tpl.slug, path: getTemplateSourcePath(tpl) }));
}

const pkgPath = path.join(projectDir, 'package.json');
if (fs.existsSync(pkgPath)) return;
throw new Error(t('create.templatePackageMissing', { name: tpl.slug, path: getTemplateSourcePath(tpl) }));
}

async function downloadTemplateSource(tpl: TemplateDef, outDir: string, ref: string, forceClean: boolean): Promise<void> {
if (tpl.kind === 'base-overlay') {
await downloadTemplatePath(tpl.basePath, outDir, ref, forceClean);

const overlayDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lism-template-overlay-'));
try {
await downloadTemplatePath(tpl.overlayPath, overlayDir, ref, true);
mergeDirectory(overlayDir, outDir);
} finally {
fs.rmSync(overlayDir, { recursive: true, force: true });
}
return;
}

await downloadTemplatePath(tpl.sourcePath, outDir, ref, forceClean);
throw new Error(t('create.templatePackageMissing', { name: tpl.slug, path: formatTemplatePath(tpl.sourcePath) }));
}

async function downloadTemplatePath(sourcePath: string, outDir: string, ref: string, forceClean: boolean): Promise<void> {
Expand Down Expand Up @@ -282,12 +258,6 @@ async function applyLangOverlay(tpl: TemplateDef, outDir: string, ref: string, l

/** テンプレート種別固有の変換後、配布不要ファイルとworkspace依存を整理する。 */
async function postProcessTemplate(projectDir: string, tpl: TemplateDef, lang: Lang): Promise<void> {
if (tpl.kind === 'static-html') return;

if (tpl.kind === 'base-overlay' && tpl.rewritePackageName !== false) {
rewritePackageName(projectDir, tpl.slug);
}

if (tpl.kind === 'single-project-variant') {
extractVariantFiles(projectDir, tpl, lang);
rewritePackageName(projectDir, tpl.packageName ?? tpl.slug);
Expand Down Expand Up @@ -452,27 +422,15 @@ function escapeRegExp(s: string): string {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function printNextSteps(projectDir: string, tpl: TemplateDef): void {
function printNextSteps(projectDir: string): void {
logger.heading(t('create.nextSteps'));
const rel = path.relative(process.cwd(), projectDir) || '.';
logger.log(` cd ${rel}`);

if (tpl.kind === 'static-html') {
logger.log(t('create.nextStepsHtmlOpen'));
logger.log('');
return;
}

logger.log(' npm install # or pnpm install / yarn');
logger.log(' npm run dev');
logger.log('');
}

function getTemplateSourcePath(tpl: TemplateDef): string {
if (tpl.kind === 'base-overlay') return `${formatTemplatePath(tpl.basePath)} + ${formatTemplatePath(tpl.overlayPath)}`;
return formatTemplatePath(tpl.sourcePath);
}

function formatTemplatePath(sourcePath: string): string {
return `${TEMPLATES_PATH}/${sourcePath}`;
}
Expand Down
8 changes: 0 additions & 8 deletions packages/lism-cli/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,6 @@ export const messages = {
ja: 'テンプレート"{name}"の取得結果にpackage.jsonがありません。取得元パスを確認してください: {path}',
en: 'Downloaded template "{name}" does not contain package.json. Check the source path: {path}',
},
'create.templateIndexMissing': {
ja: 'テンプレート"{name}"の取得結果にindex.htmlがありません。取得元パスを確認してください: {path}',
en: 'Downloaded template "{name}" does not contain index.html. Check the source path: {path}',
},
'create.variantMissing': {
ja: 'variant "{variant}" のディレクトリが見つかりません。取得元パスを確認してください: {path}',
en: 'Variant "{variant}" directory not found. Check the source path: {path}',
Expand All @@ -217,10 +213,6 @@ export const messages = {
ja: '次のコマンドで開発を開始できます:',
en: 'Next steps:',
},
'create.nextStepsHtmlOpen': {
ja: ' index.htmlをブラウザで開いてください',
en: ' Open index.html in your browser',
},
'create.packageNameRewritten': {
ja: ' package.jsonのnameを"{name}"に変更しました。',
en: ' Updated package.json name to "{name}".',
Expand Down
17 changes: 4 additions & 13 deletions scripts/build-previews.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// に通して `.preview/merged/<dest>/` へ配置する。最後に全体 noindex の `_headers` を出力する。
//
// 配信パス設計(PR の決定事項どおり):
// - project / static-html : /{slug}/ (en overlay があれば /{slug}/en/ も)
// - project : /{slug}/ (en overlay があれば /{slug}/en/ も)
// - single-project-variant(lp/astro): /{prefix}/ (en は dist 同梱の /{prefix}/en/...)
// prefix = sourcePath.replace('/', '-')(lp/astro → lp-astro)
// - draft variant は merged から除外する
Expand Down Expand Up @@ -55,19 +55,15 @@ const CATEGORY_ORDER = [
];

// stack 表示ラベル(docs の stackLabels と揃える)
const STACK_LABELS = { astro: 'Astro', next: 'Next.js', vite: 'Vite + React', html: 'Static HTML' };
const STACK_LABELS = { astro: 'Astro', next: 'Next.js', vite: 'Vite + React' };

// テンプレ定義から ja / en の配信パスを導出(build-previews のパス設計と対)。
// 配信非対応 kind は null。
function previewPaths(t) {
if (t.kind === 'single-project-variant') {
const prefix = t.sourcePath.replace(/\//g, '-');
return { jaPath: `/${prefix}/${t.variant}/`, enPath: `/${prefix}/en/${t.variant}/` };
}
if ('sourcePath' in t) {
return { jaPath: `/${t.slug}/`, enPath: `/${t.slug}/en/` };
}
return null;
return { jaPath: `/${t.slug}/`, enPath: `/${t.slug}/en/` };
}

// 実際に merged へ出力された index.html の有無で掲載カードを集める(カテゴリ別)。
Expand All @@ -76,7 +72,6 @@ function collectLandingCards() {
for (const t of TEMPLATES) {
if (t.draft) continue;
const paths = previewPaths(t);
if (!paths) continue;
if (!existsSync(path.join(MERGED, paths.jaPath, 'index.html'))) continue; // 未配信は載せない
const enExists = existsSync(path.join(MERGED, paths.enPath, 'index.html'));
const card = { title: t.title.ja, stack: t.stack, jaPath: paths.jaPath, ...(enExists ? { enPath: paths.enPath } : {}) };
Expand Down Expand Up @@ -163,14 +158,10 @@ function writeLanding() {

const units = [];

// --- project / static-html: slug ごとに 1 ユニット(en overlay があれば en も) ---
// --- project: slug ごとに 1 ユニット(en overlay があれば en も) ---
for (const t of TEMPLATES) {
if (t.kind === 'single-project-variant') continue;
if (t.draft) continue; // 単独 draft は配信しない
if (!('sourcePath' in t)) {
console.warn(`⚠ skip ${t.slug}: kind="${t.kind}" は build-previews 未対応`);
continue;
}
if (t.stack !== 'astro' && t.stack !== 'vite') {
console.warn(`⚠ skip ${t.slug}: stack="${t.stack}" は build-previews 未対応`);
continue;
Expand Down
Loading
Loading