@n8n/frontend-module-sdk 0.5.1 → 0.6.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@n8n/frontend-module-sdk",
4
- "version": "0.5.1",
4
+ "version": "0.6.0",
5
5
  "main": "src/index.ts",
6
6
  "import": "src/index.ts",
7
7
  "exports": {
@@ -10,8 +10,8 @@
10
10
  "dependencies": {
11
11
  "vue": "^3.5.13",
12
12
  "vue-router": "^4.5.0",
13
- "@n8n/design-system": "2.34.1",
14
- "@n8n/api-types": "1.35.1"
13
+ "@n8n/api-types": "1.36.0",
14
+ "@n8n/design-system": "2.35.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "typescript": "6.0.2",
@@ -20,8 +20,8 @@
20
20
  "vitest": "^4.1.9",
21
21
  "vue-tsc": "^2.2.8",
22
22
  "@n8n/eslint-config": "0.0.1",
23
- "@n8n/vitest-config": "1.20.0",
24
- "@n8n/typescript-config": "1.9.0"
23
+ "@n8n/typescript-config": "1.10.0",
24
+ "@n8n/vitest-config": "1.21.0"
25
25
  },
26
26
  "license": "SEE LICENSE IN LICENSE.md",
27
27
  "homepage": "https://n8n.io",
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { computed, isReactive } from 'vue';
2
3
  import type { Component } from 'vue';
3
4
 
4
5
  import * as modalRegistry from './modalRegistry';
@@ -341,4 +342,66 @@ describe('modalRegistry', () => {
341
342
  expect(keys).toEqual(['test-modal-2', 'test-modal-1', 'test-modal-3']);
342
343
  });
343
344
  });
345
+
346
+ // The registry is the single definition source: consumers derive from it rather
347
+ // than mirroring it, which only works if reading it inside a computed tracks.
348
+ describe('reactivity', () => {
349
+ it('should re-evaluate a computed over getAll on register, unregister and clear', () => {
350
+ const keys = computed(() => [...modalRegistry.getAll().keys()]);
351
+
352
+ expect(keys.value).toEqual([]);
353
+
354
+ modalRegistry.register(mockModal1);
355
+ expect(keys.value).toEqual(['test-modal-1']);
356
+
357
+ modalRegistry.register(mockModal2);
358
+ expect(keys.value).toEqual(['test-modal-1', 'test-modal-2']);
359
+
360
+ modalRegistry.unregister('test-modal-1');
361
+ expect(keys.value).toEqual(['test-modal-2']);
362
+
363
+ modalRegistry.clear();
364
+ expect(keys.value).toEqual([]);
365
+ });
366
+
367
+ it('should not make a registered component reactive', () => {
368
+ modalRegistry.register(mockModal1);
369
+
370
+ // Wrapping a component in a reactive proxy costs render performance and
371
+ // breaks identity checks — the registry is shallow for this reason.
372
+ expect(isReactive(modalRegistry.get('test-modal-1')?.component)).toBe(false);
373
+ expect(modalRegistry.get('test-modal-1')?.component).toBe(mockComponent1);
374
+ });
375
+ });
376
+ describe('ad-hoc key prefixes', () => {
377
+ it('should report a key built from a declared prefix as ad-hoc', () => {
378
+ modalRegistry.declareAdHocKeyPrefix('downloadDataTableModal');
379
+
380
+ expect(modalRegistry.isAdHocKey('downloadDataTableModal-42')).toBe(true);
381
+ expect(modalRegistry.isAdHocKey('downloadDataTableModal')).toBe(true);
382
+ });
383
+
384
+ it('should not report an undeclared key as ad-hoc', () => {
385
+ modalRegistry.declareAdHocKeyPrefix('downloadDataTableModal');
386
+
387
+ expect(modalRegistry.isAdHocKey('someOtherModal')).toBe(false);
388
+ expect(modalRegistry.isAdHocKey('someOtherModal-42')).toBe(false);
389
+ });
390
+
391
+ it('should not treat a prefix as a match for an unrelated key that merely starts with it', () => {
392
+ modalRegistry.declareAdHocKeyPrefix('importCsvModal');
393
+
394
+ // Only the bare prefix and the `<prefix>-<id>` form count, so a longer
395
+ // camelCase name starting with the same letters is still a real key.
396
+ expect(modalRegistry.isAdHocKey('importCsvModalSettings')).toBe(false);
397
+ });
398
+
399
+ it('should survive clear(), which empties registrations and not declarations', () => {
400
+ modalRegistry.declareAdHocKeyPrefix('importCsvModal');
401
+
402
+ modalRegistry.clear();
403
+
404
+ expect(modalRegistry.isAdHocKey('importCsvModal-7')).toBe(true);
405
+ });
406
+ });
344
407
  });
@@ -1,8 +1,18 @@
1
+ import { shallowReactive } from 'vue';
2
+
1
3
  import type { ModalDefinition } from '../types/modal';
2
4
 
3
- const modals = new Map<string, ModalDefinition>();
5
+ /**
6
+ * Shallow-reactive so consumers can derive from the registry with a plain
7
+ * `computed` instead of mirroring it through a subscription. Shallow on purpose:
8
+ * a definition's `component` must not be turned into a reactive object.
9
+ */
10
+ const modals = shallowReactive(new Map<string, ModalDefinition>());
4
11
  const listeners = new Set<(modals: Map<string, ModalDefinition>) => void>();
5
12
 
13
+ /** Declarations, not registrations — deliberately not emptied by `clear()`. */
14
+ const adHocKeyPrefixes = new Set<string>();
15
+
6
16
  export function getAll(): Map<string, ModalDefinition> {
7
17
  return new Map(modals);
8
18
  }
@@ -39,6 +49,30 @@ export function has(key: string): boolean {
39
49
  return modals.has(key);
40
50
  }
41
51
 
52
+ /**
53
+ * Declare that keys starting with `prefix` are minted at runtime and will never
54
+ * be registered, so the unknown-key warning can tell them apart from a modal
55
+ * whose registration was forgotten.
56
+ */
57
+ export function declareAdHocKeyPrefix(prefix: string): void {
58
+ adHocKeyPrefixes.add(prefix);
59
+ }
60
+
61
+ /**
62
+ * Whether `key` was minted from a declared ad-hoc prefix — either the bare
63
+ * prefix or the `<prefix>-<id>` form features build per row.
64
+ *
65
+ * Prefix matching is the trade this makes: a forgotten registration whose key
66
+ * happens to share a declared prefix goes unwarned.
67
+ */
68
+ export function isAdHocKey(key: string): boolean {
69
+ for (const prefix of adHocKeyPrefixes) {
70
+ if (key === prefix || key.startsWith(`${prefix}-`)) return true;
71
+ }
72
+
73
+ return false;
74
+ }
75
+
42
76
  export function subscribe(listener: (modals: Map<string, ModalDefinition>) => void): () => void {
43
77
  listeners.add(listener);
44
78
  return () => {
@@ -31,6 +31,11 @@ export type FrontendModuleDescription = {
31
31
  };
32
32
  resources?: ResourceMetadata[];
33
33
  modals?: ModalDefinition[];
34
+ /**
35
+ * Prefixes for modal keys minted at runtime instead of registered. Declared
36
+ * here so they arrive on the same path as `modals`, not by import side effect.
37
+ */
38
+ adHocModalKeyPrefixes?: string[];
34
39
  settingsPages?: IMenuItem[];
35
40
 
36
41
  // --- descriptor v2 (all optional, additive) ---