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.
Files changed (2) hide show
  1. package/dist/index.mjs +233 -1
  2. 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.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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sayren",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "type": "module",
5
5
  "description": "sayren 스토어프론트 CLI — 호스팅 중인 사이트의 소스를 내려받고(pull) 로컬에서 고친 뒤 배포(deploy)합니다",
6
6
  "bin": {