@nuraly/lumenjs 0.1.2 → 0.1.4
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 +76 -235
- package/dist/auth/config.d.ts +23 -0
- package/dist/auth/config.js +115 -0
- package/dist/auth/guard.d.ts +12 -0
- package/dist/auth/guard.js +28 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/middleware.d.ts +23 -0
- package/dist/auth/middleware.js +89 -0
- package/dist/auth/native-auth.d.ts +73 -0
- package/dist/auth/native-auth.js +293 -0
- package/dist/auth/oidc-client.d.ts +17 -0
- package/dist/auth/oidc-client.js +123 -0
- package/dist/auth/providers/google.d.ts +23 -0
- package/dist/auth/providers/google.js +25 -0
- package/dist/auth/providers/index.d.ts +2 -0
- package/dist/auth/providers/index.js +1 -0
- package/dist/auth/routes/login.d.ts +8 -0
- package/dist/auth/routes/login.js +98 -0
- package/dist/auth/routes/logout.d.ts +4 -0
- package/dist/auth/routes/logout.js +79 -0
- package/dist/auth/routes/oidc-callback.d.ts +3 -0
- package/dist/auth/routes/oidc-callback.js +70 -0
- package/dist/auth/routes/password.d.ts +5 -0
- package/dist/auth/routes/password.js +149 -0
- package/dist/auth/routes/signup.d.ts +3 -0
- package/dist/auth/routes/signup.js +81 -0
- package/dist/auth/routes/token.d.ts +4 -0
- package/dist/auth/routes/token.js +70 -0
- package/dist/auth/routes/utils.d.ts +7 -0
- package/dist/auth/routes/utils.js +35 -0
- package/dist/auth/routes/verify.d.ts +3 -0
- package/dist/auth/routes/verify.js +26 -0
- package/dist/auth/routes.d.ts +8 -0
- package/dist/auth/routes.js +110 -0
- package/dist/auth/session.d.ts +8 -0
- package/dist/auth/session.js +54 -0
- package/dist/auth/token.d.ts +33 -0
- package/dist/auth/token.js +90 -0
- package/dist/auth/types.d.ts +156 -0
- package/dist/auth/types.js +2 -0
- package/dist/build/build-client.d.ts +15 -0
- package/dist/build/build-client.js +45 -0
- package/dist/build/build-prerender.d.ts +11 -0
- package/dist/build/build-prerender.js +159 -0
- package/dist/build/build-server.d.ts +17 -0
- package/dist/build/build-server.js +98 -0
- package/dist/build/build.js +52 -120
- package/dist/build/scan.d.ts +19 -0
- package/dist/build/scan.js +77 -6
- package/dist/build/serve-api.js +8 -2
- package/dist/build/serve-loaders.d.ts +4 -2
- package/dist/build/serve-loaders.js +128 -10
- package/dist/build/serve-ssr.js +38 -11
- package/dist/build/serve-static.js +3 -3
- package/dist/build/serve.js +229 -14
- package/dist/cli.js +37 -6
- package/dist/communication/encryption.d.ts +35 -0
- package/dist/communication/encryption.js +90 -0
- package/dist/communication/handlers/context.d.ts +27 -0
- package/dist/communication/handlers/context.js +1 -0
- package/dist/communication/handlers/conversation.d.ts +24 -0
- package/dist/communication/handlers/conversation.js +113 -0
- package/dist/communication/handlers/file-upload.d.ts +17 -0
- package/dist/communication/handlers/file-upload.js +62 -0
- package/dist/communication/handlers/messaging.d.ts +30 -0
- package/dist/communication/handlers/messaging.js +237 -0
- package/dist/communication/handlers/presence.d.ts +15 -0
- package/dist/communication/handlers/presence.js +76 -0
- package/dist/communication/handlers.d.ts +5 -0
- package/dist/communication/handlers.js +5 -0
- package/dist/communication/index.d.ts +9 -0
- package/dist/communication/index.js +7 -0
- package/dist/communication/link-preview.d.ts +18 -0
- package/dist/communication/link-preview.js +115 -0
- package/dist/communication/schema.d.ts +10 -0
- package/dist/communication/schema.js +101 -0
- package/dist/communication/server.d.ts +86 -0
- package/dist/communication/server.js +212 -0
- package/dist/communication/signaling.d.ts +43 -0
- package/dist/communication/signaling.js +271 -0
- package/dist/communication/store.d.ts +71 -0
- package/dist/communication/store.js +289 -0
- package/dist/communication/types.d.ts +454 -0
- package/dist/communication/types.js +1 -0
- package/dist/create.d.ts +1 -0
- package/dist/create.js +55 -0
- package/dist/db/auto-migrate.d.ts +3 -0
- package/dist/db/auto-migrate.js +100 -0
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.js +18 -0
- package/dist/db/context.d.ts +2 -0
- package/dist/db/context.js +9 -0
- package/dist/db/index.d.ts +23 -0
- package/dist/db/index.js +258 -0
- package/dist/db/seed.d.ts +12 -0
- package/dist/db/seed.js +88 -0
- package/dist/db/table.d.ts +10 -0
- package/dist/db/table.js +12 -0
- package/dist/dev-server/config.d.ts +14 -0
- package/dist/dev-server/config.js +26 -9
- package/dist/dev-server/index-html.d.ts +3 -0
- package/dist/dev-server/index-html.js +18 -6
- package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
- package/dist/dev-server/nuralyui-aliases.js +115 -94
- package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
- package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
- package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
- package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
- package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
- package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.d.ts +0 -1
- package/dist/dev-server/plugins/vite-plugin-loaders.js +311 -42
- package/dist/dev-server/plugins/vite-plugin-routes.js +18 -6
- package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
- package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
- package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +111 -2
- package/dist/dev-server/server.js +128 -12
- package/dist/dev-server/ssr-render.d.ts +2 -1
- package/dist/dev-server/ssr-render.js +107 -48
- package/dist/editor/ai/backend.d.ts +20 -0
- package/dist/editor/ai/backend.js +104 -0
- package/dist/editor/ai/claude-code-client.d.ts +20 -0
- package/dist/editor/ai/claude-code-client.js +145 -0
- package/dist/editor/ai/opencode-client.d.ts +14 -0
- package/dist/editor/ai/opencode-client.js +125 -0
- package/dist/editor/ai/snapshot-store.d.ts +22 -0
- package/dist/editor/ai/snapshot-store.js +35 -0
- package/dist/editor/ai/types.d.ts +30 -0
- package/dist/editor/ai/types.js +136 -0
- package/dist/editor/ai-chat-panel.d.ts +13 -0
- package/dist/editor/ai-chat-panel.js +587 -0
- package/dist/editor/ai-markdown.d.ts +10 -0
- package/dist/editor/ai-markdown.js +70 -0
- package/dist/editor/ai-project-panel.d.ts +11 -0
- package/dist/editor/ai-project-panel.js +332 -0
- package/dist/editor/ast-modification.d.ts +11 -0
- package/dist/editor/ast-modification.js +1 -0
- package/dist/editor/ast-service.d.ts +30 -0
- package/dist/editor/ast-service.js +180 -0
- package/dist/editor/css-rules.d.ts +54 -0
- package/dist/editor/css-rules.js +423 -0
- package/dist/editor/editor-api-client.d.ts +51 -0
- package/dist/editor/editor-api-client.js +162 -0
- package/dist/editor/editor-bridge.d.ts +1 -0
- package/dist/editor/editor-bridge.js +17 -8
- package/dist/editor/editor-toolbar.d.ts +14 -0
- package/dist/editor/editor-toolbar.js +115 -0
- package/dist/editor/file-editor.d.ts +9 -0
- package/dist/editor/file-editor.js +236 -0
- package/dist/editor/file-service.d.ts +16 -0
- package/dist/editor/file-service.js +52 -0
- package/dist/editor/i18n-key-gen.d.ts +1 -0
- package/dist/editor/i18n-key-gen.js +7 -0
- package/dist/editor/inline-text-edit.d.ts +5 -0
- package/dist/editor/inline-text-edit.js +173 -92
- package/dist/editor/overlay-events.d.ts +5 -0
- package/dist/editor/overlay-events.js +364 -0
- package/dist/editor/overlay-hmr.d.ts +2 -0
- package/dist/editor/overlay-hmr.js +75 -0
- package/dist/editor/overlay-selection.d.ts +29 -0
- package/dist/editor/overlay-selection.js +148 -0
- package/dist/editor/overlay-utils.d.ts +12 -0
- package/dist/editor/overlay-utils.js +59 -0
- package/dist/editor/properties-panel-persist.d.ts +14 -0
- package/dist/editor/properties-panel-persist.js +70 -0
- package/dist/editor/properties-panel-rows.d.ts +10 -0
- package/dist/editor/properties-panel-rows.js +349 -0
- package/dist/editor/properties-panel-styles.d.ts +4 -0
- package/dist/editor/properties-panel-styles.js +174 -0
- package/dist/editor/properties-panel.d.ts +4 -0
- package/dist/editor/properties-panel.js +148 -0
- package/dist/editor/property-registry.d.ts +16 -0
- package/dist/editor/property-registry.js +303 -0
- package/dist/editor/standalone-file-panel.d.ts +0 -0
- package/dist/editor/standalone-file-panel.js +1 -0
- package/dist/editor/standalone-overlay-dom.d.ts +0 -0
- package/dist/editor/standalone-overlay-dom.js +1 -0
- package/dist/editor/standalone-overlay-styles.d.ts +0 -0
- package/dist/editor/standalone-overlay-styles.js +1 -0
- package/dist/editor/standalone-overlay.d.ts +1 -0
- package/dist/editor/standalone-overlay.js +76 -0
- package/dist/editor/syntax-highlighter.d.ts +4 -0
- package/dist/editor/syntax-highlighter.js +81 -0
- package/dist/editor/text-toolbar.d.ts +11 -0
- package/dist/editor/text-toolbar.js +327 -0
- package/dist/editor/toolbar-styles.d.ts +4 -0
- package/dist/editor/toolbar-styles.js +198 -0
- package/dist/email/index.d.ts +32 -0
- package/dist/email/index.js +154 -0
- package/dist/email/providers/resend.d.ts +2 -0
- package/dist/email/providers/resend.js +24 -0
- package/dist/email/providers/sendgrid.d.ts +2 -0
- package/dist/email/providers/sendgrid.js +31 -0
- package/dist/email/providers/smtp.d.ts +13 -0
- package/dist/email/providers/smtp.js +125 -0
- package/dist/email/template-engine.d.ts +18 -0
- package/dist/email/template-engine.js +116 -0
- package/dist/email/templates/base.d.ts +9 -0
- package/dist/email/templates/base.js +65 -0
- package/dist/email/templates/password-reset.d.ts +5 -0
- package/dist/email/templates/password-reset.js +15 -0
- package/dist/email/templates/verify-email.d.ts +5 -0
- package/dist/email/templates/verify-email.js +15 -0
- package/dist/email/templates/welcome.d.ts +5 -0
- package/dist/email/templates/welcome.js +13 -0
- package/dist/email/types.d.ts +49 -0
- package/dist/email/types.js +1 -0
- package/dist/llms/generate.d.ts +46 -0
- package/dist/llms/generate.js +185 -0
- package/dist/permissions/guard.d.ts +28 -0
- package/dist/permissions/guard.js +30 -0
- package/dist/permissions/index.d.ts +6 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/service.d.ts +80 -0
- package/dist/permissions/service.js +210 -0
- package/dist/permissions/tables.d.ts +5 -0
- package/dist/permissions/tables.js +68 -0
- package/dist/permissions/types.d.ts +33 -0
- package/dist/permissions/types.js +1 -0
- package/dist/runtime/app-shell.js +163 -0
- package/dist/runtime/auth.d.ts +10 -0
- package/dist/runtime/auth.js +30 -0
- package/dist/runtime/communication.d.ts +137 -0
- package/dist/runtime/communication.js +228 -0
- package/dist/runtime/error-boundary.d.ts +23 -0
- package/dist/runtime/error-boundary.js +120 -0
- package/dist/runtime/i18n.d.ts +6 -1
- package/dist/runtime/i18n.js +42 -21
- package/dist/runtime/router-data.d.ts +5 -0
- package/dist/runtime/router-data.js +121 -16
- package/dist/runtime/router-hydration.js +25 -0
- package/dist/runtime/router.d.ts +21 -1
- package/dist/runtime/router.js +221 -39
- package/dist/runtime/socket-client.d.ts +2 -0
- package/dist/runtime/socket-client.js +30 -0
- package/dist/runtime/webrtc.d.ts +47 -0
- package/dist/runtime/webrtc.js +178 -0
- package/dist/shared/graceful-shutdown.d.ts +8 -0
- package/dist/shared/graceful-shutdown.js +36 -0
- package/dist/shared/health.d.ts +8 -0
- package/dist/shared/health.js +25 -0
- package/dist/shared/llms-txt.d.ts +31 -0
- package/dist/shared/llms-txt.js +85 -0
- package/dist/shared/logger.d.ts +32 -0
- package/dist/shared/logger.js +93 -0
- package/dist/shared/meta.d.ts +27 -0
- package/dist/shared/meta.js +71 -0
- package/dist/shared/middleware-runner.d.ts +9 -0
- package/dist/shared/middleware-runner.js +29 -0
- package/dist/shared/rate-limit.d.ts +18 -0
- package/dist/shared/rate-limit.js +71 -0
- package/dist/shared/request-id.d.ts +5 -0
- package/dist/shared/request-id.js +18 -0
- package/dist/shared/route-matching.js +16 -1
- package/dist/shared/security-headers.d.ts +18 -0
- package/dist/shared/security-headers.js +38 -0
- package/dist/shared/socket-io-setup.d.ts +11 -0
- package/dist/shared/socket-io-setup.js +51 -0
- package/dist/shared/types.d.ts +16 -0
- package/dist/shared/utils.d.ts +37 -7
- package/dist/shared/utils.js +175 -26
- package/dist/storage/adapters/local.d.ts +44 -0
- package/dist/storage/adapters/local.js +85 -0
- package/dist/storage/adapters/s3.d.ts +32 -0
- package/dist/storage/adapters/s3.js +116 -0
- package/dist/storage/adapters/types.d.ts +53 -0
- package/dist/storage/adapters/types.js +1 -0
- package/dist/storage/index.d.ts +76 -0
- package/dist/storage/index.js +83 -0
- package/package.json +20 -1
- package/templates/blog/api/posts.ts +6 -0
- package/templates/blog/data/migrations/001_init.sql +13 -0
- package/templates/blog/lumenjs.config.ts +3 -0
- package/templates/blog/package.json +14 -0
- package/templates/blog/pages/_layout.ts +25 -0
- package/templates/blog/pages/index.ts +65 -0
- package/templates/blog/pages/posts/[slug].ts +60 -0
- package/templates/blog/pages/tag/[tag].ts +44 -0
- package/templates/dashboard/api/stats.ts +10 -0
- package/templates/dashboard/data/migrations/001_init.sql +13 -0
- package/templates/dashboard/lumenjs.config.ts +3 -0
- package/templates/dashboard/package.json +14 -0
- package/templates/dashboard/pages/_layout.ts +25 -0
- package/templates/dashboard/pages/index.ts +72 -0
- package/templates/dashboard/pages/settings/index.ts +29 -0
- package/templates/default/lumenjs.config.ts +3 -0
- package/templates/default/package.json +14 -0
- package/templates/default/pages/index.ts +24 -0
- package/templates/social/api/posts/[id].ts +14 -0
- package/templates/social/api/posts.ts +11 -0
- package/templates/social/api/profile/[username].ts +10 -0
- package/templates/social/api/upload.ts +19 -0
- package/templates/social/data/migrations/001_init.sql +78 -0
- package/templates/social/data/migrations/002_add_image_url.sql +1 -0
- package/templates/social/data/migrations/003_auth.sql +7 -0
- package/templates/social/docs/architecture.md +76 -0
- package/templates/social/docs/components.md +100 -0
- package/templates/social/docs/data.md +89 -0
- package/templates/social/docs/pages.md +96 -0
- package/templates/social/docs/theming.md +52 -0
- package/templates/social/lib/media.ts +130 -0
- package/templates/social/lumenjs.auth.ts +21 -0
- package/templates/social/lumenjs.config.ts +3 -0
- package/templates/social/package.json +5 -0
- package/templates/social/pages/_layout.ts +239 -0
- package/templates/social/pages/apps/[id].ts +173 -0
- package/templates/social/pages/apps/index.ts +116 -0
- package/templates/social/pages/auth/login.ts +92 -0
- package/templates/social/pages/bookmarks.ts +57 -0
- package/templates/social/pages/explore.ts +73 -0
- package/templates/social/pages/index.ts +351 -0
- package/templates/social/pages/messages.ts +298 -0
- package/templates/social/pages/new.ts +77 -0
- package/templates/social/pages/notifications.ts +73 -0
- package/templates/social/pages/post/[id].ts +124 -0
- package/templates/social/pages/profile/[username].ts +100 -0
- package/templates/social/pages/settings/accessibility.ts +153 -0
- package/templates/social/pages/settings/account.ts +260 -0
- package/templates/social/pages/settings/help.ts +141 -0
- package/templates/social/pages/settings/language.ts +103 -0
- package/templates/social/pages/settings/privacy.ts +183 -0
- package/templates/social/pages/settings/security.ts +133 -0
- package/templates/social/pages/settings.ts +185 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { pathToFileURL } from 'url';
|
|
3
|
+
export async function setupSocketIO(options) {
|
|
4
|
+
// Resolve socket.io from the project's node_modules (not from lumenjs's own node_modules)
|
|
5
|
+
let SocketIOServer;
|
|
6
|
+
if (options.projectDir) {
|
|
7
|
+
const require = createRequire(pathToFileURL(options.projectDir + '/package.json').href);
|
|
8
|
+
const socketIo = require('socket.io');
|
|
9
|
+
SocketIOServer = socketIo.Server || socketIo.default?.Server || socketIo;
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
const socketIo = await import('socket.io');
|
|
13
|
+
SocketIOServer = socketIo.Server;
|
|
14
|
+
}
|
|
15
|
+
const io = new SocketIOServer(options.httpServer, {
|
|
16
|
+
path: '/__nk_socketio/',
|
|
17
|
+
cors: { origin: process.env.NODE_ENV === 'production' ? false : '*' },
|
|
18
|
+
});
|
|
19
|
+
for (const route of options.routes) {
|
|
20
|
+
if (!route.hasSocket)
|
|
21
|
+
continue;
|
|
22
|
+
const ns = `/nk${route.path === '/' ? '/index' : route.path}`;
|
|
23
|
+
io.of(ns).on('connection', async (socket) => {
|
|
24
|
+
try {
|
|
25
|
+
const mod = await options.loadModule(route.filePath);
|
|
26
|
+
if (!mod.socket)
|
|
27
|
+
return;
|
|
28
|
+
const push = (data) => socket.emit('nk:data', data);
|
|
29
|
+
const on = (event, handler) => {
|
|
30
|
+
socket.on(`nk:${event}`, handler);
|
|
31
|
+
};
|
|
32
|
+
const room = {
|
|
33
|
+
join: (name) => socket.join(name),
|
|
34
|
+
leave: (name) => socket.leave(name),
|
|
35
|
+
broadcast: (name, data) => socket.to(name).emit('nk:data', data),
|
|
36
|
+
broadcastAll: (name, data) => io.of(ns).to(name).emit('nk:data', data),
|
|
37
|
+
};
|
|
38
|
+
const params = socket.handshake.query.__params
|
|
39
|
+
? JSON.parse(socket.handshake.query.__params) : {};
|
|
40
|
+
const locale = socket.handshake.query.__locale;
|
|
41
|
+
const cleanup = mod.socket({ on, push, room, params, headers: socket.handshake.headers, locale, socket });
|
|
42
|
+
socket.on('disconnect', () => { if (typeof cleanup === 'function')
|
|
43
|
+
cleanup(); });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
console.error(`[LumenJS] Socket handler error for ${route.path}:`, err);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return io;
|
|
51
|
+
}
|
package/dist/shared/types.d.ts
CHANGED
|
@@ -2,22 +2,38 @@ export interface ManifestLayout {
|
|
|
2
2
|
dir: string;
|
|
3
3
|
module: string;
|
|
4
4
|
hasLoader: boolean;
|
|
5
|
+
hasSubscribe: boolean;
|
|
5
6
|
}
|
|
6
7
|
export interface ManifestRoute {
|
|
7
8
|
path: string;
|
|
8
9
|
module: string;
|
|
9
10
|
hasLoader: boolean;
|
|
11
|
+
hasSubscribe: boolean;
|
|
12
|
+
hasAuth?: boolean;
|
|
13
|
+
hasMeta?: boolean;
|
|
14
|
+
authRoles?: string[];
|
|
15
|
+
hasStandalone?: boolean;
|
|
10
16
|
tagName?: string;
|
|
11
17
|
layouts?: string[];
|
|
18
|
+
prerender?: boolean;
|
|
12
19
|
}
|
|
13
20
|
export interface I18nManifest {
|
|
14
21
|
locales: string[];
|
|
15
22
|
defaultLocale: string;
|
|
16
23
|
prefixDefault: boolean;
|
|
17
24
|
}
|
|
25
|
+
export interface ManifestMiddleware {
|
|
26
|
+
dir: string;
|
|
27
|
+
module: string;
|
|
28
|
+
}
|
|
18
29
|
export interface BuildManifest {
|
|
19
30
|
routes: ManifestRoute[];
|
|
20
31
|
apiRoutes: ManifestRoute[];
|
|
21
32
|
layouts: ManifestLayout[];
|
|
33
|
+
middlewares?: ManifestMiddleware[];
|
|
22
34
|
i18n?: I18nManifest;
|
|
35
|
+
auth?: {
|
|
36
|
+
configModule: string;
|
|
37
|
+
};
|
|
38
|
+
prefetch: 'hover' | 'viewport' | 'none';
|
|
23
39
|
}
|
package/dist/shared/utils.d.ts
CHANGED
|
@@ -12,11 +12,6 @@ export declare function stripOuterLitMarkers(html: string): string;
|
|
|
12
12
|
* 'app/[id]' → 'layout-app-id'
|
|
13
13
|
*/
|
|
14
14
|
export declare function dirToLayoutTagName(dir: string): string;
|
|
15
|
-
/**
|
|
16
|
-
* Find the custom element tag name from a page module.
|
|
17
|
-
* Pages use @customElement('page-xxx') which registers the element.
|
|
18
|
-
*/
|
|
19
|
-
export declare function findTagName(mod: Record<string, any>): string | null;
|
|
20
15
|
/**
|
|
21
16
|
* Convert a relative file path within pages/ to a page tag name.
|
|
22
17
|
* 'index.ts' → 'page-index'
|
|
@@ -32,17 +27,52 @@ export declare function isRedirectResponse(value: any): value is {
|
|
|
32
27
|
status?: number;
|
|
33
28
|
};
|
|
34
29
|
/**
|
|
35
|
-
*
|
|
30
|
+
* Patch a custom element class's loaderData setter to also spread
|
|
31
|
+
* individual properties from the loader data object onto the element.
|
|
32
|
+
* This enables components to declare individual reactive properties
|
|
33
|
+
* (e.g., `stats: { type: Array }`) instead of using `this.loaderData.stats`.
|
|
34
|
+
*
|
|
35
|
+
* Must be called AFTER the element is registered and BEFORE SSR rendering.
|
|
36
36
|
*/
|
|
37
|
-
export declare function
|
|
37
|
+
export declare function patchLoaderDataSpread(tagName: string): void;
|
|
38
|
+
export declare function readBody(req: any, maxSize?: number): Promise<any>;
|
|
38
39
|
/**
|
|
39
40
|
* Escape HTML special characters for safe embedding.
|
|
40
41
|
*/
|
|
41
42
|
export declare function escapeHtml(text: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Check if a page file exports `export const prerender = true`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function fileHasPrerender(filePath: string): boolean;
|
|
42
47
|
/**
|
|
43
48
|
* Check if a page/layout file exports a loader() function.
|
|
44
49
|
*/
|
|
45
50
|
export declare function fileHasLoader(filePath: string): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Check if a page/layout file exports a subscribe() function.
|
|
53
|
+
*/
|
|
54
|
+
export declare function fileHasSubscribe(filePath: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Check if a page file exports an `auth` constant (before the class definition).
|
|
57
|
+
*/
|
|
58
|
+
export declare function fileHasAuth(filePath: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Check if a page file exports a `meta` object or function.
|
|
61
|
+
* Supports both `export const meta = { ... }` and `export function meta(...)`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function fileHasMeta(filePath: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Check if a page file exports a `socket` function or constant.
|
|
66
|
+
*/
|
|
67
|
+
export declare function fileHasSocket(filePath: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Check if a page exports `standalone = true` (renders without any layout).
|
|
70
|
+
*/
|
|
71
|
+
export declare function fileHasStandalone(filePath: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Return the HTTP methods exported by an API route file (e.g. ['GET', 'POST']).
|
|
74
|
+
*/
|
|
75
|
+
export declare function fileGetApiMethods(filePath: string): string[];
|
|
46
76
|
/**
|
|
47
77
|
* Convert a file path (relative to pages/) to a route path.
|
|
48
78
|
*/
|
package/dist/shared/utils.js
CHANGED
|
@@ -27,28 +27,6 @@ export function dirToLayoutTagName(dir) {
|
|
|
27
27
|
.toLowerCase();
|
|
28
28
|
return `layout-${name}`;
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Find the custom element tag name from a page module.
|
|
32
|
-
* Pages use @customElement('page-xxx') which registers the element.
|
|
33
|
-
*/
|
|
34
|
-
export function findTagName(mod) {
|
|
35
|
-
for (const key of Object.keys(mod)) {
|
|
36
|
-
const val = mod[key];
|
|
37
|
-
if (typeof val === 'function' && val.prototype) {
|
|
38
|
-
if (val.is)
|
|
39
|
-
return val.is;
|
|
40
|
-
if (val.elementProperties || val.properties) {
|
|
41
|
-
const className = val.name || key;
|
|
42
|
-
const tag = className
|
|
43
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
44
|
-
.toLowerCase();
|
|
45
|
-
if (tag.includes('-'))
|
|
46
|
-
return tag;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
30
|
/**
|
|
53
31
|
* Convert a relative file path within pages/ to a page tag name.
|
|
54
32
|
* 'index.ts' → 'page-index'
|
|
@@ -71,13 +49,65 @@ export function filePathToTagName(filePath) {
|
|
|
71
49
|
export function isRedirectResponse(value) {
|
|
72
50
|
return value && typeof value === 'object' && typeof value.location === 'string' && value.__nk_redirect === true;
|
|
73
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Patch a custom element class's loaderData setter to also spread
|
|
54
|
+
* individual properties from the loader data object onto the element.
|
|
55
|
+
* This enables components to declare individual reactive properties
|
|
56
|
+
* (e.g., `stats: { type: Array }`) instead of using `this.loaderData.stats`.
|
|
57
|
+
*
|
|
58
|
+
* Must be called AFTER the element is registered and BEFORE SSR rendering.
|
|
59
|
+
*/
|
|
60
|
+
export function patchLoaderDataSpread(tagName) {
|
|
61
|
+
const g = globalThis;
|
|
62
|
+
const ElementClass = g.customElements?.get?.(tagName);
|
|
63
|
+
if (!ElementClass)
|
|
64
|
+
return;
|
|
65
|
+
const proto = ElementClass.prototype;
|
|
66
|
+
const original = Object.getOwnPropertyDescriptor(proto, 'loaderData');
|
|
67
|
+
Object.defineProperty(proto, 'loaderData', {
|
|
68
|
+
set(value) {
|
|
69
|
+
if (original?.set) {
|
|
70
|
+
original.set.call(this, value);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
this.__nk_loaderData = value;
|
|
74
|
+
}
|
|
75
|
+
if (value && typeof value === 'object') {
|
|
76
|
+
const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype', 'loaderData',
|
|
77
|
+
'render', 'connectedCallback', 'disconnectedCallback', 'attributeChangedCallback',
|
|
78
|
+
'adoptedCallback', 'innerHTML', 'outerHTML', 'textContent']);
|
|
79
|
+
for (const [key, val] of Object.entries(value)) {
|
|
80
|
+
if (!BLOCKED_KEYS.has(key)) {
|
|
81
|
+
this[key] = val;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
get() {
|
|
87
|
+
if (original?.get)
|
|
88
|
+
return original.get.call(this);
|
|
89
|
+
return this.__nk_loaderData;
|
|
90
|
+
},
|
|
91
|
+
configurable: true
|
|
92
|
+
});
|
|
93
|
+
}
|
|
74
94
|
/**
|
|
75
95
|
* Read and parse the body of an HTTP request.
|
|
76
96
|
*/
|
|
77
|
-
|
|
97
|
+
const DEFAULT_MAX_BODY = 1024 * 1024; // 1 MB
|
|
98
|
+
export function readBody(req, maxSize = DEFAULT_MAX_BODY) {
|
|
78
99
|
return new Promise((resolve, reject) => {
|
|
79
100
|
let data = '';
|
|
80
|
-
|
|
101
|
+
let size = 0;
|
|
102
|
+
req.on('data', (chunk) => {
|
|
103
|
+
size += chunk.length;
|
|
104
|
+
if (size > maxSize) {
|
|
105
|
+
req.destroy();
|
|
106
|
+
reject(new Error('Request body too large'));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
data += chunk.toString();
|
|
110
|
+
});
|
|
81
111
|
req.on('end', () => {
|
|
82
112
|
if (!data)
|
|
83
113
|
return resolve(undefined);
|
|
@@ -95,7 +125,33 @@ export function readBody(req) {
|
|
|
95
125
|
* Escape HTML special characters for safe embedding.
|
|
96
126
|
*/
|
|
97
127
|
export function escapeHtml(text) {
|
|
98
|
-
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
128
|
+
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Check if code has a top-level export of a named function (before the class definition).
|
|
132
|
+
* In LumenJS, loader/subscribe are always declared before `export class`.
|
|
133
|
+
*/
|
|
134
|
+
function hasTopLevelExport(content, fnName) {
|
|
135
|
+
const classStart = content.search(/export\s+class\s+\w+/);
|
|
136
|
+
const fnRegex = new RegExp(`export\\s+(async\\s+)?function\\s+${fnName}\\s*\\(`);
|
|
137
|
+
const match = fnRegex.exec(content);
|
|
138
|
+
if (!match)
|
|
139
|
+
return false;
|
|
140
|
+
if (classStart >= 0 && match.index > classStart)
|
|
141
|
+
return false;
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Check if a page file exports `export const prerender = true`.
|
|
146
|
+
*/
|
|
147
|
+
export function fileHasPrerender(filePath) {
|
|
148
|
+
try {
|
|
149
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
150
|
+
return /export\s+const\s+prerender\s*=\s*true/.test(content);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
99
155
|
}
|
|
100
156
|
/**
|
|
101
157
|
* Check if a page/layout file exports a loader() function.
|
|
@@ -103,12 +159,105 @@ export function escapeHtml(text) {
|
|
|
103
159
|
export function fileHasLoader(filePath) {
|
|
104
160
|
try {
|
|
105
161
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
106
|
-
return
|
|
162
|
+
return hasTopLevelExport(content, 'loader');
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Check if a page/layout file exports a subscribe() function.
|
|
170
|
+
*/
|
|
171
|
+
export function fileHasSubscribe(filePath) {
|
|
172
|
+
try {
|
|
173
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
174
|
+
return hasTopLevelExport(content, 'subscribe');
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Check if a page file exports an `auth` constant (before the class definition).
|
|
182
|
+
*/
|
|
183
|
+
export function fileHasAuth(filePath) {
|
|
184
|
+
try {
|
|
185
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
186
|
+
const classStart = content.search(/export\s+class\s+\w+/);
|
|
187
|
+
const match = /export\s+const\s+auth\s*=/.exec(content);
|
|
188
|
+
if (!match)
|
|
189
|
+
return false;
|
|
190
|
+
if (classStart >= 0 && match.index > classStart)
|
|
191
|
+
return false;
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Check if a page file exports a `meta` object or function.
|
|
200
|
+
* Supports both `export const meta = { ... }` and `export function meta(...)`.
|
|
201
|
+
*/
|
|
202
|
+
export function fileHasMeta(filePath) {
|
|
203
|
+
try {
|
|
204
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
205
|
+
const classStart = content.search(/export\s+class\s+\w+/);
|
|
206
|
+
// Check for `export const meta` or `export function meta`
|
|
207
|
+
const match = /export\s+(const\s+meta\s*=|(async\s+)?function\s+meta\s*\()/.exec(content);
|
|
208
|
+
if (!match)
|
|
209
|
+
return false;
|
|
210
|
+
if (classStart >= 0 && match.index > classStart)
|
|
211
|
+
return false;
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Check if a page file exports a `socket` function or constant.
|
|
220
|
+
*/
|
|
221
|
+
export function fileHasSocket(filePath) {
|
|
222
|
+
try {
|
|
223
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
224
|
+
return /export\s+(function|const)\s+socket[\s(=]/.test(content);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Check if a page exports `standalone = true` (renders without any layout).
|
|
232
|
+
*/
|
|
233
|
+
export function fileHasStandalone(filePath) {
|
|
234
|
+
try {
|
|
235
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
236
|
+
const classStart = content.search(/export\s+class\s+\w+/);
|
|
237
|
+
const match = /export\s+const\s+standalone\s*=/.exec(content);
|
|
238
|
+
if (!match)
|
|
239
|
+
return false;
|
|
240
|
+
if (classStart >= 0 && match.index > classStart)
|
|
241
|
+
return false;
|
|
242
|
+
return true;
|
|
107
243
|
}
|
|
108
244
|
catch {
|
|
109
245
|
return false;
|
|
110
246
|
}
|
|
111
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Return the HTTP methods exported by an API route file (e.g. ['GET', 'POST']).
|
|
250
|
+
*/
|
|
251
|
+
export function fileGetApiMethods(filePath) {
|
|
252
|
+
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
253
|
+
try {
|
|
254
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
255
|
+
return HTTP_METHODS.filter(m => new RegExp(`export\\s+(async\\s+)?function\\s+${m}[\\s(]`).test(content));
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
112
261
|
/**
|
|
113
262
|
* Convert a file path (relative to pages/) to a route path.
|
|
114
263
|
*/
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { StorageAdapter, StoredFile, PutOptions, PresignPutOptions, PresignGetOptions, PresignedUpload } from './types.js';
|
|
2
|
+
interface PendingUpload {
|
|
3
|
+
key: string;
|
|
4
|
+
mimeType?: string;
|
|
5
|
+
maxSize?: number;
|
|
6
|
+
/** Absolute expiry time in ms (Date.now()) */
|
|
7
|
+
expiresAt: number;
|
|
8
|
+
}
|
|
9
|
+
export interface LocalStorageOptions {
|
|
10
|
+
/** Directory to write uploaded files to. Default: './uploads' */
|
|
11
|
+
uploadDir: string;
|
|
12
|
+
/** URL path prefix for serving files. Default: '/uploads' */
|
|
13
|
+
publicPath: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Local-disk storage adapter for development.
|
|
17
|
+
*
|
|
18
|
+
* Files are written to `uploadDir` and served at `publicPath`.
|
|
19
|
+
* Presigned uploads use in-memory tokens consumed by `vite-plugin-storage`.
|
|
20
|
+
*/
|
|
21
|
+
export declare class LocalStorageAdapter implements StorageAdapter {
|
|
22
|
+
readonly uploadDir: string;
|
|
23
|
+
readonly publicPath: string;
|
|
24
|
+
/**
|
|
25
|
+
* Pending presigned upload tokens.
|
|
26
|
+
* Keyed by token → consumed by the Vite dev server plugin on upload.
|
|
27
|
+
*/
|
|
28
|
+
readonly pendingUploads: Map<string, PendingUpload>;
|
|
29
|
+
constructor(options: LocalStorageOptions);
|
|
30
|
+
/** Resolve a storage key to an absolute path and ensure it stays within uploadDir. */
|
|
31
|
+
private safePath;
|
|
32
|
+
put(data: Buffer, options?: PutOptions): Promise<StoredFile>;
|
|
33
|
+
delete(key: string): Promise<void>;
|
|
34
|
+
presignPut(key: string, options?: PresignPutOptions): Promise<PresignedUpload>;
|
|
35
|
+
presignGet(_key: string, _options?: PresignGetOptions): Promise<string>;
|
|
36
|
+
publicUrl(key: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* Consume a presigned upload token.
|
|
39
|
+
* Returns the pending upload metadata if the token is valid and unexpired, else undefined.
|
|
40
|
+
* Calling this removes the token (one-time use).
|
|
41
|
+
*/
|
|
42
|
+
consumeUpload(token: string): PendingUpload | undefined;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
/**
|
|
5
|
+
* Local-disk storage adapter for development.
|
|
6
|
+
*
|
|
7
|
+
* Files are written to `uploadDir` and served at `publicPath`.
|
|
8
|
+
* Presigned uploads use in-memory tokens consumed by `vite-plugin-storage`.
|
|
9
|
+
*/
|
|
10
|
+
export class LocalStorageAdapter {
|
|
11
|
+
constructor(options) {
|
|
12
|
+
/**
|
|
13
|
+
* Pending presigned upload tokens.
|
|
14
|
+
* Keyed by token → consumed by the Vite dev server plugin on upload.
|
|
15
|
+
*/
|
|
16
|
+
this.pendingUploads = new Map();
|
|
17
|
+
this.uploadDir = path.resolve(options.uploadDir);
|
|
18
|
+
this.publicPath = options.publicPath.replace(/\/$/, '');
|
|
19
|
+
fs.mkdirSync(this.uploadDir, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
/** Resolve a storage key to an absolute path and ensure it stays within uploadDir. */
|
|
22
|
+
safePath(key) {
|
|
23
|
+
const resolved = path.resolve(this.uploadDir, key);
|
|
24
|
+
if (!resolved.startsWith(this.uploadDir + path.sep) && resolved !== this.uploadDir) {
|
|
25
|
+
throw new Error('Invalid storage key: path traversal detected');
|
|
26
|
+
}
|
|
27
|
+
return resolved;
|
|
28
|
+
}
|
|
29
|
+
async put(data, options) {
|
|
30
|
+
const key = options?.key ?? crypto.randomUUID();
|
|
31
|
+
const filePath = this.safePath(key);
|
|
32
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
33
|
+
fs.writeFileSync(filePath, data);
|
|
34
|
+
return {
|
|
35
|
+
key,
|
|
36
|
+
url: this.publicUrl(key),
|
|
37
|
+
size: data.length,
|
|
38
|
+
mimeType: options?.mimeType ?? 'application/octet-stream',
|
|
39
|
+
fileName: options?.fileName ?? key,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
async delete(key) {
|
|
43
|
+
const filePath = this.safePath(key);
|
|
44
|
+
if (fs.existsSync(filePath)) {
|
|
45
|
+
fs.unlinkSync(filePath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async presignPut(key, options) {
|
|
49
|
+
const expiresIn = options?.expiresIn ?? 3600;
|
|
50
|
+
const expiresAt = new Date(Date.now() + expiresIn * 1000);
|
|
51
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
52
|
+
this.pendingUploads.set(token, {
|
|
53
|
+
key,
|
|
54
|
+
mimeType: options?.mimeType,
|
|
55
|
+
maxSize: options?.maxSize,
|
|
56
|
+
expiresAt: expiresAt.getTime(),
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
uploadUrl: `/__nk_storage/upload/${token}`,
|
|
60
|
+
key,
|
|
61
|
+
expiresAt: expiresAt.toISOString(),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async presignGet(_key, _options) {
|
|
65
|
+
// Local dev — files are served publicly at the static path.
|
|
66
|
+
return this.publicUrl(_key);
|
|
67
|
+
}
|
|
68
|
+
publicUrl(key) {
|
|
69
|
+
return `${this.publicPath}/${key}`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Consume a presigned upload token.
|
|
73
|
+
* Returns the pending upload metadata if the token is valid and unexpired, else undefined.
|
|
74
|
+
* Calling this removes the token (one-time use).
|
|
75
|
+
*/
|
|
76
|
+
consumeUpload(token) {
|
|
77
|
+
const pending = this.pendingUploads.get(token);
|
|
78
|
+
if (!pending)
|
|
79
|
+
return undefined;
|
|
80
|
+
this.pendingUploads.delete(token);
|
|
81
|
+
if (Date.now() > pending.expiresAt)
|
|
82
|
+
return undefined;
|
|
83
|
+
return pending;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { StorageAdapter, StoredFile, PutOptions, PresignPutOptions, PresignGetOptions, PresignedUpload } from './types.js';
|
|
2
|
+
export interface S3StorageOptions {
|
|
3
|
+
bucket: string;
|
|
4
|
+
region: string;
|
|
5
|
+
accessKeyId: string;
|
|
6
|
+
secretAccessKey: string;
|
|
7
|
+
/** Custom endpoint for S3-compatible APIs: MinIO, Cloudflare R2, etc. */
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
/** Public CDN base URL. Defaults to https://{bucket}.s3.{region}.amazonaws.com */
|
|
10
|
+
publicBaseUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* S3-compatible storage adapter.
|
|
14
|
+
* Works with AWS S3, Cloudflare R2, MinIO, and any S3-compatible API.
|
|
15
|
+
*
|
|
16
|
+
* Requires optional peer dependencies:
|
|
17
|
+
* @aws-sdk/client-s3
|
|
18
|
+
* @aws-sdk/s3-request-presigner
|
|
19
|
+
*/
|
|
20
|
+
export declare class S3StorageAdapter implements StorageAdapter {
|
|
21
|
+
private readonly options;
|
|
22
|
+
private _client;
|
|
23
|
+
private _getSignedUrl;
|
|
24
|
+
constructor(options: S3StorageOptions);
|
|
25
|
+
private getClient;
|
|
26
|
+
private getSignedUrl;
|
|
27
|
+
put(data: Buffer, options?: PutOptions): Promise<StoredFile>;
|
|
28
|
+
delete(key: string): Promise<void>;
|
|
29
|
+
presignPut(key: string, options?: PresignPutOptions): Promise<PresignedUpload>;
|
|
30
|
+
presignGet(key: string, options?: PresignGetOptions): Promise<string>;
|
|
31
|
+
publicUrl(key: string): string;
|
|
32
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
const SDK_INSTALL_HINT = 'Install with: npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner';
|
|
3
|
+
/**
|
|
4
|
+
* S3-compatible storage adapter.
|
|
5
|
+
* Works with AWS S3, Cloudflare R2, MinIO, and any S3-compatible API.
|
|
6
|
+
*
|
|
7
|
+
* Requires optional peer dependencies:
|
|
8
|
+
* @aws-sdk/client-s3
|
|
9
|
+
* @aws-sdk/s3-request-presigner
|
|
10
|
+
*/
|
|
11
|
+
export class S3StorageAdapter {
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this._client = null;
|
|
14
|
+
this._getSignedUrl = null;
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
async getClient() {
|
|
18
|
+
if (this._client)
|
|
19
|
+
return this._client;
|
|
20
|
+
let S3Client, PutObjectCommand, DeleteObjectCommand, GetObjectCommand;
|
|
21
|
+
try {
|
|
22
|
+
const mod = await import('@aws-sdk/client-s3');
|
|
23
|
+
S3Client = mod.S3Client;
|
|
24
|
+
PutObjectCommand = mod.PutObjectCommand;
|
|
25
|
+
DeleteObjectCommand = mod.DeleteObjectCommand;
|
|
26
|
+
GetObjectCommand = mod.GetObjectCommand;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error(`[LumenJS:Storage] @aws-sdk/client-s3 is required for S3 storage. ${SDK_INSTALL_HINT}`);
|
|
30
|
+
}
|
|
31
|
+
this._client = {
|
|
32
|
+
s3: new S3Client({
|
|
33
|
+
region: this.options.region,
|
|
34
|
+
endpoint: this.options.endpoint,
|
|
35
|
+
credentials: {
|
|
36
|
+
accessKeyId: this.options.accessKeyId,
|
|
37
|
+
secretAccessKey: this.options.secretAccessKey,
|
|
38
|
+
},
|
|
39
|
+
forcePathStyle: !!this.options.endpoint, // required for MinIO
|
|
40
|
+
}),
|
|
41
|
+
PutObjectCommand,
|
|
42
|
+
DeleteObjectCommand,
|
|
43
|
+
GetObjectCommand,
|
|
44
|
+
};
|
|
45
|
+
return this._client;
|
|
46
|
+
}
|
|
47
|
+
async getSignedUrl() {
|
|
48
|
+
if (this._getSignedUrl)
|
|
49
|
+
return this._getSignedUrl;
|
|
50
|
+
try {
|
|
51
|
+
const mod = await import('@aws-sdk/s3-request-presigner');
|
|
52
|
+
this._getSignedUrl = mod.getSignedUrl;
|
|
53
|
+
return this._getSignedUrl;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error(`[LumenJS:Storage] @aws-sdk/s3-request-presigner is required for presigned URLs. ${SDK_INSTALL_HINT}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async put(data, options) {
|
|
60
|
+
const key = options?.key ?? crypto.randomUUID();
|
|
61
|
+
const mimeType = options?.mimeType ?? 'application/octet-stream';
|
|
62
|
+
const acl = options?.acl ?? 'public-read';
|
|
63
|
+
const { s3, PutObjectCommand } = await this.getClient();
|
|
64
|
+
await s3.send(new PutObjectCommand({
|
|
65
|
+
Bucket: this.options.bucket,
|
|
66
|
+
Key: key,
|
|
67
|
+
Body: data,
|
|
68
|
+
ContentType: mimeType,
|
|
69
|
+
ACL: acl,
|
|
70
|
+
...(options?.fileName
|
|
71
|
+
? { ContentDisposition: `inline; filename="${options.fileName.replace(/[\r\n"\\]/g, '_')}"` }
|
|
72
|
+
: {}),
|
|
73
|
+
}));
|
|
74
|
+
return {
|
|
75
|
+
key,
|
|
76
|
+
url: this.publicUrl(key),
|
|
77
|
+
size: data.length,
|
|
78
|
+
mimeType,
|
|
79
|
+
fileName: options?.fileName ?? key,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async delete(key) {
|
|
83
|
+
const { s3, DeleteObjectCommand } = await this.getClient();
|
|
84
|
+
await s3.send(new DeleteObjectCommand({ Bucket: this.options.bucket, Key: key }));
|
|
85
|
+
}
|
|
86
|
+
async presignPut(key, options) {
|
|
87
|
+
const expiresIn = options?.expiresIn ?? 3600;
|
|
88
|
+
const expiresAt = new Date(Date.now() + expiresIn * 1000);
|
|
89
|
+
const { s3, PutObjectCommand } = await this.getClient();
|
|
90
|
+
const getSignedUrl = await this.getSignedUrl();
|
|
91
|
+
const command = new PutObjectCommand({
|
|
92
|
+
Bucket: this.options.bucket,
|
|
93
|
+
Key: key,
|
|
94
|
+
...(options?.mimeType ? { ContentType: options.mimeType } : {}),
|
|
95
|
+
});
|
|
96
|
+
const uploadUrl = await getSignedUrl(s3, command, { expiresIn });
|
|
97
|
+
return { uploadUrl, key, expiresAt: expiresAt.toISOString() };
|
|
98
|
+
}
|
|
99
|
+
async presignGet(key, options) {
|
|
100
|
+
const expiresIn = options?.expiresIn ?? 3600;
|
|
101
|
+
const { s3, GetObjectCommand } = await this.getClient();
|
|
102
|
+
const getSignedUrl = await this.getSignedUrl();
|
|
103
|
+
const command = new GetObjectCommand({ Bucket: this.options.bucket, Key: key });
|
|
104
|
+
return getSignedUrl(s3, command, { expiresIn });
|
|
105
|
+
}
|
|
106
|
+
publicUrl(key) {
|
|
107
|
+
if (this.options.publicBaseUrl) {
|
|
108
|
+
return `${this.options.publicBaseUrl.replace(/\/$/, '')}/${key}`;
|
|
109
|
+
}
|
|
110
|
+
if (this.options.endpoint) {
|
|
111
|
+
// MinIO / R2: {endpoint}/{bucket}/{key}
|
|
112
|
+
return `${this.options.endpoint.replace(/\/$/, '')}/${this.options.bucket}/${key}`;
|
|
113
|
+
}
|
|
114
|
+
return `https://${this.options.bucket}.s3.${this.options.region}.amazonaws.com/${key}`;
|
|
115
|
+
}
|
|
116
|
+
}
|