@enfyra/sdk-nuxt 0.2.4 → 0.3.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 (32) hide show
  1. package/README.md +58 -1
  2. package/dist/module.cjs +14 -1
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +14 -1
  5. package/dist/runtime/server/api/extension_definition/[id].patch.d.ts +0 -0
  6. package/dist/runtime/server/api/extension_definition/[id].patch.js +40 -0
  7. package/dist/runtime/server/api/extension_definition/[id].patch.mjs +40 -0
  8. package/dist/runtime/server/api/extension_definition.post.d.ts +0 -0
  9. package/dist/runtime/server/api/extension_definition.post.js +40 -0
  10. package/dist/runtime/server/api/extension_definition.post.mjs +40 -0
  11. package/dist/utils/server/extension/compiler.d.ts +0 -0
  12. package/dist/utils/server/extension/compiler.mjs +64 -0
  13. package/dist/utils/server/extension/index.d.ts +0 -0
  14. package/dist/utils/server/extension/index.mjs +4 -0
  15. package/dist/utils/server/extension/naming.d.ts +0 -0
  16. package/dist/utils/server/extension/naming.mjs +13 -0
  17. package/dist/utils/server/extension/processor.d.ts +0 -0
  18. package/dist/utils/server/extension/processor.mjs +34 -0
  19. package/dist/utils/server/extension/validation.d.ts +0 -0
  20. package/dist/utils/server/extension/validation.mjs +67 -0
  21. package/dist/utils/server/extension.d.ts +0 -0
  22. package/dist/utils/server/extension.mjs +1 -0
  23. package/package.json +11 -6
  24. package/src/module.ts +27 -8
  25. package/src/runtime/server/api/extension_definition/[id].patch.ts +50 -0
  26. package/src/runtime/server/api/extension_definition.post.ts +50 -0
  27. package/src/utils/server/extension/compiler.ts +78 -0
  28. package/src/utils/server/extension/index.ts +4 -0
  29. package/src/utils/server/extension/naming.ts +18 -0
  30. package/src/utils/server/extension/processor.ts +51 -0
  31. package/src/utils/server/extension/validation.ts +91 -0
  32. package/src/utils/server/extension.ts +2 -0
package/README.md CHANGED
@@ -6,6 +6,7 @@ Nuxt SDK for Enfyra CMS - A powerful composable-based API client with full SSR s
6
6
 
7
7
  ✅ **SSR & Client-Side Support** - Automatic server-side rendering with `useFetch` or client-side with `$fetch`
8
8
  ✅ **Authentication Integration** - Built-in auth composables with automatic header forwarding
9
+ ✅ **Asset Proxy** - Automatic `/assets/**` proxy to backend with no configuration needed
9
10
  ✅ **TypeScript Support** - Full type safety with auto-generated declarations
10
11
  ✅ **Batch Operations** - Efficient bulk operations with real-time progress tracking (client-side)
11
12
  ✅ **Error Handling** - Automatic error management with console logging
@@ -107,6 +108,28 @@ await logout();
107
108
  </script>
