Skip to content

Commit 52eb1f7

Browse files
committed
2 parents 0efe1a8 + 1e3928a commit 52eb1f7

5 files changed

Lines changed: 119 additions & 13 deletions

File tree

bds-tools/is-main.test.mjs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* is-main 契约测试:直接入口 / shim 入口 / 被异名入口 import
3+
* 运行: npm test -w @sfmc-bds/bds-tools(需先 build)
4+
*
5+
* 注:仓库根 .gitignore 忽略名为 test 的路径,故用例放在包根而非 test/。
6+
*/
7+
8+
import assert from "node:assert/strict";
9+
import { spawnSync } from "node:child_process";
10+
import fs from "node:fs";
11+
import path from "node:path";
12+
import { after, describe, it } from "node:test";
13+
import { fileURLToPath } from "node:url";
14+
15+
const pkgRoot = path.dirname(fileURLToPath(import.meta.url));
16+
const distDir = path.join(pkgRoot, "dist");
17+
18+
/** 与 recovery.js → dist/recovery.js 同构:根目录 shim 与 dist 模块同名 */
19+
const MODULE_NAME = "_sfmc_is_main_fixture.js";
20+
const distModule = path.join(distDir, MODULE_NAME);
21+
const rootShim = path.join(pkgRoot, MODULE_NAME);
22+
const alienEntry = path.join(pkgRoot, "_sfmc_alien_entry.js");
23+
24+
const moduleSrc = `
25+
import { isMainModule } from "./is-main.js";
26+
console.log(isMainModule(import.meta.url) ? "MAIN" : "NOT_MAIN");
27+
`;
28+
29+
const shimSrc = `
30+
import path from "node:path";
31+
import { fileURLToPath, pathToFileURL } from "node:url";
32+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
33+
await import(pathToFileURL(path.join(__dirname, "dist", ${JSON.stringify(MODULE_NAME)})).href);
34+
`;
35+
36+
const alienSrc = `
37+
import path from "node:path";
38+
import { fileURLToPath, pathToFileURL } from "node:url";
39+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
40+
await import(pathToFileURL(path.join(__dirname, "dist", ${JSON.stringify(MODULE_NAME)})).href);
41+
`;
42+
43+
describe("isMainModule", () => {
44+
it("dist 直接作为 argv 入口时为 true", () => {
45+
fs.mkdirSync(distDir, { recursive: true });
46+
fs.writeFileSync(distModule, moduleSrc);
47+
const r = spawnSync(process.execPath, [distModule], { encoding: "utf8" });
48+
assert.equal(r.status, 0, r.stderr);
49+
assert.equal(r.stdout.trim(), "MAIN");
50+
});
51+
52+
it("同名根目录 shim 动态 import dist 时为 true(recovery.js 契约)", () => {
53+
fs.mkdirSync(distDir, { recursive: true });
54+
fs.writeFileSync(distModule, moduleSrc);
55+
fs.writeFileSync(rootShim, shimSrc);
56+
const r = spawnSync(process.execPath, [rootShim], { encoding: "utf8" });
57+
assert.equal(r.status, 0, r.stderr);
58+
assert.equal(r.stdout.trim(), "MAIN");
59+
});
60+
61+
it("被异名入口 import 时为 false(check-update 加载 bds-manager)", () => {
62+
fs.mkdirSync(distDir, { recursive: true });
63+
fs.writeFileSync(distModule, moduleSrc);
64+
fs.writeFileSync(alienEntry, alienSrc);
65+
const r = spawnSync(process.execPath, [alienEntry], { encoding: "utf8" });
66+
assert.equal(r.status, 0, r.stderr);
67+
assert.equal(r.stdout.trim(), "NOT_MAIN");
68+
});
69+
70+
after(() => {
71+
for (const p of [distModule, rootShim, alienEntry]) {
72+
try {
73+
fs.unlinkSync(p);
74+
} catch {
75+
/* ignore */
76+
}
77+
}
78+
});
79+
});

bds-tools/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"dev": "tsx src/dist/bds-manager.ts",
4242
"typecheck": "tsc --noEmit",
4343
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
44+
"test": "node --test is-main.test.mjs",
4445
"update": "node dist/check-update.js",
4546
"update:check": "node dist/check-update.js --check-only",
4647
"update:force": "node dist/check-update.js --force",

