nuxt-studio 1.4.0 → 1.5.1

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.
Files changed (47) hide show
  1. package/README.md +1 -0
  2. package/dist/app/bash-B8OHlvmy.js +1 -0
  3. package/dist/app/css-qVUxccX0.js +1 -0
  4. package/dist/app/en-CZaXIbpT.js +1 -0
  5. package/dist/app/engine-compile-1A_TmkOK.js +47 -0
  6. package/dist/app/fr-TmrooVTw.js +1 -0
  7. package/dist/app/html-derivative-DL2d6cy-.js +1 -0
  8. package/dist/app/hu-d1rgPQJS.js +1 -0
  9. package/dist/app/{index-BzjOWvSV.js → index-BizPcbmC.js} +1 -1
  10. package/dist/app/index-D52QVfmA.js +2 -0
  11. package/dist/app/index-DmPVZd3w.js +1 -0
  12. package/dist/app/{index-DN_Tmc8V.js → index-JQKg5d2g.js} +1 -1
  13. package/dist/app/main-DVjyyjbg.js +42 -0
  14. package/dist/app/main-KMOUKf4W.js +43 -0
  15. package/dist/app/main.d.ts +51 -29
  16. package/dist/app/main.js +1 -1
  17. package/dist/app/mdc-DHVjNz-H.js +1 -0
  18. package/dist/app/{rehype-B0pzK8Ph.js → rehype-BqN365ZH.js} +1 -1
  19. package/dist/app/rehype-CnrSkHB2.js +1 -0
  20. package/dist/app/rst-Fqy0Smiz.js +1 -0
  21. package/dist/app/service-worker.d.ts +37 -28
  22. package/dist/app/service-worker.js +1 -1
  23. package/dist/app/shared.d.ts +37 -28
  24. package/dist/app/{shiki-DIDkQbrM.js → shiki-Boq42Ne5.js} +1 -1
  25. package/dist/app/shiki-CVI8BlHQ.js +1 -0
  26. package/dist/app/sk-BvhYsbV3.js +1 -0
  27. package/dist/app/svelte-CLa4vPyC.js +1 -0
  28. package/dist/app/vue-CitcOps-.js +1 -0
  29. package/dist/module/module.d.mts +37 -0
  30. package/dist/module/module.json +1 -1
  31. package/dist/module/module.mjs +21 -6
  32. package/dist/module/runtime/composables/useMeta.d.ts +11 -1
  33. package/dist/module/runtime/composables/useMeta.js +18 -4
  34. package/dist/module/runtime/host.js +15 -1
  35. package/dist/module/runtime/plugins/studio.client.dev.js +2 -1
  36. package/dist/module/runtime/plugins/studio.client.js +2 -1
  37. package/dist/module/runtime/server/routes/ipx/[...path].d.ts +6 -0
  38. package/dist/module/runtime/server/routes/ipx/[...path].js +40 -0
  39. package/dist/module/runtime/server/routes/meta.d.ts +10 -3
  40. package/dist/module/runtime/server/routes/meta.js +12 -2
  41. package/dist/module/runtime/server/utils/media/ipx.d.ts +14 -0
  42. package/dist/module/runtime/server/utils/media/ipx.js +111 -0
  43. package/dist/module/runtime/utils/componentGroups.d.ts +20 -0
  44. package/dist/module/runtime/utils/componentGroups.js +30 -0
  45. package/dist/module/runtime/utils/document/generate.js +6 -3
  46. package/package.json +2 -1
  47. package/dist/app/main-BaiiAKHN.js +0 -42
