@memberjunction/dynamic-packages 0.0.0 → 6.1.0-edge.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +183 -0
- package/README.md +385 -28
- package/dist/discover.d.ts +38 -0
- package/dist/discover.d.ts.map +1 -0
- package/dist/discover.js +178 -0
- package/dist/discover.js.map +1 -0
- package/dist/host-import.d.ts +43 -0
- package/dist/host-import.d.ts.map +1 -0
- package/dist/host-import.js +170 -0
- package/dist/host-import.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +114 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +404 -0
- package/dist/loader.js.map +1 -0
- package/dist/mode.d.ts +27 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +60 -0
- package/dist/mode.js.map +1 -0
- package/dist/process-id.d.ts +54 -0
- package/dist/process-id.d.ts.map +1 -0
- package/dist/process-id.js +104 -0
- package/dist/process-id.js.map +1 -0
- package/dist/types.d.ts +153 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/package.json +36 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
License text copyright (c) 2024 MariaDB plc, All Rights Reserved.
|
|
4
|
+
"Business Source License" is a trademark of MariaDB plc.
|
|
5
|
+
|
|
6
|
+
-----------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
|
|
10
|
+
Licensor: Blue Cypress, Inc.
|
|
11
|
+
|
|
12
|
+
Licensed Work: MemberJunction.
|
|
13
|
+
The Licensed Work is (c) 2023-2026 Blue Cypress, Inc.
|
|
14
|
+
|
|
15
|
+
Additional Use Grant: Subject to the terms of this License, Licensor grants
|
|
16
|
+
you the following additional rights to make Production
|
|
17
|
+
Use of the Licensed Work.
|
|
18
|
+
|
|
19
|
+
1. Internal Use
|
|
20
|
+
|
|
21
|
+
You may make production use of the Licensed Work for
|
|
22
|
+
your own internal business or organizational operations.
|
|
23
|
+
|
|
24
|
+
2. Nonprofit Use
|
|
25
|
+
|
|
26
|
+
If you are a Nonprofit, you may make production use of
|
|
27
|
+
the Licensed Work for the operations and activities of
|
|
28
|
+
your Organizational Family.
|
|
29
|
+
|
|
30
|
+
3. MemberJunction Certified Program Use
|
|
31
|
+
|
|
32
|
+
If you are authorized by Licensor under the
|
|
33
|
+
MemberJunction Certified Program to provide professional
|
|
34
|
+
services using the Licensed Work, you may make
|
|
35
|
+
production use of the Licensed Work in providing such
|
|
36
|
+
professional services to a client, provided that:
|
|
37
|
+
|
|
38
|
+
(a) the Licensed Work is deployed in, and the applicable
|
|
39
|
+
production use occurs within, an environment owned,
|
|
40
|
+
leased, licensed, subscribed to, or otherwise controlled
|
|
41
|
+
by that client; and
|
|
42
|
+
|
|
43
|
+
(b) the production use is for that client's own internal
|
|
44
|
+
business or organizational operations or is otherwise
|
|
45
|
+
independently permitted to that client under this
|
|
46
|
+
Additional Use Grant.
|
|
47
|
+
|
|
48
|
+
4. Definitions Applicable to the Additional Use Grant
|
|
49
|
+
|
|
50
|
+
"Affiliate" means, with respect to a specified Person,
|
|
51
|
+
any other Person that directly or indirectly Controls,
|
|
52
|
+
is Controlled by, or is under common Control with such
|
|
53
|
+
specified Person.
|
|
54
|
+
|
|
55
|
+
"Control" (including the terms "Controls," "Controlled
|
|
56
|
+
by," and "under common Control with") means the direct
|
|
57
|
+
or indirect possession of the power to direct or cause
|
|
58
|
+
the direction of the management and policies of a
|
|
59
|
+
Person, whether through ownership of voting interests,
|
|
60
|
+
by contract, or otherwise.
|
|
61
|
+
|
|
62
|
+
"Organizational Family" means, with respect to a Person,
|
|
63
|
+
(a) such Person and its Affiliates, and (b) any
|
|
64
|
+
nonprofit organization, governmental entity, chapter,
|
|
65
|
+
division, local affiliate, regional affiliate, state
|
|
66
|
+
affiliate, national affiliate, or other entity that is
|
|
67
|
+
formally affiliated with such Person through governing
|
|
68
|
+
documents, a charter, bylaws, a membership agreement, or
|
|
69
|
+
another written organizational instrument, and is
|
|
70
|
+
recognized under such documents as part of the same
|
|
71
|
+
organizational structure.
|
|
72
|
+
|
|
73
|
+
"Nonprofit" means a Person recognized by the Internal
|
|
74
|
+
Revenue Service as exempt from federal income taxation
|
|
75
|
+
under Section 501(c)(3), 501(c)(4), 501(c)(5), or
|
|
76
|
+
501(c)(6) of the Internal Revenue Code, or a foreign
|
|
77
|
+
organization recognized under substantially equivalent
|
|
78
|
+
laws.
|
|
79
|
+
|
|
80
|
+
A Person claiming eligibility as a Nonprofit shall, upon
|
|
81
|
+
Licensor's reasonable request, provide documentation
|
|
82
|
+
reasonably sufficient to demonstrate that it qualifies
|
|
83
|
+
as a Nonprofit. If such Person materially misrepresents,
|
|
84
|
+
or is unable to demonstrate, its qualification as a
|
|
85
|
+
Nonprofit, the rights granted to such Person under
|
|
86
|
+
Section 2 of this Additional Use Grant shall terminate.
|
|
87
|
+
|
|
88
|
+
"MemberJunction Certified Program" means Licensor's
|
|
89
|
+
then-current program for certifying and authorizing a
|
|
90
|
+
Person to provide professional services using the
|
|
91
|
+
Licensed Work.
|
|
92
|
+
|
|
93
|
+
"Person" means any individual, corporation, limited
|
|
94
|
+
liability company, partnership, association, nonprofit
|
|
95
|
+
organization, governmental entity, or other legal or
|
|
96
|
+
organizational entity.
|
|
97
|
+
|
|
98
|
+
Change Date: Four (4) years from the date the Licensed Work is first
|
|
99
|
+
made available.
|
|
100
|
+
|
|
101
|
+
Change License: MIT License.
|
|
102
|
+
|
|
103
|
+
For information about alternative licensing arrangements for the Licensed
|
|
104
|
+
Work, please contact Blue Cypress, Inc.
|
|
105
|
+
|
|
106
|
+
-----------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
Terms
|
|
109
|
+
|
|
110
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
111
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
112
|
+
Licensor may make an Additional Use Grant, above, permitting limited
|
|
113
|
+
production use.
|
|
114
|
+
|
|
115
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly
|
|
116
|
+
available distribution of a specific version of the Licensed Work under this
|
|
117
|
+
License, whichever comes first, the Licensor hereby grants you rights under
|
|
118
|
+
the terms of the Change License, and the rights granted in the paragraph
|
|
119
|
+
above terminate.
|
|
120
|
+
|
|
121
|
+
If your use of the Licensed Work does not comply with the requirements
|
|
122
|
+
currently in effect as described in this License, you must purchase a
|
|
123
|
+
commercial license from the Licensor, its affiliated entities, or authorized
|
|
124
|
+
resellers, or you must refrain from using the Licensed Work.
|
|
125
|
+
|
|
126
|
+
All copies of the original and modified Licensed Work, and derivative works
|
|
127
|
+
of the Licensed Work, are subject to this License. This License applies
|
|
128
|
+
separately for each version of the Licensed Work and the Change Date may vary
|
|
129
|
+
for each version of the Licensed Work released by Licensor.
|
|
130
|
+
|
|
131
|
+
You must conspicuously display this License on each original or modified copy
|
|
132
|
+
of the Licensed Work. If you receive the Licensed Work in original or
|
|
133
|
+
modified form from a third party, the terms and conditions set forth in this
|
|
134
|
+
License apply to your use of that work.
|
|
135
|
+
|
|
136
|
+
Any use of the Licensed Work in violation of this License will automatically
|
|
137
|
+
terminate your rights under this License for the current and all other
|
|
138
|
+
versions of the Licensed Work.
|
|
139
|
+
|
|
140
|
+
This License does not grant you any right in any trademark or logo of
|
|
141
|
+
Licensor or its affiliates (provided that you may use a trademark or logo of
|
|
142
|
+
Licensor as expressly required by this License).
|
|
143
|
+
|
|
144
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
|
|
145
|
+
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
|
|
146
|
+
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
|
|
147
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
|
|
148
|
+
TITLE.
|
|
149
|
+
|
|
150
|
+
MariaDB hereby grants you permission to use this License's text to license
|
|
151
|
+
your works, and to refer to it using the trademark "Business Source License",
|
|
152
|
+
as long as you comply with the Covenants of Licensor below.
|
|
153
|
+
|
|
154
|
+
-----------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
Covenants of Licensor
|
|
157
|
+
|
|
158
|
+
In consideration of the right to use this License's text and the "Business
|
|
159
|
+
Source License" name and trademark, Licensor covenants to MariaDB, and to all
|
|
160
|
+
other recipients of the licensed work to be provided by Licensor:
|
|
161
|
+
|
|
162
|
+
1. To specify as the Change License the GPL Version 2.0 or any later version,
|
|
163
|
+
or a license that is compatible with GPL Version 2.0 or a later version,
|
|
164
|
+
where "compatible" means that software provided under the Change License
|
|
165
|
+
can be included in a program with software provided under GPL Version 2.0
|
|
166
|
+
or a later version. Licensor may specify additional Change Licenses without
|
|
167
|
+
limitation.
|
|
168
|
+
|
|
169
|
+
2. To either: (a) specify an additional grant of rights to use that does not
|
|
170
|
+
impose any additional restriction on the right granted in this License, as
|
|
171
|
+
the Additional Use Grant; or (b) insert the text "None".
|
|
172
|
+
|
|
173
|
+
3. To specify a Change Date.
|
|
174
|
+
|
|
175
|
+
4. Not to modify this License in any other way.
|
|
176
|
+
|
|
177
|
+
-----------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
Notice
|
|
180
|
+
|
|
181
|
+
The Business Source License (this document, or the "License") is not an Open
|
|
182
|
+
Source license. However, the Licensed Work will eventually be made available
|
|
183
|
+
under an Open Source License, as stated in this License.
|
package/README.md
CHANGED
|
@@ -1,45 +1,402 @@
|
|
|
1
1
|
# @memberjunction/dynamic-packages
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Process-agnostic loader for packages whose names are only known at runtime:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- Open App server / client packages recorded in `mj.config.cjs` `dynamicPackages.server[]` /
|
|
6
|
+
`dynamicPackages.client[]` by `mj app install`
|
|
7
|
+
- a host's own generated packages under `codeGeneration.packages`
|
|
8
|
+
- the packages of the Open App whose repository a process is standing in (`mj-app.json`)
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
Importing one of these packages fires its `@RegisterClass` decorators, so `Metadata.GetEntityObject`
|
|
11
|
+
returns the app's entity subclass (validation, `Save()` overrides, lifecycle hooks) instead of a
|
|
12
|
+
generic `BaseEntity`. MJAPI has always done this at boot. Every other MJ process — the `mj` CLI
|
|
13
|
+
(`sync push`, `app …`, `test`, …), the MCP and A2A servers, the integration-test bootstrap, an ad-hoc
|
|
14
|
+
script — needs exactly the same behaviour, and this package is where it lives so each host is one call.
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
The repo-wide story (why it exists, how a downstream app and an MJ install configure it, how to add
|
|
17
|
+
it to a new host) is in [`guides/DYNAMIC_PACKAGE_LOADING_GUIDE.md`](../../guides/DYNAMIC_PACKAGE_LOADING_GUIDE.md).
|
|
18
|
+
This README is the package reference.
|
|
10
19
|
|
|
11
|
-
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@memberjunction/dynamic-packages`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
20
|
+
## Contents
|
|
15
21
|
|
|
16
|
-
|
|
22
|
+
1. [The problem in one picture](#the-problem-in-one-picture)
|
|
23
|
+
2. [Usage](#usage)
|
|
24
|
+
3. [How a call proceeds](#how-a-call-proceeds)
|
|
25
|
+
4. [Discovery sources and merge rules](#discovery-sources-and-merge-rules)
|
|
26
|
+
5. [Process IDs](#process-ids)
|
|
27
|
+
6. [Scoping entries per process](#scoping-entries-per-process)
|
|
28
|
+
7. [Mode: turning it off](#mode-turning-it-off)
|
|
29
|
+
8. [Nested hosts](#nested-hosts)
|
|
30
|
+
9. [Running inside an Open App repository](#running-inside-an-open-app-repository)
|
|
31
|
+
10. [Resolution: how a package is found](#resolution-how-a-package-is-found)
|
|
32
|
+
11. [Loading twice in one process](#loading-twice-in-one-process)
|
|
33
|
+
12. [The report](#the-report)
|
|
34
|
+
13. [Loggers](#loggers)
|
|
35
|
+
14. [Contract for `StartupExport`](#contract-for-startupexport)
|
|
36
|
+
15. [API](#api)
|
|
37
|
+
16. [Testing a host against it](#testing-a-host-against-it)
|
|
17
38
|
|
|
18
|
-
|
|
39
|
+
## The problem in one picture
|
|
19
40
|
|
|
20
|
-
|
|
41
|
+
`MJGlobal.ClassFactory` is a registry keyed by `(baseClass, key)`. `@RegisterClass(BaseEntity, 'MJ_BizApps_Orders: Orders')`
|
|
42
|
+
registers a subclass under that key **as a side effect of importing the module that declares it**.
|
|
43
|
+
Nothing else ever puts it there. So a process that has not imported the app's package gets the fallback:
|
|
21
44
|
|
|
22
|
-
|
|
45
|
+
```mermaid
|
|
46
|
+
flowchart LR
|
|
47
|
+
subgraph MJAPI["MJAPI (always loaded app packages)"]
|
|
48
|
+
A1["import '@mj-biz-apps/orders-server'"] --> A2["@RegisterClass fires"]
|
|
49
|
+
A2 --> A3["ClassFactory: Orders → OrderEntityServer"]
|
|
50
|
+
A3 --> A4["GetEntityObject('…Orders')<br/>→ OrderEntityServer<br/>Save() override runs ✅"]
|
|
51
|
+
end
|
|
52
|
+
subgraph CLI["mj sync push (before this package)"]
|
|
53
|
+
B1["no import"] --> B3["ClassFactory: Orders → (nothing)"]
|
|
54
|
+
B3 --> B4["GetEntityObject('…Orders')<br/>→ plain BaseEntity<br/>Save() override skipped ❌"]
|
|
55
|
+
end
|
|
56
|
+
```
|
|
23
57
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
58
|
+
The fallback is silent: the push reports success, the row is written, and the app's validation and
|
|
59
|
+
lifecycle logic simply never ran (MemberJunction/MJ#4199). The loader closes that gap for every process
|
|
60
|
+
by making "import whatever the configuration names" a single call with one set of rules.
|
|
28
61
|
|
|
29
|
-
##
|
|
62
|
+
## Usage
|
|
30
63
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- Provides no functionality
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
64
|
+
```ts
|
|
65
|
+
import { LoadDynamicPackages, DiscoverMJConfig } from '@memberjunction/dynamic-packages';
|
|
36
66
|
|
|
37
|
-
|
|
67
|
+
// 1. Discover the RAW mj.config.cjs (Zod-parsed configs usually strip `dynamicPackages`).
|
|
68
|
+
const { config, configFilePath } = DiscoverMJConfig();
|
|
38
69
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
70
|
+
// 2. Load — BEFORE any database provider is set up, exactly where MJAPI does it.
|
|
71
|
+
const report = await LoadDynamicPackages({ processId: 'mcp', config, configFilePath });
|
|
42
72
|
|
|
43
|
-
|
|
73
|
+
report.Loaded; // [{ Entry, Source, Module, RanStartupExport }]
|
|
74
|
+
report.Skipped; // disabled / process-filter / mode-none / duplicate
|
|
75
|
+
report.NotFound; // not resolvable from any anchor (expected before `npm install`)
|
|
76
|
+
report.Failed; // resolved but threw while loading — its own error, never masked
|
|
77
|
+
```
|
|
44
78
|
|
|
45
|
-
**
|
|
79
|
+
Call it **after** the host's own class-registration manifest (`@memberjunction/server-bootstrap`,
|
|
80
|
+
`@memberjunction/server-bootstrap-lite`) has been imported: the ClassFactory's load-order priority
|
|
81
|
+
means the app's registrations land last and win. Call it **before** the provider is created: a
|
|
82
|
+
`StartupExport` may rely on nothing but the ClassFactory.
|
|
83
|
+
|
|
84
|
+
The package has one runtime dependency (`cosmiconfig`) and no MJ dependencies, so it can be the first
|
|
85
|
+
MJ-shaped thing a process imports.
|
|
86
|
+
|
|
87
|
+
## How a call proceeds
|
|
88
|
+
|
|
89
|
+
```mermaid
|
|
90
|
+
sequenceDiagram
|
|
91
|
+
autonumber
|
|
92
|
+
participant Host as Host process<br/>(mjapi, cli:sync:push, mcp, …)
|
|
93
|
+
participant L as LoadDynamicPackages
|
|
94
|
+
participant D as Discovery
|
|
95
|
+
participant M as Mode
|
|
96
|
+
participant R as Resolution
|
|
97
|
+
participant CF as ClassFactory
|
|
98
|
+
|
|
99
|
+
Host->>L: { processId, config, configFilePath, tier }
|
|
100
|
+
L->>M: ResolveDynamicPackagesMode(env, option, policy)
|
|
101
|
+
M-->>L: 'load' | 'none' (+ source)
|
|
102
|
+
L->>D: codeGeneration.packages → dynamicPackages.<tier>[] → mj-app.json
|
|
103
|
+
D-->>L: ordered candidates (tagged generated / config / manifest)
|
|
104
|
+
L->>L: mergeCandidates() — dedupe by PackageName, config entry is authority
|
|
105
|
+
alt mode 'none'
|
|
106
|
+
L-->>Host: every candidate in Skipped (mode-none)
|
|
107
|
+
else mode 'load'
|
|
108
|
+
loop each candidate, in order
|
|
109
|
+
L->>L: Enabled === false? → Skipped (disabled)
|
|
110
|
+
L->>L: MatchesProcess(processId, entry)? no → Skipped (process-filter)
|
|
111
|
+
L->>L: already loaded in this process? → Loaded (RanStartupExport: false)
|
|
112
|
+
L->>R: importFromHost(name, configFilePath)
|
|
113
|
+
R-->>L: module | resolution failure | load error
|
|
114
|
+
opt manifest entry, resolution failed
|
|
115
|
+
L->>R: FindWorkspacePackageDir → import entry file by path
|
|
116
|
+
end
|
|
117
|
+
Note over CF: importing the module fires @RegisterClass
|
|
118
|
+
L->>L: run StartupExport (if named and exported)
|
|
119
|
+
end
|
|
120
|
+
L-->>Host: DynamicPackagesReport
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Two properties of that loop are deliberate and worth knowing:
|
|
125
|
+
|
|
126
|
+
- **Filters run before the cache check.** An entry that is disabled or out of scope stays skipped
|
|
127
|
+
even when an earlier call in the same process loaded the package. The cache is not a bypass.
|
|
128
|
+
- **Order is priority.** `@RegisterClass` uses load-order priority — the last registration for a key
|
|
129
|
+
wins — so discovery is arranged generic-to-specific (see next section).
|
|
130
|
+
|
|
131
|
+
## Discovery sources and merge rules
|
|
132
|
+
|
|
133
|
+
Three sources, always in this order:
|
|
134
|
+
|
|
135
|
+
| # | Source | Comes from | `Source` tag | Why this position |
|
|
136
|
+
|---|---|---|---|---|
|
|
137
|
+
| 1 | Generated packages | `codeGeneration.packages.{entities,actions,graphqlResolvers}.name` (server tier) / `{entities,actions,angularForms}` (client tier) | `generated` | The host's own schema subclasses — the most generic layer |
|
|
138
|
+
| 2 | Installed Open Apps | `dynamicPackages.server[]` / `dynamicPackages.client[]`, written by `mj app install` | `config` | Apps layered on the host |
|
|
139
|
+
| 3 | The app you are standing in | `mj-app.json` next to the config (or `appManifestDir`): `packages.shared` then `packages.<tier>` | `manifest` | The most local definition overrides everything |
|
|
140
|
+
|
|
141
|
+
```mermaid
|
|
142
|
+
flowchart TD
|
|
143
|
+
G["1 · generated<br/>codeGeneration.packages"] --> C["2 · config<br/>dynamicPackages.<tier>[]"] --> M["3 · manifest<br/>mj-app.json packages.shared + packages.<tier>"]
|
|
144
|
+
M --> MG["mergeCandidates()"]
|
|
145
|
+
MG --> OUT["one candidate per PackageName<br/>+ duplicates → Skipped (duplicate)"]
|
|
146
|
+
style G fill:#eef,stroke:#88a
|
|
147
|
+
style C fill:#efe,stroke:#8a8
|
|
148
|
+
style M fill:#fee,stroke:#a88
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`mergeCandidates` collapses entries that name the same package:
|
|
152
|
+
|
|
153
|
+
- The **`config` entry is the operator's authority**. It carries `Enabled` (what `mj app disable`
|
|
154
|
+
writes) and the `Processes` / `ExcludeProcesses` scoping, so when an `mj-app.json` beside the
|
|
155
|
+
config names a package the config also names, the config entry decides whether and where it loads.
|
|
156
|
+
- The **manifest's on-disk location is kept as the resolution fallback** (`WorkspaceHome`), so a
|
|
157
|
+
package the host cannot `require.resolve` still loads from the workspace.
|
|
158
|
+
- Every later duplicate is returned separately and lands in `report.Skipped` with `Reason: 'duplicate'`.
|
|
159
|
+
|
|
160
|
+
Manifest discovery reads `mj-app.json` from the config file's directory by default; pass
|
|
161
|
+
`appManifestDir` to point elsewhere, or `discoverAppManifest: false` to skip it. `includeGeneratedPackages: false`
|
|
162
|
+
skips source 1.
|
|
163
|
+
|
|
164
|
+
## Process IDs
|
|
165
|
+
|
|
166
|
+
Every host names itself with a lowercase, colon-separated ID. IDs are hierarchical: a pattern matches
|
|
167
|
+
an ID when it equals the ID or names one of its ancestor **segments**.
|
|
168
|
+
|
|
169
|
+
```mermaid
|
|
170
|
+
flowchart TD
|
|
171
|
+
ANY["'*' (matches everything)"]
|
|
172
|
+
ANY --> MJAPI["mjapi"]
|
|
173
|
+
ANY --> MCP["mcp"]
|
|
174
|
+
ANY --> A2A["a2a"]
|
|
175
|
+
ANY --> IT["integration-tests"]
|
|
176
|
+
ANY --> CR["component-registry"]
|
|
177
|
+
ANY --> CLI["cli"]
|
|
178
|
+
CLI --> SYNC["cli:sync"]
|
|
179
|
+
SYNC --> SP["cli:sync:push"]
|
|
180
|
+
SYNC --> SPL["cli:sync:pull"]
|
|
181
|
+
CLI --> CG["cli:codegen"]
|
|
182
|
+
CLI --> APP["cli:app"]
|
|
183
|
+
APP --> AI["cli:app:install"]
|
|
184
|
+
CLI --> CAI["cli:ai"]
|
|
185
|
+
CAI --> CAIR["cli:ai:agents:run"]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| Pattern | Matches | Does not match |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| `cli` | `cli`, `cli:sync:push`, `cli:codegen` | `mjapi` |
|
|
191
|
+
| `cli:sync` | `cli:sync`, `cli:sync:push` | `cli:syncother`, `cli:migrate` |
|
|
192
|
+
| `cli:sync:push` | `cli:sync:push` only | `cli:sync:pull` |
|
|
193
|
+
| `*` | everything | — |
|
|
194
|
+
|
|
195
|
+
`CliProcessId('sync push')` and `CliProcessId('sync:push')` both build `cli:sync:push` from an oclif
|
|
196
|
+
command ID. `NormalizeProcessId` trims, lowercases and collapses whitespace around the separators.
|
|
197
|
+
`ProcessIdMatches`, `MatchesProcess` and `ResolveMostSpecific` are the three matching primitives, all
|
|
198
|
+
exported for hosts that need to evaluate scoping themselves.
|
|
199
|
+
|
|
200
|
+
## Scoping entries per process
|
|
201
|
+
|
|
202
|
+
`dynamicPackages.server[]` stays the single source of truth that `mj app install` writes. Two optional,
|
|
203
|
+
hand-authored fields scope an entry:
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
dynamicPackages: {
|
|
207
|
+
server: [
|
|
208
|
+
// written by `mj app install` — no Processes = loads wherever the server tier loads
|
|
209
|
+
{ PackageName: '@mj-biz-apps/orders-server', StartupExport: 'LoadBizAppsOrdersServer', AppName: 'mj-bizapps-orders', Enabled: true },
|
|
210
|
+
// only when the CLI runs any `mj sync` command
|
|
211
|
+
{ PackageName: '@acme/demo-seed-server', StartupExport: 'LoadDemoSeed', Processes: ['cli:sync'] },
|
|
212
|
+
// everywhere except CodeGen
|
|
213
|
+
{ PackageName: '@acme/audit-server', StartupExport: 'LoadAudit', ExcludeProcesses: ['cli:codegen'] },
|
|
214
|
+
],
|
|
215
|
+
// optional per-process on/off switch; the most specific key wins
|
|
216
|
+
policy: { 'cli:codegen': 'none' },
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`Processes` is evaluated first (omitted or empty means everywhere the tier loads), then
|
|
221
|
+
`ExcludeProcesses`. An entry that fails either lands in `report.Skipped` with `Reason: 'process-filter'`.
|
|
222
|
+
|
|
223
|
+
Scoping only applies to processes that run the loader. The `mj` CLI's *light* commands — `mj migrate`
|
|
224
|
+
(and `migrate create` / `migrate convert`), `mj install`, `mj clean`, `mj bundle`, `mj doctor`,
|
|
225
|
+
`mj version`, `help` and the `usage` pages — skip the class-registration manifest for instant
|
|
226
|
+
startup and never load app packages, so a `Processes: ['cli:migrate']` entry or a `cli:migrate`
|
|
227
|
+
policy key has no effect: migrations never run app entity subclasses in any mode.
|
|
228
|
+
|
|
229
|
+
## Mode: turning it off
|
|
230
|
+
|
|
231
|
+
Mode is resolved once per call, highest precedence first:
|
|
232
|
+
|
|
233
|
+
```mermaid
|
|
234
|
+
flowchart LR
|
|
235
|
+
ENV["MJ_DYNAMIC_PACKAGES<br/>env var"] -->|unset or invalid| OPT["mode option<br/>(programmatic, e.g. a CLI flag)"]
|
|
236
|
+
OPT -->|unset| POL["dynamicPackages.policy<br/>most specific process key"]
|
|
237
|
+
POL -->|no match or invalid| DEF["'load'"]
|
|
238
|
+
ENV -->|"load / none"| DONE["resolved<br/>(ModeSource: env)"]
|
|
239
|
+
OPT -->|set| DONE2["resolved<br/>(ModeSource: option)"]
|
|
240
|
+
POL -->|"load / none"| DONE3["resolved<br/>(ModeSource: policy)"]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`MJ_DYNAMIC_PACKAGES=none` (also `off`, `skip`, `false`, `0`; the positive spellings `on`, `true`,
|
|
244
|
+
`1`, `full` mean `load`) disables loading for a single invocation. The `mj` CLI exposes it as the
|
|
245
|
+
global `--no-app-packages` flag. An invalid value never crashes a process: it is reported on the warn
|
|
246
|
+
path (`Ignoring invalid …`) and precedence falls through to the next source.
|
|
247
|
+
|
|
248
|
+
Mode `none` skips every dynamic package: the installed Open App packages *and* the host's own
|
|
249
|
+
`codeGeneration.packages` (generated entities/actions/resolvers), so a host that relies on the latter
|
|
250
|
+
loses its custom entity subclasses too for that run. MJ core's own server subclasses still register
|
|
251
|
+
through the host's manifest, so a "raw" run is raw for app and host entities only — which is why the
|
|
252
|
+
CLI flag is not called `--raw`.
|
|
253
|
+
|
|
254
|
+
## Nested hosts
|
|
255
|
+
|
|
256
|
+
Some MJ processes host other MJ processes: `mj ai agents run` imports `@memberjunction/ai-cli`, and
|
|
257
|
+
`mj test …` imports `@memberjunction/testing-cli`, each of which has its own provider bootstrap that
|
|
258
|
+
calls the loader. Without coordination the inner host would evaluate scoping under its standalone
|
|
259
|
+
identity (`ai-cli`, `testing-cli`) a moment after the outer host evaluated it under `cli:ai:agents:run`,
|
|
260
|
+
and an entry the operator excluded from `cli` would load anyway.
|
|
261
|
+
|
|
262
|
+
```mermaid
|
|
263
|
+
sequenceDiagram
|
|
264
|
+
participant U as operator
|
|
265
|
+
participant CLI as mj (prerun hook)
|
|
266
|
+
participant ENV as process.env
|
|
267
|
+
participant AI as @memberjunction/ai-cli bootstrap
|
|
268
|
+
|
|
269
|
+
U->>CLI: mj ai agents run …
|
|
270
|
+
CLI->>CLI: LoadDynamicPackages({ processId: 'cli:ai:agents:run' })
|
|
271
|
+
CLI->>ENV: MJ_DYNAMIC_PACKAGES_PROCESS = 'cli:ai:agents:run'
|
|
272
|
+
CLI->>AI: import + run command
|
|
273
|
+
AI->>ENV: EffectiveProcessId('ai-cli') → 'cli:ai:agents:run'
|
|
274
|
+
AI->>AI: LoadDynamicPackages({ processId: 'cli:ai:agents:run' })
|
|
275
|
+
Note over AI: same scoping decisions as the outer host;<br/>already-loaded packages come back cached, startup export not re-run
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The outer host publishes its ID through `DYNAMIC_PACKAGES_PROCESS_ENV_VAR` (`MJ_DYNAMIC_PACKAGES_PROCESS`);
|
|
279
|
+
the inner host reads it with `EffectiveProcessId(hostDefault)`, which falls back to `hostDefault` when
|
|
280
|
+
the variable is unset (the standalone `mj-ai` case). The mode env var propagates the same way, since it
|
|
281
|
+
is plain `process.env`.
|
|
282
|
+
|
|
283
|
+
## Running inside an Open App repository
|
|
284
|
+
|
|
285
|
+
When `mj-app.json` sits next to the config (or in `appManifestDir`), the loader also imports that
|
|
286
|
+
app's `packages.shared` libraries and its `packages.server` / `packages.client` bootstrap packages,
|
|
287
|
+
running each `startupExport`. Under pnpm's strict layout nothing at the repo root can
|
|
288
|
+
`require.resolve` a workspace member, so the loader finds the package under `code.sourceDirectory`
|
|
289
|
+
(default `packages/`) and imports it by path. A member that exists but has not been built yet (its
|
|
290
|
+
entry file is missing) is reported as **not found**, with the file the build is expected to produce,
|
|
291
|
+
on the info path — the expected state before the app's own build, not an error.
|
|
292
|
+
|
|
293
|
+
Dependency apps are **not** walked from the manifest: installed ones are already in the host's
|
|
294
|
+
`dynamicPackages`, and dev-linked ones ride the workspace's env-supplied entries.
|
|
295
|
+
|
|
296
|
+
## Resolution: how a package is found
|
|
297
|
+
|
|
298
|
+
A bare `import(name)` resolves from *this* package, which cannot declare packages whose names come from
|
|
299
|
+
configuration. npm's hoisted layout lets that work by accident; pnpm's strict layout does not, because
|
|
300
|
+
the packages are declared by (and linked into) the **host** application. `importFromHost` therefore
|
|
301
|
+
tries the bare import first and then retries from each host anchor:
|
|
302
|
+
|
|
303
|
+
```mermaid
|
|
304
|
+
flowchart TD
|
|
305
|
+
S["import(name)"] -->|ok| OK["module"]
|
|
306
|
+
S -->|resolution failure| A1["createRequire(configFilePath).resolve(name)"]
|
|
307
|
+
A1 -->|ok| IMP["import(resolved file)"]
|
|
308
|
+
A1 -->|cannot see it| A2["createRequire(cwd/package.json).resolve(name)"]
|
|
309
|
+
A2 -->|ok| IMP
|
|
310
|
+
A2 -->|cannot see it| A3["createRequire(process.argv[1]).resolve(name)"]
|
|
311
|
+
A3 -->|ok| IMP
|
|
312
|
+
A3 -->|cannot see it| MF{"manifest entry with<br/>WorkspaceHome?"}
|
|
313
|
+
MF -->|yes| WS["FindWorkspacePackageDir → entry file<br/>(package.json exports / main / index.js)"]
|
|
314
|
+
WS -->|file exists| IMP
|
|
315
|
+
WS -->|not built| NF["NotFound (info: 'build it first')"]
|
|
316
|
+
MF -->|no| NF2["NotFound (info: run npm install?)"]
|
|
317
|
+
IMP -->|throws| FAIL["Failed (warn: the module's OWN error)"]
|
|
318
|
+
IMP -->|ok| OK
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Resolution and evaluation are kept apart on purpose: an anchor that cannot *see* the package means
|
|
322
|
+
"try the next anchor", but once an anchor resolves it, anything that fails while loading (a missing
|
|
323
|
+
transitive dependency, a throw in top-level code) is the module's own problem and surfaces as-is —
|
|
324
|
+
never masked by the original "cannot find package" error. `isOwnResolutionFailure` checks that the
|
|
325
|
+
quoted subject of the error is *this* package name, so a missing transitive dependency is reported as
|
|
326
|
+
`Failed`, not `NotFound`.
|
|
327
|
+
|
|
328
|
+
`resolvePackageJsonFromHost` uses the same anchors to find a package's `package.json` (for hosts that
|
|
329
|
+
read `memberjunction.serverExtensions` off it), tolerating an exports map that omits `./package.json`
|
|
330
|
+
by resolving the main entry and walking up.
|
|
331
|
+
|
|
332
|
+
## Loading twice in one process
|
|
333
|
+
|
|
334
|
+
The loader remembers what it has loaded, on a `globalThis` symbol store rather than a module variable,
|
|
335
|
+
so two physical copies of this package (two dist paths under pnpm, a bundled and an unbundled copy)
|
|
336
|
+
still agree. A second call in the same process gets the cached module back in `Loaded` so it can still
|
|
337
|
+
read `RESOLVER_PATHS` / `MJ_SERVER_EXTENSIONS`, but the `StartupExport` is not run again
|
|
338
|
+
(`RanStartupExport: false`). ESM already caches the module; this only prevents the double hook.
|
|
339
|
+
`ResetLoadedDynamicPackages()` clears the store — a test seam, not something a host calls.
|
|
340
|
+
|
|
341
|
+
## The report
|
|
342
|
+
|
|
343
|
+
`LoadDynamicPackages` never throws for a package problem (it throws only for a missing `processId`).
|
|
344
|
+
Everything it decided is in the returned `DynamicPackagesReport`:
|
|
345
|
+
|
|
346
|
+
| Field | Meaning |
|
|
347
|
+
|---|---|
|
|
348
|
+
| `ProcessId`, `Tier` | Normalized inputs |
|
|
349
|
+
| `Mode`, `ModeSource` | What was resolved and from where (`env` / `option` / `policy` / `default`) |
|
|
350
|
+
| `Loaded[]` | `{ Entry, Source, WorkspaceHome?, Module, RanStartupExport }` — `Module` is the namespace object hosts read conventions off |
|
|
351
|
+
| `Skipped[]` | `{ …, Reason }` with `disabled`, `process-filter`, `mode-none`, or `duplicate` |
|
|
352
|
+
| `NotFound[]` | No anchor could resolve it, or an unbuilt workspace member — logged on the info path |
|
|
353
|
+
| `Failed[]` | `{ …, Error }` — resolved but threw while loading; logged on the warn path |
|
|
354
|
+
|
|
355
|
+
A named `StartupExport` that the module does not export is a real misconfiguration (renamed export,
|
|
356
|
+
stale config) and is warned about; the package still counts as `Loaded` with `RanStartupExport: false`.
|
|
357
|
+
|
|
358
|
+
## Loggers
|
|
359
|
+
|
|
360
|
+
| Logger | Use when |
|
|
361
|
+
|---|---|
|
|
362
|
+
| `ConsoleDynamicPackagesLogger` (default) | Plain console, the way MJAPI has always logged boot |
|
|
363
|
+
| `StderrDynamicPackagesLogger` | Stdout is a machine-readable envelope (`--format=json`, `--output=json`) — progress and warnings go to stderr, verbose detail is dropped |
|
|
364
|
+
| `SilentDynamicPackagesLogger` | The host reads the report and renders it itself |
|
|
365
|
+
|
|
366
|
+
Any object with `info(message)` and `warn(message, error?)` (and optionally `verbose(message)`)
|
|
367
|
+
satisfies `DynamicPackagesLogger`; the `mj` CLI passes one that routes through its own output.
|
|
368
|
+
|
|
369
|
+
## Contract for `StartupExport`
|
|
370
|
+
|
|
371
|
+
The loader runs before any database provider exists. A startup export registers classes and returns —
|
|
372
|
+
it may be sync or async, and it must not touch a provider, exactly the contract MJAPI has always
|
|
373
|
+
imposed. It runs at most once per process for a given package.
|
|
374
|
+
|
|
375
|
+
## API
|
|
376
|
+
|
|
377
|
+
| Export | Purpose |
|
|
378
|
+
|---|---|
|
|
379
|
+
| `LoadDynamicPackages(options)` | The loader. Never throws for a package problem; returns a `DynamicPackagesReport`. |
|
|
380
|
+
| `DiscoverMJConfig(searchFrom?, { searchStrategy })` | cosmiconfig discovery of the raw `mj.config.cjs` + its path. `searchStrategy` defaults to `'global'` (walk up to the home directory, what MJAPI / the CLI / CodeGen do); pass `'none'` when the host's own config loader only looks in the working directory, so the packages come from the same file the database settings came from. |
|
|
381
|
+
| `mergeCandidates(candidates)` | The dedupe rule described above, exported for hosts that assemble their own candidate lists. |
|
|
382
|
+
| `ResetLoadedDynamicPackages()` | Test seam: forget what has been loaded. |
|
|
383
|
+
| `ConsoleDynamicPackagesLogger`, `StderrDynamicPackagesLogger`, `SilentDynamicPackagesLogger` | Ready-made loggers. |
|
|
384
|
+
| `importFromHost`, `resolvePackageJsonFromHost`, `isResolutionFailure` | Host-anchored resolution (moved here from `@memberjunction/server-bootstrap`). |
|
|
385
|
+
| `CliProcessId`, `NormalizeProcessId`, `ProcessIdMatches`, `MatchesProcess`, `ResolveMostSpecific`, `ANY_PROCESS` | Process-ID utilities. |
|
|
386
|
+
| `EffectiveProcessId`, `DYNAMIC_PACKAGES_PROCESS_ENV_VAR` | Nested-host identity propagation. |
|
|
387
|
+
| `ResolveDynamicPackagesMode`, `DYNAMIC_PACKAGES_MODE_ENV_VAR` | Mode precedence. |
|
|
388
|
+
| `DiscoverGeneratedPackages`, `DiscoverAppManifestPackages`, `FindWorkspacePackageDir`, `ReadDynamicPackagesConfig`, `GENERATED_PACKAGE_TYPES_BY_TIER`, `APP_MANIFEST_FILE_NAME` | Discovery primitives. |
|
|
389
|
+
|
|
390
|
+
Every option and report field is documented inline in `src/types.ts`.
|
|
391
|
+
|
|
392
|
+
## Testing a host against it
|
|
393
|
+
|
|
394
|
+
Unit tests for the loader itself live in `src/__tests__/` and cover discovery, matching, mode
|
|
395
|
+
precedence, host-anchored resolution and the loader loop with stubbed imports.
|
|
396
|
+
|
|
397
|
+
The proof that a package **not statically imported anywhere** really registers into the calling
|
|
398
|
+
process's ClassFactory lives with the first host that needed it: `packages/MJCLI/src/__tests__/dynamic-packages.registration.test.ts`
|
|
399
|
+
loads a committed miniature Open App (`fixtures/fixture-open-app`, an `mj-app.json` plus plain-ESM
|
|
400
|
+
entities and server packages) through both the `mj-app.json` path and a `dynamicPackages.server[]`
|
|
401
|
+
entry resolved through a throwaway host `node_modules`, then asserts on `MJGlobal.Instance.ClassFactory.GetRegistration(BaseEntity, …)`.
|
|
402
|
+
Copy that shape to prove the same thing for a new host.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { DiscoveredDynamicPackage, DynamicPackageTier, DynamicPackagesConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `codeGeneration.packages.<type>.name` keys that make sense for each tier. `angularForms` is
|
|
4
|
+
* a browser library and must never be imported into a Node process; `graphqlResolvers` has no
|
|
5
|
+
* meaning in a browser bundle.
|
|
6
|
+
*/
|
|
7
|
+
export declare const GENERATED_PACKAGE_TYPES_BY_TIER: Record<DynamicPackageTier, readonly string[]>;
|
|
8
|
+
/** The manifest file every Open App repository carries at its root. */
|
|
9
|
+
export declare const APP_MANIFEST_FILE_NAME = "mj-app.json";
|
|
10
|
+
/** Reads the `dynamicPackages` section off a raw config object, tolerating any shape. */
|
|
11
|
+
export declare function ReadDynamicPackagesConfig(config: Record<string, unknown> | null | undefined): DynamicPackagesConfig;
|
|
12
|
+
/** Entries for the host's own generated packages (`codeGeneration.packages`). */
|
|
13
|
+
export declare function DiscoverGeneratedPackages(config: Record<string, unknown> | null | undefined, tier: DynamicPackageTier): DiscoveredDynamicPackage[];
|
|
14
|
+
export interface AppManifestDiscovery {
|
|
15
|
+
/** Directory the manifest was read from. */
|
|
16
|
+
RepoDir: string;
|
|
17
|
+
/** `mj-app.json` `name`. */
|
|
18
|
+
AppName: string;
|
|
19
|
+
/** Directory under the repo that holds the app's workspace packages (`code.sourceDirectory`, default `packages`). */
|
|
20
|
+
SourceDirectory: string;
|
|
21
|
+
Entries: DiscoveredDynamicPackage[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Reads `mj-app.json` from `repoDir` and returns the packages a process of `tier` should
|
|
25
|
+
* load: `shared` libraries first (entities/actions register on import), then the tier's own
|
|
26
|
+
* packages, with each `startupExport` carried through. Returns `null` when there is no
|
|
27
|
+
* manifest, and throws only when a manifest exists but is not valid JSON — a corrupt file is
|
|
28
|
+
* a problem the operator must see, an absent one is the common case.
|
|
29
|
+
*/
|
|
30
|
+
export declare function DiscoverAppManifestPackages(repoDir: string, tier: DynamicPackageTier): AppManifestDiscovery | null;
|
|
31
|
+
/**
|
|
32
|
+
* Locates a workspace package by name under an app repo's source directory (one level deep,
|
|
33
|
+
* matching `code.sourceDirectory`), returning its directory. Used when the process runs inside
|
|
34
|
+
* the app's own repository: the package is a workspace member there, but under pnpm's strict
|
|
35
|
+
* layout nothing at the repo root can `require.resolve` it, so we find it on disk instead.
|
|
36
|
+
*/
|
|
37
|
+
export declare function FindWorkspacePackageDir(repoDir: string, sourceDirectory: string, packageName: string): string | null;
|
|
38
|
+
//# sourceMappingURL=discover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../src/discover.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,wBAAwB,EAAuB,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAE3H;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,EAAE,MAAM,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAGzF,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AAEpD,yFAAyF;AACzF,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,qBAAqB,CAWnH;AAkDD,iFAAiF;AACjF,wBAAgB,yBAAyB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EAClD,IAAI,EAAE,kBAAkB,GACzB,wBAAwB,EAAE,CAc5B;AAcD,MAAM,WAAW,oBAAoB;IACjC,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,qHAAqH;IACrH,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,wBAAwB,EAAE,CAAC;CACvC;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,GAAG,oBAAoB,GAAG,IAAI,CAwClH;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA0BpH"}
|