108
109
  ```
109
110
 
111
+ ### Asset URLs - Automatic Proxy
112
+
113
+ The SDK automatically proxies all asset requests to your backend. Simply use `/assets/**` paths directly:
114
+
115
+ ```vue
116
+ <template>
117
+ <!-- ✅ Assets are automatically proxied to your backend -->
118
+ <img src="/assets/images/logo.svg" alt="Logo" />
119
+ <img :src="`/assets/images/users/${user.id}/avatar.jpg`" alt="Avatar" />
120
+
121
+ <!-- Works with any asset type -->
122
+ <video src="/assets/videos/intro.mp4" controls />
123
+ <a :href="`/assets/documents/${doc.filename}`" download>Download PDF</a>
124
+ </template>
125
+ ```
126
+
127
+ **How it works:**
128
+ - All requests to `/assets/**` are automatically proxied to `{apiUrl}/enfyra/api/assets/**`
129
+ - No configuration needed - works out of the box
130
+ - Supports all asset types: images, videos, documents, etc.
131
+ - Maintains proper authentication headers
132
+
110
133
  ## Core Composables
111
134
 
112
135
  ### `useEnfyraApi<T>(path, options)`
@@ -505,13 +528,47 @@ const { execute } = useEnfyraApi<CreateUserResponse>('/users', {
505
528
  - Implement proper cache keys to avoid over-caching
506
529
  - Group related operations with batch APIs
507
530
 
531
+ ## Development
532
+
533
+ ### Testing
534
+
535
+ The SDK includes a comprehensive test suite using Vitest:
536
+
537
+ ```bash
538
+ # Run tests once
539
+ npm run test:run
540
+
541
+ # Run tests in watch mode
542
+ npm test
543
+
544
+ # Run tests with UI
545
+ npm run test:ui
546
+ ```
547
+
548
+ **Test Coverage:**
549
+ - ✅ **Extension naming utilities** - UUID generation and validation
550
+ - ✅ **Vue SFC validation** - Syntax and structure validation
551
+ - ✅ **JS bundle validation** - Syntax and export validation
552
+ - ✅ **Extension processing** - Complete workflow testing
553
+ - ✅ **35 test cases** covering all edge cases and error handling
554
+
555
+ ### Building
556
+
557
+ ```bash
558
+ # Build the module
559
+ npm run build
560
+
561
+ # Development mode
562
+ npm run dev
563
+ ```
564
+
508
565
  ## License
509
566
 
510
567
  MIT
511
568
 
512
569
  ## Contributing
513
570
 
514
- Pull requests are welcome! Please read our contributing guidelines and ensure tests pass.
571
+ Pull requests are welcome! Please read our contributing guidelines and ensure tests pass before submitting.
515
572
 
516
573
  ## Changelog
517
574
 
package/dist/module.cjs CHANGED
@@ -26,7 +26,6 @@ enfyraSDK: {
26
26
  );
27
27
  }
28
28
  nuxt.options.runtimeConfig.public.enfyraSDK = {
29
- ...nuxt.options.runtimeConfig.public.enfyraSDK,
30
29
  ...options,
31
30
  apiPrefix: config_mjs.ENFYRA_API_PREFIX
32
31
  };
@@ -45,6 +44,20 @@ enfyraSDK: {
45
44
  handler: resolve("./runtime/server/api/logout.post"),
46
45
  method: "post"
47
46
  });
47
+ kit.addServerHandler({
48
+ route: `${config_mjs.ENFYRA_API_PREFIX}/extension_definition`,
49
+ handler: resolve("./runtime/server/api/extension_definition.post"),
50
+ method: "post"
51
+ });
52
+ kit.addServerHandler({
53
+ route: `${config_mjs.ENFYRA_API_PREFIX}/extension_definition/**`,
54
+ handler: resolve("./runtime/server/api/extension_definition/[id].patch"),
55
+ method: "patch"
56
+ });
57
+ kit.addServerHandler({
58
+ route: "/assets/**",
59
+ handler: resolve("./runtime/server/api/all")
60
+ });
48
61
  kit.addServerHandler({
49
62
  route: `${config_mjs.ENFYRA_API_PREFIX}/**`,
50
63
  handler: resolve("./runtime/server/api/all")
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@enfyra/sdk-nuxt",
3
3
  "configKey": "enfyraSDK",
4
- "version": "0.2.4",
4
+ "version": "0.3.1",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "0.8.4",
7
7
  "unbuild": "2.0.0"
package/dist/module.mjs CHANGED
@@ -23,7 +23,6 @@ enfyraSDK: {
23
23
  );
24
24
  }
25
25
  nuxt.options.runtimeConfig.public.enfyraSDK = {
26
- ...nuxt.options.runtimeConfig.public.enfyraSDK,
27
26
  ...options,
28
27
  apiPrefix: ENFYRA_API_PREFIX
29
28
  };
@@ -42,6 +41,20 @@ enfyraSDK: {
42
41
  handler: resolve("./runtime/server/api/logout.post"),
43
42
  method: "post"
44
43
  });
44
+ addServerHandler({
45
+ route: `${ENFYRA_API_PREFIX}/extension_definition`,
46
+ handler: resolve("./runtime/server/api/extension_definition.post"),
47
+ method: "post"
48
+ });
49
+ addServerHandler({
50
+ route: `${ENFYRA_API_PREFIX}/extension_definition/**`,
51
+ handler: resolve("./runtime/server/api/extension_definition/[id].patch"),
52
+ method: "patch"
53
+ });
54
+ addServerHandler({
55
+ route: "/assets/**",
56
+ handler: resolve("./runtime/server/api/all")
57
+ });
45
58
  addServerHandler({
46
59
  route: `${ENFYRA_API_PREFIX}/**`,
47
60
  handler: resolve("./runtime/server/api/all")
@@ -0,0 +1,40 @@
1
+ import {
2
+ defineEventHandler,
3
+ readBody,
4
+ getHeader,
5
+ createError
6
+ } from "h3";
7
+ import { useRuntimeConfig } from "#imports";
8
+ import { $fetch } from "ofetch";
9
+ import {
10
+ processExtensionDefinition
11
+ } from "../../../../utils/server/extension";
12
+ export default defineEventHandler(async (event) => {
13
+ const method = event.method;
14
+ try {
15
+ let body = await readBody(event);
16
+ const { processedBody, compiledCode } = await processExtensionDefinition(body, method);
17
+ body = processedBody;
18
+ const config = useRuntimeConfig();
19
+ const apiPath = event.path.replace("/enfyra/api", "");
20
+ const targetUrl = `${config.public.enfyraSDK.apiUrl}${apiPath}`;
21
+ const response = await $fetch(targetUrl, {
22
+ method: "PATCH",
23
+ headers: {
24
+ cookie: getHeader(event, "cookie") || "",
25
+ authorization: event.context.proxyHeaders?.authorization || "",
26
+ "Content-Type": "application/json"
27
+ },
28
+ body: body || void 0
29
+ });
30
+ return response;
31
+ } catch (error) {
32
+ if (error.statusCode) {
33
+ throw error;
34
+ }
35
+ throw createError({
36
+ statusCode: 500,
37
+ statusMessage: error.message || `Failed to process extension definition ${method}`
38
+ });
39
+ }
40
+ });
@@ -0,0 +1,40 @@
1
+ import {
2
+ defineEventHandler,
3
+ readBody,
4
+ getHeader,
5
+ createError
6
+ } from "h3";
7
+ import { useRuntimeConfig } from "#imports";
8
+ import { $fetch } from "ofetch";
9
+ import {
10
+ processExtensionDefinition
11
+ } from "../../../../utils/server/extension";
12
+ export default defineEventHandler(async (event) => {
13
+ const method = event.method;
14
+ try {
15
+ let body = await readBody(event);
16
+ const { processedBody, compiledCode } = await processExtensionDefinition(body, method);
17
+ body = processedBody;
18
+ const config = useRuntimeConfig();
19
+ const apiPath = event.path.replace("/enfyra/api", "");
20
+ const targetUrl = `${config.public.enfyraSDK.apiUrl}${apiPath}`;
21
+ const response = await $fetch(targetUrl, {
22
+ method: "PATCH",
23
+ headers: {
24
+ cookie: getHeader(event, "cookie") || "",
25
+ authorization: event.context.proxyHeaders?.authorization || "",
26
+ "Content-Type": "application/json"
27
+ },
28
+ body: body || void 0
29
+ });
30
+ return response;
31
+ } catch (error) {
32
+ if (error.statusCode) {
33
+ throw error;
34
+ }
35
+ throw createError({
36
+ statusCode: 500,
37
+ statusMessage: error.message || `Failed to process extension definition ${method}`
38
+ });
39
+ }
40
+ });
@@ -0,0 +1,40 @@
1
+ import {
2
+ defineEventHandler,
3
+ readBody,
4
+ getHeader,
5
+ createError
6
+ } from "h3";
7
+ import { useRuntimeConfig } from "#imports";
8
+ import { $fetch } from "ofetch";
9
+ import {
10
+ processExtensionDefinition
11
+ } from "../../../utils/server/extension";
12
+ export default defineEventHandler(async (event) => {
13
+ const method = event.method;
14
+ try {
15
+ let body = await readBody(event);
16
+ const { processedBody, compiledCode } = await processExtensionDefinition(body, method);
17
+ body = processedBody;
18
+ const config = useRuntimeConfig();
19
+ const apiPath = event.path.replace("/enfyra/api", "");
20
+ const targetUrl = `${config.public.enfyraSDK.apiUrl}${apiPath}`;
21
+ const response = await $fetch(targetUrl, {
22
+ method,
23
+ headers: {
24
+ cookie: getHeader(event, "cookie") || "",
25
+ authorization: event.context.proxyHeaders?.authorization || "",
26
+ "Content-Type": "application/json"
27
+ },
28
+ body: body || void 0
29
+ });
30
+ return response;
31
+ } catch (error) {
32
+ if (error.statusCode) {
33
+ throw error;
34
+ }
35
+ throw createError({
36
+ statusCode: 500,
37
+ statusMessage: error.message || `Failed to process extension definition ${method}`
38
+ });
39
+ }
40
+ });
@@ -0,0 +1,40 @@
1
+ import {
2
+ defineEventHandler,
3
+ readBody,
4
+ getHeader,
5
+ createError
6
+ } from "h3";
7
+ import { useRuntimeConfig } from "#imports";
8
+ import { $fetch } from "ofetch";
9
+ import {
10
+ processExtensionDefinition
11
+ } from "../../../utils/server/extension";
12
+ export default defineEventHandler(async (event) => {
13
+ const method = event.method;
14
+ try {
15
+ let body = await readBody(event);
16
+ const { processedBody, compiledCode } = await processExtensionDefinition(body, method);
17
+ body = processedBody;
18
+ const config = useRuntimeConfig();
19
+ const apiPath = event.path.replace("/enfyra/api", "");
20
+ const targetUrl = `${config.public.enfyraSDK.apiUrl}${apiPath}`;
21
+ const response = await $fetch(targetUrl, {
22
+ method,
23
+ headers: {
24
+ cookie: getHeader(event, "cookie") || "",
25
+ authorization: event.context.proxyHeaders?.authorization || "",
26
+ "Content-Type": "application/json"
27
+ },
28
+ body: body || void 0
29
+ });
30
+ return response;
31
+ } catch (error) {
32
+ if (error.statusCode) {
33
+ throw error;
34
+ }
35
+ throw createError({
36
+ statusCode: 500,
37
+ statusMessage: error.message || `Failed to process extension definition ${method}`
38
+ });
39
+ }
40
+ });
File without changes
@@ -0,0 +1,64 @@
1
+ import { randomUUID } from "crypto";
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { build } from "vite";
5
+ import vue from "@vitejs/plugin-vue";
6
+ import { createError } from "h3";
7
+ export async function buildExtensionWithVite(vueContent, extensionId) {
8
+ const buildId = `${extensionId}-${Date.now()}-${randomUUID()}`;
9
+ const tempDir = join(process.cwd(), ".temp-extension-builds", buildId);
10
+ const tempExtensionFile = join(tempDir, "extension.vue");
11
+ const tempEntryFile = join(tempDir, "entry.js");
12
+ try {
13
+ if (!existsSync(tempDir)) {
14
+ mkdirSync(tempDir, { recursive: true });
15
+ }
16
+ writeFileSync(tempExtensionFile, vueContent);
17
+ writeFileSync(tempEntryFile, `
18
+ import ExtensionComponent from './extension.vue'
19
+ export default ExtensionComponent
20
+ `);
21
+ await build({
22
+ root: tempDir,
23
+ build: {
24
+ lib: {
25
+ entry: tempEntryFile,
26
+ name: extensionId,
27
+ fileName: () => "extension.js",
28
+ formats: ["umd"]
29
+ },
30
+ outDir: join(tempDir, "dist"),
31
+ emptyOutDir: true,
32
+ write: true,
33
+ rollupOptions: {
34
+ external: ["vue"],
35
+ output: {
36
+ globals: {
37
+ vue: "Vue"
38
+ }
39
+ }
40
+ }
41
+ },
42
+ plugins: [vue()]
43
+ });
44
+ const compiledFile = join(tempDir, "dist", "extension.js");
45
+ const compiledCode = readFileSync(compiledFile, "utf-8");
46
+ return compiledCode;
47
+ } catch (error) {
48
+ throw createError({
49
+ statusCode: 500,
50
+ statusMessage: `Failed to build extension: ${error.message || "Unknown error"}`
51
+ });
52
+ } finally {
53
+ await cleanupTempDirectory(tempDir);
54
+ }
55
+ }
56
+ async function cleanupTempDirectory(tempDir) {
57
+ try {
58
+ if (existsSync(tempDir)) {
59
+ const fs = await import("fs/promises");
60
+ await fs.rm(tempDir, { recursive: true, force: true });
61
+ }
62
+ } catch (cleanupError) {
63
+ }
64
+ }
File without changes
@@ -0,0 +1,4 @@
1
+ export * from "./naming.mjs";
2
+ export * from "./validation.mjs";
3
+ export * from "./compiler.mjs";
4
+ export * from "./processor.mjs";
File without changes
@@ -0,0 +1,13 @@
1
+ import { randomUUID } from "crypto";
2
+ const EXTENSION_UUID_PATTERN = /^extension_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3
+ export function autoAssignExtensionName(body) {
4
+ const currentExtensionId = body.extensionId || "";
5
+ if (!currentExtensionId || !EXTENSION_UUID_PATTERN.test(currentExtensionId)) {
6
+ const uuid = randomUUID();
7
+ body.extensionId = `extension_${uuid}`;
8
+ }
9
+ return body;
10
+ }
11
+ export function isValidExtensionId(extensionId) {
12
+ return EXTENSION_UUID_PATTERN.test(extensionId);
13
+ }
File without changes
@@ -0,0 +1,34 @@
1
+ import { createError } from "h3";
2
+ import { autoAssignExtensionName } from "./naming.mjs";
3
+ import { isProbablyVueSFC, assertValidVueSFC, assertValidJsBundleSyntax } from "./validation.mjs";
4
+ import { buildExtensionWithVite } from "./compiler.mjs";
5
+ export function isExtensionDefinitionPath(path) {
6
+ return path.includes("/extension_definition");
7
+ }
8
+ export async function processExtensionDefinition(body, method) {
9
+ if (method !== "POST" && method !== "PATCH") {
10
+ return { processedBody: body };
11
+ }
12
+ if (!body || typeof body.code !== "string") {
13
+ return { processedBody: body };
14
+ }
15
+ body = autoAssignExtensionName(body);
16
+ const code = body.code;
17
+ const extensionId = body.id || body.name || "extension_" + Date.now();
18
+ if (isProbablyVueSFC(code)) {
19
+ assertValidVueSFC(code);
20
+ try {
21
+ const compiledCode = await buildExtensionWithVite(code, body.extensionId);
22
+ body.compiledCode = compiledCode;
23
+ return { processedBody: body, compiledCode };
24
+ } catch (compileError) {
25
+ throw createError({
26
+ statusCode: 400,
27
+ statusMessage: compileError?.statusMessage || `Failed to build Vue SFC for ${extensionId}: ${compileError?.message || "Unknown error"}`
28
+ });
29
+ }
30
+ } else {
31
+ assertValidJsBundleSyntax(code);
32
+ return { processedBody: body };
33
+ }
34
+ }
File without changes
@@ -0,0 +1,67 @@
1
+ import { createError } from "h3";
2
+ export function isProbablyVueSFC(content) {
3
+ if (typeof content !== "string") return false;
4
+ const trimmed = content.trim();
5
+ if (!trimmed) return false;
6
+ const hasSfcTags = /<template[\s>]|<script[\s>]|<style[\s>]/i.test(trimmed);
7
+ const hasClosing = /<\/template>|<\/script>|<\/style>/i.test(trimmed);
8
+ return hasSfcTags && hasClosing;
9
+ }
10
+ export function assertValidVueSFC(content) {
11
+ const templateOpen = (content.match(/<template[^>]*>/g) || []).length;
12
+ const templateClose = (content.match(/<\/template>/g) || []).length;
13
+ const scriptOpen = (content.match(/<script[^>]*>/g) || []).length;
14
+ const scriptClose = (content.match(/<\/script>/g) || []).length;
15
+ const styleOpen = (content.match(/<style[^>]*>/g) || []).length;
16
+ const styleClose = (content.match(/<\/style>/g) || []).length;
17
+ if (templateOpen !== templateClose || scriptOpen !== scriptClose || styleOpen !== styleClose) {
18
+ throw createError({
19
+ statusCode: 400,
20
+ statusMessage: "Invalid Vue SFC: unbalanced tags"
21
+ });
22
+ }
23
+ if (templateOpen === 0 && scriptOpen === 0) {
24
+ throw createError({
25
+ statusCode: 400,
26
+ statusMessage: "Invalid Vue SFC: must have at least <template> or <script>"
27
+ });
28
+ }
29
+ if (scriptOpen > 0) {
30
+ const scriptContent = content.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
31
+ if (scriptContent && scriptContent[1]) {
32
+ const script = scriptContent[1];
33
+ if (script.includes("export default") && !script.includes("{")) {
34
+ throw createError({
35
+ statusCode: 400,
36
+ statusMessage: "Invalid Vue SFC: script must have proper export default syntax"
37
+ });
38
+ }
39
+ }
40
+ }
41
+ }
42
+ export function assertValidJsBundleSyntax(code) {
43
+ const brackets = { "(": 0, ")": 0, "{": 0, "}": 0, "[": 0, "]": 0 };
44
+ for (const char of code) {
45
+ if (char in brackets) {
46
+ brackets[char]++;
47
+ }
48
+ }
49
+ if (brackets["("] !== brackets[")"] || brackets["{"] !== brackets["}"] || brackets["["] !== brackets["]"]) {
50
+ throw createError({
51
+ statusCode: 400,
52
+ statusMessage: "Invalid JS syntax: unbalanced brackets"
53
+ });
54
+ }
55
+ if (!code.includes("export") && !code.includes("module.exports") && !code.includes("window.")) {
56
+ throw createError({
57
+ statusCode: 400,
58
+ statusMessage: "Invalid JS bundle: must have export statement or module.exports"
59
+ });
60
+ }
61
+ if (code.includes("function(") && !code.includes(")")) {
62
+ throw createError({
63
+ statusCode: 400,
64
+ statusMessage: "Invalid JS syntax: incomplete function declaration"
65
+ });
66
+ }
67
+ }
File without changes
@@ -0,0 +1 @@
1
+ export * from "./extension/index.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enfyra/sdk-nuxt",
3
- "version": "0.2.4",
3
+ "version": "0.3.1",
4
4
  "description": "Nuxt SDK for Enfyra CMS",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,26 +37,31 @@
37
37
  "scripts": {
38
38
  "dev": "nuxi dev playground",
39
39
  "build": "nuxt-module-build build && npx tsc -p tsconfig.build.json && node scripts/copy-composables-types.js",
40
- "prepack": "nuxt-module-build build && npx tsc -p tsconfig.build.json && node scripts/copy-composables-types.js"
40
+ "prepack": "nuxt-module-build build && npx tsc -p tsconfig.build.json && node scripts/copy-composables-types.js",
41
+ "test": "vitest",
42
+ "test:ui": "vitest --ui",
43
+ "test:run": "vitest run"
41
44
  },
42
45
  "peerDependencies": {
43
46
  "@nuxt/kit": "^3.18.1",
44
47
  "vue": "^3.0.0"
45
48
  },
46
49
  "dependencies": {
47
- "@enfyra/sdk-nuxt": "^0.1.8",
50
+ "@vitejs/plugin-vue": "^5.2.0",
48
51
  "cookie": "^0.6.0",
49
52
  "glob": "^8.1.0",
50
53
  "h3": "^1.15.4",
51
54
  "jwt-decode": "^4.0.0",
52
55
  "nuxt": "^3.18.1",
53
- "ofetch": "^1.3.3"
56
+ "ofetch": "^1.3.3",
57
+ "vite": "^6.0.7"
54
58
  },
55
59
  "devDependencies": {
56
60
  "@nuxt/module-builder": "^0.8.4",
57
61
  "@types/cookie": "^0.6.0",
62
+ "@vitest/ui": "^3.2.4",
58
63
  "typescript": "^5.0.0",
59
- "vite": "^6.0.3",
60
- "vite-plugin-dts": "^4.3.0"
64
+ "vite-plugin-dts": "^4.3.0",
65
+ "vitest": "^3.2.4"
61
66
  }
62
67
  }
package/src/module.ts CHANGED
@@ -22,19 +22,18 @@ export default defineNuxtModule({
22
22
  if (!options.apiUrl || !options.appUrl) {
23
23
  throw new Error(
24
24
  `[Enfyra SDK Nuxt] Missing required configuration:\n` +
25
- `${!options.apiUrl ? '- apiUrl is required\n' : ''}` +
26
- `${!options.appUrl ? '- appUrl is required\n' : ''}` +
27
- `Please configure both in your nuxt.config.ts:\n` +
28
- `enfyraSDK: {\n` +
29
- ` apiUrl: 'https://your-api-url',\n` +
30
- ` appUrl: 'https://your-app-url'\n` +
31
- `}`
25
+ `${!options.apiUrl ? "- apiUrl is required\n" : ""}` +
26
+ `${!options.appUrl ? "- appUrl is required\n" : ""}` +
27
+ `Please configure both in your nuxt.config.ts:\n` +
28
+ `enfyraSDK: {\n` +
29
+ ` apiUrl: 'https://your-api-url',\n` +
30
+ ` appUrl: 'https://your-app-url'\n` +
31
+ `}`
32
32
  );
33
33
  }
34
34
 
35
35
  // Make module options available at runtime with hardcoded apiPrefix
36
36
  nuxt.options.runtimeConfig.public.enfyraSDK = {
37
- ...nuxt.options.runtimeConfig.public.enfyraSDK,
38
37
  ...options,
39
38
  apiPrefix: ENFYRA_API_PREFIX,
40
39
  };
@@ -61,6 +60,26 @@ export default defineNuxtModule({
61
60
  method: "post",
62
61
  });
63
62
 
63
+ // Register extension_definition specific handlers
64
+ addServerHandler({
65
+ route: `${ENFYRA_API_PREFIX}/extension_definition`,
66
+ handler: resolve("./runtime/server/api/extension_definition.post"),
67
+ method: "post",
68
+ });
69
+
70
+ addServerHandler({
71
+ route: `${ENFYRA_API_PREFIX}/extension_definition/**`,
72
+ handler: resolve("./runtime/server/api/extension_definition/[id].patch"),
73
+ method: "patch",
74
+ });
75
+
76
+ // Assets proxy handler - catch all assets requests
77
+ addServerHandler({
78
+ route: "/assets/**",
79
+ handler: resolve("./runtime/server/api/all"),
80
+ });
81
+
82
+ // Catch-all handler for other routes
64
83
  addServerHandler({
65
84
  route: `${ENFYRA_API_PREFIX}/**`,
66
85
  handler: resolve("./runtime/server/api/all"),
@@ -0,0 +1,50 @@
1
+ import {
2
+ defineEventHandler,
3
+ readBody,
4
+ getHeader,
5
+ createError,
6
+ } from "h3";
7
+ import { useRuntimeConfig } from "#imports";
8
+ import { $fetch } from "ofetch";
9
+ import {
10
+ processExtensionDefinition,
11
+ } from "../../../../utils/server/extension";
12
+
13
+ export default defineEventHandler(async (event) => {
14
+ const method = event.method;
15
+
16
+ try {
17
+ let body = await readBody(event);
18
+
19
+ // Process extension definition logic
20
+ const { processedBody, compiledCode } = await processExtensionDefinition(body, method);
21
+ body = processedBody;
22
+
23
+ // Make API call to backend
24
+ const config = useRuntimeConfig();
25
+ const apiPath = event.path.replace("/enfyra/api", "");
26
+ const targetUrl = `${config.public.enfyraSDK.apiUrl}${apiPath}`;
27
+
28
+ const response = await $fetch(targetUrl, {
29
+ method: "PATCH",
30
+ headers: {
31
+ cookie: getHeader(event, "cookie") || "",
32
+ authorization: event.context.proxyHeaders?.authorization || "",
33
+ "Content-Type": "application/json",
34
+ },
35
+ body: body || undefined,
36
+ });
37
+
38
+ return response;
39
+ } catch (error: any) {
40
+ if (error.statusCode) {
41
+ throw error;
42
+ }
43
+
44
+ throw createError({
45
+ statusCode: 500,
46
+ statusMessage:
47
+ error.message || `Failed to process extension definition ${method}`,
48
+ });
49
+ }
50
+ });
@@ -0,0 +1,50 @@
1
+ import {
2
+ defineEventHandler,
3
+ readBody,
4
+ getHeader,
5
+ createError,
6
+ } from "h3";
7
+ import { useRuntimeConfig } from "#imports";
8
+ import { $fetch } from "ofetch";
9
+ import {
10
+ processExtensionDefinition,
11
+ } from "../../../utils/server/extension";
12
+
13
+ export default defineEventHandler(async (event) => {
14
+ const method = event.method;
15
+
16
+ try {
17
+ let body = await readBody(event);
18
+
19
+ // Process extension definition logic
20
+ const { processedBody, compiledCode } = await processExtensionDefinition(body, method);
21
+ body = processedBody;
22
+
23
+ // Make API call to backend
24
+ const config = useRuntimeConfig();
25
+ const apiPath = event.path.replace("/enfyra/api", "");
26
+ const targetUrl = `${config.public.enfyraSDK.apiUrl}${apiPath}`;
27
+
28
+ const response = await $fetch(targetUrl, {
29
+ method: method as any,
30
+ headers: {
31
+ cookie: getHeader(event, "cookie") || "",
32
+ authorization: event.context.proxyHeaders?.authorization || "",
33
+ "Content-Type": "application/json",
34
+ },
35
+ body: body || undefined,
36
+ });
37
+
38
+ return response;
39
+ } catch (error: any) {
40
+ if (error.statusCode) {
41
+ throw error;
42
+ }
43
+
44
+ throw createError({
45
+ statusCode: 500,
46
+ statusMessage:
47
+ error.message || `Failed to process extension definition ${method}`,
48
+ });
49
+ }
50
+ });
@@ -0,0 +1,78 @@
1
+ import { randomUUID } from "crypto";
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { build } from "vite";
5
+ import vue from "@vitejs/plugin-vue";
6
+ import { createError } from "h3";
7
+
8
+ export async function buildExtensionWithVite(
9
+ vueContent: string,
10
+ extensionId: string
11
+ ): Promise<string> {
12
+ // Generate unique temp directory name for each build to avoid conflicts
13
+ const buildId = `${extensionId}-${Date.now()}-${randomUUID()}`;
14
+ const tempDir = join(process.cwd(), ".temp-extension-builds", buildId);
15
+ const tempExtensionFile = join(tempDir, "extension.vue");
16
+ const tempEntryFile = join(tempDir, "entry.js");
17
+
18
+ try {
19
+ if (!existsSync(tempDir)) {
20
+ mkdirSync(tempDir, { recursive: true });
21
+ }
22
+
23
+ writeFileSync(tempExtensionFile, vueContent);
24
+ writeFileSync(tempEntryFile, `
25
+ import ExtensionComponent from './extension.vue'
26
+ export default ExtensionComponent
27
+ `);
28
+
29
+ await build({
30
+ root: tempDir,
31
+ build: {
32
+ lib: {
33
+ entry: tempEntryFile,
34
+ name: extensionId,
35
+ fileName: () => "extension.js",
36
+ formats: ["umd"],
37
+ },
38
+ outDir: join(tempDir, "dist"),
39
+ emptyOutDir: true,
40
+ write: true,
41
+ rollupOptions: {
42
+ external: ["vue"],
43
+ output: {
44
+ globals: {
45
+ vue: "Vue",
46
+ },
47
+ },
48
+ },
49
+ },
50
+ plugins: [vue()],
51
+ });
52
+
53
+ const compiledFile = join(tempDir, "dist", "extension.js");
54
+ const compiledCode = readFileSync(compiledFile, "utf-8");
55
+
56
+ return compiledCode;
57
+ } catch (error: any) {
58
+ throw createError({
59
+ statusCode: 500,
60
+ statusMessage: `Failed to build extension: ${
61
+ error.message || "Unknown error"
62
+ }`,
63
+ });
64
+ } finally {
65
+ await cleanupTempDirectory(tempDir);
66
+ }
67
+ }
68
+
69
+ async function cleanupTempDirectory(tempDir: string): Promise<void> {
70
+ try {
71
+ if (existsSync(tempDir)) {
72
+ const fs = await import("fs/promises");
73
+ await fs.rm(tempDir, { recursive: true, force: true });
74
+ }
75
+ } catch (cleanupError) {
76
+ // Silent cleanup failure - not critical
77
+ }
78
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./naming";
2
+ export * from "./validation";
3
+ export * from "./compiler";
4
+ export * from "./processor";
@@ -0,0 +1,18 @@
1
+ import { randomUUID } from "crypto";
2
+
3
+ const EXTENSION_UUID_PATTERN = /^extension_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
4
+
5
+ export function autoAssignExtensionName(body: any): any {
6
+ const currentExtensionId = body.extensionId || "";
7
+
8
+ if (!currentExtensionId || !EXTENSION_UUID_PATTERN.test(currentExtensionId)) {
9
+ const uuid = randomUUID();
10
+ body.extensionId = `extension_${uuid}`;
11
+ }
12
+
13
+ return body;
14
+ }
15
+
16
+ export function isValidExtensionId(extensionId: string): boolean {
17
+ return EXTENSION_UUID_PATTERN.test(extensionId);
18
+ }
@@ -0,0 +1,51 @@
1
+ import { createError } from "h3";
2
+ import { autoAssignExtensionName } from "./naming";
3
+ import { isProbablyVueSFC, assertValidVueSFC, assertValidJsBundleSyntax } from "./validation";
4
+ import { buildExtensionWithVite } from "./compiler";
5
+
6
+ export function isExtensionDefinitionPath(path: string): boolean {
7
+ return path.includes('/extension_definition');
8
+ }
9
+
10
+ export async function processExtensionDefinition(
11
+ body: any,
12
+ method: string
13
+ ): Promise<{ processedBody: any; compiledCode?: string }> {
14
+ if (method !== "POST" && method !== "PATCH") {
15
+ return { processedBody: body };
16
+ }
17
+
18
+ if (!body || typeof body.code !== "string") {
19
+ return { processedBody: body };
20
+ }
21
+
22
+ // Auto-assign extension name
23
+ body = autoAssignExtensionName(body);
24
+
25
+ const code: string = body.code;
26
+ const extensionId = body.id || body.name || "extension_" + Date.now();
27
+
28
+ if (isProbablyVueSFC(code)) {
29
+ // Validate SFC syntax before building
30
+ assertValidVueSFC(code);
31
+
32
+ try {
33
+ const compiledCode = await buildExtensionWithVite(code, body.extensionId);
34
+ body.compiledCode = compiledCode;
35
+ return { processedBody: body, compiledCode };
36
+ } catch (compileError: any) {
37
+ throw createError({
38
+ statusCode: 400,
39
+ statusMessage:
40
+ compileError?.statusMessage ||
41
+ `Failed to build Vue SFC for ${extensionId}: ${
42
+ compileError?.message || "Unknown error"
43
+ }`,
44
+ });
45
+ }
46
+ } else {
47
+ // Treat as compiled bundle; validate syntax first
48
+ assertValidJsBundleSyntax(code);
49
+ return { processedBody: body };
50
+ }
51
+ }
@@ -0,0 +1,91 @@
1
+ import { createError } from "h3";
2
+
3
+ export function isProbablyVueSFC(content: string): boolean {
4
+ if (typeof content !== "string") return false;
5
+ const trimmed = content.trim();
6
+ if (!trimmed) return false;
7
+
8
+ const hasSfcTags = /<template[\s>]|<script[\s>]|<style[\s>]/i.test(trimmed);
9
+ const hasClosing = /<\/template>|<\/script>|<\/style>/i.test(trimmed);
10
+
11
+ return hasSfcTags && hasClosing;
12
+ }
13
+
14
+ export function assertValidVueSFC(content: string): void {
15
+ const templateOpen = (content.match(/<template[^>]*>/g) || []).length;
16
+ const templateClose = (content.match(/<\/template>/g) || []).length;
17
+ const scriptOpen = (content.match(/<script[^>]*>/g) || []).length;
18
+ const scriptClose = (content.match(/<\/script>/g) || []).length;
19
+ const styleOpen = (content.match(/<style[^>]*>/g) || []).length;
20
+ const styleClose = (content.match(/<\/style>/g) || []).length;
21
+
22
+ if (
23
+ templateOpen !== templateClose ||
24
+ scriptOpen !== scriptClose ||
25
+ styleOpen !== styleClose
26
+ ) {
27
+ throw createError({
28
+ statusCode: 400,
29
+ statusMessage: "Invalid Vue SFC: unbalanced tags",
30
+ });
31
+ }
32
+
33
+ if (templateOpen === 0 && scriptOpen === 0) {
34
+ throw createError({
35
+ statusCode: 400,
36
+ statusMessage: "Invalid Vue SFC: must have at least <template> or <script>",
37
+ });
38
+ }
39
+
40
+ if (scriptOpen > 0) {
41
+ const scriptContent = content.match(/<script[^>]*>([\s\S]*?)<\/script>/i);
42
+ if (scriptContent && scriptContent[1]) {
43
+ const script = scriptContent[1];
44
+ if (script.includes("export default") && !script.includes("{")) {
45
+ throw createError({
46
+ statusCode: 400,
47
+ statusMessage: "Invalid Vue SFC: script must have proper export default syntax",
48
+ });
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ export function assertValidJsBundleSyntax(code: string): void {
55
+ const brackets = { "(": 0, ")": 0, "{": 0, "}": 0, "[": 0, "]": 0 };
56
+
57
+ for (const char of code) {
58
+ if (char in brackets) {
59
+ brackets[char as keyof typeof brackets]++;
60
+ }
61
+ }
62
+
63
+ if (
64
+ brackets["("] !== brackets[")"] ||
65
+ brackets["{"] !== brackets["}"] ||
66
+ brackets["["] !== brackets["]"]
67
+ ) {
68
+ throw createError({
69
+ statusCode: 400,
70
+ statusMessage: "Invalid JS syntax: unbalanced brackets",
71
+ });
72
+ }
73
+
74
+ if (
75
+ !code.includes("export") &&
76
+ !code.includes("module.exports") &&
77
+ !code.includes("window.")
78
+ ) {
79
+ throw createError({
80
+ statusCode: 400,
81
+ statusMessage: "Invalid JS bundle: must have export statement or module.exports",
82
+ });
83
+ }
84
+
85
+ if (code.includes("function(") && !code.includes(")")) {
86
+ throw createError({
87
+ statusCode: 400,
88
+ statusMessage: "Invalid JS syntax: incomplete function declaration",
89
+ });
90
+ }
91
+ }
@@ -0,0 +1,2 @@
1
+ // Re-export all extension utilities from modular structure
2
+ export * from "./extension";