Files
Gnuboard7/app/Support/StaticExtensionPattern.php
HeuJung 482faf00be chore(release): 7.0.10 출시일 정렬 + 공개 배포본 누출 정정
7.0.9 페이로드 대비 버전이 오른 패키지 31종과 코어의 CHANGELOG 최상단 절
날짜를 2026-09-04 로 맞췄다.

이어서 릴리즈 스크립트의 게이트를 브랜치 생성 없이 전수 실행한 결과, 파일
본문 누출 검사가 error 25건으로 릴리즈를 막고 있었다. 전부 이번 사이클에
들어온 것이다.

- 확장 문서·코어 주석 20건이 공개 배포본에 없는 를 가리켰다.
 공개되는 AGENTS.md 참조로 바꾸고, 참조된 9개 절의 실재를 전수 확인했다.
 유일하게 빠져 있던 「독립 레이아웃의 글로벌 호스트 컴포넌트」절은 원래
 동기화 대상이었으므로 AGENTS.md 에 반영했다.
- 시나리오 매니페스트의 내부 문서 포인터는 소비자가 하나도 없어 제거했다.
- 공개 테스트 3건이 내부 검사 스크립트를 열어 도구 간 리터럴을 대조하고
 있었다. tests/** 는 배포 페이로드에 포함되고 는 제외되므로
 배포본에서 원리상 성립할 수 없는 검사다. 대조를 그 도구 옆으로 옮기고
 공개 테스트에는 자기 동작 단언만 남겼다. 존재 가드로 무마하지 않은 것은
 그 경우 배포본에서 검사가 조용히 건너뛰어져 "검사하지 않음" 과 "통과" 가
 구분되지 않기 때문이다.

옮긴 대조는 돌지 않으면 잠그지 못하므로 대상 파일 편집 시 자동 실행되도록
등록했다. 누출 검사 자체도 릴리즈 시점에만 돌아 위반이 다음 릴리즈까지
드러나지 않았으므로, 룰을 복제하지 않고 실행 시점만 세션 종료로 앞당겼다.

같은 검증 중 드러난 선행 결함 하나(이커머스 문서의 집계 블록 드리프트)도
함께 정리했다.
2026-09-04 12:18:22 +09:00

65 lines
3.2 KiB
PHP

<?php
namespace App\Support;
use App\Rules\AllowedModuleFileType;
use App\Rules\AllowedPluginFileType;
use App\Rules\AllowedTemplateFileType;
/**
* SPA catch-all 라우트의 정적 확장자 제외 패턴 (#122).
*
* catch-all 은 등록되지 않은 경로에 SPA 셸 HTML 을 돌려준다. 그런데 **정적 자산 요청**이
* 거기 걸리면 존재하지 않는 `.mjs` 가 `Content-Type: text/html` 인 200 을 받고, 브라우저는
* 그것을 스크립트로 파싱하다 죽는다. `onerror` 는 발화하지 않으므로(응답은 성공이다)
* 태그 복구기도 뜨지 않는다 — 예외도 404 도 없이 화면만 조용히 비는 결함이다.
*
* 제외 목록을 라우트 파일에 손으로 적어 두면 에셋 서빙이 허용하는 확장자와 갈라진다.
* 실제로 갈라져 있었다 — `mjs` · `webp` · `otf` 세 가지가 서빙은 되는데 제외 목록에는
* 없었다. `.json` 이 무사했던 것은 lookahead 에 끝 앵커가 없어 `.js` 에 **부분일치**한
* 우연이었을 뿐이다. 그래서 목록을 손으로 두지 않고 에셋 서빙 화이트리스트에서 **파생**시켜
* 우연을 제거한다.
*
* 모집단은 세 화이트리스트의 **합집합**이다. 게시 트리(`public/build/ext/{v}/`)에는
* 템플릿 dist 뿐 아니라 모듈·플러그인의 사용자 추가 에셋도 함께 실리므로, 한 종류만
* 기준으로 삼으면 나머지가 서빙하는 확장자가 다시 빠진다(`ico` 가 그 경우다 — 모듈·
* 플러그인은 허용하고 템플릿은 허용하지 않는다).
*
* 라우트 캐시 안전: `where()` 인자는 **정의 시점**에 평가되어 컴파일된 정규식으로 캐시에
* 박히므로, 캐시 로드 경로는 이 클래스를 참조하지 않는다. 라우트 파일에 전역 함수를
* 선언하지도 않는다 (AGENTS.md "라우트 캐시 안전성").
*
* 환경 의존 게터(`getAllowedExtensions()`)를 쓰지 않는 이유도 같다 — 그것은 로컬에서
* 소스맵(`map`)을 덧붙이므로, 패턴이 **캐시를 구운 환경**에 따라 달라진다.
*/
final class StaticExtensionPattern
{
/**
* SPA catch-all `where()` 에 넣을 제외 lookahead 를 반환합니다.
*
* 끝 앵커(`$`)를 붙이지 않는다 — 쿼리스트링이 아닌 경로 뒤 세그먼트가 붙은 형태
* (`/a.js/b`)까지 함께 제외하는 종전 동작을 유지하기 위함이며, 앵커를 붙이면
* 정적 미스가 SPA 풀 렌더를 경유해 조용히 비싸진다.
*
* @return string `(?!.*\.(js|css|…)).*` 형태의 라우트 패턴 조각
*/
public static function catchAllExclusion(): string
{
return '(?!.*\.('.implode('|', self::servedExtensions()).')).*';
}
/**
* 정적으로 서빙될 수 있는 확장자 전체(세 화이트리스트 합집합)를 반환합니다.
*
* @return array<int, string> 중복 없는 확장자 목록
*/
public static function servedExtensions(): array
{
return array_values(array_unique([
...AllowedTemplateFileType::allowedExtensions(),
...AllowedModuleFileType::allowedExtensions(),
...AllowedPluginFileType::allowedExtensions(),
]));
}
}