@antelopejs/dms-frontend 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
@@ -188,17 +188,28 @@ The SDK also exposes `use` for Vue plugins. Entries execute by descending manife
188
188
 
189
189
  Within one setup, the first module to register a name keeps it. `ajs-dms dev` sets every module up again each time a change to a layer reaches the server renderer, into empty registries, so the next server render uses the edited component without a restart. A `setup` may therefore run more than once in a process: it should only register, and keep no state of its own between runs.
190
190
 
191
- ### Private components
191
+ ### Component prefix
192
192
 
193
- A component registers as public by default: it joins the application's global components and resolves by name anywhere. A component that only exists to render the module's own backend pages registers as private instead:
193
+ Every registered component joins the application's global components and resolves by name anywhere, in templates and in backend page trees. To keep the components of two modules from colliding, a module declares a `componentPrefix`, put in front of the name of every component it registers:
194
194
 
195
195
  ```ts
196
- sdk.registerComponent("BillingTotals", BillingTotals, { private: true });
196
+ const frontendModule: DmsFrontendModule = {
197
+ componentPrefix: "Acme",
198
+ setup(sdk) {
199
+ sdk.registerComponent("Banner", Banner); // registered as AcmeBanner
200
+ },
201
+ };
197
202
  ```
198
203
 
199
- A private component is not registered with `app.component`, so no template or other module reaches it by name. `resolveDmsComponent(name, owner)` returns it only when `owner` is the registering module, and then ahead of a public component of the same name; without an owner, only public components resolve. The generic page renderer passes the page's owner, so a backend page tree naming the component renders it, and the page preloader and the server render's hydration handover cover it too.
204
+ The full name, prefix included, is the one a template uses (`<AcmeBanner>`) and a backend sends (`CustomComponent("AcmeBanner")`, or `acme-banner`). Names are compared ignoring case, a leading `lazy` and non-alphanumeric characters; a leading `Dms` is no longer dropped, since it is the DMS module's own prefix.
205
+
206
+ When two modules register the same name, the module of higher priority keeps it and a warning names both. A module that means to replace another's component registers its full name without its own prefix, which is not reported:
207
+
208
+ ```ts
209
+ sdk.registerComponent("AcmeBanner", MyBanner, { prefix: false });
210
+ ```
200
211
 
201
- The owner is the page payload's `module` field: the name of the frontend module that owns the page's component tree, as the backend added it (the module's `name` in the frontend manifest, which the generated loader hands each module's setup). A backend that does not send `module` reaches no private component, whatever the tree names.
212
+ A module's `app/build/` directory is never auto-imported (see below): its components are registered so backend pages can name them, but they are the module's internals, not a surface other modules should build on.
202
213
 
203
214
  ### Auto-imports (`dms.frontend.build.ts`)
