@yangyongtao/gaea 1.1.29 → 1.1.31

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.31] - 2026-09-09
4
+
5
+ ### Added
6
+ - Vite 插件 `transformIndexHtml` 自动给外部跨域 `<script src="http(s)://...">` 补 `crossorigin="anonymous"`:解决跨域隔离(COEP: require-corp)下第三方脚本因缺少 CORP 响应头被拦截(`NotSameOriginAfterDefaultedToSameOriginByCoep`)并阻塞页面加载的问题(如 `live.dcyun.com` 的 DcVideo.js 视频库)
7
+ - 注入前置脚本 hook `Node.prototype.appendChild/insertBefore`:第三方脚本内部 `document.createElement('script')` 动态加载的跨域子脚本自动补 `crossorigin`(DcVideo.js 内部的 jquery / DCHls / EasyWasmPlayer / ezuikit 等均可正常加载)
8
+ - 新增 `gaeaVitePlugin({ skipExternalScripts: [...] })` 配置:构建期移除匹配的外部脚本(字符串子串或正则),用于跳过跨域隔离下无法正常工作且当前未使用的第三方脚本
9
+
10
+ ## [1.1.30] - 2026-09-09
11
+
12
+ ### Added
13
+ - Vite 插件生产构建支持:新增 `closeBundle` 钩子,`vite build` 时自动把引擎静态资源(`Gaea.js` / `Gaea.wasm` / `Gaea.pck` / `Gaea.worker.js` / `static/` / `math/` / `image/` / `font/` / `icon/` 等)从包内 `public` 复制到输出目录(默认 `dist`,遵循 `build.outDir`)。修复此前生产包访问 `/Gaea.js` 返回 404 的问题。`configResolved` 记录输出目录;拷贝时跳过输出目录中已存在的文件,保证消费者自身 `public/` 下的资源优先。
14
+
3
15
  ## [1.1.29] - 2026-09-04
4
16
 
5
17
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yangyongtao/gaea",
3
- "version": "1.1.29",
3
+ "version": "1.1.31",
4
4
  "description": "Gaea 3D visualization component library - Vue 3 components based on Godot WASM engine. Includes WMTS layer loading, camera view control, Vite plugin auto-injection, etc.",
5
5
  "type": "module",
6
6
  "main": "src/index.cjs",
@@ -6,6 +6,7 @@
6
6
  * 2. 提供 /Gaea.js /Gaea.wasm /Gaea.pck 等静态文件
7
7
  * 3. 设置 COEP/COOP 头(SharedArrayBuffer 必需)
8
8
  * 4. 检测跨域隔离状态并自动刷新
9
+ * 5. 生产构建时把引擎静态资源复制到输出目录(closeBundle)
9
10
  *
10
11
  * 消费者用法(vite.config.js):
11
12
  * import { gaeaVitePlugin } from '@yangyongtao/gaea/plugin';
@@ -19,6 +20,24 @@ const __filename = fileURLToPath(import.meta.url);
19
20
  const __dirname = path.dirname(__filename);
20
21
  const pkgPublic = path.resolve(__dirname, '..', 'public');
21
22
 
