@opencloud-eu/extension-sdk 6.1.0 → 7.0.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.
@@ -0,0 +1,229 @@
1
+ import { mergeConfig as e, searchForWorkspaceRoot as t } from "vite";
2
+ import { join as n, posix as r } from "path";
3
+ import { cwd as i } from "process";
4
+ import { existsSync as a, readFileSync as o, watch as s } from "fs";
5
+ import c from "@tailwindcss/vite";
6
+ import l from "@vitejs/plugin-basic-ssl";
7
+ import u from "@vitejs/plugin-vue";
8
+ import { federation as d } from "@module-federation/vite";
9
+ import f from "https";
10
+ import p from "http";
11
+ //#region src/externalModules.ts
12
+ var m = [
13
+ "vue",
14
+ "luxon",
15
+ "pinia",
16
+ "vue3-gettext",
17
+ "@opencloud-eu/web-pkg",
18
+ "@opencloud-eu/web-pkg/editor",
19
+ "@opencloud-eu/web-client",
20
+ "@opencloud-eu/web-client/graph",
21
+ "@opencloud-eu/web-client/graph/generated",
22
+ "@opencloud-eu/web-client/ocs",
23
+ "@opencloud-eu/web-client/sse",
24
+ "@opencloud-eu/web-client/webdav"
25
+ ];
26
+ //#endregion
27
+ //#region src/plugins/federationRegistrationClient.ts
28
+ function h(e, { method: t = "GET", body: n } = {}) {
29
+ return new Promise((r, i) => {
30
+ let a = (new URL(e).protocol === "https:" ? f : p).request(e, {
31
+ method: t,
32
+ rejectUnauthorized: !1
33
+ }, (e) => {
34
+ let t = "";
35
+ e.on("data", (e) => t += e), e.on("end", () => r({
36
+ ok: e.statusCode >= 200 && e.statusCode < 300,
37
+ status: e.statusCode,
38
+ data: t
39
+ }));
40
+ });
41
+ a.on("error", i), n && (a.setHeader("Content-Type", "application/json"), a.write(n)), a.end();
42
+ });
43
+ }
44
+ function g({ hostUrl: e, name: t, entryPoint: n, getMetadata: r, interval: i = 5e3, path: a = "/_dev/remotes", metadataWatchFiles: o = [] }) {
45
+ return {
46
+ name: "federation-registration-client",
47
+ apply: "serve",
48
+ configureServer(c) {
49
+ if (c.config.mode === "test") return;
50
+ let l = null, u = "", d = [];
51
+ async function f() {
52
+ if (!u) return;
53
+ let n = r?.(), i = `${e}${a}`;
54
+ try {
55
+ let e = await h(i, {
56
+ method: "POST",
57
+ body: JSON.stringify({
58
+ id: t,
59
+ path: u,
60
+ ...n && { metadata: n }
61
+ })
62
+ });
63
+ e.ok || console.warn(`[federation-registration] Registration failed: ${e.status}`);
64
+ } catch {}
65
+ }
66
+ c.httpServer?.on("listening", () => {
67
+ let e = c.httpServer.address(), t = typeof e == "object" ? e?.port : e;
68
+ u = `https://${typeof c.config.server.host == "string" ? c.config.server.host : "localhost"}:${t}/${n}`, f(), l = setInterval(f, i);
69
+ for (let e of o) try {
70
+ let t = s(e, { persistent: !1 }, () => {
71
+ console.log(`[federation-registration] ${e} changed, re-registering...`), f();
72
+ });
73
+ d.push(t);
74
+ } catch {}
75
+ }), c.httpServer?.on("close", () => {
76
+ l &&= (clearInterval(l), null);
77
+ for (let e of d) e.close();
78
+ });
79
+ }
80
+ };
81
+ }
82
+ //#endregion
83
+ //#region src/plugins/manifest.ts
84
+ var _ = "manifest.json", v = n("./src/", _);
85
+ function y(e) {
86
+ let t = "";
87
+ return {
88
+ name: "manifest",
89
+ apply: "build",
90
+ configResolved(e) {
91
+ t = e.build.outDir;
92
+ },
93
+ buildStart() {
94
+ this.addWatchFile(v);
95
+ },
96
+ generateBundle(r, i) {
97
+ let s = n(t, _);
98
+ if (a(s)) {
99
+ this.warn(`${s} already exists in output directory (likely from public/), skipping generation\nConsider using --emptyOutDir if outDir is outside of project root.`);
100
+ return;
101
+ }
102
+ let c = Object.values(i).find((t) => t.type === "chunk" && t.name === e);
103
+ c || this.error("No entry chunk found");
104
+ let l = {};
105
+ if (a(v)) try {
106
+ l = JSON.parse(o(v).toString());
107
+ } catch (e) {
108
+ this.error(`Failed to parse manifest.json at ${v}: ${e.message}\nPlease ensure manifest.json contains a valid JSON object.`);
109
+ }
110
+ l.entrypoint = c.fileName, this.emitFile({
111
+ type: "asset",
112
+ fileName: _,
113
+ source: JSON.stringify(l, null, 2)
114
+ });
115
+ }
116
+ };
117
+ }
118
+ //#endregion
119
+ //#region src/utils.ts
120
+ function b(e, t) {
121
+ let n = { ...e };
122
+ for (let r of Object.keys(t)) {
123
+ let i = e[r], a = t[r];
124
+ a && typeof a == "object" && !Array.isArray(a) && i && typeof i == "object" && !Array.isArray(i) ? n[r] = b(i, a) : n[r] = a;
125
+ }
126
+ return n;
127
+ }
128
+ //#endregion
129
+ //#region src/index.ts
130
+ var x = process.env.OPENCLOUD_EXTENSION_DIST_DIR || "dist", S = process.env.OPENCLOUD_CERTS_DIR, C = S ? {
131
+ key: o(n(S, "server.key")),
132
+ cert: o(n(S, "server.crt"))
133
+ } : null, w = n("./src/", "config.json"), T = "remoteEntry", E = ".mjs";
134
+ function D(s = {}) {
135
+ return ({ mode: f }) => {
136
+ let p = f === "production", h = f === "test", _ = JSON.parse(o(n(t(i()), "package.json")).toString()), S = s.name || _.name, { port: D = 9210 } = s?.server || {}, O = s?.opencloudWebHostUrl || process.env.OPENCLOUD_WEB_HOST_URL || "https://host.docker.internal:9201";
137
+ function k() {
138
+ let e = {};
139
+ if (a(v)) try {
140
+ let t = JSON.parse(o(v).toString());
141
+ t.config && (e = t.config);
142
+ } catch {}
143
+ if (a(w)) try {
144
+ let t = JSON.parse(o(w).toString());
145
+ e = b(e, t);
146
+ } catch {}
147
+ return Object.keys(e).length > 0 ? e : void 0;
148
+ }
149
+ return e({
150
+ base: "./",
151
+ build: {
152
+ cssCodeSplit: !0,
153
+ minify: p,
154
+ outDir: x,
155
+ rollupOptions: {
156
+ input: { [S]: "./src/index.ts" },
157
+ output: {
158
+ entryFileNames: r.join("js", `[name]${p ? "-[hash]" : ""}${E}`),
159
+ chunkFileNames: r.join("js", `[name]-[hash]${E}`)
160
+ }
161
+ }
162
+ },
163
+ plugins: [
164
+ u({
165
+ customElement: !1,
166
+ ...h && { template: { compilerOptions: { whitespace: "preserve" } } }
167
+ }),
168
+ ...C ? [] : [l({ name: "opencloud" })],
169
+ {
170
+ name: "fix-sec-fetch-dest",
171
+ configureServer(e) {
172
+ e.middlewares.use((e, t, n) => {
173
+ (e.url?.endsWith(".js") || e.url?.endsWith(".mjs")) && e.headers["sec-fetch-dest"] === "document" && (e.headers["sec-fetch-dest"] = "script"), n();
174
+ });
175
+ }
176
+ },
177
+ ...h ? [] : [d({
178
+ name: S,
179
+ exposes: { ".": "./src/index.ts" },
180
+ filename: `${T}${p ? "-[hash]" : ""}${E}`,
181
+ shared: Object.fromEntries(m.map((e) => [e, {
182
+ singleton: !0,
183
+ import: !1
184
+ }])),
185
+ manifest: !1,
186
+ dts: !1
187
+ })],
188
+ c(),
189
+ y(T),
190
+ g({
191
+ hostUrl: O,
192
+ name: S,
193
+ entryPoint: `${T}${E}`,
194
+ getMetadata: k,
195
+ metadataWatchFiles: [v, w]
196
+ })
197
+ ],
198
+ server: {
199
+ origin: `https://host.docker.internal:${D}`,
200
+ host: "host.docker.internal",
201
+ port: D,
202
+ cors: !0,
203
+ ...C && { https: C }
204
+ },
205
+ test: {
206
+ globals: !0,
207
+ environment: "happy-dom",
208
+ clearMocks: !0,
209
+ include: ["**/*.spec.ts"],
210
+ exclude: [
211
+ "**/node_modules/**",
212
+ "**/dist/**",
213
+ "**/cypress/**",
214
+ "**/.{idea,git,cache,output,temp}/**",
215
+ "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*",
216
+ ".pnpm-store/*",
217
+ "**/e2e/**"
218
+ ],
219
+ coverage: {
220
+ provider: "v8",
221
+ reportsDirectory: "./coverage",
222
+ reporter: "lcov"
223
+ }
224
+ }
225
+ }, s);
226
+ };
227
+ }
228
+ //#endregion
229
+ export { D as defineConfig };
@@ -0,0 +1 @@
1
+ export declare const externalModules: string[];
@@ -0,0 +1,10 @@
1
+ import { ViteUserConfig } from 'vitest/config';
2
+ export interface ExtensionConfigOverrides extends ViteUserConfig {
3
+ /** Name of the extension, defaults to the package name */
4
+ name?: string;
5
+ /** URL of the OpenCloud host web dev server, defaults to 'https://host.docker.internal:9201' */
6
+ opencloudWebHostUrl?: string;
7
+ }
8
+ export declare function defineConfig(overrides?: ExtensionConfigOverrides): ({ mode }: {
9
+ mode: string;
10
+ }) => Record<string, any>;
@@ -0,0 +1,24 @@
1
+ import type { Plugin } from 'vite';
2
+ interface FederationRegistrationClientOptions {
3
+ /** Host dev server URL (e.g. 'https://host.docker.internal:9201') */
4
+ hostUrl: string;
5
+ /** Remote ID used for registration */
6
+ name: string;
7
+ /** Remote entry filename (e.g. 'remoteEntry.mjs'), combined with the server address to form the full URL */
8
+ entryPoint: string;
9
+ /** Returns optional metadata to send with registration */
10
+ getMetadata?: () => Record<string, unknown> | undefined;
11
+ /** Re-registration interval in ms (default: 5000) */
12
+ interval?: number;
13
+ /** Host endpoint path (default: '/_dev/remotes') */
14
+ path?: string;
15
+ /** File paths to watch for immediate re-registration on metadata change */
16
+ metadataWatchFiles?: string[];
17
+ }
18
+ /**
19
+ * Vite plugin: remote-side periodic registration with a host dev server.
20
+ * Sends registration data at a fixed interval so the host always knows
21
+ * about this remote, even after the host restarts.
22
+ */
23
+ export declare function federationRegistrationClient({ hostUrl, name, entryPoint, getMetadata, interval, path, metadataWatchFiles }: FederationRegistrationClientOptions): Plugin;
24
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from './federationRegistrationClient';
2
+ export * from './manifest';
@@ -0,0 +1,6 @@
1
+ import type { Plugin } from 'vite';
2
+ export declare const manifestPath: string;
3
+ /**
4
+ * Generates manifest.json for OpenCloud app discovery.
5
+ */
6
+ export declare function manifestPlugin(remoteEntryName: string): Plugin;
@@ -0,0 +1 @@
1
+ export declare function deepMerge(target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,62 @@
1
+ @layer theme, base, components, utilities;
2
+ @import 'tailwindcss/theme.css' layer(theme) prefix(ext);
3
+ @import 'tailwindcss/utilities.css' layer(utilities) prefix(ext);
4
+
5
+ @theme {
6
+ --font-sans: var(--oc-font-family);
7
+
8
+ --spacing: 4px;
9
+ --breakpoint-xs: 580px;
10
+ --breakpoint-sm: 640px;
11
+ --breakpoint-md: 960px;
12
+ --breakpoint-lg: 1200px;
13
+ --breakpoint-xl: 1600px;
14
+
15
+ --z-index-modal: 9999;
16
+
17
+ --color-role-background: var(--oc-role-background);
18
+ --color-role-chrome: var(--oc-role-chrome);
19
+ --color-role-error: var(--oc-role-error);
20
+ --color-role-error-container: var(--oc-role-error-container);
21
+ --color-role-inverse-on-surface: var(--oc-role-inverse-on-surface);
22
+ --color-role-inverse-primary: var(--oc-role-inverse-primary);
23
+ --color-role-inverse-surface: var(--oc-role-inverse-surface);
24
+ --color-role-on-background: var(--oc-role-on-background);
25
+ --color-role-on-chrome: var(--oc-role-on-chrome);
26
+ --color-role-on-error: var(--oc-role-on-error);
27
+ --color-role-on-error-container: var(--oc-role-on-error-container);
28
+ --color-role-on-primary: var(--oc-role-on-primary);
29
+ --color-role-on-primary-container: var(--oc-role-on-primary-container);
30
+ --color-role-on-primary-fixed: var(--oc-role-on-primary-fixed);
31
+ --color-role-on-secondary: var(--oc-role-on-secondary);
32
+ --color-role-on-secondary-container: var(--oc-role-on-secondary-container);
33
+ --color-role-on-secondary-fixed: var(--oc-role-on-secondary-fixed);
34
+ --color-role-on-surface: var(--oc-role-on-surface);
35
+ --color-role-on-surface-variant: var(--oc-role-on-surface-variant);
36
+ --color-role-on-tertiary: var(--oc-role-on-tertiary);
37
+ --color-role-on-tertiary-container: var(--oc-role-on-tertiary-container);
38
+ --color-role-on-tertiary-fixed: var(--oc-role-on-tertiary-fixed);
39
+ --color-role-outline: var(--oc-role-outline);
40
+ --color-role-outline-variant: var(--oc-role-outline-variant);
41
+ --color-role-primary: var(--oc-role-primary);
42
+ --color-role-primary-container: var(--oc-role-primary-container);
43
+ --color-role-primary-fixed: var(--oc-role-primary-fixed);
44
+ --color-role-scrim: var(--oc-role-scrim);
45
+ --color-role-secondary: var(--oc-role-secondary);
46
+ --color-role-secondary-container: var(--oc-role-secondary-container);
47
+ --color-role-secondary-fixed: var(--oc-role-secondary-fixed);
48
+ --color-role-shadow: var(--oc-role-shadow);
49
+ --color-role-surface: var(--oc-role-surface);
50
+ --color-role-surface-bright: var(--oc-role-surface-bright);
51
+ --color-role-surface-container: var(--oc-role-surface-container);
52
+ --color-role-surface-container-high: var(--oc-role-surface-container-high);
53
+ --color-role-surface-container-highest: var(--oc-role-surface-container-highest);
54
+ --color-role-surface-container-low: var(--oc-role-surface-container-low);
55
+ --color-role-surface-container-lowest: var(--oc-role-surface-container-lowest);
56
+ --color-role-surface-dim: var(--oc-role-surface-dim);
57
+ --color-role-surface-tint: var(--oc-role-surface-tint);
58
+ --color-role-surface-variant: var(--oc-role-surface-variant);
59
+ --color-role-tertiary: var(--oc-role-tertiary);
60
+ --color-role-tertiary-container: var(--oc-role-tertiary-container);
61
+ --color-role-tertiary-fixed: var(--oc-role-tertiary-fixed);
62
+ }
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@opencloud-eu/extension-sdk",
3
- "version": "6.1.0",
3
+ "version": "7.0.0",
4
4
  "description": "OpenCloud Web Extension SDK",
5
5
  "license": "AGPL-3.0",
6
- "main": "index.mjs",
6
+ "main": "dist/extension-sdk.js",
7
+ "types": "dist/src/index.d.ts",
7
8
  "type": "module",
8
9
  "private": false,
9
10
  "author": "OpenCloud GmbH <info@opencloud.eu>",
@@ -13,13 +14,31 @@
13
14
  "url": "https://github.com/opencloud-eu/web",
14
15
  "directory": "packages/extension-sdk"
15
16
  },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "exports": {
21
+ ".": {
22
+ "default": "./dist/extension-sdk.js",
23
+ "types": "./dist/src/index.d.ts"
24
+ },
25
+ "./tailwind.css": "./dist/tailwind.css"
26
+ },
16
27
  "dependencies": {
17
- "@module-federation/vite": "^1.13.5",
28
+ "@module-federation/vite": "1.15.4",
18
29
  "@tailwindcss/vite": "^4.2.2",
19
30
  "@vitejs/plugin-basic-ssl": "^2.3.0",
20
31
  "@vitejs/plugin-vue": "^6.0.5"
21
32
  },
