feat(storybook): add one-click Windows launcher
This commit is contained in:
1 parent
d537926417
commit
c86d12bebc
3 files changed
+116
No files matched your search
@@ -16,6 +16,14 @@ Storybook은 기존 HM Component·Pattern·Theme·Variant·State·Interaction을
|
||||
|
||||
현재 검증된 테스트 배포 기준은 `HMDesignTest-Portable-Test-r3.zip`입니다. 사전 빌드된 Storybook과 Windows x64 포터블 Node를 포함하며 정확한 포함 범위는 r3의 BUILD-INFO·Inventory를 기준으로 확인합니다. `HMDesignTest-Portable-Test-r2.zip`과 `HMDesignTest-Portable-Test.zip`은 이전 스냅샷 기록으로 보존하며 현재 기준이 아닙니다. 포터블 ZIP의 기본 포트는 6007이고 충돌 시 자동으로 다른 포트를 사용합니다. 개발 저장소 Storybook의 6024와는 별개입니다. 테스트 사용자는 ZIP 해제 → `HMDesignTest/start-preview.bat` 더블클릭 → 표시된 로컬 URL 열기만 수행합니다. 별도 Node/npm 설치, npm 명령, Git 작업 또는 운영 업로드는 필요 없습니다. [실행 및 포함 범위](docs/guidelines/portable-test-distribution.md).
|
||||
|
||||
## Gitea 소스 다운로드 / Windows Storybook 실행
|
||||
|
||||
정식 저장소 `https://gitea.hmac.kr/b25027/HMDesign.git`를 Git Clone하거나 Gitea 소스 ZIP을 다운로드해 전체 압축을 풉니다. 이 방식은 포터블 ZIP과 달리 PC에 Node.js/npm이 필요합니다. 현재 lockfile의 요구 조건은 **Node.js 20.x의 20.19 이상 또는 22.12 이상**이며 Node.js 21.x는 지원하지 않습니다.
|
||||
|
||||
프로젝트 루트의 `start-storybook.bat`을 더블클릭하세요. 의존성이 없으면 `npm ci`를 자동 실행하고, workspace 패키지 산출물이 없으면 기존 `npm run build:packages`를 실행합니다. 이후 기존 `npm run storybook`으로 서버를 시작하며, HMDesign Storybook의 Story index와 preview HTTP 응답이 준비된 뒤 기본 브라우저에서 **http://localhost:6024/** 를 엽니다. 이미 준비된 HMDesign Storybook이 있으면 서버를 추가 실행하지 않고 기존 주소를 엽니다. 서버 실행 창의 `Ctrl+C`로 종료합니다.
|
||||
|
||||
실행 파일 위치를 기준으로 동작하므로 한글·공백 폴더, 다른 드라이브 및 Git이 없는 ZIP 환경에서도 사용할 수 있습니다. 최초 설치에는 npm 레지스트리 접속이 필요합니다. 포트 6024를 다른 프로그램이 사용하거나 아직 준비되지 않은 서버가 점유하면 포트를 바꾸지 않고 오류를 표시합니다. 해당 프로그램을 종료하거나 기존 서버 준비를 기다린 뒤 다시 실행하세요. 오류가 발생한 경우에만 창을 유지합니다. 출력된 오류, Node.js/npm의 PATH와 버전, 전체 소스 압축 해제 여부 및 네트워크를 확인하세요. 의존성이 불완전하면 프로젝트 루트에서 `npm ci`를 직접 실행하고 다시 시도합니다.
|
||||
|
||||
## 오전 소스 ZIP / 개발자 전용 절차 (AI 참조용)
|
||||
|
||||
오전 소스 배포의 역사적 기준은 `HMDesignTest.zip`입니다. 당시 전달 구성에서는 `.git` 디렉터리와 Git 이력·원격 연결 정보를 제외하고 `.gitignore`와 소스·Storybook·문서·패키지 설정을 유지했습니다. 이 절의 수량·실행 조건은 오전 ZIP에 대한 기록이며 현재 포터블 r3의 포함 범위는 위 절과 BUILD-INFO를 기준으로 확인합니다. `HMDesignSystem-test-2026-10-08-r4.zip`, `HMDesignSystem-Distribution-Test-v2.zip`, `HMDesignSystem-AI-Reference-Test.zip`은 오전 기준을 대체하지 않습니다.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Git source launcher. Uses the existing npm scripts; no portable runtime.
|
||||
import { spawn } from 'node:child_process';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import net from 'node:net';
|
||||
|
||||
process.chdir(fileURLToPath(new URL('../', import.meta.url)));
|
||||
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
|
||||
const portMatch = pkg.scripts?.storybook?.match(/(?:-p|--port)\s+(\d+)/);
|
||||
if (!portMatch) throw new Error('Cannot determine the port from package.json scripts.storybook.');
|
||||
const port = Number(portMatch[1]);
|
||||
const url = `http://localhost:${port}/`;
|
||||
const [major, minor] = process.versions.node.split('.').map(Number);
|
||||
if (!((major === 20 && minor >= 19) || (major === 22 && minor >= 12) || major > 22)) {
|
||||
throw new Error('Node.js 20.19+ (20.x) or 22.12+ is required by the lockfile.');
|
||||
}
|
||||
|
||||
function run(args, options = {}) {
|
||||
return spawn(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', args], {
|
||||
stdio: 'inherit', windowsHide: true, ...options,
|
||||
});
|
||||
}
|
||||
function completed(child) {
|
||||
return new Promise((resolve, reject) => {
|
||||
child.once('error', reject);
|
||||
child.once('exit', (code) => code === 0 ? resolve() : reject(new Error(`Command failed with exit code ${code}. See output above.`)));
|
||||
});
|
||||
}
|
||||
async function ready() {
|
||||
try {
|
||||
const response = await fetch(`${url}index.json`, { signal: AbortSignal.timeout(1500) });
|
||||
if (!response.ok) return false;
|
||||
const index = await response.json();
|
||||
if (!index.entries?.['components-form-button--all'] || !index.entries?.['theme-designer-playground--editor']) return false;
|
||||
return (await fetch(`${url}iframe.html`, { signal: AbortSignal.timeout(1500) })).ok;
|
||||
} catch { return false; }
|
||||
}
|
||||
function occupied() {
|
||||
return new Promise(resolve => {
|
||||
const socket = net.connect({ host: '127.0.0.1', port });
|
||||
socket.setTimeout(1500);
|
||||
socket.once('connect', () => { socket.destroy(); resolve(true); });
|
||||
socket.once('error', () => { socket.destroy(); resolve(false); });
|
||||
socket.once('timeout', () => { socket.destroy(); resolve(true); });
|
||||
});
|
||||
}
|
||||
async function openBrowser() {
|
||||
await completed(spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `Start-Process '${url}'`], {
|
||||
stdio: 'inherit', windowsHide: true,
|
||||
}));
|
||||
}
|
||||
if (await ready()) {
|
||||
await openBrowser();
|
||||
} else {
|
||||
if (await occupied()) throw new Error(`Port ${port} is occupied by a server that is not a ready HMDesign Storybook. Stop it and retry.`);
|
||||
await completed(run('npm.cmd --version', { stdio: 'ignore' }));
|
||||
if (!existsSync('node_modules/.bin/storybook.cmd')) await completed(run('npm.cmd ci'));
|
||||
const missingDist = ['packages/ui', 'packages/center-graphic'].some(dir => {
|
||||
const workspace = JSON.parse(readFileSync(`${dir}/package.json`, 'utf8'));
|
||||
return Object.values(workspace.exports).some(value => {
|
||||
const entry = typeof value === 'string' ? value : value.import;
|
||||
return entry && !existsSync(`${dir}/${entry}`);
|
||||
});
|
||||
});
|
||||
if (missingDist) await completed(run('npm.cmd run build:packages'));
|
||||
// Exact port prevents interactive port selection, including a startup race.
|
||||
const server = run('npm.cmd run storybook -- --exact-port --ci');
|
||||
let stopped = false;
|
||||
const exit = completed(server).finally(() => { stopped = true; });
|
||||
// Attach a handler immediately while readiness polling is in progress.
|
||||
exit.catch(() => {});
|
||||
const stop = () => completed(run(`taskkill /pid ${server.pid} /t /f`, { stdio: 'ignore' })).catch(() => {});
|
||||
process.once('SIGINT', async () => { await stop(); process.exit(0); });
|
||||
process.once('SIGTERM', async () => { await stop(); process.exit(0); });
|
||||
try {
|
||||
const deadline = Date.now() + 180000;
|
||||
while (!stopped && Date.now() < deadline) {
|
||||
if (await ready()) { await openBrowser(); await exit; break; }
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
if (stopped) await exit;
|
||||
else throw new Error('Storybook readiness timed out after 3 minutes.');
|
||||
} catch (error) { await stop(); throw error; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
@echo off
|
||||
setlocal EnableExtensions DisableDelayedExpansion
|
||||
chcp 65001 >nul
|
||||
title HMDesign - Storybook
|
||||
pushd "%~dp0"
|
||||
if errorlevel 1 goto root_error
|
||||
node scripts\start-storybook.mjs
|
||||
if errorlevel 1 goto fail
|
||||
popd
|
||||
endlocal
|
||||
exit /b 0
|
||||
:fail
|
||||
echo.
|
||||
echo ERROR: Storybook could not start. Review the error above.
|
||||
echo Check Node.js/npm, dependency installation, and port availability.
|
||||
popd
|
||||
pause
|
||||
endlocal
|
||||
exit /b 1
|
||||
:root_error
|
||||
echo ERROR: Cannot access the launcher folder.
|
||||
pause
|
||||
endlocal
|
||||
exit /b 1
|
||||
Reference in new issue
Block a user