sayren 0.3.2 → 0.3.3

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 +184 -9
  2. 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 */
@@ -6312,6 +6385,24 @@ const mcp_verify_loader_first_query = ((inputs = {}, options = {}) => {
6312
6385
  return ko_mcp_verify_loader_first_query(inputs);
6313
6386
  });
6314
6387
 
6388
+ //#endregion
6389
+ //#region ../i18n/src/paraglide/messages/mcp_verify_local_import_missing.js
6390
+ /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
6391
+ /** @typedef {{ specifier: NonNullable<unknown>, path: NonNullable<unknown> }} Mcp_Verify_Local_Import_MissingInputs */
6392
+ const ko_mcp_verify_local_import_missing = (i) => {
6393
+ return `\`${i?.specifier}\`를 import하지만 작업 트리에 \`${i?.path}\`가 없다`;
6394
+ };
6395
+ /**
6396
+
6397
+ * @param {Mcp_Verify_Local_Import_MissingInputs} inputs
6398
+ * @param {{ locale?: "ko" }} options
6399
+ * @returns {LocalizedString}
6400
+ */
6401
+ const mcp_verify_local_import_missing = ((inputs, options = {}) => {
6402
+ experimentalStaticLocale ?? options.locale ?? getLocale();
6403
+ return ko_mcp_verify_local_import_missing(inputs);
6404
+ });
6405
+
6315
6406
  //#endregion
6316
6407
  //#region ../i18n/src/paraglide/messages/mcp_verify_no_module_token.js
6317
6408
  /** @typedef {import('../runtime.js').LocalizedString} LocalizedString */
@@ -6561,7 +6652,7 @@ function takeLangFlag(argv) {
6561
6652
 
6562
6653
  //#endregion
6563
6654
  //#region package.json
6564
- var version = "0.3.2";
6655
+ var version = "0.3.3";
6565
6656
 
6566
6657
  //#endregion
6567
6658
  //#region src/client.ts
@@ -7783,6 +7874,14 @@ const RULES = [
7783
7874
  doc: "/guides/storefront-sdk",
7784
7875
  templateFile: "src/lib/ordering.ts",
7785
7876
  severity: "warning"
7877
+ }),
7878
+ defineRule({
7879
+ id: "local-import-exists",
7880
+ title: mcp_rule_local_import_exists_title,
7881
+ why: mcp_rule_local_import_exists_why,
7882
+ fix: mcp_rule_local_import_exists_fix,
7883
+ example: mcp_rule_local_import_exists_example,
7884
+ doc: "/guides/mcp-customize"
7786
7885
  })
7787
7886
  ];
7788
7887
  /**
@@ -7816,7 +7915,7 @@ const CUSTOM_INPUT_LEAKS = [
7816
7915
  * 만들어진 프로젝트의 소스를 규칙과 대조한다. 정적 검사라 모든 문제를 잡지는 못하지만,
7817
7916
  * 여기 있는 것들은 실제로 반복해서 깨지는 것들이다.
7818
7917
  */
