Skip to content

Commit be97b39

Browse files
committed
feat(di): trace usage of legacy APIs slated for deprecation
reportDeprecation() dedups per api+detail and logs at trace level by default; NS_DEPRECATIONS=warn|error previews the stricter stages so the same call sites can be escalated over releases. Wired at the external entry points only: param-name hook invocation, require-time extension registration, and dynamicCall help templating.
1 parent 7fe0f09 commit be97b39

4 files changed

Lines changed: 193 additions & 0 deletions

File tree

lib/common/deprecation.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/**
2+
* Central reporting point for invocations of legacy/deprecated CLI APIs.
3+
*
4+
* The severity is a single dial so the same call sites can be staged over
5+
* releases: trace (observe usage) → warn (tell users) → error (removal).
6+
* `NS_DEPRECATIONS=warn|error` previews a stricter stage ahead of the default.
7+
*/
8+
9+
type DeprecationStage = "trace" | "warn" | "error";
10+
11+
const DEFAULT_STAGE: DeprecationStage = "trace";
12+
13+
interface IDeprecationLogger {
14+
trace(...args: any[]): void;
15+
warn(...args: any[]): void;
16+
}
17+
18+
export interface IDeprecationReport {
19+
/** Stable identifier of the deprecated API, e.g. "hooks.param-name-signature". */
20+
api: string;
21+
/** Distinguishes call sites of one API, e.g. a hook path or extension name. */
22+
detail?: string;
23+
/**
24+
* Logger to report through. When omitted, the logger is resolved lazily from
25+
* the global injector at call time — never at import time — and the report
26+
* is dropped if no logger is resolvable yet.
27+
*/
28+
logger?: IDeprecationLogger;
29+
}
30+
31+
const reported = new Set<string>();
32+
33+
export function reportDeprecation(report: IDeprecationReport): void {
34+
const stage = getDeprecationStage();
35+
const message = formatMessage(report);
36+
37+
if (stage === "error") {
38+
throw new Error(message);
39+
}
40+
41+
const key = report.detail ? `${report.api}::${report.detail}` : report.api;
42+
if (reported.has(key)) {
43+
return;
44+
}
45+
reported.add(key);
46+
47+
const logger = report.logger || tryResolveGlobalLogger();
48+
if (!logger) {
49+
return;
50+
}
51+
52+
if (stage === "warn") {
53+
logger.warn(message);
54+
} else {
55+
logger.trace(message);
56+
}
57+
}
58+
59+
/** Test seam: reports are deduplicated once per process otherwise. */
60+
export function clearReportedDeprecations(): void {
61+
reported.clear();
62+
}
63+
64+
function formatMessage(report: IDeprecationReport): string {
65+
const detail = report.detail ? ` (${report.detail})` : "";
66+
return (
67+
`Legacy CLI API used: ${report.api}${detail}. ` +
68+
`This API is planned for deprecation in a future release; ` +
69+
`set NS_DEPRECATIONS=warn or NS_DEPRECATIONS=error to preview stricter handling.`
70+
);
71+
}
72+
73+
function getDeprecationStage(): DeprecationStage {
74+
const value = (process.env.NS_DEPRECATIONS || "").toLowerCase();
75+
if (value === "warn" || value === "error" || value === "trace") {
76+
return value;
77+
}
78+
return DEFAULT_STAGE;
79+
}
80+
81+
function tryResolveGlobalLogger(): IDeprecationLogger | null {
82+
try {
83+
const globalInjector = (<any>global).$injector;
84+
if (!globalInjector) {
85+
return null;
86+
}
87+
return globalInjector.resolve("logger");
88+
} catch (err) {
89+
return null;
90+
}
91+
}

