@jonathanleelx/meddle 0.5.0-beta.2 → 0.5.0-beta.21
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/CONFIG_STRUCTURE.md +8 -2
- package/README.md +2 -0
- package/bin/commands/mock/add.js +33 -4
- package/bin/commands/mock/delete.js +8 -1
- package/bin/commands/mock/index.js +8 -4
- package/bin/commands/mock/list.js +1 -0
- package/bin/commands/mock/toggle.js +8 -1
- package/bin/commands/mock/update.js +31 -5
- package/bin/commands/route/active.js +6 -2
- package/bin/commands/route/add.js +12 -2
- package/bin/commands/route/create.js +8 -1
- package/bin/commands/route/delete.js +12 -2
- package/bin/commands/route/update.js +12 -2
- package/bin/commands/update.js +4 -0
- package/bin/lib/file-access.js +68 -25
- package/bin/lib/output.js +20 -1
- package/bin/lib/proxy-detect.js +99 -20
- package/bin/lib/update-check.js +67 -6
- package/dist/core/config-diagnostics.d.ts.map +1 -1
- package/dist/core/config-diagnostics.js +6 -1
- package/dist/core/config-diagnostics.js.map +1 -1
- package/dist/core/custom-plugin-loader.d.ts.map +1 -1
- package/dist/core/custom-plugin-loader.js +2 -0
- package/dist/core/custom-plugin-loader.js.map +1 -1
- package/dist/core/h2-pool.d.ts +1 -0
- package/dist/core/h2-pool.d.ts.map +1 -1
- package/dist/core/h2-pool.js +79 -23
- package/dist/core/h2-pool.js.map +1 -1
- package/dist/core/mock-handler.d.ts +65 -0
- package/dist/core/mock-handler.d.ts.map +1 -1
- package/dist/core/mock-handler.js +525 -29
- package/dist/core/mock-handler.js.map +1 -1
- package/dist/core/mock-utils.d.ts +58 -0
- package/dist/core/mock-utils.d.ts.map +1 -0
- package/dist/core/mock-utils.js +167 -0
- package/dist/core/mock-utils.js.map +1 -0
- package/dist/core/plugin-bootstrap-runner.d.ts +1 -0
- package/dist/core/plugin-bootstrap-runner.d.ts.map +1 -1
- package/dist/core/plugin-bootstrap-runner.js +37 -1
- package/dist/core/plugin-bootstrap-runner.js.map +1 -1
- package/dist/core/plugin-intercept.d.ts.map +1 -1
- package/dist/core/plugin-intercept.js +1 -0
- package/dist/core/plugin-intercept.js.map +1 -1
- package/dist/core/plugin-source.d.ts +24 -0
- package/dist/core/plugin-source.d.ts.map +1 -0
- package/dist/core/plugin-source.js +47 -0
- package/dist/core/plugin-source.js.map +1 -0
- package/dist/core/types.d.ts +13 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/helpers.d.ts +8 -1
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +22 -5
- package/dist/helpers.js.map +1 -1
- package/dist/plugins/builtin/mock-plugin.d.ts.map +1 -1
- package/dist/plugins/builtin/mock-plugin.js +6 -5
- package/dist/plugins/builtin/mock-plugin.js.map +1 -1
- package/dist/server/agent/tools.d.ts.map +1 -1
- package/dist/server/agent/tools.js +4 -0
- package/dist/server/agent/tools.js.map +1 -1
- package/dist/server/app-info.d.ts +19 -0
- package/dist/server/app-info.d.ts.map +1 -0
- package/dist/server/app-info.js +84 -0
- package/dist/server/app-info.js.map +1 -0
- package/dist/server/config.d.ts +1 -0
- package/dist/server/config.d.ts.map +1 -1
- package/dist/server/config.js +57 -7
- package/dist/server/config.js.map +1 -1
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/health.js +8 -0
- package/dist/server/health.js.map +1 -1
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/mocks.d.ts +26 -0
- package/dist/server/mocks.d.ts.map +1 -1
- package/dist/server/mocks.js +110 -13
- package/dist/server/mocks.js.map +1 -1
- package/dist/server/plugins.d.ts.map +1 -1
- package/dist/server/plugins.js +56 -11
- package/dist/server/plugins.js.map +1 -1
- package/dist/server/rule-files.d.ts +53 -0
- package/dist/server/rule-files.d.ts.map +1 -1
- package/dist/server/rule-files.js +281 -30
- package/dist/server/rule-files.js.map +1 -1
- package/index.js +33 -5
- package/mcp-server.js +3 -0
- package/package.json +3 -2
- package/plugins/builtin/mock-plugin.ts +9 -5
- package/web/dist/assets/alert-BmsRCcSL.js +1 -0
- package/web/dist/assets/arrow-right-BNum_060.js +1 -0
- package/web/dist/assets/{body-diff-view-D7dCWajG.js → body-diff-view-ByCHr9bv.js} +2 -2
- package/web/dist/assets/button-BFNlGZ5p.js +1 -0
- package/web/dist/assets/check-BkfHQ6re.js +1 -0
- package/web/dist/assets/checkbox-v2kzp7K8.js +1 -0
- package/web/dist/assets/chevron-down-DHwUtL4O.js +1 -0
- package/web/dist/assets/{circle-alert-Q8cZk9Rv.js → circle-alert-zMagrFuI.js} +1 -1
- package/web/dist/assets/{circle-x-C9USji_R.js → circle-x-BEkinhez.js} +1 -1
- package/web/dist/assets/code-DD-AYxxu.js +1 -0
- package/web/dist/assets/{copy-BYjq1hdN.js → copy-BRnscYy1.js} +1 -1
- package/web/dist/assets/detail-panel-DK7pM43g.js +4 -0
- package/web/dist/assets/{dist-D5Dj17P8.js → dist-BCvvYbmI.js} +1 -1
- package/web/dist/assets/dist-BNcjMpca.js +1 -0
- package/web/dist/assets/{dist-Bb9TeEub.js → dist-C-E2R2jk.js} +1 -1
- package/web/dist/assets/{dist-C6eaYCUo.js → dist-CFZ4xMSF.js} +1 -1
- package/web/dist/assets/{dist-C5nMErq8.js → dist-CRmX_NvH.js} +1 -1
- package/web/dist/assets/dist-DD7AvbXP.js +1 -0
- package/web/dist/assets/{dist-Cn0y0IRn.js → dist-LiAcI8CK.js} +1 -1
- package/web/dist/assets/dist-UcOFuoN0.js +5 -0
- package/web/dist/assets/{dist-B8XUKgXe.js → dist-YM3jbhT-.js} +1 -1
- package/web/dist/assets/editor-pane-actions-DN-pxFsl.js +1 -0
- package/web/dist/assets/es2015-CRmLuA2o.js +41 -0
- package/web/dist/assets/{global-panel-shell-R-EZQRhs.js → global-panel-shell-D32I98dR.js} +4 -4
- package/web/dist/assets/health-panel-Dw7qdFVS.js +1 -0
- package/web/dist/assets/index-CYUNdxqh.css +2 -0
- package/web/dist/assets/index-CkoYwomI.js +18 -0
- package/web/dist/assets/mobile-proxy-panel-DtiDkpAa.js +8 -0
- package/web/dist/assets/mock-config-C3jSpNNu.js +1 -0
- package/web/dist/assets/mock-editor-panel-ByCYWJjO.js +20 -0
- package/web/dist/assets/mocks-file-missing-note-DmrrCBYY.js +1 -0
- package/web/dist/assets/monaco-editor-6oLiu4W6.js +11 -0
- package/web/dist/assets/pane-split-DYB05onX.js +1 -0
- package/web/dist/assets/panel-heading-BS8loc49.js +1 -0
- package/web/dist/assets/{pencil-line-Cqe41E4h.js → pencil-line-CEZ7D-de.js} +1 -1
- package/web/dist/assets/plugin-code-editor-DGzCBEpg.js +2 -0
- package/web/dist/assets/plugin-config-NVw234EF.js +2 -0
- package/web/dist/assets/plugin-generator-Ba4hP5rg.js +2 -0
- package/web/dist/assets/plugin-test-dialog-B4ojc2_9.js +7 -0
- package/web/dist/assets/plus-ixZQRprm.js +1 -0
- package/web/dist/assets/{refresh-cw-CG3HaiQT.js → refresh-cw-BfLoHqg5.js} +1 -1
- package/web/dist/assets/rotate-ccw-CRguJya4.js +1 -0
- package/web/dist/assets/route-preview-BspvfNdW.js +1 -0
- package/web/dist/assets/{rule-ai-assistant-panel-CNL1O0GY.js → rule-ai-assistant-panel-CQTsZMJu.js} +2 -2
- package/web/dist/assets/rule-config-DDOeMRXa.js +15 -0
- package/web/dist/assets/rule-overview-panel-CmWdMs4I.js +1 -0
- package/web/dist/assets/{server-Cpj4KNwN.js → server-eYXq3PbE.js} +1 -1
- package/web/dist/assets/settings-panel-aB0ojA9D.js +1 -0
- package/web/dist/assets/{shield-check-OqUmVLQt.js → shield-check-hQXjdfZ7.js} +1 -1
- package/web/dist/assets/smartphone-D4Q7cQfm.js +1 -0
- package/web/dist/assets/switch-DpLnRvOJ.js +1 -0
- package/web/dist/assets/{syntax-highlight-BHsSDUha.js → syntax-highlight-EJCxGikd.js} +1 -1
- package/web/dist/assets/table-2-DvGHhMm4.js +1 -0
- package/web/dist/assets/table-C3mgEjkP.js +1 -0
- package/web/dist/assets/textarea-BjWF0pk4.js +1 -0
- package/web/dist/assets/{wand-sparkles-wkul5vje.js → wand-sparkles-DDGjzjnX.js} +1 -1
- package/web/dist/assets/{wifi-D9M35Ajj.js → wifi-QkTcTrED.js} +1 -1
- package/web/dist/assets/{zap-Djo4cgux.js → zap-CEVoVkAg.js} +1 -1
- package/web/dist/index.html +17 -12
- package/web/dist/assets/alert-Bve02Ufr.js +0 -1
- package/web/dist/assets/arrow-right-B4Mrlj2-.js +0 -1
- package/web/dist/assets/button-BTToMaBW.js +0 -1
- package/web/dist/assets/checkbox-DK9f9Mxu.js +0 -1
- package/web/dist/assets/chevron-down-CPngAx4M.js +0 -1
- package/web/dist/assets/chevron-right-V1DGIDbL.js +0 -1
- package/web/dist/assets/code-5_eJ_XpJ.js +0 -1
- package/web/dist/assets/code-fixer-IRTVs-a6.js +0 -20
- package/web/dist/assets/detail-panel--2r_Fgpi.js +0 -4
- package/web/dist/assets/dist-CxdY_A67.js +0 -5
- package/web/dist/assets/es2015-CfXu88Y7.js +0 -41
- package/web/dist/assets/file-system-access-DK7FI6pN.js +0 -1
- package/web/dist/assets/health-panel-CKt2-Gkj.js +0 -1
- package/web/dist/assets/index-DhzkcDnh.js +0 -18
- package/web/dist/assets/index-PcoCj3ST.css +0 -2
- package/web/dist/assets/mobile-proxy-panel-Dxz-7iYx.js +0 -8
- package/web/dist/assets/mock-config-84LSNP6M.js +0 -1
- package/web/dist/assets/mock-editor-panel-RJ1abekr.js +0 -1
- package/web/dist/assets/monaco-editor-CbL970eS.js +0 -11
- package/web/dist/assets/plugin-code-editor-DfLc4JdP.js +0 -2
- package/web/dist/assets/plugin-config-CT-u4XYw.js +0 -1
- package/web/dist/assets/plugin-generator-CyBK9FuM.js +0 -2
- package/web/dist/assets/plugin-test-dialog-B-NF0sGH.js +0 -7
- package/web/dist/assets/plus-tVxSydJ8.js +0 -1
- package/web/dist/assets/rotate-ccw-CAYM6Vv0.js +0 -1
- package/web/dist/assets/route-preview-C8nW9w7i.js +0 -1
- package/web/dist/assets/rule-config-DXfovZnm.js +0 -15
- package/web/dist/assets/select-DQ6s7CL8.js +0 -1
- package/web/dist/assets/settings-panel-BhXgMr_a.js +0 -1
- package/web/dist/assets/smartphone-DKeDuJTU.js +0 -1
- package/web/dist/assets/switch-C-Xk56TK.js +0 -1
- package/web/dist/assets/table-DymgLlcc.js +0 -1
- package/web/dist/assets/textarea-vuxJwU0x.js +0 -1
- /package/web/dist/assets/{ai-config-store-DQXVJVn_.js → ai-config-store-B_VpGvwx.js} +0 -0
- /package/web/dist/assets/{babel-DsprIpXZ.js → babel-D1quDdNt.js} +0 -0
- /package/web/dist/assets/{clipboard-Dz6MgwV-.js → clipboard-DUgC_tNv.js} +0 -0
- /package/web/dist/assets/{dist-DZJ9SMlJ.js → dist-Bq2klZ3g.js} +0 -0
- /package/web/dist/assets/{estree-BP7k0NIt.js → estree--Vq99DGn.js} +0 -0
- /package/web/dist/assets/{html-6FIWbqxX.js → html-D2PZBPnP.js} +0 -0
- /package/web/dist/assets/{postcss-Dnf30fQU.js → postcss-DfIslWug.js} +0 -0
- /package/web/dist/assets/{standalone-CjxtLKw-.js → standalone-D-fjaAIs.js} +0 -0
|
@@ -36,15 +36,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.MocksFileInvalidError = void 0;
|
|
39
40
|
exports.dedupeMockRulesById = dedupeMockRulesById;
|
|
41
|
+
exports.formatMocksParseError = formatMocksParseError;
|
|
42
|
+
exports.parseMocksFileText = parseMocksFileText;
|
|
43
|
+
exports.findJsonErrorOffset = findJsonErrorOffset;
|
|
44
|
+
exports.resolveMocksWriteTarget = resolveMocksWriteTarget;
|
|
45
|
+
exports.resolveConfiguredMocksPath = resolveConfiguredMocksPath;
|
|
46
|
+
exports.detectLegacyMocksPath = detectLegacyMocksPath;
|
|
47
|
+
exports.formatLegacyMocksNotice = formatLegacyMocksNotice;
|
|
48
|
+
exports.readConfiguredMocksPath = readConfiguredMocksPath;
|
|
40
49
|
exports.createMockHandler = createMockHandler;
|
|
41
50
|
const fs = __importStar(require("fs"));
|
|
42
51
|
const path = __importStar(require("path"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
43
54
|
const debug_1 = __importDefault(require("debug"));
|
|
44
55
|
const map_local_1 = require("./map-local");
|
|
45
56
|
const proxy_record_1 = require("./proxy-record");
|
|
46
57
|
const client_identity_1 = require("./client-identity");
|
|
47
58
|
const body_utils_1 = require("./body-utils");
|
|
59
|
+
const mock_utils_1 = require("./mock-utils");
|
|
48
60
|
const proxyDebug = (0, debug_1.default)('proxy');
|
|
49
61
|
/** 按 id 去重,后出现的条目覆盖先前的(与前端 use-mocks 行为一致) */
|
|
50
62
|
function dedupeMockRulesById(rules) {
|
|
@@ -56,13 +68,256 @@ function dedupeMockRulesById(rules) {
|
|
|
56
68
|
}
|
|
57
69
|
return Array.from(byId.values()).sort((a, b) => a.id - b.id);
|
|
58
70
|
}
|
|
71
|
+
/** Thrown when a write would overwrite a mocks file that failed to parse (issue #120). */
|
|
72
|
+
class MocksFileInvalidError extends Error {
|
|
73
|
+
constructor(filePath, parseError) {
|
|
74
|
+
super(formatMocksParseError(filePath, parseError));
|
|
75
|
+
this.filePath = filePath;
|
|
76
|
+
this.parseError = parseError;
|
|
77
|
+
this.code = 'MOCKS_FILE_INVALID';
|
|
78
|
+
this.name = 'MocksFileInvalidError';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.MocksFileInvalidError = MocksFileInvalidError;
|
|
82
|
+
function formatMocksParseError(filePath, err) {
|
|
83
|
+
const loc = err.line != null ? `第 ${err.line} 行第 ${err.column} 列:` : '';
|
|
84
|
+
return `Mock 文件格式错误,已禁止写入以免覆盖: ${filePath} ${loc}${err.message}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse mocks file text. Issue #120: empty / whitespace-only text is an empty
|
|
88
|
+
* rule list (not an error); anything else that fails JSON.parse returns a
|
|
89
|
+
* parse error with 1-based line/column.
|
|
90
|
+
*/
|
|
91
|
+
function parseMocksFileText(text) {
|
|
92
|
+
if (text.replace(/^\uFEFF/, '').trim() === '')
|
|
93
|
+
return { data: {}, error: null };
|
|
94
|
+
try {
|
|
95
|
+
return { data: JSON.parse(text.replace(/^\uFEFF/, '')), error: null };
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
const message = String(e?.message || e);
|
|
99
|
+
let line = null;
|
|
100
|
+
let column = null;
|
|
101
|
+
const body = text.replace(/^\uFEFF/, '');
|
|
102
|
+
const lc = /\(line (\d+) column (\d+)\)/.exec(message);
|
|
103
|
+
const pos = /at position (\d+)/.exec(message);
|
|
104
|
+
const offset = lc ? null : pos ? Number(pos[1]) : findJsonErrorOffset(body);
|
|
105
|
+
if (lc) {
|
|
106
|
+
line = Number(lc[1]);
|
|
107
|
+
column = Number(lc[2]);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const before = body.slice(0, offset ?? body.length).split('\n');
|
|
111
|
+
line = before.length;
|
|
112
|
+
column = before[before.length - 1].length + 1;
|
|
113
|
+
}
|
|
114
|
+
// Drop V8's quoted source snippet and the "(line x column y)" suffix; position is reported separately.
|
|
115
|
+
const short = message.replace(/\s*\(line \d+ column \d+\)/, '').split(/,\s*(?:\.\.\.)?"/s)[0].replace(/\s+in JSON at position \d+/, '').trim();
|
|
116
|
+
return { data: null, error: { line, column, message: short } };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Offset of the first JSON syntax error (V8 does not always report a
|
|
121
|
+
* position, e.g. "Unexpected token ']', ... is not valid JSON").
|
|
122
|
+
*/
|
|
123
|
+
function findJsonErrorOffset(t) {
|
|
124
|
+
let i = 0;
|
|
125
|
+
const ws = () => { while (i < t.length && ' \t\n\r'.includes(t[i]))
|
|
126
|
+
i++; };
|
|
127
|
+
const fail = () => { throw i; };
|
|
128
|
+
const str = () => {
|
|
129
|
+
i++;
|
|
130
|
+
while (i < t.length && t[i] !== '"') {
|
|
131
|
+
if (t[i] === '\\')
|
|
132
|
+
i++;
|
|
133
|
+
else if (t.charCodeAt(i) < 0x20)
|
|
134
|
+
fail();
|
|
135
|
+
i++;
|
|
136
|
+
}
|
|
137
|
+
if (i >= t.length)
|
|
138
|
+
fail();
|
|
139
|
+
i++;
|
|
140
|
+
};
|
|
141
|
+
const value = () => {
|
|
142
|
+
ws();
|
|
143
|
+
const c = t[i];
|
|
144
|
+
if (c === '{') {
|
|
145
|
+
i++;
|
|
146
|
+
ws();
|
|
147
|
+
if (t[i] === '}') {
|
|
148
|
+
i++;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
for (;;) {
|
|
152
|
+
ws();
|
|
153
|
+
if (t[i] !== '"')
|
|
154
|
+
fail();
|
|
155
|
+
str();
|
|
156
|
+
ws();
|
|
157
|
+
if (t[i] !== ':')
|
|
158
|
+
fail();
|
|
159
|
+
i++;
|
|
160
|
+
value();
|
|
161
|
+
ws();
|
|
162
|
+
if (t[i] === ',') {
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (t[i] === '}') {
|
|
167
|
+
i++;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
fail();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else if (c === '[') {
|
|
174
|
+
i++;
|
|
175
|
+
ws();
|
|
176
|
+
if (t[i] === ']') {
|
|
177
|
+
i++;
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
for (;;) {
|
|
181
|
+
value();
|
|
182
|
+
ws();
|
|
183
|
+
if (t[i] === ',') {
|
|
184
|
+
i++;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (t[i] === ']') {
|
|
188
|
+
i++;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
fail();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (c === '"')
|
|
195
|
+
str();
|
|
196
|
+
else if (/[-0-9]/.test(c || '')) {
|
|
197
|
+
const m = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/.exec(t.slice(i));
|
|
198
|
+
if (!m)
|
|
199
|
+
fail();
|
|
200
|
+
i += m[0].length;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
for (const w of ['true', 'false', 'null'])
|
|
204
|
+
if (t.startsWith(w, i)) {
|
|
205
|
+
i += w.length;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
fail();
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
value();
|
|
213
|
+
ws();
|
|
214
|
+
return i < t.length ? i : t.length;
|
|
215
|
+
}
|
|
216
|
+
catch (at) {
|
|
217
|
+
return typeof at === 'number' ? Math.min(at, t.length) : t.length;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Issue #123: the real file a write to `file` must replace. Symlinks are
|
|
222
|
+
* followed so the link itself is never replaced. A dangling link (target
|
|
223
|
+
* missing) creates the target at the link destination when its directory
|
|
224
|
+
* exists, otherwise throws a clear error (the link is left untouched).
|
|
225
|
+
*/
|
|
226
|
+
function resolveMocksWriteTarget(file) {
|
|
227
|
+
try {
|
|
228
|
+
return fs.realpathSync(file);
|
|
229
|
+
}
|
|
230
|
+
catch (_) { /* missing or dangling */ }
|
|
231
|
+
let cur = path.resolve(file);
|
|
232
|
+
for (let hops = 0; hops < 40; hops++) {
|
|
233
|
+
let st;
|
|
234
|
+
try {
|
|
235
|
+
st = fs.lstatSync(cur);
|
|
236
|
+
}
|
|
237
|
+
catch (_) {
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
if (!st.isSymbolicLink())
|
|
241
|
+
break;
|
|
242
|
+
cur = path.resolve(path.dirname(cur), fs.readlinkSync(cur));
|
|
243
|
+
}
|
|
244
|
+
if (cur !== path.resolve(file)) {
|
|
245
|
+
const parent = path.dirname(cur);
|
|
246
|
+
let parentReal;
|
|
247
|
+
try {
|
|
248
|
+
parentReal = fs.realpathSync(parent);
|
|
249
|
+
}
|
|
250
|
+
catch (_) {
|
|
251
|
+
throw new Error(`Mock 文件是指向不存在位置的符号链接,无法写入: ${file} -> ${cur}(目录 ${parent} 不存在)`);
|
|
252
|
+
}
|
|
253
|
+
return path.join(parentReal, path.basename(cur));
|
|
254
|
+
}
|
|
255
|
+
return cur;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Issue #121: turn the configured `mocksFilePath` into an absolute path.
|
|
259
|
+
* `~` / `~/…` expand to the user's home directory; a relative path is
|
|
260
|
+
* resolved against the meddle directory (MEDDLE_HOME), not the directory the
|
|
261
|
+
* proxy happened to be started from.
|
|
262
|
+
*/
|
|
263
|
+
function resolveConfiguredMocksPath(raw, meddleDir, homeDir = os.homedir()) {
|
|
264
|
+
const v = typeof raw === 'string' ? raw.trim() : '';
|
|
265
|
+
if (!v)
|
|
266
|
+
return null;
|
|
267
|
+
let p = v;
|
|
268
|
+
if (p === '~')
|
|
269
|
+
p = homeDir;
|
|
270
|
+
else if (p.startsWith('~/') || p.startsWith('~\\'))
|
|
271
|
+
p = path.join(homeDir, p.slice(2));
|
|
272
|
+
return path.resolve(meddleDir, p);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Issue #121 migration notice: a relative `mocksFilePath` used to resolve
|
|
276
|
+
* against the proxy's start directory. If the file exists there but not
|
|
277
|
+
* under the new rule (MEDDLE_HOME), report both paths so the user can switch
|
|
278
|
+
* to the old absolute path. Never migrates or rewrites settings.
|
|
279
|
+
*/
|
|
280
|
+
function detectLegacyMocksPath(raw, meddleDir, cwd = process.cwd(), homeDir = os.homedir()) {
|
|
281
|
+
const v = typeof raw === 'string' ? raw.trim() : '';
|
|
282
|
+
if (!v || path.isAbsolute(v) || v === '~' || v.startsWith('~/') || v.startsWith('~\\'))
|
|
283
|
+
return null;
|
|
284
|
+
const newPath = resolveConfiguredMocksPath(v, meddleDir, homeDir);
|
|
285
|
+
const oldPath = path.resolve(cwd, v);
|
|
286
|
+
if (!newPath || oldPath === newPath)
|
|
287
|
+
return null;
|
|
288
|
+
const isFile = (p) => { try {
|
|
289
|
+
return fs.statSync(p).isFile();
|
|
290
|
+
}
|
|
291
|
+
catch (_) {
|
|
292
|
+
return false;
|
|
293
|
+
} };
|
|
294
|
+
if (!isFile(oldPath) || isFile(newPath))
|
|
295
|
+
return null;
|
|
296
|
+
return { configured: v, oldPath, newPath };
|
|
297
|
+
}
|
|
298
|
+
function formatLegacyMocksNotice(l) {
|
|
299
|
+
return `Mock 文件路径「${l.configured}」现在相对于 Meddle 配置目录解析为 ${l.newPath}(不存在);` +
|
|
300
|
+
`按旧规则(相对于启动目录)找到的文件是 ${l.oldPath}。如需继续使用它,请把路径改为该绝对路径。`;
|
|
301
|
+
}
|
|
302
|
+
/** Read the raw (unresolved) mocksFilePath from settings.json. */
|
|
303
|
+
function readConfiguredMocksPath(settingsPath) {
|
|
304
|
+
try {
|
|
305
|
+
if (!fs.existsSync(settingsPath))
|
|
306
|
+
return null;
|
|
307
|
+
const v = JSON.parse(fs.readFileSync(settingsPath, 'utf8')).mocksFilePath;
|
|
308
|
+
return typeof v === 'string' ? v : null;
|
|
309
|
+
}
|
|
310
|
+
catch (_) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
59
314
|
function createMockHandler(ctx) {
|
|
60
315
|
const DEFAULT_MOCK_FILE = path.join(ctx.meddleDir, 'mocks.json');
|
|
61
316
|
function loadCustomPathsFromSettings() {
|
|
62
317
|
try {
|
|
63
318
|
if (fs.existsSync(ctx.settingsPath)) {
|
|
64
319
|
const settings = JSON.parse(fs.readFileSync(ctx.settingsPath, 'utf8'));
|
|
65
|
-
return { mocksFilePath: settings.mocksFilePath
|
|
320
|
+
return { mocksFilePath: resolveConfiguredMocksPath(settings.mocksFilePath, ctx.meddleDir) };
|
|
66
321
|
}
|
|
67
322
|
}
|
|
68
323
|
catch (error) {
|
|
@@ -70,45 +325,165 @@ function createMockHandler(ctx) {
|
|
|
70
325
|
}
|
|
71
326
|
return { mocksFilePath: null };
|
|
72
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Effective mocks file. Issue #118: a configured `mocksFilePath` is honoured
|
|
330
|
+
* even when the file does not exist yet (same as the CLI file mode) — the
|
|
331
|
+
* proxy starts with an empty rule list and the file is created on the first
|
|
332
|
+
* save. Previously a missing custom file silently fell back to the default
|
|
333
|
+
* file, so "switching" to a new file kept reading/writing the old one.
|
|
334
|
+
*/
|
|
73
335
|
function getMockFilePath() {
|
|
74
336
|
if (ctx.currentMocksPath)
|
|
75
337
|
return ctx.currentMocksPath;
|
|
76
338
|
const customPaths = loadCustomPathsFromSettings();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
339
|
+
ctx.currentMocksPath = customPaths.mocksFilePath || DEFAULT_MOCK_FILE;
|
|
340
|
+
return ctx.currentMocksPath;
|
|
341
|
+
}
|
|
342
|
+
/** Path whose contents are currently in ctx.mockRules (null before first load). */
|
|
343
|
+
let loadedMocksPath = null;
|
|
344
|
+
/** Issue #120: parse failure of the loaded file; while set, every write is refused. */
|
|
345
|
+
let parseError = null;
|
|
346
|
+
function getMocksParseError() { return parseError; }
|
|
347
|
+
/**
|
|
348
|
+
* Issue #123 (CI flake root cause): chokidar does not report writes that
|
|
349
|
+
* land while it is still setting up (ignoreInitial). Remember what was
|
|
350
|
+
* loaded so the watcher can re-check once it is ready.
|
|
351
|
+
*/
|
|
352
|
+
let loadedSig = null;
|
|
353
|
+
function fileSig(file) {
|
|
354
|
+
try {
|
|
355
|
+
const st = fs.statSync(file);
|
|
356
|
+
return `${st.ino}:${st.size}:${st.mtimeMs}`;
|
|
357
|
+
}
|
|
358
|
+
catch (_) {
|
|
359
|
+
return 'missing';
|
|
80
360
|
}
|
|
81
|
-
ctx.currentMocksPath = DEFAULT_MOCK_FILE;
|
|
82
|
-
return DEFAULT_MOCK_FILE;
|
|
83
361
|
}
|
|
84
362
|
function loadMockRules() {
|
|
363
|
+
const mockFile = getMockFilePath();
|
|
364
|
+
// Issue #118: switching to another file must not inherit the old
|
|
365
|
+
// file's id counter or rules; re-reading the same file keeps the
|
|
366
|
+
// counter monotonic (issue #115).
|
|
367
|
+
const switched = loadedMocksPath !== null && loadedMocksPath !== mockFile;
|
|
368
|
+
const firstLoadOfThisFile = loadedMocksPath !== mockFile;
|
|
369
|
+
if (switched) {
|
|
370
|
+
proxyDebug(`Mock 文件切换: ${loadedMocksPath} -> ${mockFile}`);
|
|
371
|
+
ctx.mockIdSeq = 1;
|
|
372
|
+
}
|
|
373
|
+
loadedMocksPath = mockFile;
|
|
374
|
+
if (watcherState && watcherState.file !== mockFile)
|
|
375
|
+
startWatcher(mockFile);
|
|
85
376
|
try {
|
|
86
|
-
const mockFile = getMockFilePath();
|
|
87
377
|
if (fs.existsSync(mockFile)) {
|
|
88
|
-
|
|
378
|
+
loadedSig = fileSig(mockFile);
|
|
379
|
+
const parsed = parseMocksFileText(fs.readFileSync(mockFile, 'utf8'));
|
|
380
|
+
if (parsed.error) {
|
|
381
|
+
// Issue #120: never treat a broken file as empty-and-writable.
|
|
382
|
+
const wasSame = parseError && parseError.message === parsed.error.message && parseError.line === parsed.error.line;
|
|
383
|
+
parseError = parsed.error;
|
|
384
|
+
ctx.mockRules = [];
|
|
385
|
+
if (!wasSame)
|
|
386
|
+
console.error(formatMocksParseError(mockFile, parsed.error));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (parseError)
|
|
390
|
+
console.log(`Mock 文件已修复,恢复正常: ${mockFile}`);
|
|
391
|
+
parseError = null;
|
|
392
|
+
const data = parsed.data || {};
|
|
89
393
|
const loaded = Array.isArray(data.rules) ? data.rules : [];
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
394
|
+
// Issue #115: never drop rules that share an id — give the
|
|
395
|
+
// later duplicates fresh ids instead (no silent data loss).
|
|
396
|
+
const { rules, reassigned } = (0, mock_utils_1.ensureUniqueMockRuleIds)(loaded);
|
|
397
|
+
ctx.mockRules = rules;
|
|
398
|
+
if (reassigned > 0) {
|
|
399
|
+
console.warn(`Mock 规则 id 重复或缺失,已为 ${reassigned} 条规则重新分配 id (${mockFile})`);
|
|
93
400
|
}
|
|
94
|
-
|
|
401
|
+
// nextId is the next id to hand out; a stale/missing value must
|
|
402
|
+
// never collide with ids already on disk (issue #115).
|
|
403
|
+
ctx.mockIdSeq = (0, mock_utils_1.nextMockRuleId)(ctx.mockRules, Math.max(Number(data.nextId) || 0, (Number(data.lastId) || 0) + 1, ctx.mockIdSeq || 0));
|
|
95
404
|
proxyDebug(`已加载 ${ctx.mockRules.length} 条 Mock 规则 (${mockFile})`);
|
|
96
405
|
}
|
|
406
|
+
else {
|
|
407
|
+
// File removed / never created — clear in-memory rules (watcher
|
|
408
|
+
// unlink, or a configured path that does not exist yet). The
|
|
409
|
+
// file is created on the first save; nothing else is touched.
|
|
410
|
+
ctx.mockRules = [];
|
|
411
|
+
parseError = null;
|
|
412
|
+
loadedSig = 'missing';
|
|
413
|
+
if (firstLoadOfThisFile && mockFile !== DEFAULT_MOCK_FILE) {
|
|
414
|
+
console.warn(`Mock 规则文件不存在,将以空列表开始,首次保存时创建: ${mockFile}`);
|
|
415
|
+
}
|
|
416
|
+
proxyDebug(`Mock 规则文件不存在,已清空内存规则 (${mockFile})`);
|
|
417
|
+
}
|
|
97
418
|
}
|
|
98
419
|
catch (err) {
|
|
99
420
|
console.error('加载 mock 规则失败:', err.message);
|
|
100
421
|
ctx.mockRules = [];
|
|
101
422
|
}
|
|
102
423
|
}
|
|
424
|
+
function assertMocksWritable() {
|
|
425
|
+
const mockFile = getMockFilePath();
|
|
426
|
+
// Re-validate what is on disk right before writing: the file may have
|
|
427
|
+
// been broken after the last load (watcher not fired yet, stale tab).
|
|
428
|
+
if (!parseError && fs.existsSync(mockFile)) {
|
|
429
|
+
try {
|
|
430
|
+
const parsed = parseMocksFileText(fs.readFileSync(mockFile, 'utf8'));
|
|
431
|
+
if (parsed.error) {
|
|
432
|
+
parseError = parsed.error;
|
|
433
|
+
console.error(formatMocksParseError(mockFile, parsed.error));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch (_) { /* unreadable: let the write report it */ }
|
|
437
|
+
}
|
|
438
|
+
if (parseError)
|
|
439
|
+
throw new MocksFileInvalidError(mockFile, parseError);
|
|
440
|
+
}
|
|
103
441
|
function saveMockRules() {
|
|
442
|
+
// Issue #120: single choke point for every write — refuse to overwrite a broken file.
|
|
443
|
+
assertMocksWritable();
|
|
104
444
|
try {
|
|
105
|
-
|
|
445
|
+
// Issue #115: keep every rule (re-id duplicates instead of dropping
|
|
446
|
+
// them) and persist a sequence that is above every stored id.
|
|
447
|
+
ctx.mockRules = (0, mock_utils_1.ensureUniqueMockRuleIds)(ctx.mockRules).rules;
|
|
448
|
+
ctx.mockIdSeq = (0, mock_utils_1.nextMockRuleId)(ctx.mockRules, ctx.mockIdSeq);
|
|
106
449
|
const mockFile = getMockFilePath();
|
|
107
|
-
|
|
450
|
+
const dir = path.dirname(mockFile);
|
|
451
|
+
if (!fs.existsSync(dir))
|
|
452
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
453
|
+
// Issue #123: write next to the real file so a symlinked mocks file
|
|
454
|
+
// (e.g. into a dotfiles repo) stays a symlink, and keep its mode/owner.
|
|
455
|
+
const target = resolveMocksWriteTarget(mockFile);
|
|
456
|
+
let existing = null;
|
|
457
|
+
try {
|
|
458
|
+
existing = fs.statSync(target);
|
|
459
|
+
}
|
|
460
|
+
catch (_) { /* new file */ }
|
|
461
|
+
// Write to a temp file then rename, so a crash never leaves half a file (issue #120).
|
|
462
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
463
|
+
fs.writeFileSync(tmp, JSON.stringify({ nextId: ctx.mockIdSeq, rules: ctx.mockRules }, null, 2), { encoding: 'utf8', mode: existing ? existing.mode & 0o7777 : 0o666 });
|
|
464
|
+
try {
|
|
465
|
+
if (existing) {
|
|
466
|
+
fs.chmodSync(tmp, existing.mode & 0o7777);
|
|
467
|
+
try {
|
|
468
|
+
fs.chownSync(tmp, existing.uid, existing.gid);
|
|
469
|
+
}
|
|
470
|
+
catch (_) { /* not permitted: best effort */ }
|
|
471
|
+
}
|
|
472
|
+
fs.renameSync(tmp, target);
|
|
473
|
+
loadedSig = fileSig(mockFile);
|
|
474
|
+
}
|
|
475
|
+
catch (e) {
|
|
476
|
+
try {
|
|
477
|
+
fs.unlinkSync(tmp);
|
|
478
|
+
}
|
|
479
|
+
catch (_) { /* ignore */ }
|
|
480
|
+
throw e;
|
|
481
|
+
}
|
|
108
482
|
proxyDebug(`Mock 规则已保存到 ${mockFile}`);
|
|
109
483
|
}
|
|
110
484
|
catch (err) {
|
|
111
485
|
console.error('保存 mock 规则失败:', err.message);
|
|
486
|
+
throw err;
|
|
112
487
|
}
|
|
113
488
|
}
|
|
114
489
|
function matchMockRule(url, method) {
|
|
@@ -117,25 +492,23 @@ function createMockHandler(ctx) {
|
|
|
117
492
|
return false;
|
|
118
493
|
if (rule.method && rule.method !== '*' && rule.method.toUpperCase() !== method.toUpperCase())
|
|
119
494
|
return false;
|
|
120
|
-
|
|
121
|
-
return
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
495
|
+
if (!(0, mock_utils_1.matchUrlPattern)(url, rule.urlPattern))
|
|
496
|
+
return false;
|
|
497
|
+
if (!(0, mock_utils_1.matchQueryCondition)(url, rule.query))
|
|
498
|
+
return false;
|
|
499
|
+
return true;
|
|
126
500
|
}) || null;
|
|
127
501
|
}
|
|
128
|
-
function buildMockResponseForTest(rule) {
|
|
502
|
+
function buildMockResponseForTest(rule, requestHeaders) {
|
|
129
503
|
const statusCode = rule.statusCode || 200;
|
|
130
504
|
const ruleHeaders = {};
|
|
131
505
|
for (const [key, value] of Object.entries(rule.headers || {})) {
|
|
132
506
|
ruleHeaders[key.toLowerCase()] = value;
|
|
133
507
|
}
|
|
134
|
-
const headers = {
|
|
508
|
+
const headers = (0, mock_utils_1.resolveHeaderPlaceholders)((0, mock_utils_1.mergeResponseHeaders)({
|
|
135
509
|
'x-mock-rule': encodeURIComponent(rule.name || rule.id?.toString() || rule.urlPattern || ''),
|
|
136
|
-
'content-type':
|
|
137
|
-
|
|
138
|
-
};
|
|
510
|
+
'content-type': 'application/json',
|
|
511
|
+
}, ruleHeaders), requestHeaders);
|
|
139
512
|
let body = '';
|
|
140
513
|
if (rule.bodyType === 'file' && rule.body) {
|
|
141
514
|
let filePath = rule.body.replace(/^file:\/\//, '').replace(/^\/[A-Za-z]:\//, '');
|
|
@@ -171,13 +544,12 @@ function createMockHandler(ctx) {
|
|
|
171
544
|
for (const [key, value] of Object.entries(rule.headers || {})) {
|
|
172
545
|
ruleHeaders[key.toLowerCase()] = value;
|
|
173
546
|
}
|
|
174
|
-
const responseHeaders = {
|
|
547
|
+
const responseHeaders = (0, mock_utils_1.resolveHeaderPlaceholders)((0, mock_utils_1.mergeResponseHeaders)({
|
|
175
548
|
'X-Mock-Rule': encodeURIComponent(mockRuleName),
|
|
176
549
|
'Access-Control-Allow-Origin': '*',
|
|
177
550
|
'Access-Control-Allow-Methods': '*',
|
|
178
551
|
'Access-Control-Allow-Headers': '*',
|
|
179
|
-
|
|
180
|
-
};
|
|
552
|
+
}, ruleHeaders), req.headers);
|
|
181
553
|
let finalStatusCode = statusCode;
|
|
182
554
|
let statusMessage = 'OK (Mock)';
|
|
183
555
|
if (isFileBody) {
|
|
@@ -338,9 +710,133 @@ function createMockHandler(ctx) {
|
|
|
338
710
|
doSend();
|
|
339
711
|
}
|
|
340
712
|
}
|
|
713
|
+
/** Active file watcher (issue #118: follows the effective mocks file). */
|
|
714
|
+
let watcherState = null;
|
|
715
|
+
let watcherOnReload;
|
|
716
|
+
function startWatcher(mockFile) {
|
|
717
|
+
if (watcherState) {
|
|
718
|
+
watcherState.close();
|
|
719
|
+
proxyDebug(`stopped watching mocks file: ${watcherState.file}`);
|
|
720
|
+
}
|
|
721
|
+
const dir = path.dirname(mockFile);
|
|
722
|
+
const base = path.basename(mockFile);
|
|
723
|
+
try {
|
|
724
|
+
if (!fs.existsSync(dir))
|
|
725
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
726
|
+
}
|
|
727
|
+
catch (_) { /* ignore */ }
|
|
728
|
+
const debugWatch = process.env.MEDDLE_DEBUG_WATCH === '1';
|
|
729
|
+
const t0 = Date.now();
|
|
730
|
+
const dbg = (msg) => { if (debugWatch)
|
|
731
|
+
console.error(`[watch +${Date.now() - t0}ms] ${msg}`); };
|
|
732
|
+
// Issue #123: when the mocks path is a symlink, edits land on the real
|
|
733
|
+
// target (git pull in a dotfiles repo, fixing JSON there), so watch the
|
|
734
|
+
// target's directory too and accept events for either path.
|
|
735
|
+
const resolveTarget = () => {
|
|
736
|
+
try {
|
|
737
|
+
return resolveMocksWriteTarget(mockFile);
|
|
738
|
+
}
|
|
739
|
+
catch (_) {
|
|
740
|
+
return path.resolve(mockFile);
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
let target = resolveTarget();
|
|
744
|
+
let targetDir = null;
|
|
745
|
+
// Watch the directory (not only the file) so atomic replace / recreate
|
|
746
|
+
// still triggers — same pattern as route-rules watcher.
|
|
747
|
+
const initialTargetDir = path.dirname(target);
|
|
748
|
+
targetDir = path.resolve(initialTargetDir) !== path.resolve(dir) && fs.existsSync(initialTargetDir) ? initialTargetDir : null;
|
|
749
|
+
// Both directories go into the initial watch so they are ready together.
|
|
750
|
+
const watcher = chokidar_1.default.watch(targetDir ? [dir, targetDir] : dir, {
|
|
751
|
+
ignoreInitial: true,
|
|
752
|
+
depth: 0,
|
|
753
|
+
// Follow the symlink target explicitly (below) instead of relying on
|
|
754
|
+
// chokidar's implicit following, which differs per platform backend.
|
|
755
|
+
followSymlinks: false,
|
|
756
|
+
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 },
|
|
757
|
+
});
|
|
758
|
+
// Catch up on a write chokidar did not report because it landed while a
|
|
759
|
+
// directory watch was still being set up (ignoreInitial).
|
|
760
|
+
const catchUp = (why) => {
|
|
761
|
+
if (closed || !watcherState || watcherState.file !== mockFile)
|
|
762
|
+
return;
|
|
763
|
+
if (fileSig(mockFile) !== loadedSig)
|
|
764
|
+
reload(why);
|
|
765
|
+
};
|
|
766
|
+
const watchTargetDir = () => {
|
|
767
|
+
const want = path.dirname(target);
|
|
768
|
+
const next = path.resolve(want) === path.resolve(dir) ? null : want;
|
|
769
|
+
if (next === targetDir)
|
|
770
|
+
return;
|
|
771
|
+
if (targetDir)
|
|
772
|
+
watcher.unwatch(targetDir);
|
|
773
|
+
targetDir = next;
|
|
774
|
+
if (targetDir && fs.existsSync(targetDir)) {
|
|
775
|
+
watcher.add(targetDir);
|
|
776
|
+
// chokidar emits no 'ready' for add(); re-check a few times
|
|
777
|
+
// while the new directory watch comes up.
|
|
778
|
+
for (const ms of [100, 300, 1000]) {
|
|
779
|
+
setTimeout(() => catchUp('changed while target watch was starting'), ms).unref?.();
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
dbg(`target ${target}; extra dir ${targetDir ?? '(none)'}`);
|
|
783
|
+
};
|
|
784
|
+
watchTargetDir();
|
|
785
|
+
let closed = false;
|
|
786
|
+
const reload = (event, changedPath) => {
|
|
787
|
+
// Ignore late events from a watcher that was replaced, and any
|
|
788
|
+
// file in the directory other than the active mocks file / its target.
|
|
789
|
+
if (closed || !watcherState || watcherState.file !== mockFile)
|
|
790
|
+
return;
|
|
791
|
+
if (changedPath) {
|
|
792
|
+
const p = path.resolve(changedPath);
|
|
793
|
+
if (p !== path.resolve(mockFile) && p !== target)
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
// The link itself may have been retargeted: follow the new target.
|
|
797
|
+
const nextTarget = resolveTarget();
|
|
798
|
+
if (nextTarget !== target) {
|
|
799
|
+
target = nextTarget;
|
|
800
|
+
watchTargetDir();
|
|
801
|
+
}
|
|
802
|
+
proxyDebug(`mocks file ${event}: ${base}`);
|
|
803
|
+
loadMockRules();
|
|
804
|
+
if (watcherOnReload)
|
|
805
|
+
watcherOnReload();
|
|
806
|
+
};
|
|
807
|
+
if (debugWatch) {
|
|
808
|
+
watcher.on('all', (ev, p) => dbg(`${ev} ${p}`));
|
|
809
|
+
}
|
|
810
|
+
watcher.on('ready', () => {
|
|
811
|
+
dbg('ready');
|
|
812
|
+
catchUp('changed before watcher ready');
|
|
813
|
+
});
|
|
814
|
+
watcher.on('change', (p) => reload('changed', p));
|
|
815
|
+
watcher.on('add', (p) => reload('added', p));
|
|
816
|
+
watcher.on('unlink', (p) => reload('removed', p));
|
|
817
|
+
watcherState = {
|
|
818
|
+
file: mockFile,
|
|
819
|
+
close: () => {
|
|
820
|
+
closed = true;
|
|
821
|
+
watcher.close().catch(() => { });
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
proxyDebug(`watching mocks file: ${mockFile}`);
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Watch the mocks file for external edits (CLI file-mode / hand edits).
|
|
828
|
+
* Aligns with route-rules watcher so file-only writes still hot-reload (issue #85).
|
|
829
|
+
* When the effective mocks file changes (issue #118), loadMockRules moves
|
|
830
|
+
* the watcher to the new file automatically.
|
|
831
|
+
*/
|
|
832
|
+
function initMockFileWatcher(onReload) {
|
|
833
|
+
watcherOnReload = onReload;
|
|
834
|
+
startWatcher(getMockFilePath());
|
|
835
|
+
}
|
|
341
836
|
return {
|
|
342
|
-
getMockFilePath, loadMockRules, saveMockRules, matchMockRule,
|
|
837
|
+
getMockFilePath, loadMockRules, saveMockRules, assertMocksWritable, getMocksParseError, matchMockRule,
|
|
343
838
|
buildMockResponseForTest, sendMockResponse, loadCustomPathsFromSettings,
|
|
839
|
+
initMockFileWatcher,
|
|
344
840
|
};
|
|
345
841
|
}
|
|
346
842
|
//# sourceMappingURL=mock-handler.js.map
|