@@ -0,0 +1,40 @@
1
+ import { eventHandler, getRequestURL, setResponseHeader } from "h3";
2
+ import { requireStudioAuth } from "../../utils/auth.js";
3
+ import { DAY_IN_SECONDS, IPX_PREFIX, getContentTypeFromPath, getIpx, getOriginalImage, parseIpxPath, requireAllowedDomain } from "../../utils/media/ipx.js";
4
+ export default eventHandler(async (event) => {
5
+ await requireStudioAuth(event);
6
+ const url = getRequestURL(event);
7
+ if (!url.pathname.startsWith(`${IPX_PREFIX}/`)) {
8
+ return;
9
+ }
10
+ const parsed = parseIpxPath(url.pathname);
11
+ if (!parsed) {
12
+ return;
13
+ }
14
+ const domain = requireAllowedDomain(parsed.id);
15
+ const ipx = getIpx(domain);
16
+ const image = ipx(parsed.id, parsed.modifiers);
17
+ let data;
18
+ let format;
19
+ try {
20
+ const result = await image.process();
21
+ data = result.data;
22
+ format = result.format;
23
+ } catch (error) {
24
+ const fallbackData = await getOriginalImage(parsed.id);
25
+ if (!fallbackData) {
26
+ throw error;
27
+ }
28
+ data = fallbackData;
29
+ }
30
+ if (format) {
31
+ setResponseHeader(event, "content-type", `image/${format}`);
32
+ } else {
33
+ const contentType = getContentTypeFromPath(parsed.id);
34
+ if (contentType) {
35
+ setResponseHeader(event, "content-type", contentType);
36
+ }
37
+ }
38
+ setResponseHeader(event, "cache-control", `public, max-age=${DAY_IN_SECONDS}, s-maxage=${DAY_IN_SECONDS}`);
39
+ return data;
40
+ });
@@ -7,8 +7,15 @@ export interface NuxtComponentMeta {
7
7
  global: boolean;
8
8
  }
9
9
  declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<{
10
- markdownConfig: any;
11
- highlightTheme: any;
12
- components: ComponentMeta[];
10
+ markdownConfig: object;
11
+ highlightTheme: object;
12
+ components: {
13
+ list: ComponentMeta[];
14
+ groups?: Array<{
15
+ label: string;
16
+ include: string[];
17
+ }>;
18
+ ungrouped?: "include" | "omit";
19
+ };
13
20
  }>>;
14
21
  export default _default;
@@ -22,9 +22,19 @@ export default eventHandler(async (event) => {
22
22
  mappedComponents,
23
23
  config.studio?.meta?.components
24
24
  );
25
- return {
25
+ const componentGroups = config.studio?.meta?.components?.groups;
26
+ const hasGroups = Array.isArray(componentGroups) && componentGroups.length > 0;
27
+ const response = {
26
28
  markdownConfig: config.studio.markdown || {},
27
29
  highlightTheme: highlight?.theme || { default: "github-light", dark: "github-dark", light: "github-light" },
28
- components: filteredComponents
30
+ components: {
31
+ list: filteredComponents
32
+ }
29
33
  };
34
+ if (hasGroups) {
35
+ response.components.groups = componentGroups.map((g) => ({ label: g.label, include: g.include }));
36
+ const ungrouped = config.studio?.meta?.components?.ungrouped;
37
+ response.components.ungrouped = ungrouped === "omit" ? "omit" : "include";
38
+ }
39
+ return response;
30
40
  });