lib/common/services/hooks-service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as path from "path";
22
import * as util from "util";
33
import * as _ from "lodash";
44
import { annotate, getValueFromNestedObject } from "../helpers";
5+
import { reportDeprecation } from "../deprecation";
56
import { AnalyticsEventLabelDelimiter } from "../../constants";
67
import { IOptions, IPerformanceService } from "../../declarations";
78
import {
@@ -245,6 +246,12 @@ export class HooksService implements IHooksService {
245246
projectDataHookArg;
246247
}
247248

249+
reportDeprecation({
250+
api: "hooks.param-name-signature",
251+
detail: hook.fullPath,
252+
logger: this.$logger,
253+
});
254+
248255
const maybePromise = this.$injector.resolve(
249256
hookEntryPoint,
250257
hookArguments,

lib/services/extensibility-service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as _ from "lodash";
33
import { cache } from "../common/decorators";
44
import * as constants from "../constants";
55
import { createRegExp, regExpEscape } from "../common/helpers";
6+
import { reportDeprecation } from "../common/deprecation";
67
import { INodePackageManager, INpmsSingleResultData } from "../declarations";
78
import {
89
IFileSystem,
@@ -144,6 +145,11 @@ export class ExtensibilityService implements IExtensibilityService {
144145
await this.assertExtensionIsInstalled(extensionName);
145146

146147
const pathToExtension = this.getPathToExtension(extensionName);
148+
reportDeprecation({
149+
api: "extensions.require-time-registration",
150+
detail: extensionName,
151+
logger: this.$logger,
152+
});
147153
this.$requireService.require(pathToExtension);
148154
return this.getInstalledExtensionData(extensionName);
149155
} catch (error) {

test/deprecation.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { assert } from "chai";
2+
import { Yok } from "../lib/common/yok";
3+
import {
4+
reportDeprecation,
5+
clearReportedDeprecations,
6+
} from "../lib/common/deprecation";
7+
import { LoggerStub } from "./stubs";
8+
9+
describe("deprecation tracer", () => {
10+
let logger: LoggerStub;
11+
let originalEnv: string | undefined;
12+
13+
beforeEach(() => {
14+
logger = new LoggerStub();
15+
clearReportedDeprecations();
16+
originalEnv = process.env.NS_DEPRECATIONS;
17+
delete process.env.NS_DEPRECATIONS;
18+
});
19+
20+
afterEach(() => {
21+
if (originalEnv === undefined) {
22+
delete process.env.NS_DEPRECATIONS;
23+
} else {
24+
process.env.NS_DEPRECATIONS = originalEnv;
25+
}
26+
});
27+
28+
it("logs at trace level by default and reports once per api+detail", () => {
29+
reportDeprecation({ api: "test.api", detail: "site-1", logger });
30+
reportDeprecation({ api: "test.api", detail: "site-1", logger });
31+
32+
const occurrences = logger.traceOutput.split("test.api").length - 1;
33+
assert.equal(occurrences, 1);
34+
assert.equal(logger.warnOutput, "");
35+
36+
reportDeprecation({ api: "test.api", detail: "site-2", logger });
37+
assert.include(logger.traceOutput, "site-2");
38+
});
39+
40+
it("escalates to warn with NS_DEPRECATIONS=warn", () => {
41+
process.env.NS_DEPRECATIONS = "warn";
42+
43+
reportDeprecation({ api: "test.warn", logger });
44+
45+
assert.include(logger.warnOutput, "test.warn");
46+
assert.equal(logger.traceOutput, "");
47+
});
48+
49+
it("throws with NS_DEPRECATIONS=error — on every call, not just the first", () => {
50+
process.env.NS_DEPRECATIONS = "error";
51+
52+
assert.throws(
53+
() => reportDeprecation({ api: "test.error", logger }),
54+
/test\.error/,
55+
);
56+
assert.throws(
57+
() => reportDeprecation({ api: "test.error", logger }),
58+
/test\.error/,
59+
);
60+
});
61+
62+
it("falls back to the global injector's logger when none is passed", () => {
63+
const globalAny = <any>global;
64+
const previousInjector = globalAny.$injector;
65+
const freshInjector = new Yok();
66+
const freshLogger = new LoggerStub();
67+
freshInjector.register("logger", freshLogger);
68+
globalAny.$injector = freshInjector;
69+
70+
try {
71+
reportDeprecation({ api: "test.global-logger" });
72+
assert.include(freshLogger.traceOutput, "test.global-logger");
73+
} finally {
74+
globalAny.$injector = previousInjector;
75+
}
76+
});
77+
78+
it("drops the report silently when no logger is resolvable", () => {
79+
const globalAny = <any>global;
80+
const previousInjector = globalAny.$injector;
81+
globalAny.$injector = undefined;
82+
83+
try {
84+
assert.doesNotThrow(() => reportDeprecation({ api: "test.no-logger" }));
85+
} finally {
86+
globalAny.$injector = previousInjector;
87+
}
88+
});
89+
});

0 commit comments

Comments
 (0)