23
+ // 生产构建需复制的引擎静态资源:
24
+ // - 顶层引擎文件(与注入的 <script src="/Gaea.js"> 位置保持一致,位于站点根路径)
25
+ // - 子目录(serveLocal 指向 /static/,引擎还会访问 math/ image/ font/ icon/)
26
+ const BUILD_TOP_FILES = [
27
+ 'Gaea.js',
28
+ 'Gaea.wasm',
29
+ 'Gaea.pck',
30
+ 'Gaea.worker.js',
31
+ 'Gaea.audio.worklet.js',
32
+ 'GaeaApp.html',
33
+ 'GaeaModule.html',
34
+ 'index.js',
35
+ 'constants.js',
36
+ 'jsencrypt.min.js',
37
+ 'web-control.min.js',
38
+ ];
39
+ const BUILD_COPY_DIRS = ['static/', 'math/', 'image/', 'font/', 'icon/'];
40
+
22
41
  const MIME = {
23
42
  '.wasm': 'application/wasm',
24
43
  '.pck': 'application/octet-stream',
@@ -36,11 +55,53 @@ const COEP_HEADERS = {
36
55
  'Cross-Origin-Embedder-Policy': 'require-corp',
37
56
  };
38
57
 
39
- export default function gaeaVitePlugin() {
58
+ /**
59
+ * 给外部跨域 <script src="http(s)://..."> 自动补 crossorigin="anonymous"。
60
+ *
61
+ * 背景:Gaea 引擎要求跨域隔离(COEP: require-corp)。在 COEP 下,普通 no-cors
62
+ * 跨域脚本因缺少 Cross-Origin-Resource-Policy 响应头会被浏览器拦截(
63
+ * NotSameOriginAfterDefaultedToSameOriginByCoep),且同步脚本会挂起阻塞页面加载。
64
+ * 补上 crossorigin 后改为 CORS 模式加载,只要服务器返回 Access-Control-Allow-Origin
65
+ * 即可通过 COEP 检查。
66
+ */
67
+ function addCrossOriginToExternalScripts(html) {
68
+ return html.replace(/<script\b([^>]*?)>/g, (match, attrs) => {
69
+ if (!/\bsrc="https?:\/\//.test(attrs)) return match;
70
+ if (/\bcrossorigin(=|[\s>])/i.test(attrs)) return match;
71
+ return match.replace(/^<script/, '<script crossorigin="anonymous"');
72
+ });
73
+ }
74
+
75
+ /**
76
+ * 移除 HTML 中匹配的外部 <script src="http(s)://..."> 标签。
77
+ * 用于跳过某些在跨域隔离(COEP)下无法正常工作、且当前项目并未使用的第三方脚本
78
+ * (如第三方视频播放库),避免它们拖慢首屏、产生无意义报错。
79
+ * @param {string} html
80
+ * @param {Array<string|RegExp>} rules 匹配规则:字符串按子串匹配 src,正则直接 test
81
+ */
82
+ function skipExternalScriptTags(html, rules) {
83
+ if (!rules?.length) return html;
84
+ return html.replace(/<script\b([^>]*?src="(https?:\/\/[^"]+)"[^>]*)>[\s\S]*?<\/script>/g, (match, attrs, src) => {
85
+ const hit = rules.some((rule) => (rule instanceof RegExp ? rule.test(src) : String(src).includes(rule)));
86
+ return hit ? '' : match;
87
+ });
88
+ }
89
+
90
+ export default function gaeaVitePlugin(options = {}) {
91
+ const skipScripts = options.skipExternalScripts || [];
92
+ let resolvedOutDir = '';
93
+
40
94
  return {
41
95
  name: 'vite-plugin-gaea',
42
96
  enforce: 'pre',
43
97
 
98
+ // 记录生产构建的输出目录(closeBundle 时用于定位拷贝目标)
99
+ configResolved(config) {
100
+ resolvedOutDir = config.build?.outDir
101
+ ? path.resolve(config.root || process.cwd(), config.build.outDir)
102
+ : '';
103
+ },
104
+
44
105
  configureServer(server) {
45
106
  // COEP/COOP 头只对非 WebSocket 请求设置,避免破坏 Vite HMR
46
107
  server.middlewares.stack.unshift({
@@ -97,7 +158,53 @@ export default function gaeaVitePlugin() {
97
158
  });
98
159
  },
99
160
 
161
+ // 生产构建:把引擎静态资源复制到输出目录。
162
+ // configureServer 只在 dev 模式生效,vite build 不会运行,因此这里需自行拷贝。
163
+ closeBundle() {
164
+ const outRoot = resolvedOutDir || path.resolve(process.cwd(), 'dist');
165
+ if (!fs.existsSync(pkgPublic)) {
166
+ console.warn('[gaea] 未找到 gaea public 目录,跳过引擎资源复制:', pkgPublic);
167
+ return;
168
+ }
169
+
170
+ // 不覆盖输出目录中已存在的文件(消费者 public/ 下的资源优先)
171
+ const copyFile = (src, dest) => {
172
+ if (fs.existsSync(dest)) return;
173
+ fs.copyFileSync(src, dest);
174
+ };
175
+ const copyDir = (src, dest) => {
176
+ if (!fs.existsSync(src)) return;
177
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
178
+ const s = path.join(src, entry.name);
179
+ const d = path.join(dest, entry.name);
180
+ if (entry.isDirectory()) {
181
+ copyDir(s, d);
182
+ } else if (!fs.existsSync(d)) {
183
+ fs.mkdirSync(path.dirname(d), { recursive: true });
184
+ fs.copyFileSync(s, d);
185
+ }
186
+ }
187
+ };
188
+
189
+ fs.mkdirSync(outRoot, { recursive: true });
190
+ for (const name of BUILD_TOP_FILES) {
191
+ const src = path.join(pkgPublic, name);
192
+ if (fs.existsSync(src)) copyFile(src, path.join(outRoot, name));
193
+ }
194
+ for (const dir of BUILD_COPY_DIRS) {
195
+ const src = path.join(pkgPublic, dir);
196
+ if (fs.existsSync(src)) copyDir(src, path.join(outRoot, dir));
197
+ }
198
+ console.log('[gaea] 引擎资源已复制到输出目录:', outRoot);
199
+ },
200
+
100
201
  transformIndexHtml(html) {
202
+ // 跳过配置中匹配的外部脚本(如项目未使用的第三方视频库)
203
+ html = skipExternalScriptTags(html, skipScripts);
204
+
205
+ // 外部跨域脚本自动补 crossorigin(绕过 COEP 拦截,避免阻塞页面)
206
+ html = addCrossOriginToExternalScripts(html);
207
+
101
208
  // 自动注入 Gaea.js(消费者无需在 HTML 中手动写 <script src="/Gaea.js">)
102
209
  if (!html.includes('/Gaea.js')) {
103
210
  html = html.replace(
@@ -106,10 +213,31 @@ export default function gaeaVitePlugin() {
106
213
  );
107
214
  }
108
215
 
109
- // 跨域隔离检测
216
+ // 跨域隔离检测 + 动态跨域脚本自动补 crossorigin(必须在 body 脚本执行前注入到 head)
110
217
  html = html.replace(
111
218
  '</head>',
112
219
  `<script>
220
+ // 动态创建的跨域 <script> 自动补 crossorigin="anonymous":
221
+ // 跨域隔离(COEP)下,第三方脚本内部 document.createElement('script') 动态加载的子脚本
222
+ // 若不带 crossorigin 会被拦截(NotSameOriginAfterDefaultedToSameOriginByCoep)并阻塞页面。
223
+ (function () {
224
+ if (!window.crossOriginIsolated) return;
225
+ function fix(el) {
226
+ if (el && el.tagName === 'SCRIPT' && el.src && !el.crossOrigin) {
227
+ try {
228
+ if (new URL(el.src, location.href).origin !== location.origin) {
229
+ el.crossOrigin = 'anonymous';
230
+ }
231
+ } catch (e) {}
232
+ }
233
+ }
234
+ var oa = Node.prototype.appendChild;
235
+ var oi = Node.prototype.insertBefore;
236
+ Node.prototype.appendChild = function (el) { fix(el); return oa.call(this, el); };
237
+ Node.prototype.insertBefore = function (el, ref) { fix(el); return oi.call(this, el, ref); };
238
+ })();
239
+ </script>
240
+ <script>
113
241
  if (!window.crossOriginIsolated) {
114
242
  if (!location.search.includes('gaea_isolated=1')) {
115
243
  location.replace(location.href.split('?')[0] + '?gaea_isolated=1');