7819
- function verifySources(files) {
7918
+ function verifySources(files, options = {}) {
7820
7919
  const findings = [];
7821
7920
  const cleaned = files.map((file) => ({
7822
7921
  path: file.path,
@@ -7889,8 +7988,67 @@ function verifySources(files) {
7889
7988
  const clientQuery = matches(/\buse(Suspense)?Query\(/);
7890
7989
  if (clientQuery && !has("ensureQueryData(") && !has("prefetchQuery(")) inFile("loader-first", clientQuery, mcp_verify_loader_first_query(), /\buse(Suspense)?Query\(/);
7891
7990
  if (has("createStorefrontClient(") && !has("loader")) inProject("loader-first", mcp_verify_loader_first_missing());
7991
+ if (options.paths) findings.push(...missingLocalImports(cleaned, options.paths));
7892
7992
  return findings;
7893
7993
  }
7994
+ /** 상대 경로 import·export·동적 import의 대상 — `from "./x"`, `import "./x"`, `import("./x")` */
7995
+ const RELATIVE_IMPORT = /(?:\bfrom\s*|\bimport\s*\(?\s*)(["'])(\.{1,2}\/[^"'\n]+)\1/g;
7996
+ /** 확장자 없이 쓴 import가 가리킬 수 있는 파일 */
7997
+ const RESOLVE_SUFFIXES = [
7998
+ ".ts",
7999
+ ".tsx",
8000
+ ".js",
8001
+ ".jsx",
8002
+ ".mjs",
8003
+ ".json",
8004
+ "/index.ts",
8005
+ "/index.tsx",
8006
+ "/index.js",
8007
+ "/index.jsx"
8008
+ ];
8009
+ /** `from` 파일 기준으로 상대 경로를 푼다. 작업 트리 루트 밖으로 나가면 null(판정하지 않는다) */
8010
+ function resolveRelative(from, specifier) {
8011
+ const segments = from.split("/").slice(0, -1);
8012
+ for (const segment of specifier.split("/")) {
8013
+ if (segment === "" || segment === ".") continue;
8014
+ if (segment === "..") {
8015
+ if (!segments.length) return null;
8016
+ segments.pop();
8017
+ } else segments.push(segment);
8018
+ }
8019
+ return segments.join("/");
8020
+ }
8021
+ /**
8022
+ * 상대 경로로 import한 파일이 작업 트리에 있는가(이슈 #78) — 규칙 검사는 소스 문자열만 보므로 빠진 파일(예: 템플릿의
8023
+ * `messages/ko.json`)을 타입 검사·빌드 전에는 알 수 없었다. `paths`는 검사 대상 트리의 모든 파일 경로다(소스 아닌 파일 포함).
8024
+ * vite 쿼리(`?url`·`?raw`)는 떼고, `.js`로 쓴 TypeScript import(`./a.js` → `a.ts`)도 받는다.
8025
+ */
8026
+ function missingLocalImports(files, paths) {
8027
+ const tree = new Set(paths);
8028
+ const out = [];
8029
+ for (const file of files) for (const match of file.content.matchAll(RELATIVE_IMPORT)) {
8030
+ const specifier = match[2];
8031
+ const target = resolveRelative(file.path, specifier.replace(/[?#].*$/, ""));
8032
+ if (target === null || target === "") continue;
8033
+ const candidates = [target, ...RESOLVE_SUFFIXES.map((suffix) => `${target}${suffix}`)];
8034
+ const js = /\.(m?js|jsx)$/.exec(target);
8035
+ if (js) {
8036
+ const stem = target.slice(0, -js[0].length);
8037
+ candidates.push(`${stem}.ts`, `${stem}.tsx`, `${stem}.mts`);
8038
+ }
8039
+ if (candidates.some((candidate) => tree.has(candidate))) continue;
8040
+ out.push({
8041
+ ruleId: "local-import-exists",
8042
+ file: file.path,
8043
+ line: file.content.slice(0, match.index).split("\n").length,
8044
+ detail: mcp_verify_local_import_missing({
8045
+ specifier,
8046
+ path: target
8047
+ })
8048
+ });
8049
+ }
8050
+ return out;
8051
+ }
7894
8052
 
7895
8053
  //#endregion
7896
8054
  //#region ../mcp/src/verify-report.ts
@@ -7932,9 +8090,12 @@ function nextSteps(findings, target) {
7932
8090
  mcp_report_next_doc()
7933
8091
  ];
7934
8092
  }
7935
- /** 소스를 규칙과 대조해 고칠 수 있는 보고서로 만든다 */
7936
- function buildReport(files, target = "local") {
7937
- return reportFromFindings(verifySources(files), files.length, target);
8093
+ /**
8094
+ * 소스를 규칙과 대조해 고칠 수 있는 보고서로 만든다. `paths`(트리의 모든 파일 경로)를 주면 상대 경로 import의 대상이
8095
+ * 트리에 있는지도 본다(`local-import-exists`, #78)
8096
+ */
8097
+ function buildReport(files, target = "local", paths) {
8098
+ return reportFromFindings(verifySources(files, { paths }), files.length, target);
7938
8099
  }
7939
8100
  /**
7940
8101
  * 이미 찾은 위반으로 보고서를 만든다. 원격은 api가 작업 트리에서 `verifySources`를 돌려 위반만 돌려주고, 원인·수정
@@ -8015,7 +8176,7 @@ function verifyTree(files) {
8015
8176
  path,
8016
8177
  content
8017
8178
  }];
8018
- }), "remote");
8179
+ }), "remote", files.keys());
8019
8180
  }
8020
8181
 
8021
8182
  //#endregion
@@ -8048,7 +8209,21 @@ const WRITABLE_ROOT_FILES = new Set([
8048
8209
  ]);
8049
8210
  /** 읽기만 되는 루트 파일 — 의존성은 전용 경로로만 바꾼다(레지스트리 프록시와 잠금 파일을 함께 맞춘다) */
8050
8211
  const READONLY_ROOT_FILES = new Set(["package.json", "pnpm-lock.yaml"]);
8051
- const WRITABLE_DIRS = new Set(["src", "public"]);
8212
+ /**
8213
+ * 쓸 수 있는 폴더 — 코드(`src`), 정적 파일(`public`), 화면 문구 카탈로그(`messages`, 템플릿 `src/i18n`이 `messages/ko.json`을 import한다, #73).
8214
+ * 템플릿이 import하는 파일이 이 밖에 있으면 첫 작업 트리(source.tgz)를 풀 때 빠져 빌드·검사가 깨진다(#78)
8215
+ */
8216
+ const WRITABLE_DIRS = new Set([
8217
+ "src",
8218
+ "public",
8219
+ "messages"
8220
+ ]);
8221
+ /** 작업 트리 루트에 둘 수 있는 이름(폴더·파일) — 컨테이너 스냅샷이 이 이름만 묶는다(`container-tree.ts`) */
8222
+ const TRACKED_ROOT_NAMES = [
8223
+ ...WRITABLE_DIRS,
8224
+ ...WRITABLE_ROOT_FILES,
8225
+ ...READONLY_ROOT_FILES
8226
+ ];
8052
8227
  /** 형식·금지 세그먼트만 본다(위치 규칙은 보지 않는다). 통과하면 null, 아니면 이유 */
8053
8228
  function formatProblem(path) {
8054
8229
  if (typeof path !== "string" || path.length === 0) return "empty";
@@ -8184,7 +8359,7 @@ async function gunzip(bytes, maxBytes) {
8184
8359
  //#endregion
8185
8360
  //#region src/tree.ts
8186
8361
  /**
8187
- * 로컬 작업 트리 — 서버 작업 트리와 **같은 경로 규칙**(편집 Sandbox `paths.ts`의 `accessOf`)으로 고른다. `src/**`·`public/**`·루트 설정
8362
+ * 로컬 작업 트리 — 서버 작업 트리와 **같은 경로 규칙**(편집 Sandbox `paths.ts`의 `accessOf`)으로 고른다. `src/**`·`public/**`·`messages/**`·루트 설정
8188
8363
  * 파일·`package.json`·`pnpm-lock.yaml`만 올리고, `.env*`·`.mcp.json`·`.sayren/`·`node_modules`·`dist`·`package-lock.json` 등은 올리지 않는다.
8189
8364
  * 비밀·로컬 상태 파일(`isNeverUpload`)은 서버 경로 규칙과 별개로 CLI가 먼저 막는다 — 서버 규칙이 넓어져도 토큰을 올리지 않게.
8190
8365
  */
@@ -8403,7 +8578,7 @@ function lockDrift(packageJson, lock) {
8403
8578
  /** 잠금 파일 `importers: .:`의 이름 → specifier. 읽지 못하면 null */
8404
8579
  function lockImporterSpecifiers(lock) {
8405
8580
  const lines = lock.split(/\r?\n/);
8406
- const start = lines.findIndex((line) => line === "importers:");
8581
+ const start = lines.indexOf("importers:");
8407
8582
  if (start < 0) return null;
8408
8583
  const out = /* @__PURE__ */ new Map();
8409
8584
  let inRoot = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sayren",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
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.1"
16
+ "@sayren/store-sdk": "^0.28.2"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@biomejs/biome": "^2.5.14",