@@ -0,0 +1,14 @@
1
+ import type { IPX } from 'ipx';
2
+ export declare const IPX_PREFIX = "/__nuxt_studio/ipx";
3
+ export declare const DAY_IN_SECONDS: number;
4
+ export declare const publicDir: string;
5
+ export declare function requireAllowedDomain(id: string): string | undefined;
6
+ export declare function getIpx(domain?: string): IPX;
7
+ export declare function getContentTypeFromPath(path: string): "image/svg+xml" | "image/jpeg" | "image/png" | "image/gif" | "image/webp" | "image/avif" | "image/x-icon" | null;
8
+ export declare function getOriginalImage(id: string): Promise<Buffer | null>;
9
+ export declare function getOriginalExternalImage(id: string): Promise<Buffer | null>;
10
+ export declare function getOriginalFsImage(id: string): Promise<NonSharedBuffer | null>;
11
+ export declare function parseIpxPath(pathname: string): {
12
+ id: string;
13
+ modifiers: Record<string, string>;
14
+ } | null;
@@ -0,0 +1,111 @@
1
+ import { createError } from "h3";
2
+ import { createIPX, ipxFSStorage, ipxHttpStorage } from "ipx";
3
+ import { readFile } from "node:fs/promises";
4
+ import { extname, resolve } from "node:path";
5
+ import { hasProtocol, parseURL } from "ufo";
6
+ import { useRuntimeConfig } from "#imports";
7
+ export const IPX_PREFIX = "/__nuxt_studio/ipx";
8
+ export const DAY_IN_SECONDS = 60 * 60 * 24;
9
+ const mediaConfig = useRuntimeConfig().public.studio.media;
10
+ export const publicDir = mediaConfig.publicUrl;
11
+ let cachedIpx = null;
12
+ export function requireAllowedDomain(id) {
13
+ if (!mediaConfig.external) return void 0;
14
+ const configuredDomain = parseURL(mediaConfig.publicUrl).host;
15
+ const requestDomain = parseURL(id).host;
16
+ if (configuredDomain && requestDomain !== configuredDomain) {
17
+ throw createError({ statusCode: 403, statusMessage: "IPX_FORBIDDEN_DOMAIN" });
18
+ }
19
+ return requestDomain || configuredDomain || void 0;
20
+ }
21
+ export function getIpx(domain) {
22
+ if (!cachedIpx) {
23
+ if (mediaConfig.external) {
24
+ cachedIpx = createIPX({
25
+ storage: {},
26
+ httpStorage: ipxHttpStorage({ domains: domain ? [domain] : [] }),
27
+ maxAge: DAY_IN_SECONDS
28
+ });
29
+ } else {
30
+ cachedIpx = createIPX({
31
+ storage: ipxFSStorage({ dir: publicDir }),
32
+ maxAge: DAY_IN_SECONDS
33
+ });
34
+ }
35
+ }
36
+ return cachedIpx;
37
+ }
38
+ export function getContentTypeFromPath(path) {
39
+ const extension = extname(path).toLowerCase();
40
+ if (extension === ".ico") return "image/x-icon";
41
+ if (extension === ".svg") return "image/svg+xml";
42
+ if (extension === ".png") return "image/png";
43
+ if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
44
+ if (extension === ".webp") return "image/webp";
45
+ if (extension === ".gif") return "image/gif";
46
+ if (extension === ".avif") return "image/avif";
47
+ return null;
48
+ }
49
+ export async function getOriginalImage(id) {
50
+ return hasProtocol(id) ? getOriginalExternalImage(id) : getOriginalFsImage(id);
51
+ }
52
+ export async function getOriginalExternalImage(id) {
53
+ try {
54
+ const response = await fetch(id);
55
+ if (!response.ok) return null;
56
+ return Buffer.from(await response.arrayBuffer());
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+ export async function getOriginalFsImage(id) {
62
+ if (hasProtocol(id)) {
63
+ return null;
64
+ }
65
+ const normalizedId = id.replace(/^\/+/, "");
66
+ if (!normalizedId) {
67
+ return null;
68
+ }
69
+ const absolutePath = resolve(publicDir, normalizedId);
70
+ if (!absolutePath.startsWith(`${publicDir}/`) && absolutePath !== publicDir) {
71
+ return null;
72
+ }
73
+ try {
74
+ return await readFile(absolutePath);
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+ export function parseIpxPath(pathname) {
80
+ const relativePath = pathname.slice(IPX_PREFIX.length).replace(/^\/+/, "");
81
+ if (!relativePath) {
82
+ return null;
83
+ }
84
+ const [modifiersString, ...idSegments] = relativePath.split("/");
85
+ if (!modifiersString) {
86
+ throw createError({
87
+ statusCode: 400,
88
+ statusMessage: "IPX_MISSING_MODIFIERS",
89
+ message: "IPX modifiers are required."
90
+ });
91
+ }
92
+ const id = decodeURIComponent(idSegments.join("/")).replace(/^(https?:\/)([^/])/, "$1/$2");
93
+ if (!id) {
94
+ throw createError({
95
+ statusCode: 400,
96
+ statusMessage: "IPX_MISSING_ID",
97
+ message: "IPX resource id is required."
98
+ });
99
+ }
100
+ const modifiers = {};
101
+ if (modifiersString !== "_") {
102
+ for (const rawModifier of modifiersString.split(/[&,]/g)) {
103
+ const [key, ...values] = rawModifier.split(/[:=_]/);
104
+ if (!key) {
105
+ continue;
106
+ }
107
+ modifiers[key] = values.map((value) => decodeURIComponent(value)).join("_");
108
+ }
109
+ }
110
+ return { id, modifiers };
111
+ }
@@ -0,0 +1,20 @@
1
+ import type { ComponentMeta } from 'nuxt-studio/app';
2
+ export interface ComponentGroupConfig {
3
+ label: string;
4
+ include: string[];
5
+ }
6
+ export interface ComponentGroup {
7
+ label: string;
8
+ components: ComponentMeta[];
9
+ }
10
+ /**
11
+ * Assigns components to groups based on include patterns.
12
+ * First-match wins when a component matches multiple groups.
13
+ *
14
+ * @param components - Flat list of components to group
15
+ * @param groups - Group configs with label and include patterns
16
+ * @param ungrouped - Whether unmatched components go in a fallback group
17
+ * @param fallbackLabel - Label for the fallback group when ungrouped is 'include'
18
+ * @returns Array of groups with their components (empty groups omitted)
19
+ */
20
+ export declare function assignComponentsToGroups(components: ComponentMeta[], groups: ComponentGroupConfig[], ungrouped: 'include' | 'omit', fallbackLabel: string): ComponentGroup[];
@@ -0,0 +1,30 @@
1
+ import { minimatch } from "minimatch";
2
+ function matchAnyPattern(component, patterns) {
3
+ return patterns.some((pattern) => {
4
+ const value = pattern.includes("/") ? component.path : component.name;
5
+ return minimatch(value, pattern);
6
+ });
7
+ }
8
+ export function assignComponentsToGroups(components, groups, ungrouped, fallbackLabel) {
9
+ const result = groups.map((g) => ({ label: g.label, components: [] }));
10
+ const unmatched = [];
11
+ for (const component of components) {
12
+ let matched = false;
13
+ for (let i = 0; i < groups.length; i++) {
14
+ const group = groups[i];
15
+ if (group && matchAnyPattern(component, group.include)) {
16
+ result[i].components.push(component);
17
+ matched = true;
18
+ break;
19
+ }
20
+ }
21
+ if (!matched) {
22
+ unmatched.push(component);
23
+ }
24
+ }
25
+ const filtered = result.filter((g) => g.components.length > 0);
26
+ if (ungrouped === "include" && unmatched.length > 0) {
27
+ filtered.push({ label: fallbackLabel, components: unmatched });
28
+ }
29
+ return filtered;
30
+ }
@@ -54,14 +54,17 @@ export async function generateDocumentFromJSONContent(id, content) {
54
54
  body: parsed
55
55
  };
56
56
  }
57
- return {
57
+ const document = {
58
58
  id,
59
59
  extension: ContentFileExtension.JSON,
60
60
  stem: generateStemFromId(id),
61
61
  meta: {},
62
- ...parsed,
63
- body: parsed.body || parsed
62
+ ...parsed
64
63
  };
64
+ if (parsed.body) {
65
+ document.body = parsed.body;
66
+ }
67
+ return document;
65
68
  }
66
69
  export async function generateDocumentFromMarkdownContent(id, content, options = { compress: true }) {
67
70
  const markdownConfig = useHostMeta().markdownConfig.value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-studio",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Nuxt Studio for Nuxt Content",
5
5
  "private": false,
6
6
  "repository": {
@@ -49,6 +49,7 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@ai-sdk/gateway": "^3.0.55",
52
+ "ipx": "^3.1.1",
52
53
  "@ai-sdk/vue": "^3.0.101",
53
54
  "@iconify-json/lucide": "^1.2.94",
54
55
  "@nuxtjs/mdc": "^0.20.1",