@frigear-nu/menu 0.1.0 → 0.1.2

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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@frigear-nu/menu",
3
3
  "configKey": "menus",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.3",
7
7
  "unbuild": "unknown"
@@ -1,7 +1,7 @@
1
1
  import { computed, unref } from "vue";
2
2
  import { useAsyncData, useNuxtApp, useRuntimeConfig } from "#imports";
3
3
  import { collectMenuItems, matchesMenuGroup, parseMenuPath, sortByOrder, translateMenuItem } from "../../utils/menu.js";
4
- import { menus } from "../utils/state.js";
4
+ import { getAppMenuStore } from "../utils/state.js";
5
5
  function useMenuLocale() {
6
6
  const nuxtApp = useNuxtApp();
7
7
  return computed(() => nuxtApp.$i18n?.locale?.value);
@@ -30,7 +30,7 @@ export const useMenu = (path) => {
30
30
  () => `menu:${path}:${locale.value ?? ""}`,
31
31
  async () => {
32
32
  const i18n = nuxtApp.$i18n?.t && nuxtApp.$i18n.te ? nuxtApp.$i18n : void 0;
33
- const registered = collectMenuItems(menus[menu], parsed).map((item) => translateMenuItem(item, i18n));
33
+ const registered = collectMenuItems(getAppMenuStore(nuxtApp)[menu], parsed).map((item) => translateMenuItem(item, i18n));
34
34
  const content = await loadContentItems(menu, parsed, unref(locale));
35
35
  const result = sortByOrder([...content, ...registered]);
36
36
  await nuxtApp.callHook("menus:visible", menu);
@@ -1,9 +1,8 @@
1
1
  /**
2
- * Populates the shared menu state consumed by `useMenu`: first with every item registered at
3
- * build time (`extendMenu`), then by running every registered module's dynamic `runtime/menus.ts`
4
- * source once per app instance (server: once per request, client: once on hydration). Resets the
5
- * state first so repeated runs within the same server process (e.g. concurrent requests) don't
6
- * accumulate duplicate items.
2
+ * Populates this app instance's menu state, consumed by `useMenu`: first with every item registered
3
+ * at build time (`extendMenu`), then by running every registered module's dynamic `runtime/menus.ts`
4
+ * source once per app instance (server: once per request, client: once on hydration). The state is
5
+ * held per app instance, so concurrent server requests never share or clobber each other's menus.
7
6
  */
8
7
  declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
9
8
  export default _default;
@@ -1,19 +1,24 @@
1
- import { defineNuxtPlugin } from "#imports";
1
+ import { defineNuxtPlugin, tryUseNuxtApp } from "#imports";
2
2
  import { staticItems, sources } from "#menus/sources";
3
- import { pushMenuItems, resetMenus } from "../utils/state.js";
3
+ import { getAppMenuStore, pushMenuItems, resetMenus, setAppMenuStoreResolver } from "../utils/state.js";
4
4
  export default defineNuxtPlugin({
5
5
  name: "frigear-nu:menus",
6
- async setup() {
7
- resetMenus();
6
+ async setup(nuxtApp) {
7
+ const store = getAppMenuStore(nuxtApp);
8
+ setAppMenuStoreResolver(() => {
9
+ const app = tryUseNuxtApp();
10
+ return app ? getAppMenuStore(app) : void 0;
11
+ });
12
+ resetMenus(store);
8
13
  for (const [menu, groups] of Object.entries(staticItems)) {
9
14
  for (const [group, items] of Object.entries(groups)) {
10
- if (items.length > 0) pushMenuItems(menu, group, items);
15
+ if (items.length > 0) pushMenuItems(store, menu, group, items);
11
16
  }
12
17
  }
13
18
  for (const { menu, source } of sources) {
14
19
  const items = [];
15
- await source(items);
16
- if (items.length > 0) pushMenuItems(menu, "", items);
20
+ await nuxtApp.runWithContext(() => source(items));
21
+ if (items.length > 0) pushMenuItems(store, menu, "", items);
17
22
  }
18
23
  }
19
24
  });
@@ -1,9 +1,9 @@
1
1
  import { parseMenuPath } from "../../utils/menu.js";
2
- import { pushMenuItems } from "./state.js";
2
+ import { currentMenuStore, pushMenuItems } from "./state.js";
3
3
  export function extendMenuItem(path, item) {
4
4
  const { menu, group, wildcard } = parseMenuPath(path);
5
5
  if (wildcard) {
6
6
  throw new Error(`[@frigear-nu/menu] Cannot register items into wildcard path "${path}" - wildcards are only supported when reading a menu via \`useMenu\`.`);
7
7
  }
8
- pushMenuItems(menu, group, item);
8
+ pushMenuItems(currentMenuStore(), menu, group, item);
9
9
  }
@@ -1,14 +1,17 @@
1
1
  import type { MenuItem } from '../../../types.js';
2
+ /** Every menu's items, keyed by menu name then by group (`''` for the root group). */
3
+ export type MenuStore = Record<string, Record<string, MenuItem[]>>;
4
+ export declare function createMenuStore(): MenuStore;
2
5
  /**
3
- * Shared, module-scoped reactive state holding every menu's items, keyed by menu name then by
4
- * group (`''` for the root group). Populated client-side (and once per SSR request) by the
5
- * `menus` plugin, which runs every registered dynamic source.
6
+ * Process-wide store, used only when no Nuxt app is in scope: the Nitro `useMenu` (populated once per
7
+ * process from the runtime config) and calls made outside an app context. It is never read by the
8
+ * Vue renderer, which uses the per-app store below.
6
9
  */
7
- export declare const menus: Record<string, Record<string, MenuItem[]>>;
8
- export declare function pushMenuItems(menu: string, group: string, items: MenuItem | MenuItem[]): void;
9
- /**
10
- * Empties every registered menu/group's items in place, keeping the same reactive object/array
11
- * instances around rather than deleting keys, so each source run starts from a clean slate
12
- * without accumulating duplicates across repeated runs (e.g. concurrent server requests).
13
- */
14
- export declare function resetMenus(): void;
10
+ export declare const processMenuStore: MenuStore;
11
+ export declare function getAppMenuStore(nuxtApp: object): MenuStore;
12
+ export declare function setAppMenuStoreResolver(resolver: () => MenuStore | undefined): void;
13
+ /** The store of the Nuxt app currently in scope, or the process-wide store outside any app. */
14
+ export declare function currentMenuStore(): MenuStore;
15
+ export declare function pushMenuItems(store: MenuStore, menu: string, group: string, items: MenuItem | MenuItem[]): void;
16
+ /** Empties every registered menu/group's items in place, so a (re)population never accumulates duplicates. */
17
+ export declare function resetMenus(store: MenuStore): void;
@@ -1,12 +1,28 @@
1
1
  import { reactive } from "vue";
2
- export const menus = reactive({});
3
- export function pushMenuItems(menu, group, items) {
4
- const groups = menus[menu] ??= {};
2
+ export function createMenuStore() {
3
+ return reactive({});
4
+ }
5
+ export const processMenuStore = createMenuStore();
6
+ const appStores = /* @__PURE__ */ new WeakMap();
7
+ export function getAppMenuStore(nuxtApp) {
8
+ let store = appStores.get(nuxtApp);
9
+ if (!store) appStores.set(nuxtApp, store = createMenuStore());
10
+ return store;
11
+ }
12
+ let resolveAppStore;
13
+ export function setAppMenuStoreResolver(resolver) {
14
+ resolveAppStore = resolver;
15
+ }
16
+ export function currentMenuStore() {
17
+ return resolveAppStore?.() ?? processMenuStore;
18
+ }
19
+ export function pushMenuItems(store, menu, group, items) {
20
+ const groups = store[menu] ??= {};
5
21
  const list = groups[group] ??= [];
6
22
  list.push(...Array.isArray(items) ? items : [items]);
7
23
  }
8
- export function resetMenus() {
9
- for (const groups of Object.values(menus)) {
24
+ export function resetMenus(store) {
25
+ for (const groups of Object.values(store)) {
10
26
  for (const list of Object.values(groups)) list.length = 0;
11
27
  }
12
28
  }
@@ -1,20 +1,20 @@
1
1
  import { useRuntimeConfig } from "nitropack/runtime";
2
2
  import { staticItems, sources } from "#menus/server-sources";
3
3
  import { collectMenuItems, matchesMenuGroup, parseMenuPath, sortByOrder } from "../utils/menu.js";
4
- import { menus, pushMenuItems, resetMenus } from "../app/utils/state.js";
4
+ import { processMenuStore as menus, pushMenuItems, resetMenus } from "../app/utils/state.js";
5
5
  let registered;
6
6
  function registerItems() {
7
7
  registered ??= (async () => {
8
- resetMenus();
8
+ resetMenus(menus);
9
9
  for (const [menu, groups] of Object.entries(staticItems)) {
10
10
  for (const [group, items] of Object.entries(groups)) {
11
- if (items.length > 0) pushMenuItems(menu, group, items);
11
+ if (items.length > 0) pushMenuItems(menus, menu, group, items);
12
12
  }
13
13
  }
14
14
  for (const { menu, source } of sources) {
15
15
  const items = [];
16
16
  await source(items);
17
- if (items.length > 0) pushMenuItems(menu, "", items);
17
+ if (items.length > 0) pushMenuItems(menus, menu, "", items);
18
18
  }
19
19
  })().catch((error) => {
20
20
  registered = void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frigear-nu/menu",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Named, shareable navigation menus - modules contribute items (extendMenu / a runtime/menus.ts source) and/or Nuxt Content, consumers read them via useMenu and render them however they like.",
5
5
  "homepage": "https://docs.frigear.dev/modules/menu",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  ],
32
32
  "dependencies": {
33
33
  "@nuxt/kit": "^4.6.1",
34
- "@frigear-nu/module-kit": "1.1.0",
34
+ "@frigear-nu/module-kit": "1.1.2",
35
35
  "zod": "^4.6.5"
36
36
  },
37
37
  "devDependencies": {