feat(extensions): nativescript.commands map — per-command lazy loading for extensions - #6102
Open
edusperoni wants to merge 10 commits into
Open
feat(extensions): nativescript.commands map — per-command lazy loading for extensions#6102edusperoni wants to merge 10 commits into
edusperoni wants to merge 10 commits into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
edusperoni
force-pushed
the
feat/define-command
branch
from
July 30, 2026 01:39
b15734b to
863f964
Compare
edusperoni
force-pushed
the
feat/extension-manifests
branch
from
July 30, 2026 01:41
220e027 to
02d6f7c
Compare
edusperoni
force-pushed
the
feat/define-command
branch
from
July 30, 2026 01:44
863f964 to
74caa8f
Compare
edusperoni
force-pushed
the
feat/extension-manifests
branch
from
July 30, 2026 01:44
02d6f7c to
d8a8fcf
Compare
edusperoni
force-pushed
the
feat/define-command
branch
from
July 30, 2026 02:27
74caa8f to
cafa737
Compare
edusperoni
force-pushed
the
feat/extension-manifests
branch
from
July 30, 2026 02:28
d8a8fcf to
e0c671c
Compare
edusperoni
force-pushed
the
feat/define-command
branch
from
July 30, 2026 02:51
cafa737 to
a1ba0ef
Compare
edusperoni
force-pushed
the
feat/extension-manifests
branch
from
July 30, 2026 02:52
e0c671c to
10aaa87
Compare
edusperoni
force-pushed
the
feat/define-command
branch
from
August 4, 2026 20:41
a1ba0ef to
07c979c
Compare
edusperoni
force-pushed
the
feat/extension-manifests
branch
from
August 4, 2026 20:41
10aaa87 to
7bbf81e
Compare
edusperoni
force-pushed
the
feat/define-command
branch
from
August 5, 2026 19:22
07c979c to
d712a4e
Compare
…s map An extension whose package.json declares nativescript.commands as a map of command name to module path is no longer require()d at startup. Each entry is registered with injector.requireCommand against the module's absolute path, so a command's implementation loads only when that command is first resolved, and the CLI stops paying every installed extension's load cost on every invocation. Entries are validated: a command name or module path that is not a non-empty string is warned about and skipped, and a name already claimed by another extension is reported as a warning naming both extensions rather than propagating the injector's "require'd twice" failure. The legacy array shape (and a missing commands key) keeps today's behavior verbatim - eager require of the extension main plus the extensions.require-time-registration deprecation report. Both shapes now feed IExtensionData.commands and the npm install suggestion for unknown commands.
A manifest entry may now point at a module that exports a defineCommand definition instead of registering itself on load: the deferred loader adapts and registers the export under the manifest key. The override also lands on a parent record the entry just created, because dispatch resolves the hierarchical parent before any child module has loaded and the dispatcher only comes into existence once a child registers. Also cross-links the authoring guides from dependency-injection.md.
… 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.
Registry operations go through the narrow subsystem contract; the full facade stays only for container-record operations (has, provider registration). First consumer of the per-face tokens.
…iner A record carrying only a lazy-require loader resolves to an error until the loader registers something onto it, so the form is not one callers should be offered: drop ILazyRequireProvider from the exported Provider union and keep it in an InternalProvider alias the container accepts. Add hasResolver() so the deferred paths can tell a record that a loader has filled in from one it left empty.
Claiming a command name and loading its implementation are now separate: the registry builds routing — the command record, the parent's subcommand list and the parent dispatcher — from the name alone, and runs the loader only when that one command is resolved. A sibling's dispatch no longer drags in the first claimant's module, and the outcome comes back as a structured result instead of a thrown message callers have to match on. Names that are not lower case are rejected: dispatch lower-cases what the user typed, so they could never be reached. A loader that throws, or that leaves the command without a resolver, fails naming the owner and the source. Extract registerDefinitionAs so a definition registered under a name chosen by its registrant is built exactly like one registered under its own.
The manifest loader no longer writes injector records or reads exception text to detect conflicts; it hands each entry to registerDeferredCommand and reports the rejection it gets back. A command claimed by another extension names that extension, one the CLI provides says so without exposing internals, and re-loading an already loaded extension is silent rather than a conflict with itself. Entry values may now be an object carrying the module path under `path`, with unrecognised keys ignored, so the shape can grow without stranding manifests on released CLIs. Default commands are registered ahead of their siblings so JSON key order carries no meaning. The manifest key is what the command is dispatched as — routing happens before the module exists — so a definition whose own name disagrees runs under the key and warns naming both, and definitions register through the same helper as registerCommandDefinition.
Lead with the peerDependency + devDependency pair that makes
`require("nativescript/contracts")` resolve and keeps a second CLI copy out of
the tree, and teach inject() as the way to reach a CLI service.
Cover what the manifest actually promises: the key is authoritative for
routing, aliases are duplicate entries pointing at one module, entry values may
be envelopes, an empty map opts out of loading, keys must be lower case, and
"first" in first-wins is the order extensions load in. Drop the JSON key-order
constraint, which no longer exists.
edusperoni
force-pushed
the
feat/extension-manifests
branch
from
August 5, 2026 19:44
7bbf81e to
bcd08f0
Compare
edusperoni
marked this pull request as ready for review
August 5, 2026 20:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Checklist
What is the current behavior?
Every installed extension is eagerly
require()d on every CLI invocation, before the command is even known — the extension's whole module tree loads so its top-level side effects can register commands againstglobal.$injector.nativescript.commandsin an extension's package.json is astring[]used only to suggest installs for unknown commands. Two extensions claiming the same command name crash at startup.What is the new behavior?
nativescript.commandsalso accepts a map of command name → module path, which becomes authoritative:string | { path }so the envelope can grow additively.defineCommanddefinition'snamedisagrees with its manifest key, the CLI warns naming both and runs under the key. Aliases are duplicate manifest entries pointing at the same module.registerDeferredCommandon theCommandRegistryfacet: claiming a name and loading its implementation are now separate registry operations. The registry builds the command record, the parent's subcommand list, and the parent dispatcher from the name alone — a sibling's dispatch never drags in the first claimant's module — and returns a structuredDeferredCommandResult(claimed/built-in/subcommand-parent/invalid-name) instead of exception text callers must match on. This is what keeps the future registry extraction a provider swap.*defaultentries sort first per parent in code — JSON key order carries no meaning. First-wins conflict resolution is defined in the docs (extension load order, alphabetical; the mid-loadns extension installexception documented). Re-declaring a command under the same owner is a no-op, sons extension install <already-installed>no longer warns about conflicting with itself."commands": {}opts out of loading entirely.defineCommanddefinition — one registration code path (registerDefinitionAs) serves both the manifest loader andregisterCommandDefinition.ILazyRequireProvideris no longer part of the exportedProviderunion (container-internal).extensions.md— leads with thepeerDependency+devDependencyonnativescriptandinject()fromnativescript/contracts.Public type names follow the new-API convention (no
Iprefix):DeferredCommandOptions,DeferredCommandResult,DeferredCommandRejection.25 tests in
test/extension-manifests.ts(lazy registration, eager-path preservation, malformed/conflict/self-conflict handling, both suggestion shapes, pure-definition modules incl. resolving the parent dispatcher before any child module has loaded, key-mismatch warning, alias entries,{}opt-out). Full stacked suite: 116 files, 1784 passed / 9 skipped; yok oracle, public-API test, and compat fixtures untouched.