@fps-games/vfx 0.3.2 → 0.3.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.
Files changed (234) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +5 -5
  3. package/dist/effects/treasure-attention/index.d.ts +1 -1
  4. package/package.json +6 -5
  5. package/tooling/vfx-cli.mjs +31 -22
  6. package/tooling/vfx-package-resolution.mjs +8 -8
  7. package/docs/AUTHORING_EFFECTS.md +0 -297
  8. package/docs/INTEGRATION.md +0 -194
  9. package/docs/VFX_LIBRARY_ARCHITECTURE_chatgpt.md +0 -1580
  10. package/docs/VFX_LIBRARY_EXECUTION_PLAN.md +0 -229
  11. package/docs/VFX_LIBRARY_PLAN.md +0 -546
  12. package/docs/hit-flash-red-method.md +0 -155
  13. package/docs/treasure_attention_fx_babylonjs_guide.md +0 -901
  14. package/docs/vfx-ab/RESULTS.md +0 -35
  15. package/docs/vfx-ab/lumber-order/vehicle-exhaust-smoke/after.png +0 -0
  16. package/docs/vfx-ab/lumber-order/vehicle-exhaust-smoke/before.png +0 -0
  17. package/docs/vfx-ab/lumber-truck/vehicle-exhaust-smoke/after.png +0 -0
  18. package/docs/vfx-ab/lumber-truck/vehicle-exhaust-smoke/before.png +0 -0
  19. package/docs/vfx-integration_chatgpt.md +0 -462
  20. package/docs/vfx-placement-design.md +0 -164
  21. package/docs/weapon-trail-visualization.html +0 -241
  22. package/packages/EffectPackageService.ts +0 -346
  23. package/packages/SceneVfxService.ts +0 -96
  24. package/packages/build.mesh.yellow_build_upgrade_indicator/assets/runtime/simpleMeshEffect.v1.js +0 -333
  25. package/packages/build.mesh.yellow_build_upgrade_indicator/assets/shaders/mesh-effect-preview.frag.glsl +0 -25
  26. package/packages/build.mesh.yellow_build_upgrade_indicator/assets/shaders/mesh-effect-preview.vert.glsl +0 -11
  27. package/packages/build.mesh.yellow_build_upgrade_indicator/assets/textures/effect_jianbian.png +0 -0
  28. package/packages/build.mesh.yellow_build_upgrade_indicator/effect.json +0 -927
  29. package/packages/build.mesh.yellow_build_upgrade_indicator/media/contact-sheet.png +0 -0
  30. package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.gif +0 -0
  31. package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.mp4 +0 -0
  32. package/packages/build.mesh.yellow_build_upgrade_indicator/media/preview.webm +0 -0
  33. package/packages/build.mesh.yellow_build_upgrade_indicator/media/thumbnail.png +0 -0
  34. package/packages/build.mesh.yellow_build_upgrade_indicator/metadata/description.json +0 -197
  35. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/03-effect.original.json +0 -895
  36. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/03-normalized-vfx-to-babylon-report.json +0 -241
  37. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/04-preview-overrides.json +0 -542
  38. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/04-preview-tuning-report.json +0 -69
  39. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/asset-map.json +0 -125
  40. package/packages/build.mesh.yellow_build_upgrade_indicator/provenance/lineage.json +0 -44
  41. package/packages/build.particle.golden_upgrade_starburst/assets/runtime/particleEmitter.v1.js +0 -210
  42. package/packages/build.particle.golden_upgrade_starburst/assets/textures/effect_guangdian.png +0 -0
  43. package/packages/build.particle.golden_upgrade_starburst/effect.json +0 -880
  44. package/packages/build.particle.golden_upgrade_starburst/media/contact-sheet.png +0 -0
  45. package/packages/build.particle.golden_upgrade_starburst/media/preview.gif +0 -0
  46. package/packages/build.particle.golden_upgrade_starburst/media/preview.mp4 +0 -0
  47. package/packages/build.particle.golden_upgrade_starburst/media/preview.webm +0 -0
  48. package/packages/build.particle.golden_upgrade_starburst/media/thumbnail.png +0 -0
  49. package/packages/build.particle.golden_upgrade_starburst/metadata/description.json +0 -161
  50. package/packages/build.particle.golden_upgrade_starburst/provenance/03-effect.original.json +0 -762
  51. package/packages/build.particle.golden_upgrade_starburst/provenance/03-normalized-vfx-to-babylon-report.json +0 -260
  52. package/packages/build.particle.golden_upgrade_starburst/provenance/04-preview-overrides.json +0 -436
  53. package/packages/build.particle.golden_upgrade_starburst/provenance/04-preview-tuning-report.json +0 -40
  54. package/packages/build.particle.golden_upgrade_starburst/provenance/asset-map.json +0 -115
  55. package/packages/build.particle.golden_upgrade_starburst/provenance/lineage.json +0 -40
  56. package/packages/build.particle.orange_upgrade_transform_sparks/assets/runtime/particleEmitter.v1.js +0 -210
  57. package/packages/build.particle.orange_upgrade_transform_sparks/assets/textures/b5b27ab1-e740-4398-b407-848fc2b2c897.png +0 -0
  58. package/packages/build.particle.orange_upgrade_transform_sparks/effect.json +0 -887
  59. package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.gif +0 -0
  60. package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.mp4 +0 -0
  61. package/packages/build.particle.orange_upgrade_transform_sparks/media/preview.webm +0 -0
  62. package/packages/build.particle.orange_upgrade_transform_sparks/media/thumbnail.png +0 -0
  63. package/packages/build.particle.orange_upgrade_transform_sparks/metadata/description.json +0 -179
  64. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/03-effect.original.json +0 -859
  65. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/03-normalized-vfx-to-babylon-report.json +0 -260
  66. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/04-preview-overrides.json +0 -478
  67. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/04-preview-tuning-report.json +0 -40
  68. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/asset-map.json +0 -114
  69. package/packages/build.particle.orange_upgrade_transform_sparks/provenance/lineage.json +0 -41
  70. package/packages/build.particle.wood_chip_dust_burst/assets/runtime/particleEmitter.v1.js +0 -210
  71. package/packages/build.particle.wood_chip_dust_burst/assets/textures/effect_yanwu4.png +0 -0
  72. package/packages/build.particle.wood_chip_dust_burst/assets/textures/suipian3.png +0 -0
  73. package/packages/build.particle.wood_chip_dust_burst/effect.json +0 -1400
  74. package/packages/build.particle.wood_chip_dust_burst/media/contact-sheet.png +0 -0
  75. package/packages/build.particle.wood_chip_dust_burst/media/preview.gif +0 -0
  76. package/packages/build.particle.wood_chip_dust_burst/media/preview.mp4 +0 -0
  77. package/packages/build.particle.wood_chip_dust_burst/media/preview.webm +0 -0
  78. package/packages/build.particle.wood_chip_dust_burst/media/thumbnail.png +0 -0
  79. package/packages/build.particle.wood_chip_dust_burst/metadata/description.json +0 -173
  80. package/packages/build.particle.wood_chip_dust_burst/provenance/03-effect.original.json +0 -1277
  81. package/packages/build.particle.wood_chip_dust_burst/provenance/03-normalized-vfx-to-babylon-report.json +0 -329
  82. package/packages/build.particle.wood_chip_dust_burst/provenance/04-preview-overrides.json +0 -749
  83. package/packages/build.particle.wood_chip_dust_burst/provenance/04-preview-tuning-report.json +0 -60
  84. package/packages/build.particle.wood_chip_dust_burst/provenance/asset-map.json +0 -146
  85. package/packages/build.particle.wood_chip_dust_burst/provenance/lineage.json +0 -69
  86. package/packages/effects/building-upgrade/effect.json +0 -6
  87. package/packages/effects/building-upgrade/index.ts +0 -387
  88. package/packages/effects/character-entrance/effect.json +0 -6
  89. package/packages/effects/character-entrance/index.ts +0 -308
  90. package/packages/effects/coin-burst/index.ts +0 -48
  91. package/packages/effects/comet-trail-flame/assets/textures/trail_5x4.webp +0 -0
  92. package/packages/effects/comet-trail-flame/index.ts +0 -107
  93. package/packages/effects/conveyor-upgrade/effect.json +0 -6
  94. package/packages/effects/conveyor-upgrade/index.ts +0 -401
  95. package/packages/effects/debris-smoke/assets/models/rock_debris.glb +0 -0
  96. package/packages/effects/debris-smoke/assets/textures/smoke22.webp +0 -0
  97. package/packages/effects/debris-smoke/index.ts +0 -102
  98. package/packages/effects/door_open_gold_flakes/assets/runtime/goldFlakes.v1.js +0 -397
  99. package/packages/effects/door_open_gold_flakes/door_open_gold_flakes.json +0 -498
  100. package/packages/effects/door_open_gold_flakes/index.ts +0 -11
  101. package/packages/effects/electric-arc/assets/textures/spark_4x4.webp +0 -0
  102. package/packages/effects/electric-arc/index.ts +0 -63
  103. package/packages/effects/energy-shield/effect.json +0 -7
  104. package/packages/effects/energy-shield/index.ts +0 -432
  105. package/packages/effects/explosion/assets/textures/baozha29.webp +0 -0
  106. package/packages/effects/explosion/assets/textures/glow01.webp +0 -0
  107. package/packages/effects/explosion/assets/textures/lizi01.webp +0 -0
  108. package/packages/effects/explosion/assets/textures/xulie_lizibaozha02_5x5.webp +0 -0
  109. package/packages/effects/explosion/index.ts +0 -115
  110. package/packages/effects/ground-tile-upgrade/effect.json +0 -6
  111. package/packages/effects/ground-tile-upgrade/index.ts +0 -410
  112. package/packages/effects/hit-flash/index.ts +0 -77
  113. package/packages/effects/laser-beam/assets/textures/laser_glow.webp +0 -0
  114. package/packages/effects/laser-beam/assets/textures/laser_noise.webp +0 -0
  115. package/packages/effects/laser-beam/index.ts +0 -6
  116. package/packages/effects/laser-beam-factory.ts +0 -146
  117. package/packages/effects/lumber-cart-suction/effect.json +0 -6
  118. package/packages/effects/lumber-cart-suction/index.ts +0 -399
  119. package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb +0 -0
  120. package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb.versions/.active +0 -1
  121. package/packages/effects/lumber-debris-scatter/assets/models/lumber_prop_model.glb.versions/1781589063890_upload.glb +0 -0
  122. package/packages/effects/lumber-debris-scatter/assets/runtime/solidParticleScatter.v1.js +0 -236
  123. package/packages/effects/lumber-debris-scatter/effect.json +0 -228
  124. package/packages/effects/lumber-debris-scatter/index.ts +0 -780
  125. package/packages/effects/lumber_vehicle_gear_spin/assets/runtime/gearSpin.v1.js +0 -202
  126. package/packages/effects/lumber_vehicle_gear_spin/index.ts +0 -11
  127. package/packages/effects/lumber_vehicle_gear_spin/lumber_vehicle_gear_spin.json +0 -338
  128. package/packages/effects/lumber_vehicle_upgrade_aura/assets/runtime/upgradeAura.v1.js +0 -353
  129. package/packages/effects/lumber_vehicle_upgrade_aura/index.ts +0 -13
  130. package/packages/effects/lumber_vehicle_upgrade_aura/lumber_vehicle_upgrade_aura.json +0 -365
  131. package/packages/effects/lumber_vehicle_wood_chips/assets/runtime/solidParticleScatter.v1.js +0 -310
  132. package/packages/effects/lumber_vehicle_wood_chips/index.ts +0 -11
  133. package/packages/effects/lumber_vehicle_wood_chips/lumber_vehicle_wood_chips.json +0 -335
  134. package/packages/effects/monster-hit/index.ts +0 -129
  135. package/packages/effects/player-upgrade/effect.json +0 -6
  136. package/packages/effects/player-upgrade/index.ts +0 -640
  137. package/packages/effects/pulse-laser-volley/assets/textures/bolt_guangshu.webp +0 -0
  138. package/packages/effects/pulse-laser-volley/index.ts +0 -215
  139. package/packages/effects/resource-collect-sparkles/effect.json +0 -6
  140. package/packages/effects/resource-collect-sparkles/index.ts +0 -254
  141. package/packages/effects/run-footstep-dust/assets/runtime/runFootstepDust.v1.js +0 -215
  142. package/packages/effects/run-footstep-dust/effect.json +0 -183
  143. package/packages/effects/run-footstep-dust/index.ts +0 -377
  144. package/packages/effects/space-shared.ts +0 -120
  145. package/packages/effects/thruster-flame/index.ts +0 -306
  146. package/packages/effects/treasure-attention/effect.json +0 -7
  147. package/packages/effects/treasure-attention/index.ts +0 -630
  148. package/packages/effects/tree-chop-dust/assets/textures/tree_chop_dust_atlas.png +0 -0
  149. package/packages/effects/tree-chop-dust/effect.json +0 -8
  150. package/packages/effects/tree-chop-dust/index.ts +0 -202
  151. package/packages/effects/tree-leaf-chop-burst/assets/runtime/particleEmitter.v1.js +0 -303
  152. package/packages/effects/tree-leaf-chop-burst/assets/textures/tree_leaf_chop_burst_leaf_atlas.png +0 -0
  153. package/packages/effects/tree-leaf-chop-burst/effect.json +0 -498
  154. package/packages/effects/tree-leaf-chop-burst/index.ts +0 -294
  155. package/packages/effects/tree_chop_dust_burst/assets/runtime/particleEmitter.v1.js +0 -318
  156. package/packages/effects/tree_chop_dust_burst/assets/textures/tree_chop_dust_burst_dust.png +0 -0
  157. package/packages/effects/tree_chop_dust_burst/index.ts +0 -13
  158. package/packages/effects/tree_chop_dust_burst/tree_chop_dust_burst.json +0 -412
  159. package/packages/effects/ufo-thread-flame/assets/textures/flame_flow.webp +0 -0
  160. package/packages/effects/ufo-thread-flame/assets/textures/flame_spike.webp +0 -0
  161. package/packages/effects/ufo-thread-flame/index.ts +0 -153
  162. package/packages/effects/vehicle-exhaust-smoke/assets/textures/vehicle_exhaust_smoke_atlas.png +0 -0
  163. package/packages/effects/vehicle-exhaust-smoke/effect.json +0 -8
  164. package/packages/effects/vehicle-exhaust-smoke/index.ts +0 -298
  165. package/packages/effects/vehicle-upgrade/effect.json +0 -6
  166. package/packages/effects/vehicle-upgrade/index.ts +0 -404
  167. package/packages/effects/weapon-trail/assets/textures/trail_wedge.png +0 -0
  168. package/packages/effects/weapon-trail/index.ts +0 -317
  169. package/packages/effects/wood-crusher-sawdust/assets/runtime/particleEmitter.v1.js +0 -312
  170. package/packages/effects/wood-crusher-sawdust/assets/textures/wood_crusher_sawdust_dust.png +0 -0
  171. package/packages/effects/wood-crusher-sawdust/assets/textures/wood_crusher_sawdust_fiber.png +0 -0
  172. package/packages/effects/wood-crusher-sawdust/index.ts +0 -43
  173. package/packages/effects/wood-crusher-sawdust/wood_crusher_sawdust.json +0 -592
  174. package/packages/effects/wood-cutter-sawdust/assets/runtime/particleEmitter.v1.js +0 -312
  175. package/packages/effects/wood-cutter-sawdust/assets/textures/wood_cutter_sawdust_chip.png +0 -0
  176. package/packages/effects/wood-cutter-sawdust/assets/textures/wood_cutter_sawdust_dust.png +0 -0
  177. package/packages/effects/wood-cutter-sawdust/index.ts +0 -43
  178. package/packages/effects/wood-cutter-sawdust/wood_cutter_sawdust.json +0 -592
  179. package/packages/effects/wood-sawdust-effect-factory.ts +0 -194
  180. package/packages/effects/wood_crusher_sawdust/assets/runtime/particleEmitter.v1.js +0 -318
  181. package/packages/effects/wood_crusher_sawdust/assets/textures/wood_crusher_sawdust_dust.png +0 -0
  182. package/packages/effects/wood_crusher_sawdust/assets/textures/wood_crusher_sawdust_fiber.png +0 -0
  183. package/packages/effects/wood_crusher_sawdust/index.ts +0 -29
  184. package/packages/effects/wood_crusher_sawdust/wood_crusher_sawdust.json +0 -592
  185. package/packages/effects/wood_cutter_sawdust/assets/runtime/particleEmitter.v1.js +0 -318
  186. package/packages/effects/wood_cutter_sawdust/assets/textures/wood_cutter_sawdust_chip.png +0 -0
  187. package/packages/effects/wood_cutter_sawdust/assets/textures/wood_cutter_sawdust_dust.png +0 -0
  188. package/packages/effects/wood_cutter_sawdust/index.ts +0 -29
  189. package/packages/effects/wood_cutter_sawdust/wood_cutter_sawdust.json +0 -592
  190. package/packages/effects/wood_peel_material_test/assets/runtime/woodPeelMaterialTest.v1.js +0 -196
  191. package/packages/effects/wood_peel_material_test/index.ts +0 -11
  192. package/packages/effects/wood_peel_material_test/wood_peel_material_test.json +0 -136
  193. package/packages/env.particle.forest_edge_fog/assets/runtime/particleEmitter.v1.js +0 -210
  194. package/packages/env.particle.forest_edge_fog/assets/textures/f12a23c4-b924-4322-a260-3d982428f1e8.png +0 -0
  195. package/packages/env.particle.forest_edge_fog/assets/textures/yuan.png +0 -0
  196. package/packages/env.particle.forest_edge_fog/effect.json +0 -854
  197. package/packages/env.particle.forest_edge_fog/media/preview.gif +0 -0
  198. package/packages/env.particle.forest_edge_fog/media/preview.mp4 +0 -0
  199. package/packages/env.particle.forest_edge_fog/media/preview.webm +0 -0
  200. package/packages/env.particle.forest_edge_fog/media/thumbnail.png +0 -0
  201. package/packages/env.particle.forest_edge_fog/metadata/description.json +0 -148
  202. package/packages/env.particle.forest_edge_fog/provenance/03-effect.original.json +0 -827
  203. package/packages/env.particle.forest_edge_fog/provenance/03-normalized-vfx-to-babylon-report.json +0 -196
  204. package/packages/env.particle.forest_edge_fog/provenance/04-preview-overrides.json +0 -432
  205. package/packages/env.particle.forest_edge_fog/provenance/04-preview-tuning-report.json +0 -28
  206. package/packages/env.particle.forest_edge_fog/provenance/asset-map.json +0 -124
  207. package/packages/env.particle.forest_edge_fog/provenance/lineage.json +0 -41
  208. package/packages/generation.ts +0 -14
  209. package/packages/index.ts +0 -14
  210. package/packages/legacy-particle-runtime.ts +0 -113
  211. package/packages/manifestRuntimePackage.ts +0 -519
  212. package/packages/registry.ts +0 -85
  213. package/packages/rendering.ts +0 -13
  214. package/packages/resource.particle.viscous_oil_suction/assets/runtime/top_disk_suction/particleEmitter.v1.js +0 -424
  215. package/packages/resource.particle.viscous_oil_suction/assets/textures/Glow ad.png +0 -0
  216. package/packages/resource.particle.viscous_oil_suction/effect.json +0 -1254
  217. package/packages/resource.particle.viscous_oil_suction/media/preview.gif +0 -0
  218. package/packages/resource.particle.viscous_oil_suction/media/preview.mp4 +0 -0
  219. package/packages/resource.particle.viscous_oil_suction/media/preview.webm +0 -0
  220. package/packages/resource.particle.viscous_oil_suction/media/thumbnail.png +0 -0
  221. package/packages/resource.particle.viscous_oil_suction/metadata/description.json +0 -122
  222. package/packages/resource.particle.viscous_oil_suction/provenance/03-effect.original.json +0 -1051
  223. package/packages/resource.particle.viscous_oil_suction/provenance/03-normalized-vfx-to-babylon-report.json +0 -200
  224. package/packages/resource.particle.viscous_oil_suction/provenance/04-preview-overrides.json +0 -581
  225. package/packages/resource.particle.viscous_oil_suction/provenance/04-preview-tuning-report.json +0 -28
  226. package/packages/resource.particle.viscous_oil_suction/provenance/asset-map.json +0 -111
  227. package/packages/resource.particle.viscous_oil_suction/provenance/lineage.json +0 -45
  228. package/packages/types.ts +0 -195
  229. package/tooling/check-dist-runtime.mjs +0 -166
  230. package/tooling/generate-index.mjs +0 -55
  231. package/tooling/test-guardrails.mjs +0 -157
  232. package/tooling/validate-boundary.mjs +0 -92
  233. package/tooling/validate-effects.mjs +0 -304
  234. package/tooling/validate.mjs +0 -53
