@opencloud-eu/extension-sdk 6.0.0 → 6.1.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 +2 -12
- package/externalModules.mjs +15 -0
- package/federationRegistrationClient.mjs +122 -0
- package/index.mjs +109 -35
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -18,23 +18,13 @@ $ yarn add @opencloud-eu/extension-sdk --dev
|
|
|
18
18
|
|
|
19
19
|
## Usage
|
|
20
20
|
|
|
21
|
-
You can use the OpenCloud vite config via the `defineConfig` method provided by this package. The following example showcases
|
|
21
|
+
You can use the OpenCloud vite config via the `defineConfig` method provided by this package. The following example showcases what your `vite.config.ts` file could look like:
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
import { defineConfig } from '@opencloud-eu/extension-sdk'
|
|
25
25
|
|
|
26
26
|
export default defineConfig({
|
|
27
|
-
name: 'your-app'
|
|
28
|
-
server: {
|
|
29
|
-
port: 9700
|
|
30
|
-
},
|
|
31
|
-
build: {
|
|
32
|
-
rollupOptions: {
|
|
33
|
-
output: {
|
|
34
|
-
entryFileNames: 'your-app.js'
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
27
|
+
name: 'your-app'
|
|
38
28
|
})
|
|
39
29
|
```
|
|
40
30
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Shared dependencies provided by the host to external apps.
|
|
2
|
+
// Keep in sync with sharedModules in packages/web-runtime/src/container/application/index.ts
|
|
3
|
+
export const externalModules = [
|
|
4
|
+
'vue',
|
|
5
|
+
'luxon',
|
|
6
|
+
'pinia',
|
|
7
|
+
'vue3-gettext',
|
|
8
|
+
'@opencloud-eu/web-pkg',
|
|
9
|
+
'@opencloud-eu/web-client',
|
|
10
|
+
'@opencloud-eu/web-client/graph',
|
|
11
|
+
'@opencloud-eu/web-client/graph/generated',
|
|
12
|
+
'@opencloud-eu/web-client/ocs',
|
|
13
|
+
'@opencloud-eu/web-client/sse',
|
|
14
|
+
'@opencloud-eu/web-client/webdav'
|
|
15
|
+
]
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Vite plugin: remote-side periodic registration with a host dev server.
|
|
2
|
+
// Sends registration data at a fixed interval so the host always knows
|
|
3
|
+
// about this remote, even after the host restarts.
|
|
4
|
+
|
|
5
|
+
import https from 'https'
|
|
6
|
+
import http from 'http'
|
|
7
|
+
import { watch } from 'fs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Minimal fetch helper that accepts self-signed certificates (no extra deps).
|
|
11
|
+
* @param {string} url
|
|
12
|
+
* @param {{ method?: string, body?: string }} [options]
|
|
13
|
+
*/
|
|
14
|
+
function devFetch(url, { method = 'GET', body } = {}) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const mod = new URL(url).protocol === 'https:' ? https : http
|
|
17
|
+
const req = mod.request(url, { method, rejectUnauthorized: false }, (res) => {
|
|
18
|
+
let data = ''
|
|
19
|
+
res.on('data', (chunk) => (data += chunk))
|
|
20
|
+
res.on('end', () =>
|
|
21
|
+
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, data })
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
req.on('error', reject)
|
|
25
|
+
if (body) {
|
|
26
|
+
req.setHeader('Content-Type', 'application/json')
|
|
27
|
+
req.write(body)
|
|
28
|
+
}
|
|
29
|
+
req.end()
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {object} options
|
|
35
|
+
* @param {string} options.hostUrl - Host dev server URL (e.g. 'https://host.docker.internal:9201')
|
|
36
|
+
* @param {string} options.name - Remote ID used for registration
|
|
37
|
+
* @param {string} options.entryPoint - Remote entry filename (e.g. 'remoteEntry.mjs'), combined with the server address to form the full URL
|
|
38
|
+
* @param {() => Record<string, unknown> | undefined} [options.getMetadata] - Returns optional metadata to send with registration
|
|
39
|
+
* @param {number} [options.interval] - Re-registration interval in ms (default: 5000)
|
|
40
|
+
* @param {string} [options.path] - Host endpoint path (default: '/_dev/remotes')
|
|
41
|
+
* @param {string[]} [options.metadataWatchFiles] - File paths to watch for immediate re-registration on metadata change
|
|
42
|
+
*/
|
|
43
|
+
export function federationRegistrationClient({
|
|
44
|
+
hostUrl,
|
|
45
|
+
name,
|
|
46
|
+
entryPoint,
|
|
47
|
+
getMetadata,
|
|
48
|
+
interval = 5_000,
|
|
49
|
+
path = '/_dev/remotes',
|
|
50
|
+
metadataWatchFiles = []
|
|
51
|
+
}) {
|
|
52
|
+
return {
|
|
53
|
+
name: 'federation-registration-client',
|
|
54
|
+
apply: 'serve',
|
|
55
|
+
|
|
56
|
+
/** @param {import('vite').ViteDevServer} server */
|
|
57
|
+
configureServer(server) {
|
|
58
|
+
// Skip registration during test runs (Vitest uses serve mode internally)
|
|
59
|
+
if (server.config.mode === 'test') return
|
|
60
|
+
|
|
61
|
+
let registrationInterval = null
|
|
62
|
+
let entryPointUrl = ''
|
|
63
|
+
const watchers = []
|
|
64
|
+
|
|
65
|
+
async function register() {
|
|
66
|
+
if (!entryPointUrl) return
|
|
67
|
+
|
|
68
|
+
const metadata = getMetadata?.()
|
|
69
|
+
const url = `${hostUrl}${path}`
|
|
70
|
+
try {
|
|
71
|
+
const res = await devFetch(url, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
id: name,
|
|
75
|
+
path: entryPointUrl,
|
|
76
|
+
...(metadata && { metadata })
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
console.warn(`[federation-registration] Registration failed: ${res.status}`)
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Host is likely down — will retry on next interval
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
server.httpServer?.on('listening', () => {
|
|
88
|
+
const address = server.httpServer.address()
|
|
89
|
+
const port = typeof address === 'object' ? address?.port : address
|
|
90
|
+
const hostname =
|
|
91
|
+
typeof server.config.server.host === 'string' ? server.config.server.host : 'localhost'
|
|
92
|
+
entryPointUrl = `https://${hostname}:${port}/${entryPoint}`
|
|
93
|
+
|
|
94
|
+
register()
|
|
95
|
+
registrationInterval = setInterval(register, interval)
|
|
96
|
+
|
|
97
|
+
// Watch files for immediate re-registration on metadata change
|
|
98
|
+
for (const filePath of metadataWatchFiles) {
|
|
99
|
+
try {
|
|
100
|
+
const watcher = watch(filePath, { persistent: false }, () => {
|
|
101
|
+
console.log(`[federation-registration] ${filePath} changed, re-registering...`)
|
|
102
|
+
register()
|
|
103
|
+
})
|
|
104
|
+
watchers.push(watcher)
|
|
105
|
+
} catch {
|
|
106
|
+
// File may not exist yet — that's fine
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
server.httpServer?.on('close', () => {
|
|
112
|
+
if (registrationInterval) {
|
|
113
|
+
clearInterval(registrationInterval)
|
|
114
|
+
registrationInterval = null
|
|
115
|
+
}
|
|
116
|
+
for (const watcher of watchers) {
|
|
117
|
+
watcher.close()
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
package/index.mjs
CHANGED
|
@@ -7,20 +7,51 @@ import { join } from 'path'
|
|
|
7
7
|
import { cwd } from 'process'
|
|
8
8
|
import { readFileSync, existsSync } from 'fs'
|
|
9
9
|
import tailwindcss from '@tailwindcss/vite'
|
|
10
|
-
|
|
10
|
+
import basicSsl from '@vitejs/plugin-basic-ssl'
|
|
11
11
|
import vue from '@vitejs/plugin-vue'
|
|
12
|
+
import { federation } from '@module-federation/vite'
|
|
13
|
+
import { externalModules } from './externalModules.mjs'
|
|
14
|
+
import { federationRegistrationClient } from './federationRegistrationClient.mjs'
|
|
12
15
|
|
|
13
16
|
const distDir = process.env.OPENCLOUD_EXTENSION_DIST_DIR || 'dist'
|
|
14
17
|
|
|
15
18
|
const certsDir = process.env.OPENCLOUD_CERTS_DIR
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
const customHttps = certsDir
|
|
20
|
+
? {
|
|
21
|
+
key: readFileSync(join(certsDir, 'server.key')),
|
|
22
|
+
cert: readFileSync(join(certsDir, 'server.crt'))
|
|
23
|
+
}
|
|
24
|
+
: null
|
|
25
|
+
|
|
26
|
+
// Deep merge objects, replace arrays (matches mergo.WithOverride behavior on the Go side).
|
|
27
|
+
function deepMerge(target, source) {
|
|
28
|
+
const result = { ...target }
|
|
29
|
+
for (const key of Object.keys(source)) {
|
|
30
|
+
const tv = target[key]
|
|
31
|
+
const sv = source[key]
|
|
32
|
+
if (
|
|
33
|
+
sv &&
|
|
34
|
+
typeof sv === 'object' &&
|
|
35
|
+
!Array.isArray(sv) &&
|
|
36
|
+
tv &&
|
|
37
|
+
typeof tv === 'object' &&
|
|
38
|
+
!Array.isArray(tv)
|
|
39
|
+
) {
|
|
40
|
+
result[key] = deepMerge(tv, sv)
|
|
41
|
+
} else {
|
|
42
|
+
result[key] = sv
|
|
43
|
+
}
|
|
20
44
|
}
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
21
47
|
|
|
22
48
|
const manifestFile = 'manifest.json'
|
|
23
49
|
const manifestPath = join('./src/', manifestFile)
|
|
50
|
+
const appConfigPath = join('./src/', 'config.json')
|
|
51
|
+
const remoteEntryName = 'remoteEntry'
|
|
52
|
+
const remoteEntryExt = '.mjs'
|
|
53
|
+
|
|
54
|
+
// Generates manifest.json for OpenCloud app discovery.
|
|
24
55
|
const manifestPlugin = () => {
|
|
25
56
|
let outputDir
|
|
26
57
|
|
|
@@ -43,9 +74,9 @@ const manifestPlugin = () => {
|
|
|
43
74
|
return
|
|
44
75
|
}
|
|
45
76
|
|
|
46
|
-
// Find the entry chunk
|
|
77
|
+
// Find the remote entry chunk (emitted by the federation plugin)
|
|
47
78
|
const entryChunk = Object.values(bundle).find(
|
|
48
|
-
(chunk) => chunk.type === 'chunk' && chunk.
|
|
79
|
+
(chunk) => chunk.type === 'chunk' && chunk.name === remoteEntryName
|
|
49
80
|
)
|
|
50
81
|
|
|
51
82
|
if (!entryChunk) {
|
|
@@ -72,7 +103,7 @@ const manifestPlugin = () => {
|
|
|
72
103
|
// Add manifest.json to the bundle
|
|
73
104
|
this.emitFile({
|
|
74
105
|
type: 'asset',
|
|
75
|
-
fileName:
|
|
106
|
+
fileName: manifestFile,
|
|
76
107
|
source: JSON.stringify(manifest, null, 2)
|
|
77
108
|
})
|
|
78
109
|
}
|
|
@@ -92,8 +123,35 @@ export const defineConfig = (overrides = {}) => {
|
|
|
92
123
|
const name = overrides.name || packageJson.name
|
|
93
124
|
|
|
94
125
|
// set default config
|
|
95
|
-
const {
|
|
96
|
-
const
|
|
126
|
+
const { port = 9210 } = overrides?.server || {}
|
|
127
|
+
const hostUrl =
|
|
128
|
+
overrides?.opencloudWebHostUrl ||
|
|
129
|
+
process.env.OPENCLOUD_WEB_HOST_URL ||
|
|
130
|
+
'https://host.docker.internal:9201'
|
|
131
|
+
|
|
132
|
+
// Read merged metadata: manifest.json defaults + config.json overrides
|
|
133
|
+
function readAppConfig() {
|
|
134
|
+
let config = {}
|
|
135
|
+
if (existsSync(manifestPath)) {
|
|
136
|
+
try {
|
|
137
|
+
const manifest = JSON.parse(readFileSync(manifestPath).toString())
|
|
138
|
+
if (manifest.config) {
|
|
139
|
+
config = manifest.config
|
|
140
|
+
}
|
|
141
|
+
} catch {
|
|
142
|
+
// ignore malformed manifest
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (existsSync(appConfigPath)) {
|
|
146
|
+
try {
|
|
147
|
+
const overrides = JSON.parse(readFileSync(appConfigPath).toString())
|
|
148
|
+
config = deepMerge(config, overrides)
|
|
149
|
+
} catch {
|
|
150
|
+
// ignore malformed config
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return Object.keys(config).length > 0 ? config : undefined
|
|
154
|
+
}
|
|
97
155
|
|
|
98
156
|
return mergeConfig(
|
|
99
157
|
{
|
|
@@ -103,47 +161,63 @@ export const defineConfig = (overrides = {}) => {
|
|
|
103
161
|
minify: isProduction,
|
|
104
162
|
outDir: distDir,
|
|
105
163
|
rollupOptions: {
|
|
106
|
-
// keep in sync with packages/web-runtime/src/container/application/index.ts
|
|
107
|
-
external: [
|
|
108
|
-
'vue',
|
|
109
|
-
'luxon',
|
|
110
|
-
'pinia',
|
|
111
|
-
'vue3-gettext',
|
|
112
|
-
|
|
113
|
-
'@opencloud-eu/web-client',
|
|
114
|
-
'@opencloud-eu/web-client/graph',
|
|
115
|
-
'@opencloud-eu/web-client/graph/generated',
|
|
116
|
-
'@opencloud-eu/web-client/ocs',
|
|
117
|
-
'@opencloud-eu/web-client/sse',
|
|
118
|
-
'@opencloud-eu/web-client/webdav',
|
|
119
|
-
'@opencloud-eu/web-pkg',
|
|
120
|
-
'web-client',
|
|
121
|
-
'web-pkg'
|
|
122
|
-
],
|
|
123
|
-
preserveEntrySignatures: 'strict',
|
|
124
164
|
input: {
|
|
125
165
|
[name]: './src/index.ts'
|
|
126
166
|
},
|
|
127
167
|
output: {
|
|
128
|
-
|
|
129
|
-
chunkFileNames: join('js',
|
|
130
|
-
entryFileNames: join('js', `[name]${isProduction ? '-[hash]' : ''}.js`)
|
|
168
|
+
entryFileNames: join('js', `[name]${isProduction ? '-[hash]' : ''}${remoteEntryExt}`),
|
|
169
|
+
chunkFileNames: join('js', `[name]-[hash]${remoteEntryExt}`)
|
|
131
170
|
}
|
|
132
171
|
}
|
|
133
172
|
},
|
|
134
173
|
plugins: [
|
|
135
174
|
vue({
|
|
136
|
-
// set to true when switching to esm
|
|
137
175
|
customElement: false,
|
|
138
176
|
...(isTesting && { template: { compilerOptions: { whitespace: 'preserve' } } })
|
|
139
177
|
}),
|
|
178
|
+
...(customHttps ? [] : [basicSsl({ name: 'opencloud' })]),
|
|
179
|
+
{
|
|
180
|
+
name: 'fix-sec-fetch-dest',
|
|
181
|
+
configureServer(server) {
|
|
182
|
+
server.middlewares.use((req, res, next) => {
|
|
183
|
+
// Vite skips its transform middleware for requests with sec-fetch-dest: document,
|
|
184
|
+
// which breaks direct browser navigation to JS files like remoteEntry.js.
|
|
185
|
+
if (
|
|
186
|
+
(req.url?.endsWith('.js') || req.url?.endsWith('.mjs')) &&
|
|
187
|
+
req.headers['sec-fetch-dest'] === 'document'
|
|
188
|
+
) {
|
|
189
|
+
req.headers['sec-fetch-dest'] = 'script'
|
|
190
|
+
}
|
|
191
|
+
next()
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
federation({
|
|
196
|
+
name,
|
|
197
|
+
exposes: { '.': './src/index.ts' },
|
|
198
|
+
filename: `${remoteEntryName}${isProduction ? '-[hash]' : ''}${remoteEntryExt}`,
|
|
199
|
+
shared: Object.fromEntries(
|
|
200
|
+
externalModules.map((pkg) => [pkg, { singleton: true, import: false }])
|
|
201
|
+
),
|
|
202
|
+
manifest: false,
|
|
203
|
+
dts: false
|
|
204
|
+
}),
|
|
205
|
+
tailwindcss(),
|
|
140
206
|
manifestPlugin(),
|
|
141
|
-
|
|
207
|
+
federationRegistrationClient({
|
|
208
|
+
hostUrl,
|
|
209
|
+
name,
|
|
210
|
+
entryPoint: `${remoteEntryName}${remoteEntryExt}`,
|
|
211
|
+
getMetadata: readAppConfig,
|
|
212
|
+
metadataWatchFiles: [manifestPath, appConfigPath]
|
|
213
|
+
})
|
|
142
214
|
],
|
|
143
215
|
server: {
|
|
216
|
+
origin: `https://host.docker.internal:${port}`,
|
|
217
|
+
host: 'host.docker.internal',
|
|
144
218
|
port,
|
|
145
|
-
|
|
146
|
-
...(
|
|
219
|
+
cors: true,
|
|
220
|
+
...(customHttps && { https: customHttps })
|
|
147
221
|
},
|
|
148
222
|
test: {
|
|
149
223
|
globals: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opencloud-eu/extension-sdk",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.1.0",
|
|
4
4
|
"description": "OpenCloud Web Extension SDK",
|
|
5
5
|
"license": "AGPL-3.0",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
"directory": "packages/extension-sdk"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@
|
|
18
|
-
"@
|
|
17
|
+
"@module-federation/vite": "^1.13.5",
|
|
18
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
19
|
+
"@vitejs/plugin-basic-ssl": "^2.3.0",
|
|
20
|
+
"@vitejs/plugin-vue": "^6.0.5"
|
|
19
21
|
},
|
|
20
22
|
"peerDependencies": {
|
|
21
|
-
"vite": "^7.2.0"
|
|
23
|
+
"vite": "^7.2.0 || ^8.0.0"
|
|
22
24
|
}
|
|
23
25
|
}
|