@enfyra/sdk-nuxt 0.2.3 → 0.3.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 +35 -4
- package/dist/composables/useEnfyraApi.mjs +3 -2
- package/dist/constants/config.d.ts +5 -0
- package/dist/constants/config.mjs +1 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/module.cjs +16 -6
- package/dist/module.d.cts +0 -2
- package/dist/module.d.mts +0 -2
- package/dist/module.d.ts +0 -2
- package/dist/module.json +1 -1
- package/dist/module.mjs +16 -6
- package/dist/runtime/server/api/extension_definition/[id].patch.d.ts +0 -0
- package/dist/runtime/server/api/extension_definition/[id].patch.js +40 -0
- package/dist/runtime/server/api/extension_definition/[id].patch.mjs +40 -0
- package/dist/runtime/server/api/extension_definition.post.d.ts +0 -0
- package/dist/runtime/server/api/extension_definition.post.js +40 -0
- package/dist/runtime/server/api/extension_definition.post.mjs +40 -0
- package/dist/utils/config.mjs +0 -1
- package/dist/utils/server/extension/compiler.d.ts +0 -0
- package/dist/utils/server/extension/compiler.mjs +64 -0
- package/dist/utils/server/extension/index.d.ts +0 -0
- package/dist/utils/server/extension/index.mjs +4 -0
- package/dist/utils/server/extension/naming.d.ts +0 -0
- package/dist/utils/server/extension/naming.mjs +13 -0
- package/dist/utils/server/extension/processor.d.ts +0 -0
- package/dist/utils/server/extension/processor.mjs +34 -0
- package/dist/utils/server/extension/validation.d.ts +0 -0
- package/dist/utils/server/extension/validation.mjs +67 -0
- package/dist/utils/server/extension.d.ts +0 -0
- package/dist/utils/server/extension.mjs +1 -0
- package/dist/utils/server/proxy.mjs +2 -1
- package/package.json +11 -6
- package/src/composables/useEnfyraApi.ts +3 -2
- package/src/constants/config.ts +5 -0
- package/src/module.ts +28 -14
- package/src/runtime/server/api/extension_definition/[id].patch.ts +50 -0
- package/src/runtime/server/api/extension_definition.post.ts +50 -0
- package/src/types/index.ts +0 -1
- package/src/utils/config.ts +0 -1
- package/src/utils/server/extension/compiler.ts +78 -0
- package/src/utils/server/extension/index.ts +4 -0
- package/src/utils/server/extension/naming.ts +18 -0
- package/src/utils/server/extension/processor.ts +51 -0
- package/src/utils/server/extension/validation.ts +91 -0
- package/src/utils/server/extension.ts +2 -0
- package/src/utils/server/proxy.ts +2 -1
package/README.md
CHANGED
|
@@ -427,9 +427,6 @@ export default defineNuxtConfig({
|
|
|
427
427
|
// Required: Main API URL
|
|
428
428
|
apiUrl: process.env.ENFYRA_API_URL || "http://localhost:1105",
|
|
429
429
|
|
|
430
|
-
// Optional: API path prefix (default: '/api')
|
|
431
|
-
apiPrefix: '/api/v1',
|
|
432
|
-
|
|
433
430
|
// Required: App URL for SSR requests
|
|
434
431
|
appUrl: process.env.ENFYRA_APP_URL || "http://localhost:3001",
|
|
435
432
|
},
|
|
@@ -508,13 +505,47 @@ const { execute } = useEnfyraApi<CreateUserResponse>('/users', {
|
|
|
508
505
|
- Implement proper cache keys to avoid over-caching
|
|
509
506
|
- Group related operations with batch APIs
|
|
510
507
|
|
|
508
|
+
## Development
|
|
509
|
+
|
|
510
|
+
### Testing
|
|
511
|
+
|
|
512
|
+
The SDK includes a comprehensive test suite using Vitest:
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
# Run tests once
|
|
516
|
+
npm run test:run
|
|
517
|
+
|
|
518
|
+
# Run tests in watch mode
|
|
519
|
+
npm test
|
|
520
|
+
|
|
521
|
+
# Run tests with UI
|
|
522
|
+
npm run test:ui
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**Test Coverage:**
|
|
526
|
+
- ✅ **Extension naming utilities** - UUID generation and validation
|
|
527
|
+
- ✅ **Vue SFC validation** - Syntax and structure validation
|
|
528
|
+
- ✅ **JS bundle validation** - Syntax and export validation
|
|
529
|
+
- ✅ **Extension processing** - Complete workflow testing
|
|
530
|
+
- ✅ **35 test cases** covering all edge cases and error handling
|
|
531
|
+
|
|
532
|
+
### Building
|
|
533
|
+
|
|
534
|
+
```bash
|
|
535
|
+
# Build the module
|
|
536
|
+
npm run build
|
|
537
|
+
|
|
538
|
+
# Development mode
|
|
539
|
+
npm run dev
|
|
540
|
+
```
|
|
541
|
+
|
|
511
542
|
## License
|
|
512
543
|
|
|
513
544
|
MIT
|
|
514
545
|
|
|
515
546
|
## Contributing
|
|
516
547
|
|
|
517
|
-
Pull requests are welcome! Please read our contributing guidelines and ensure tests pass.
|
|
548
|
+
Pull requests are welcome! Please read our contributing guidelines and ensure tests pass before submitting.
|
|
518
549
|
|
|
519
550
|
## Changelog
|
|
520
551
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ref, unref, toRaw } from "vue";
|
|
2
2
|
import { $fetch } from "../utils/http";
|
|
3
|
+
import { ENFYRA_API_PREFIX } from "../constants/config";
|
|
3
4
|
import { useRuntimeConfig, useFetch, useRequestHeaders } from "#imports";
|
|
4
5
|
function handleError(error, context, customHandler) {
|
|
5
6
|
const apiError = {
|
|
@@ -20,7 +21,7 @@ export function useEnfyraApi(path, opts = {}) {
|
|
|
20
21
|
if (ssr) {
|
|
21
22
|
const config = useRuntimeConfig().public.enfyraSDK;
|
|
22
23
|
const basePath = (typeof path === "function" ? path() : path).replace(/^\/?api\/?/, "").replace(/^\/+/, "");
|
|
23
|
-
const finalUrl = (config?.appUrl || "") + (config?.apiPrefix ||
|
|
24
|
+
const finalUrl = (config?.appUrl || "") + (config?.apiPrefix || ENFYRA_API_PREFIX) + "/" + basePath;
|
|
24
25
|
const clientHeaders = process.client ? {} : useRequestHeaders([
|
|
25
26
|
"authorization",
|
|
26
27
|
"cookie",
|
|
@@ -72,7 +73,7 @@ export function useEnfyraApi(path, opts = {}) {
|
|
|
72
73
|
const buildPath = (...segments) => {
|
|
73
74
|
return segments.filter(Boolean).join("/");
|
|
74
75
|
};
|
|
75
|
-
const fullBaseURL = apiUrl + (apiPrefix ||
|
|
76
|
+
const fullBaseURL = apiUrl + (apiPrefix || ENFYRA_API_PREFIX);
|
|
76
77
|
async function processBatch(items, processor) {
|
|
77
78
|
const results = [];
|
|
78
79
|
const progressResults = [];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ENFYRA_API_PREFIX = "/enfyra/api";
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/dist/module.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const kit = require('@nuxt/kit');
|
|
4
|
+
const config_mjs = require('../dist/constants/config.mjs');
|
|
4
5
|
|
|
5
6
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
6
7
|
const module$1 = kit.defineNuxtModule({
|
|
@@ -10,7 +11,6 @@ const module$1 = kit.defineNuxtModule({
|
|
|
10
11
|
},
|
|
11
12
|
defaults: {
|
|
12
13
|
apiUrl: "",
|
|
13
|
-
apiPrefix: "/api",
|
|
14
14
|
appUrl: ""
|
|
15
15
|
},
|
|
16
16
|
setup(options, nuxt) {
|
|
@@ -26,8 +26,8 @@ enfyraSDK: {
|
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
nuxt.options.runtimeConfig.public.enfyraSDK = {
|
|
29
|
-
...
|
|
30
|
-
|
|
29
|
+
...options,
|
|
30
|
+
apiPrefix: config_mjs.ENFYRA_API_PREFIX
|
|
31
31
|
};
|
|
32
32
|
kit.addImportsDir(resolve("./composables"));
|
|
33
33
|
kit.addServerHandler({
|
|
@@ -35,17 +35,27 @@ enfyraSDK: {
|
|
|
35
35
|
middleware: true
|
|
36
36
|
});
|
|
37
37
|
kit.addServerHandler({
|
|
38
|
-
route:
|
|
38
|
+
route: `${config_mjs.ENFYRA_API_PREFIX}/login`,
|
|
39
39
|
handler: resolve("./runtime/server/api/login.post"),
|
|
40
40
|
method: "post"
|
|
41
41
|
});
|
|
42
42
|
kit.addServerHandler({
|
|
43
|
-
route:
|
|
43
|
+
route: `${config_mjs.ENFYRA_API_PREFIX}/logout`,
|
|
44
44
|
handler: resolve("./runtime/server/api/logout.post"),
|
|
45
45
|
method: "post"
|
|
46
46
|
});
|
|
47
47
|
kit.addServerHandler({
|
|
48
|
-
route:
|
|
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: `${config_mjs.ENFYRA_API_PREFIX}/**`,
|
|
49
59
|
handler: resolve("./runtime/server/api/all")
|
|
50
60
|
});
|
|
51
61
|
}
|
package/dist/module.d.cts
CHANGED
package/dist/module.d.mts
CHANGED
package/dist/module.d.ts
CHANGED
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineNuxtModule, createResolver, addImportsDir, addServerHandler } from '@nuxt/kit';
|
|
2
|
+
import { ENFYRA_API_PREFIX } from '../dist/constants/config.mjs';
|
|
2
3
|
|
|
3
4
|
const module = defineNuxtModule({
|
|
4
5
|
meta: {
|
|
@@ -7,7 +8,6 @@ const module = defineNuxtModule({
|
|
|
7
8
|
},
|
|
8
9
|
defaults: {
|
|
9
10
|
apiUrl: "",
|
|
10
|
-
apiPrefix: "/api",
|
|
11
11
|
appUrl: ""
|
|
12
12
|
},
|
|
13
13
|
setup(options, nuxt) {
|
|
@@ -23,8 +23,8 @@ enfyraSDK: {
|
|
|
23
23
|
);
|
|
24
24
|
}
|
|
25
25
|
nuxt.options.runtimeConfig.public.enfyraSDK = {
|
|
26
|
-
...
|
|
27
|
-
|
|
26
|
+
...options,
|
|
27
|
+
apiPrefix: ENFYRA_API_PREFIX
|
|
28
28
|
};
|
|
29
29
|
addImportsDir(resolve("./composables"));
|
|
30
30
|
addServerHandler({
|
|
@@ -32,17 +32,27 @@ enfyraSDK: {
|
|
|
32
32
|
middleware: true
|
|
33
33
|
});
|
|
34
34
|
addServerHandler({
|
|
35
|
-
route:
|
|
35
|
+
route: `${ENFYRA_API_PREFIX}/login`,
|
|
36
36
|
handler: resolve("./runtime/server/api/login.post"),
|
|
37
37
|
method: "post"
|
|
38
38
|
});
|
|
39
39
|
addServerHandler({
|
|
40
|
-
route:
|
|
40
|
+
route: `${ENFYRA_API_PREFIX}/logout`,
|
|
41
41
|
handler: resolve("./runtime/server/api/logout.post"),
|
|
42
42
|
method: "post"
|
|
43
43
|
});
|
|
44
44
|
addServerHandler({
|
|
45
|
-
route:
|
|
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: `${ENFYRA_API_PREFIX}/**`,
|
|
46
56
|
handler: resolve("./runtime/server/api/all")
|
|
47
57
|
});
|
|
48
58
|
}
|
|
File without changes
|
|
@@ -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
|
+
});
|
|
File without changes
|
|
@@ -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
|
+
});
|
package/dist/utils/config.mjs
CHANGED
|
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
|
|
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";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { proxyRequest } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
import { ENFYRA_API_PREFIX } from "../../constants/config";
|
|
3
4
|
export function proxyToAPI(event, customPath) {
|
|
4
5
|
const config = useRuntimeConfig();
|
|
5
|
-
const apiPrefix = config.public.enfyraSDK.apiPrefix;
|
|
6
|
+
const apiPrefix = config.public.enfyraSDK.apiPrefix || ENFYRA_API_PREFIX;
|
|
6
7
|
const rawPath = customPath || event.path.replace(new RegExp(`^${apiPrefix}`), "");
|
|
7
8
|
const targetUrl = `${config.public.enfyraSDK.apiUrl}${rawPath}`;
|
|
8
9
|
const headers = event.context.proxyHeaders || {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@enfyra/sdk-nuxt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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
|
-
"@
|
|
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": "^
|
|
60
|
-
"
|
|
64
|
+
"vite-plugin-dts": "^4.3.0",
|
|
65
|
+
"vitest": "^3.2.4"
|
|
61
66
|
}
|
|
62
67
|
}
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
BatchProgress,
|
|
10
10
|
} from "../types";
|
|
11
11
|
import { $fetch } from "../utils/http";
|
|
12
|
+
import { ENFYRA_API_PREFIX } from "../constants/config";
|
|
12
13
|
import { useRuntimeConfig, useFetch, useRequestHeaders } from "#imports";
|
|
13
14
|
|
|
14
15
|
function handleError(
|
|
@@ -58,7 +59,7 @@ export function useEnfyraApi<T = any>(
|
|
|
58
59
|
.replace(/^\/+/, ""); // Remove leading slashes
|
|
59
60
|
|
|
60
61
|
const finalUrl =
|
|
61
|
-
(config?.appUrl || "") + (config?.apiPrefix ||
|
|
62
|
+
(config?.appUrl || "") + (config?.apiPrefix || ENFYRA_API_PREFIX) + "/" + basePath;
|
|
62
63
|
|
|
63
64
|
// Get headers from client request and filter out connection-specific headers
|
|
64
65
|
const clientHeaders = process.client
|
|
@@ -138,7 +139,7 @@ export function useEnfyraApi<T = any>(
|
|
|
138
139
|
};
|
|
139
140
|
|
|
140
141
|
// Build full base URL with prefix
|
|
141
|
-
const fullBaseURL = apiUrl + (apiPrefix ||
|
|
142
|
+
const fullBaseURL = apiUrl + (apiPrefix || ENFYRA_API_PREFIX);
|
|
142
143
|
|
|
143
144
|
// Helper function for batch processing with chunking, concurrency control, and real-time progress tracking
|
|
144
145
|
async function processBatch<T>(
|
package/src/module.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
addServerHandler,
|
|
5
5
|
addImportsDir,
|
|
6
6
|
} from "@nuxt/kit";
|
|
7
|
+
import { ENFYRA_API_PREFIX } from "./constants/config";
|
|
7
8
|
|
|
8
9
|
export default defineNuxtModule({
|
|
9
10
|
meta: {
|
|
@@ -12,7 +13,6 @@ export default defineNuxtModule({
|
|
|
12
13
|
},
|
|
13
14
|
defaults: {
|
|
14
15
|
apiUrl: "",
|
|
15
|
-
apiPrefix: "/api",
|
|
16
16
|
appUrl: "",
|
|
17
17
|
},
|
|
18
18
|
setup(options, nuxt) {
|
|
@@ -22,20 +22,20 @@ 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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
// Make module options available at runtime
|
|
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,
|
|
38
|
+
apiPrefix: ENFYRA_API_PREFIX,
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
// Auto-import composables
|
|
@@ -47,21 +47,35 @@ export default defineNuxtModule({
|
|
|
47
47
|
middleware: true,
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
// Register server handlers from SDK
|
|
50
|
+
// Register server handlers from SDK with hardcoded prefix
|
|
51
51
|
addServerHandler({
|
|
52
|
-
route:
|
|
52
|
+
route: `${ENFYRA_API_PREFIX}/login`,
|
|
53
53
|
handler: resolve("./runtime/server/api/login.post"),
|
|
54
54
|
method: "post",
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
addServerHandler({
|
|
58
|
-
route:
|
|
58
|
+
route: `${ENFYRA_API_PREFIX}/logout`,
|
|
59
59
|
handler: resolve("./runtime/server/api/logout.post"),
|
|
60
60
|
method: "post",
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
// Register extension_definition specific handlers
|
|
63
64
|
addServerHandler({
|
|
64
|
-
route:
|
|
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
|
+
// Catch-all handler for other routes
|
|
77
|
+
addServerHandler({
|
|
78
|
+
route: `${ENFYRA_API_PREFIX}/**`,
|
|
65
79
|
handler: resolve("./runtime/server/api/all"),
|
|
66
80
|
});
|
|
67
81
|
},
|
|
@@ -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
|
+
});
|
package/src/types/index.ts
CHANGED
package/src/utils/config.ts
CHANGED
|
@@ -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,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
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { H3Event, proxyRequest } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
import { ENFYRA_API_PREFIX } from "../../constants/config";
|
|
3
4
|
|
|
4
5
|
export function proxyToAPI(event: H3Event, customPath?: string) {
|
|
5
6
|
const config = useRuntimeConfig();
|
|
6
|
-
const apiPrefix = config.public.enfyraSDK.apiPrefix;
|
|
7
|
+
const apiPrefix = config.public.enfyraSDK.apiPrefix || ENFYRA_API_PREFIX;
|
|
7
8
|
const rawPath =
|
|
8
9
|
customPath || event.path.replace(new RegExp(`^${apiPrefix}`), "");
|
|
9
10
|
const targetUrl = `${config.public.enfyraSDK.apiUrl}${rawPath}`;
|