33
+ "devDependencies": {
34
+ "vite-plugin-static-copy": "^4.0.0"
35
+ },
22
36
  "peerDependencies": {
23
- "vite": "^7.2.0 || ^8.0.0"
37
+ "vite": "^7.2.0 || ^8.0.0",
38
+ "vitest": "^4.0.0"
39
+ },
40
+ "scripts": {
41
+ "build": "vite build && vue-tsc --project tsconfig.build.json",
42
+ "check:types": "vue-tsc --declaration --noEmit --project tsconfig.build.json"
24
43
  }
25
44
  }
@@ -1,15 +0,0 @@
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
- ]
@@ -1,122 +0,0 @@
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 DELETED
@@ -1,246 +0,0 @@
1
- // ATTENTION: this is a .mjs (instead of a .ts) file on purpose,
2
- // because we don't want to transpile it before publishing
3
- // c.f. https://github.com/vitejs/vite/issues/5370
4
-
5
- import { mergeConfig, searchForWorkspaceRoot } from 'vite'
6
- import { join } from 'path'
7
- import { cwd } from 'process'
8
- import { readFileSync, existsSync } from 'fs'
9
- import tailwindcss from '@tailwindcss/vite'
10
- import basicSsl from '@vitejs/plugin-basic-ssl'
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'
15
-
16
- const distDir = process.env.OPENCLOUD_EXTENSION_DIST_DIR || 'dist'
17
-
18
- const certsDir = process.env.OPENCLOUD_CERTS_DIR
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
- }
44
- }
45
- return result
46
- }
47
-
48
- const manifestFile = 'manifest.json'
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.
55
- const manifestPlugin = () => {
56
- let outputDir
57
-
58
- return {
59
- name: 'manifest',
60
- apply: 'build',
61
- configResolved(config) {
62
- outputDir = config.build.outDir
63
- },
64
- buildStart() {
65
- this.addWatchFile(manifestPath)
66
- },
67
- generateBundle(options, bundle) {
68
- const generatedManifestPath = join(outputDir, manifestFile)
69
- if (existsSync(generatedManifestPath)) {
70
- this.warn(
71
- `${generatedManifestPath} already exists in output directory (likely from public/), skipping generation\n` +
72
- `Consider using --emptyOutDir if outDir is outside of project root.`
73
- )
74
- return
75
- }
76
-
77
- // Find the remote entry chunk (emitted by the federation plugin)
78
- const entryChunk = Object.values(bundle).find(
79
- (chunk) => chunk.type === 'chunk' && chunk.name === remoteEntryName
80
- )
81
-
82
- if (!entryChunk) {
83
- this.error('No entry chunk found')
84
- return
85
- }
86
-
87
- let manifest = {}
88
- if (existsSync(manifestPath)) {
89
- try {
90
- manifest = JSON.parse(readFileSync(manifestPath).toString())
91
- } catch (err) {
92
- this.error(
93
- `Failed to parse manifest.json at ${manifestPath}: ${err.message}\n` +
94
- `Please ensure manifest.json contains a valid JSON object.`
95
- )
96
- return
97
- }
98
- }
99
-
100
- // set entryPoint
101
- manifest.entrypoint = entryChunk.fileName
102
-
103
- // Add manifest.json to the bundle
104
- this.emitFile({
105
- type: 'asset',
106
- fileName: manifestFile,
107
- source: JSON.stringify(manifest, null, 2)
108
- })
109
- }
110
- }
111
- }
112
-
113
- export const defineConfig = (overrides = {}) => {
114
- return ({ mode }) => {
115
- const isProduction = mode === 'production'
116
- const isTesting = mode === 'test'
117
-
118
- // read package name from vite workspace
119
- const packageJson = JSON.parse(
120
- readFileSync(join(searchForWorkspaceRoot(cwd()), 'package.json')).toString()
121
- )
122
-
123
- const name = overrides.name || packageJson.name
124
-
125
- // set default config
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
- }
155
-
156
- return mergeConfig(
157
- {
158
- base: './', // make asset paths relative so imports work from wherever the extension is loaded
159
- build: {
160
- cssCodeSplit: true,
161
- minify: isProduction,
162
- outDir: distDir,
163
- rollupOptions: {
164
- input: {
165
- [name]: './src/index.ts'
166
- },
167
- output: {
168
- entryFileNames: join('js', `[name]${isProduction ? '-[hash]' : ''}${remoteEntryExt}`),
169
- chunkFileNames: join('js', `[name]-[hash]${remoteEntryExt}`)
170
- }
171
- }
172
- },
173
- plugins: [
174
- vue({
175
- customElement: false,
176
- ...(isTesting && { template: { compilerOptions: { whitespace: 'preserve' } } })
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(),
206
- manifestPlugin(),
207
- federationRegistrationClient({
208
- hostUrl,
209
- name,
210
- entryPoint: `${remoteEntryName}${remoteEntryExt}`,
211
- getMetadata: readAppConfig,
212
- metadataWatchFiles: [manifestPath, appConfigPath]
213
- })
214
- ],
215
- server: {
216
- origin: `https://host.docker.internal:${port}`,
217
- host: 'host.docker.internal',
218
- port,
219
- cors: true,
220
- ...(customHttps && { https: customHttps })
221
- },
222
- test: {
223
- globals: true,
224
- environment: 'happy-dom',
225
- clearMocks: true,
226
- include: ['**/*.spec.ts'],
227
- exclude: [
228
- '**/node_modules/**',
229
- '**/dist/**',
230
- '**/cypress/**',
231
- '**/.{idea,git,cache,output,temp}/**',
232
- '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
233
- '.pnpm-store/*',
234
- '**/e2e/**'
235
- ],
236
- coverage: {
237
- provider: 'v8',
238
- reportsDirectory: './coverage',
239
- reporter: 'lcov'
240
- }
241
- }
242
- },
243
- overrides
244
- )
245
- }
246
- }
package/tailwind.css DELETED
@@ -1,14 +0,0 @@
1
- @layer theme, base, components, utilities;
2
- @import 'tailwindcss/theme.css' layer(theme) prefix(ext);
3
- @import 'tailwindcss/utilities.css' layer(utilities) prefix(ext);
4
-
5
- @theme {
6
- --font-sans: var(--oc-font-family);
7
-
8
- --spacing: 4px;
9
- --breakpoint-xs: 580px;
10
- --breakpoint-sm: 640px;
11
- --breakpoint-md: 960px;
12
- --breakpoint-lg: 1200px;
13
- --breakpoint-xl: 1600px;
14
- }