204
215
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-frontend",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
5
5
  "keywords": [
6
6
  "antelope",
@@ -37,7 +37,6 @@ interface PageRoute {
37
37
  interface PagePayload {
38
38
  layout?: PageLayout;
39
39
  route?: PageRoute;
40
- module?: string;
41
40
  }
42
41
 
43
42
  interface DynamicPageProps {
@@ -56,8 +55,7 @@ function resolveDefinition(
56
55
  return {
57
56
  id,
58
57
  component: definition.componentName
59
- ? resolveDmsComponent(definition.componentName, props.page.module) ||
60
- "div"
58
+ ? resolveDmsComponent(definition.componentName) || "div"
61
59
  : "div",
62
60
  componentName: definition.componentName,
63
61
  options: definition.options,
@@ -15,6 +15,8 @@ import {
15
15
  type Component,
16
16
  type ComponentPublicInstance,
17
17
  type ComputedRef,
18
+ camelize,
19
+ capitalize,
18
20
  computed,
19
21
  defineComponent,
20
22
  type EffectScope,
@@ -51,12 +53,6 @@ export interface DmsPagePayload {
51
53
  layoutUrl?: string;
52
54
  layout?: DmsPageLayout;
53
55
  shared?: DmsSharedPagePayload;
54
- /**
55
- * The frontend module that owns the page's component tree, by the name the
56
- * backend added it under (its manifest name). Components that module
57
- * registered with `{ private: true }` resolve in this page's tree only.
58
- */
59
- module?: string;
60
56
  [key: string]: unknown;
61
57
  }
62
58
  export interface DmsSharedPagePayload {
@@ -236,12 +232,11 @@ export interface DmsPluginRegistrationOptions {
236
232
  export type DmsComponentPreloader = () => Promise<unknown>;
237
233
  export interface DmsComponentRegistrationOptions {
238
234
  /**
239
- * Keeps the component out of the application's global components: it
240
- * resolves by name only in the component trees of the backend pages the
241
- * registering module owns (see `DmsPagePayload.module`), where it takes
242
- * precedence over a public component of the same name. Defaults to `false`.
235
+ * Registers the component under `name` as given, without the module's
236
+ * `componentPrefix`: the way to replace a component another module
237
+ * registers, from a module of higher priority. Defaults to `true`.
243
238
  */
244
- private?: boolean;
239
+ prefix?: boolean;
245
240
  }
246
241
  export interface DmsFrontendSdk {
247
242
  options: DmsModuleOptions;
@@ -288,13 +283,17 @@ export interface DmsFrontendSdk {
288
283
  use(plugin: Plugin): void;
289
284
  }
290
285
  export interface DmsFrontendModule {
286
+ /**
287
+ * Put in front of the name of every component the module registers, so
288
+ * the components of two modules never collide: with `"Acme"`,
289
+ * `registerComponent("Banner", ...)` registers `AcmeBanner`, the name the
290
+ * backend sends and templates use.
291
+ */
292
+ componentPrefix?: string;
291
293
  setup(sdk: DmsFrontendSdk): void | Promise<void>;
292
294
  }
293
295
  export interface DmsFrontendModuleRegistration {
294
- /**
295
- * The module's manifest name, which its backend pages carry as their
296
- * `module`. Without it, the module's private components never resolve.
297
- */
296
+ /** The module's manifest name, which collision warnings name it by. */
298
297
  name?: string;
299
298
  module: DmsFrontendModule;
300
299
  options: DmsModuleOptions;
@@ -315,8 +314,10 @@ export interface DmsRuntimeHooks {
315
314
  interface DmsComponentRegistration {
316
315
  name: string;
317
316
  component: Component;
318
- /** The registering module, for a private component. */
317
+ /** The registering module, for a collision warning. */
319
318
  module?: string;
319
+ /** Registered without the module's prefix, to replace another module's. */
320
+ unprefixed?: boolean;
320
321
  }
321
322
 
322
323
  interface DmsFrontendEntry {
@@ -339,8 +340,6 @@ interface DmsPluginRegistration {
339
340
  */
340
341
  interface DmsFrontendRegistry {
341
342
  components: Map<string, DmsComponentRegistration>;
342
- /** Private components, by registering module, then by normalized name. */
343
- privateComponents: Map<string, Map<string, DmsComponentRegistration>>;
344
343
  pages: Map<string, DmsFrontendEntry>;
345
344
  dynamicPages: Map<string, DmsFrontendEntry>;
346
345
  layouts: Map<string, DmsFrontendEntry>;
@@ -389,7 +388,6 @@ export interface DmsFrontendRuntime {
389
388
  function createDmsFrontendRegistry(): DmsFrontendRegistry {
390
389
  return {
391
390
  components: new Map(),
392
- privateComponents: new Map(),
393
391
  pages: new Map(),
394
392
  dynamicPages: new Map(),
395
393
  layouts: new Map(),
@@ -1256,53 +1254,60 @@ export function dmsPageSuspenseProps(): Record<string, () => void> {
1256
1254
  };
1257
1255
  }
1258
1256
 
1257
+ /**
1258
+ * The key a component name is registered and resolved under: case, a leading
1259
+ * `lazy` and every non-alphanumeric character ignored, so PascalCase,
1260
+ * kebab-case and lowercase spellings match. Unlike `normalizeDmsName`, a
1261
+ * leading `Dms` is kept: it is the DMS module's prefix, and dropping it would
1262
+ * make `DmsBanner` and another module's `Banner` the same component.
1263
+ */
1264
+ function normalizeDmsComponentName(name: string): string {
1265
+ return name
1266
+ .replace(/^lazy/i, "")
1267
+ .replace(/[^a-z0-9]/gi, "")
1268
+ .toLowerCase();
1269
+ }
1270
+
1271
+ function moduleLabel(module: string | undefined): string {
1272
+ return module ? `module "${module}"` : "an unnamed module";
1273
+ }
1274
+
1275
+ /**
1276
+ * A component registered under a name already taken is ignored: the first
1277
+ * registration, from the module of higher priority, keeps the name. That is
1278
+ * how a module replaces another's component, by registering its full name
1279
+ * without a prefix, and only a collision neither side asked for is reported.
1280
+ */
1259
1281
  function registerDmsComponentEntry(
1260
1282
  entries: Map<string, DmsComponentRegistration>,
1261
1283
  registration: DmsComponentRegistration,
1262
1284
  ): void {
1263
- const key = normalizeDmsName(registration.name);
1264
- if (!entries.has(key)) entries.set(key, registration);
1285
+ const key = normalizeDmsComponentName(registration.name);
1286
+ const existing = entries.get(key);
1287
+ if (!existing) {
1288
+ entries.set(key, registration);
1289
+ return;
1290
+ }
1291
+ if (existing.unprefixed || registration.unprefixed) return;
1292
+ console.warn(
1293
+ `[dms] Component "${registration.name}" from ${moduleLabel(registration.module)} is ignored: ` +
1294
+ `${moduleLabel(existing.module)} already registers "${existing.name}". ` +
1295
+ "Give the modules different component prefixes.",
1296
+ );
1265
1297
  }
1266
1298
  /** Registers a component unless a higher-priority module already owns its name. */
1267
1299
  export function registerDmsComponent(name: string, component: Component): void {
1268
1300
  registerDmsComponentEntry(registry.components, { name, component });
1269
1301
  }
1270
1302
  /**
1271
- * Resolves a registered component by name. `owner` is the `module` of the
1272
- * backend page whose component tree is rendered: the private components that
1273
- * module registered resolve there, ahead of a public one of the same name.
1274
- * Without it, only public components resolve.
1303
+ * Resolves a registered component by its full name, prefix included. Case,
1304
+ * a leading `lazy` and non-alphanumeric characters are ignored.
1275
1305
  */
1276
- export function resolveDmsComponent(
1277
- name: string,
1278
- owner?: string,
1279
- ): Component | undefined {
1280
- const key = normalizeDmsName(name);
1281
- const own = owner
1282
- ? registry.privateComponents.get(owner)?.get(key)
1283
- : undefined;
1284
- return (own ?? registry.components.get(key))?.component;
1306
+ export function resolveDmsComponent(name: string): Component | undefined {
1307
+ return registry.components.get(normalizeDmsComponentName(name))?.component;
1285
1308
  }
1286
1309
  export const getDmsComponent = resolveDmsComponent;
1287
1310
 
1288
- const PRIVATE_COMPONENT_SEPARATOR = "::";
1289
-
1290
- function registeredComponents(): DmsComponentRegistration[] {
1291
- return [
1292
- ...registry.components.values(),
1293
- ...[...registry.privateComponents.values()].flatMap((entries) => [
1294
- ...entries.values(),
1295
- ]),
1296
- ];
1297
- }
1298
-
1299
- /** Names a registered component across the server render and hydration. */
1300
- function componentReference({ name, module }: DmsComponentRegistration) {
1301
- return module === undefined
1302
- ? name
1303
- : `${module}${PRIVATE_COMPONENT_SEPARATOR}${name}`;
1304
- }
1305
-
1306
1311
  /**
1307
1312
  * Records, by registered name, every registered async component a server
1308
1313
  * render reaches, so the client can resolve the same ones before hydrating.
@@ -1312,9 +1317,9 @@ function componentReference({ name, module }: DmsComponentRegistration) {
1312
1317
  */
1313
1318
  export function trackDmsAsyncComponents(app: App): Set<string> {
1314
1319
  const names = new Map<Component, string>();
1315
- registeredComponents().forEach((registration) => {
1316
- if ((registration.component as DmsAsyncComponent).__asyncLoader)
1317
- names.set(registration.component, componentReference(registration));
1320
+ registry.components.forEach(({ component, name }) => {
1321
+ if ((component as DmsAsyncComponent).__asyncLoader)
1322
+ names.set(component, name);
1318
1323
  });
1319
1324
  const rendered = new Set<string>();
1320
1325
  app.mixin({
@@ -1357,18 +1362,10 @@ export function trackDmsServerScopes(app: App): () => void {
1357
1362
  export async function resolveDmsAsyncComponents(
1358
1363
  names: readonly string[],
1359
1364
  ): Promise<void> {
1360
- const components = new Map(
1361
- registeredComponents().map((registration) => [
1362
- componentReference(registration),
1363
- registration.component,
1364
- ]),
1365
- );
1366
1365
  await Promise.all(
1367
1366
  names.map((name) =>
1368
1367
  (
1369
- (components.get(name) ?? resolveDmsComponent(name)) as
1370
- | DmsAsyncComponent
1371
- | undefined
1368
+ resolveDmsComponent(name) as DmsAsyncComponent | undefined
1372
1369
  )?.__asyncLoader?.(),
1373
1370
  ),
1374
1371
  );
@@ -1410,7 +1407,6 @@ function collectRenderedComponentNames(
1410
1407
 
1411
1408
  async function preloadDmsLayoutComponents(
1412
1409
  layout: DmsPageLayout | undefined,
1413
- owner: string | undefined,
1414
1410
  ): Promise<void> {
1415
1411
  const names = new Set<string>();
1416
1412
  collectRenderedComponentNames(layout?.layout, names);
@@ -1419,7 +1415,7 @@ async function preloadDmsLayoutComponents(
1419
1415
  });
1420
1416
  await Promise.all(
1421
1417
  [...names].map((name) => {
1422
- const component = resolveDmsComponent(name, owner) as
1418
+ const component = resolveDmsComponent(name) as
1423
1419
  | DmsAsyncComponent
1424
1420
  | undefined;
1425
1421
  return component?.__asyncLoader?.();
@@ -1561,8 +1557,7 @@ export async function preloadDmsPage(props: DmsPageProps): Promise<void> {
1561
1557
  (entry?.component as DmsAsyncComponent | undefined)?.__asyncLoader?.(),
1562
1558
  ]),
1563
1559
  );
1564
- if (!props.error)
1565
- await preloadDmsLayoutComponents(props.page.layout, props.page.module);
1560
+ if (!props.error) await preloadDmsLayoutComponents(props.page.layout);
1566
1561
  }
1567
1562
 
1568
1563
  export function useDmsInjection<T>(
@@ -1580,29 +1575,28 @@ function registerDmsFrontendEntry(
1580
1575
  if (!entries.has(name)) entries.set(name, { component, preload });
1581
1576
  }
1582
1577
 
1583
- function privateComponentsOf(
1584
- target: DmsFrontendRegistry,
1585
- owner: string,
1586
- ): Map<string, DmsComponentRegistration> {
1587
- const entries = target.privateComponents.get(owner) ?? new Map();
1588
- target.privateComponents.set(owner, entries);
1589
- return entries;
1578
+ /** `name` behind `prefix`, as a PascalCase component name. */
1579
+ function prefixedComponentName(prefix: string, name: string): string {
1580
+ return prefix ? `${prefix}${capitalize(camelize(name))}` : name;
1590
1581
  }
1591
1582
 
1592
1583
  function createSdk(
1593
1584
  target: DmsFrontendRegistry,
1594
- { name: owner = "", options }: DmsFrontendModuleRegistration,
1585
+ {
1586
+ name: module,
1587
+ module: { componentPrefix = "" },
1588
+ options,
1589
+ }: DmsFrontendModuleRegistration,
1595
1590
  ): DmsFrontendSdk {
1596
1591
  return {
1597
1592
  options,
1598
- registerComponent: (name, component, registration = {}) =>
1599
- registration.private
1600
- ? registerDmsComponentEntry(privateComponentsOf(target, owner), {
1601
- name,
1602
- component,
1603
- module: owner,
1604
- })
1605
- : registerDmsComponentEntry(target.components, { name, component }),
1593
+ registerComponent: (name, component, { prefix = true } = {}) =>
1594
+ registerDmsComponentEntry(target.components, {
1595
+ name: prefix ? prefixedComponentName(componentPrefix, name) : name,
1596
+ component,
1597
+ module,
1598
+ unprefixed: !prefix,
1599
+ }),
1606
1600
  registerPage: (name, component, preload) => {
1607
1601
  const key = normalizeDmsPageKey(name);
1608
1602
  registerDmsFrontendEntry(target.pages, key, component, preload);