@collagejs/core 0.4.0 → 0.5.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/README.md CHANGED
@@ -23,9 +23,10 @@
23
23
  ```typescript
24
24
  type UnmountFn = () => Promise<void>;
25
25
 
26
- interface CorePiece<TProps> {
26
+ interface CorePiece<TProps, TCap> {
27
27
  mount: (target: HTMLElement | ShadowRoot, props?: TProps) => Promise<UnmountFn>;
28
28
  update?: (props: TProps) => Promise<void>;
29
+ readonly capabilities?: CorePieceCapabilities & TCap;
29
30
  };
30
31
  ```
31
32
  > ℹ️ These types were simplified. See the real ones after installing the library.
@@ -34,6 +35,7 @@ In short: Micro-frontend creators must simply provide a way to generate an obje
34
35
 
35
36
  - `mount` mounts the micro-frontend user interface in the document
36
37
  - `update` updates the properties given to the micro-frontend
38
+ - `capabilities` is a feature introduced in v0.5.0 that allows core piece objects to declare its capabilities (see more in its own section)
37
39
 
38
40
  The `update` function is optional. The `mount` function must return a cleanup function that, ideally, reverts the mounting process.
39
41
 
@@ -67,7 +69,7 @@ export function buildTestPiece<TProps extends Record<string, any> = Record<strin
67
69
  // Here's the unmounting function:
68
70
  return () => {
69
71
  callbacks?.unmount?.();
70
- target.removeChild(pre);
72
+ pre.remove();
71
73
  return Promise.resolve();
72
74
  };
73
75
  },
@@ -142,7 +144,7 @@ Then the micro-frontends: The concept doesn't exist. At this point (after crea
142
144
 
143
145
  While `single-spa` asks you to shape your module exports in a particular way (the lifecycle functions), *CollageJS* imposes no such restriction. It is just not necessary. Just make sure you can get an object of type `CorePiece` to the `<Piece>` component of your preferred framework. Then use your framework's marvels to make the `<Piece>` component appear or disappear.
144
146
 
145
- Yes, you'll still be working with import maps. They are super handy. We provide an enhanced (and simplified at the same time) version of `import-map-overrides` named `@collagejs/imo`. It only supports the `overridable-importmap` type (and therefore only native import maps for native ES modules), but carries support for our `@collagejs/vite-aim` plug-in that let's you statically import from micro-frontends. **That's right! We are free from dynamic `import()` calls!** We can statically import from micro-frontends. Furthermore, it has a more modern user interface:
147
+ Yes, you'll still be working with import maps. They are super handy. We provide an enhanced (and simplified at the same time) version of `import-map-overrides` named `@collagejs/imo`. It only supports the `overridable-importmap` type (and therefore only native import maps for native ES modules), but carries support for our `@collagejs/vite-aim` plug-in that lets you statically import from micro-frontends. **That's right! We are free from dynamic `import()` calls!** We can statically import from micro-frontends. Furthermore, it has a more modern user interface:
146
148
 
147
149
  ![Main screen of @collagejs/imo](./_docs/collagejs-imo.png)
148
150
 
@@ -156,19 +158,31 @@ In *CollageJS*, `CorePiece.mount()` returns the clean-up (unmounting) function.
156
158
 
157
159
  Gone. There's no equivalent in *CollageJS*, as experience with `single-spa` has demonstrated that is rarely needed, and if needed, one can do this initialization easily without having to impose the function requirement. At least for now, there's no foreseeable future where an initialization function similar to `single-spa`'s `bootstrap()` will be defined. But we agree: *Never say NEVER*.
158
160
 
