pict-microapp 1.0.0
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 +21 -0
- package/README.md +180 -0
- package/package.json +59 -0
- package/source/Pict-MicroApp-Compose.js +149 -0
- package/source/Pict-MicroApp-Manifest.js +249 -0
- package/source/Pict-MicroApp.js +486 -0
- package/types/Pict-MicroApp-Compose.d.ts +24 -0
- package/types/Pict-MicroApp-Compose.d.ts.map +1 -0
- package/types/Pict-MicroApp-Manifest.d.ts +88 -0
- package/types/Pict-MicroApp-Manifest.d.ts.map +1 -0
- package/types/Pict-MicroApp.d.ts +192 -0
- package/types/Pict-MicroApp.d.ts.map +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Steven Velozo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# pict-microapp
|
|
2
|
+
|
|
3
|
+
Present a large pict application as a small, focused one.
|
|
4
|
+
|
|
5
|
+
A **micro app** is not a fork and not a rebuild: it is the *same* application, booted the same way,
|
|
6
|
+
with a smaller surface. You keep every view, provider, template, style and bug fix the host app
|
|
7
|
+
already has — and an enhancement to the host lands in every micro app on its next build — but the
|
|
8
|
+
routes, the navigation and the landing page are the micro app's own.
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
const libHostApplication = require('../../some-big-app/source/Big-Application-Web.js');
|
|
12
|
+
const libMicroApp = require('pict-microapp');
|
|
13
|
+
|
|
14
|
+
class WidgetInspection extends libMicroApp.composeMicroApp(libHostApplication, require('./Widget-MicroApp-Manifest.js'),
|
|
15
|
+
{
|
|
16
|
+
HostRouterProviderHash: 'BigApplicationRouter',
|
|
17
|
+
NavigationGateMethod: '_applyCustomerNavGate',
|
|
18
|
+
HostPermittedMethod: '_navItemPermitted',
|
|
19
|
+
IsPrivilegedMethod: '_isSuperUser'
|
|
20
|
+
}) {}
|
|
21
|
+
|
|
22
|
+
module.exports = WidgetInspection;
|
|
23
|
+
module.exports.default_configuration = libHostApplication.default_configuration;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## The seam
|
|
27
|
+
|
|
28
|
+
A pict application registers every route it owns through one funnel: `PictRouter.addRoute`. That is
|
|
29
|
+
true of the application's own routes, of routes contributed by modules like `pict-section-workspace`,
|
|
30
|
+
and of the wildcard record-set routes from `pict-section-recordset`.
|
|
31
|
+
|
|
32
|
+
`pict-microapp` wraps that funnel with an allow list. The host's registration code runs completely
|
|
33
|
+
unmodified — it just finds that some of what it registered did not stick. Nothing else about the
|
|
34
|
+
boot sequence changes, which is what keeps a micro app cheap to build and cheap to keep working as
|
|
35
|
+
the host evolves.
|
|
36
|
+
|
|
37
|
+
Three seams, in order of how much they matter:
|
|
38
|
+
|
|
39
|
+
| Seam | What it does |
|
|
40
|
+
| --- | --- |
|
|
41
|
+
| `gateRouter()` | Wraps `addRoute` with the manifest's allow / deny / guard decision |
|
|
42
|
+
| `applyNavigationGraph()` / `applyNavigationGate()` | Swaps the navigation graph and its visibility filter |
|
|
43
|
+
| `applyDefaultRoute()` | Re-points the landing route |
|
|
44
|
+
|
|
45
|
+
## What this does not do
|
|
46
|
+
|
|
47
|
+
**It does not make the JavaScript bundle smaller.** The host's module graph is still bundled whole,
|
|
48
|
+
because the micro app extends the host's application class and that class requires everything. A
|
|
49
|
+
micro app slims the app the *user* sees, not the bytes the browser downloads.
|
|
50
|
+
|
|
51
|
+
That is usually the right trade. The alternative — composing an app out of deep subpath requires
|
|
52
|
+
into the host — genuinely shrinks the bundle, but it means reimplementing the host's boot sequence
|
|
53
|
+
and re-deciding it every time the host changes, which is exactly the coupling a micro app is trying
|
|
54
|
+
to avoid. Reach for it when download size becomes a real constraint, not before.
|
|
55
|
+
|
|
56
|
+
## The manifest
|
|
57
|
+
|
|
58
|
+
Everything a micro app declares lives in one plain object.
|
|
59
|
+
|
|
60
|
+
```js
|
|
61
|
+
module.exports =
|
|
62
|
+
{
|
|
63
|
+
Name: 'Widget Inspection',
|
|
64
|
+
Hash: 'WidgetInspection',
|
|
65
|
+
|
|
66
|
+
// Where the app lands, and where a blocked route bounces to.
|
|
67
|
+
DefaultRoute: '/WidgetDashboard',
|
|
68
|
+
|
|
69
|
+
Routes:
|
|
70
|
+
{
|
|
71
|
+
// Route patterns exactly as the host registers them. A trailing '*' is a prefix glob.
|
|
72
|
+
// An EMPTY list exposes everything — useful when you only want different navigation.
|
|
73
|
+
Allow:
|
|
74
|
+
[
|
|
75
|
+
'/WidgetDashboard',
|
|
76
|
+
'/WidgetDashboard/:View',
|
|
77
|
+
'/Widget/Workspace/:ID',
|
|
78
|
+
'/Widget/Workspace/:ID/:Tab',
|
|
79
|
+
'/Gadget/*'
|
|
80
|
+
],
|
|
81
|
+
|
|
82
|
+
// Removed even when Allow would admit them. Deny wins.
|
|
83
|
+
Deny: [],
|
|
84
|
+
|
|
85
|
+
// Wildcard record-set routes ('/PSRS/:RecordSet/List') are registered ONCE by the host and
|
|
86
|
+
// fan out over every entity it knows, so they cannot be refused per-entity at registration
|
|
87
|
+
// time. They are GUARDED instead: the route registers, but its handler bounces to Fallback
|
|
88
|
+
// when the resolved entity is not listed here. An empty list exposes every entity.
|
|
89
|
+
EntityParameter: 'RecordSet',
|
|
90
|
+
Entities: [ 'Widget', 'Gadget' ],
|
|
91
|
+
|
|
92
|
+
// A hash matching no surviving route lands on Fallback rather than on a blank page.
|
|
93
|
+
CatchUnmatched: true,
|
|
94
|
+
Fallback: '',
|
|
95
|
+
|
|
96
|
+
// Always admitted, whatever Allow says — the boot and logout plumbing.
|
|
97
|
+
AlwaysAllow: [ '/', '/Logout' ]
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
Navigation:
|
|
101
|
+
{
|
|
102
|
+
// The graph this micro app renders, in the host navigation module's own shape.
|
|
103
|
+
Sections: [ /* … */ ],
|
|
104
|
+
|
|
105
|
+
// Keep the host's capability / module / session gates …
|
|
106
|
+
HonorHostGates: true,
|
|
107
|
+
// … but drop its audience short-circuit. See below.
|
|
108
|
+
SuperUserSeesEverything: true
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
Branding: { Title: 'Widget Inspection' }
|
|
112
|
+
};
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Why `SuperUserSeesEverything` defaults to true
|
|
116
|
+
|
|
117
|
+
A host that serves many audiences usually trims its own large graph hard. The platform application
|
|
118
|
+
this module was extracted from, for example, shows platform super-users *only* the handful of
|
|
119
|
+
cross-customer destinations, because the full menu is meaningless without a customer context.
|
|
120
|
+
|
|
121
|
+
Applied to a micro app graph — which is *already* the slimmed surface — that trim empties the menu
|
|
122
|
+
completely, for exactly the staff building and demoing the app. So the default keeps the host's real
|
|
123
|
+
gates (session, module, capability) and drops only the audience short-circuit. A privileged session
|
|
124
|
+
holds no entitlements in a customer's own tenancy, so it skips the capability test too; otherwise
|
|
125
|
+
the menu blanks a second way the moment the capability map finishes loading.
|
|
126
|
+
|
|
127
|
+
## Compose options
|
|
128
|
+
|
|
129
|
+
`composeMicroApp(pHostApplicationClass, pManifest, pComposeOptions)` binds the manifest to one host's
|
|
130
|
+
naming. Every option is a service hash or a method name on the host.
|
|
131
|
+
|
|
132
|
+
| Option | Default | Meaning |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `ProviderHash` | `'MicroApp'` | Where the provider registers on the host's pict instance |
|
|
135
|
+
| `RouterProviderHash` | `'PictRouter'` | The `pict-router` provider — the route funnel |
|
|
136
|
+
| `NavigationProviderHash` | `'Pict-Navigation'` | The `pict-section-navigation` provider |
|
|
137
|
+
| `HostRouterProviderHash` | `RouterProviderHash` | The provider that owns `defaultRoute` (often an application router wrapping `pict-router`) |
|
|
138
|
+
| `AttachOn` | `'onAfterInitializeAsync'` | The lifecycle hook the gate installs from |
|
|
139
|
+
| `NavigationGateMethod` | `''` | Host method that installs the nav filter; overridden when named |
|
|
140
|
+
| `HostPermittedMethod` | `''` | Host method answering "may this session see this item?" |
|
|
141
|
+
| `IsPrivilegedMethod` | `''` | Host method answering "is this a privileged platform session?" |
|
|
142
|
+
|
|
143
|
+
### Getting `AttachOn` right
|
|
144
|
+
|
|
145
|
+
The gate must be installed **after the router provider exists** and **before the host registers its
|
|
146
|
+
routes**. In a typical pict application the router provider is created during `onInitializeAsync`
|
|
147
|
+
and routes are registered during the login / data-load cycle, which makes `onAfterInitializeAsync`
|
|
148
|
+
the correct hook — the composed override runs the gate, then calls the host's implementation.
|
|
149
|
+
|
|
150
|
+
If routes come up ungated, the hook fired too late. `MicroApp.routeTally` tells you immediately:
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
_Pict.providers.MicroApp.routeTally;
|
|
154
|
+
// { Name: 'Widget Inspection', Allowed: 24, Guarded: 7, Blocked: 96 }
|
|
155
|
+
_Pict.providers.MicroApp.blockedRoutes; // every pattern that did not survive
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`blockedRoutes` is the first place to look when a screen is missing: a route you expected to keep
|
|
159
|
+
but spelled differently than the host registers it shows up there.
|
|
160
|
+
|
|
161
|
+
## Sharing the host's DOM shell
|
|
162
|
+
|
|
163
|
+
Reused views render into the *host's* container addresses, which are baked into each view's
|
|
164
|
+
configuration. A micro app's `index.html` therefore has to reproduce the host's DOM contract —
|
|
165
|
+
the application container id, the loading-splash element, the dynamic-CSS `<style>` tag, and any
|
|
166
|
+
wrapper class the host's layout modes toggle. Only the chrome *around* that contract (the top bar,
|
|
167
|
+
the branding, the panels) is the micro app's to redesign.
|
|
168
|
+
|
|
169
|
+
This is the one place a micro app is genuinely coupled to its host. Keep the host's ids; change the
|
|
170
|
+
chrome.
|
|
171
|
+
|
|
172
|
+
## Installation
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
npm install pict-microapp
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pict-microapp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Present a large pict application as a small, focused one: a declarative manifest gates the host's routes, swaps its navigation graph and re-points its landing page, so a micro app reuses every view, provider and style the host already has.",
|
|
5
|
+
"main": "source/Pict-MicroApp.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "npx quack test",
|
|
8
|
+
"tests": "npx quack test -g",
|
|
9
|
+
"start": "node source/Pict-MicroApp.js",
|
|
10
|
+
"coverage": "npx quack coverage",
|
|
11
|
+
"build": "quack build",
|
|
12
|
+
"types": "tsc -p ."
|
|
13
|
+
},
|
|
14
|
+
"types": "types/Pict-MicroApp.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"source",
|
|
17
|
+
"types"
|
|
18
|
+
],
|
|
19
|
+
"directories": {
|
|
20
|
+
"test": "test"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/fable-retold/pict-microapp.git"
|
|
25
|
+
},
|
|
26
|
+
"author": "steven velozo <steven@velozo.com>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/fable-retold/pict-microapp/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/fable-retold/pict-microapp#readme",
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"browser-env": "^3.3.0",
|
|
34
|
+
"chai": "^4.3.10",
|
|
35
|
+
"pict": "^1.0.372",
|
|
36
|
+
"pict-application": "^1.0.34",
|
|
37
|
+
"pict-router": "^1.0.10",
|
|
38
|
+
"quackage": "^1.3.0",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
},
|
|
41
|
+
"mocha": {
|
|
42
|
+
"diff": true,
|
|
43
|
+
"extension": [
|
|
44
|
+
"js"
|
|
45
|
+
],
|
|
46
|
+
"package": "./package.json",
|
|
47
|
+
"reporter": "spec",
|
|
48
|
+
"slow": "75",
|
|
49
|
+
"timeout": "5000",
|
|
50
|
+
"ui": "tdd",
|
|
51
|
+
"watch-files": [
|
|
52
|
+
"source/**/*.js",
|
|
53
|
+
"test/**/*.js"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"pict-provider": "^1.0.13"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pict MicroApp — compose a micro app class from a host application class and a manifest.
|
|
3
|
+
*
|
|
4
|
+
* This is the one-liner an app author writes:
|
|
5
|
+
*
|
|
6
|
+
* const libHostApplication = require('some-big-pict-application');
|
|
7
|
+
* const libMicroApp = require('pict-microapp');
|
|
8
|
+
*
|
|
9
|
+
* class MyApp extends libMicroApp.composeMicroApp(libHostApplication, require('./MyApp-Manifest.js'), {...}) {}
|
|
10
|
+
* module.exports = MyApp;
|
|
11
|
+
* module.exports.default_configuration = libHostApplication.default_configuration;
|
|
12
|
+
*
|
|
13
|
+
* The composed class inherits everything the host does and overrides exactly four things: it
|
|
14
|
+
* registers the MicroApp provider, installs the route gate before the host registers routes,
|
|
15
|
+
* swaps the navigation graph and its visibility gate, and re-points the landing route.
|
|
16
|
+
*
|
|
17
|
+
* @author steven velozo <steven@velozo.com>
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const _DEFAULT_COMPOSE_OPTIONS =
|
|
21
|
+
{
|
|
22
|
+
// Service hash the MicroApp provider is registered under on the host pict instance.
|
|
23
|
+
ProviderHash: 'MicroApp',
|
|
24
|
+
|
|
25
|
+
// Service hash of the pict-router provider (the route funnel the gate wraps).
|
|
26
|
+
RouterProviderHash: 'PictRouter',
|
|
27
|
+
|
|
28
|
+
// Service hash of the pict-section-navigation provider, when the host has one.
|
|
29
|
+
NavigationProviderHash: 'Pict-Navigation',
|
|
30
|
+
|
|
31
|
+
// Service hash of the provider that owns the host's `defaultRoute` property. Many hosts wrap
|
|
32
|
+
// pict-router in an application router provider; that wrapper is what holds the landing route.
|
|
33
|
+
// Defaults to RouterProviderHash when not set.
|
|
34
|
+
HostRouterProviderHash: '',
|
|
35
|
+
|
|
36
|
+
// The host lifecycle method the gate is installed from. The gate runs BEFORE the host's own
|
|
37
|
+
// implementation, so this must be a hook that fires after the router provider exists and
|
|
38
|
+
// before the host registers its routes.
|
|
39
|
+
AttachOn: 'onAfterInitializeAsync',
|
|
40
|
+
|
|
41
|
+
// Host method that installs the navigation visibility filter. Overridden so the micro app's
|
|
42
|
+
// gate wins. Set to '' to leave the host's navigation gate alone.
|
|
43
|
+
NavigationGateMethod: '',
|
|
44
|
+
|
|
45
|
+
// Host method that answers "may this session see this nav item?" — reused verbatim so
|
|
46
|
+
// capability and module gating keeps working against the micro app's own graph.
|
|
47
|
+
HostPermittedMethod: '',
|
|
48
|
+
|
|
49
|
+
// Host method that answers "is this a privileged platform session?" — used to keep the micro
|
|
50
|
+
// app's menu visible to platform staff who hold no tenant entitlements.
|
|
51
|
+
IsPrivilegedMethod: ''
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build a micro app application class from a host application class and a manifest.
|
|
56
|
+
*
|
|
57
|
+
* @param {Function} pHostApplicationClass - the host pict application class to present a subset of
|
|
58
|
+
* @param {Object} pManifest - the micro app manifest (see Pict-MicroApp-Manifest.js)
|
|
59
|
+
* @param {Object} [pComposeOptions] - the host binding options above
|
|
60
|
+
* @return {Function} a class extending the host application class
|
|
61
|
+
*/
|
|
62
|
+
function composeMicroApp(pHostApplicationClass, pManifest, pComposeOptions)
|
|
63
|
+
{
|
|
64
|
+
if (typeof pHostApplicationClass !== 'function')
|
|
65
|
+
{
|
|
66
|
+
throw new Error('pict-microapp: composeMicroApp requires a host application class.');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const libPictMicroApp = require('./Pict-MicroApp.js');
|
|
70
|
+
const tmpOptions = Object.assign({}, _DEFAULT_COMPOSE_OPTIONS, pComposeOptions || {});
|
|
71
|
+
const tmpHostRouterProviderHash = tmpOptions.HostRouterProviderHash || tmpOptions.RouterProviderHash;
|
|
72
|
+
const tmpAttachOn = tmpOptions.AttachOn;
|
|
73
|
+
// The host class is only known at runtime, so it is cast to a constructor signature for the
|
|
74
|
+
// benefit of the declaration build; the composed class is deliberately untyped beyond that.
|
|
75
|
+
const tmpHostClass = /** @type {new (...pArguments: Array<any>) => any} */ (pHostApplicationClass);
|
|
76
|
+
|
|
77
|
+
class PictMicroApplication extends tmpHostClass
|
|
78
|
+
{
|
|
79
|
+
constructor(pFable, pApplicationOptions, pServiceHash)
|
|
80
|
+
{
|
|
81
|
+
super(pFable, pApplicationOptions, pServiceHash);
|
|
82
|
+
|
|
83
|
+
/** @type {any} */
|
|
84
|
+
this.pict;
|
|
85
|
+
|
|
86
|
+
// The host has finished registering its views and providers by now, so the navigation
|
|
87
|
+
// provider exists and the graph can be swapped in place.
|
|
88
|
+
this.pict.addProvider(tmpOptions.ProviderHash,
|
|
89
|
+
{
|
|
90
|
+
Manifest: pManifest,
|
|
91
|
+
RouterProviderHash: tmpOptions.RouterProviderHash,
|
|
92
|
+
NavigationProviderHash: tmpOptions.NavigationProviderHash
|
|
93
|
+
}, libPictMicroApp);
|
|
94
|
+
|
|
95
|
+
this.MicroApp.applyNavigationGraph();
|
|
96
|
+
this.MicroApp.applyBranding();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The MicroApp provider for this application.
|
|
101
|
+
*
|
|
102
|
+
* @return {Object} the provider
|
|
103
|
+
*/
|
|
104
|
+
get MicroApp()
|
|
105
|
+
{
|
|
106
|
+
return this.pict.providers[tmpOptions.ProviderHash];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The gate has to be installed from a lifecycle hook, and the hook's name is configuration, so
|
|
111
|
+
// the override is assigned onto the prototype rather than written in the class body.
|
|
112
|
+
PictMicroApplication.prototype[tmpAttachOn] = async function (fCallback)
|
|
113
|
+
{
|
|
114
|
+
const tmpMicroApp = this.MicroApp;
|
|
115
|
+
if (tmpMicroApp)
|
|
116
|
+
{
|
|
117
|
+
tmpMicroApp.gateRouter();
|
|
118
|
+
tmpMicroApp.installNotFound();
|
|
119
|
+
tmpMicroApp.applyDefaultRoute(this.pict.providers[tmpHostRouterProviderHash]);
|
|
120
|
+
}
|
|
121
|
+
return pHostApplicationClass.prototype[tmpAttachOn].call(this, fCallback);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// The host's navigation gate is usually written for the host's own (much larger) audience mix.
|
|
125
|
+
// Replacing it keeps the micro app's menu from being trimmed to nothing.
|
|
126
|
+
if (tmpOptions.NavigationGateMethod)
|
|
127
|
+
{
|
|
128
|
+
PictMicroApplication.prototype[tmpOptions.NavigationGateMethod] = function ()
|
|
129
|
+
{
|
|
130
|
+
const tmpMicroApp = this.MicroApp;
|
|
131
|
+
if (!tmpMicroApp)
|
|
132
|
+
{
|
|
133
|
+
return pHostApplicationClass.prototype[tmpOptions.NavigationGateMethod].call(this);
|
|
134
|
+
}
|
|
135
|
+
const tmpHostPermitted = tmpOptions.HostPermittedMethod
|
|
136
|
+
? (pItem) => this[tmpOptions.HostPermittedMethod](pItem)
|
|
137
|
+
: null;
|
|
138
|
+
const tmpIsPrivileged = tmpOptions.IsPrivilegedMethod
|
|
139
|
+
? () => this[tmpOptions.IsPrivilegedMethod]()
|
|
140
|
+
: (() => false);
|
|
141
|
+
return tmpMicroApp.applyNavigationGate(tmpHostPermitted, { IsPrivileged: tmpIsPrivileged });
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return PictMicroApplication;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = composeMicroApp;
|
|
149
|
+
module.exports.default_compose_options = _DEFAULT_COMPOSE_OPTIONS;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pict MicroApp — manifest normalization and route matching.
|
|
3
|
+
*
|
|
4
|
+
* A micro app is described by a plain-object manifest: the routes it exposes, the navigation it
|
|
5
|
+
* renders, where it lands, and how it is branded. Everything in this file is pure — no pict, no
|
|
6
|
+
* DOM — so the matching rules can be unit tested headlessly.
|
|
7
|
+
*
|
|
8
|
+
* @author steven velozo <steven@velozo.com>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The shape a normalized manifest always has, whatever the author wrote.
|
|
13
|
+
*/
|
|
14
|
+
const _DEFAULT_MANIFEST =
|
|
15
|
+
{
|
|
16
|
+
// Display name of the micro app (used in logs and as the fallback document title).
|
|
17
|
+
Name: 'MicroApp',
|
|
18
|
+
// Stable identifier for this micro app.
|
|
19
|
+
Hash: 'MicroApp',
|
|
20
|
+
|
|
21
|
+
// The route the app lands on when it boots with no deep link, and where blocked routes bounce to.
|
|
22
|
+
// Written without the leading '#'.
|
|
23
|
+
DefaultRoute: '/',
|
|
24
|
+
|
|
25
|
+
Routes:
|
|
26
|
+
{
|
|
27
|
+
// Route patterns this micro app exposes, exactly as the host registers them (leading '#'
|
|
28
|
+
// optional). A trailing '*' makes an entry a prefix glob: '/Asset/*' allows every route
|
|
29
|
+
// whose pattern starts with '/Asset/'.
|
|
30
|
+
Allow: [],
|
|
31
|
+
// Patterns removed even when Allow would admit them. Deny wins.
|
|
32
|
+
Deny: [],
|
|
33
|
+
// Parameterized host routes that fan out over an entity name — the pict-section-recordset
|
|
34
|
+
// wildcards ('/PSRS/:RecordSet/List') are the motivating case. These cannot be dropped
|
|
35
|
+
// per-entity at registration time, so they are GUARDED instead: the route registers, but
|
|
36
|
+
// its handler bounces to the fallback route when the resolved entity is not in Entities.
|
|
37
|
+
EntityParameter: 'RecordSet',
|
|
38
|
+
Entities: [],
|
|
39
|
+
// When true (the default) an EntityParameter route is admitted and guarded. When false it
|
|
40
|
+
// is treated like any other route and must be listed in Allow to survive.
|
|
41
|
+
GuardEntityRoutes: true,
|
|
42
|
+
// Where a blocked entity bounces to. Defaults to DefaultRoute.
|
|
43
|
+
Fallback: '',
|
|
44
|
+
// When true, a hash that matches no surviving route lands on Fallback instead of nothing.
|
|
45
|
+
CatchUnmatched: true,
|
|
46
|
+
// Routes always admitted regardless of Allow — the boot, logout and default plumbing every
|
|
47
|
+
// host app needs to keep working.
|
|
48
|
+
AlwaysAllow: [ '/', '/Logout' ]
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
Navigation:
|
|
52
|
+
{
|
|
53
|
+
// The navigation graph this micro app renders, in the host navigation module's own shape.
|
|
54
|
+
// When empty the host's own graph is left alone.
|
|
55
|
+
Sections: [],
|
|
56
|
+
// Keep the host from replacing the graph later. Hosts rebuild their navigation as config and
|
|
57
|
+
// permissions resolve, which silently reverts the swap — usually after login, where it is
|
|
58
|
+
// easy to miss. Turn this off only if a micro app genuinely wants the host's later graph.
|
|
59
|
+
PinGraph: true,
|
|
60
|
+
// Drop the host's super-user short-circuit. A micro app's graph IS already the slimmed
|
|
61
|
+
// surface, so hiding all of it from platform staff is never what you want.
|
|
62
|
+
SuperUserSeesEverything: true,
|
|
63
|
+
// Keep the host's capability / module / session gates on top of the micro app graph.
|
|
64
|
+
HonorHostGates: true
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
Branding:
|
|
68
|
+
{
|
|
69
|
+
// document.title for the app.
|
|
70
|
+
Title: '',
|
|
71
|
+
// Title shown on the host's login screen, when the host supports one.
|
|
72
|
+
LoginTitle: ''
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Strip a route to the canonical form used for comparison: no leading '#', exactly one leading '/',
|
|
78
|
+
* no trailing '/' (except the bare root).
|
|
79
|
+
*
|
|
80
|
+
* @param {String} pRoute - a route pattern or hash
|
|
81
|
+
* @return {String} the canonical route pattern
|
|
82
|
+
*/
|
|
83
|
+
function normalizeRoute(pRoute)
|
|
84
|
+
{
|
|
85
|
+
let tmpRoute = String(pRoute == null ? '' : pRoute).trim();
|
|
86
|
+
if (tmpRoute.length < 1)
|
|
87
|
+
{
|
|
88
|
+
return '';
|
|
89
|
+
}
|
|
90
|
+
tmpRoute = tmpRoute.replace(/^#/, '');
|
|
91
|
+
tmpRoute = tmpRoute.replace(/^\/+/, '/');
|
|
92
|
+
if (tmpRoute.charAt(0) !== '/')
|
|
93
|
+
{
|
|
94
|
+
tmpRoute = `/${ tmpRoute }`;
|
|
95
|
+
}
|
|
96
|
+
// Collapse every trailing slash, not just the last one, so '//Widgets//' and '/Widgets' are the
|
|
97
|
+
// same route. The bare root keeps its slash.
|
|
98
|
+
tmpRoute = tmpRoute.replace(/\/+$/, '');
|
|
99
|
+
return (tmpRoute.length > 0) ? tmpRoute : '/';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Deep-ish merge of an author manifest over the defaults. Only the three known sub-objects are
|
|
104
|
+
* merged a level down; everything else is a straight overwrite.
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} [pManifest] - the author's manifest
|
|
107
|
+
* @return {Object} a fully populated manifest
|
|
108
|
+
*/
|
|
109
|
+
function normalizeManifest(pManifest)
|
|
110
|
+
{
|
|
111
|
+
const tmpManifest = (pManifest && (typeof pManifest === 'object')) ? pManifest : {};
|
|
112
|
+
const tmpNormalized = Object.assign({}, _DEFAULT_MANIFEST, tmpManifest);
|
|
113
|
+
|
|
114
|
+
tmpNormalized.Routes = Object.assign({}, _DEFAULT_MANIFEST.Routes, tmpManifest.Routes || {});
|
|
115
|
+
tmpNormalized.Navigation = Object.assign({}, _DEFAULT_MANIFEST.Navigation, tmpManifest.Navigation || {});
|
|
116
|
+
tmpNormalized.Branding = Object.assign({}, _DEFAULT_MANIFEST.Branding, tmpManifest.Branding || {});
|
|
117
|
+
|
|
118
|
+
tmpNormalized.DefaultRoute = normalizeRoute(tmpNormalized.DefaultRoute) || '/';
|
|
119
|
+
tmpNormalized.Routes.Allow = (tmpNormalized.Routes.Allow || []).map(normalizeRoute).filter((pEntry) => (pEntry.length > 0));
|
|
120
|
+
tmpNormalized.Routes.Deny = (tmpNormalized.Routes.Deny || []).map(normalizeRoute).filter((pEntry) => (pEntry.length > 0));
|
|
121
|
+
tmpNormalized.Routes.AlwaysAllow = (tmpNormalized.Routes.AlwaysAllow || []).map(normalizeRoute).filter((pEntry) => (pEntry.length > 0));
|
|
122
|
+
tmpNormalized.Routes.Fallback = normalizeRoute(tmpNormalized.Routes.Fallback) || tmpNormalized.DefaultRoute;
|
|
123
|
+
tmpNormalized.Routes.Entities = (tmpNormalized.Routes.Entities || []).map((pEntry) => String(pEntry));
|
|
124
|
+
|
|
125
|
+
return tmpNormalized;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Does a canonical route pattern match one allow/deny entry? An entry ending in '*' is a prefix
|
|
130
|
+
* glob; anything else is an exact pattern match (so '/Asset/Workspace/:ID' matches only the route
|
|
131
|
+
* registered with that exact parameter spelling).
|
|
132
|
+
*
|
|
133
|
+
* @param {String} pRoute - a canonical route pattern
|
|
134
|
+
* @param {String} pEntry - a canonical allow/deny entry
|
|
135
|
+
* @return {Boolean} whether the entry matches
|
|
136
|
+
*/
|
|
137
|
+
function routeMatchesEntry(pRoute, pEntry)
|
|
138
|
+
{
|
|
139
|
+
if (pEntry === '*')
|
|
140
|
+
{
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (pEntry.charAt(pEntry.length - 1) === '*')
|
|
144
|
+
{
|
|
145
|
+
return (pRoute.indexOf(pEntry.slice(0, -1)) === 0);
|
|
146
|
+
}
|
|
147
|
+
return (pRoute === pEntry);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Does this route pattern fan out over the manifest's entity parameter (for example
|
|
152
|
+
* '/PSRS/:RecordSet/List' with an EntityParameter of 'RecordSet')?
|
|
153
|
+
*
|
|
154
|
+
* @param {String} pRoute - a canonical route pattern
|
|
155
|
+
* @param {String} pEntityParameter - the parameter name that names an entity
|
|
156
|
+
* @return {Boolean} whether the route is entity-parameterized
|
|
157
|
+
*/
|
|
158
|
+
function routeIsEntityParameterized(pRoute, pEntityParameter)
|
|
159
|
+
{
|
|
160
|
+
if (!pEntityParameter)
|
|
161
|
+
{
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return (pRoute.indexOf(`:${ pEntityParameter }`) > -1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Decide what the gate should do with a route the host is trying to register.
|
|
169
|
+
*
|
|
170
|
+
* Returns one of three modes:
|
|
171
|
+
* Allow — register it untouched.
|
|
172
|
+
* Guard — register it, but wrap the handler so disallowed entities bounce to the fallback.
|
|
173
|
+
* Block — do not register it at all.
|
|
174
|
+
*
|
|
175
|
+
* @param {Object} pManifest - a normalized manifest
|
|
176
|
+
* @param {String} pRoute - the route pattern the host passed to addRoute
|
|
177
|
+
* @return {{Mode: String, Route: String, Reason: String}} the decision
|
|
178
|
+
*/
|
|
179
|
+
function decideRoute(pManifest, pRoute)
|
|
180
|
+
{
|
|
181
|
+
const tmpRoute = normalizeRoute(pRoute);
|
|
182
|
+
const tmpRoutes = pManifest.Routes;
|
|
183
|
+
|
|
184
|
+
if (tmpRoute.length < 1)
|
|
185
|
+
{
|
|
186
|
+
return { Mode: 'Block', Route: tmpRoute, Reason: 'EmptyRoute' };
|
|
187
|
+
}
|
|
188
|
+
for (const tmpEntry of tmpRoutes.Deny)
|
|
189
|
+
{
|
|
190
|
+
if (routeMatchesEntry(tmpRoute, tmpEntry))
|
|
191
|
+
{
|
|
192
|
+
return { Mode: 'Block', Route: tmpRoute, Reason: 'Denied' };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const tmpEntry of tmpRoutes.AlwaysAllow)
|
|
196
|
+
{
|
|
197
|
+
if (routeMatchesEntry(tmpRoute, tmpEntry))
|
|
198
|
+
{
|
|
199
|
+
return { Mode: 'Allow', Route: tmpRoute, Reason: 'AlwaysAllowed' };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// An empty Allow list means "expose everything" — a micro app that only wants a different
|
|
203
|
+
// navigation and landing page does not have to enumerate the host's whole route table.
|
|
204
|
+
if (tmpRoutes.Allow.length < 1)
|
|
205
|
+
{
|
|
206
|
+
return { Mode: 'Allow', Route: tmpRoute, Reason: 'NoAllowList' };
|
|
207
|
+
}
|
|
208
|
+
for (const tmpEntry of tmpRoutes.Allow)
|
|
209
|
+
{
|
|
210
|
+
if (routeMatchesEntry(tmpRoute, tmpEntry))
|
|
211
|
+
{
|
|
212
|
+
return { Mode: 'Allow', Route: tmpRoute, Reason: 'Allowed' };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (tmpRoutes.GuardEntityRoutes && routeIsEntityParameterized(tmpRoute, tmpRoutes.EntityParameter))
|
|
216
|
+
{
|
|
217
|
+
return { Mode: 'Guard', Route: tmpRoute, Reason: 'EntityParameterized' };
|
|
218
|
+
}
|
|
219
|
+
return { Mode: 'Block', Route: tmpRoute, Reason: 'NotAllowed' };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Is an entity name exposed by this micro app? An empty Entities list means every entity is
|
|
224
|
+
* exposed (the manifest author did not want to constrain the record-set surface).
|
|
225
|
+
*
|
|
226
|
+
* @param {Object} pManifest - a normalized manifest
|
|
227
|
+
* @param {String} pEntity - the entity name resolved from the route
|
|
228
|
+
* @return {Boolean} whether the entity is exposed
|
|
229
|
+
*/
|
|
230
|
+
function entityPermitted(pManifest, pEntity)
|
|
231
|
+
{
|
|
232
|
+
const tmpEntities = pManifest.Routes.Entities;
|
|
233
|
+
if (tmpEntities.length < 1)
|
|
234
|
+
{
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return (tmpEntities.indexOf(String(pEntity)) > -1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports =
|
|
241
|
+
{
|
|
242
|
+
default_manifest: _DEFAULT_MANIFEST,
|
|
243
|
+
normalizeManifest: normalizeManifest,
|
|
244
|
+
normalizeRoute: normalizeRoute,
|
|
245
|
+
routeMatchesEntry: routeMatchesEntry,
|
|
246
|
+
routeIsEntityParameterized: routeIsEntityParameterized,
|
|
247
|
+
decideRoute: decideRoute,
|
|
248
|
+
entityPermitted: entityPermitted
|
|
249
|
+
};
|