@gaonjs/cli 0.31.1 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/build.d.ts +11 -0
- package/dist/commands/build.js +49 -0
- package/dist/commands/check.js +41 -0
- package/dist/commands/db.d.ts +5 -1
- package/dist/commands/db.js +92 -36
- package/dist/db/resolve.d.ts +4 -6
- package/dist/db/resolve.js +26 -13
- package/dist/dev/build.d.ts +34 -0
- package/dist/dev/build.js +144 -0
- package/dist/dev/frontend-build.d.ts +3 -3
- package/dist/dev/frontend-build.js +42 -37
- package/dist/doctor/async-offload.d.ts +3 -1
- package/dist/doctor/async-offload.js +5 -2
- package/dist/doctor/auth-wiring.d.ts +4 -2
- package/dist/doctor/auth-wiring.js +13 -6
- package/dist/doctor/connections.d.ts +23 -1
- package/dist/doctor/connections.js +85 -14
- package/dist/doctor/csrf-wiring.d.ts +3 -1
- package/dist/doctor/csrf-wiring.js +9 -5
- package/dist/doctor/schema-relations.d.ts +7 -0
- package/dist/doctor/schema-relations.js +114 -0
- package/dist/doctor/seal-security.js +12 -6
- package/dist/doctor/source-scan.d.ts +11 -0
- package/dist/doctor/source-scan.js +70 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor/types.js +1 -1
- package/dist/doctor.d.ts +2 -1
- package/dist/doctor.js +8 -2
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +55 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.js +22 -4
- package/dist/scaffold/app.js +2 -1
- package/dist/templates/auth/Dashboard.vue.tpl +2 -2
- package/dist/templates/auth/Login.vue.tpl +2 -2
- package/dist/templates/auth/Signup.vue.tpl +2 -2
- package/dist/templates/auth/app.config.ts.tpl +3 -3
- package/dist/templates/auth/dashboard.controller.ts.tpl +1 -1
- package/dist/templates/auth/registration.controller.ts.tpl +3 -3
- package/dist/templates/auth/session.controller.ts.tpl +5 -5
- package/dist/templates/project/AGENTS.md.tpl +12 -6
- package/dist/templates/project/agents/async.md.tpl +26 -5
- package/dist/templates/project/agents/data.md.tpl +56 -12
- package/dist/templates/project/agents/security.md.tpl +16 -2
- package/dist/templates/project/agents/storage.md.tpl +110 -0
- package/dist/templates/project/agents/testing.md.tpl +24 -5
- package/dist/templates/project/docker-compose.yaml.tpl +7 -5
- package/dist/templates/project/gaon.config.ts.tpl +4 -0
- package/dist/templates/project/package.json.tpl +1 -1
- package/dist/templates/project/test/setup.ts.tpl +10 -7
- package/dist/templates/project/vite.config.ts.tpl +6 -5
- package/dist/templates/project/vitest.config.ts.tpl +5 -0
- package/package.json +7 -7
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @gaonjs/cli · dev/frontend-build — 프론트 번들 watch 빌드 (결정 67 ·
|
|
2
|
+
* @gaonjs/cli · dev/frontend-build — 멀티 앱 프론트 번들 watch 빌드 (결정 67 · 결정 146)
|
|
3
3
|
*
|
|
4
4
|
* `gaon dev` 는 서버(serve)를 재시작 방식으로 띄운다(§dev.ts). 프론트 번들은
|
|
5
5
|
* serve 가 dist/<앱>/index.html 을 매 요청 읽어 문서 셸에 이어 붙이므로
|
|
@@ -7,50 +7,55 @@
|
|
|
7
7
|
* watch 모드로 함께 돌린다. 진짜 HMR(모듈 교체)은 v1 어댑터 범위 밖이라(§6.4)
|
|
8
8
|
* "저장 → 재빌드 → 새로고침" 루프를 제공한다.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* 결정 146(12차 W2): apps/* 를 순회해 **앱마다** watch 빌드한다(root=apps/<앱>·
|
|
11
|
+
* outDir=dist/<앱>·base=/<앱>/). 예전엔 apps/web 하나만 감시해 둘째 앱(admin)의
|
|
12
|
+
* dist 가 dev 에서 갱신되지 않았다. 프로그램적 build(configFile 명시)로 vue 플러그인
|
|
13
|
+
* 누락 함정을 피한다.
|
|
14
|
+
*
|
|
15
|
+
* fail-open: 프론트 진입(index.html)·vite 가 없으면 조용히 건너뛴다(프론트 없는
|
|
16
|
+
* API 전용 프로젝트도 gaon dev 가 동작해야 한다). vite 실패는 통합 콘솔에 알리되
|
|
17
|
+
* dev 전체를 막지 않는다.
|
|
13
18
|
*/
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { join, resolve } from 'node:path';
|
|
17
|
-
/** 프로젝트에 프론트 진입 HTML(apps/web/index.html)이 있는가. */
|
|
18
|
-
function hasFrontend(cwd) {
|
|
19
|
-
return existsSync(join(cwd, 'apps', 'web', 'index.html'));
|
|
20
|
-
}
|
|
21
|
-
/** cwd 의 로컬 vite bin 을 찾는다(없으면 undefined — 건너뜀). */
|
|
22
|
-
function resolveViteBin(cwd) {
|
|
23
|
-
const bin = join(cwd, 'node_modules', '.bin', 'vite');
|
|
24
|
-
return existsSync(bin) ? bin : undefined;
|
|
25
|
-
}
|
|
19
|
+
import { resolve } from 'node:path';
|
|
20
|
+
import { appBase, buildApp, listFrontendApps } from './build.js';
|
|
26
21
|
/**
|
|
27
|
-
* vite
|
|
28
|
-
* 않고 no-op 핸들을 돌려준다(dev 는 계속 진행).
|
|
22
|
+
* apps/* 를 순회해 앱마다 vite watch 빌드를 띄운다. 프론트가 없거나 vite 가 없으면
|
|
23
|
+
* 아무 것도 하지 않고 no-op 핸들을 돌려준다(dev 는 계속 진행 · fail-open).
|
|
29
24
|
*/
|
|
30
25
|
export function startFrontendBuild(opts) {
|
|
31
26
|
const cwd = resolve(opts.cwd);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
const watchers = [];
|
|
28
|
+
let closed = false;
|
|
29
|
+
void (async () => {
|
|
30
|
+
const apps = await listFrontendApps(cwd);
|
|
31
|
+
if (apps.length === 0) {
|
|
32
|
+
opts.onLog('프론트 진입 HTML(apps/<앱>/index.html)이 없어 vite watch 빌드를 건너뜁니다.', 'info');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const app of apps) {
|
|
36
|
+
if (closed)
|
|
37
|
+
break;
|
|
38
|
+
opts.onLog(`▶ vite build --watch — apps/${app} → dist/${app} (base=${appBase(app)})`, 'info');
|
|
39
|
+
try {
|
|
40
|
+
const handle = await buildApp(cwd, app, { watch: true, onLog: opts.onLog });
|
|
41
|
+
if (closed) {
|
|
42
|
+
await handle.close();
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
watchers.push(handle);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
opts.onLog(`vite watch 빌드를 건너뜁니다(apps/${app}): ${err instanceof Error ? err.message : String(err)}`, 'warn');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
})().catch((err) => {
|
|
52
|
+
opts.onLog(`프론트 watch 빌드 오류: ${err instanceof Error ? err.message : String(err)}`, 'error');
|
|
45
53
|
});
|
|
46
|
-
child.stdout?.on('data', (b) => opts.onLog(b.toString().trimEnd(), 'info'));
|
|
47
|
-
child.stderr?.on('data', (b) => opts.onLog(b.toString().trimEnd(), 'error'));
|
|
48
|
-
child.on('error', (err) => opts.onLog(`vite 실행 실패: ${err.message}`, 'error'));
|
|
49
|
-
opts.onLog('▶ vite build --watch — 프론트 번들 감시 (dist/web)', 'info');
|
|
50
54
|
return {
|
|
51
55
|
close() {
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
closed = true;
|
|
57
|
+
for (const w of watchers)
|
|
58
|
+
void w.close();
|
|
54
59
|
},
|
|
55
60
|
};
|
|
56
61
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { RuleReport } from './types.js';
|
|
2
|
-
/** app.config 소스가 JWT/API 앱인가 (외부 HTTP arm 제외 대상 · csrf-wiring 과 동일 판정).
|
|
2
|
+
/** app.config 소스가 JWT/API 앱인가 (외부 HTTP arm 제외 대상 · csrf-wiring 과 동일 판정).
|
|
3
|
+
* 결정 140(12차 W1): jwt 판정을 usesJwtStrategy 한 곳으로 통일 — 주석의
|
|
4
|
+
* `// strategy: 'jwt'` 오탐이 offload 검사를 잘못 끄지 않도록. */
|
|
3
5
|
export declare function isApiApp(appConfigSource: string): boolean;
|
|
4
6
|
/** 파일이 메일 SDK 를 직접 import 하는가(단위 테스트 진입점). */
|
|
5
7
|
export declare function importsMailSdk(source: string): boolean;
|
|
@@ -28,6 +28,7 @@ import { readdir, readFile } from 'node:fs/promises';
|
|
|
28
28
|
import { existsSync } from 'node:fs';
|
|
29
29
|
import { join, relative } from 'node:path';
|
|
30
30
|
import ts from 'typescript';
|
|
31
|
+
import { usesJwtStrategy } from './auth-wiring.js';
|
|
31
32
|
// ── 검출 시그니처 ─────────────────────────────────────────────────────
|
|
32
33
|
/** 메일 SDK 모듈 import (컨트롤러 = 잡 발행만 · async.md 함정 · async-flow forbidden 동일 집합). */
|
|
33
34
|
const MAIL_SDK_IMPORT = /import\s+[\s\S]*?\bfrom\s+['"](?:nodemailer|resend|@sendgrid\/mail|mailgun[.\-/][^'"]*|mailgun)['"]/;
|
|
@@ -44,9 +45,11 @@ const EXTERNAL_HTTP = [
|
|
|
44
45
|
// axios('https://...') · axios.get('https://...') · axios({ url: 'https://...' })
|
|
45
46
|
/\baxios\s*(?:\.\s*(?:get|post|put|patch|delete|request|head)\s*)?\(\s*(?:\{[\s\S]*?\burl\s*:\s*)?[`'"]https?:\/\/(?!localhost|127\.0\.0\.1)/i,
|
|
46
47
|
];
|
|
47
|
-
/** app.config 소스가 JWT/API 앱인가 (외부 HTTP arm 제외 대상 · csrf-wiring 과 동일 판정).
|
|
48
|
+
/** app.config 소스가 JWT/API 앱인가 (외부 HTTP arm 제외 대상 · csrf-wiring 과 동일 판정).
|
|
49
|
+
* 결정 140(12차 W1): jwt 판정을 usesJwtStrategy 한 곳으로 통일 — 주석의
|
|
50
|
+
* `// strategy: 'jwt'` 오탐이 offload 검사를 잘못 끄지 않도록. */
|
|
48
51
|
export function isApiApp(appConfigSource) {
|
|
49
|
-
return
|
|
52
|
+
return usesJwtStrategy(appConfigSource);
|
|
50
53
|
}
|
|
51
54
|
/** 파일이 메일 SDK 를 직접 import 하는가(단위 테스트 진입점). */
|
|
52
55
|
export function importsMailSdk(source) {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { RuleReport } from './types.js';
|
|
2
2
|
/** 컨트롤러 소스가 인증 표면을 쓰는지 판정한다(단위 테스트 진입점). */
|
|
3
3
|
export declare function usesAuthSurface(source: string): boolean;
|
|
4
|
-
/** app.config.ts 소스에 auth 배선이 있는지(
|
|
4
|
+
/** app.config.ts 소스에 auth 배선이 있는지(주석/문자열 제외 후 판정). */
|
|
5
5
|
export declare function hasAuthWiring(source: string): boolean;
|
|
6
|
-
/** app.config.ts 소스에 session 배선이 있는지(
|
|
6
|
+
/** app.config.ts 소스에 session 배선이 있는지(주석/문자열 제외 후 판정). */
|
|
7
7
|
export declare function hasSessionWiring(source: string): boolean;
|
|
8
|
+
/** app.config.ts 소스가 JWT 전략(strategy:'jwt')인지(주석 제외 후 판정 · 문자열 내용 보존). */
|
|
9
|
+
export declare function usesJwtStrategy(source: string): boolean;
|
|
8
10
|
/** apps/ 를 훑어 인증 배선 누락을 낸다. */
|
|
9
11
|
export declare function checkAuthWiring(cwd: string): Promise<RuleReport>;
|
|
@@ -11,17 +11,24 @@
|
|
|
11
11
|
import { readdir, readFile } from 'node:fs/promises';
|
|
12
12
|
import { existsSync } from 'node:fs';
|
|
13
13
|
import { join, relative } from 'node:path';
|
|
14
|
+
import { stripComments, stripCommentsAndStrings } from './source-scan.js';
|
|
15
|
+
// 결정 140(12차 W1): 판정 전 주석/문자열을 지운다. `// session:` 주석이나 문자열
|
|
16
|
+
// 안의 우연한 일치가 배선으로 오탐되면 보안 검사(세션·CSRF)가 통째로 skip 된다.
|
|
14
17
|
/** 컨트롤러 소스가 인증 표면을 쓰는지 판정한다(단위 테스트 진입점). */
|
|
15
18
|
export function usesAuthSurface(source) {
|
|
16
|
-
return /this\.requireAuth\s*\(|this\.auth\.login\s*\(|this\.currentUser\b/.test(source);
|
|
19
|
+
return /this\.requireAuth\s*\(|this\.auth\.login\s*\(|this\.currentUser\b/.test(stripCommentsAndStrings(source));
|
|
17
20
|
}
|
|
18
|
-
/** app.config.ts 소스에 auth 배선이 있는지(
|
|
21
|
+
/** app.config.ts 소스에 auth 배선이 있는지(주석/문자열 제외 후 판정). */
|
|
19
22
|
export function hasAuthWiring(source) {
|
|
20
|
-
return /\bauth\s*:/.test(source);
|
|
23
|
+
return /\bauth\s*:/.test(stripCommentsAndStrings(source));
|
|
21
24
|
}
|
|
22
|
-
/** app.config.ts 소스에 session 배선이 있는지(
|
|
25
|
+
/** app.config.ts 소스에 session 배선이 있는지(주석/문자열 제외 후 판정). */
|
|
23
26
|
export function hasSessionWiring(source) {
|
|
24
|
-
return /\bsession\s*:/.test(source);
|
|
27
|
+
return /\bsession\s*:/.test(stripCommentsAndStrings(source));
|
|
28
|
+
}
|
|
29
|
+
/** app.config.ts 소스가 JWT 전략(strategy:'jwt')인지(주석 제외 후 판정 · 문자열 내용 보존). */
|
|
30
|
+
export function usesJwtStrategy(source) {
|
|
31
|
+
return /strategy\s*:\s*['"]jwt['"]/.test(stripComments(source));
|
|
25
32
|
}
|
|
26
33
|
/** apps/ 를 훑어 인증 배선 누락을 낸다. */
|
|
27
34
|
export async function checkAuthWiring(cwd) {
|
|
@@ -42,7 +49,7 @@ export async function checkAuthWiring(cwd) {
|
|
|
42
49
|
const acPath = join(appsDir, app, 'app.config.ts');
|
|
43
50
|
const acRel = relative(cwd, acPath);
|
|
44
51
|
const acSource = existsSync(acPath) ? await readFile(acPath, 'utf8') : undefined;
|
|
45
|
-
const jwt = acSource != null &&
|
|
52
|
+
const jwt = acSource != null && usesJwtStrategy(acSource);
|
|
46
53
|
if (acSource == null || !hasAuthWiring(acSource)) {
|
|
47
54
|
issues.push({
|
|
48
55
|
rule: 'auth-wiring',
|
|
@@ -5,7 +5,29 @@ interface KeyUse {
|
|
|
5
5
|
readonly line: number;
|
|
6
6
|
readonly kind: 'table' | 'getConnection';
|
|
7
7
|
}
|
|
8
|
-
/** gaon.config.ts
|
|
8
|
+
/** gaon.config.ts 정적 분석 결과(결정 135). keys = 뽑아낸 커넥션 키, dbDeclared =
|
|
9
|
+
* db 프로퍼티 존재 여부, unanalyzable = db 초기화식에 정적으로 못 읽는 부분(식별자
|
|
10
|
+
* 참조·함수 호출 등)이 섞여 있었는지. unanalyzable 이면 커넥션 검사가 커버리지를
|
|
11
|
+
* 보장할 수 없어 doctor 가 안내를 낸다(W2). */
|
|
12
|
+
export interface ConfigDbAnalysis {
|
|
13
|
+
readonly keys: string[];
|
|
14
|
+
readonly dbDeclared: boolean;
|
|
15
|
+
readonly unanalyzable: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* gaon.config.ts 를 정적 AST 로 파싱해 db 커넥션 키를 분석한다(결정 135 · W2).
|
|
19
|
+
*
|
|
20
|
+
* 객체 리터럴뿐 아니라 스캐폴드·실사용에서 흔한 형태를 모두 훑는다:
|
|
21
|
+
* - `db: { main: {...} }` → ['main']
|
|
22
|
+
* - `db: process.env.X ? { main:{...} } : undefined` → 삼항 양 분기(결정 135)
|
|
23
|
+
* - `db: cfg ?? { main:{...} }` · `a && { ... }` → ?? · && · || 양변
|
|
24
|
+
* - `db: ({ main:{...} })` → 괄호 벗김
|
|
25
|
+
* - `db: { main, legacy }`(shorthand) → ['main','legacy']
|
|
26
|
+
* 못 읽는 형태(식별자 참조·함수 호출·스프레드)는 unanalyzable=true 로 표시한다 —
|
|
27
|
+
* "키 0" 로 조용히 통과시키던(main 폴백) 스캐폴드 삼항 사각(W2 근본)을 없앤다.
|
|
28
|
+
*/
|
|
29
|
+
export declare function analyzeConfigDb(source: string): ConfigDbAnalysis;
|
|
30
|
+
/** 하위 호환 — db 커넥션 키만 돌려준다(analyzeConfigDb 위임). */
|
|
9
31
|
export declare function extractConfigDbKeys(source: string): string[];
|
|
10
32
|
/** 소스에서 table(name, defs, { db: 'X' }) 와 getConnection('X') 호출을 추출한다. */
|
|
11
33
|
export declare function extractKeyUses(cwd: string, file: string, source: string): KeyUse[];
|
|
@@ -13,26 +13,77 @@ import { existsSync } from 'node:fs';
|
|
|
13
13
|
import { readdir, readFile } from 'node:fs/promises';
|
|
14
14
|
import { join, relative } from 'node:path';
|
|
15
15
|
import ts from 'typescript';
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* gaon.config.ts 를 정적 AST 로 파싱해 db 커넥션 키를 분석한다(결정 135 · W2).
|
|
18
|
+
*
|
|
19
|
+
* 객체 리터럴뿐 아니라 스캐폴드·실사용에서 흔한 형태를 모두 훑는다:
|
|
20
|
+
* - `db: { main: {...} }` → ['main']
|
|
21
|
+
* - `db: process.env.X ? { main:{...} } : undefined` → 삼항 양 분기(결정 135)
|
|
22
|
+
* - `db: cfg ?? { main:{...} }` · `a && { ... }` → ?? · && · || 양변
|
|
23
|
+
* - `db: ({ main:{...} })` → 괄호 벗김
|
|
24
|
+
* - `db: { main, legacy }`(shorthand) → ['main','legacy']
|
|
25
|
+
* 못 읽는 형태(식별자 참조·함수 호출·스프레드)는 unanalyzable=true 로 표시한다 —
|
|
26
|
+
* "키 0" 로 조용히 통과시키던(main 폴백) 스캐폴드 삼항 사각(W2 근본)을 없앤다.
|
|
27
|
+
*/
|
|
28
|
+
export function analyzeConfigDb(source) {
|
|
18
29
|
const sf = ts.createSourceFile('gaon.config.ts', source, ts.ScriptTarget.ES2022, true);
|
|
19
30
|
const keys = [];
|
|
31
|
+
let dbDeclared = false;
|
|
32
|
+
let unanalyzable = false;
|
|
33
|
+
const collect = (node) => {
|
|
34
|
+
if (ts.isParenthesizedExpression(node))
|
|
35
|
+
return collect(node.expression);
|
|
36
|
+
if (ts.isObjectLiteralExpression(node)) {
|
|
37
|
+
for (const dp of node.properties) {
|
|
38
|
+
if (ts.isPropertyAssignment(dp)) {
|
|
39
|
+
const n = propNameText(dp.name);
|
|
40
|
+
if (n)
|
|
41
|
+
keys.push(n);
|
|
42
|
+
else
|
|
43
|
+
unanalyzable = true; // 계산된 키 등
|
|
44
|
+
}
|
|
45
|
+
else if (ts.isShorthandPropertyAssignment(dp)) {
|
|
46
|
+
keys.push(dp.name.text);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
unanalyzable = true; // 스프레드·메서드 등
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (ts.isConditionalExpression(node)) {
|
|
55
|
+
collect(node.whenTrue);
|
|
56
|
+
collect(node.whenFalse);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (ts.isBinaryExpression(node)) {
|
|
60
|
+
const op = node.operatorToken.kind;
|
|
61
|
+
if (op === ts.SyntaxKind.QuestionQuestionToken ||
|
|
62
|
+
op === ts.SyntaxKind.BarBarToken ||
|
|
63
|
+
op === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
64
|
+
collect(node.left);
|
|
65
|
+
collect(node.right);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
unanalyzable = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// `undefined` · `null` 분기 = 커넥션 없음(분석 가능 · 키 기여 0).
|
|
72
|
+
if (node.kind === ts.SyntaxKind.NullKeyword)
|
|
73
|
+
return;
|
|
74
|
+
if (ts.isIdentifier(node) && node.text === 'undefined')
|
|
75
|
+
return;
|
|
76
|
+
// 그 외(식별자 참조·함수 호출 등) = 정적으로 못 읽음.
|
|
77
|
+
unanalyzable = true;
|
|
78
|
+
};
|
|
20
79
|
const visit = (node) => {
|
|
21
|
-
// defineConfig({ db: { main: {...}, legacy: {...} } })
|
|
22
80
|
if (ts.isCallExpression(node) && isDefineConfig(node.expression)) {
|
|
23
81
|
const arg = node.arguments[0];
|
|
24
82
|
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
25
83
|
for (const p of arg.properties) {
|
|
26
84
|
if (ts.isPropertyAssignment(p) && propNameText(p.name) === 'db') {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (ts.isPropertyAssignment(dp)) {
|
|
30
|
-
const n = propNameText(dp.name);
|
|
31
|
-
if (n)
|
|
32
|
-
keys.push(n);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
85
|
+
dbDeclared = true;
|
|
86
|
+
collect(p.initializer);
|
|
36
87
|
}
|
|
37
88
|
}
|
|
38
89
|
}
|
|
@@ -40,7 +91,11 @@ export function extractConfigDbKeys(source) {
|
|
|
40
91
|
ts.forEachChild(node, visit);
|
|
41
92
|
};
|
|
42
93
|
visit(sf);
|
|
43
|
-
return keys;
|
|
94
|
+
return { keys, dbDeclared, unanalyzable };
|
|
95
|
+
}
|
|
96
|
+
/** 하위 호환 — db 커넥션 키만 돌려준다(analyzeConfigDb 위임). */
|
|
97
|
+
export function extractConfigDbKeys(source) {
|
|
98
|
+
return analyzeConfigDb(source).keys;
|
|
44
99
|
}
|
|
45
100
|
function isDefineConfig(e) {
|
|
46
101
|
if (ts.isIdentifier(e) && e.text === 'defineConfig')
|
|
@@ -107,8 +162,24 @@ export async function checkConnections(cwd) {
|
|
|
107
162
|
const configPath = findConfigPath(cwd);
|
|
108
163
|
if (configPath) {
|
|
109
164
|
const src = await readFile(configPath, 'utf8');
|
|
110
|
-
|
|
165
|
+
const analysis = analyzeConfigDb(src);
|
|
166
|
+
for (const k of analysis.keys)
|
|
111
167
|
registered.add(k);
|
|
168
|
+
// W2(결정 135): db 가 선언됐는데 정적으로 못 읽는 형태면 커넥션 검사가
|
|
169
|
+
// 등록 키를 놓쳐 오탐/사각이 생긴다 — "main 만 허용" 으로 조용히 통과하지 말고
|
|
170
|
+
// 안내한다(에러 = 수리 안내서 · §7.5.3).
|
|
171
|
+
if (analysis.dbDeclared && analysis.unanalyzable) {
|
|
172
|
+
issues.push({
|
|
173
|
+
rule: 'connections',
|
|
174
|
+
level: 'warning',
|
|
175
|
+
file: relative(cwd, configPath),
|
|
176
|
+
message: `gaon.config.ts 의 db 설정을 정적으로 읽지 못했습니다 — 커넥션 키 검사가 불완전할 수 있습니다.\n` +
|
|
177
|
+
`읽어낸 키: ${analysis.keys.length ? analysis.keys.join(', ') : '(없음)'}\n` +
|
|
178
|
+
`→ db 를 리터럴 객체로 두세요(키는 리터럴 · 값만 env). 예:\n` +
|
|
179
|
+
` db: { main: { adapter: 'postgres', url: process.env.DATABASE_URL ?? '' } }`,
|
|
180
|
+
detail: { kind: 'unanalyzable-db', keys: analysis.keys },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
112
183
|
}
|
|
113
184
|
const files = [];
|
|
114
185
|
await collectTsFiles(join(cwd, 'domain'), files);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { RuleReport } from './types.js';
|
|
2
|
-
/** routes.ts 소스에 상태 변경(비-GET) 라우트가 있는지 판정한다(단위 테스트 진입점).
|
|
2
|
+
/** routes.ts 소스에 상태 변경(비-GET) 라우트가 있는지 판정한다(단위 테스트 진입점).
|
|
3
|
+
* 결정 140(12차 W1): 주석/문자열 안의 우연한 일치가 CSRF 검사를 왜곡하지 않도록
|
|
4
|
+
* 판정 전 정규화한다(여는 따옴표 구분자는 보존되어 라우트 매칭은 유지). */
|
|
3
5
|
export declare function hasStateChangingRoutes(routesSource: string): boolean;
|
|
4
6
|
/** apps/ 를 훑어 CSRF/세션 미배선(비-GET 라우트 + session 없음)을 경고로 낸다. */
|
|
5
7
|
export declare function checkCsrfWiring(cwd: string): Promise<RuleReport>;
|
|
@@ -14,14 +14,18 @@
|
|
|
14
14
|
import { readdir, readFile } from 'node:fs/promises';
|
|
15
15
|
import { existsSync } from 'node:fs';
|
|
16
16
|
import { join, relative } from 'node:path';
|
|
17
|
-
import { hasSessionWiring } from './auth-wiring.js';
|
|
18
|
-
|
|
17
|
+
import { hasSessionWiring, usesJwtStrategy } from './auth-wiring.js';
|
|
18
|
+
import { stripCommentsAndStrings } from './source-scan.js';
|
|
19
|
+
/** routes.ts 소스에 상태 변경(비-GET) 라우트가 있는지 판정한다(단위 테스트 진입점).
|
|
20
|
+
* 결정 140(12차 W1): 주석/문자열 안의 우연한 일치가 CSRF 검사를 왜곡하지 않도록
|
|
21
|
+
* 판정 전 정규화한다(여는 따옴표 구분자는 보존되어 라우트 매칭은 유지). */
|
|
19
22
|
export function hasStateChangingRoutes(routesSource) {
|
|
23
|
+
const src = stripCommentsAndStrings(routesSource);
|
|
20
24
|
// r.post('/x', ...) · r.put/patch/delete — 메서드 + 경로 문자열.
|
|
21
|
-
if (/\.\s*(post|put|patch|delete)\s*\(\s*['"]/.test(
|
|
25
|
+
if (/\.\s*(post|put|patch|delete)\s*\(\s*['"]/.test(src))
|
|
22
26
|
return true;
|
|
23
27
|
// resource(s)('posts') — 리소스 라우트는 create/update/destroy(POST/PUT/DELETE)를 만든다.
|
|
24
|
-
if (/\.\s*resources?\s*\(\s*['"]/.test(
|
|
28
|
+
if (/\.\s*resources?\s*\(\s*['"]/.test(src))
|
|
25
29
|
return true;
|
|
26
30
|
return false;
|
|
27
31
|
}
|
|
@@ -40,7 +44,7 @@ export async function checkCsrfWiring(cwd) {
|
|
|
40
44
|
const acRel = relative(cwd, acPath);
|
|
41
45
|
const acSource = existsSync(acPath) ? await readFile(acPath, 'utf8') : undefined;
|
|
42
46
|
// JWT/API 앱은 토큰 인증 — CSRF 대상 아님(제외).
|
|
43
|
-
const jwt = acSource != null &&
|
|
47
|
+
const jwt = acSource != null && usesJwtStrategy(acSource);
|
|
44
48
|
if (jwt)
|
|
45
49
|
continue;
|
|
46
50
|
if (acSource != null && hasSessionWiring(acSource))
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RuleReport } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 프로젝트 전체의 §4.5 관계 제약을 검사한다. domain/schema 가 없으면 통과.
|
|
4
|
+
* 스키마 로드 실패(문법 오류 등)는 이 검사만 warning 으로 강등하고 안내한다 —
|
|
5
|
+
* connections(static)·gaon check(typecheck)가 별도로 원인을 짚는다.
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkSchemaRelations(cwd: string): Promise<RuleReport>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @gaonjs/cli · doctor · 스키마 관계 검사 배선 (§4.5 · 결정 134)
|
|
2
|
+
//
|
|
3
|
+
// @gaonjs/data 의 checkCrossConnectionRelations·checkRelationTargets 를 `gaon
|
|
4
|
+
// doctor` 러너에 배선한다. 이 검사들은 이미 data 패키지에 구현+테스트가 완비돼
|
|
5
|
+
// 있었으나 CLI 러너가 호출하지 않아 커넥션을 가로지르는 belongsTo 가 doctor 로
|
|
6
|
+
// 잡히지 않고 배포 후 raw postgres 에러로만 드러났다 — "미구현"이 아니라
|
|
7
|
+
// "배선 안 됨"(§13.9 "단위 green ≠ 개발자 도달" 4번째 사례 · 결정 134).
|
|
8
|
+
//
|
|
9
|
+
// connections 검사(static AST)와 달리 관계 그래프는 TableDef 값(참조 컬럼·db
|
|
10
|
+
// 키·역방향 관계)이 필요하다. 그래서 domain/schema/*.ts 를 실제 로드해 TableDef[]
|
|
11
|
+
// 를 모은다 — `gaon db diff`·`migrate` 와 같은 스키마 로드 경로(scanSchemaDir).
|
|
12
|
+
// 커넥션으로 걸러내지 않고 **전 커넥션 테이블을 함께** 봐야 커넥션을 가로지르는
|
|
13
|
+
// belongsTo 를 검출할 수 있다(resolve.ts scanTables 는 단일 dbKey 로 거르므로 부적합).
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { join, relative } from 'node:path';
|
|
16
|
+
import { scanSchemaDir, checkCrossConnectionRelations, checkRelationTargets, } from '@gaonjs/data';
|
|
17
|
+
import { registerTsResolve } from '../tsResolve.js';
|
|
18
|
+
/** TableDef 판별 — scanSchemaDir 이 준 모듈 네임스페이스에서 table() 산출만 고른다. */
|
|
19
|
+
function isTableDef(v) {
|
|
20
|
+
return (typeof v === 'object' &&
|
|
21
|
+
v !== null &&
|
|
22
|
+
'name' in v &&
|
|
23
|
+
'defs' in v &&
|
|
24
|
+
'db' in v &&
|
|
25
|
+
typeof v.name === 'string');
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* domain/schema/ 의 모든 TableDef 를 커넥션 구분 없이 로드하고, 각 테이블이
|
|
29
|
+
* 어느 소스 파일에서 왔는지 함께 기록한다(진단에 파일 위치를 붙이기 위함).
|
|
30
|
+
*/
|
|
31
|
+
async function loadAllTables(cwd, schemaDir) {
|
|
32
|
+
registerTsResolve();
|
|
33
|
+
// outDir 은 import 지정자 계산에만 쓰이고 파일을 쓰지 않는다(resolve.ts 관례).
|
|
34
|
+
const outDir = join(cwd, '.gaon');
|
|
35
|
+
const modules = await scanSchemaDir(schemaDir, outDir);
|
|
36
|
+
const tables = [];
|
|
37
|
+
const fileOf = new Map();
|
|
38
|
+
for (const mod of modules) {
|
|
39
|
+
// importPath 는 .gaon 기준 상대(./../domain/schema/x.js) — 사람이 읽을 프로젝트
|
|
40
|
+
// 상대 경로로 정규화한다(진단 file 필드는 cwd 상대 관례).
|
|
41
|
+
const relFile = importPathToProjectRel(cwd, outDir, mod.importPath);
|
|
42
|
+
for (const [, value] of Object.entries(mod.ns)) {
|
|
43
|
+
if (isTableDef(value)) {
|
|
44
|
+
tables.push(value);
|
|
45
|
+
if (!fileOf.has(value.name))
|
|
46
|
+
fileOf.set(value.name, relFile);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
tables.sort((a, b) => a.name.localeCompare(b.name));
|
|
51
|
+
return { tables, fileOf };
|
|
52
|
+
}
|
|
53
|
+
/** scanSchemaDir 의 importPath(.gaon 기준, .js)를 cwd 상대 .ts 경로로 되돌린다. */
|
|
54
|
+
function importPathToProjectRel(cwd, outDir, importPath) {
|
|
55
|
+
const abs = join(outDir, importPath).replace(/\.js$/, '.ts');
|
|
56
|
+
return relative(cwd, abs).replace(/\\/g, '/');
|
|
57
|
+
}
|
|
58
|
+
/** data 의 Diagnostic 를 doctor 의 DoctorCheck 로 변환한다(§4.5 → RuleReport 정합). */
|
|
59
|
+
function toCheck(d, fileOf, owner) {
|
|
60
|
+
const file = fileOf.get(owner);
|
|
61
|
+
return {
|
|
62
|
+
rule: 'schema-relations',
|
|
63
|
+
level: d.level === 'warning' ? 'warning' : 'error',
|
|
64
|
+
message: d.message,
|
|
65
|
+
...(file ? { file } : {}),
|
|
66
|
+
detail: { code: d.code, table: owner },
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 관계 진단이 어느 테이블 소유인지를 message 앞머리(`<table>.<...>`)에서 뽑아
|
|
71
|
+
* 파일을 붙인다. 관계·컬럼 검사 모두 "테이블명.멤버 …" 로 시작한다.
|
|
72
|
+
*/
|
|
73
|
+
function ownerOf(message) {
|
|
74
|
+
const m = message.match(/^([A-Za-z0-9_]+)\./);
|
|
75
|
+
return m ? m[1] : '';
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 프로젝트 전체의 §4.5 관계 제약을 검사한다. domain/schema 가 없으면 통과.
|
|
79
|
+
* 스키마 로드 실패(문법 오류 등)는 이 검사만 warning 으로 강등하고 안내한다 —
|
|
80
|
+
* connections(static)·gaon check(typecheck)가 별도로 원인을 짚는다.
|
|
81
|
+
*/
|
|
82
|
+
export async function checkSchemaRelations(cwd) {
|
|
83
|
+
const schemaDir = join(cwd, 'domain', 'schema');
|
|
84
|
+
if (!existsSync(schemaDir))
|
|
85
|
+
return { rule: 'schema-relations', issues: [] };
|
|
86
|
+
let tables;
|
|
87
|
+
let fileOf;
|
|
88
|
+
try {
|
|
89
|
+
;
|
|
90
|
+
({ tables, fileOf } = await loadAllTables(cwd, schemaDir));
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
94
|
+
return {
|
|
95
|
+
rule: 'schema-relations',
|
|
96
|
+
issues: [
|
|
97
|
+
{
|
|
98
|
+
rule: 'schema-relations',
|
|
99
|
+
level: 'warning',
|
|
100
|
+
message: `스키마 관계 검사를 건너뜁니다 — domain/schema 로드 실패: ${msg}\n` +
|
|
101
|
+
`→ 'gaon check' 로 타입 오류를 먼저 확인하세요(스키마가 컴파일되면 이 검사가 활성화됩니다).`,
|
|
102
|
+
detail: { skipped: true },
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// data 패키지 구현을 그대로 호출한다(재구현 금지 · 결정 134).
|
|
108
|
+
const diags = [
|
|
109
|
+
...checkCrossConnectionRelations(tables),
|
|
110
|
+
...checkRelationTargets(tables),
|
|
111
|
+
];
|
|
112
|
+
const issues = diags.map((d) => toCheck(d, fileOf, ownerOf(d.message)));
|
|
113
|
+
return { rule: 'schema-relations', issues };
|
|
114
|
+
}
|
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
import { readdir, readFile } from 'node:fs/promises';
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
+
import { stripComments, stripCommentsAndStrings } from './source-scan.js';
|
|
14
|
+
// 결정 140(12차 W1): 판정 전 주석/문자열을 지운다 — 주석의 `seal: {` 나 문자열 안의
|
|
15
|
+
// 우연한 일치가 검사를 왜곡하지 않도록.
|
|
13
16
|
/** app.config.ts 소스에 seal 토글(true 또는 { … })이 켜져 있는지 판정한다(단위 테스트 진입점). */
|
|
14
17
|
export function hasSealEnabled(appConfigSource) {
|
|
15
18
|
// seal: true · seal: { except: [...] } — false/생략은 제외.
|
|
16
|
-
return /\bseal\s*:\s*(true|\{)/.test(appConfigSource);
|
|
19
|
+
return /\bseal\s*:\s*(true|\{)/.test(stripCommentsAndStrings(appConfigSource));
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
19
22
|
* 결정 121(§3.1): seal 앱의 main.ts 가 seal 클라이언트를 배선했는지 판정한다(단위 테스트 진입점).
|
|
@@ -22,19 +25,22 @@ export function hasSealEnabled(appConfigSource) {
|
|
|
22
25
|
* (seal-browser-e2e 로 실측). import + sealClient 전달을 둘 다 확인한다.
|
|
23
26
|
*/
|
|
24
27
|
export function hasSealClientWired(mainSource) {
|
|
25
|
-
|
|
28
|
+
// import 경로 문자열을 봐야 하므로 문자열 내용은 보존(주석만 제거).
|
|
29
|
+
const src = stripComments(mainSource);
|
|
30
|
+
const imported = /import[^\n]*@gaonjs\/seal\/client/.test(src);
|
|
26
31
|
// createGaonApp(...) 호출 안에 sealClient 가 실제로 전달됐는지(멀티라인 객체 허용).
|
|
27
|
-
const passed = /createGaonApp\s*\([\s\S]*\bsealClient\b/.test(
|
|
32
|
+
const passed = /createGaonApp\s*\([\s\S]*\bsealClient\b/.test(src);
|
|
28
33
|
return imported && passed;
|
|
29
34
|
}
|
|
30
35
|
/** gaon.config.ts 소스에서 명시적으로 꺼진 보안 방어층 이름을 뽑는다(rateLimit·securityHeaders·cors). */
|
|
31
36
|
export function disabledDefenses(gaonConfigSource) {
|
|
37
|
+
const src = stripCommentsAndStrings(gaonConfigSource);
|
|
32
38
|
const off = [];
|
|
33
|
-
if (/\brateLimit\s*:\s*false/.test(
|
|
39
|
+
if (/\brateLimit\s*:\s*false/.test(src))
|
|
34
40
|
off.push('rateLimit');
|
|
35
|
-
if (/\bsecurityHeaders\s*:\s*false/.test(
|
|
41
|
+
if (/\bsecurityHeaders\s*:\s*false/.test(src))
|
|
36
42
|
off.push('securityHeaders');
|
|
37
|
-
if (/\bcors\s*:\s*false/.test(
|
|
43
|
+
if (/\bcors\s*:\s*false/.test(src))
|
|
38
44
|
off.push('cors');
|
|
39
45
|
return off;
|
|
40
46
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 주석을 지운 소스를 돌려준다(문자열·코드는 보존 · 라인 번호 유지).
|
|
3
|
+
* 문자열 **내용**을 봐야 하는 탐지기(예: `strategy: 'jwt'` · import 경로)에 쓴다.
|
|
4
|
+
*/
|
|
5
|
+
export declare function stripComments(source: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* 주석 + 문자열/템플릿 리터럴 내용을 지운 소스를 돌려준다(구분자·라인 보존).
|
|
8
|
+
* 프로퍼티 키·키워드만 보는 탐지기(예: `session:` · `auth:` · `rateLimit: false`)에
|
|
9
|
+
* 쓴다 — 주석도 문자열도 키를 위장하지 못한다.
|
|
10
|
+
*/
|
|
11
|
+
export declare function stripCommentsAndStrings(source: string): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// @gaonjs/cli · doctor · 소스 정규화 유틸 (12차 W1 · 결정 140)
|
|
2
|
+
//
|
|
3
|
+
// 탐지기가 정규식으로 소스를 볼 때 **주석·문자열 안의 우연한 일치**를 배선으로
|
|
4
|
+
// 오탐하지 않도록, TS 스캐너로 토큰을 훑어 주석/문자열을 안전하게 지운다.
|
|
5
|
+
//
|
|
6
|
+
// 왜 스캐너인가(regex 스트립이 아니라): 보안 탐지기(auth·csrf·seal)는 오탐이
|
|
7
|
+
// 검사 skip = 취약점 노출로 이어진다(12차 W1 · gaon g app 스캐폴드의 주석
|
|
8
|
+
// `// session: { ... }` 가 hasSessionWiring 을 만족시켜 CSRF/세션 검사를
|
|
9
|
+
// 통째로 건너뛴 P0). regex 기반 주석 스트립은 문자열 안의 `//` 를 잘못 지우는
|
|
10
|
+
// 등 경계에서 새므로, 정확한 토큰 경계가 필요한 보안 탐지기는 스캐너로 지운다.
|
|
11
|
+
// (connections.ts·schema-relations.ts 가 이미 TS AST 를 쓰는 것과 같은 결.)
|
|
12
|
+
//
|
|
13
|
+
// 라인/컬럼(개행)은 보존한다 — 탐지기가 라인 번호를 그대로 보고할 수 있게.
|
|
14
|
+
import ts from 'typescript';
|
|
15
|
+
function isCommentKind(k) {
|
|
16
|
+
return k === ts.SyntaxKind.SingleLineCommentTrivia || k === ts.SyntaxKind.MultiLineCommentTrivia;
|
|
17
|
+
}
|
|
18
|
+
function isStringKind(k) {
|
|
19
|
+
return (k === ts.SyntaxKind.StringLiteral ||
|
|
20
|
+
k === ts.SyntaxKind.NoSubstitutionTemplateLiteral ||
|
|
21
|
+
k === ts.SyntaxKind.TemplateHead ||
|
|
22
|
+
k === ts.SyntaxKind.TemplateMiddle ||
|
|
23
|
+
k === ts.SyntaxKind.TemplateTail);
|
|
24
|
+
}
|
|
25
|
+
/** 개행만 남기고 나머지 문자는 공백으로 — 라인 번호를 보존한다. */
|
|
26
|
+
function blankKeepLines(text) {
|
|
27
|
+
return text.replace(/[^\n]/g, ' ');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 문자열/템플릿 토큰의 **내용**만 비우고 구분자(따옴표·백틱·`${`·`}`)는 남긴다.
|
|
31
|
+
* `'jwt'` → `' '` · `''` → `''`. 프로퍼티 키·구분자에 걸린 탐지기(예: 라우트
|
|
32
|
+
* `.post('...')` 의 여는 따옴표)는 여전히 매칭되지만, 문자열 내용이 키를 위장하는
|
|
33
|
+
* 오탐은 막는다. 개행은 보존한다.
|
|
34
|
+
*/
|
|
35
|
+
function blankStringContent(text) {
|
|
36
|
+
if (text.length <= 2)
|
|
37
|
+
return text;
|
|
38
|
+
return text[0] + blankKeepLines(text.slice(1, -1)) + text[text.length - 1];
|
|
39
|
+
}
|
|
40
|
+
function scanTransform(source, strings) {
|
|
41
|
+
// skipTrivia=false 로 주석·공백까지 토큰으로 받아 원문을 무손실 재구성한다.
|
|
42
|
+
const scanner = ts.createScanner(ts.ScriptTarget.Latest,
|
|
43
|
+
/* skipTrivia */ false, ts.LanguageVariant.Standard, source);
|
|
44
|
+
let out = '';
|
|
45
|
+
for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
|
46
|
+
const text = scanner.getTokenText();
|
|
47
|
+
if (isCommentKind(token))
|
|
48
|
+
out += blankKeepLines(text);
|
|
49
|
+
else if (strings && isStringKind(token))
|
|
50
|
+
out += blankStringContent(text);
|
|
51
|
+
else
|
|
52
|
+
out += text;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 주석을 지운 소스를 돌려준다(문자열·코드는 보존 · 라인 번호 유지).
|
|
58
|
+
* 문자열 **내용**을 봐야 하는 탐지기(예: `strategy: 'jwt'` · import 경로)에 쓴다.
|
|
59
|
+
*/
|
|
60
|
+
export function stripComments(source) {
|
|
61
|
+
return scanTransform(source, /* strings */ false);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 주석 + 문자열/템플릿 리터럴 내용을 지운 소스를 돌려준다(구분자·라인 보존).
|
|
65
|
+
* 프로퍼티 키·키워드만 보는 탐지기(예: `session:` · `auth:` · `rateLimit: false`)에
|
|
66
|
+
* 쓴다 — 주석도 문자열도 키를 위장하지 못한다.
|
|
67
|
+
*/
|
|
68
|
+
export function stripCommentsAndStrings(source) {
|
|
69
|
+
return scanTransform(source, /* strings */ true);
|
|
70
|
+
}
|