sayren 0.3.2 → 0.3.4
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/index.mjs +416 -9
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -5294,6 +5294,79 @@ const mcp_rule_loader_first_why = ((inputs = {}, options = {}) => {
|
|
|
5294
5294
|
return ko_mcp_rule_loader_first_why(inputs);
|
|
5295
5295
|
});
|
|
5296
5296
|
|
|
5297
|
+
//#endregion
|
|
5298
|
+
//#region ../i18n/src/paraglide/messages/mcp_rule_local_import_exists_example.js
|
|
5299
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
5300
|
+
/** @typedef {{}} Mcp_Rule_Local_Import_Exists_ExampleInputs */
|
|
5301
|
+
const ko_mcp_rule_local_import_exists_example = () => {
|
|
5302
|
+
return `// src/i18n/index.ts — 작업 트리에 messages/ko.json이 있어야 한다
|
|
5303
|
+
import ko from "../../messages/ko.json";`;
|
|
5304
|
+
};
|
|
5305
|
+
/**
|
|
5306
|
+
|
|
5307
|
+
* @param {Mcp_Rule_Local_Import_Exists_ExampleInputs} inputs
|
|
5308
|
+
* @param {{ locale?: "ko" }} options
|
|
5309
|
+
* @returns {LocalizedString}
|
|
5310
|
+
*/
|
|
5311
|
+
const mcp_rule_local_import_exists_example = ((inputs = {}, options = {}) => {
|
|
5312
|
+
experimentalStaticLocale ?? options.locale ?? getLocale();
|
|
5313
|
+
return ko_mcp_rule_local_import_exists_example(inputs);
|
|
5314
|
+
});
|
|
5315
|
+
|
|
5316
|
+
//#endregion
|
|
5317
|
+
//#region ../i18n/src/paraglide/messages/mcp_rule_local_import_exists_fix.js
|
|
5318
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
5319
|
+
/** @typedef {{}} Mcp_Rule_Local_Import_Exists_FixInputs */
|
|
5320
|
+
const ko_mcp_rule_local_import_exists_fix = () => {
|
|
5321
|
+
return `빠진 파일을 템플릿 원본(로컬 \`get_template_file\`, 또는 \`npx @sayren/mcp create\`로 받은 사본)에서 가져와 같은 경로에 둔다. 호스팅 사이트는 \`write_files\`(\`expectedHash: null\`)나 \`sayren deploy\`로 올린다. 쓰지 않는 import면 지운다. 템플릿의 화면 문구는 \`messages/ko.json\`이다`;
|
|
5322
|
+
};
|
|
5323
|
+
/**
|
|
5324
|
+
|
|
5325
|
+
* @param {Mcp_Rule_Local_Import_Exists_FixInputs} inputs
|
|
5326
|
+
* @param {{ locale?: "ko" }} options
|
|
5327
|
+
* @returns {LocalizedString}
|
|
5328
|
+
*/
|
|
5329
|
+
const mcp_rule_local_import_exists_fix = ((inputs = {}, options = {}) => {
|
|
5330
|
+
experimentalStaticLocale ?? options.locale ?? getLocale();
|
|
5331
|
+
return ko_mcp_rule_local_import_exists_fix(inputs);
|
|
5332
|
+
});
|
|
5333
|
+
|
|
5334
|
+
//#endregion
|
|
5335
|
+
//#region ../i18n/src/paraglide/messages/mcp_rule_local_import_exists_title.js
|
|
5336
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
5337
|
+
/** @typedef {{}} Mcp_Rule_Local_Import_Exists_TitleInputs */
|
|
5338
|
+
const ko_mcp_rule_local_import_exists_title = () => {
|
|
5339
|
+
return `상대 경로로 import한 파일은 작업 트리에 있어야 한다`;
|
|
5340
|
+
};
|
|
5341
|
+
/**
|
|
5342
|
+
|
|
5343
|
+
* @param {Mcp_Rule_Local_Import_Exists_TitleInputs} inputs
|
|
5344
|
+
* @param {{ locale?: "ko" }} options
|
|
5345
|
+
* @returns {LocalizedString}
|
|
5346
|
+
*/
|
|
5347
|
+
const mcp_rule_local_import_exists_title = ((inputs = {}, options = {}) => {
|
|
5348
|
+
experimentalStaticLocale ?? options.locale ?? getLocale();
|
|
5349
|
+
return ko_mcp_rule_local_import_exists_title(inputs);
|
|
5350
|
+
});
|
|
5351
|
+
|
|
5352
|
+
//#endregion
|
|
5353
|
+
//#region ../i18n/src/paraglide/messages/mcp_rule_local_import_exists_why.js
|
|
5354
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
5355
|
+
/** @typedef {{}} Mcp_Rule_Local_Import_Exists_WhyInputs */
|
|
5356
|
+
const ko_mcp_rule_local_import_exists_why = () => {
|
|
5357
|
+
return `규칙 검사는 코드 문자열만 보지만 타입 검사와 빌드는 import한 파일을 실제로 읽는다. 대상 파일이 없으면 \`Cannot find module\`(TS2307)로 검사와 버전 빌드가 실패한다`;
|
|
5358
|
+
};
|
|
5359
|
+
/**
|
|
5360
|
+
|
|
5361
|
+
* @param {Mcp_Rule_Local_Import_Exists_WhyInputs} inputs
|
|
5362
|
+
* @param {{ locale?: "ko" }} options
|
|
5363
|
+
* @returns {LocalizedString}
|
|
5364
|
+
*/
|
|
5365
|
+
const mcp_rule_local_import_exists_why = ((inputs = {}, options = {}) => {
|
|
5366
|
+
experimentalStaticLocale ?? options.locale ?? getLocale();
|
|
5367
|
+
return ko_mcp_rule_local_import_exists_why(inputs);
|
|
5368
|
+
});
|
|
5369
|
+
|
|
5297
5370
|
//#endregion
|
|
5298
5371
|
//#region ../i18n/src/paraglide/messages/mcp_rule_no_module_token_example.js
|
|
5299
5372
|
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
@@ -5880,6 +5953,85 @@ const mcp_rule_server_only_env_why = ((inputs = {}, options = {}) => {
|
|
|
5880
5953
|
return ko_mcp_rule_server_only_env_why(inputs);
|
|
5881
5954
|
});
|
|
5882
5955
|
|
|
5956
|
+
//#endregion
|
|
5957
|
+
//#region ../i18n/src/paraglide/messages/mcp_rule_server_only_import_example.js
|
|
5958
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
5959
|
+
/** @typedef {{}} Mcp_Rule_Server_Only_Import_ExampleInputs */
|
|
5960
|
+
const ko_mcp_rule_server_only_import_example = () => {
|
|
5961
|
+
return `// src/lib/my-orders.ts — 브라우저도 가져가는 파일
|
|
5962
|
+
import { apiFor } from "./api.server";
|
|
5963
|
+
import { readToken } from "./session.server";
|
|
5964
|
+
|
|
5965
|
+
// handler 안에서만 부른다. 일반 함수(export function)로 두면 브라우저 번들에 남는다
|
|
5966
|
+
export const listMyOrders = createServerFn({ method: "GET" }).handler(() =>
|
|
5967
|
+
apiFor({ accessToken: readToken() }).orders.list(),
|
|
5968
|
+
);`;
|
|
5969
|
+
};
|
|
5970
|
+
/**
|
|
5971
|
+
|
|
5972
|
+
* @param {Mcp_Rule_Server_Only_Import_ExampleInputs} inputs
|
|
5973
|
+
* @param {{ locale?: "ko" }} options
|
|
5974
|
+
* @returns {LocalizedString}
|
|
5975
|
+
*/
|
|
5976
|
+
const mcp_rule_server_only_import_example = ((inputs = {}, options = {}) => {
|
|
5977
|
+
experimentalStaticLocale ?? options.locale ?? getLocale();
|
|
5978
|
+
return ko_mcp_rule_server_only_import_example(inputs);
|
|
5979
|
+
});
|
|
5980
|
+
|
|
5981
|
+
//#endregion
|
|
5982
|
+
//#region ../i18n/src/paraglide/messages/mcp_rule_server_only_import_fix.js
|
|
5983
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
5984
|
+
/** @typedef {{}} Mcp_Rule_Server_Only_Import_FixInputs */
|
|
5985
|
+
const ko_mcp_rule_server_only_import_fix = () => {
|
|
5986
|
+
return `서버 API를 부르는 함수는 \`*.server.ts\` 파일로 옮기거나 \`createServerFn().handler(…)\`로 감싼다. 브라우저가 함께 가져가는 파일(컴포넌트·\`src/lib/*.ts\`)은 서버 함수만 내보내고, \`*.server.ts\`의 값은 handler 안에서만 부른다. 서버 전용 모듈을 다시 내보내지 않는다`;
|
|
5987
|
+
};
|
|
5988
|
+
/**
|
|
5989
|
+
|
|
5990
|
+
* @param {Mcp_Rule_Server_Only_Import_FixInputs} inputs
|
|
5991
|
+
* @param {{ locale?: "ko" }} options
|
|
5992
|
+
* @returns {LocalizedString}
|
|
5993
|
+
*/
|
|
5994
|
+
const mcp_rule_server_only_import_fix = ((inputs = {}, options = {}) => {
|
|
5995
|
+
experimentalStaticLocale ?? options.locale ?? getLocale();
|
|
5996
|
+
return ko_mcp_rule_server_only_import_fix(inputs);
|
|
5997
|
+
});
|
|
5998
|
+
|
|
5999
|
+
//#endregion
|
|
6000
|
+
//#region ../i18n/src/paraglide/messages/mcp_rule_server_only_import_title.js
|
|
6001
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
6002
|
+
/** @typedef {{}} Mcp_Rule_Server_Only_Import_TitleInputs */
|
|
6003
|
+
const ko_mcp_rule_server_only_import_title = () => {
|
|
6004
|
+
return `서버 전용 모듈(\`*.server.ts\`)에서 가져온 것은 서버 함수 안에서만 쓴다`;
|
|
6005
|
+
};
|
|
6006
|
+
/**
|
|
6007
|
+
|
|
6008
|
+
* @param {Mcp_Rule_Server_Only_Import_TitleInputs} inputs
|
|
6009
|
+
* @param {{ locale?: "ko" }} options
|
|
6010
|
+
* @returns {LocalizedString}
|
|
6011
|
+
*/
|
|
6012
|
+
const mcp_rule_server_only_import_title = ((inputs = {}, options = {}) => {
|
|
6013
|
+
experimentalStaticLocale ?? options.locale ?? getLocale();
|
|
6014
|
+
return ko_mcp_rule_server_only_import_title(inputs);
|
|
6015
|
+
});
|
|
6016
|
+
|
|
6017
|
+
//#endregion
|
|
6018
|
+
//#region ../i18n/src/paraglide/messages/mcp_rule_server_only_import_why.js
|
|
6019
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
6020
|
+
/** @typedef {{}} Mcp_Rule_Server_Only_Import_WhyInputs */
|
|
6021
|
+
const ko_mcp_rule_server_only_import_why = () => {
|
|
6022
|
+
return `TanStack Start는 서버 함수 handler만 브라우저 번들에서 걷어낸다. 그 밖에서 \`*.server.ts\`(\`api.server\` 등)의 값을 쓰면 import가 브라우저 번들에 남고, import 보호가 버전 빌드(\`vite build\`)를 실패시킨다. 개발 서버와 타입 검사는 이것을 잡지 않는다`;
|
|
6023
|
+
};
|
|
6024
|
+
/**
|
|
6025
|
+
|
|
6026
|
+
* @param {Mcp_Rule_Server_Only_Import_WhyInputs} inputs
|
|
6027
|
+
* @param {{ locale?: "ko" }} options
|
|
6028
|
+
* @returns {LocalizedString}
|
|
6029
|
+
*/
|
|
6030
|
+
const mcp_rule_server_only_import_why = ((inputs = {}, options = {}) => {
|
|
6031
|
+
experimentalStaticLocale ?? options.locale ?? getLocale();
|
|
6032
|
+
return ko_mcp_rule_server_only_import_why(inputs);
|
|
6033
|
+
});
|
|
6034
|
+
|
|
5883
6035
|
//#endregion
|
|
5884
6036
|
//#region ../i18n/src/paraglide/messages/mcp_rule_tenant_header_fix.js
|
|
5885
6037
|
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
@@ -6312,6 +6464,24 @@ const mcp_verify_loader_first_query = ((inputs = {}, options = {}) => {
|
|
|
6312
6464
|
return ko_mcp_verify_loader_first_query(inputs);
|
|
6313
6465
|
});
|
|
6314
6466
|
|
|
6467
|
+
//#endregion
|
|
6468
|
+
//#region ../i18n/src/paraglide/messages/mcp_verify_local_import_missing.js
|
|
6469
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
6470
|
+
/** @typedef {{ specifier: NonNullable<unknown>, path: NonNullable<unknown> }} Mcp_Verify_Local_Import_MissingInputs */
|
|
6471
|
+
const ko_mcp_verify_local_import_missing = (i) => {
|
|
6472
|
+
return `\`${i?.specifier}\`를 import하지만 작업 트리에 \`${i?.path}\`가 없다`;
|
|
6473
|
+
};
|
|
6474
|
+
/**
|
|
6475
|
+
|
|
6476
|
+
* @param {Mcp_Verify_Local_Import_MissingInputs} inputs
|
|
6477
|
+
* @param {{ locale?: "ko" }} options
|
|
6478
|
+
* @returns {LocalizedString}
|
|
6479
|
+
*/
|
|
6480
|
+
const mcp_verify_local_import_missing = ((inputs, options = {}) => {
|
|
6481
|
+
experimentalStaticLocale ?? options.locale ?? getLocale();
|
|
6482
|
+
return ko_mcp_verify_local_import_missing(inputs);
|
|
6483
|
+
});
|
|
6484
|
+
|
|
6315
6485
|
//#endregion
|
|
6316
6486
|
//#region ../i18n/src/paraglide/messages/mcp_verify_no_module_token.js
|
|
6317
6487
|
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
@@ -6474,6 +6644,42 @@ const mcp_verify_server_only_env = ((inputs = {}, options = {}) => {
|
|
|
6474
6644
|
return ko_mcp_verify_server_only_env(inputs);
|
|
6475
6645
|
});
|
|
6476
6646
|
|
|
6647
|
+
//#endregion
|
|
6648
|
+
//#region ../i18n/src/paraglide/messages/mcp_verify_server_only_import.js
|
|
6649
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
6650
|
+
/** @typedef {{ specifier: NonNullable<unknown>, name: NonNullable<unknown> }} Mcp_Verify_Server_Only_ImportInputs */
|
|
6651
|
+
const ko_mcp_verify_server_only_import = (i) => {
|
|
6652
|
+
return `서버 전용 모듈 \`${i?.specifier}\`의 \`${i?.name}\`을 서버 함수(handler) 밖에서 쓴다. 브라우저 번들에 남아 버전 빌드가 import 보호로 실패한다`;
|
|
6653
|
+
};
|
|
6654
|
+
/**
|
|
6655
|
+
|
|
6656
|
+
* @param {Mcp_Verify_Server_Only_ImportInputs} inputs
|
|
6657
|
+
* @param {{ locale?: "ko" }} options
|
|
6658
|
+
* @returns {LocalizedString}
|
|
6659
|
+
*/
|
|
6660
|
+
const mcp_verify_server_only_import = ((inputs, options = {}) => {
|
|
6661
|
+
experimentalStaticLocale ?? options.locale ?? getLocale();
|
|
6662
|
+
return ko_mcp_verify_server_only_import(inputs);
|
|
6663
|
+
});
|
|
6664
|
+
|
|
6665
|
+
//#endregion
|
|
6666
|
+
//#region ../i18n/src/paraglide/messages/mcp_verify_server_only_import_reexport.js
|
|
6667
|
+
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
6668
|
+
/** @typedef {{ specifier: NonNullable<unknown> }} Mcp_Verify_Server_Only_Import_ReexportInputs */
|
|
6669
|
+
const ko_mcp_verify_server_only_import_reexport = (i) => {
|
|
6670
|
+
return `서버 전용 모듈 \`${i?.specifier}\`를 부수 효과 import나 다시 내보내기로 가져온다. 브라우저 번들에 남아 버전 빌드가 import 보호로 실패한다`;
|
|
6671
|
+
};
|
|
6672
|
+
/**
|
|
6673
|
+
|
|
6674
|
+
* @param {Mcp_Verify_Server_Only_Import_ReexportInputs} inputs
|
|
6675
|
+
* @param {{ locale?: "ko" }} options
|
|
6676
|
+
* @returns {LocalizedString}
|
|
6677
|
+
*/
|
|
6678
|
+
const mcp_verify_server_only_import_reexport = ((inputs, options = {}) => {
|
|
6679
|
+
experimentalStaticLocale ?? options.locale ?? getLocale();
|
|
6680
|
+
return ko_mcp_verify_server_only_import_reexport(inputs);
|
|
6681
|
+
});
|
|
6682
|
+
|
|
6477
6683
|
//#endregion
|
|
6478
6684
|
//#region ../i18n/src/paraglide/messages/mcp_verify_tenant_header.js
|
|
6479
6685
|
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
@@ -6561,7 +6767,7 @@ function takeLangFlag(argv) {
|
|
|
6561
6767
|
|
|
6562
6768
|
//#endregion
|
|
6563
6769
|
//#region package.json
|
|
6564
|
-
var version = "0.3.
|
|
6770
|
+
var version = "0.3.4";
|
|
6565
6771
|
|
|
6566
6772
|
//#endregion
|
|
6567
6773
|
//#region src/client.ts
|
|
@@ -7783,6 +7989,23 @@ const RULES = [
|
|
|
7783
7989
|
doc: "/guides/storefront-sdk",
|
|
7784
7990
|
templateFile: "src/lib/ordering.ts",
|
|
7785
7991
|
severity: "warning"
|
|
7992
|
+
}),
|
|
7993
|
+
defineRule({
|
|
7994
|
+
id: "server-only-import",
|
|
7995
|
+
title: mcp_rule_server_only_import_title,
|
|
7996
|
+
why: mcp_rule_server_only_import_why,
|
|
7997
|
+
fix: mcp_rule_server_only_import_fix,
|
|
7998
|
+
example: mcp_rule_server_only_import_example,
|
|
7999
|
+
doc: "/guides/mcp-customize",
|
|
8000
|
+
templateFile: "src/lib/api.server.ts"
|
|
8001
|
+
}),
|
|
8002
|
+
defineRule({
|
|
8003
|
+
id: "local-import-exists",
|
|
8004
|
+
title: mcp_rule_local_import_exists_title,
|
|
8005
|
+
why: mcp_rule_local_import_exists_why,
|
|
8006
|
+
fix: mcp_rule_local_import_exists_fix,
|
|
8007
|
+
example: mcp_rule_local_import_exists_example,
|
|
8008
|
+
doc: "/guides/mcp-customize"
|
|
7786
8009
|
})
|
|
7787
8010
|
];
|
|
7788
8011
|
/**
|
|
@@ -7816,7 +8039,7 @@ const CUSTOM_INPUT_LEAKS = [
|
|
|
7816
8039
|
* 만들어진 프로젝트의 소스를 규칙과 대조한다. 정적 검사라 모든 문제를 잡지는 못하지만,
|
|
7817
8040
|
* 여기 있는 것들은 실제로 반복해서 깨지는 것들이다.
|
|
7818
8041
|
*/
|
|
7819
|
-
function verifySources(files) {
|
|
8042
|
+
function verifySources(files, options = {}) {
|
|
7820
8043
|
const findings = [];
|
|
7821
8044
|
const cleaned = files.map((file) => ({
|
|
7822
8045
|
path: file.path,
|
|
@@ -7889,8 +8112,175 @@ function verifySources(files) {
|
|
|
7889
8112
|
const clientQuery = matches(/\buse(Suspense)?Query\(/);
|
|
7890
8113
|
if (clientQuery && !has("ensureQueryData(") && !has("prefetchQuery(")) inFile("loader-first", clientQuery, mcp_verify_loader_first_query(), /\buse(Suspense)?Query\(/);
|
|
7891
8114
|
if (has("createStorefrontClient(") && !has("loader")) inProject("loader-first", mcp_verify_loader_first_missing());
|
|
8115
|
+
findings.push(...serverOnlyImports(cleaned));
|
|
8116
|
+
if (options.paths) findings.push(...missingLocalImports(cleaned, options.paths));
|
|
7892
8117
|
return findings;
|
|
7893
8118
|
}
|
|
8119
|
+
/** 상대 경로 import·export·동적 import의 대상 — `from "./x"`, `import "./x"`, `import("./x")` */
|
|
8120
|
+
const RELATIVE_IMPORT = /(?:\bfrom\s*|\bimport\s*\(?\s*)(["'])(\.{1,2}\/[^"'\n]+)\1/g;
|
|
8121
|
+
/** 확장자 없이 쓴 import가 가리킬 수 있는 파일 */
|
|
8122
|
+
const RESOLVE_SUFFIXES = [
|
|
8123
|
+
".ts",
|
|
8124
|
+
".tsx",
|
|
8125
|
+
".js",
|
|
8126
|
+
".jsx",
|
|
8127
|
+
".mjs",
|
|
8128
|
+
".json",
|
|
8129
|
+
"/index.ts",
|
|
8130
|
+
"/index.tsx",
|
|
8131
|
+
"/index.js",
|
|
8132
|
+
"/index.jsx"
|
|
8133
|
+
];
|
|
8134
|
+
/** `from` 파일 기준으로 상대 경로를 푼다. 작업 트리 루트 밖으로 나가면 null(판정하지 않는다) */
|
|
8135
|
+
function resolveRelative(from, specifier) {
|
|
8136
|
+
const segments = from.split("/").slice(0, -1);
|
|
8137
|
+
for (const segment of specifier.split("/")) {
|
|
8138
|
+
if (segment === "" || segment === ".") continue;
|
|
8139
|
+
if (segment === "..") {
|
|
8140
|
+
if (!segments.length) return null;
|
|
8141
|
+
segments.pop();
|
|
8142
|
+
} else segments.push(segment);
|
|
8143
|
+
}
|
|
8144
|
+
return segments.join("/");
|
|
8145
|
+
}
|
|
8146
|
+
/**
|
|
8147
|
+
* 상대 경로로 import한 파일이 작업 트리에 있는가(이슈 #78) — 규칙 검사는 소스 문자열만 보므로 빠진 파일(예: 템플릿의
|
|
8148
|
+
* `messages/ko.json`)을 타입 검사·빌드 전에는 알 수 없었다. `paths`는 검사 대상 트리의 모든 파일 경로다(소스 아닌 파일 포함).
|
|
8149
|
+
* vite 쿼리(`?url`·`?raw`)는 떼고, `.js`로 쓴 TypeScript import(`./a.js` → `a.ts`)도 받는다.
|
|
8150
|
+
*/
|
|
8151
|
+
function missingLocalImports(files, paths) {
|
|
8152
|
+
const tree = new Set(paths);
|
|
8153
|
+
const out = [];
|
|
8154
|
+
for (const file of files) for (const match of file.content.matchAll(RELATIVE_IMPORT)) {
|
|
8155
|
+
const specifier = match[2];
|
|
8156
|
+
const target = resolveRelative(file.path, specifier.replace(/[?#].*$/, ""));
|
|
8157
|
+
if (target === null || target === "") continue;
|
|
8158
|
+
const candidates = [target, ...RESOLVE_SUFFIXES.map((suffix) => `${target}${suffix}`)];
|
|
8159
|
+
const js = /\.(m?js|jsx)$/.exec(target);
|
|
8160
|
+
if (js) {
|
|
8161
|
+
const stem = target.slice(0, -js[0].length);
|
|
8162
|
+
candidates.push(`${stem}.ts`, `${stem}.tsx`, `${stem}.mts`);
|
|
8163
|
+
}
|
|
8164
|
+
if (candidates.some((candidate) => tree.has(candidate))) continue;
|
|
8165
|
+
out.push({
|
|
8166
|
+
ruleId: "local-import-exists",
|
|
8167
|
+
file: file.path,
|
|
8168
|
+
line: file.content.slice(0, match.index).split("\n").length,
|
|
8169
|
+
detail: mcp_verify_local_import_missing({
|
|
8170
|
+
specifier,
|
|
8171
|
+
path: target
|
|
8172
|
+
})
|
|
8173
|
+
});
|
|
8174
|
+
}
|
|
8175
|
+
return out;
|
|
8176
|
+
}
|
|
8177
|
+
/**
|
|
8178
|
+
* 서버 전용 모듈 — TanStack Start import 보호의 기본 판정과 같다(클라이언트 환경에서 `*.server.*` 파일과
|
|
8179
|
+
* `@tanstack/{react,solid,vue}-start/server`를 막는다). 표시 모듈 `…/server-only`는 막는 대상이 아니다.
|
|
8180
|
+
*/
|
|
8181
|
+
const SERVER_ONLY_SPECIFIER = /^(?:@tanstack\/(?:react|solid|vue)-start\/server|\.{1,2}\/(?:[^"'\n]*\/)?[^/"'\n]+\.server(?:\.[cm]?[jt]sx?)?)$/;
|
|
8182
|
+
/** 그 자체가 브라우저로 가지 않아 import 보호가 보지 않는 파일 — `*.server.*`와 테스트 */
|
|
8183
|
+
const SERVER_ONLY_IMPORTER = /\.server\.[cm]?[jt]sx?$|\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
8184
|
+
/** 스스로 서버 전용임을 밝힌 모듈 — 브라우저가 가져가면 import 보호가 그 모듈을 막으므로 여기서는 보지 않는다 */
|
|
8185
|
+
const SERVER_ONLY_MARKER = /\bimport\s*["']@tanstack\/(?:react|solid|vue)-start\/server-only["']/;
|
|
8186
|
+
/** 정적 import 문(여러 줄 포함). 1: `type` 2: 가져오는 이름들 3: 모듈 */
|
|
8187
|
+
const STATIC_IMPORT = /\bimport\s+(type\s+)?([^;"'`]*?)\s*\bfrom\s*["']([^"'\n]+)["']\s*;?/g;
|
|
8188
|
+
/** 부수 효과 import·다시 내보내기 — 서버 전용 모듈이면 그대로 브라우저 번들에 남는다 */
|
|
8189
|
+
const SIDE_EFFECT_OR_REEXPORT = /\bimport\s*["']([^"'\n]+)["']|\bexport\s+(?:type\s+)?(?:\*(?:\s+as\s+[\w$]+)?|\{[^}]*\})\s*from\s*["']([^"'\n]+)["']/g;
|
|
8190
|
+
/**
|
|
8191
|
+
* 서버에서만 도는 자리 — Start 컴파일러가 클라이언트 번들에서 걷어내는 인자다. `createServerFn().handler(…)`,
|
|
8192
|
+
* `createMiddleware().server(…)`·`createIsomorphicFn().server(…)`, `createServerOnlyFn(…)`, 서버 라우트
|
|
8193
|
+
* `createFileRoute(…)({ server: { handlers } })`의 `server` 객체. 일치의 마지막 글자가 여는 괄호다
|
|
8194
|
+
*/
|
|
8195
|
+
const SERVER_CONTEXT = /\.(?:handler|server)\s*\(|\bcreateServerOnlyFn\s*\(|\bserver\s*:\s*\{/g;
|
|
8196
|
+
/** import 문이 가져오는 값 이름 — `type` 표시가 붙은 이름은 빼고, `a as b`는 `b`다 */
|
|
8197
|
+
function importedBindings(clause) {
|
|
8198
|
+
const names = [];
|
|
8199
|
+
const named = /\{([^}]*)\}/.exec(clause);
|
|
8200
|
+
for (const part of named?.[1]?.split(",") ?? []) {
|
|
8201
|
+
const item = part.trim();
|
|
8202
|
+
if (!item || /^type\s/.test(item)) continue;
|
|
8203
|
+
const alias = /\bas\s+([\w$]+)$/.exec(item);
|
|
8204
|
+
names.push(alias ? alias[1] : item.split(/\s+/)[0]);
|
|
8205
|
+
}
|
|
8206
|
+
const rest = clause.replace(/\{[^}]*\}/, "");
|
|
8207
|
+
const namespace = /\*\s*as\s+([\w$]+)/.exec(rest);
|
|
8208
|
+
if (namespace) names.push(namespace[1]);
|
|
8209
|
+
const defaultName = /^\s*([\w$]+)\s*(?:,|$)/.exec(rest.replace(/\*\s*as\s+[\w$]+/, ""));
|
|
8210
|
+
if (defaultName) names.push(defaultName[1]);
|
|
8211
|
+
return names.filter((name) => /^[A-Za-z_$][\w$]*$/.test(name));
|
|
8212
|
+
}
|
|
8213
|
+
/** `open` 위치의 여는 괄호와 짝이 맞는 닫는 괄호 다음 위치. 문자열·템플릿 리터럴 안의 괄호는 세지 않는다 */
|
|
8214
|
+
function closingBracket(source, open) {
|
|
8215
|
+
let depth = 0;
|
|
8216
|
+
let quote = null;
|
|
8217
|
+
for (let i = open; i < source.length; i++) {
|
|
8218
|
+
const ch = source[i];
|
|
8219
|
+
if (quote) {
|
|
8220
|
+
if (ch === "\\") i++;
|
|
8221
|
+
else if (ch === quote) quote = null;
|
|
8222
|
+
continue;
|
|
8223
|
+
}
|
|
8224
|
+
if (ch === "\"" || ch === "'" || ch === "`") quote = ch;
|
|
8225
|
+
else if (ch === "(" || ch === "{" || ch === "[") depth++;
|
|
8226
|
+
else if ((ch === ")" || ch === "}" || ch === "]") && --depth === 0) return i + 1;
|
|
8227
|
+
}
|
|
8228
|
+
return source.length;
|
|
8229
|
+
}
|
|
8230
|
+
/** 줄 수를 지키며 구간을 공백으로 지운다 — 남은 코드의 위치가 원본 줄 번호와 맞아야 한다 */
|
|
8231
|
+
const blank = (text) => text.replace(/[^\n]/g, " ");
|
|
8232
|
+
/**
|
|
8233
|
+
* 브라우저로 갈 수 있는 모듈이 서버 전용 모듈을 쓰는가(이슈 #88). TanStack Start는 서버 함수 handler처럼 서버에서만
|
|
8234
|
+
* 도는 인자를 클라이언트 번들에서 걷어내고, 그 뒤에도 `*.server.*` import가 남으면 import 보호로 빌드를 실패시킨다.
|
|
8235
|
+
* 그래서 import 자체가 아니라 **서버 자리 밖에서 가져온 이름을 쓰는 것**을 위반으로 본다. 템플릿은 라우트·`lib`에서
|
|
8236
|
+
* `apiFor`를 import하고 `createServerFn().handler(…)`·서버 라우트 `server` 안에서만 부른다. 서버 자리 밖의 일반 함수(예: 서버 API를 부르는
|
|
8237
|
+
* `export function wishedOf()`)는 브라우저 번들에 남으므로 위반이다 — 그 함수를 `*.server.ts`로 옮기거나 서버 함수로 감싼다.
|
|
8238
|
+
* 정적 검사라 서버 자리에서만 부르는 지역 함수까지 따라가지는 않는다(보수적으로 위반으로 본다).
|
|
8239
|
+
*/
|
|
8240
|
+
function serverOnlyImports(files) {
|
|
8241
|
+
const out = [];
|
|
8242
|
+
for (const file of files) {
|
|
8243
|
+
if (!CLIENT_FILE.test(file.path) || SERVER_ONLY_IMPORTER.test(file.path)) continue;
|
|
8244
|
+
if (SERVER_ONLY_MARKER.test(file.content)) continue;
|
|
8245
|
+
const lineAt = (index) => file.content.slice(0, index).split("\n").length;
|
|
8246
|
+
for (const match of file.content.matchAll(SIDE_EFFECT_OR_REEXPORT)) {
|
|
8247
|
+
const specifier = match[1] ?? match[2];
|
|
8248
|
+
if (!SERVER_ONLY_SPECIFIER.test(specifier)) continue;
|
|
8249
|
+
out.push({
|
|
8250
|
+
ruleId: "server-only-import",
|
|
8251
|
+
file: file.path,
|
|
8252
|
+
line: lineAt(match.index),
|
|
8253
|
+
detail: mcp_verify_server_only_import_reexport({ specifier })
|
|
8254
|
+
});
|
|
8255
|
+
}
|
|
8256
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
8257
|
+
let body = file.content.replace(STATIC_IMPORT, (statement, typeOnly, clause, specifier) => {
|
|
8258
|
+
if (!typeOnly && SERVER_ONLY_SPECIFIER.test(specifier)) for (const name of importedBindings(clause)) bindings.set(name, specifier);
|
|
8259
|
+
return blank(statement);
|
|
8260
|
+
});
|
|
8261
|
+
if (bindings.size === 0) continue;
|
|
8262
|
+
for (const context of [...body.matchAll(SERVER_CONTEXT)]) {
|
|
8263
|
+
const open = context.index + context[0].length - 1;
|
|
8264
|
+
if (body[open] !== "(" && body[open] !== "{") continue;
|
|
8265
|
+
const end = closingBracket(body, open);
|
|
8266
|
+
body = body.slice(0, open) + blank(body.slice(open, end)) + body.slice(end);
|
|
8267
|
+
}
|
|
8268
|
+
for (const [name, specifier] of bindings) {
|
|
8269
|
+
const use = (/* @__PURE__ */ new RegExp(`(?<![\\w$.])${name.replace(/\$/g, "\\$")}(?![\\w$])`)).exec(body);
|
|
8270
|
+
if (!use) continue;
|
|
8271
|
+
out.push({
|
|
8272
|
+
ruleId: "server-only-import",
|
|
8273
|
+
file: file.path,
|
|
8274
|
+
line: lineAt(use.index),
|
|
8275
|
+
detail: mcp_verify_server_only_import({
|
|
8276
|
+
name,
|
|
8277
|
+
specifier
|
|
8278
|
+
})
|
|
8279
|
+
});
|
|
8280
|
+
}
|
|
8281
|
+
}
|
|
8282
|
+
return out;
|
|
8283
|
+
}
|
|
7894
8284
|
|
|
7895
8285
|
//#endregion
|
|
7896
8286
|
//#region ../mcp/src/verify-report.ts
|
|
@@ -7932,9 +8322,12 @@ function nextSteps(findings, target) {
|
|
|
7932
8322
|
mcp_report_next_doc()
|
|
7933
8323
|
];
|
|
7934
8324
|
}
|
|
7935
|
-
/**
|
|
7936
|
-
|
|
7937
|
-
|
|
8325
|
+
/**
|
|
8326
|
+
* 소스를 규칙과 대조해 고칠 수 있는 보고서로 만든다. `paths`(트리의 모든 파일 경로)를 주면 상대 경로 import의 대상이
|
|
8327
|
+
* 트리에 있는지도 본다(`local-import-exists`, #78)
|
|
8328
|
+
*/
|
|
8329
|
+
function buildReport(files, target = "local", paths) {
|
|
8330
|
+
return reportFromFindings(verifySources(files, { paths }), files.length, target);
|
|
7938
8331
|
}
|
|
7939
8332
|
/**
|
|
7940
8333
|
* 이미 찾은 위반으로 보고서를 만든다. 원격은 api가 작업 트리에서 `verifySources`를 돌려 위반만 돌려주고, 원인·수정
|
|
@@ -8015,7 +8408,7 @@ function verifyTree(files) {
|
|
|
8015
8408
|
path,
|
|
8016
8409
|
content
|
|
8017
8410
|
}];
|
|
8018
|
-
}), "remote");
|
|
8411
|
+
}), "remote", files.keys());
|
|
8019
8412
|
}
|
|
8020
8413
|
|
|
8021
8414
|
//#endregion
|
|
@@ -8048,7 +8441,21 @@ const WRITABLE_ROOT_FILES = new Set([
|
|
|
8048
8441
|
]);
|
|
8049
8442
|
/** 읽기만 되는 루트 파일 — 의존성은 전용 경로로만 바꾼다(레지스트리 프록시와 잠금 파일을 함께 맞춘다) */
|
|
8050
8443
|
const READONLY_ROOT_FILES = new Set(["package.json", "pnpm-lock.yaml"]);
|
|
8051
|
-
|
|
8444
|
+
/**
|
|
8445
|
+
* 쓸 수 있는 폴더 — 코드(`src`), 정적 파일(`public`), 화면 문구 카탈로그(`messages`, 템플릿 `src/i18n`이 `messages/ko.json`을 import한다, #73).
|
|
8446
|
+
* 템플릿이 import하는 파일이 이 밖에 있으면 첫 작업 트리(source.tgz)를 풀 때 빠져 빌드·검사가 깨진다(#78)
|
|
8447
|
+
*/
|
|
8448
|
+
const WRITABLE_DIRS = new Set([
|
|
8449
|
+
"src",
|
|
8450
|
+
"public",
|
|
8451
|
+
"messages"
|
|
8452
|
+
]);
|
|
8453
|
+
/** 작업 트리 루트에 둘 수 있는 이름(폴더·파일) — 컨테이너 스냅샷이 이 이름만 묶는다(`container-tree.ts`) */
|
|
8454
|
+
const TRACKED_ROOT_NAMES = [
|
|
8455
|
+
...WRITABLE_DIRS,
|
|
8456
|
+
...WRITABLE_ROOT_FILES,
|
|
8457
|
+
...READONLY_ROOT_FILES
|
|
8458
|
+
];
|
|
8052
8459
|
/** 형식·금지 세그먼트만 본다(위치 규칙은 보지 않는다). 통과하면 null, 아니면 이유 */
|
|
8053
8460
|
function formatProblem(path) {
|
|
8054
8461
|
if (typeof path !== "string" || path.length === 0) return "empty";
|
|
@@ -8184,7 +8591,7 @@ async function gunzip(bytes, maxBytes) {
|
|
|
8184
8591
|
//#endregion
|
|
8185
8592
|
//#region src/tree.ts
|
|
8186
8593
|
/**
|
|
8187
|
-
* 로컬 작업 트리 — 서버 작업 트리와 **같은 경로 규칙**(편집 Sandbox `paths.ts`의 `accessOf`)으로 고른다. `src/**`·`public/**`·루트 설정
|
|
8594
|
+
* 로컬 작업 트리 — 서버 작업 트리와 **같은 경로 규칙**(편집 Sandbox `paths.ts`의 `accessOf`)으로 고른다. `src/**`·`public/**`·`messages/**`·루트 설정
|
|
8188
8595
|
* 파일·`package.json`·`pnpm-lock.yaml`만 올리고, `.env*`·`.mcp.json`·`.sayren/`·`node_modules`·`dist`·`package-lock.json` 등은 올리지 않는다.
|
|
8189
8596
|
* 비밀·로컬 상태 파일(`isNeverUpload`)은 서버 경로 규칙과 별개로 CLI가 먼저 막는다 — 서버 규칙이 넓어져도 토큰을 올리지 않게.
|
|
8190
8597
|
*/
|
|
@@ -8403,7 +8810,7 @@ function lockDrift(packageJson, lock) {
|
|
|
8403
8810
|
/** 잠금 파일 `importers: .:`의 이름 → specifier. 읽지 못하면 null */
|
|
8404
8811
|
function lockImporterSpecifiers(lock) {
|
|
8405
8812
|
const lines = lock.split(/\r?\n/);
|
|
8406
|
-
const start = lines.
|
|
8813
|
+
const start = lines.indexOf("importers:");
|
|
8407
8814
|
if (start < 0) return null;
|
|
8408
8815
|
const out = /* @__PURE__ */ new Map();
|
|
8409
8816
|
let inRoot = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sayren",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "sayren 스토어프론트 CLI — 호스팅 중인 사이트의 소스를 내려받고(pull) 로컬에서 고친 뒤 배포(deploy)합니다",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"node": ">=20.10"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@sayren/store-sdk": "^0.28.
|
|
16
|
+
"@sayren/store-sdk": "^0.28.2"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@biomejs/biome": "^2.5.14",
|