Skip to content

Commit cc873e6

Browse files
committed
refactor(extensions): inject the injector; keep globals to the legacy seam
The service takes $injector as a constructor dependency instead of the module-level import, so manifest registration and the definition-aware loaders target the instance that resolved it. Tests assert on their own per-test injector; the process-wide injector is swapped only because legacy-shape fixture modules register through the published global surface at load, and that seam is labeled as such. extensions.md no longer teaches the global-injector patterns: the legacy array path and self-registering modules are described under their deprecation framing without runnable samples.
1 parent c7059c7 commit cc873e6

3 files changed

Lines changed: 41 additions & 67 deletions

File tree

extensions.md

Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,14 @@ The array is a discovery aid only — it lists the names the CLI may suggest you
8888
extension for, but it says nothing about where the implementations live. An
8989
extension declaring commands this way (or declaring no commands at all) is
9090
loaded the old way: the CLI `require()`s the package's main entry on **every**
91-
invocation and expects the module's top-level code to register everything.
91+
invocation and expects the module's top-level code to register everything
92+
through the injector global.
9293

93-
```js
94-
// index.js of a legacy extension
95-
const path = require("path");
96-
97-
global.$injector.requireCommand(
98-
"hello|world",
99-
path.join(__dirname, "commands", "hello-world"),
100-
);
101-
```
102-
103-
This path remains supported, but it is tracked for eventual deprecation. Run any
104-
command with `--log trace` to see which installed extensions still rely on it,
105-
or set `NS_DEPRECATIONS=warn` to have those reports printed as warnings.
94+
This path remains supported for published extensions, but it is tracked for
95+
eventual deprecation and new extensions should not use it — declare the map
96+
instead. Run any command with `--log trace` to see which installed extensions
97+
still rely on it, or set `NS_DEPRECATIONS=warn` to have those reports printed
98+
as warnings.
10699

107100
## Writing a command module
108101

