Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions bds-tools/is-main.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* is-main 契约测试:直接入口 / shim 入口 / 被异名入口 import
* 运行: npm test -w @sfmc-bds/bds-tools(需先 build)
*
* 注:仓库根 .gitignore 忽略名为 test 的路径,故用例放在包根而非 test/。
*/

import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { after, describe, it } from "node:test";
import { fileURLToPath } from "node:url";

const pkgRoot = path.dirname(fileURLToPath(import.meta.url));
const distDir = path.join(pkgRoot, "dist");

/** 与 recovery.js → dist/recovery.js 同构:根目录 shim 与 dist 模块同名 */
const MODULE_NAME = "_sfmc_is_main_fixture.js";
const distModule = path.join(distDir, MODULE_NAME);
const rootShim = path.join(pkgRoot, MODULE_NAME);
const alienEntry = path.join(pkgRoot, "_sfmc_alien_entry.js");

const moduleSrc = `
import { isMainModule } from "./is-main.js";
console.log(isMainModule(import.meta.url) ? "MAIN" : "NOT_MAIN");
`;

const shimSrc = `
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
await import(pathToFileURL(path.join(__dirname, "dist", ${JSON.stringify(MODULE_NAME)})).href);
`;

const alienSrc = `
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
await import(pathToFileURL(path.join(__dirname, "dist", ${JSON.stringify(MODULE_NAME)})).href);
`;

describe("isMainModule", () => {
it("dist 直接作为 argv 入口时为 true", () => {
fs.mkdirSync(distDir, { recursive: true });
fs.writeFileSync(distModule, moduleSrc);
const r = spawnSync(process.execPath, [distModule], { encoding: "utf8" });
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout.trim(), "MAIN");
});

it("同名根目录 shim 动态 import dist 时为 true(recovery.js 契约)", () => {
fs.mkdirSync(distDir, { recursive: true });
fs.writeFileSync(distModule, moduleSrc);
fs.writeFileSync(rootShim, shimSrc);
const r = spawnSync(process.execPath, [rootShim], { encoding: "utf8" });
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout.trim(), "MAIN");
});

it("被异名入口 import 时为 false(check-update 加载 bds-manager)", () => {
fs.mkdirSync(distDir, { recursive: true });
fs.writeFileSync(distModule, moduleSrc);
fs.writeFileSync(alienEntry, alienSrc);
const r = spawnSync(process.execPath, [alienEntry], { encoding: "utf8" });
assert.equal(r.status, 0, r.stderr);
assert.equal(r.stdout.trim(), "NOT_MAIN");
});

after(() => {
for (const p of [distModule, rootShim, alienEntry]) {
try {
fs.unlinkSync(p);
} catch {
/* ignore */
}
}
});
});
1 change: 1 addition & 0 deletions bds-tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"dev": "tsx src/dist/bds-manager.ts",
"typecheck": "tsc --noEmit",
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
"test": "node --test is-main.test.mjs",
"update": "node dist/check-update.js",
"update:check": "node dist/check-update.js --check-only",
"update:force": "node dist/check-update.js --force",
Expand Down
17 changes: 9 additions & 8 deletions bds-tools/recovery.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
/**
* recovery.js — shim, 委托给 src/recovery.ts 编译产物
* recovery.js — shim, 委托给 dist/recovery.js
*
* 用法: node recovery.js
*
* 注意:bds-tools 为 ESM("type":"module"),不能再用 createRequire 加载 dist。
*/

import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";

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

try {
require(dist);
await import(distUrl);
} catch (e) {
if (e.code === "MODULE_NOT_FOUND") {
const err = /** @type {NodeJS.ErrnoException} */ (e);
if (err && (err.code === "ERR_MODULE_NOT_FOUND" || err.code === "MODULE_NOT_FOUND")) {
console.error("[BDSRecovery] dist/ 未找到,请先运行 `npm run build`");
process.exit(2);
}
throw e;
}
}
2 changes: 2 additions & 0 deletions bds-tools/src/bds-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ bdsEvents_enabled();

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

Expand Down
33 changes: 28 additions & 5 deletions bds-tools/src/is-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,34 @@
*
* package.json 声明 "type": "module" 后,`require` 不存在,
* 不能再用 `require.main === module`。
*
* 契约(与历史 endsWith 行为对齐,满足 LSP):
* 1. process.argv[1] 与本文件 realpath 相同 → true
* 2. 根目录 shim(如 recovery.js → dist/recovery.js)同 stem → true
* 3. 被其他入口 import 时(argv stem 不同)→ false
*/

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";

function normPath(p: string): string {
return process.platform === "win32" ? p.toLowerCase() : p;
}

/** 去掉 .js/.ts/.mjs/.cjs 等扩展,得到入口 stem */
function entryStem(filePath: string): string {
return path.basename(filePath).replace(/\.(c|m)?(js|ts)$/i, "");
}

function tryRealpath(filePath: string): string {
try {
return fs.realpathSync.native(filePath);
} catch {
return filePath;
}
}

/**
* 判断 metaUrl 对应模块是否为 node 进程主入口。
* 替代 CommonJS 的 `require.main === module`。
Expand All @@ -16,11 +39,11 @@ export function isMainModule(metaUrl: string): boolean {
const entry = process.argv[1];
if (!entry) return false;
try {
const thisFile = path.resolve(fileURLToPath(metaUrl));
const entryFile = path.resolve(entry);
return process.platform === "win32"
? thisFile.toLowerCase() === entryFile.toLowerCase()
: thisFile === entryFile;
const thisFile = tryRealpath(path.resolve(fileURLToPath(metaUrl)));
const entryFile = tryRealpath(path.resolve(entry));
if (normPath(thisFile) === normPath(entryFile)) return true;
// shim 兼容:入口与本模块同 stem(recovery.js 加载 dist/recovery.js)
return normPath(entryStem(thisFile)) === normPath(entryStem(entryFile));
} catch {
return false;
}
Expand Down
Loading