v0.1.31 로컬 실시간 확인 스크립트 추가
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# 배포 가이드
|
||||
|
||||
## 현재 버전
|
||||
- `v0.1.30`
|
||||
- `v0.1.31`
|
||||
|
||||
## Git 기본 설정
|
||||
- 저장소 작성자 정보는 아래 값으로 통일한다.
|
||||
@@ -45,6 +45,16 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## 로컬 실시간 확인
|
||||
```bash
|
||||
npm run dev:ghost:start
|
||||
npm run dev:watch
|
||||
```
|
||||
|
||||
- `npm run dev:ghost:start`는 로컬 Ghost 컨테이너를 시작한다.
|
||||
- `npm run dev:watch`는 초기 빌드와 sync를 한 번 수행한 뒤, Tailwind watch와 파일 변경 감지 기반 theme sync를 계속 유지한다.
|
||||
- 템플릿이나 자산을 저장한 뒤 브라우저 새로고침만으로 바로 반영 상태를 확인할 수 있다.
|
||||
|
||||
## 로컬 스타일 빌드
|
||||
```bash
|
||||
npm run build:alpine
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-04-14 v0.1.31
|
||||
zip 업로드 없이 로컬 Ghost에서 바로 확인하려면 자동 sync가 필요했지만, `fs.watch` 기반 재귀 watcher는 현재 환경에서 `EMFILE` 오류로 안정적으로 유지되지 않았다. 그래서 별도 의존성을 추가하지 않고, 제외 디렉터리를 뺀 파일 목록의 수정 시간을 주기적으로 비교하는 polling 방식으로 `dev:watch`를 구현했다. 이 방식은 다소 단순하지만 현재 저장소 크기에서는 충분히 가볍고, 사용자가 템플릿을 저장한 뒤 바로 브라우저 새로고침으로 확인할 수 있다는 점이 더 중요하다고 판단했다.
|
||||
|
||||
## 2026-04-14 v0.1.30
|
||||
`prose`는 Tailwind 기본 유틸리티가 아니라 `@tailwindcss/typography` 플러그인 영역인데, 현재 저장소에는 해당 플러그인이 들어있지 않았다. 이번 단계에서는 사용자가 이미 수정한 Tailwind 마크업을 최대한 유지하는 것이 우선이었기 때문에, 의존성을 새로 늘리기보다 `assets/styles/tailwind.css`의 `@layer components`에 원본 소스 기준 타이포그래피 규칙을 직접 옮겨 적용했다. 동시에 `bg-accent/10`처럼 현재 빌드에서 누락되던 유틸리티도 같은 방식으로 보강해 카드 태그 배경과 리스트 구분선이 다시 보이도록 정리했다.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 파일-화면 매핑 가이드
|
||||
|
||||
## 현재 버전
|
||||
- `v0.1.30`
|
||||
- `v0.1.31`
|
||||
|
||||
## 공통 레이아웃
|
||||
- [default.hbs](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/default.hbs): 전체 3열 셸과 공통 자산 로드
|
||||
@@ -34,5 +34,6 @@
|
||||
- [assets/built/theme.js](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/assets/built/theme.js): 인터랙션 스크립트
|
||||
- [assets/styles/tailwind.css](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/assets/styles/tailwind.css): Tailwind 입력 파일, `prose` 타이포그래피 규칙, accent/구분선 보조 유틸리티
|
||||
- [tailwind.config.js](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/tailwind.config.js): Tailwind 스캔 경로, 테마 설정, preflight 초기화 설정
|
||||
- [scripts/dev-watch.js](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/scripts/dev-watch.js): 로컬 파일 변경 감지 후 `dev:sync`와 Tailwind watch를 함께 실행하는 개발용 watcher
|
||||
- [routes.yaml.example](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/routes.yaml.example): Ghost 커스텀 라우트 예시
|
||||
- [package.json](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/package.json): Ghost 테마 메타데이터
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 기술 명세
|
||||
|
||||
## 현재 버전
|
||||
- `v0.1.30`
|
||||
- `v0.1.31`
|
||||
|
||||
## 테마 개요
|
||||
- Ghost `v5` 대응 커스텀 테마
|
||||
@@ -16,6 +16,7 @@
|
||||
- Tailwind CSS 빌드 결과물(`assets/built/tailwind.css`)을 기존 `screen.css`와 함께 로드
|
||||
- Tailwind 기본 초기화(`preflight`)를 활성화해 브라우저 기본 마진과 폼 스타일을 리셋
|
||||
- Alpine.js 로컬 자산(`assets/built/alpine.js`)을 전역 로드
|
||||
- `npm run dev:watch`는 초기 `dev:prepare` 실행 후 Tailwind `--watch`와 파일 변경 감지 기반 `dev:sync`를 함께 실행함
|
||||
- 좌측 카테고리 영역은 Alpine.js로 제어되며 `1024px` 이상에서 기본 열림, 미만에서 기본 닫힘
|
||||
- 좌측 네비게이션 마커와 카테고리 마커는 동일한 세로 바 → 원형 hover 패턴 사용
|
||||
- 전역 `ol`, `ul`, `menu` 기본 패딩과 리스트 스타일 리셋 적용
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# 업데이트 로그
|
||||
|
||||
## v0.1.31 - 2026-04-14
|
||||
- `npm run dev:watch` 스크립트 추가.
|
||||
- 로컬 파일 변경 감지 후 자동 `dev:sync` 반영 흐름 추가.
|
||||
- Tailwind `--watch`와 테마 sync를 함께 사용하는 로컬 확인 루프 정리.
|
||||
|
||||
## v0.1.30 - 2026-04-14
|
||||
- `prose` 본문 타이포그래피 스타일 추가.
|
||||
- `ul`, `ol`, `blockquote`, `code`, `table` 본문 표현 복구.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghost-theme-thred-clone",
|
||||
"version": "0.1.30",
|
||||
"version": "0.1.31",
|
||||
"private": true,
|
||||
"description": "A Ghost theme inspired by the Thred reference layout.",
|
||||
"keywords": [
|
||||
@@ -79,6 +79,7 @@
|
||||
"build:tailwind": "tailwindcss -c ./tailwind.config.js -i ./assets/styles/tailwind.css -o ./assets/built/tailwind.css --minify",
|
||||
"dev": "npm run dev:ghost:start",
|
||||
"dev:prepare": "npm run build:alpine && npm run build:tailwind && npm run dev:sync",
|
||||
"dev:watch": "node ./scripts/dev-watch.js",
|
||||
"dev:sync": "sh ./scripts/sync-theme.sh",
|
||||
"dev:seed": "node ./scripts/build-sample-content.js",
|
||||
"dev:seed:zip": "npm run dev:seed && cd seed && zip -q -r thred-inspired-sample-content.ghost.zip thred-inspired-sample-content.ghost.json",
|
||||
|
||||
211
scripts/dev-watch.js
Normal file
211
scripts/dev-watch.js
Normal file
@@ -0,0 +1,211 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { spawn } = require('child_process')
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..')
|
||||
const ignoredDirectories = new Set([
|
||||
'.git',
|
||||
'.cursor',
|
||||
'.vscode',
|
||||
'.docker',
|
||||
'docs',
|
||||
'local-ghost',
|
||||
'node_modules',
|
||||
'seed',
|
||||
'theme-export'
|
||||
])
|
||||
const ignoredFiles = new Set([
|
||||
'.DS_Store'
|
||||
])
|
||||
const syncExtensions = new Set([
|
||||
'.css',
|
||||
'.hbs',
|
||||
'.js',
|
||||
'.json',
|
||||
'.svg',
|
||||
'.yaml',
|
||||
'.yml'
|
||||
])
|
||||
const watchedRoots = [rootDir]
|
||||
|
||||
let syncTimer = null
|
||||
let syncProcess = null
|
||||
let scanTimer = null
|
||||
let previousSnapshot = new Map()
|
||||
|
||||
/**
|
||||
* @param {string} relativePath
|
||||
*/
|
||||
function shouldIgnorePath(relativePath) {
|
||||
if (!relativePath || relativePath === '.') {
|
||||
return false
|
||||
}
|
||||
|
||||
const segments = relativePath.split(path.sep)
|
||||
return segments.some((segment) => ignoredDirectories.has(segment) || ignoredFiles.has(segment))
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} relativePath
|
||||
*/
|
||||
function shouldSyncFile(relativePath) {
|
||||
if (shouldIgnorePath(relativePath)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (relativePath === path.join('assets', 'styles', 'tailwind.css')) {
|
||||
return false
|
||||
}
|
||||
|
||||
const extension = path.extname(relativePath)
|
||||
return syncExtensions.has(extension)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} command
|
||||
* @param {string[]} args
|
||||
* @param {(code: number | null) => void} [onExit]
|
||||
*/
|
||||
function runCommand(command, args, onExit) {
|
||||
const child = spawn(command, args, {
|
||||
cwd: rootDir,
|
||||
stdio: 'inherit',
|
||||
shell: false
|
||||
})
|
||||
|
||||
child.on('exit', (code) => {
|
||||
if (onExit) {
|
||||
onExit(code)
|
||||
}
|
||||
})
|
||||
|
||||
return child
|
||||
}
|
||||
|
||||
function queueSync() {
|
||||
if (syncTimer) {
|
||||
clearTimeout(syncTimer)
|
||||
}
|
||||
|
||||
syncTimer = setTimeout(() => {
|
||||
if (syncProcess) {
|
||||
queueSync()
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[dev:watch] theme sync')
|
||||
syncProcess = runCommand('npm', ['run', 'dev:sync'], (code) => {
|
||||
syncProcess = null
|
||||
|
||||
if (code !== 0) {
|
||||
console.error(`[dev:watch] sync failed with code ${code}`)
|
||||
}
|
||||
})
|
||||
}, 180)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} directoryPath
|
||||
* @param {Map<string, number>} snapshot
|
||||
*/
|
||||
function collectSnapshot(directoryPath, snapshot) {
|
||||
if (!fs.existsSync(directoryPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const entry of fs.readdirSync(directoryPath, { withFileTypes: true })) {
|
||||
const entryPath = path.join(directoryPath, entry.name)
|
||||
const relativePath = path.relative(rootDir, entryPath)
|
||||
|
||||
if (shouldIgnorePath(relativePath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
collectSnapshot(entryPath, snapshot)
|
||||
continue
|
||||
}
|
||||
|
||||
if (shouldSyncFile(relativePath)) {
|
||||
snapshot.set(relativePath, fs.statSync(entryPath).mtimeMs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildSnapshot() {
|
||||
const snapshot = new Map()
|
||||
|
||||
for (const watchedRoot of watchedRoots) {
|
||||
collectSnapshot(watchedRoot, snapshot)
|
||||
}
|
||||
|
||||
return snapshot
|
||||
}
|
||||
|
||||
function scanForChanges() {
|
||||
const nextSnapshot = buildSnapshot()
|
||||
|
||||
for (const [relativePath, modifiedTime] of nextSnapshot.entries()) {
|
||||
if (previousSnapshot.get(relativePath) !== modifiedTime) {
|
||||
console.log(`[dev:watch] change detected: ${relativePath}`)
|
||||
queueSync()
|
||||
previousSnapshot = nextSnapshot
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for (const relativePath of previousSnapshot.keys()) {
|
||||
if (!nextSnapshot.has(relativePath)) {
|
||||
console.log(`[dev:watch] change detected: ${relativePath}`)
|
||||
queueSync()
|
||||
previousSnapshot = nextSnapshot
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
previousSnapshot = nextSnapshot
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
previousSnapshot = buildSnapshot()
|
||||
scanTimer = setInterval(scanForChanges, 800)
|
||||
}
|
||||
|
||||
function shutdown() {
|
||||
if (syncTimer) {
|
||||
clearTimeout(syncTimer)
|
||||
}
|
||||
|
||||
if (tailwindProcess && !tailwindProcess.killed) {
|
||||
tailwindProcess.kill('SIGINT')
|
||||
}
|
||||
|
||||
if (syncProcess && !syncProcess.killed) {
|
||||
syncProcess.kill('SIGINT')
|
||||
}
|
||||
|
||||
if (scanTimer) {
|
||||
clearInterval(scanTimer)
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.log('[dev:watch] initial prepare')
|
||||
|
||||
runCommand('npm', ['run', 'dev:prepare'], (prepareCode) => {
|
||||
if (prepareCode !== 0) {
|
||||
process.exit(prepareCode || 1)
|
||||
}
|
||||
|
||||
console.log('[dev:watch] tailwind watch start')
|
||||
tailwindProcess = runCommand('npm', ['run', 'build:tailwind', '--', '--watch'])
|
||||
|
||||
console.log('[dev:watch] filesystem watch start')
|
||||
startPolling()
|
||||
})
|
||||
|
||||
let tailwindProcess = null
|
||||
|
||||
process.on('SIGINT', shutdown)
|
||||
process.on('SIGTERM', shutdown)
|
||||
Reference in New Issue
Block a user