@@ -124,31 +117,12 @@ module.exports = defineCommand({
124117
});
125118
```
126119

127-
Alternatively, a module may register itself when it is loaded, by calling
128-
`$injector.registerCommand` at the top level with the same name it is declared
129-
under in the manifest. The CLI resolves the command through the injector right
130-
after loading the module, so registration has to happen as a side effect of the
131-
`require`.
132-
133-
```js
134-
// dist/commands/hello-world.js
135-
class HelloWorldCommand {
136-
constructor($logger) {
137-
this.$logger = $logger;
138-
this.allowedParameters = [];
139-
}
140-
141-
async execute(args) {
142-
this.$logger.info(`Hello, ${args[0] || "world"}!`);
143-
}
144-
}
145-
146-
global.$injector.registerCommand("hello|world", HelloWorldCommand);
147-
```
148-
149-
Constructor parameters are injected by name: a parameter called `$logger`
150-
receives the CLI's logger, `$fs` its file system service, and so on. A command
151-
class must expose `allowedParameters` and an `execute(args)` method.
120+
Legacy modules — command classes that register themselves at load time through
121+
the injector global, with parameter-name constructor injection — keep working
122+
when a manifest entry points at them, so existing extensions can adopt the map
123+
without rewriting their commands. Both of those mechanisms are deprecated
124+
(see [dependency-injection.md](dependency-injection.md)); write new modules as
125+
definitions.
152126

153127
## Command names
154128

lib/services/extensibility-service.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
IGetExtensionCommandInfoParams,
1919
} from "../common/definitions/extensibility";
2020
import { injector } from "../common/yok";
21+
import { IInjector } from "../common/definitions/yok";
2122
import { CommandsDelimiters } from "../common/constants";
2223
import { isCommandDefinition } from "../common/define-command";
2324
import { createCommandFromDefinition } from "../common/services/command-definition-adapter";
@@ -77,6 +78,7 @@ export class ExtensibilityService implements IExtensibilityService {
7778
private $packageManager: INodePackageManager,
7879
private $settingsService: ISettingsService,
7980
private $requireService: IRequireService,
81+
private $injector: IInjector,
8082
) {}
8183

8284
public async installExtension(
@@ -362,12 +364,12 @@ export class ExtensibilityService implements IExtensibilityService {
362364
const parentName = commandName.split(
363365
CommandsDelimiters.HierarchicalCommand,
364366
)[0];
365-
const container = (<any>injector).di;
367+
const container = (<any>this.$injector).di;
366368
const parentWasAbsent =
367369
parentName !== commandName && !container.has(`commands.${parentName}`);
368370

369371
try {
370-
injector.requireCommand(commandName, absoluteModulePath);
372+
this.$injector.requireCommand(commandName, absoluteModulePath);
371373
} catch (err) {
372374
const owner = this.manifestCommandOwners[commandName];
373375
const ownerInfo = owner
@@ -389,7 +391,7 @@ export class ExtensibilityService implements IExtensibilityService {
389391
const exported = require(absoluteModulePath);
390392
const candidate = (exported && exported.default) ?? exported;
391393
if (isCommandDefinition(candidate)) {
392-
injector.registerCommand(commandName, () =>
394+
this.$injector.registerCommand(commandName, () =>
393395
createCommandFromDefinition(<any>candidate),
394396
);
395397
}

test/extension-manifests.ts

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,24 @@ import * as fs from "fs";
33
import * as os from "os";
44
import * as path from "path";
55
import { ExtensibilityService } from "../lib/services/extensibility-service";
6-
import { Yok } from "../lib/common/yok";
6+
import { Yok, getInjector, setGlobalInjector } from "../lib/common/yok";
77
import { LoggerStub } from "./stubs";
88
import { clearReportedDeprecations } from "../lib/common/deprecation";
99
import { CommandsDelimiters } from "../lib/common/constants";
10-
import { ICliGlobal } from "../lib/common/definitions/cli-global";
1110
import { IInjector } from "../lib/common/definitions/yok";
1211
import {
1312
IExtensibilityService,
1413
IExtensionData,
1514
} from "../lib/common/definitions/extensibility";
1615
import { IStringDictionary } from "../lib/common/declarations";
1716

18-
// The service registers commands on the global injector imported by
19-
// lib/common/yok, so every assertion about registered commands goes through it.
20-
// That injector is shared by the whole file, so command names must be unique per
21-
// test - a name reused across tests would collide like a real conflict does.
22-
const cliGlobal = <ICliGlobal>(<unknown>global);
17+
// Every assertion about registered commands goes through the per-test
18+
// injector: the service takes $injector as a constructor dependency. The
19+
// process-wide injector is pointed at that same instance for each test's
20+
// duration ONLY because legacy-shape fixture modules register through the
21+
// published global surface when they load - that swap is the legacy-compat
22+
// seam, not the assertion path. Command names stay unique per test since the
23+
// module require cache outlives a test.
2324

2425
interface ITestCapture {
2526
loadedModules: string[];
@@ -32,9 +33,14 @@ describe("extension manifests", () => {
3233
let profileDir: string;
3334
let requiredPaths: string[];
3435
let capture: ITestCapture;
36+
let testInjector: IInjector;
37+
let previousProcessInjector: IInjector;
3538

3639
beforeEach(() => {
3740
profileDir = fs.mkdtempSync(path.join(os.tmpdir(), "ns-ext-manifest-"));
41+
testInjector = getTestInjector();
42+
previousProcessInjector = getInjector();
43+
setGlobalInjector(testInjector);
3844
requiredPaths = [];
3945
capture = (<any>global).__nsmCapture = {
4046
loadedModules: [],
@@ -48,6 +54,7 @@ describe("extension manifests", () => {
4854
});
4955

5056
afterEach(() => {
57+
setGlobalInjector(previousProcessInjector);
5158
fs.rmSync(profileDir, { recursive: true, force: true });
5259
delete (<any>global).__nsmCapture;
5360
});
@@ -195,7 +202,6 @@ describe("extension manifests", () => {
195202
},
196203
);
197204

198-
const testInjector = getTestInjector();
199205
const extensibilityService = resolveService(testInjector);
200206
const extensionData =
201207
await extensibilityService.loadExtension(extensionName);
@@ -212,7 +218,7 @@ describe("extension manifests", () => {
212218
"nsmlazy|clean",
213219
]);
214220

215-
const command = cliGlobal.$injector.resolveCommand("nsmlazy|run");
221+
const command = testInjector.resolveCommand("nsmlazy|run");
216222
assert.isOk(command);
217223
assert.deepStrictEqual(capture.loadedModules, ["lazy-run"]);
218224

@@ -244,7 +250,6 @@ describe("extension manifests", () => {
244250
},
245251
);
246252

247-
const testInjector = getTestInjector();
248253
const extensibilityService = resolveService(testInjector);
249254
await extensibilityService.loadExtension(extensionName);
250255

@@ -253,9 +258,9 @@ describe("extension manifests", () => {
253258
assert.include(warnOutput, "nsmbad|empty");
254259
assert.include(warnOutput, extensionName);
255260

256-
assert.isOk(cliGlobal.$injector.resolveCommand("nsmbad|good"));
257-
assert.isNull(cliGlobal.$injector.resolveCommand("nsmbad|number"));
258-
assert.isNull(cliGlobal.$injector.resolveCommand("nsmbad|empty"));
261+
assert.isOk(testInjector.resolveCommand("nsmbad|good"));
262+
assert.isNull(testInjector.resolveCommand("nsmbad|number"));
263+
assert.isNull(testInjector.resolveCommand("nsmbad|empty"));
259264
assert.deepStrictEqual(capture.loadedModules, ["malformed-good"]);
260265
});
261266

@@ -279,7 +284,6 @@ describe("extension manifests", () => {
279284
},
280285
);
281286

282-
const testInjector = getTestInjector();
283287
const extensibilityService = resolveService(testInjector);
284288
await extensibilityService.loadExtension(firstExtension);
285289
await extensibilityService.loadExtension(secondExtension);
@@ -289,7 +293,7 @@ describe("extension manifests", () => {
289293
assert.include(warnOutput, firstExtension);
290294
assert.include(warnOutput, secondExtension);
291295

292-
const command = cliGlobal.$injector.resolveCommand("nsmconflict|run");
296+
const command = testInjector.resolveCommand("nsmconflict|run");
293297
await command.execute([]);
294298
assert.deepStrictEqual(capture.executed, [
295299
{ marker: "first-run", args: [] },
@@ -309,7 +313,6 @@ describe("extension manifests", () => {
309313
},
310314
);
311315

312-
const testInjector = getTestInjector();
313316
const extensibilityService = resolveService(testInjector);
314317
const extensionData =
315318
await extensibilityService.loadExtension(extensionName);
@@ -325,7 +328,7 @@ describe("extension manifests", () => {
325328
assert.include(traceOutput, extensionName);
326329

327330
assert.deepStrictEqual(extensionData.commands, ["nsmeager|run"]);
328-
assert.isOk(cliGlobal.$injector.resolveCommand("nsmeager|run"));
331+
assert.isOk(testInjector.resolveCommand("nsmeager|run"));
329332
});
330333

331334
it("keeps the eager path when the extension declares no commands", async () => {
@@ -336,7 +339,6 @@ describe("extension manifests", () => {
336339
{ "main.js": mainModule("no-commands-main") },
337340
);
338341

339-
const testInjector = getTestInjector();
340342
const extensibilityService = resolveService(testInjector);
341343
const extensionData =
342344
await extensibilityService.loadExtension(extensionName);
@@ -357,7 +359,6 @@ describe("extension manifests", () => {
357359
writeExtension("nsm-data-array-ext", { commands: ["nsmdata|three"] }, {});
358360
writeExtension("nsm-data-plain-ext", {}, {});
359361

360-
const testInjector = getTestInjector();
361362
const extensibilityService = resolveService(testInjector);
362363
const extensionsData = extensibilityService.getInstalledExtensionsData();
363364
const dataByName: { [name: string]: IExtensionData } = {};
@@ -382,7 +383,6 @@ describe("extension manifests", () => {
382383
inputStrings: string[],
383384
): Promise<any> => {
384385
const extensionName = "nsm-registry-ext";
385-
const testInjector = getTestInjector();
386386
const packageManager = testInjector.resolve<any>("packageManager");
387387
packageManager.searchNpms = async (keyword: string): Promise<any> => {
388388
assert.equal(keyword, "nativescript:extension");
@@ -485,13 +485,12 @@ describe("extension manifests", () => {
485485
},
486486
);
487487

488-
const testInjector = getTestInjector();
489488
const extensibilityService = resolveService(testInjector);
490489
await extensibilityService.loadExtension(extensionName);
491490

492491
assert.deepEqual(capture.loadedModules, []);
493492

494-
const command = cliGlobal.$injector.resolveCommand("nsmdef|hello");
493+
const command = testInjector.resolveCommand("nsmdef|hello");
495494
assert.isOk(command);
496495
assert.deepEqual(capture.loadedModules, ["def-hello"]);
497496

@@ -512,18 +511,17 @@ describe("extension manifests", () => {
512511
},
513512
);
514513

515-
const testInjector = getTestInjector();
516514
const extensibilityService = resolveService(testInjector);
517515
await extensibilityService.loadExtension(extensionName);
518516

519517
// Dispatch hits the parent first; its record must load the child module
520518
// so the dispatcher the child's registration synthesizes exists.
521-
const parent = <any>cliGlobal.$injector.resolveCommand("nsmdefp");
519+
const parent = <any>testInjector.resolveCommand("nsmdefp");
522520
assert.isOk(parent);
523521
assert.isTrue(parent.isHierarchicalCommand);
524522
assert.include(capture.loadedModules, "defp-go");
525523

526-
const child = cliGlobal.$injector.resolveCommand("nsmdefp|go");
524+
const child = testInjector.resolveCommand("nsmdefp|go");
527525
assert.isOk(child);
528526
});
529527
});

0 commit comments

Comments
 (0)