161
+ ## I'm Curious about `capabilities`
162
+
163
+ Ok, in a nutshell, what inspired this feature was implementing the ability to mount micro-frontends (and its CSS) inside shadow root objects, plus give the developers a good DX. In short: If you, the dev, said you wanted the MFE in an open shadow root, but then changed your mind and now you want a closed shadow root, we have to unmount and remount or some other hacky things, like a full page reload. But for this case, and at least in Svelte, we could relocate the component's generated DOM tree without remounting.
164
+
165
+ This sounds great, but what if that breaks the MFE? So the safe path is not to take advantage of cool features. This conclusion made me sad as a developer. Therefore, enter `capabilties`.
166
+
167
+ The `capabilities` object simply states what the core piece object is able to withstand. The core library defines 2 capabilities, and devs can add user-defined ones to the object.
168
+
169
+ One of the stock (or "official" if you will) capabilities is `relocatable: boolean`. If a core piece object returns `true` for `relocatable`, it is making this statement: "I can take DOM relocation operations without going through the mounting lifecycle".
170
+
171
+ With this reassurance at hand, framework adapters like `@collagejs/svelte` and others that can pull the trick cleanly can go ahead and do it, enhancing DX. This made me happy once more as a developer, and hope that it makes more devs happy.
172
+
159
173
  ## Packages
160
174
 
161
175
  | Package | Status | Links | Description |
162
176
  | - | - | - | - |
163
177
  | `@collagejs/core` | ✔️ | (This repo) | Core functionality. Provides the general mounting and unmounting logic. |