bds-tools/recovery.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
/**
2-
* recovery.js — shim, 委托给 src/recovery.ts 编译产物
2+
* recovery.js — shim, 委托给 dist/recovery.js
33
*
44
* 用法: node recovery.js
5+
*
6+
* 注意:bds-tools 为 ESM("type":"module"),不能再用 createRequire 加载 dist。
57
*/
68

7-
import { createRequire } from "node:module";
89
import path from "node:path";
9-
import { fileURLToPath } from "node:url";
10+
import { fileURLToPath, pathToFileURL } from "node:url";
1011

1112
const __dirname = path.dirname(fileURLToPath(import.meta.url));
12-
const require = createRequire(import.meta.url);
13-
const dist = path.join(__dirname, "dist", "recovery.js");
13+
const distUrl = pathToFileURL(path.join(__dirname, "dist", "recovery.js")).href;
1414

1515
try {
16-
require(dist);
16+
await import(distUrl);
1717
} catch (e) {
18-
if (e.code === "MODULE_NOT_FOUND") {
18+
const err = /** @type {NodeJS.ErrnoException} */ (e);
19+
if (err && (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND")) {
1920
console.error("[BDSRecovery] dist/ 未找到,请先运行 `npm run build`");
2021
process.exit(2);
2122
}
2223
throw e;
23-
}
24+
}

bds-tools/src/bds-manager.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ bdsEvents_enabled();
275275

276276
/** 判断当前是否作为 CLI 主入口运行(被 check-update 等 import 时为 false) */
277277
function isMain(): boolean {
278+
// sfmc supervisor 通过 SFMC_SERVICE 拉起子进程时优先判定(与 check-update 对称)
279+
if (process.env.SFMC_SERVICE === "manager") return true;
278280
return isMainModule(import.meta.url);
279281
}
280282

bds-tools/src/is-main.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,34 @@
33
*
44
* package.json 声明 "type": "module" 后,`require` 不存在,
55
* 不能再用 `require.main === module`。
6+
*
7+
* 契约(与历史 endsWith 行为对齐,满足 LSP):
8+
* 1. process.argv[1] 与本文件 realpath 相同 → true
9+
* 2. 根目录 shim(如 recovery.js → dist/recovery.js)同 stem → true
10+
* 3. 被其他入口 import 时(argv stem 不同)→ false
611
*/
712

13+
import fs from "node:fs";
814
import path from "node:path";
915
import { fileURLToPath } from "node:url";
1016

17+
function normPath(p: string): string {
18+
return process.platform === "win32" ? p.toLowerCase() : p;
19+
}
20+
21+
/** 去掉 .js/.ts/.mjs/.cjs 等扩展,得到入口 stem */
22+
function entryStem(filePath: string): string {
23+
return path.basename(filePath).replace(/\.(c|m)?(js|ts)$/i, "");
24+
}
25+
26+
function tryRealpath(filePath: string): string {
27+
try {
28+
return fs.realpathSync.native(filePath);
29+
} catch {
30+
return filePath;
31+
}
32+
}
33+
1134
/**
1235
* 判断 metaUrl 对应模块是否为 node 进程主入口。
1336
* 替代 CommonJS 的 `require.main === module`。
@@ -16,11 +39,11 @@ export function isMainModule(metaUrl: string): boolean {
1639
const entry = process.argv[1];
1740
if (!entry) return false;
1841
try {
19-
const thisFile = path.resolve(fileURLToPath(metaUrl));
20-
const entryFile = path.resolve(entry);
21-
return process.platform === "win32"
22-
? thisFile.toLowerCase() === entryFile.toLowerCase()
23-
: thisFile === entryFile;
42+
const thisFile = tryRealpath(path.resolve(fileURLToPath(metaUrl)));
43+
const entryFile = tryRealpath(path.resolve(entry));
44+
if (normPath(thisFile) === normPath(entryFile)) return true;
45+
// shim 兼容:入口与本模块同 stem(recovery.js 加载 dist/recovery.js)
46+
return normPath(entryStem(thisFile)) === normPath(entryStem(entryFile));
2447
} catch {
2548
return false;
2649
}

0 commit comments

Comments
 (0)