미리보기 편집기는 설정을 시험해 보는 자리인데, 본문 동기화가 hasChanges 를 렌더를 일으키는 setLocal 로 올리면서 시험 입력만으로 [저장] 이 켜졌다. 어느 편집기가 저장 대상인지는 레이아웃만 알므로 핸들러가 필드명을 알아보게 하지 않고 trackChanges 선언으로 받는다 — 하드코딩하면 다른 확장이 같은 미리보기 패턴을 쓸 때 그대로 재발한다. 같은 결함이 형제 경로에도 있었다. 평문 폴백은 hasChanges 를 본문 배치 (render:false + selfManaged:true)에 실어, 편집기 자산을 못 불러온 환경에서 본문만 고치면 저장 버튼이 잠긴 채로 남았다 — 폐쇄망에서는 그 폴백이 정상 경로다. 폴백은 렌더 시점에도 같은 함수를 부르므로 화면을 여는 것만으로 변경됨이 되지 않도록, 사용자 입력에서 온 호출만 플래그를 올린다. components.json 생성기는 종결 개행을 PHP_EOL 로 써서 Windows 빌드마다 그 파일을 유령 변경으로 남겼다. 줄 내용이 같아 diff 가 비어 보이므로 무엇이 바뀐 것인지 알 수 없는 변경만 쌓인다.
172 lines
7.2 KiB
PHP
172 lines
7.2 KiB
PHP
<?php
|
|
|
|
namespace App\Extension\Traits;
|
|
|
|
/**
|
|
* 확장(모듈/플러그인) 컴포넌트 매니페스트(components.json) 생성 트레이트
|
|
*
|
|
* `module:build` / `plugin:build` 의 빌드 성공 직후 호출되어, 그 확장의 컴포넌트
|
|
* 소스를 스캔해 `{ identifier, version, components: { basic[], composite[], layout[] } }`
|
|
* 매니페스트를 빌드 경로 루트에 작성한다.
|
|
*
|
|
* 편집 모드 부팅 시 코어 `ComponentRegistry` 가 활성 확장의 components.json 을
|
|
* 네임스페이스 병합해 자동 컨트롤 생성(props 메타)의 입력으로 쓴다. 본 매니페스트가
|
|
* 없으면 그 확장 컴포넌트는 메타데이터 없이 무손실 보존만 된다(원칙 4.6 디그레이드).
|
|
*
|
|
* 스캔 규칙:
|
|
* - 컴포넌트 소스는 확장의 `resources/js/components/{basic,composite,layout}/` 하위
|
|
* `*.tsx` / `*.ts` 파일로 본다. 하위 분류 디렉토리 이름이 type(basic/composite/layout).
|
|
* - 분류 디렉토리 밖의 컴포넌트는 `composite` 로 분류(보수적 기본값).
|
|
* - 컴포넌트 name = 파일명(확장자 제외). props 정밀 추출은 본 S6-1 범위 밖 —
|
|
* `props: {}` 빈 메타로 둔다(코어 ComponentRegistry 가 런타임 props 메타로 보강).
|
|
* - 컴포넌트 소스 디렉토리가 없으면 빈 매니페스트를 작성한다(핸들러 전용 확장 등).
|
|
*
|
|
* 본 트레이트는 도메인-특화 컴포넌트 구조를 가정하지 않는다 — 디렉토리 컨벤션만으로
|
|
* 분류하고, 컨벤션 밖 구조는 composite 로 보존한다.
|
|
*
|
|
* @since engine-v1.54.0
|
|
*/
|
|
trait GeneratesComponentManifest
|
|
{
|
|
/**
|
|
* 확장 빌드 경로의 컴포넌트 소스를 스캔해 components.json 을 작성합니다.
|
|
*
|
|
* @param string $buildPath 빌드 경로 (확장 루트 — _bundled 또는 활성)
|
|
* @param string $identifier 확장 식별자 (vendor-extension 형식)
|
|
* @return array{written: bool, count: int, path: string} 작성 결과
|
|
*/
|
|
protected function generateComponentManifest(string $buildPath, string $identifier): array
|
|
{
|
|
$version = $this->readManifestVersion($buildPath);
|
|
$components = $this->scanComponentSources($buildPath);
|
|
|
|
$manifest = [
|
|
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
|
|
'identifier' => $identifier,
|
|
'version' => $version,
|
|
'components' => $components,
|
|
];
|
|
|
|
$outputPath = $buildPath.'/components.json';
|
|
$json = json_encode(
|
|
$manifest,
|
|
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
|
|
);
|
|
|
|
// 종결 개행은 "\n" 고정 — PHP_EOL 은 Windows 에서 "\r\n" 이라, 같은 소스를 빌드해도
|
|
// 빌드한 OS 에 따라 산출물의 마지막 바이트가 달라진다. components.json 은 Git 추적
|
|
// 대상이므로 그 차이가 매 빌드마다 변경으로 잡히는데, 줄 내용이 같아 diff 는 비어
|
|
// 보인다 — 무엇이 바뀐 것인지 알 수 없는 변경만 남는다.
|
|
$written = $json !== false && file_put_contents($outputPath, $json."\n") !== false;
|
|
|
|
$count = count($components['basic']) + count($components['composite']) + count($components['layout']);
|
|
|
|
return ['written' => $written, 'count' => $count, 'path' => $outputPath];
|
|
}
|
|
|
|
/**
|
|
* 확장 매니페스트(module.json/plugin.json)에서 version 을 읽습니다.
|
|
*
|
|
* @param string $buildPath 빌드 경로
|
|
* @return string version 문자열 (미확인 시 '0.0.0')
|
|
*/
|
|
private function readManifestVersion(string $buildPath): string
|
|
{
|
|
foreach (['module.json', 'plugin.json'] as $file) {
|
|
$path = $buildPath.'/'.$file;
|
|
if (file_exists($path)) {
|
|
$decoded = json_decode((string) file_get_contents($path), true);
|
|
if (is_array($decoded) && isset($decoded['version']) && is_string($decoded['version'])) {
|
|
return $decoded['version'];
|
|
}
|
|
}
|
|
}
|
|
|
|
return '0.0.0';
|
|
}
|
|
|
|
/**
|
|
* 컴포넌트 소스 디렉토리를 스캔해 type 별 컴포넌트 목록을 구성합니다.
|
|
*
|
|
* @param string $buildPath 빌드 경로
|
|
* @return array{basic: list<array<string, mixed>>, composite: list<array<string, mixed>>, layout: list<array<string, mixed>>}
|
|
*/
|
|
private function scanComponentSources(string $buildPath): array
|
|
{
|
|
$result = ['basic' => [], 'composite' => [], 'layout' => []];
|
|
|
|
$componentsRoot = $buildPath.'/resources/js/components';
|
|
if (! is_dir($componentsRoot)) {
|
|
return $result;
|
|
}
|
|
|
|
$iterator = new \RecursiveIteratorIterator(
|
|
new \RecursiveDirectoryIterator($componentsRoot, \FilesystemIterator::SKIP_DOTS)
|
|
);
|
|
|
|
foreach ($iterator as $file) {
|
|
/** @var \SplFileInfo $file */
|
|
if (! $file->isFile()) {
|
|
continue;
|
|
}
|
|
$ext = strtolower($file->getExtension());
|
|
if (! in_array($ext, ['ts', 'tsx'], true)) {
|
|
continue;
|
|
}
|
|
$name = $file->getBasename('.'.$file->getExtension());
|
|
// index/엔트리 파일·테스트 파일은 컴포넌트로 보지 않는다
|
|
if (in_array(strtolower($name), ['index', 'main', 'entry'], true)) {
|
|
continue;
|
|
}
|
|
if (str_contains(strtolower($file->getFilename()), '.test.')
|
|
|| str_contains($file->getPathname(), DIRECTORY_SEPARATOR.'__tests__'.DIRECTORY_SEPARATOR)) {
|
|
continue;
|
|
}
|
|
|
|
$type = $this->classifyComponentByPath($componentsRoot, $file->getPathname());
|
|
$relativePath = ltrim(
|
|
str_replace(
|
|
[base_path().DIRECTORY_SEPARATOR, '\\'],
|
|
['', '/'],
|
|
$file->getPathname()
|
|
),
|
|
'/'
|
|
);
|
|
|
|
$result[$type][] = [
|
|
'name' => $name,
|
|
'type' => $type,
|
|
'path' => $relativePath,
|
|
'props' => new \stdClass,
|
|
];
|
|
}
|
|
|
|
// 결정적 순서 — name 기준 정렬 (빌드 재현성)
|
|
foreach ($result as $type => $list) {
|
|
usort($result[$type], fn ($a, $b) => strcmp($a['name'], $b['name']));
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 파일 경로의 분류 디렉토리(basic/composite/layout)로 컴포넌트 type 을 판정합니다.
|
|
*
|
|
* @param string $componentsRoot components 디렉토리 절대 경로
|
|
* @param string $filePath 컴포넌트 파일 절대 경로
|
|
* @return 'basic'|'composite'|'layout' 컴포넌트 type (컨벤션 밖이면 composite)
|
|
*/
|
|
private function classifyComponentByPath(string $componentsRoot, string $filePath): string
|
|
{
|
|
$relative = str_replace('\\', '/', substr($filePath, strlen($componentsRoot)));
|
|
$segments = array_values(array_filter(explode('/', $relative)));
|
|
$firstDir = $segments[0] ?? '';
|
|
|
|
return match (strtolower($firstDir)) {
|
|
'basic' => 'basic',
|
|
'layout' => 'layout',
|
|
default => 'composite',
|
|
};
|
|
}
|
|
}
|