git(['ls-files', '*.map']); $this->assertSame( [], $tracked, "소스맵이 git 에 추적되고 있습니다. 원본 코드가 배포판에 노출됩니다:\n ". implode("\n ", $tracked) ); } /** * .gitignore 가 소스맵을 실제로 무시하는지 테스트 * * 규칙이 존재해도 다른 규칙에 의해 무력화될 수 있으므로 git 에 직접 판정시킨다. */ public function test_gitignore_actually_ignores_sourcemap_files(): void { $probe = 'public/build/core/__sourcemap_guard_probe__.js.map'; $result = $this->git(['check-ignore', $probe]); $this->assertContains( $probe, $result, '.gitignore 가 .map 을 무시하지 않습니다. 빌드 산출 소스맵이 저장소에 유입될 수 있습니다.' ); } /** * 추적 중인 빌드 산출 JS 에 sourceMappingURL 주석이 없는지 테스트 * * 주석만 남고 .map 이 없으면 브라우저가 404 를 일으키고, 주석과 .map 이 함께 * 있으면 원본이 노출된다. 배포 산출물에는 둘 다 없어야 한다. * * 판정 대상은 워킹트리(= 지금 배포하면 나갈 상태)다. HEAD(직전 커밋)를 보면 * 워킹트리에서 이미 고친 내용을 반영하지 못한다. */ public function test_tracked_build_output_has_no_source_mapping_url(): void { $offenders = $this->git([ 'grep', '--files-with-matches', 'sourceMappingURL', '--', 'public/build/core/*.js', 'modules/_bundled/*/dist/js/*.js', 'plugins/_bundled/*/dist/js/*.js', 'templates/_bundled/*/dist/js/*.js', ]); $this->assertSame( [], $offenders, "배포 산출 JS 에 소스맵 참조 주석이 남아 있습니다(--production 재빌드 필요):\n ". implode("\n ", $offenders) ); } /** * 공개 docroot(`public/build`)에 소스맵 산출물이 남아 있지 않은지 테스트 * * 확장 에셋과 달리 `public/` 은 웹서버가 직접 서빙하는 docroot 라, 확장자 * 화이트리스트(AllowedModuleFileType 등)를 전혀 거치지 않는다. 즉 여기 남은 * `.map` 은 URL 만 알면 그대로 내려받을 수 있고 `sourcesContent` 에 원본 * 전문이 담겨 있다. * * `--production` 빌드는 `.map` 을 생성하지 않지만 **기존 파일을 지우지도 않는다**. * 따라서 한 번이라도 dev 빌드를 돌린 트리에는 stale 한 소스맵이 그대로 남는다 * (실측: `GET /build/core/template-engine.min.js.map` → 200 + 원본 노출). */ public function test_public_build_dir_has_no_sourcemap_artifacts(): void { $offenders = []; foreach (glob(base_path('public/build/**/*.map')) ?: [] as $path) { $offenders[] = str_replace(base_path().DIRECTORY_SEPARATOR, '', $path); } foreach (glob(base_path('public/build/*.map')) ?: [] as $path) { $offenders[] = str_replace(base_path().DIRECTORY_SEPARATOR, '', $path); } $this->assertSame( [], $offenders, "공개 docroot 에 소스맵이 남아 있습니다. URL 로 직접 내려받아 원본 코드가 노출됩니다.\n". "`--production` 재빌드는 기존 .map 을 지우지 않으므로 직접 삭제해야 합니다:\n ". implode("\n ", $offenders) ); } /** * 활성(서빙) 확장 디렉토리에 소스맵 산출물이 남아 있지 않은지 테스트 * * 위 세 테스트는 모두 git 추적 파일만 본다. 그러나 웹서버가 실제로 서빙하는 것은 * 활성 디렉토리(`templates/{id}`, `modules/{id}`, `plugins/{id}`)이고, 이들은 * .gitignore 대상이라 추적 기반 검사에 전혀 걸리지 않는다. * * 실제로 `_bundled` 를 --production 으로 재빌드한 뒤에도 `{type}:update` 를 돌리지 * 않은 확장은 활성 디렉토리에 예전 .map 과 sourceMappingURL 주석을 그대로 안고 있었다 * (브라우저 실측으로 발견). 서빙 계층이 .map 을 거부하므로 원본 유출로 이어지지는 * 않지만, 배포본 위생상 남아 있어서는 안 된다. * * `_bundled` 원본이 없는 활성 디렉토리는 배포본에 실리지 않는 로컬 잔재이므로 제외한다. */ public function test_active_extension_dirs_have_no_sourcemap_artifacts(): void { $offenders = []; foreach (['templates', 'modules', 'plugins'] as $type) { foreach (glob(base_path("{$type}/*/dist/js/*.map")) ?: [] as $path) { $identifier = basename(dirname($path, 3)); if ($this->isReservedExtensionDir($identifier) || ! $this->isBundledExtension($type, $identifier)) { continue; } $offenders[] = str_replace(base_path().DIRECTORY_SEPARATOR, '', $path); } } $this->assertSame( [], $offenders, '활성(서빙) 확장 디렉토리에 소스맵이 남아 있습니다. 해당 확장을 --production 으로 '. "재빌드한 뒤 `{type}:update {id} --force` 를 실행하세요:\n ". implode("\n ", $offenders) ); } /** * `_bundled` / `_pending` 등 활성 확장이 아닌 예약 디렉토리인지 판정합니다. * * @param string $identifier 디렉토리명 * @return bool 예약 디렉토리 여부 */ private function isReservedExtensionDir(string $identifier): bool { return str_starts_with($identifier, '_'); } /** * 해당 확장이 배포본에 실리는 번들 확장인지 판정합니다. * * 판정 기준은 `_bundled` 원본의 존재다. DB 레코드로 판정하면 안 된다 — 테스트 DB 에는 * 확장 레코드가 없어(0건) 모든 파일이 건너뛰어지고 테스트가 영구 green 이 된다 * (실제로 이 함정에 빠졌다: .map 을 심어도 통과했다). * * `_bundled` 에 없는 활성 디렉토리는 과거 설치의 로컬 잔재이며 gitignore 대상이라 * 공개 배포본에 포함되지 않으므로 판정에서 제외한다. * * @param string $type 확장 타입 (templates|modules|plugins) * @param string $identifier 확장 식별자 * @return bool 번들 확장 여부 */ private function isBundledExtension(string $type, string $identifier): bool { return is_dir(base_path("{$type}/_bundled/{$identifier}")); } /** * git 명령을 실행하고 출력 라인 배열을 반환합니다. * * @param array $args git 인자 * @return array 출력 라인 (빈 줄 제외) */ private function git(array $args): array { $process = new Process(array_merge(['git'], $args), base_path()); $process->run(); $lines = preg_split('/\R/', trim($process->getOutput())) ?: []; return array_values(array_filter($lines, fn (string $line): bool => $line !== '')); } }