164
178
  | `@collagejs/vite-css` | ✔️ | [Repo](https://github.com/collagejs/vite) | Vite plug-in that offers a CSS-mounting algorithm that is fully compatible with Vite's CSS bundling, including split CSS. It also features FOUC prevention. |
165
- | `@collagejs/vite-im` | ✔️ | [Repo](https://github.com/collagejs/vite) | Vite plug-in that injects an import map and optionally the `import-map-overrides` package to define bare module identifiers for easy micro-frontend loading and debugging. |
166
- | `@collagejs/vite-aim` | ✔️ | [Repo](https://github.com/collagejs/vite) | Vite-plugin that gives the Vite development server the ability to accept import maps from the client, which are used to resolve modules in the Vite pipeline, enabling static imports from micro-frontend bare module identifiers. |
179
+ | `@collagejs/vite-im` | ✔️ | [Repo](https://github.com/collagejs/vite) | Vite plug-in that injects an import map and optionally the `@collagejs/imo` package to define bare module identifiers for easy micro-frontend loading and debugging. |
180
+ | `@collagejs/vite-aim` | ✔️ | [Repo](https://github.com/collagejs/vite) | Vite-plugin that auto-externalizes the module identifiers found in the application's import map. It receives the import map live (and with overrides) from the client. This enables static imports (no more dynamic `import()` calls). |
167
181
  | `@collagejs/imo` | ✔️ | [Repo](https://github.com/collagejs/imo) | Our version of `import-map-overrides` that does the usual overriding of import map entries, plus it transmits the final import map to Vite development servers found in it. |
168
182
  | `@collagejs/svelte` | ✔️ | [Repo](https://github.com/collagejs/svelte) | Svelte component library that can be used to create `CorePiece`-compliant objects and to mount `CorePiece` objects (of any technology) by providing the `<Piece>` component. |
169
- | `@collagejs/react` | 🚧 | [Repo](https://github.com/collagejs/react) | **Coming soon**. React component library that can be used to create `CorePiece`-compliant objects and to mount `CorePiece` objects (of any technology) by providing the `<Piece>` component. |
183
+ | `@collagejs/react` | ✔️ | [Repo](https://github.com/collagejs/react) | React component library that can be used to create `CorePiece`-compliant objects and to mount `CorePiece` objects (of any technology) by providing the `<Piece>` component. |
170
184
  | `@collagejs/solidjs` | ❌ | [Repo](https://github.com/collagejs/solidjs) | SolidJS component library that can be used to create `CorePiece`-compliant objects and to mount `CorePiece` objects (of any technology) by providing the `<Piece>` component. |
171
- | `@collagejs/vue` | | [Repo](https://github.com/collagejs/vue) | VueJS component library that can be used to create `CorePiece`-compliant objects and to mount `CorePiece` objects (of any technology) by providing the `<Piece>` component. |
185
+ | `@collagejs/vue` | 🚧 | [Repo](https://github.com/collagejs/vue) | **Next in line** VueJS component library that can be used to create `CorePiece`-compliant objects and to mount `CorePiece` objects (of any technology) by providing the `<Piece>` component. |
172
186
  | `@collagejs/angular` | ❌ | | **External help needed.** We don't have expertise in Angular, nor do we want to acquire it. If you're an Angular developer, please consider contributing. |
173
187
 
174
188
  ## Other Repositories
@@ -1,10 +1,11 @@
1
- import type { CorePiece, MountPiece, AcceptableTarget } from "./types.js";
1
+ import type { CorePiece, MountPiece, AcceptableTarget, CorePieceCapabilities } from "./types.js";
2
2
  export declare const mountKey: unique symbol;
3
- export declare class MountedPiece<TProps extends Record<string, any> = Record<string, any>> {
3
+ export declare class MountedPiece<TProps extends Record<string, any> = Record<string, any>, TCap extends Record<string, any> = {}> {
4
4
  #private;
5
- get mountPiece(): MountPiece<TProps>;
6
- constructor(piece: CorePiece<TProps>, mountPiece: MountPiece<TProps>, parent?: MountedPiece);
5
+ get mountPiece(): <UProps extends Record<string, any> = Record<string, any>, UCap extends CorePieceCapabilities = CorePieceCapabilities>(piece: CorePiece<UProps, UCap> | Promise<CorePiece<UProps, UCap>>, target: AcceptableTarget, props?: UProps) => Promise<MountedPiece<UProps, UCap>>;
6
+ constructor(piece: CorePiece<TProps, TCap>, mountPiece: MountPiece<TProps, TCap>, parent?: MountedPiece);
7
7
  [mountKey](target: AcceptableTarget, props?: TProps): Promise<void>;
8
8
  unmount(): Promise<void>;
9
9
  update(props: TProps): Promise<void>;
10
+ get capabilities(): (CorePieceCapabilities & TCap) | undefined;
10
11
  }
@@ -63,8 +63,12 @@ export class MountedPiece {
63
63
  if (this.#parent) {
64
64
  this.#parent.#childPieces.delete((item) => item.#id === this.#id);
65
65
  }
66
+ this.#cleanup = undefined;
66
67
  }
67
68
  update(props) {
68
69
  return doUpdate(this.#piece.update, props);
69
70
  }
71
+ get capabilities() {
72
+ return this.#piece.capabilities;
73
+ }
70
74
  }
package/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export type * from './types.js';
2
2
  export { mountPiece } from './mountPiece.js';
3
3
  export { mountPieceKey } from './common.js';
4
4
  export { ensureGlobalCollageJs } from './global.js';
5
+ export { preventRemount } from './preventRemount.js';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { mountPiece } from './mountPiece.js';
2
2
  export { mountPieceKey } from './common.js';
3
3
  export { ensureGlobalCollageJs } from './global.js';
4
+ export { preventRemount } from './preventRemount.js';
@@ -6,13 +6,13 @@ import type { AcceptableTarget, CorePiece, MountPiece } from "./types.js";
6
6
  * This exists merely to allow unit testing.
7
7
  */
8
8
  export interface MountedPieceConstructor {
9
- new <TProps extends Record<string, any> = Record<string, any>>(piece: CorePiece<TProps>, mountPiece: MountPiece<any>, parent?: MountedPiece<any>): MountedPiece<TProps>;
9
+ new (piece: CorePiece<any, any>, mountPiece: MountPiece<any, any>, parent?: MountedPiece<any, any>): MountedPiece<any, any>;
10
10
  }
11
- export declare function mountPieceCore<TProps extends Record<string, any> = Record<string, any>>(this: MountedPiece | undefined, piece: CorePiece<TProps> | Promise<CorePiece<TProps>>, target: AcceptableTarget, props?: TProps, MountedPieceClass?: MountedPieceConstructor): Promise<MountedPiece<TProps>>;
11
+ export declare function mountPieceCore<TProps extends Record<string, any> = Record<string, any>, TCap extends Record<string, any> = {}>(this: MountedPiece<any, any> | undefined, piece: CorePiece<TProps, TCap> | Promise<CorePiece<TProps, TCap>>, target: AcceptableTarget, props?: TProps, MountedPieceClass?: MountedPieceConstructor): Promise<MountedPiece<TProps, TCap>>;
12
12
  /**
13
13
  * Mounts the CollageJS piece as a child of the target element.
14
14
  * @param piece The CollageJS piece to mount.
15
15
  * @param target The target HTML element or shadow root where to mount the piece.
16
16
  * @param props The properties to pass to the piece.
17
17
  */
18
- export declare function mountPiece<TProps extends Record<string, any> = Record<string, any>>(piece: CorePiece<TProps>, target: AcceptableTarget, props?: TProps): Promise<MountedPiece<TProps>>;
18
+ export declare function mountPiece<TProps extends Record<string, any> = Record<string, any>, TCap extends Record<string, any> = {}>(piece: CorePiece<TProps, TCap>, target: AcceptableTarget, props?: TProps): Promise<MountedPiece<TProps, TCap>>;
@@ -3,7 +3,7 @@ export async function mountPieceCore(piece, target, props, MountedPieceClass = M
3
3
  if (piece instanceof Promise) {
4
4
  piece = await piece;
5
5
  }
6
- const mp = new MountedPieceClass(piece, (mountPieceCore), this);
6
+ const mp = new MountedPieceClass(piece, mountPieceCore, this);
7
7
  await mp[mountKey](target, props);
8
8
  return mp;
9
9
  }
@@ -0,0 +1,32 @@
1
+ import type { MountFn } from "./types.js";
2
+ /**
3
+ * Creates a mount function that can only be called once. If the mount function is called more than once, it will throw
4
+ * an error. This effectively prevents a piece from being mounted more than once.
5
+ *
6
+ * Use this on core piece objects that cannot guarantee the integrity of their state after they unmount.
7
+ *
8
+ * @example
9
+ * import { preventRemount } from "@collagejs/core";
10
+ *
11
+ * export function myCorePieceFactory() {
12
+ * ...
13
+ * return {
14
+ * // The most logical place is at the very beginning of the mount array.
15
+ * mount: [preventRemount(), myMount],
16
+ * update: ...,
17
+ * capabilities: {
18
+ * // Informational only: Allow the core piece object to answer the question.
19
+ * remountable: false,
20
+ * }
21
+ * };
22
+ * }
23
+ *
24
+ * ### 💡 Tips
25
+ *
26
+ * - Try to always follow the "fail fast" principle, so call `preventRemount()` as early as possible in the mount array.
27
+ * - If for any reason the mount operation is expected to fail (for whatever needed reason), consider moving the call
28
+ * to `preventRemount()` after the mount operation that may fail, so that the piece object does not have to be
29
+ * discarded unnecessarily.
30
+ * @returns A mount function that throws an error if called more than once.
31
+ */
32
+ export declare function preventRemount<TProps extends Record<string, any> = Record<string, any>>(): MountFn<TProps>;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Creates a mount function that can only be called once. If the mount function is called more than once, it will throw
3
+ * an error. This effectively prevents a piece from being mounted more than once.
4
+ *
5
+ * Use this on core piece objects that cannot guarantee the integrity of their state after they unmount.
6
+ *
7
+ * @example
8
+ * import { preventRemount } from "@collagejs/core";
9
+ *
10
+ * export function myCorePieceFactory() {
11
+ * ...
12
+ * return {
13
+ * // The most logical place is at the very beginning of the mount array.
14
+ * mount: [preventRemount(), myMount],
15
+ * update: ...,
16
+ * capabilities: {
17
+ * // Informational only: Allow the core piece object to answer the question.
18
+ * remountable: false,
19
+ * }
20
+ * };
21
+ * }
22
+ *
23
+ * ### 💡 Tips
24
+ *
25
+ * - Try to always follow the "fail fast" principle, so call `preventRemount()` as early as possible in the mount array.
26
+ * - If for any reason the mount operation is expected to fail (for whatever needed reason), consider moving the call
27
+ * to `preventRemount()` after the mount operation that may fail, so that the piece object does not have to be
28
+ * discarded unnecessarily.
29
+ * @returns A mount function that throws an error if called more than once.
30
+ */
31
+ export function preventRemount() {
32
+ let mountCount = 0;
33
+ return () => {
34
+ if (mountCount > 0) {
35
+ throw new Error("This piece cannot be mounted more than once. If this is unexpected, you might be unknowingly sharing the same piece object in different places or Piece components.");
36
+ }
37
+ ++mountCount;
38
+ return Promise.resolve(() => Promise.resolve());
39
+ };
40
+ }
package/dist/types.d.ts CHANGED
@@ -34,10 +34,39 @@ export type Mount<TProps extends Record<string, any> = Record<string, any>> = Mo
34
34
  * Defines the accepted shapes for `CorePiece.update`.
35
35
  */
36
36
  export type Update<TProps extends Record<string, any> = Record<string, any>> = UpdateFn<TProps> | UpdateFn<TProps>[] | Update[];
37
+ /**
38
+ * Defines the capabilities of a `CorePiece` object recognized by the core *CollageJS* library. These capabilities are
39
+ * used to determine how the core library should handle the piece's lifecycle, or whether a particular action or
40
+ * feature can be enabled or allowed.
41
+ */
42
+ export type CorePieceCapabilities = {
43
+ /**
44
+ * Informative only: Indicates that the piece can be mounted more than once.
45
+ *
46
+ * Since `@collagejs/core` never injects code into `CorePiece` objects, it cannot enforce this capability. The
47
+ * only place where this can be enforced is at `CorePiece.mount`. The core library provides the `preventRemount()`
48
+ * function to help developers create mount functions that throw an error if called more than once.
49
+ *
50
+ * **💡TIP**: Official framework adapters provide this functionality.
51
+ */
52
+ remountable?: boolean;
53
+ /**
54
+ * Indicates that the piece allows relocation of its HTML markup to a new parent without unmounting. For the best
55
+ * development experience, piece objects should always strive to be relocatable.
56
+ *
57
+ * If `false`, `Piece` components created with official framework adapters will most likely have to ask HMR to
58
+ * perform a full page reload whenever the developer changes shadow DOM options, like moving from an open to a
59
+ * closed shadow root.
60
+ *
61
+ * If `true` **and if the framework is capable** (i. e. Svelte), the piece can be relocated without unmounting, and
62
+ * HMR will be able to update the shadow root options without a full page reload.
63
+ */
64
+ relocatable?: boolean;
65
+ };
37
66
  /**
38
67
  * Defines the contract that objects must follow in order to be mountable as *CollageJS* pieces (micro-frontends).
39
68
  */
40
- export interface CorePiece<TProps extends Record<string, any> = Record<string, any>> {
69
+ export interface CorePiece<TProps extends Record<string, any> = Record<string, any>, TCap extends Record<string, any> = {}> {
41
70
  /**
42
71
  * Mounts the piece (micro-frontend) in the document. Every mount function should always return a cleanup function
43
72
  * that, when called, unmounts the piece.
@@ -48,11 +77,21 @@ export interface CorePiece<TProps extends Record<string, any> = Record<string, a
48
77
  * while mounted in the document, and all property values must have been passed during mounting.
49
78
  */
50
79
  update?: Update<TProps>;
80
+ /**
81
+ * Declares the capabilities of the piece. This is optional. If not provided, the piece will be assumed to have no
82
+ * capabilities, and the core library will treat it as a simple piece that can be mounted once and unmounted once, and
83
+ * that cannot be relocated or re-mounted.
84
+ *
85
+ * **💡TIP**: Always try to at least create pieces that are relocatable by not storing the original target element in
86
+ * the piece's state. Instead, just use the piece's root element's `parentElement` property to determine the
87
+ * current parent element.
88
+ */
89
+ readonly capabilities?: CorePieceCapabilities & TCap;
51
90
  }
52
91
  /**
53
92
  * Defines the shape of the object returned by the process of mounting a `CorePiece` object.
54
93
  */
55
- export interface MountedPiece<TProps extends Record<string, any> = Record<string, any>> {
94
+ export interface MountedPiece<TProps extends Record<string, any> = Record<string, any>, TCap extends Record<string, any> = {}> {
56
95
  /**
57
96
  * Function used to apply updated property values to the mounted `CorePiece` object.
58
97
  */
@@ -68,7 +107,11 @@ export interface MountedPiece<TProps extends Record<string, any> = Record<string
68
107
  * **IMPORTANT:** Always use this function instead of the global `mountPiece` function when mounting other
69
108
  * `CorePiece` objects inside the mounted `CorePiece` object to prevent lifecycle issues.
70
109
  */
71
- mountPiece: MountPiece<TProps>;
110
+ mountPiece<UProps extends Record<string, any> = Record<string, any>, UCap extends CorePieceCapabilities = CorePieceCapabilities>(piece: CorePiece<UProps, UCap> | Promise<CorePiece<UProps, UCap>>, target: AcceptableTarget, props?: UProps): Promise<MountedPiece<UProps, UCap>>;
111
+ /**
112
+ * The declared capabilities of the mounted `CorePiece` object.
113
+ */
114
+ readonly capabilities: (CorePieceCapabilities & TCap) | undefined;
72
115
  }
73
116
  /**
74
117
  * Type definition for the `mountPiece` functions that mount *CollageJS* pieces in the HTML document.
@@ -80,7 +123,7 @@ export interface MountedPiece<TProps extends Record<string, any> = Record<string
80
123
  * @param target HTML element or shadow root where to mount.
81
124
  * @param props Optional properties for the `CorePiece` object.
82
125
  */
83
- export type MountPiece<TProps extends Record<string, any> = Record<string, any>> = (piece: CorePiece<TProps> | Promise<CorePiece<TProps>>, target: AcceptableTarget, props?: TProps) => Promise<MountedPiece<TProps>>;
126
+ export type MountPiece<TProps extends Record<string, any> = Record<string, any>, TCap extends Record<string, any> = {}> = (piece: CorePiece<TProps, TCap> | Promise<CorePiece<TProps, TCap>>, target: AcceptableTarget, props?: TProps) => Promise<MountedPiece<TProps, TCap>>;
84
127
  declare global {
85
128
  /**
86
129
  * Defines the capabilities in the global `CollageJs` object.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@collagejs/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Core functionality for CollageJS.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -9,7 +9,8 @@
9
9
  "test:unit": "vitest run",
10
10
  "test:watch": "vitest",
11
11
  "test:types": "tstyche ./tests/typetests",
12
- "build": "tsc && pwsh -File ./post-build.ps1 && publint",
12
+ "clean": "rimraf dist",
13
+ "build": "npm run clean && tsc && pwsh -File ./post-build.ps1 && publint",
13
14
  "check": "tsc --noEmit"
14
15
  },
15
16
  "keywords": [
@@ -72,6 +73,7 @@
72
73
  "@types/sinon": "^21.0.0",
73
74
  "jsdom": "^29.1.1",
74
75
  "publint": "^0.3.11",
76
+ "rimraf": "^6.1.3",
75
77
  "sinon": "^22.0.0",
76
78
  "tstyche": "^7.2.1",
77
79
  "typescript": "^6.0.3",
@@ -1,8 +0,0 @@
1
- import type { UnmountFn, UpdateFn } from "./types.js";
2
- export declare namespace Internal {
3
- type MountedPiece<TProps extends Record<string, any> = Record<string, any>> = {
4
- update: UpdateFn<TProps>;
5
- unmount: UnmountFn;
6
- childPieces: Map<string, MountedPiece>;
7
- };
8
- }
@@ -1 +0,0 @@
1
- export {};