@appshell/runtime 1.0.0-alpha.100

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 Navaris Technologies, LLC
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,77 @@
1
+ # @appshell/runtime
2
+
3
+ The store that delivers a package's runtime configuration (`vars`) to it.
4
+
5
+ This is a **shared singleton**. There is exactly one instance per page, held in the
6
+ module federation share scope, and both the host and every package must declare it as
7
+ such. It replaced `window.__appshell_vars__<scope>`.
8
+
9
+ Most packages should not import this directly — use `@appshell/runtime/vars` below,
10
+ which supplies the scope for you.
11
+
12
+ ## `@appshell/runtime/vars`
13
+
14
+ The accessor most packages actually want. It reads *this* package's vars, with no scope
15
+ to pass and no way to name someone else's:
16
+
17
+ ```ts
18
+ import { getVars } from '@appshell/runtime/vars';
19
+
20
+ const { BACKGROUND_COLOR } = getVars<{ BACKGROUND_COLOR: string }>();
21
+ ```
22
+
23
+ It is a subpath rather than a main-entry export for a reason. `AppshellPlugin` compiles
24
+ the package's own scope into whatever imports it, and the main entry is the *shared*
25
+ module — one instance for the whole page — so a scope baked in there would be one
26
+ package's, and every other package would read the wrong vars.
27
+
28
+ The exact-match share key `'@appshell/runtime'` does not catch `'@appshell/runtime/vars'`,
29
+ so each package bundles its own copy of the accessor with its own scope, while the store
30
+ they all reach stays the single shared instance.
31
+
32
+ `getVars` throws `MissingScopeError` when the package was built without `AppshellPlugin`,
33
+ since nothing substituted the scope.
34
+
35
+ ## Contract
36
+
37
+ ```ts
38
+ setVars(scope, vars); // the host, immediately before loading a remote
39
+ readVars(scope); // a package, reading its own configuration
40
+ hasVars(scope); // branch instead of catch
41
+ ```
42
+
43
+ `@appshell/loader` calls `setVars` before it loads a remote, so a package's vars are in
44
+ place before its modules evaluate — including a read at module-eval time.
45
+
46
+ The first write for a scope wins and is frozen. Re-delivering the identical vars is a
47
+ no-op, because the same remote can be mounted more than once; replacing them throws
48
+ `VarsConflictError`.
49
+
50
+ `readVars` throws `MissingVarsError` rather than returning an empty object. A package
51
+ that silently renders with no configuration is the failure this replaced.
52
+
53
+ ## What this does and does not fix
54
+
55
+ Against the global it replaced, it fixes: unbounded, never-cleaned-up keys; silent
56
+ cross-package overwrite; the load-order coupling between host and remote; and being
57
+ untyped and undiscoverable.
58
+
59
+ It **does not make a scope's vars private.** Any code on the page can call
60
+ `readVars('SomeOtherScope')`, and a package that loads early can squat on a scope that
61
+ has not been delivered yet. Nothing short of a separate realm — an iframe, a worker —
62
+ would change that, and none of this is a security boundary. Do not put a secret in
63
+ `vars`; the registry serves them to the browser in the composition either way.
64
+
65
+ ## Setup
66
+
67
+ Both sides declare it, or each ends up with its own empty store:
68
+
69
+ ```js
70
+ new ModuleFederationPlugin({
71
+ shared: {
72
+ '@appshell/runtime': { singleton: true },
73
+ },
74
+ });
75
+ ```
76
+
77
+ `AppshellPlugin` fails the build when a package omits it.
package/dist/main.js ADDED
@@ -0,0 +1 @@
1
+ !function(e,r){if("object"==typeof exports&&"object"==typeof module)module.exports=r();else if("function"==typeof define&&define.amd)define([],r);else{var t=r();for(var s in t)("object"==typeof exports?exports:e)[s]=t[s]}}(this,(()=>(()=>{"use strict";var e={d:(r,t)=>{for(var s in t)e.o(t,s)&&!e.o(r,s)&&Object.defineProperty(r,s,{enumerable:!0,get:t[s]})},o:(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},r={};e.r(r),e.d(r,{ANONYMOUS:()=>a,MissingScopeError:()=>o,MissingVarsError:()=>t,VarsConflictError:()=>s,getIdentity:()=>n,hasVars:()=>d,readVars:()=>p,resetVars:()=>l,setVars:()=>c});class t extends Error{constructor(e,r){super(`No vars were delivered for '${e}'. ${r.length?`Vars are present for ${r.map((e=>`'${e}'`)).join(", ")} — check that '${e}' matches the module federation name the registry knows this package by.`:"Nothing has delivered vars on this page — check that the host and this package both declare '@appshell/runtime' as a shared singleton."}`),this.name="MissingVarsError",this.scope=e}}class s extends Error{constructor(e){super(`Vars for '${e}' were already delivered and cannot be replaced. A scope receives its vars once per page load.`),this.name="VarsConflictError",this.scope=e}}class o extends Error{constructor(){super("Cannot tell which package this is. '@appshell/runtime/vars' needs the scope that AppshellPlugin injects at build time — add it to this package's webpack plugins, or read the store directly with readVars(scope) from '@appshell/runtime'."),this.name="MissingScopeError"}}const a={authenticated:!1},n=()=>{if("undefined"==typeof window)return a;const e=window.__appshell_identity__;return(e=>{if("object"!=typeof e||null===e)return!1;const r=e;return!1===r.authenticated||!0===r.authenticated&&"string"==typeof r.subject&&"string"==typeof r.username&&Array.isArray(r.roles)})(e)?e:a},i=new Map,c=(e,r)=>{const t=i.get(e);var o,a;if(t){if(o=t,a=r,![...new Set([...Object.keys(o),...Object.keys(a)])].every((e=>o[e]===a[e])))throw new s(e)}else i.set(e,Object.freeze({...r}))},p=e=>{const r=i.get(e);if(!r)throw new t(e,[...i.keys()]);return r},d=e=>i.has(e),l=()=>{i.clear()};return r})()));
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A package asked for vars that were never delivered.
3
+ *
4
+ * Almost always one of three things: the package was loaded outside `remoteLoader`
5
+ * (a test, a storybook, a direct import), the host and the package resolved separate
6
+ * copies of `@appshell/runtime` because one of them failed to declare it as a
7
+ * singleton, or the scope compiled into the package is not the scope the registry
8
+ * knows it by.
9
+ */
10
+ export declare class MissingVarsError extends Error {
11
+ readonly scope: string;
12
+ constructor(scope: string, known: string[]);
13
+ }
14
+ /**
15
+ * Something tried to replace vars that were already delivered for a scope.
16
+ *
17
+ * The first write wins and is final. Within one page load a scope has exactly one
18
+ * vars object — both resolvers read it from the same `composition.vars[scope]` — so a
19
+ * differing second write is a bug or a package reaching for a scope that is not its own,
20
+ * and neither should be applied quietly.
21
+ */
22
+ export declare class VarsConflictError extends Error {
23
+ readonly scope: string;
24
+ constructor(scope: string);
25
+ }
26
+ /**
27
+ * The package was built without `AppshellPlugin`, so nothing substituted the scope the
28
+ * `@appshell/runtime/vars` accessor needs to know which vars are its own.
29
+ */
30
+ export declare class MissingScopeError extends Error {
31
+ constructor();
32
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Who is looking at the page, as the registry was told by the gateway.
3
+ *
4
+ * Unlike vars this is not scope-specific — there is one visitor per page, not one
5
+ * per package — so it lives in the main entry rather than needing the subpath
6
+ * trick `@appshell/runtime/vars` uses to get a per-package scope substituted.
7
+ *
8
+ * It is also not delivered through the vars store, and that is a constraint rather
9
+ * than a preference: `setVars` freezes on first write and throws on replacement,
10
+ * which is what stops one package overwriting another's configuration. Identity
11
+ * changes — sign-in, sign-out, token refresh — so it cannot live behind a
12
+ * write-once door without either breaking that guarantee or never updating.
13
+ */
14
+ /**
15
+ * A discriminated union rather than an optional user, so `username` cannot be read
16
+ * without the anonymous case having been handled. The compiler enforces what a
17
+ * convention would only ask for.
18
+ */
19
+ export type Identity = {
20
+ authenticated: false;
21
+ } | {
22
+ authenticated: true;
23
+ subject: string;
24
+ username: string;
25
+ roles: string[];
26
+ };
27
+ export declare const ANONYMOUS: Identity;
28
+ declare global {
29
+ interface Window {
30
+ __appshell_identity__?: unknown;
31
+ }
32
+ }
33
+ /**
34
+ * The visitor, or the anonymous identity when there is none.
35
+ *
36
+ * Never throws and never returns undefined — an absent or malformed value is
37
+ * anonymous, which is a state every caller already has to handle. That is the
38
+ * opposite of `readVars`, which throws when a scope is missing, and the difference
39
+ * is deliberate: a package with no configuration cannot do its job, while a package
40
+ * with no signed-in user usually can and simply renders differently.
41
+ *
42
+ * This is a typed accessor, not a boundary. The value is inlined into the document
43
+ * and any script on the page can read it directly, exactly as with vars. Treat it
44
+ * as what the server said about the visitor, never as proof of anything: it carries
45
+ * no token and grants nothing. Anything that matters is re-checked server-side.
46
+ */
47
+ export declare const getIdentity: () => Identity;
@@ -0,0 +1,6 @@
1
+ export { MissingScopeError, MissingVarsError, VarsConflictError } from './errors';
2
+ export { ANONYMOUS, getIdentity } from './identity';
3
+ export type { Identity } from './identity';
4
+ export { hasVars, readVars, resetVars, setVars } from './store';
5
+ export type { Vars } from './types';
6
+ export type { AppshellIndex, AppshellRemote, Metadata, ModuleFederationLoader, RemoteLoader, } from './wire';
@@ -0,0 +1,25 @@
1
+ import type { Vars } from './types';
2
+ /**
3
+ * Delivers a scope's vars. Called by `@appshell/loader` immediately before the remote
4
+ * is loaded, so they are in place before the package's modules evaluate.
5
+ *
6
+ * The first write for a scope wins and is frozen. Re-delivering the identical vars is a
7
+ * no-op — the same remote can be mounted more than once — but replacing them throws.
8
+ * That is what keeps one package from overwriting another's: by the time any package
9
+ * evaluates, the host has already written for it.
10
+ *
11
+ * It does not make a scope's vars *private*. Any code on the page can still read another
12
+ * scope by name, and nothing short of a separate realm would change that.
13
+ */
14
+ export declare const setVars: (scope: string, vars: Vars) => void;
15
+ /**
16
+ * Reads a scope's vars, throwing rather than handing back an empty object — a package
17
+ * that silently renders with no configuration is the failure this replaced.
18
+ *
19
+ * Prefer `getVars()` from `@appshell/runtime/vars`, which supplies the scope for you.
20
+ */
21
+ export declare const readVars: <TVars extends Vars = Vars>(scope: string) => TVars;
22
+ /** Whether a scope has been delivered, for callers that want to branch instead of catch. */
23
+ export declare const hasVars: (scope: string) => boolean;
24
+ /** Test seam. Not part of the contract a package should build on. */
25
+ export declare const resetVars: () => void;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * A package's runtime configuration, already merged by the registry — a package's
3
+ * declared vars, then the application's `overrides.vars`.
4
+ *
5
+ * Structurally identical to `AppshellComposition['vars'][scope]` in `@appshell/config`,
6
+ * but declared here rather than imported. This package is a shared singleton loaded
7
+ * into every micro-frontend on the page, so it carries no dependencies at all.
8
+ */
9
+ export type Vars = Record<string, string | number | undefined>;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The scope-aware accessor, deliberately kept out of this package's main entry.
3
+ *
4
+ * `@appshell/runtime` is shared as a singleton, so exactly one copy of it exists on the
5
+ * page — whichever package wins the share negotiation. A `__APPSHELL_SCOPE__` compiled
6
+ * into that copy would be one package's scope, and every other package would silently
7
+ * read the wrong vars. So this module ships as the `@appshell/runtime/vars` subpath,
8
+ * which the exact-match share key does not catch: each package bundles its own copy and
9
+ * gets its own scope substituted, while the store they all reach through the bare
10
+ * `@appshell/runtime` import below stays the one shared instance.
11
+ *
12
+ * That bare specifier is load-bearing. A relative import of the store here would bind to
13
+ * a local copy and bypass the singleton entirely.
14
+ */
15
+ import { type Vars } from '@appshell/runtime';
16
+ /**
17
+ * This package's runtime configuration, as the registry composed it — the vars the
18
+ * package declared in its `appshell.config.yaml`, with the application's
19
+ * `overrides.vars` layered over them.
20
+ *
21
+ * Throws rather than returning an empty object when nothing was delivered. A package
22
+ * that renders with no configuration at all is the failure this is here to prevent.
23
+ *
24
+ * ```ts
25
+ * import { getVars } from '@appshell/runtime/vars';
26
+ *
27
+ * const { BACKGROUND_COLOR } = getVars<{ BACKGROUND_COLOR: string }>();
28
+ * ```
29
+ */
30
+ export declare const getVars: <TVars extends Vars = Vars>() => TVars;
31
+ export { MissingScopeError, MissingVarsError, type Vars } from '@appshell/runtime';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * What the browser is told about a remote.
3
+ *
4
+ * These live here rather than in `@appshell/config` because config is build tooling — it
5
+ * carries yaml, lodash and axios — and a package that only wants to type a remote should
6
+ * not install a compiler to get it. This package is already the one every micro-frontend
7
+ * on the page shares, and it has no dependencies of its own to pass on.
8
+ *
9
+ * `AppshellManifest` deliberately stays in `@appshell/config`. It is a build artifact, and
10
+ * its `modules` field is Module Federation plugin options — build-time webpack
11
+ * configuration the browser never sees and this package should never drag in.
12
+ */
13
+ /** Arbitrary, application-defined description of a remote. Appshell never reads it. */
14
+ export type Metadata = Record<string, unknown>;
15
+ /**
16
+ * How a host loads a remote, named as one kind among possible kinds.
17
+ *
18
+ * scope, module and shareScope are Module Federation's vocabulary, and they used to sit
19
+ * at the top level of a remote where nothing said so — which is what would have made a
20
+ * second framework awkward, not the words themselves. Nested under a kind they are
21
+ * exactly correct, and something like single-spa slots beside them without reinterpreting
22
+ * anything already published.
23
+ *
24
+ * apiVersion follows the house style set by appshell.app.yaml, and the group is appshell's
25
+ * because appshell owns this schema: it decides which fields it stores, even though the
26
+ * fields describe federation concepts. If federation ever needs a different recipe that is
27
+ * a v2, and manifests published under v1 keep meaning what they meant — which matters,
28
+ * because they are immutable and hashed.
29
+ */
30
+ export type ModuleFederationLoader = {
31
+ apiVersion: 'federation.appshell.org/v1';
32
+ kind: 'ModuleFederation';
33
+ scope: string;
34
+ module: string;
35
+ shareScope?: string;
36
+ /** The entry file the build emits, conventionally remoteEntry.js. */
37
+ filename: string;
38
+ /**
39
+ * What built this, recorded as a fact rather than a constraint.
40
+ *
41
+ * Deliberately not a version range. A range is a claim about the environment, and a
42
+ * package cannot make it: it knows what built it and not what will load it, and the
43
+ * host is what registers the remote. Freezing one into an immutable artifact is the
44
+ * same mistake as freezing its origin.
45
+ */
46
+ builtWith?: string;
47
+ };
48
+ export type RemoteLoader = ModuleFederationLoader;
49
+ /** A remote the registry has already resolved, so the browser needs no manifest fetch. */
50
+ export type AppshellRemote<TMetadata = Metadata> = {
51
+ id: string;
52
+ manifestUrl: string;
53
+ remoteEntryUrl: string;
54
+ loader: RemoteLoader;
55
+ metadata: TMetadata;
56
+ };
57
+ /** Remote key to manifest url. */
58
+ export type AppshellIndex = Record<string, string>;
package/dist/vars.js ADDED
@@ -0,0 +1 @@
1
+ !function(e,r){if("object"==typeof exports&&"object"==typeof module)module.exports=r(require("@appshell/runtime"));else if("function"==typeof define&&define.amd)define(["@appshell/runtime"],r);else{var o="object"==typeof exports?r(require("@appshell/runtime")):r(e["@appshell/runtime"]);for(var t in o)("object"==typeof exports?exports:e)[t]=o[t]}}(this,(e=>(()=>{"use strict";var r={164(r){r.exports=e}},o={};function t(e){var n=o[e];if(void 0!==n)return n.exports;var i=o[e]={exports:{}};return r[e](i,i.exports,t),i.exports}t.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return t.d(r,{a:r}),r},t.d=(e,r)=>{for(var o in r)t.o(r,o)&&!t.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:r[o]})},t.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),t.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var n={};t.r(n),t.d(n,{MissingScopeError:()=>i.MissingScopeError,MissingVarsError:()=>i.MissingVarsError,getVars:()=>s});var i=t(164);const s=()=>(0,i.readVars)((()=>{if("undefined"==typeof __APPSHELL_SCOPE__)throw new i.MissingScopeError;return __APPSHELL_SCOPE__})());return n})()));
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@appshell/runtime",
3
+ "version": "1.0.0-alpha.100",
4
+ "description": "The shared singleton that delivers a package's runtime configuration to it",
5
+ "main": "dist/main.js",
6
+ "types": "dist/types/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/types/index.d.ts",
10
+ "default": "./dist/main.js"
11
+ },
12
+ "./vars": {
13
+ "types": "./dist/types/vars.d.ts",
14
+ "default": "./dist/vars.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "typesVersions": {
19
+ "*": {
20
+ "vars": [
21
+ "./dist/types/vars.d.ts"
22
+ ]
23
+ }
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/appshellhq/appshell.git"
28
+ },
29
+ "author": "Robert Hamilton <rh@navaris.com>",
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "registry": "https://registry.npmjs.org/"
33
+ },
34
+ "scripts": {
35
+ "build": "webpack --mode production && npm run build:types",
36
+ "build:types": "tsc --emitDeclarationOnly",
37
+ "clean": "rm -rf dist",
38
+ "prebuild": "npm run clean",
39
+ "start": "webpack --watch --mode development && npm run build:types",
40
+ "stats": "webpack --json > stats.json",
41
+ "typecheck": "tsc --noEmit"
42
+ },
43
+ "files": [
44
+ "dist"
45
+ ],
46
+ "keywords": [
47
+ "module federation",
48
+ "micro-frontends",
49
+ "appshell"
50
+ ],
51
+ "license": "MIT",
52
+ "gitHead": "2df8eb5b605b036d36af7e8d3ee2990bc2b63e5e"
53
+ }