sayren 0.3.3 → 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 +233 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -5953,6 +5953,85 @@ const mcp_rule_server_only_env_why = ((inputs = {}, options = {}) => {
|
|
|
5953
5953
|
return ko_mcp_rule_server_only_env_why(inputs);
|
|
5954
5954
|
});
|
|
5955
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
|
+
|
|
5956
6035
|
//#endregion
|
|
5957
6036
|
//#region ../i18n/src/paraglide/messages/mcp_rule_tenant_header_fix.js
|
|
5958
6037
|
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
@@ -6565,6 +6644,42 @@ const mcp_verify_server_only_env = ((inputs = {}, options = {}) => {
|
|
|
6565
6644
|
return ko_mcp_verify_server_only_env(inputs);
|
|
6566
6645
|
});
|
|
6567
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
|
+
|
|
6568
6683
|
//#endregion
|
|
6569
6684
|
//#region ../i18n/src/paraglide/messages/mcp_verify_tenant_header.js
|
|
6570
6685
|
/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
|
|
@@ -6652,7 +6767,7 @@ function takeLangFlag(argv) {
|
|
|
6652
6767
|
|
|
6653
6768
|
//#endregion
|
|
6654
6769
|
//#region package.json
|
|
6655
|
-
var version = "0.3.
|
|
6770
|
+
var version = "0.3.4";
|
|
6656
6771
|
|
|
6657
6772
|
//#endregion
|
|
6658
6773
|
//#region src/client.ts
|
|
@@ -7875,6 +7990,15 @@ const RULES = [
|
|
|
7875
7990
|
templateFile: "src/lib/ordering.ts",
|
|
7876
7991
|
severity: "warning"
|
|
7877
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
|
+
}),
|
|
7878
8002
|
defineRule({
|
|
7879
8003
|
id: "local-import-exists",
|
|
7880
8004
|
title: mcp_rule_local_import_exists_title,
|
|
@@ -7988,6 +8112,7 @@ function verifySources(files, options = {}) {
|
|
|
7988
8112
|
const clientQuery = matches(/\buse(Suspense)?Query\(/);
|
|
7989
8113
|
if (clientQuery && !has("ensureQueryData(") && !has("prefetchQuery(")) inFile("loader-first", clientQuery, mcp_verify_loader_first_query(), /\buse(Suspense)?Query\(/);
|
|
7990
8114
|
if (has("createStorefrontClient(") && !has("loader")) inProject("loader-first", mcp_verify_loader_first_missing());
|
|
8115
|
+
findings.push(...serverOnlyImports(cleaned));
|
|
7991
8116
|
if (options.paths) findings.push(...missingLocalImports(cleaned, options.paths));
|
|
7992
8117
|
return findings;
|
|
7993
8118
|
}
|
|
@@ -8049,6 +8174,113 @@ function missingLocalImports(files, paths) {
|
|
|
8049
8174
|
}
|
|
8050
8175
|
return out;
|
|
8051
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
|
+
}
|
|
8052
8284
|
|
|
8053
8285
|
//#endregion
|
|
8054
8286
|
//#region ../mcp/src/verify-report.ts
|