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 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
+ };