@@ -1,157 +0,0 @@
1
- #!/usr/bin/env node
2
- // guardrail 回归用例(issue #3 验收最后一条):
3
- // "故意造一个引全量 glTF 的特效 → 被 Layer 1 + Layer 2 双重拦下"。
4
- // 全程 hermetic(写临时目录,不碰 packages/effects 与真实 dist),跑完即清理。
5
- // Layer 1:对一批坏 fixture 跑 validate-effects,断言每个都被拒(exit≠0)+ 对干净 fixture 断言放行。
6
- // Layer 2:造一个含 meshopt token 的假 dist chunk(= 全量 glTF loader 会拉进来的东西),
7
- // 断言 check-dist-runtime 命中并归因到引它的特效;且 `basis` 变量名不被误报。
8
- // 用法:node tooling/test-guardrails.mjs 退出码:全过 0,任一断言失败 1。
9
- import { spawnSync } from 'node:child_process';
10
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
11
- import { tmpdir } from 'node:os';
12
- import { resolve, dirname, join } from 'node:path';
13
- import { fileURLToPath } from 'node:url';
14
-
15
- const LIB_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
16
- const tmp = mkdtempSync(join(tmpdir(), 'vfx-guardrail-'));
17
- let failures = 0;
18
-
19
- function runValidateEffects(effectsDir) {
20
- return spawnSync('node', ['tooling/validate-effects.mjs', '--effects-dir', effectsDir], { cwd: LIB_ROOT, encoding: 'utf8' });
21
- }
22
- function runCheckDist(distDir) {
23
- return spawnSync('node', ['tooling/check-dist-runtime.mjs', '--dist', distDir], { cwd: LIB_ROOT, encoding: 'utf8' });
24
- }
25
- function assert(cond, msg, detail = '') {
26
- if (cond) { console.log(` ✅ ${msg}`); }
27
- else { console.log(` ❌ ${msg}${detail ? `\n ${detail.replace(/\n/g, '\n ')}` : ''}`); failures += 1; }
28
- }
29
- function write(p, content) { mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, content); }
30
-
31
- try {
32
- // ── Layer 1 ────────────────────────────────────────────────────────────────
33
- console.log('Layer 1(validate-effects)回归:');
34
- const fxRoot = join(tmp, 'effects');
35
-
36
- // 坏 1:引全量 glTF barrel(meshopt 事故的根因)
37
- write(join(fxRoot, 'full-gltf', 'index.ts'),
38
- `import '@babylonjs/loaders/glTF';\nexport const e = { id:'full-gltf', nameZh:'x', defaultParams:{}, debugParams:[], create(){ return { root:{}, dispose(){} }; } };\n`);
39
- // 坏 2:new URL 资源 + 缺资源
40
- write(join(fxRoot, 'new-url', 'index.ts'),
41
- `const u = new URL('./assets/a.bin', import.meta.url);\nimport tex from './assets/missing.png?url';\nexport const e = { id:'new-url', nameZh:'x', defaultParams:{}, debugParams:[], create(){ return { root:{}, dispose(){} }; } };\n`);
42
- // 坏 3:直接引解码器类(间接 token)
43
- write(join(fxRoot, 'decoder-token', 'index.ts'),
44
- `import { MeshoptDecoder } from '@babylonjs/core/Meshes/Compression/meshoptCompression';\nexport const e = { id:'decoder-token', nameZh:'x', defaultParams:{}, debugParams:[], create(){ MeshoptDecoder.UseInstances = true; return { root:{}, dispose(){} }; } };\n`);
45
- // 坏 4:不是合法 package
46
- write(join(fxRoot, 'not-a-package', 'index.ts'), `export const notAPackage = { hello: 'world' };\n`);
47
- // 干净:核心 loader + ?url 存在 + 合法契约 + 注释里提 meshopt 不应误报
48
- write(join(fxRoot, 'clean', 'assets', 'm.glb'), 'GLB');
49
- write(join(fxRoot, 'clean', 'index.ts'),
50
- `// 用核心 loader,避免 EXT_meshopt_compression / MeshoptDecoder 被拉进包\nimport '@babylonjs/loaders/glTF/2.0/glTFLoader';\nimport url from './assets/m.glb?url';\nexport const e = { id:'clean', nameZh:'干净', defaultParams:{ a:1 }, debugParams:[{key:'a',label:'a',kind:'number'}], create(){ return { root:{}, dispose(){} }; } };\n`);
51
- write(join(fxRoot, 'clean', 'vfx-params.json'), `{ "schemaVersion":"vfx-params/1.0","effectId":"clean","params":{} }`);
52
-
53
- // 一次扫整根:坏的应被拒
54
- const allRes = runValidateEffects(fxRoot);
55
- assert(allRes.status !== 0, '坏 fixture 集合被 Layer 1 拒绝(exit≠0)', allRes.stdout);
56
- for (const [bad, needle] of [
57
- ['full-gltf', '@babylonjs/loaders/glTF'],
58
- ['new-url', 'new URL'],
59
- ['decoder-token', 'MeshoptDecoder'],
60
- ['not-a-package', '未检测到合法 VfxEffectPackage'],
61
- ]) {
62
- assert(allRes.stdout.includes(bad) && allRes.stdout.includes(needle), `命中坏 fixture「${bad}」并给出原因`, allRes.stdout);
63
- }
64
- // 规避向量回归(对抗式审查发现并已堵住的绕过手法,每个都必须仍被拦):
65
- const evasion = join(tmp, 'evasion');
66
- // 把全量 barrel + MeshoptDecoder 藏在 helper .js 里(非 .ts sibling)
67
- write(join(evasion, 'sibling-js', 'index.ts'), `import './heavy.js';\nexport const e={id:'x',nameZh:'x',defaultParams:{},debugParams:[],create(){return{root:{},dispose(){}};}};\n`);
68
- write(join(evasion, 'sibling-js', 'heavy.js'), `import '@babylonjs/loaders/glTF';\nexport const z = MeshoptDecoder;\n`);
69
- // 无空格副作用导入
70
- write(join(evasion, 'nospace', 'index.ts'), `import'@babylonjs/loaders/glTF';\nexport const e={id:'x',nameZh:'x',defaultParams:{},debugParams:[],create(){return{root:{},dispose(){}};}};\n`);
71
- // 模板字符串动态 import
72
- write(join(evasion, 'tmpl-dyn', 'index.ts'), "export const e={id:'x',nameZh:'x',defaultParams:{},debugParams:[],async create(){await import(`@babylonjs/loaders/glTF`);return{root:{},dispose(){}};}};\n");
73
- // 字符串拼接动态 import
74
- write(join(evasion, 'concat', 'index.ts'), `export const e={id:'x',nameZh:'x',defaultParams:{},debugParams:[],async create(){await import('@babylonjs/loaders'+'/glTF');return{root:{},dispose(){}};}};\n`);
75
- // 模板字符串 new URL
76
- write(join(evasion, 'tmpl-url', 'index.ts'), "const u=new URL(`./a.glb`,import.meta.url);\nexport const e={id:'x',nameZh:'x',defaultParams:{},debugParams:[],create(){return{root:{},dispose(){}};}};\n");
77
- const evRes = runValidateEffects(evasion);
78
- assert(evRes.status !== 0, '规避向量集合被 Layer 1 拒绝(exit≠0)', evRes.stdout);
79
- for (const id of ['sibling-js', 'nospace', 'tmpl-dyn', 'concat', 'tmpl-url']) {
80
- assert(evRes.stdout.includes(id), `规避向量「${id}」被拦下`, evRes.stdout);
81
- }
82
-
83
- // 契约误报回归:箭头 create / 泛型 create / re-export barrel 都应放行(0 error)
84
- const contractOk = join(tmp, 'contract-ok');
85
- write(join(contractOk, 'arrow', 'index.ts'), `export const e={id:'x',nameZh:'测',defaultParams:{a:1},debugParams:[{key:'a',label:'a',kind:'number'}],create:(ctx)=>({root:ctx.root,dispose(){}})};\n`);
86
- write(join(contractOk, 'arrow', 'vfx-params.json'), `{"schemaVersion":"vfx-params/1.0","effectId":"arrow","params":{}}`);
87
- write(join(contractOk, 'barrel', 'index.ts'), `export * from './impl';\n`);
88
- write(join(contractOk, 'barrel', 'impl.ts'), `export const e={id:'x',nameZh:'测',defaultParams:{},debugParams:[],create(){return{root:{},dispose(){}};}};\n`);
89
- write(join(contractOk, 'barrel', 'vfx-params.json'), `{"schemaVersion":"vfx-params/1.0","effectId":"barrel","params":{}}`);
90
- const contractRes = runValidateEffects(contractOk);
91
- assert(contractRes.status === 0, '箭头 create / re-export barrel 合法写法被放行(无契约误报)', contractRes.stdout);
92
-
93
- // 面板↔参数 key 一致性:debugParams 旋钮的 key 不在 defaultParams → 拦;对齐 → 放行
94
- const keyBad = join(tmp, 'key-bad');
95
- write(join(keyBad, 'orphan-knob', 'index.ts'),
96
- `const DEFAULT_PARAMS = { lifetime: 1, scale: 2 };\nexport const e = { id:'orphan-knob', nameZh:'测', defaultParams: DEFAULT_PARAMS, debugParams: [ {key:'lifetime',label:'a',kind:'lifetime'}, {key:'scale',label:'b',kind:'scale'}, {key:'partcileCount',label:'拼错',kind:'number'} ], create(){ return { root:{}, dispose(){} }; } };\n`);
97
- write(join(keyBad, 'orphan-knob', 'vfx-params.json'), `{"schemaVersion":"vfx-params/1.0","effectId":"orphan-knob","params":{}}`);
98
- const keyBadRes = runValidateEffects(keyBad);
99
- assert(keyBadRes.status !== 0 && /partcileCount/.test(keyBadRes.stdout), 'debugParams 旋钮 key 无对应 defaultParams 被拦(面板↔参数一致性)', keyBadRes.stdout);
100
-
101
- const keyOk = join(tmp, 'key-ok');
102
- write(join(keyOk, 'aligned', 'index.ts'),
103
- `const DEFAULT_PARAMS = { lifetime: 1, count: 8 };\nexport const e = { id:'aligned', nameZh:'测', defaultParams: DEFAULT_PARAMS, debugParams: [ {key:'lifetime',label:'a',kind:'lifetime'}, {key:'count',label:'b',kind:'number'} ], create(){ return { root:{}, dispose(){} }; } };\n`);
104
- write(join(keyOk, 'aligned', 'vfx-params.json'), `{"schemaVersion":"vfx-params/1.0","effectId":"aligned","params":{}}`);
105
- const keyOkRes = runValidateEffects(keyOk);
106
- assert(keyOkRes.status === 0, 'debugParams.key ⊆ defaultParams.key 时放行', keyOkRes.stdout);
107
-
108
- // 干净 fixture 单独扫应放行
109
- const cleanRoot = join(tmp, 'effects-clean');
110
- write(join(cleanRoot, 'clean', 'assets', 'm.glb'), 'GLB');
111
- write(join(cleanRoot, 'clean', 'index.ts'),
112
- `// 注释提到 EXT_meshopt_compression / MeshoptDecoder 不应误报\nimport '@babylonjs/loaders/glTF/2.0/glTFLoader';\nimport url from './assets/m.glb?url';\nexport const e = { id:'clean', nameZh:'干净', defaultParams:{ a:1 }, debugParams:[{key:'a',label:'a',kind:'number'}], create(){ return { root:{}, dispose(){} }; } };\n`);
113
- write(join(cleanRoot, 'clean', 'vfx-params.json'), `{ "schemaVersion":"vfx-params/1.0","effectId":"clean","params":{} }`);
114
- const cleanRes = runValidateEffects(cleanRoot);
115
- assert(cleanRes.status === 0, '干净 fixture 被放行(注释里的 meshopt 字样不误报)', cleanRes.stdout);
116
-
117
- // ── Layer 2 ────────────────────────────────────────────────────────────────
118
- console.log('\nLayer 2(check-dist-runtime)回归:');
119
- const fakeDist = join(tmp, 'dist');
120
- // 共享 heavy chunk = 全量 glTF loader 会拉进来的 meshopt 运行时
121
- write(join(fakeDist, 'chunks', 'heavy-aaa.js'),
122
- `export const EXT = "EXT_meshopt_compression";\nexport function setup(){ MeshoptDecoder.UseInstances = true; }\n`);
123
- // light chunk:含 `basis` 变量名(必须不被误报)
124
- write(join(fakeDist, 'chunks', 'light-bbb.js'), `export const ringBasis = "a normal basis variable";\n`);
125
- write(join(fakeDist, 'index.js'), `import { ringBasis } from "./chunks/light-bbb.js";\nconsole.log(ringBasis);\n`);
126
- write(join(fakeDist, 'effects', 'bad-effect', 'index.js'), `import { setup } from "../../chunks/heavy-aaa.js";\nsetup();\n`);
127
- write(join(fakeDist, 'effects', 'clean-effect', 'index.js'), `import { ringBasis } from "../../chunks/light-bbb.js";\nconsole.log(ringBasis);\n`);
128
- const distRes = runCheckDist(fakeDist);
129
- assert(distRes.status !== 0, '含 meshopt 的假 dist 被 Layer 2 拦下(exit≠0)', distRes.stdout);
130
- assert(distRes.stdout.includes('bad-effect') && /meshopt/i.test(distRes.stdout), '命中并归因到引入它的特效「bad-effect」', distRes.stdout);
131
- assert(!distRes.stdout.includes('clean-effect'), '只引 light chunk 的「clean-effect」不被误报', distRes.stdout);
132
- assert(!/✗.*basis['"]?\s*$/m.test(distRes.stdout) && !distRes.stdout.includes('light-bbb'), '`basis` 变量名不触发误报', distRes.stdout);
133
-
134
- // Layer 2 鲁棒性回归:meshopt 藏 .mjs chunk 仍被拦
135
- const distMjs = join(tmp, 'dist-mjs');
136
- write(join(distMjs, 'chunks', 'heavy.mjs'), `export const x="EXT_meshopt_compression"; MeshoptDecoder.foo();\n`);
137
- write(join(distMjs, 'effects', 'evil', 'index.js'), `import '../../chunks/heavy.mjs';\n`);
138
- const mjsRes = runCheckDist(distMjs);
139
- assert(mjsRes.status !== 0 && mjsRes.stdout.includes('evil'), 'meshopt 藏 .mjs chunk 仍被 Layer 2 拦下并归因', mjsRes.stdout);
140
-
141
- // Layer 2 假阳性回归:.wasm 错误消息(无 /)+ 注释里的 token 都不应误报
142
- const distFp = join(tmp, 'dist-fp');
143
- write(join(distFp, 'effects', 'a', 'index.js'), `export function c(){ throw new Error("dropped optional physics.wasm"); }\n`);
144
- write(join(distFp, 'effects', 'b', 'index.js'), `/** intentionally avoids MeshoptDecoder and DracoCompression */\nexport const e=1;\n`);
145
- const fpRes = runCheckDist(distFp);
146
- assert(fpRes.status === 0, '.wasm 文件名错误消息 + 注释里的禁 token 不触发 Layer 2 误报', fpRes.stdout);
147
- } finally {
148
- rmSync(tmp, { recursive: true, force: true });
149
- }
150
-
151
- console.log('');
152
- if (failures === 0) {
153
- console.log('✅ guardrail 回归全部通过(全量 glTF 特效被 Layer 1 + Layer 2 双重拦下;无误报)。');
154
- process.exit(0);
155
- }
156
- console.log(`❌ guardrail 回归:${failures} 条断言失败。`);
157
- process.exit(1);
@@ -1,92 +0,0 @@
1
- #!/usr/bin/env node
2
- // 库纯净契约(plan §17)的执行点:扫描库内所有 import,凡越过库边界 / 反向依赖项目 → 报违规。
3
- // 允许:@babylonjs/*、@fps/*、node: 内置、第三方裸包、库内相对路径(解析到库内存在的文件)。
4
- // 违规:相对路径解析后逃出库根或指向不存在的文件;命中项目模块特征(config/core/services/systems/gameplay/assets-index 等)。
5
- import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs';
6
- import { resolve, dirname, join, relative } from 'node:path';
7
- import { fileURLToPath } from 'node:url';
8
-
9
- const LIB_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
10
- // 新包面 = packages/ 根的运行时 .ts + packages/effects/**(不含旧 Cocos 包 build.particle.* 等)
11
- const PKG_ROOT = join(LIB_ROOT, 'packages');
12
-
13
- // 项目模块特征(反向依赖项目的明显信号)
14
- const PROJECT_HINTS = [
15
- /(^|\/)config\//, /(^|\/)core\//, /(^|\/)services\//, /(^|\/)systems\//,
16
- /(^|\/)gameplay\//, /projectGameplayConfig/, /babylon-gltf-loader/,
17
- /(^|\/)assets$/, /(^|\/)assets\/index/, /UIImages/, /configService/,
18
- ];
19
- const ALLOWED_BARE = [/^@babylonjs\//, /^@fps\//, /^node:/];
20
-
21
- function walk(dir, out = []) {
22
- if (!existsSync(dir)) return out;
23
- for (const e of readdirSync(dir)) {
24
- if (e === 'node_modules' || e === 'dist' || e.startsWith('.')) continue;
25
- const p = join(dir, e);
26
- const s = statSync(p);
27
- if (s.isDirectory()) walk(p, out);
28
- else if (/\.(ts|tsx|js|mjs)$/.test(e)) out.push(p);
29
- }
30
- return out;
31
- }
32
-
33
- function specifiers(src) {
34
- const out = new Set();
35
- let m;
36
- // a) `... from '...'`(普通/重导出)
37
- const fromRe = /\bfrom\s*['"]([^'"]+)['"]/g;
38
- while ((m = fromRe.exec(src))) out.add(m[1]);
39
- // b) 副作用导入 `import '...'`(无 from)
40
- const sideRe = /\bimport\s+['"]([^'"]+)['"]/g;
41
- while ((m = sideRe.exec(src))) out.add(m[1]);
42
- // c) 动态 import('...')
43
- const dynRe = /\bimport\(\s*['"]([^'"]+)['"]\s*\)/g;
44
- while ((m = dynRe.exec(src))) out.add(m[1]);
45
- return [...out];
46
- }
47
-
48
- function resolvesInLib(fromFile, spec) {
49
- // 去掉 ?url / ?raw 等 query
50
- const clean = spec.replace(/\?.*$/, '');
51
- const base = resolve(dirname(fromFile), clean);
52
- if (!base.startsWith(LIB_ROOT)) return { ok: false, reason: '逃出库根' };
53
- const cands = [base, base + '.ts', base + '.tsx', base + '.js', base + '.mjs', base + '.json',
54
- join(base, 'index.ts'), join(base, 'index.js')];
55
- if (cands.some(existsSync)) return { ok: true };
56
- return { ok: false, reason: '指向库内不存在的文件(可能本应指向项目)' };
57
- }
58
-
59
- const violations = [];
60
- // 扫:packages/ 根的运行时 .ts(非递归)+ packages/effects/ 全树
61
- const rootTsFiles = existsSync(PKG_ROOT)
62
- ? readdirSync(PKG_ROOT).filter((e) => /\.(ts|tsx|js|mjs)$/.test(e)).map((e) => join(PKG_ROOT, e))
63
- : [];
64
- const files = [...rootTsFiles, ...walk(join(PKG_ROOT, 'effects'))];
65
- for (const f of files) {
66
- const src = readFileSync(f, 'utf8');
67
- for (const spec of specifiers(src)) {
68
- const rel = relative(LIB_ROOT, f);
69
- if (spec.startsWith('.')) {
70
- const r = resolvesInLib(f, spec);
71
- if (!r.ok) violations.push({ file: rel, spec, reason: r.reason });
72
- } else if (ALLOWED_BARE.some((re) => re.test(spec))) {
73
- continue;
74
- } else if (PROJECT_HINTS.some((re) => re.test(spec))) {
75
- violations.push({ file: rel, spec, reason: '命中项目模块特征' });
76
- }
77
- // 其它裸包(第三方)默认放行
78
- }
79
- }
80
-
81
- if (violations.length === 0) {
82
- console.log('✅ 库纯净契约:无越界 import。');
83
- process.exit(0);
84
- }
85
- console.log(`❌ 库纯净契约:发现 ${violations.length} 处越界/反向依赖 import:\n`);
86
- const byFile = {};
87
- for (const v of violations) (byFile[v.file] ??= []).push(v);
88
- for (const [file, vs] of Object.entries(byFile)) {
89
- console.log(` ${file}`);
90
- for (const v of vs) console.log(` → "${v.spec}" [${v.reason}]`);
91
- }
92
- process.exit(1);
@@ -1,304 +0,0 @@
1
- #!/usr/bin/env node
2
- // 新增特效 guardrail · Layer 1(源码静态 lint,见 issue #3)。
3
- // 对每个 packages/effects/<id>/ 静态扫描,在「进库前 / 发版前」拦住会坑消费方的模式:
4
- // 1. 引用策略 —— 禁全量 glTF barrel / meshopt / draco / ktx2 等会被急切拉进 consumer 包的重运行时
5
- // 扩展(根治 v0.1.1 的 meshopt 事故);要核心 loader `@babylonjs/loaders/glTF/2.0/glTFLoader`。
6
- // 2. 资源内联 —— 资源用 `?url` 导入(库 build 才会内联成 base64);禁 `new URL(...,import.meta.url)`;
7
- // 引用的 .glb/.png/.js 资源必须存在。
8
- // 3. 契约 —— index.ts 导出合法 VfxEffectPackage(create+id+nameZh+defaultParams+debugParams,或 data 工厂)。
9
- // 4. 结构 —— 有 index.ts、vfx-params.json 合法 JSON;目录名 vs 导出 id(软提示)。
10
- //
11
- // 用法:
12
- // node tooling/validate-effects.mjs # 扫全部特效
13
- // node tooling/validate-effects.mjs --effect <id> # 只扫一个(发版 gate / 回归用)
14
- // node tooling/validate-effects.mjs --effects-dir <path> # 换扫描根(回归 fixture 用)
15
- // 退出码:有 error → 1;只有 warning 或全过 → 0。
16
- import { readdirSync, statSync, readFileSync, existsSync } from 'node:fs';
17
- import { resolve, dirname, join, relative } from 'node:path';
18
- import { fileURLToPath } from 'node:url';
19
-
20
- const LIB_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
21
-
22
- // ── CLI ──────────────────────────────────────────────────────────────────────
23
- const argv = process.argv.slice(2);
24
- function flag(name) {
25
- const i = argv.indexOf(name);
26
- return i >= 0 ? argv[i + 1] : undefined;
27
- }
28
- const onlyEffect = flag('--effect');
29
- const effectsDir = resolve(flag('--effects-dir') ?? join(LIB_ROOT, 'packages', 'effects'));
30
-
31
- // ── 引用策略 ──────────────────────────────────────────────────────────────────
32
- // 唯一允许的 glTF loader 入口(只注册 2.0 核心,不引扩展)。
33
- const ALLOWED_LOADER_SPECIFIERS = new Set(['@babylonjs/loaders/glTF/2.0/glTFLoader']);
34
- // 轻量扩展白名单(按需扩;默认空 → 任何 @babylonjs/loaders 扩展都需显式审查后加入)。
35
- const ALLOWED_LOADER_EXTENSION_PREFIXES = [];
36
- // 明确点名的重 barrel/扩展(命中给定向修复提示)。
37
- const HEAVY_LOADER_HINTS = [
38
- { re: /^@babylonjs\/loaders$/, why: '全量 loaders barrel(注册所有格式 + 全部 glTF 扩展)' },
39
- { re: /^@babylonjs\/loaders\/glTF$/, why: '全量 glTF barrel(急切注册 EXT_meshopt_compression / KHR_draco 等扩展)' },
40
- { re: /^@babylonjs\/loaders\/glTF\/2\.0$/, why: 'glTF 2.0 barrel(index,会拉入扩展注册)' },
41
- { re: /EXT_meshopt_compression/, why: 'meshopt 扩展(把 MeshoptDecoder 拉进 consumer 包)' },
42
- { re: /KHR_draco_mesh_compression/, why: 'draco 扩展(把 Draco 解码器拉进 consumer 包)' },
43
- { re: /KHR_texture_basisu/, why: 'ktx2/basisu 扩展(把 basis transcoder 拉进 consumer 包)' },
44
- ];
45
- const LOADER_FIX = '改用核心加载器 `import \'@babylonjs/loaders/glTF/2.0/glTFLoader\'`(只注册 2.0 核心,不引扩展);GLB 须为 plain(零扩展)。';
46
-
47
- // 在已剥注释的源码里直接出现 = 危险解码器引用(即便不是通过 loaders barrel 引入,如直接 import @babylonjs/core 的解码器)。
48
- const FORBIDDEN_RUNTIME_TOKENS = [
49
- 'EXT_meshopt_compression', 'MeshoptDecoder', 'meshopt_decoder',
50
- 'KHR_draco_mesh_compression', 'DracoCompression', 'DracoDecoder',
51
- 'KHR_texture_basisu', 'KhronosTextureContainer2', '_KTX2Decoder', 'MSCTranscoder', 'basis_transcoder',
52
- 'application/x-glb-brotli', 'brotli-dec-wasm',
53
- ];
54
-
55
- // ── 工具 ──────────────────────────────────────────────────────────────────────
56
- function stripComments(src) {
57
- // 先去块注释,再去行注释(用 (?<!:) 避免吃掉 URL 里的 `://`)。
58
- return src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(?<!:)\/\/[^\n]*/g, '');
59
- }
60
-
61
- // import/require specifier(含无空格副作用导入、反引号动态 import、require)。
62
- function specifiers(src) {
63
- const out = new Set();
64
- let m;
65
- const fromRe = /\bfrom\s*[`'"]([^`'"]+)[`'"]/g;
66
- while ((m = fromRe.exec(src))) out.add(m[1]);
67
- const sideRe = /\bimport\s*[`'"]([^`'"]+)[`'"]/g; // \s* :允许 import'x'(零空格)
68
- while ((m = sideRe.exec(src))) out.add(m[1]);
69
- const dynRe = /\bimport\(\s*[`'"]([^`'"]+)[`'"]\s*\)/g; // [`'"] :含模板字符串动态 import
70
- while ((m = dynRe.exec(src))) out.add(m[1]);
71
- const reqRe = /\brequire\(\s*[`'"]([^`'"]+)[`'"]\s*\)/g;
72
- while ((m = reqRe.exec(src))) out.add(m[1]);
73
- return [...out];
74
- }
75
-
76
- // 所有字符串字面量(单/双引号 + 不含插值的反引号)。用于在任意位置(import / require /
77
- // 字符串拼接 fragment)抓 heavy loader specifier —— 比只看 import 位置更难绕过。
78
- function stringLiterals(src) {
79
- const out = [];
80
- let m;
81
- const re = /(['"])((?:[^'"\\]|\\.)*)\1|`([^`$\\]*)`/g;
82
- while ((m = re.exec(src))) out.push(m[2] ?? m[3] ?? '');
83
- return out;
84
- }
85
-
86
- // ── key 一致性(面板↔参数)用的轻量括号配平解析 ────────────────────────────────
87
- // 从 startIdx(open 字符 { 或 [ 处)取平衡子串(处理字符串/转义)。
88
- function balancedFrom(src, startIdx) {
89
- const open = src[startIdx];
90
- const close = open === '{' ? '}' : open === '[' ? ']' : null;
91
- if (!close) return '';
92
- let depth = 0, inStr = null;
93
- for (let i = startIdx; i < src.length; i++) {
94
- const c = src[i];
95
- if (inStr) { if (c === '\\') { i++; continue; } if (c === inStr) inStr = null; continue; }
96
- if (c === '"' || c === "'" || c === '`') { inStr = c; continue; }
97
- if (c === open) depth++;
98
- else if (c === close) { depth--; if (depth === 0) return src.slice(startIdx, i + 1); }
99
- }
100
- return '';
101
- }
102
- // 对象字面量(含外层 {})的顶层键名集合。
103
- function topLevelKeys(objStr) {
104
- const keys = new Set();
105
- let depth = 0, inStr = null, atProp = false;
106
- for (let i = 0; i < objStr.length; i++) {
107
- const c = objStr[i];
108
- if (inStr) { if (c === '\\') { i++; continue; } if (c === inStr) inStr = null; continue; }
109
- if (c === '"' || c === "'" || c === '`') {
110
- if (depth === 1 && atProp) { const m = objStr.slice(i).match(/^(['"])([A-Za-z_$][\w$]*)\1\s*:/); if (m) { keys.add(m[2]); atProp = false; } }
111
- inStr = c; continue;
112
- }
113
- if (c === '{' || c === '[' || c === '(') { depth++; if (depth === 1) atProp = true; continue; }
114
- if (c === '}' || c === ']' || c === ')') { depth--; continue; }
115
- if (depth === 1) {
116
- if (c === ',') { atProp = true; continue; }
117
- if (atProp && /[A-Za-z_$]/.test(c)) { const m = objStr.slice(i).match(/^([A-Za-z_$][\w$]*)\s*:/); if (m) { keys.add(m[1]); atProp = false; } }
118
- }
119
- }
120
- return keys;
121
- }
122
- function valueAfterProp(code, prop) {
123
- const m = new RegExp(`\\b${prop}\\s*:\\s*`).exec(code);
124
- if (!m) return null;
125
- let i = m.index + m[0].length;
126
- while (i < code.length && /\s/.test(code[i])) i++;
127
- return { idx: i, ch: code[i] };
128
- }
129
- // defaultParams 的键:支持内联 { } 或指向 const FOO = { }
130
- function defaultParamKeys(code) {
131
- const v = valueAfterProp(code, 'defaultParams');
132
- if (!v) return null;
133
- if (v.ch === '{') return topLevelKeys(balancedFrom(code, v.idx));
134
- const idm = code.slice(v.idx).match(/^([A-Za-z_$][\w$]*)/);
135
- if (!idm) return null;
136
- const cm = new RegExp(`\\b(?:const|let|var)\\s+${idm[1]}\\b[^=]*=\\s*`).exec(code);
137
- if (!cm) return null;
138
- let j = cm.index + cm[0].length;
139
- while (j < code.length && /\s/.test(code[j])) j++;
140
- if (code[j] !== '{') return null;
141
- return topLevelKeys(balancedFrom(code, j));
142
- }
143
- // debugParams 数组里每个条目的 key
144
- function debugParamKeys(code) {
145
- const v = valueAfterProp(code, 'debugParams');
146
- if (!v || v.ch !== '[') return null;
147
- const arr = balancedFrom(code, v.idx);
148
- return new Set([...arr.matchAll(/\bkey\s*:\s*['"]([^'"]+)['"]/g)].map((m) => m[1]));
149
- }
150
-
151
- // 扫特效目录下"会被打包成模块"的代码文件;assets/ 是 ?url 运行时资产(非静态 import),排除。
152
- const CODE_EXT = /\.(tsx?|mts|cts|jsx?|mjs|cjs)$/;
153
- function codeFilesIn(dir, isRoot = true) {
154
- const out = [];
155
- for (const e of readdirSync(dir)) {
156
- if (e === 'node_modules' || e.startsWith('.')) continue;
157
- if (e === 'assets') continue; // ?url 资产目录,运行时按 URL 加载,不参与模块打包
158
- const p = join(dir, e);
159
- const s = statSync(p);
160
- if (s.isDirectory()) out.push(...codeFilesIn(p, false));
161
- else if (CODE_EXT.test(e)) out.push(p);
162
- }
163
- return out;
164
- }
165
-
166
- // ── 单特效校验 ────────────────────────────────────────────────────────────────
167
- function validateEffect(id) {
168
- const dir = join(effectsDir, id);
169
- const errors = [];
170
- const warnings = [];
171
- const indexPath = join(dir, 'index.ts');
172
-
173
- if (!existsSync(indexPath)) {
174
- errors.push(`缺少 index.ts(每个特效目录必须有 index.ts 导出 VfxEffectPackage)。`);
175
- return { id, errors, warnings };
176
- }
177
-
178
- const files = codeFilesIn(dir);
179
- const indexSrcRaw = readFileSync(indexPath, 'utf8');
180
- const indexCode = stripComments(indexSrcRaw);
181
-
182
- for (const file of files) {
183
- const rel = relative(dirname(effectsDir), file);
184
- const raw = readFileSync(file, 'utf8');
185
- const code = stripComments(raw);
186
-
187
- // 1) 引用策略:扫所有字符串字面量(覆盖 import / require / 字符串拼接 fragment / 反引号)。
188
- // 凡以 @babylonjs/loaders 开头、又不是唯一允许的核心 loader / 白名单扩展 → 拦。
189
- for (const lit of stringLiterals(code)) {
190
- if (!lit.startsWith('@babylonjs/loaders')) continue;
191
- if (ALLOWED_LOADER_SPECIFIERS.has(lit)) continue;
192
- if (ALLOWED_LOADER_EXTENSION_PREFIXES.some((p) => lit.startsWith(p))) continue;
193
- const hint = HEAVY_LOADER_HINTS.find((h) => h.re.test(lit));
194
- errors.push(`[引用策略] ${rel} 引用 "${lit}"${hint ? `(${hint.why})` : '(非白名单 loaders 子路径)'} → ${LOADER_FIX}`);
195
- }
196
-
197
- // 1b) 危险解码器 token(剥注释后,覆盖 helper .js/.mjs 等)
198
- for (const token of FORBIDDEN_RUNTIME_TOKENS) {
199
- if (code.includes(token)) {
200
- errors.push(`[引用策略] ${rel} 出现禁运行时标识 "${token}" → 会把重解码器拉进 consumer 包(违反 Playable bundle 策略,见 #2 v0.1.1)。`);
201
- }
202
- }
203
-
204
- // 2) 资源:禁 new URL(...,import.meta.url)(含模板字符串/单双引号)
205
- if (/new\s+URL\s*\(\s*[`'"][^`'"]+[`'"]\s*,\s*import\.meta\.url\s*\)/.test(code)) {
206
- errors.push(`[资源] ${rel} 用了 \`new URL(..., import.meta.url)\` → 库 build 不会内联它。改用 \`import X from './path?url'\`(Phase1 已统一,见 #2)。`);
207
- }
208
-
209
- // 2b) ?url / ?raw 资源必须存在(query 里出现 url/raw 标志即算,容忍 ?url&inline 等附加 query)
210
- for (const spec of specifiers(code)) {
211
- if (!/[?&](url|raw)(?=&|=|$)/.test(spec)) continue;
212
- if (!spec.startsWith('.')) continue; // 只校验相对资源
213
- const cleaned = spec.replace(/\?.*$/, '');
214
- const assetPath = resolve(dirname(file), cleaned);
215
- if (!existsSync(assetPath)) {
216
- errors.push(`[资源] ${rel} 引用的资源不存在:"${spec}"(解析为 ${relative(dirname(effectsDir), assetPath)})。`);
217
- }
218
- }
219
- }
220
-
221
- // 3) 契约:导出合法 VfxEffectPackage
222
- // data 类用 createManifestRuntimeEffectPackage / createDataDrivenEffectPackage;
223
- // 另有共享工厂(如 createWoodSawdustEffectPackage)。统一识别 create*EffectPackage( 调用。
224
- const usesFactory = /\bcreate\w*EffectPackage\s*\(/.test(indexCode);
225
- // create 可为方法 create( / 泛型 create<T>( / 箭头属性 create: (ctx)=>
226
- const hasInlineShape = /\bcreate\s*[(:<]/.test(indexCode)
227
- && /\bid\s*:/.test(indexCode)
228
- && /\bnameZh\s*:/.test(indexCode)
229
- && /\bdefaultParams\b/.test(indexCode)
230
- && /\bdebugParams\b/.test(indexCode);
231
- // index.ts 仅做 re-export(package 定义在 ./impl 等同目录模块)也算合规
232
- const reExportsRelative = /export\s+(?:\*|\{[^}]*\}|.+\s+as\s+default)\s+from\s*[`'"]\.[^`'"]*[`'"]/.test(indexCode);
233
- const hasExport = /\bexport\b/.test(indexCode);
234
- if (!hasExport) {
235
- errors.push(`[契约] index.ts 没有任何 export(需导出 VfxEffectPackage)。`);
236
- } else if (!usesFactory && !hasInlineShape && !reExportsRelative) {
237
- errors.push(`[契约] index.ts 未检测到合法 VfxEffectPackage:code 类需 create+id+nameZh+defaultParams+debugParams;data 类用 create*EffectPackage 工厂;或 re-export 自同目录模块。`);
238
- }
239
-
240
- // 3b) 面板↔参数 key 一致性:每个 debugParams[].key 必须在 defaultParams 里有默认值,
241
- // 否则面板生成的旋钮调它不生效(以 undefined 起步)。仅对 inline code 特效;
242
- // 工厂/data 类的 default/debug 由工厂生成,跳过(解析不到也跳过,不误报)。
243
- if (hasInlineShape && !usesFactory && !reExportsRelative) {
244
- const defaultKeys = defaultParamKeys(indexCode);
245
- const debugKeys = debugParamKeys(indexCode);
246
- if (defaultKeys && debugKeys) {
247
- const orphan = [...debugKeys].filter((k) => !defaultKeys.has(k));
248
- if (orphan.length) {
249
- errors.push(`[参数] debugParams 旋钮 [${orphan.join(', ')}] 在 defaultParams 里没有对应默认值 → 面板调它不生效(以 undefined 起步)。把这些 key 补进 defaultParams,或修正 debugParams 的 key 使其与 defaultParams/create() 读取的 key 一致。`);
250
- }
251
- }
252
- }
253
-
254
- // 4) 结构:vfx-params.json
255
- const paramsPath = join(dir, 'vfx-params.json');
256
- if (existsSync(paramsPath)) {
257
- try {
258
- JSON.parse(readFileSync(paramsPath, 'utf8'));
259
- } catch (e) {
260
- errors.push(`[结构] vfx-params.json 不是合法 JSON:${e.message}`);
261
- }
262
- } else {
263
- warnings.push(`[结构] 缺 vfx-params.json(项目调参种子;建议补一个 {schemaVersion,effectId,params:{}})。`);
264
- }
265
- // 注:本库导出 id 可带命名空间(如 run-footstep-dust → build.particle.run_footstep_dust),
266
- // 故不强制「目录名==导出 id」;tooling(generate-index/vfx-cli)统一以目录名作 dir key。
267
-
268
- return { id, errors, warnings };
269
- }
270
-
271
- // ── 主流程 ────────────────────────────────────────────────────────────────────
272
- if (!existsSync(effectsDir)) {
273
- console.error(`✗ effects 目录不存在:${effectsDir}`);
274
- process.exit(1);
275
- }
276
- const ids = (onlyEffect ? [onlyEffect] : readdirSync(effectsDir))
277
- .filter((id) => {
278
- try { return statSync(join(effectsDir, id)).isDirectory(); } catch { return false; }
279
- })
280
- .sort();
281
-
282
- let errorCount = 0;
283
- let warnCount = 0;
284
- const failed = [];
285
- for (const id of ids) {
286
- const { errors, warnings } = validateEffect(id);
287
- errorCount += errors.length;
288
- warnCount += warnings.length;
289
- if (errors.length || warnings.length) {
290
- const mark = errors.length ? '✗' : '⚠';
291
- console.log(`${mark} ${id}`);
292
- for (const e of errors) console.log(` ✗ ${e}`);
293
- for (const w of warnings) console.log(` ⚠ ${w}`);
294
- }
295
- if (errors.length) failed.push(id);
296
- }
297
-
298
- console.log('');
299
- if (errorCount === 0) {
300
- console.log(`✅ Layer 1 特效 lint 通过:${ids.length} 个特效,0 error${warnCount ? `,${warnCount} warning(不阻断)` : ''}。`);
301
- process.exit(0);
302
- }
303
- console.log(`❌ Layer 1 特效 lint:${failed.length}/${ids.length} 个特效有问题,共 ${errorCount} error。`);
304
- process.exit(1);
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env node
2
- // `pnpm validate` 聚合闸(issue #3 串联):一条命令跑完所有 guardrail。
3
- // boundary(纯净契约) → effects(Layer 1 源码 lint) → typecheck → build → check-dist(Layer 2)
4
- // 任一步失败即中止并以非 0 退出(作者本地 / CI PR / 发版 gate 共用,见 release.mjs)。
5
- //
6
- // 用法:
7
- // node tooling/validate.mjs # 全量(含 build + dist 扫描)
8
- // node tooling/validate.mjs --no-build # 快速:只 boundary + effects + typecheck(跳过 build/dist)
9
- import { spawnSync } from 'node:child_process';
10
- import { existsSync } from 'node:fs';
11
- import { resolve, dirname, join } from 'node:path';
12
- import { fileURLToPath } from 'node:url';
13
-
14
- const LIB_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
15
- const noBuild = process.argv.includes('--no-build');
16
-
17
- function bin(name) {
18
- const local = join(LIB_ROOT, 'node_modules', '.bin', name);
19
- return existsSync(local) ? local : name; // 退回 PATH(如 CI 已全局装)
20
- }
21
-
22
- const steps = [
23
- { name: '纯净契约 (validate-boundary)', cmd: 'node', args: ['tooling/validate-boundary.mjs'] },
24
- { name: 'Layer 1 特效 lint (validate-effects)', cmd: 'node', args: ['tooling/validate-effects.mjs'] },
25
- { name: 'typecheck (tsc --noEmit)', cmd: bin('tsc'), args: ['-p', 'tsconfig.json', '--noEmit'] },
26
- ];
27
- if (!noBuild) {
28
- steps.push({ name: 'build (vite)', cmd: bin('vite'), args: ['build'] });
29
- steps.push({ name: 'Layer 2 dist 禁运行时 (check-dist-runtime)', cmd: 'node', args: ['tooling/check-dist-runtime.mjs'] });
30
- }
31
-
32
- console.log(`▶ pnpm validate${noBuild ? ' --no-build' : ''} —— ${steps.length} 步\n`);
33
- const results = [];
34
- for (const [i, step] of steps.entries()) {
35
- console.log(`── [${i + 1}/${steps.length}] ${step.name} ──────────────────────────`);
36
- const r = spawnSync(step.cmd, step.args, { cwd: LIB_ROOT, stdio: 'inherit' });
37
- const ok = r.status === 0;
38
- results.push({ name: step.name, ok });
39
- console.log('');
40
- if (!ok) {
41
- console.log('❌ validate 中止于:' + step.name + (r.error ? `(${r.error.message})` : ` (exit ${r.status})`));
42
- summarize(results, i + 1, steps.length);
43
- process.exit(1);
44
- }
45
- }
46
- console.log('✅ validate 全部通过。');
47
- summarize(results, steps.length, steps.length);
48
-
49
- function summarize(results, ran, total) {
50
- console.log('\n摘要:');
51
- for (const r of results) console.log(` ${r.ok ? '✅' : '❌'} ${r.name}`);
52
- if (ran < total) console.log(` ⋯ 剩余 ${total - ran} 步未执行(已中止)。`);
53
- }