@pie-element/cli 0.1.1 → 0.1.2-next.1

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 (281) hide show
  1. package/README.md +48 -10
  2. package/dist/commands/dev/demo/index.d.ts +1 -1
  3. package/dist/commands/dev/demo/index.js +21 -0
  4. package/dist/commands/dev/demo/index.js.map +1 -1
  5. package/dist/commands/dev/test-bundler.d.ts +0 -1
  6. package/dist/commands/dev/test-bundler.js +2 -10
  7. package/dist/commands/dev/test-bundler.js.map +1 -1
  8. package/dist/commands/docs/generate.d.ts +16 -0
  9. package/dist/commands/docs/generate.js +150 -0
  10. package/dist/commands/docs/generate.js.map +1 -0
  11. package/dist/commands/docs/verify.d.ts +12 -0
  12. package/dist/commands/docs/verify.js +40 -0
  13. package/dist/commands/docs/verify.js.map +1 -0
  14. package/dist/commands/packages/create-controllers.d.ts +0 -1
  15. package/dist/commands/packages/create-controllers.js.map +1 -1
  16. package/dist/commands/packages/create-lib-react.d.ts +0 -1
  17. package/dist/commands/packages/create-lib-react.js +1 -3
  18. package/dist/commands/packages/create-lib-react.js.map +1 -1
  19. package/dist/commands/packages/create-react-elements.d.ts +0 -1
  20. package/dist/commands/packages/create-react-elements.js +2 -3
  21. package/dist/commands/packages/create-react-elements.js.map +1 -1
  22. package/dist/commands/packages/enable-publishing.d.ts +0 -1
  23. package/dist/commands/packages/enable-publishing.js +26 -23
  24. package/dist/commands/packages/enable-publishing.js.map +1 -1
  25. package/dist/commands/packages/init-synced-elements.d.ts +0 -1
  26. package/dist/commands/packages/init-synced-elements.js +1 -1
  27. package/dist/commands/packages/init-synced-elements.js.map +1 -1
  28. package/dist/commands/upstream/analyze-esm.d.ts +0 -1
  29. package/dist/commands/upstream/analyze-esm.js +5 -1
  30. package/dist/commands/upstream/analyze-esm.js.map +1 -1
  31. package/dist/commands/upstream/check.d.ts +0 -1
  32. package/dist/commands/upstream/check.js.map +1 -1
  33. package/dist/commands/upstream/deps.d.ts +0 -1
  34. package/dist/commands/upstream/deps.js.map +1 -1
  35. package/dist/commands/upstream/sync.d.ts +11 -2
  36. package/dist/commands/upstream/sync.js +364 -25
  37. package/dist/commands/upstream/sync.js.map +1 -1
  38. package/dist/commands/upstream/track.d.ts +0 -1
  39. package/dist/commands/upstream/track.js.map +1 -1
  40. package/dist/commands/upstream/update.d.ts +0 -1
  41. package/dist/commands/upstream/update.js +37 -7
  42. package/dist/commands/upstream/update.js.map +1 -1
  43. package/dist/commands/verify/controllers.d.ts +0 -1
  44. package/dist/commands/verify/controllers.js +213 -59
  45. package/dist/commands/verify/controllers.js.map +1 -1
  46. package/dist/commands/verify/dependency-integrity.d.ts +18 -0
  47. package/dist/commands/verify/dependency-integrity.js +147 -0
  48. package/dist/commands/verify/dependency-integrity.js.map +1 -0
  49. package/dist/commands/verify/react-build.d.ts +0 -1
  50. package/dist/commands/verify/react-build.js.map +1 -1
  51. package/dist/index.d.ts +0 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/lib/docs/contract-validator.d.ts +12 -0
  54. package/dist/lib/docs/contract-validator.js +38 -0
  55. package/dist/lib/docs/contract-validator.js.map +1 -0
  56. package/dist/lib/docs/contracts.d.ts +11 -0
  57. package/dist/lib/docs/contracts.js +241 -0
  58. package/dist/lib/docs/contracts.js.map +1 -0
  59. package/dist/lib/docs/discovery.d.ts +10 -0
  60. package/dist/lib/docs/discovery.js +91 -0
  61. package/dist/lib/docs/discovery.js.map +1 -0
  62. package/dist/lib/docs/generator.d.ts +14 -0
  63. package/dist/lib/docs/generator.js +450 -0
  64. package/dist/lib/docs/generator.js.map +1 -0
  65. package/dist/lib/docs/source-extractor.d.ts +2 -0
  66. package/dist/lib/docs/source-extractor.js +209 -0
  67. package/dist/lib/docs/source-extractor.js.map +1 -0
  68. package/dist/lib/docs/types.d.ts +77 -0
  69. package/dist/lib/docs/types.js +2 -0
  70. package/dist/lib/docs/types.js.map +1 -0
  71. package/dist/lib/upstream/repo-utils.d.ts +0 -1
  72. package/dist/lib/upstream/repo-utils.js.map +1 -1
  73. package/dist/lib/upstream/sync-compatibility.d.ts +0 -1
  74. package/dist/lib/upstream/sync-compatibility.js.map +1 -1
  75. package/dist/lib/upstream/sync-constants.d.ts +35 -7
  76. package/dist/lib/upstream/sync-constants.js +40 -11
  77. package/dist/lib/upstream/sync-constants.js.map +1 -1
  78. package/dist/lib/upstream/sync-controllers-strategy.d.ts +0 -1
  79. package/dist/lib/upstream/sync-controllers-strategy.js +14 -4
  80. package/dist/lib/upstream/sync-controllers-strategy.js.map +1 -1
  81. package/dist/lib/upstream/sync-demo-metadata.d.ts +0 -1
  82. package/dist/lib/upstream/sync-demo-metadata.js +1 -1
  83. package/dist/lib/upstream/sync-demo-metadata.js.map +1 -1
  84. package/dist/lib/upstream/sync-entry-discovery.d.ts +3 -0
  85. package/dist/lib/upstream/sync-entry-discovery.js +23 -0
  86. package/dist/lib/upstream/sync-entry-discovery.js.map +1 -0
  87. package/dist/lib/upstream/sync-externals.d.ts +7 -7
  88. package/dist/lib/upstream/sync-externals.js +51 -35
  89. package/dist/lib/upstream/sync-externals.js.map +1 -1
  90. package/dist/lib/upstream/sync-filesystem.d.ts +0 -1
  91. package/dist/lib/upstream/sync-filesystem.js.map +1 -1
  92. package/dist/lib/upstream/sync-imports.d.ts +82 -62
  93. package/dist/lib/upstream/sync-imports.js +658 -185
  94. package/dist/lib/upstream/sync-imports.js.map +1 -1
  95. package/dist/lib/upstream/sync-package-json.d.ts +0 -1
  96. package/dist/lib/upstream/sync-package-json.js +7 -0
  97. package/dist/lib/upstream/sync-package-json.js.map +1 -1
  98. package/dist/lib/upstream/sync-package-manager.d.ts +14 -4
  99. package/dist/lib/upstream/sync-package-manager.js +408 -72
  100. package/dist/lib/upstream/sync-package-manager.js.map +1 -1
  101. package/dist/lib/upstream/sync-pielib-strategy.d.ts +2 -1
  102. package/dist/lib/upstream/sync-pielib-strategy.js +228 -110
  103. package/dist/lib/upstream/sync-pielib-strategy.js.map +1 -1
  104. package/dist/lib/upstream/sync-presets.d.ts +53 -0
  105. package/dist/lib/upstream/sync-presets.js +203 -0
  106. package/dist/lib/upstream/sync-presets.js.map +1 -0
  107. package/dist/lib/upstream/sync-react-strategy.d.ts +6 -1
  108. package/dist/lib/upstream/sync-react-strategy.js +342 -134
  109. package/dist/lib/upstream/sync-react-strategy.js.map +1 -1
  110. package/dist/lib/upstream/sync-source-edit.d.ts +14 -0
  111. package/dist/lib/upstream/sync-source-edit.js +89 -0
  112. package/dist/lib/upstream/sync-source-edit.js.map +1 -0
  113. package/dist/lib/upstream/sync-source-tree.d.ts +22 -0
  114. package/dist/lib/upstream/sync-source-tree.js +99 -0
  115. package/dist/lib/upstream/sync-source-tree.js.map +1 -0
  116. package/dist/lib/upstream/sync-strategy.d.ts +1 -1
  117. package/dist/lib/upstream/sync-strategy.js.map +1 -1
  118. package/dist/lib/upstream/sync-summary.d.ts +0 -1
  119. package/dist/lib/upstream/sync-summary.js.map +1 -1
  120. package/dist/lib/upstream/sync-transforms.d.ts +13 -11
  121. package/dist/lib/upstream/sync-transforms.js +23 -17
  122. package/dist/lib/upstream/sync-transforms.js.map +1 -1
  123. package/dist/lib/upstream/sync-vite-config.d.ts +13 -1
  124. package/dist/lib/upstream/sync-vite-config.js +65 -46
  125. package/dist/lib/upstream/sync-vite-config.js.map +1 -1
  126. package/dist/utils/compatibility.d.ts +13 -1
  127. package/dist/utils/compatibility.js +143 -2
  128. package/dist/utils/compatibility.js.map +1 -1
  129. package/dist/utils/conversion.d.ts +0 -1
  130. package/dist/utils/conversion.js.map +1 -1
  131. package/dist/utils/dependency-integrity.d.ts +37 -0
  132. package/dist/utils/dependency-integrity.js +557 -0
  133. package/dist/utils/dependency-integrity.js.map +1 -0
  134. package/dist/utils/git.d.ts +0 -1
  135. package/dist/utils/git.js.map +1 -1
  136. package/dist/utils/logger.d.ts +0 -1
  137. package/dist/utils/logger.js.map +1 -1
  138. package/dist/utils/package-json.d.ts +0 -1
  139. package/dist/utils/package-json.js.map +1 -1
  140. package/package.json +24 -12
  141. package/.compatibility/report.json +0 -2491
  142. package/.turbo/turbo-build.log +0 -1
  143. package/CHANGELOG.md +0 -8
  144. package/REFACTORING.md +0 -131
  145. package/bin/dev.js +0 -5
  146. package/dist/commands/dev/demo/index.d.ts.map +0 -1
  147. package/dist/commands/dev/test-bundler.d.ts.map +0 -1
  148. package/dist/commands/mathquill/migrate.d.ts +0 -20
  149. package/dist/commands/mathquill/migrate.d.ts.map +0 -1
  150. package/dist/commands/mathquill/migrate.js +0 -240
  151. package/dist/commands/mathquill/migrate.js.map +0 -1
  152. package/dist/commands/mathquill/status.d.ts +0 -20
  153. package/dist/commands/mathquill/status.d.ts.map +0 -1
  154. package/dist/commands/mathquill/status.js +0 -231
  155. package/dist/commands/mathquill/status.js.map +0 -1
  156. package/dist/commands/packages/create-controllers.d.ts.map +0 -1
  157. package/dist/commands/packages/create-lib-react.d.ts.map +0 -1
  158. package/dist/commands/packages/create-react-elements.d.ts.map +0 -1
  159. package/dist/commands/packages/enable-publishing.d.ts.map +0 -1
  160. package/dist/commands/packages/init-synced-elements.d.ts.map +0 -1
  161. package/dist/commands/upstream/analyze-esm.d.ts.map +0 -1
  162. package/dist/commands/upstream/check.d.ts.map +0 -1
  163. package/dist/commands/upstream/deps.d.ts.map +0 -1
  164. package/dist/commands/upstream/sync-mathquill.d.ts +0 -12
  165. package/dist/commands/upstream/sync-mathquill.d.ts.map +0 -1
  166. package/dist/commands/upstream/sync-mathquill.js +0 -97
  167. package/dist/commands/upstream/sync-mathquill.js.map +0 -1
  168. package/dist/commands/upstream/sync.d.ts.map +0 -1
  169. package/dist/commands/upstream/track.d.ts.map +0 -1
  170. package/dist/commands/upstream/update.d.ts.map +0 -1
  171. package/dist/commands/verify/controllers.d.ts.map +0 -1
  172. package/dist/commands/verify/react-build.d.ts.map +0 -1
  173. package/dist/index.d.ts.map +0 -1
  174. package/dist/lib/mathquill/extension-generator.d.ts +0 -9
  175. package/dist/lib/mathquill/extension-generator.d.ts.map +0 -1
  176. package/dist/lib/mathquill/extension-generator.js +0 -107
  177. package/dist/lib/mathquill/extension-generator.js.map +0 -1
  178. package/dist/lib/mathquill/khan-extractor.d.ts +0 -16
  179. package/dist/lib/mathquill/khan-extractor.d.ts.map +0 -1
  180. package/dist/lib/mathquill/khan-extractor.js +0 -148
  181. package/dist/lib/mathquill/khan-extractor.js.map +0 -1
  182. package/dist/lib/mathquill/learnosity-extractor.d.ts +0 -15
  183. package/dist/lib/mathquill/learnosity-extractor.d.ts.map +0 -1
  184. package/dist/lib/mathquill/learnosity-extractor.js +0 -170
  185. package/dist/lib/mathquill/learnosity-extractor.js.map +0 -1
  186. package/dist/lib/mathquill/migration-orchestrator.d.ts +0 -77
  187. package/dist/lib/mathquill/migration-orchestrator.d.ts.map +0 -1
  188. package/dist/lib/mathquill/migration-orchestrator.js +0 -391
  189. package/dist/lib/mathquill/migration-orchestrator.js.map +0 -1
  190. package/dist/lib/mathquill/package-updater.d.ts +0 -9
  191. package/dist/lib/mathquill/package-updater.d.ts.map +0 -1
  192. package/dist/lib/mathquill/package-updater.js +0 -103
  193. package/dist/lib/mathquill/package-updater.js.map +0 -1
  194. package/dist/lib/mathquill/pie-extractor.d.ts +0 -14
  195. package/dist/lib/mathquill/pie-extractor.d.ts.map +0 -1
  196. package/dist/lib/mathquill/pie-extractor.js +0 -167
  197. package/dist/lib/mathquill/pie-extractor.js.map +0 -1
  198. package/dist/lib/upstream/repo-utils.d.ts.map +0 -1
  199. package/dist/lib/upstream/sync-compatibility.d.ts.map +0 -1
  200. package/dist/lib/upstream/sync-constants.d.ts.map +0 -1
  201. package/dist/lib/upstream/sync-controllers-strategy.d.ts.map +0 -1
  202. package/dist/lib/upstream/sync-demo-metadata.d.ts.map +0 -1
  203. package/dist/lib/upstream/sync-dev-exports.d.ts +0 -13
  204. package/dist/lib/upstream/sync-dev-exports.d.ts.map +0 -1
  205. package/dist/lib/upstream/sync-dev-exports.js +0 -115
  206. package/dist/lib/upstream/sync-dev-exports.js.map +0 -1
  207. package/dist/lib/upstream/sync-externals.d.ts.map +0 -1
  208. package/dist/lib/upstream/sync-filesystem.d.ts.map +0 -1
  209. package/dist/lib/upstream/sync-imports.d.ts.map +0 -1
  210. package/dist/lib/upstream/sync-mathquill.d.ts +0 -19
  211. package/dist/lib/upstream/sync-mathquill.d.ts.map +0 -1
  212. package/dist/lib/upstream/sync-mathquill.js +0 -160
  213. package/dist/lib/upstream/sync-mathquill.js.map +0 -1
  214. package/dist/lib/upstream/sync-package-json.d.ts.map +0 -1
  215. package/dist/lib/upstream/sync-package-manager.d.ts.map +0 -1
  216. package/dist/lib/upstream/sync-pielib-strategy.d.ts.map +0 -1
  217. package/dist/lib/upstream/sync-react-strategy.d.ts.map +0 -1
  218. package/dist/lib/upstream/sync-strategy.d.ts.map +0 -1
  219. package/dist/lib/upstream/sync-summary.d.ts.map +0 -1
  220. package/dist/lib/upstream/sync-transforms.d.ts.map +0 -1
  221. package/dist/lib/upstream/sync-vite-config.d.ts.map +0 -1
  222. package/dist/utils/compatibility.d.ts.map +0 -1
  223. package/dist/utils/conversion.d.ts.map +0 -1
  224. package/dist/utils/git.d.ts.map +0 -1
  225. package/dist/utils/logger.d.ts.map +0 -1
  226. package/dist/utils/package-json.d.ts.map +0 -1
  227. package/packages/cli/bin/dev.js +0 -5
  228. package/packages/cli/bin/run.js +0 -5
  229. package/src/commands/dev/demo/index.ts +0 -313
  230. package/src/commands/dev/test-bundler.ts +0 -345
  231. package/src/commands/mathquill/migrate.ts +0 -269
  232. package/src/commands/mathquill/status.ts +0 -267
  233. package/src/commands/packages/create-controllers.ts +0 -127
  234. package/src/commands/packages/create-lib-react.ts +0 -279
  235. package/src/commands/packages/create-react-elements.ts +0 -306
  236. package/src/commands/packages/enable-publishing.ts +0 -161
  237. package/src/commands/packages/init-synced-elements.ts +0 -259
  238. package/src/commands/upstream/README.md +0 -444
  239. package/src/commands/upstream/analyze-esm.ts +0 -1973
  240. package/src/commands/upstream/check.ts +0 -228
  241. package/src/commands/upstream/deps.ts +0 -222
  242. package/src/commands/upstream/sync-mathquill.ts +0 -108
  243. package/src/commands/upstream/sync.ts +0 -687
  244. package/src/commands/upstream/track.ts +0 -145
  245. package/src/commands/upstream/update.ts +0 -110
  246. package/src/commands/verify/controllers.ts +0 -166
  247. package/src/commands/verify/react-build.ts +0 -171
  248. package/src/index.ts +0 -1
  249. package/src/lib/mathquill/extension-generator.ts +0 -111
  250. package/src/lib/mathquill/khan-extractor.ts +0 -175
  251. package/src/lib/mathquill/learnosity-extractor.ts +0 -198
  252. package/src/lib/mathquill/migration-orchestrator.ts +0 -513
  253. package/src/lib/mathquill/package-updater.ts +0 -120
  254. package/src/lib/mathquill/pie-extractor.ts +0 -194
  255. package/src/lib/upstream/repo-utils.ts +0 -18
  256. package/src/lib/upstream/sync-compatibility.ts +0 -89
  257. package/src/lib/upstream/sync-constants.ts +0 -77
  258. package/src/lib/upstream/sync-controllers-strategy.ts +0 -291
  259. package/src/lib/upstream/sync-demo-metadata.ts +0 -199
  260. package/src/lib/upstream/sync-dev-exports.ts +0 -151
  261. package/src/lib/upstream/sync-externals.ts +0 -159
  262. package/src/lib/upstream/sync-filesystem.ts +0 -87
  263. package/src/lib/upstream/sync-imports.ts +0 -992
  264. package/src/lib/upstream/sync-mathquill.ts +0 -194
  265. package/src/lib/upstream/sync-package-json.ts +0 -86
  266. package/src/lib/upstream/sync-package-manager.ts +0 -694
  267. package/src/lib/upstream/sync-pielib-strategy.ts +0 -441
  268. package/src/lib/upstream/sync-react-strategy.ts +0 -945
  269. package/src/lib/upstream/sync-strategy.ts +0 -61
  270. package/src/lib/upstream/sync-summary.ts +0 -194
  271. package/src/lib/upstream/sync-transforms.ts +0 -154
  272. package/src/lib/upstream/sync-vite-config.ts +0 -243
  273. package/src/types/es-module-lexer.d.ts +0 -4
  274. package/src/utils/compatibility.ts +0 -182
  275. package/src/utils/conversion.ts +0 -153
  276. package/src/utils/git.ts +0 -63
  277. package/src/utils/logger.ts +0 -69
  278. package/src/utils/package-json.ts +0 -43
  279. package/tests/sync-package-manager.test.ts +0 -112
  280. package/tests/transform-assign-props.test.ts +0 -101
  281. package/tsconfig.json +0 -15
@@ -1,1973 +0,0 @@
1
- import { Command, Flags } from '@oclif/core';
2
- import { Logger } from '../../utils/logger.js';
3
- import { loadPackageJson, type PackageJson } from '../../utils/package-json.js';
4
- import { existsSync } from 'node:fs';
5
- import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
6
- import { join } from 'node:path';
7
- import type {
8
- CompatibilityReport,
9
- ElementDetail,
10
- EsmPackageValidation,
11
- EsmRuntimeValidationResult,
12
- EsmValidationResult,
13
- } from '../../utils/compatibility.js';
14
- import { assertReposExist } from '../../lib/upstream/repo-utils.js';
15
-
16
- // PIE elements that are actively used in pie-elements-ng
17
- // Only these elements will be analyzed
18
- const ACTIVE_ELEMENTS = [
19
- 'categorize',
20
- 'charting',
21
- 'complex-rubric',
22
- 'drag-in-the-blank',
23
- 'drawing-response',
24
- 'ebsr',
25
- 'explicit-constructed-response',
26
- 'extended-text-entry',
27
- 'fraction-model',
28
- 'graphing',
29
- 'graphing-solution-set',
30
- 'hotspot',
31
- 'image-cloze-association',
32
- 'inline-dropdown',
33
- 'likert',
34
- 'match',
35
- 'match-list',
36
- 'math-inline',
37
- 'math-templated',
38
- 'matrix',
39
- 'multi-trait-rubric',
40
- 'multiple-choice',
41
- 'number-line',
42
- 'passage',
43
- 'placement-ordering',
44
- 'rubric',
45
- 'select-text',
46
- ];
47
-
48
- // Known ESM blockers - packages that prevent ESM builds
49
- const ESM_BLOCKERS = [
50
- // Old Slate editor (v0.36.x) - not ESM compatible
51
- { pattern: /^slate@0\.[0-4]\d/, reason: 'Slate v0.x is not ESM compatible' },
52
- { pattern: /^slate-/, reason: 'Slate v0.x plugins are not ESM compatible' },
53
-
54
- // Known CommonJS-only packages (add more as discovered)
55
- { pattern: /^enzyme/, reason: 'Enzyme is not ESM compatible' },
56
- ];
57
-
58
- // PIE lib packages that should always be included if compatible
59
- // These are packages that may be imported directly in code (e.g., controllers)
60
- // but not declared in package.json dependencies
61
- const ALWAYS_INCLUDE_PIE_LIB = [
62
- 'controller-utils', // Used in controller code via direct imports
63
- ];
64
-
65
- export default class AnalyzeEsm extends Command {
66
- static override description = 'Analyze PIE elements for ESM compatibility';
67
-
68
- static override examples = [
69
- '<%= config.bin %> <%= command.id %>',
70
- '<%= config.bin %> <%= command.id %> --verbose',
71
- '<%= config.bin %> <%= command.id %> --output=./.compatibility/custom-report.json',
72
- ];
73
-
74
- static override flags = {
75
- verbose: Flags.boolean({
76
- char: 'v',
77
- description: 'Show detailed analysis output',
78
- default: false,
79
- }),
80
- 'pie-elements': Flags.string({
81
- description: 'Path to pie-elements repo',
82
- default: '../pie-elements',
83
- }),
84
- 'pie-lib': Flags.string({
85
- description: 'Path to pie-lib repo',
86
- default: '../pie-lib',
87
- }),
88
- output: Flags.string({
89
- description: 'Output JSON file path',
90
- default: './.compatibility/report.json',
91
- }),
92
- 'include-dev-deps': Flags.boolean({
93
- description:
94
- 'Include devDependencies in blocker analysis (usually unnecessary since pie-elements-ng does not sync upstream tests)',
95
- default: false,
96
- }),
97
- 'validate-esm-player': Flags.boolean({
98
- description: 'Validate ESM player compatibility (package.json structure, exports).',
99
- default: true,
100
- allowNo: true,
101
- }),
102
- 'runtime-cdn-base-url': Flags.string({
103
- description: 'CDN base URL to use for runtime probes (e.g. https://esm.sh)',
104
- default: 'https://esm.sh',
105
- }),
106
- 'runtime-local-pie-elements-path': Flags.string({
107
- description: 'Path to sibling pie-elements repo used for local PIE resolution during probes.',
108
- default: '../pie-elements',
109
- }),
110
- 'runtime-local-pie-lib-path': Flags.string({
111
- description: 'Path to sibling pie-lib repo used for local PIE resolution during probes.',
112
- default: '../pie-lib',
113
- }),
114
- 'runtime-timeout-ms': Flags.integer({
115
- description: 'Timeout for each runtime probe request (ms)',
116
- default: 6000,
117
- }),
118
- 'runtime-concurrency': Flags.integer({
119
- description: 'Max concurrent runtime probes',
120
- default: 8,
121
- }),
122
- 'runtime-max-depth': Flags.integer({
123
- description: 'Max import graph depth for the deep runtime probe',
124
- default: 6,
125
- }),
126
- 'runtime-max-modules': Flags.integer({
127
- description: 'Max total modules fetched per element for the deep runtime probe',
128
- default: 250,
129
- }),
130
- 'runtime-cache-file': Flags.string({
131
- description:
132
- 'Optional JSON cache file path for runtime probe results (speeds up repeated runs)',
133
- default: './.cache/analyze-esm.runtime-cache.json',
134
- }),
135
- };
136
-
137
- private logger = new Logger();
138
-
139
- public async run(): Promise<void> {
140
- const { flags } = await this.parse(AnalyzeEsm);
141
-
142
- this.logger = new Logger(flags.verbose);
143
-
144
- if (flags.verbose) {
145
- this.logger.section('🔍 Analyzing ESM Compatibility');
146
- this.logger.info(` PIE Elements: ${flags['pie-elements']}`);
147
- this.logger.info(` PIE Lib: ${flags['pie-lib']}\n`);
148
- }
149
-
150
- // Verify repos exist
151
- try {
152
- assertReposExist([
153
- { label: 'pie-elements', path: flags['pie-elements'] },
154
- { label: 'pie-lib', path: flags['pie-lib'] },
155
- ]);
156
- } catch (error) {
157
- this.error(error instanceof Error ? error.message : String(error));
158
- }
159
-
160
- const report = await this.analyzeCompatibility(
161
- flags['pie-elements'],
162
- flags['pie-lib'],
163
- flags.verbose,
164
- flags['include-dev-deps'],
165
- flags['validate-esm-player'],
166
- flags['runtime-cdn-base-url'],
167
- flags['runtime-local-pie-elements-path'],
168
- flags['runtime-local-pie-lib-path'],
169
- flags['runtime-timeout-ms'],
170
- flags['runtime-concurrency'],
171
- flags['runtime-max-depth'],
172
- flags['runtime-max-modules'],
173
- flags['runtime-cache-file']
174
- );
175
-
176
- // Save JSON report
177
- const outputDir = join(flags.output, '..');
178
- await mkdir(outputDir, { recursive: true });
179
- await writeFile(flags.output, JSON.stringify(report, null, 2), 'utf-8');
180
-
181
- // Print summary
182
- this.printReport(report, flags.output);
183
- }
184
-
185
- private async analyzeCompatibility(
186
- pieElementsPath: string,
187
- pieLibPath: string,
188
- verbose: boolean,
189
- includeDevDeps: boolean,
190
- validateEsmPlayer: boolean,
191
- runtimeCdnBaseUrl: string,
192
- runtimeLocalPieElementsPath: string,
193
- runtimeLocalPieLibPath: string,
194
- runtimeTimeoutMs: number,
195
- runtimeConcurrency: number,
196
- runtimeMaxDepth: number,
197
- runtimeMaxModules: number,
198
- runtimeCacheFile: string
199
- ): Promise<CompatibilityReport> {
200
- if (!verbose) {
201
- this.logger.progressStart('Analyzing ESM compatibility...');
202
- }
203
-
204
- const report: CompatibilityReport = {
205
- elements: [],
206
- pieLibPackages: [],
207
- blockedElements: {},
208
- esmPlayerReady: [],
209
- esmValidation: {},
210
- esmPlayerValidationEnabled: validateEsmPlayer,
211
- esmRuntimeValidationEnabled: true,
212
- esmRuntimeCdnBaseUrl: runtimeCdnBaseUrl,
213
- esmRuntimeValidation: {},
214
- elementDetails: {},
215
- pieLibDetails: {},
216
- lastAnalyzed: new Date().toISOString(),
217
- summary: {
218
- totalElements: 0,
219
- compatibleElements: 0,
220
- blockedElements: 0,
221
- esmPlayerReady: 0,
222
- esmRuntimeReady: 0,
223
- totalPieLibPackages: 0,
224
- compatiblePieLibPackages: 0,
225
- },
226
- };
227
-
228
- // Cache for pie-lib compatibility checks
229
- const pieLibCache = new Map<
230
- string,
231
- {
232
- compatible: boolean;
233
- blockers: string[];
234
- }
235
- >();
236
-
237
- // Get all element packages
238
- const elementsDir = join(pieElementsPath, 'packages');
239
- const allElements = await readdir(elementsDir);
240
-
241
- // Filter to only active elements used in pie-elements-ng
242
- const elements = allElements.filter((name) => ACTIVE_ELEMENTS.includes(name));
243
-
244
- report.summary.totalElements = elements.length;
245
-
246
- if (verbose) {
247
- this.logger.info(`📊 Analyzing ${elements.length} elements...\n`);
248
- }
249
-
250
- // Track which elements use which pie-lib packages
251
- const pieLibUsage = new Map<string, Set<string>>();
252
-
253
- // Analyze each element
254
- for (const element of elements.sort()) {
255
- const result = await this.checkElementCompatibility(
256
- element,
257
- pieElementsPath,
258
- pieLibPath,
259
- pieLibCache,
260
- verbose,
261
- includeDevDeps
262
- );
263
-
264
- // Check subdirectories separately
265
- const srcCheck = await this.checkSubdirectoryCompatibility(
266
- element,
267
- 'src',
268
- pieElementsPath,
269
- pieLibPath,
270
- pieLibCache,
271
- verbose,
272
- includeDevDeps
273
- );
274
-
275
- const configureCheck = await this.checkSubdirectoryCompatibility(
276
- element,
277
- 'configure',
278
- pieElementsPath,
279
- pieLibPath,
280
- pieLibCache,
281
- verbose,
282
- includeDevDeps
283
- );
284
-
285
- const controllerCheck = await this.checkSubdirectoryCompatibility(
286
- element,
287
- 'controller',
288
- pieElementsPath,
289
- pieLibPath,
290
- pieLibCache,
291
- verbose,
292
- includeDevDeps
293
- );
294
-
295
- // Add subdirectory results to element details
296
- if (srcCheck !== null) {
297
- result.studentUI = srcCheck;
298
- }
299
- if (configureCheck !== null) {
300
- result.configure = configureCheck;
301
- }
302
- if (controllerCheck !== null) {
303
- result.controller = controllerCheck;
304
- }
305
-
306
- report.elementDetails[element] = result;
307
-
308
- if (result.compatible) {
309
- report.elements.push(element);
310
- report.summary.compatibleElements++;
311
-
312
- // Track pie-lib usage (including transitive deps)
313
- // For example: if element uses text-select, and text-select uses style-utils,
314
- // we need to sync both text-select AND style-utils
315
- const allPieLibDeps = await this.collectAllTransitivePieLibDeps(
316
- result.pieLibDeps,
317
- pieLibPath,
318
- includeDevDeps
319
- );
320
-
321
- for (const pieLibDep of allPieLibDeps) {
322
- if (!pieLibUsage.has(pieLibDep)) {
323
- pieLibUsage.set(pieLibDep, new Set());
324
- }
325
- const usageSet = pieLibUsage.get(pieLibDep);
326
- if (usageSet) {
327
- usageSet.add(element);
328
- }
329
- }
330
- } else {
331
- report.blockedElements[element] = result.blockers;
332
- report.summary.blockedElements++;
333
- }
334
- }
335
-
336
- // Second pass: Check element-to-element dependencies
337
- // Elements can only be compatible if all their element dependencies are also compatible
338
- if (verbose) {
339
- this.logger.info('\n🔗 Checking element-to-element dependencies...\n');
340
- }
341
-
342
- let changed = true;
343
- while (changed) {
344
- changed = false;
345
- for (const element of elements) {
346
- const details = report.elementDetails[element];
347
- if (!details?.compatible) continue; // Already blocked, skip
348
-
349
- // Check if this element depends on any blocked elements
350
- for (const depElement of details.pieElementDeps) {
351
- const depDetails = report.elementDetails[depElement];
352
-
353
- if (!depDetails?.compatible) {
354
- // This element depends on a blocked element
355
- const blocker = `@pie-element/${depElement} - dependency is not ESM compatible`;
356
- details.blockers.push(blocker);
357
- details.compatible = false;
358
-
359
- // Move from compatible to blocked
360
- const idx = report.elements.indexOf(element);
361
- if (idx !== -1) {
362
- report.elements.splice(idx, 1);
363
- report.summary.compatibleElements--;
364
- }
365
- report.blockedElements[element] = details.blockers;
366
- report.summary.blockedElements++;
367
-
368
- changed = true;
369
-
370
- if (verbose) {
371
- this.logger.info(` ❌ ${element}: depends on blocked element ${depElement}`);
372
- }
373
- break;
374
- }
375
- }
376
- }
377
- }
378
-
379
- // Build pie-lib report
380
- for (const [pieLibPkg, usedBy] of pieLibUsage.entries()) {
381
- const check = pieLibCache.get(pieLibPkg);
382
- if (check?.compatible) {
383
- report.pieLibPackages.push(pieLibPkg);
384
- report.pieLibDetails[pieLibPkg] = {
385
- compatible: true,
386
- usedBy: Array.from(usedBy),
387
- blockers: [],
388
- };
389
- } else if (check) {
390
- report.pieLibDetails[pieLibPkg] = {
391
- compatible: false,
392
- usedBy: Array.from(usedBy),
393
- blockers: check.blockers,
394
- };
395
- }
396
- }
397
-
398
- // Add always-include pie-lib packages if they're compatible
399
- for (const pieLibPkg of ALWAYS_INCLUDE_PIE_LIB) {
400
- // Skip if already in the report
401
- if (pieLibUsage.has(pieLibPkg)) {
402
- continue;
403
- }
404
-
405
- // Check if this package exists and is compatible
406
- let check = pieLibCache.get(pieLibPkg);
407
- if (!check) {
408
- check = await this.checkPieLibCompatibility(
409
- pieLibPkg,
410
- pieLibPath,
411
- verbose,
412
- includeDevDeps,
413
- pieLibCache
414
- );
415
- pieLibCache.set(pieLibPkg, check);
416
- }
417
-
418
- if (check.compatible) {
419
- report.pieLibPackages.push(pieLibPkg);
420
- report.pieLibDetails[pieLibPkg] = {
421
- compatible: true,
422
- usedBy: ['(always included)'], // Mark as always included
423
- blockers: [],
424
- };
425
- if (verbose) {
426
- this.logger.info(` ✅ ${pieLibPkg}: always included (compatible)`);
427
- }
428
- } else if (verbose) {
429
- this.logger.info(` ⏭️ ${pieLibPkg}: skipped (not compatible)`);
430
- }
431
- }
432
-
433
- report.summary.totalPieLibPackages = pieLibUsage.size + ALWAYS_INCLUDE_PIE_LIB.length;
434
- report.summary.compatiblePieLibPackages = report.pieLibPackages.length;
435
-
436
- // Identify student-UI-only elements (blocked overall but student UI is compatible)
437
- report.studentUIOnly = [];
438
- for (const element of elements) {
439
- const details = report.elementDetails[element];
440
- if (!details?.compatible && details?.studentUI?.compatible) {
441
- report.studentUIOnly.push(element);
442
- }
443
- }
444
- report.summary.studentUIOnlyElements = report.studentUIOnly.length;
445
-
446
- if (verbose && report.studentUIOnly.length > 0) {
447
- this.logger.info(
448
- `\n📱 Found ${report.studentUIOnly.length} elements with student-UI-only support:\n`
449
- );
450
- for (const element of report.studentUIOnly) {
451
- this.logger.info(` ${element}`);
452
- }
453
- }
454
-
455
- // ESM player validation (only for compatible elements)
456
- if (validateEsmPlayer) {
457
- if (verbose) {
458
- this.logger.info('\n🎮 Validating ESM player compatibility...\n');
459
- }
460
-
461
- for (const element of report.elements) {
462
- const validation = await this.validateEsmPlayerCompatibility(
463
- element,
464
- pieElementsPath,
465
- verbose
466
- );
467
-
468
- report.esmValidation[element] = validation;
469
-
470
- if (validation.compatible) {
471
- report.esmPlayerReady.push(element);
472
- report.summary.esmPlayerReady++;
473
- }
474
- }
475
- }
476
-
477
- // Runtime-ish CDN probes (production-like signal)
478
- const base = runtimeCdnBaseUrl.replace(/\/+$/, '');
479
-
480
- // Probe all CommonJS-free elements by default.
481
- // Rationale: "ESM ready in production" is primarily about whether the CDN can resolve
482
- // the package + transitives into browser-loadable ESM, not whether upstream package.json
483
- // has ideal exports metadata (which we often rewrite during sync).
484
- const elementsToProbe = report.elements.slice();
485
-
486
- const cache = await this.loadRuntimeCache(runtimeCacheFile);
487
-
488
- if (verbose) {
489
- this.logger.info(
490
- `\n🌐 Probing ESM runtime readiness via CDN (${elementsToProbe.length} elements, concurrency=${runtimeConcurrency})...\n`
491
- );
492
- this.logger.info(` CDN base: ${base}\n`);
493
- this.logger.info(
494
- ` Mode: deep (maxDepth=${runtimeMaxDepth}, maxModules=${runtimeMaxModules})\n`
495
- );
496
- this.logger.info(
497
- ` Local PIE resolution: enabled (pie-elements=${runtimeLocalPieElementsPath}, pie-lib=${runtimeLocalPieLibPath})\n`
498
- );
499
- } else {
500
- this.logger.info(
501
- `🌐 Probing ESM runtime readiness via CDN (${elementsToProbe.length} elements, concurrency=${runtimeConcurrency})...`
502
- );
503
- }
504
-
505
- const results = await this.probeElementsRuntime({
506
- elements: elementsToProbe,
507
- pieElementsPath,
508
- cdnBaseUrl: base,
509
- localPieEnabled: true,
510
- localPieElementsPath: runtimeLocalPieElementsPath,
511
- localPieLibPath: runtimeLocalPieLibPath,
512
- timeoutMs: runtimeTimeoutMs,
513
- concurrency: runtimeConcurrency,
514
- deep: true,
515
- maxDepth: runtimeMaxDepth,
516
- maxModules: runtimeMaxModules,
517
- cache,
518
- verbose,
519
- });
520
-
521
- let okCount = 0;
522
- for (const [element, result] of Object.entries(results)) {
523
- report.esmRuntimeValidation ??= {};
524
- report.esmRuntimeValidation[element] = result;
525
- if (result.compatible) okCount++;
526
- }
527
- report.summary.esmRuntimeReady = okCount;
528
-
529
- await this.saveRuntimeCache(runtimeCacheFile, cache);
530
-
531
- if (!verbose) {
532
- this.logger.info(` Runtime-ready: ${okCount}/${elementsToProbe.length}\n`);
533
- }
534
-
535
- if (!verbose) {
536
- this.logger.progressComplete();
537
- this.logger.info('');
538
- this.logger.success(`${report.summary.compatibleElements} elements compatible`);
539
- if (report.summary.blockedElements > 0) {
540
- this.logger.warn(`${report.summary.blockedElements} elements blocked`);
541
- }
542
- if (report.summary.compatiblePieLibPackages > 0) {
543
- this.logger.info(`${report.summary.compatiblePieLibPackages} @pie-lib packages compatible`);
544
- }
545
- if (report.summary.totalPieLibPackages - report.summary.compatiblePieLibPackages > 0) {
546
- this.logger.warn(
547
- `${report.summary.totalPieLibPackages - report.summary.compatiblePieLibPackages} @pie-lib packages blocked`
548
- );
549
- }
550
- this.logger.info('');
551
- this.logger.info('Run with --verbose to see detailed blocker information');
552
- this.logger.info('');
553
- }
554
-
555
- return report;
556
- }
557
-
558
- private async loadRuntimeCache(
559
- filePath: string
560
- ): Promise<Record<string, EsmRuntimeValidationResult>> {
561
- try {
562
- if (!existsSync(filePath)) return {};
563
- const raw = await readFile(filePath, 'utf-8');
564
- return JSON.parse(raw) as Record<string, EsmRuntimeValidationResult>;
565
- } catch {
566
- return {};
567
- }
568
- }
569
-
570
- private async saveRuntimeCache(
571
- filePath: string,
572
- cache: Record<string, EsmRuntimeValidationResult>
573
- ): Promise<void> {
574
- try {
575
- const dir = filePath.includes('/') ? filePath.split('/').slice(0, -1).join('/') : '.';
576
- if (dir && dir !== '.' && !existsSync(dir)) {
577
- await mkdir(dir, { recursive: true });
578
- }
579
- await writeFile(filePath, JSON.stringify(cache, null, 2), 'utf-8');
580
- } catch {
581
- // Best-effort cache only.
582
- }
583
- }
584
-
585
- private async parseAsEsm(code: string): Promise<boolean> {
586
- try {
587
- const mod = (await import('es-module-lexer')) as unknown as {
588
- init: Promise<void>;
589
- parse: (source: string) => [unknown[], unknown];
590
- };
591
- await mod.init;
592
- mod.parse(code);
593
- return true;
594
- } catch {
595
- // Fallback: very light heuristic (still useful signal for obvious non-JS responses).
596
- return typeof code === 'string' && code.length > 0;
597
- }
598
- }
599
-
600
- private async getImportSpecifiers(code: string): Promise<string[]> {
601
- try {
602
- const mod = (await import('es-module-lexer')) as unknown as {
603
- init: Promise<void>;
604
- parse: (source: string) => [{ s: number; e: number }[], unknown];
605
- };
606
- await mod.init;
607
- const [imports] = mod.parse(code);
608
- const specs: string[] = [];
609
- for (const i of imports) {
610
- const spec = code.slice(i.s, i.e);
611
- if (spec) specs.push(spec);
612
- }
613
- return specs;
614
- } catch {
615
- return [];
616
- }
617
- }
618
-
619
- private resolveImportUrl(fromUrl: string, spec: string, cdnBaseUrl: string): string | null {
620
- if (!spec) return null;
621
- if (spec.startsWith('data:')) return null;
622
- if (spec.startsWith('http://') || spec.startsWith('https://')) return spec;
623
- if (spec.startsWith('//')) return `https:${spec}`;
624
- if (spec.startsWith('/')) return `${cdnBaseUrl.replace(/\/+$/, '')}${spec}`;
625
- if (spec.startsWith('./') || spec.startsWith('../')) return new URL(spec, fromUrl).toString();
626
- // Bare specifiers shouldn't typically appear in CDN output; best-effort map to base.
627
- return `${cdnBaseUrl.replace(/\/+$/, '')}/${spec}`;
628
- }
629
-
630
- private isPiePackageSpecifier(spec: string): boolean {
631
- return (
632
- spec.startsWith('@pie-lib/') ||
633
- spec.startsWith('@pie-element/') ||
634
- spec.startsWith('@pie-element/')
635
- );
636
- }
637
-
638
- private tryResolvePieToLocalFile(
639
- spec: string,
640
- pieElementsPath: string,
641
- pieLibPath: string
642
- ): string | null {
643
- // Map PIE package specifiers to sibling upstream source trees.
644
- // Supports:
645
- // - @pie-lib/<name>[/subpath] -> <pie-lib>/packages/<name>/src/...
646
- // - @pie-element/<name>[/subpath] -> <pie-elements>/packages/<name>/src/... (and controller/configure sources)
647
- const [scope, name, ...rest] = spec.split('/');
648
- const subpath = rest.join('/');
649
-
650
- if (scope === '@pie-lib') {
651
- const base = join(pieLibPath, 'packages', name, 'src');
652
- return this.resolveLocalEntrypoint(base, subpath);
653
- }
654
-
655
- if (scope === '@pie-element') {
656
- // Special-case known subpaths that live outside src/ in upstream.
657
- if (subpath === 'controller' || subpath.startsWith('controller/')) {
658
- const controllerSub = subpath === 'controller' ? '' : subpath.replace(/^controller\//, '');
659
- const base = join(pieElementsPath, 'packages', name, 'controller', 'src');
660
- return this.resolveLocalEntrypoint(base, controllerSub);
661
- }
662
- if (subpath === 'configure' || subpath.startsWith('configure/')) {
663
- const cfgSub = subpath === 'configure' ? '' : subpath.replace(/^configure\//, '');
664
- const base = join(pieElementsPath, 'packages', name, 'configure', 'src');
665
- return this.resolveLocalEntrypoint(base, cfgSub);
666
- }
667
-
668
- const base = join(pieElementsPath, 'packages', name, 'src');
669
- return this.resolveLocalEntrypoint(base, subpath);
670
- }
671
-
672
- return null;
673
- }
674
-
675
- private resolveLocalEntrypoint(srcBaseDir: string, subpath: string): string | null {
676
- const candidates: string[] = [];
677
- if (!subpath) {
678
- candidates.push(join(srcBaseDir, 'index.ts'));
679
- candidates.push(join(srcBaseDir, 'index.tsx'));
680
- candidates.push(join(srcBaseDir, 'index.js'));
681
- candidates.push(join(srcBaseDir, 'index.jsx'));
682
- } else {
683
- const normalized = subpath.replace(/^\/+/, '');
684
- // common convention: <subpath>/index.*
685
- candidates.push(join(srcBaseDir, normalized, 'index.ts'));
686
- candidates.push(join(srcBaseDir, normalized, 'index.tsx'));
687
- candidates.push(join(srcBaseDir, normalized, 'index.js'));
688
- candidates.push(join(srcBaseDir, normalized, 'index.jsx'));
689
- // direct file
690
- candidates.push(join(srcBaseDir, `${normalized}.ts`));
691
- candidates.push(join(srcBaseDir, `${normalized}.tsx`));
692
- candidates.push(join(srcBaseDir, `${normalized}.js`));
693
- candidates.push(join(srcBaseDir, `${normalized}.jsx`));
694
- }
695
-
696
- for (const c of candidates) {
697
- if (existsSync(c)) return `file://${c}`;
698
- }
699
- return null;
700
- }
701
-
702
- private resolveLocalImport(fromFileUrl: string, spec: string): string | null {
703
- // Supports extensionless TS/JS imports and index resolution.
704
- const fromPath = fromFileUrl.startsWith('file://')
705
- ? fromFileUrl.slice('file://'.length)
706
- : fromFileUrl;
707
- const baseDir = join(fromPath, '..');
708
- const tryPaths: string[] = [];
709
-
710
- const resolved = join(baseDir, spec);
711
- if (
712
- spec.endsWith('.ts') ||
713
- spec.endsWith('.tsx') ||
714
- spec.endsWith('.js') ||
715
- spec.endsWith('.jsx')
716
- ) {
717
- tryPaths.push(resolved);
718
- } else {
719
- tryPaths.push(resolved + '.ts');
720
- tryPaths.push(resolved + '.tsx');
721
- tryPaths.push(resolved + '.js');
722
- tryPaths.push(resolved + '.jsx');
723
- tryPaths.push(join(resolved, 'index.ts'));
724
- tryPaths.push(join(resolved, 'index.tsx'));
725
- tryPaths.push(join(resolved, 'index.js'));
726
- tryPaths.push(join(resolved, 'index.jsx'));
727
- }
728
-
729
- for (const p of tryPaths) {
730
- if (existsSync(p)) return `file://${p}`;
731
- }
732
- return null;
733
- }
734
-
735
- private getTsImportSpecifiers(code: string): string[] {
736
- // Minimal TS import extractor that handles import ... from "x" and export ... from "x".
737
- // We do not attempt full TS AST walking here to keep dependencies minimal.
738
- const specs = new Set<string>();
739
- const re = /\b(?:import|export)\s+(?:type\s+)?(?:[^'"]*?\s+from\s+)?['"]([^'"]+)['"]/g;
740
- for (const m of code.matchAll(re)) {
741
- if (m[1]) specs.add(m[1]);
742
- }
743
- // dynamic import("x")
744
- const reDyn = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
745
- for (const m of code.matchAll(reDyn)) {
746
- if (m[1]) specs.add(m[1]);
747
- }
748
- return Array.from(specs);
749
- }
750
-
751
- private async deepProbeFromUrl(opts: {
752
- entryUrl: string;
753
- cdnBaseUrl: string;
754
- timeoutMs: number;
755
- maxDepth: number;
756
- maxModules: number;
757
- logPrefix: string;
758
- localPieEnabled: boolean;
759
- localPieElementsPath: string;
760
- localPieLibPath: string;
761
- }): Promise<{
762
- ok: boolean;
763
- fetched: number;
764
- fetchedOk: number;
765
- firstFailingUrl?: string;
766
- errors: string[];
767
- }> {
768
- const {
769
- entryUrl,
770
- cdnBaseUrl,
771
- timeoutMs,
772
- maxDepth,
773
- maxModules,
774
- logPrefix,
775
- localPieEnabled,
776
- localPieElementsPath,
777
- localPieLibPath,
778
- } = opts;
779
- const visited = new Set<string>();
780
- const errors: string[] = [];
781
- let fetched = 0;
782
- let fetchedOk = 0;
783
- let firstFailingUrl: string | undefined;
784
- let lastLogAt = 0;
785
-
786
- const queue: Array<{ url: string; depth: number }> = [{ url: entryUrl, depth: 0 }];
787
- while (queue.length > 0) {
788
- const next = queue.shift();
789
- if (!next) break;
790
- if (next.depth > maxDepth) continue;
791
- if (visited.has(next.url)) continue;
792
- if (visited.size >= maxModules) break;
793
- visited.add(next.url);
794
-
795
- fetched++;
796
- // Periodic progress logging (deep probe can take a while)
797
- // Log at most every ~2s, and also on notable milestones.
798
- const now = Date.now();
799
- if (fetched === 1 || fetched % 50 === 0 || now - lastLogAt > 2000) {
800
- lastLogAt = now;
801
- this.logger.info(
802
- ` ⏳ ${logPrefix}: fetched ${fetched}/${maxModules} modules (ok ${fetchedOk}), queue ${queue.length}, depth ${next.depth}/${maxDepth}`
803
- );
804
- }
805
- const res = await this.fetchTextWithTimeout(next.url, timeoutMs);
806
- if (!res.ok) {
807
- const baseMsg = `fetch ${next.url} -> ${res.status || 'error'} (${res.text.slice(0, 120)})`;
808
- const jsxHint =
809
- typeof res.text === 'string' &&
810
- res.text.includes('The JSX syntax extension is not currently enabled')
811
- ? ' Hint: CDN failed parsing JSX in a .js/.mjs file. This usually means the published package contains uncompiled JSX; fix build output or file extensions.'
812
- : '';
813
- const msg = baseMsg + jsxHint;
814
- errors.push(msg);
815
- if (!firstFailingUrl) firstFailingUrl = next.url;
816
- continue;
817
- }
818
- fetchedOk++;
819
-
820
- // For local file:// sources, parse imports in a TS-friendly way (regex-based).
821
- // For CDN URLs, parse as ESM and use es-module-lexer import locations.
822
- const isFile = next.url.startsWith('file://');
823
- if (!isFile) {
824
- const parses = await this.parseAsEsm(res.text);
825
- if (!parses) {
826
- const msg = `parse ${next.url} -> non-parseable JS`;
827
- errors.push(msg);
828
- if (!firstFailingUrl) firstFailingUrl = next.url;
829
- continue;
830
- }
831
- }
832
-
833
- const specs = isFile
834
- ? this.getTsImportSpecifiers(res.text)
835
- : await this.getImportSpecifiers(res.text);
836
- for (const spec of specs) {
837
- let resolved: string | null = null;
838
-
839
- // Prefer local resolution for PIE packages when enabled.
840
- if (localPieEnabled && this.isPiePackageSpecifier(spec)) {
841
- resolved = this.tryResolvePieToLocalFile(spec, localPieElementsPath, localPieLibPath);
842
- }
843
-
844
- if (!resolved) {
845
- if (isFile && (spec.startsWith('./') || spec.startsWith('../'))) {
846
- resolved = this.resolveLocalImport(next.url, spec);
847
- } else {
848
- resolved = this.resolveImportUrl(next.url, spec, cdnBaseUrl);
849
- }
850
- }
851
-
852
- if (!resolved) continue;
853
- if (!visited.has(resolved)) queue.push({ url: resolved, depth: next.depth + 1 });
854
- }
855
- }
856
-
857
- return { ok: errors.length === 0, fetched, fetchedOk, firstFailingUrl, errors };
858
- }
859
-
860
- private async fetchTextWithTimeout(
861
- url: string,
862
- timeoutMs: number
863
- ): Promise<{ ok: boolean; status: number; text: string }> {
864
- const controller = new AbortController();
865
- const t = setTimeout(() => controller.abort(), timeoutMs);
866
- try {
867
- const res = await fetch(url, { signal: controller.signal });
868
- const text = await res.text();
869
- return { ok: res.ok, status: res.status, text };
870
- } catch (e: any) {
871
- return { ok: false, status: 0, text: String(e?.message || e) };
872
- } finally {
873
- clearTimeout(t);
874
- }
875
- }
876
-
877
- private async probeElementsRuntime(opts: {
878
- elements: string[];
879
- pieElementsPath: string;
880
- cdnBaseUrl: string;
881
- localPieEnabled: boolean;
882
- localPieElementsPath: string;
883
- localPieLibPath: string;
884
- timeoutMs: number;
885
- concurrency: number;
886
- deep: boolean;
887
- maxDepth: number;
888
- maxModules: number;
889
- cache: Record<string, EsmRuntimeValidationResult>;
890
- verbose: boolean;
891
- }): Promise<Record<string, EsmRuntimeValidationResult>> {
892
- const {
893
- elements,
894
- pieElementsPath,
895
- cdnBaseUrl,
896
- localPieEnabled,
897
- localPieElementsPath,
898
- localPieLibPath,
899
- timeoutMs,
900
- concurrency,
901
- deep,
902
- maxDepth,
903
- maxModules,
904
- cache,
905
- verbose,
906
- } = opts;
907
-
908
- const results: Record<string, EsmRuntimeValidationResult> = {};
909
- const queue = elements.slice();
910
- let done = 0;
911
-
912
- const worker = async () => {
913
- while (queue.length > 0) {
914
- const element = queue.shift();
915
- if (!element) return;
916
-
917
- const pkgPath = join(pieElementsPath, 'packages', element, 'package.json');
918
- const pkg = (await loadPackageJson(pkgPath)) as PackageJson;
919
- const pkgName = `@pie-element/${element}`;
920
- const version = String((pkg as any).version || 'latest');
921
- // Cache key must include probe mode + limits, otherwise a shallow cached result
922
- // would incorrectly satisfy a deep/transitive run.
923
- const cacheKey = `${cdnBaseUrl}|${pkgName}@${version}|mode=${deep ? 'deep' : 'shallow'}|depth=${maxDepth}|modules=${maxModules}`;
924
-
925
- if (cache[cacheKey]) {
926
- results[element] = cache[cacheKey];
927
- done++;
928
- if (!verbose && done % 5 === 0)
929
- this.logger.info(` Progress: ${done}/${elements.length}`);
930
- continue;
931
- }
932
-
933
- const entryUrl = `${cdnBaseUrl}/${pkgName}@${version}`;
934
- const controllerUrl = `${cdnBaseUrl}/${pkgName}@${version}/controller`;
935
-
936
- const out: EsmRuntimeValidationResult = {
937
- compatible: false,
938
- cdnBaseUrl,
939
- version,
940
- entryOk: false,
941
- controllerOk: false,
942
- entryParseOk: false,
943
- controllerParseOk: false,
944
- errors: [],
945
- probeMode: deep ? 'deep' : 'shallow',
946
- maxDepth: deep ? maxDepth : undefined,
947
- maxModules: deep ? maxModules : undefined,
948
- };
949
-
950
- if (!deep) {
951
- const entry = await this.fetchTextWithTimeout(entryUrl, timeoutMs);
952
- out.entryOk = entry.ok;
953
- if (!entry.ok) {
954
- out.errors.push(
955
- `entry ${entryUrl} -> ${entry.status || 'error'} (${entry.text.slice(0, 120)})`
956
- );
957
- } else {
958
- out.entryParseOk = await this.parseAsEsm(entry.text);
959
- if (!out.entryParseOk) out.errors.push(`entry ${entryUrl} returned non-parseable JS`);
960
- }
961
-
962
- const ctrl = await this.fetchTextWithTimeout(controllerUrl, timeoutMs);
963
- out.controllerOk = ctrl.ok;
964
- if (!ctrl.ok) {
965
- out.errors.push(
966
- `controller ${controllerUrl} -> ${ctrl.status || 'error'} (${ctrl.text.slice(0, 120)})`
967
- );
968
- } else {
969
- out.controllerParseOk = await this.parseAsEsm(ctrl.text);
970
- if (!out.controllerParseOk)
971
- out.errors.push(`controller ${controllerUrl} returned non-parseable JS`);
972
- }
973
-
974
- out.compatible =
975
- out.entryOk && out.entryParseOk && out.controllerOk && out.controllerParseOk;
976
- } else {
977
- // Deep probe: walk import graphs for entry and controller
978
- // Still record the top-level entry/controller status for readability.
979
- const entryTop = await this.fetchTextWithTimeout(entryUrl, timeoutMs);
980
- out.entryOk = entryTop.ok;
981
- out.entryParseOk = entryTop.ok ? await this.parseAsEsm(entryTop.text) : false;
982
-
983
- const ctrlTop = await this.fetchTextWithTimeout(controllerUrl, timeoutMs);
984
- out.controllerOk = ctrlTop.ok;
985
- out.controllerParseOk = ctrlTop.ok ? await this.parseAsEsm(ctrlTop.text) : false;
986
-
987
- const entryDeep = await this.deepProbeFromUrl({
988
- entryUrl,
989
- cdnBaseUrl,
990
- timeoutMs,
991
- maxDepth,
992
- maxModules,
993
- logPrefix: `${element} entry`,
994
- localPieEnabled,
995
- localPieElementsPath,
996
- localPieLibPath,
997
- });
998
- const ctrlDeep = await this.deepProbeFromUrl({
999
- entryUrl: controllerUrl,
1000
- cdnBaseUrl,
1001
- timeoutMs,
1002
- maxDepth,
1003
- maxModules,
1004
- logPrefix: `${element} controller`,
1005
- localPieEnabled,
1006
- localPieElementsPath,
1007
- localPieLibPath,
1008
- });
1009
-
1010
- out.fetchedModules = (entryDeep.fetched || 0) + (ctrlDeep.fetched || 0);
1011
- out.fetchedOk = (entryDeep.fetchedOk || 0) + (ctrlDeep.fetchedOk || 0);
1012
- out.firstFailingUrl = entryDeep.firstFailingUrl || ctrlDeep.firstFailingUrl;
1013
- out.errors.push(...entryDeep.errors.slice(0, 5));
1014
- out.errors.push(...ctrlDeep.errors.slice(0, 5));
1015
-
1016
- out.compatible =
1017
- out.entryOk &&
1018
- out.entryParseOk &&
1019
- out.controllerOk &&
1020
- out.controllerParseOk &&
1021
- entryDeep.ok &&
1022
- ctrlDeep.ok;
1023
- }
1024
-
1025
- cache[cacheKey] = out;
1026
- results[element] = out;
1027
-
1028
- done++;
1029
- if (verbose) {
1030
- this.logger.info(
1031
- ` • ${element} -> ${out.compatible ? '✅' : '❌'} (entry:${out.entryOk ? 'ok' : 'bad'}, controller:${out.controllerOk ? 'ok' : 'bad'})`
1032
- );
1033
- if (!out.compatible && out.errors.length > 0) {
1034
- this.logger.info(` - ${out.errors[0]}`);
1035
- }
1036
- } else if (done % 5 === 0 || done === elements.length) {
1037
- this.logger.info(` Progress: ${done}/${elements.length}`);
1038
- }
1039
- }
1040
- };
1041
-
1042
- await Promise.all(
1043
- Array.from({ length: Math.max(1, Math.min(concurrency, elements.length)) }, () => worker())
1044
- );
1045
- return results;
1046
- }
1047
-
1048
- private async checkElementCompatibility(
1049
- elementName: string,
1050
- pieElementsPath: string,
1051
- pieLibPath: string,
1052
- pieLibCache: Map<string, { compatible: boolean; blockers: string[] }>,
1053
- verbose: boolean,
1054
- includeDevDeps: boolean
1055
- ): Promise<ElementDetail> {
1056
- const blockers: string[] = [];
1057
- const elementPath = join(pieElementsPath, 'packages', elementName, 'package.json');
1058
- const pkg = await loadPackageJson(elementPath);
1059
-
1060
- if (!pkg) {
1061
- blockers.push(`Package.json not found at ${elementPath}`);
1062
- return { compatible: false, directDeps: [], pieLibDeps: [], pieElementDeps: [], blockers };
1063
- }
1064
-
1065
- const deps = this.getAllDeps(pkg, includeDevDeps);
1066
- const directDeps = Object.keys(deps);
1067
- const pieLibDeps = this.extractPieLibDeps(deps);
1068
- const pieElementDeps = this.extractPieElementDeps(deps);
1069
-
1070
- if (verbose) {
1071
- this.logger.info(`\n 📦 ${elementName}:`);
1072
- this.logger.info(` Direct deps: ${directDeps.length}`);
1073
- this.logger.info(` PIE Lib deps: ${pieLibDeps.join(', ') || 'none'}`);
1074
- this.logger.info(` PIE Element deps: ${pieElementDeps.join(', ') || 'none'}`);
1075
- }
1076
-
1077
- // Deep scan all dependencies recursively
1078
- const depCache = new Map<string, { compatible: boolean; blockers: string[] }>();
1079
- for (const [dep, version] of Object.entries(deps)) {
1080
- // Skip pie-lib packages - we'll check those separately
1081
- if (dep.startsWith('@pie-lib/')) {
1082
- continue;
1083
- }
1084
-
1085
- await this.checkDependencyDeep(
1086
- dep,
1087
- version,
1088
- pieElementsPath,
1089
- pieLibPath,
1090
- depCache,
1091
- blockers,
1092
- verbose,
1093
- includeDevDeps,
1094
- []
1095
- );
1096
- }
1097
-
1098
- // Check transitive pie-lib dependencies
1099
- for (const pieLibDep of pieLibDeps) {
1100
- let pieLibCheck = pieLibCache.get(pieLibDep);
1101
-
1102
- if (!pieLibCheck) {
1103
- pieLibCheck = await this.checkPieLibCompatibility(
1104
- pieLibDep,
1105
- pieLibPath,
1106
- verbose,
1107
- includeDevDeps,
1108
- pieLibCache
1109
- );
1110
- pieLibCache.set(pieLibDep, pieLibCheck);
1111
- }
1112
-
1113
- if (!pieLibCheck.compatible) {
1114
- for (const blocker of pieLibCheck.blockers) {
1115
- const transitiveBlocker = `@pie-lib/${pieLibDep} -> ${blocker}`;
1116
- blockers.push(transitiveBlocker);
1117
- if (verbose) {
1118
- this.logger.info(` ❌ Transitive: ${transitiveBlocker}`);
1119
- }
1120
- }
1121
- }
1122
- }
1123
-
1124
- const compatible = blockers.length === 0;
1125
-
1126
- if (verbose && compatible) {
1127
- this.logger.info(' ✅ Compatible!');
1128
- }
1129
-
1130
- return {
1131
- compatible,
1132
- directDeps,
1133
- pieLibDeps,
1134
- pieElementDeps,
1135
- blockers,
1136
- };
1137
- }
1138
-
1139
- /**
1140
- * Check subdirectory compatibility (src/, configure/, controller/)
1141
- * Returns compatibility status for each subdirectory that has its own package.json
1142
- */
1143
- private async checkSubdirectoryCompatibility(
1144
- elementName: string,
1145
- subdirName: 'src' | 'configure' | 'controller',
1146
- pieElementsPath: string,
1147
- pieLibPath: string,
1148
- pieLibCache: Map<string, { compatible: boolean; blockers: string[] }>,
1149
- verbose: boolean,
1150
- includeDevDeps: boolean
1151
- ): Promise<{ compatible: boolean; blockers: string[] } | null> {
1152
- const subdirPath = join(pieElementsPath, 'packages', elementName, subdirName);
1153
- const pkgJsonPath = join(subdirPath, 'package.json');
1154
-
1155
- // If no package.json in subdirectory, it's part of the main element
1156
- if (!existsSync(pkgJsonPath)) {
1157
- return null;
1158
- }
1159
-
1160
- const blockers: string[] = [];
1161
- const pkg = await loadPackageJson(pkgJsonPath);
1162
-
1163
- if (!pkg) {
1164
- return null;
1165
- }
1166
-
1167
- const deps = this.getAllDeps(pkg, includeDevDeps);
1168
- const pieLibDeps = this.extractPieLibDeps(deps);
1169
-
1170
- if (verbose) {
1171
- this.logger.info(` 📁 ${subdirName}/: checking ${Object.keys(deps).length} dependencies`);
1172
- }
1173
-
1174
- // Deep scan all dependencies
1175
- const depCache = new Map<string, { compatible: boolean; blockers: string[] }>();
1176
- for (const [dep, version] of Object.entries(deps)) {
1177
- if (dep.startsWith('@pie-lib/')) {
1178
- continue;
1179
- }
1180
-
1181
- await this.checkDependencyDeep(
1182
- dep,
1183
- version,
1184
- pieElementsPath,
1185
- pieLibPath,
1186
- depCache,
1187
- blockers,
1188
- verbose,
1189
- includeDevDeps,
1190
- []
1191
- );
1192
- }
1193
-
1194
- // Check pie-lib dependencies
1195
- for (const pieLibDep of pieLibDeps) {
1196
- let pieLibCheck = pieLibCache.get(pieLibDep);
1197
-
1198
- if (!pieLibCheck) {
1199
- pieLibCheck = await this.checkPieLibCompatibility(
1200
- pieLibDep,
1201
- pieLibPath,
1202
- verbose,
1203
- includeDevDeps,
1204
- pieLibCache
1205
- );
1206
- pieLibCache.set(pieLibDep, pieLibCheck);
1207
- }
1208
-
1209
- if (!pieLibCheck.compatible) {
1210
- for (const blocker of pieLibCheck.blockers) {
1211
- const transitiveBlocker = `@pie-lib/${pieLibDep} -> ${blocker}`;
1212
- blockers.push(transitiveBlocker);
1213
- if (verbose) {
1214
- this.logger.info(` ❌ ${subdirName}/: ${transitiveBlocker}`);
1215
- }
1216
- }
1217
- }
1218
- }
1219
-
1220
- const compatible = blockers.length === 0;
1221
-
1222
- if (verbose) {
1223
- if (compatible) {
1224
- this.logger.info(` ✅ ${subdirName}/: Compatible`);
1225
- } else {
1226
- this.logger.info(` ❌ ${subdirName}/: Blocked`);
1227
- }
1228
- }
1229
-
1230
- return { compatible, blockers };
1231
- }
1232
-
1233
- private async checkDependencyDeep(
1234
- depName: string,
1235
- version: string,
1236
- pieElementsPath: string,
1237
- pieLibPath: string,
1238
- cache: Map<string, { compatible: boolean; blockers: string[] }>,
1239
- blockers: string[],
1240
- verbose: boolean,
1241
- includeDevDeps: boolean,
1242
- depChain: string[]
1243
- ): Promise<void> {
1244
- const depKey = `${depName}@${version}`;
1245
-
1246
- // Check cache
1247
- if (cache.has(depKey)) {
1248
- const cached = cache.get(depKey);
1249
- if (cached && !cached.compatible) {
1250
- for (const blocker of cached.blockers) {
1251
- const chain = [...depChain, blocker].join(' -> ');
1252
- if (!blockers.includes(chain)) {
1253
- blockers.push(chain);
1254
- }
1255
- }
1256
- }
1257
- return;
1258
- }
1259
-
1260
- // Check if this is a known ESM blocker
1261
- const blocker = this.checkEsmBlocker(depName, version);
1262
- if (blocker) {
1263
- const chain = [...depChain, blocker].join(' -> ');
1264
- if (!blockers.includes(chain)) {
1265
- blockers.push(chain);
1266
- if (verbose) {
1267
- this.logger.info(` ❌ Transitive: ${chain}`);
1268
- }
1269
- }
1270
- cache.set(depKey, { compatible: false, blockers: [blocker] });
1271
- return;
1272
- }
1273
-
1274
- // Try to load package.json from upstream repos to get transitive deps
1275
- // Look in pie-elements node_modules first, then pie-lib node_modules
1276
- let pkg: PackageJson | null = null;
1277
- const possiblePaths = [
1278
- join(pieElementsPath, 'node_modules', depName, 'package.json'),
1279
- join(pieLibPath, 'node_modules', depName, 'package.json'),
1280
- ];
1281
-
1282
- for (const path of possiblePaths) {
1283
- if (existsSync(path)) {
1284
- pkg = await loadPackageJson(path);
1285
- if (pkg) break;
1286
- }
1287
- }
1288
-
1289
- if (!pkg) {
1290
- // Can't resolve - assume compatible (will be caught by runtime validation if problematic)
1291
- cache.set(depKey, { compatible: true, blockers: [] });
1292
- return;
1293
- }
1294
-
1295
- // Check transitive dependencies
1296
- const transitiveDeps = this.getAllDeps(pkg, false); // Don't include devDeps for transitive checks
1297
- const hasBlockers = false;
1298
-
1299
- for (const [transitiveDep, transitiveVersion] of Object.entries(transitiveDeps)) {
1300
- // Skip PIE packages in transitive scan
1301
- if (
1302
- transitiveDep.startsWith('@pie-lib/') ||
1303
- transitiveDep.startsWith('@pie-element/') ||
1304
- transitiveDep.startsWith('@pie-element/')
1305
- ) {
1306
- continue;
1307
- }
1308
-
1309
- await this.checkDependencyDeep(
1310
- transitiveDep,
1311
- transitiveVersion,
1312
- pieElementsPath,
1313
- pieLibPath,
1314
- cache,
1315
- blockers,
1316
- verbose,
1317
- includeDevDeps,
1318
- [...depChain, `${depName}@${version}`]
1319
- );
1320
- }
1321
-
1322
- cache.set(depKey, { compatible: !hasBlockers, blockers: [] });
1323
- }
1324
-
1325
- private async checkPieLibCompatibility(
1326
- pkgName: string,
1327
- pieLibPath: string,
1328
- verbose: boolean,
1329
- includeDevDeps: boolean,
1330
- pieLibCache?: Map<string, { compatible: boolean; blockers: string[] }>
1331
- ): Promise<{
1332
- compatible: boolean;
1333
- blockers: string[];
1334
- }> {
1335
- // Check cache first to avoid infinite recursion
1336
- if (pieLibCache?.has(pkgName)) {
1337
- const cached = pieLibCache.get(pkgName);
1338
- if (cached) return cached;
1339
- }
1340
-
1341
- const blockers: string[] = [];
1342
- const pkgPath = join(pieLibPath, 'packages', pkgName, 'package.json');
1343
- const pkg = await loadPackageJson(pkgPath);
1344
-
1345
- if (!pkg) {
1346
- blockers.push(`Package.json not found at ${pkgPath}`);
1347
- return { compatible: false, blockers };
1348
- }
1349
-
1350
- const deps = this.getAllDeps(pkg, includeDevDeps);
1351
-
1352
- // Check direct dependencies for blockers
1353
- for (const [dep, version] of Object.entries(deps)) {
1354
- // Check for direct ESM blockers (non pie-lib packages)
1355
- if (!dep.startsWith('@pie-lib/')) {
1356
- const blocker = this.checkEsmBlocker(dep, version);
1357
- if (blocker) {
1358
- blockers.push(blocker);
1359
- if (verbose) {
1360
- this.logger.info(` ❌ ${pkgName}: ${blocker}`);
1361
- }
1362
- }
1363
- }
1364
- }
1365
-
1366
- // Store preliminary result in cache to prevent infinite recursion
1367
- const preliminaryResult = {
1368
- compatible: blockers.length === 0,
1369
- blockers: [...blockers],
1370
- };
1371
- if (pieLibCache) {
1372
- pieLibCache.set(pkgName, preliminaryResult);
1373
- }
1374
-
1375
- // Recursively check transitive pie-lib dependencies
1376
- const pieLibDeps = this.extractPieLibDeps(deps);
1377
- for (const transitivePieLib of pieLibDeps) {
1378
- const transitiveCheck = await this.checkPieLibCompatibility(
1379
- transitivePieLib,
1380
- pieLibPath,
1381
- verbose,
1382
- includeDevDeps,
1383
- pieLibCache
1384
- );
1385
-
1386
- if (!transitiveCheck.compatible) {
1387
- for (const blocker of transitiveCheck.blockers) {
1388
- const transitiveBlocker = `@pie-lib/${transitivePieLib} -> ${blocker}`;
1389
- blockers.push(transitiveBlocker);
1390
- if (verbose) {
1391
- this.logger.info(` ❌ ${pkgName}: transitive ${transitiveBlocker}`);
1392
- }
1393
- }
1394
- }
1395
- }
1396
-
1397
- // Update cache with final result
1398
- const finalResult = {
1399
- compatible: blockers.length === 0,
1400
- blockers,
1401
- };
1402
- if (pieLibCache) {
1403
- pieLibCache.set(pkgName, finalResult);
1404
- }
1405
-
1406
- return finalResult;
1407
- }
1408
-
1409
- /**
1410
- * Collect all transitive pie-lib dependencies recursively.
1411
- * For example, if text-select depends on style-utils, we need style-utils too.
1412
- */
1413
- private async collectAllTransitivePieLibDeps(
1414
- pieLibDeps: string[],
1415
- pieLibPath: string,
1416
- includeDevDeps: boolean,
1417
- visited = new Set<string>()
1418
- ): Promise<string[]> {
1419
- const allDeps = new Set<string>(pieLibDeps);
1420
-
1421
- for (const dep of pieLibDeps) {
1422
- if (visited.has(dep)) {
1423
- continue;
1424
- }
1425
- visited.add(dep);
1426
-
1427
- const pkgPath = join(pieLibPath, 'packages', dep, 'package.json');
1428
- let pkg: PackageJson | null = null;
1429
- try {
1430
- pkg = await loadPackageJson(pkgPath);
1431
- } catch (error) {
1432
- // Package doesn't exist or can't be read, skip it
1433
- continue;
1434
- }
1435
- if (!pkg) continue;
1436
-
1437
- const deps = this.getAllDeps(pkg, includeDevDeps);
1438
- const transitivePieLibDeps = this.extractPieLibDeps(deps);
1439
-
1440
- // Recursively collect transitive deps
1441
- const deepDeps = await this.collectAllTransitivePieLibDeps(
1442
- transitivePieLibDeps,
1443
- pieLibPath,
1444
- includeDevDeps,
1445
- visited
1446
- );
1447
-
1448
- for (const transitiveDep of deepDeps) {
1449
- allDeps.add(transitiveDep);
1450
- }
1451
- }
1452
-
1453
- return Array.from(allDeps);
1454
- }
1455
-
1456
- private getAllDeps(pkg: PackageJson, includeDevDeps: boolean): Record<string, string> {
1457
- const optionalDependencies =
1458
- (pkg.optionalDependencies as Record<string, string> | undefined) ?? {};
1459
- return {
1460
- ...pkg.dependencies,
1461
- ...optionalDependencies,
1462
- ...(includeDevDeps ? pkg.devDependencies : {}),
1463
- };
1464
- }
1465
-
1466
- private extractPieLibDeps(deps: Record<string, string>): string[] {
1467
- return Object.keys(deps)
1468
- .filter((dep) => dep.startsWith('@pie-lib/'))
1469
- .map((dep) => dep.replace('@pie-lib/', ''));
1470
- }
1471
-
1472
- private extractPieElementDeps(deps: Record<string, string>): string[] {
1473
- return Object.keys(deps)
1474
- .filter((dep) => dep.startsWith('@pie-element/'))
1475
- .map((dep) => dep.replace('@pie-element/', ''));
1476
- }
1477
-
1478
- private checkEsmBlocker(dep: string, version: string): string | null {
1479
- const depVersion = `${dep}@${version}`;
1480
-
1481
- for (const blocker of ESM_BLOCKERS) {
1482
- if (blocker.pattern.test(depVersion) || blocker.pattern.test(dep)) {
1483
- return `${dep}@${version} - ${blocker.reason}`;
1484
- }
1485
- }
1486
-
1487
- return null;
1488
- }
1489
-
1490
- private async validateEsmPlayerCompatibility(
1491
- elementName: string,
1492
- pieElementsPath: string,
1493
- verbose: boolean
1494
- ): Promise<EsmValidationResult> {
1495
- const blockers: string[] = [];
1496
- const warnings: string[] = [];
1497
-
1498
- const elementPath = join(pieElementsPath, 'packages', elementName, 'package.json');
1499
- const pkg = await loadPackageJson(elementPath);
1500
-
1501
- if (!pkg) {
1502
- blockers.push(`Package.json not found at ${elementPath}`);
1503
- return {
1504
- compatible: false,
1505
- packageJson: {
1506
- hasTypeModule: false,
1507
- hasExportsField: false,
1508
- hasMainExport: false,
1509
- hasControllerExport: false,
1510
- sideEffectsFree: false,
1511
- mainExportIsEsm: false,
1512
- controllerExportIsEsm: false,
1513
- },
1514
- blockers,
1515
- warnings,
1516
- };
1517
- }
1518
-
1519
- // Check 1: type: "module"
1520
- const hasTypeModule = pkg.type === 'module';
1521
- if (!hasTypeModule) {
1522
- blockers.push('Missing "type": "module" in package.json');
1523
- }
1524
-
1525
- // Check 2: exports field exists
1526
- const hasExportsField = !!pkg.exports;
1527
- if (!hasExportsField) {
1528
- blockers.push('Missing "exports" field in package.json');
1529
- }
1530
-
1531
- // Check 3: main export (".")
1532
- const exportsObj = pkg.exports as Record<string, unknown> | undefined;
1533
- const mainExport = exportsObj?.['.'];
1534
- const hasMainExport = !!mainExport;
1535
- if (!hasMainExport && hasExportsField) {
1536
- blockers.push('Missing "." export in package.json exports field');
1537
- }
1538
-
1539
- // Check 4: controller export ("./controller")
1540
- const controllerExport = exportsObj?.['./controller'];
1541
- const hasControllerExport = !!controllerExport;
1542
- if (!hasControllerExport && hasExportsField) {
1543
- warnings.push('Missing "./controller" export - ESM player needs controller subpath');
1544
- }
1545
-
1546
- // Check 5: sideEffects
1547
- const sideEffectsFree = pkg.sideEffects === false;
1548
- if (!sideEffectsFree) {
1549
- warnings.push('Consider adding "sideEffects": false for better tree-shaking');
1550
- }
1551
-
1552
- // Check 6: main export points to .js file
1553
- let mainExportIsEsm = false;
1554
- if (mainExport) {
1555
- const exportObj = mainExport as Record<string, unknown> | string;
1556
- const mainPath =
1557
- typeof exportObj === 'string'
1558
- ? exportObj
1559
- : (exportObj.import as string | undefined) || (exportObj.default as string | undefined);
1560
- mainExportIsEsm = typeof mainPath === 'string' && mainPath.endsWith('.js');
1561
- if (!mainExportIsEsm) {
1562
- blockers.push(`Main export must point to .js file (ESM), got: ${mainPath}`);
1563
- }
1564
- }
1565
-
1566
- // Check 7: controller export points to .js file
1567
- let controllerExportIsEsm = false;
1568
- if (controllerExport) {
1569
- const exportObj = controllerExport as Record<string, unknown> | string;
1570
- const controllerPath =
1571
- typeof exportObj === 'string'
1572
- ? exportObj
1573
- : (exportObj.import as string | undefined) || (exportObj.default as string | undefined);
1574
- controllerExportIsEsm = typeof controllerPath === 'string' && controllerPath.endsWith('.js');
1575
- if (!controllerExportIsEsm) {
1576
- warnings.push(`Controller export should point to .js file (ESM), got: ${controllerPath}`);
1577
- }
1578
- }
1579
-
1580
- const validation: EsmPackageValidation = {
1581
- hasTypeModule,
1582
- hasExportsField,
1583
- hasMainExport,
1584
- hasControllerExport,
1585
- sideEffectsFree,
1586
- mainExportIsEsm,
1587
- controllerExportIsEsm,
1588
- };
1589
-
1590
- const compatible = blockers.length === 0;
1591
-
1592
- if (verbose) {
1593
- if (compatible) {
1594
- this.logger.info(` ✅ ${elementName}: ESM player ready`);
1595
- } else {
1596
- this.logger.info(` ❌ ${elementName}: Not ESM player ready`);
1597
- for (const blocker of blockers) {
1598
- this.logger.info(` - ${blocker}`);
1599
- }
1600
- }
1601
- }
1602
-
1603
- return {
1604
- compatible,
1605
- packageJson: validation,
1606
- blockers,
1607
- warnings,
1608
- };
1609
- }
1610
-
1611
- private printReport(report: CompatibilityReport, outputPath: string): void {
1612
- const verbose = this.logger.isVerbose();
1613
-
1614
- if (verbose) {
1615
- this.log(`\n${'='.repeat(70)}`);
1616
- this.log('📊 ESM COMPATIBILITY REPORT');
1617
- this.log('='.repeat(70));
1618
- this.log('\n📦 Elements:');
1619
- this.log(` Total: ${report.summary.totalElements}`);
1620
- this.log(
1621
- ` CommonJS-free: ${report.summary.compatibleElements} ✅ (no CommonJS deps)`
1622
- );
1623
- if (report.esmPlayerValidationEnabled) {
1624
- this.log(
1625
- ` ESM player ready: ${report.summary.esmPlayerReady} ✅ (package.json exports)`
1626
- );
1627
- } else {
1628
- this.log(' ESM player ready: n/a (validation disabled)');
1629
- }
1630
-
1631
- if (report.esmRuntimeValidationEnabled) {
1632
- const runtimeReady = report.summary.esmRuntimeReady ?? 0;
1633
- this.log(` Runtime ready: ${runtimeReady} ✅ (CDN probe: fetch + parse)`);
1634
- } else {
1635
- this.log(' Runtime ready: n/a (runtime probes disabled)');
1636
- }
1637
- this.log(` Blocked: ${report.summary.blockedElements} ❌`);
1638
-
1639
- this.log('\n📚 PIE Lib Packages:');
1640
- this.log(` Total: ${report.summary.totalPieLibPackages}`);
1641
- this.log(` Compatible: ${report.summary.compatiblePieLibPackages} ✅`);
1642
- this.log(
1643
- ` Blocked: ${report.summary.totalPieLibPackages - report.summary.compatiblePieLibPackages} ❌`
1644
- );
1645
-
1646
- if (report.esmPlayerValidationEnabled) {
1647
- // ESM player ready elements (best category)
1648
- if (report.esmPlayerReady.length > 0) {
1649
- this.log(`\n✅ ESM Player Ready (${report.esmPlayerReady.length}):`);
1650
- for (const element of report.esmPlayerReady.sort()) {
1651
- const validation = report.esmValidation[element];
1652
- const deps = report.elementDetails[element]?.pieLibDeps || [];
1653
- this.log(` • ${element}`);
1654
- this.log(` - type: module ${validation.packageJson.hasTypeModule ? '✅' : '❌'}`);
1655
- this.log(` - exports: . ${validation.packageJson.hasMainExport ? '✅' : '❌'}`);
1656
- this.log(
1657
- ` - exports: ./controller ${validation.packageJson.hasControllerExport ? '✅' : '❌'}`
1658
- );
1659
- this.log(` - deps: ${deps.length > 0 ? deps.join(', ') : 'none'}`);
1660
-
1661
- if (report.esmRuntimeValidationEnabled && report.esmRuntimeValidation?.[element]) {
1662
- const r = report.esmRuntimeValidation[element];
1663
- this.log(
1664
- ` - runtime probe: ${r.compatible ? '✅' : '❌'} (${r.cdnBaseUrl}, entry:${r.entryOk ? 'ok' : 'bad'}, controller:${r.controllerOk ? 'ok' : 'bad'})`
1665
- );
1666
- if (!r.compatible && r.errors?.length) {
1667
- this.log(` - ${r.errors[0]}`);
1668
- }
1669
- }
1670
- }
1671
- }
1672
-
1673
- // CommonJS-free but not ESM player ready
1674
- const commonJsFreeNotEsmReady = report.elements.filter(
1675
- (element) => !report.esmPlayerReady.includes(element)
1676
- );
1677
-
1678
- if (commonJsFreeNotEsmReady.length > 0) {
1679
- this.log(
1680
- `\n⚠️ CommonJS-free but not ESM Player Ready (${commonJsFreeNotEsmReady.length}):`
1681
- );
1682
- for (const element of commonJsFreeNotEsmReady.sort()) {
1683
- const validation = report.esmValidation[element];
1684
- this.log(` • ${element}`);
1685
-
1686
- if (!validation.packageJson.hasTypeModule) {
1687
- this.log(` ❌ Missing "type": "module"`);
1688
- }
1689
- if (!validation.packageJson.hasExportsField) {
1690
- this.log(` ❌ Missing "exports" field`);
1691
- }
1692
- if (!validation.packageJson.hasMainExport) {
1693
- this.log(` ❌ Missing "." export`);
1694
- }
1695
- if (!validation.packageJson.hasControllerExport) {
1696
- this.log(` ❌ Missing "./controller" export`);
1697
- }
1698
- if (!validation.packageJson.mainExportIsEsm) {
1699
- this.log(` ❌ Main export not ESM (.js)`);
1700
- }
1701
-
1702
- // Show warnings
1703
- for (const warning of validation.warnings) {
1704
- this.log(` ⚠️ ${warning}`);
1705
- }
1706
- }
1707
- }
1708
- }
1709
-
1710
- if (Object.keys(report.blockedElements).length > 0) {
1711
- this.log(`\n❌ Blocked Elements (${Object.keys(report.blockedElements).length}):`);
1712
- for (const [element, blockers] of Object.entries(report.blockedElements).sort()) {
1713
- this.log(` • ${element}:`);
1714
- for (const blocker of blockers) {
1715
- this.log(` - ${blocker}`);
1716
- }
1717
- }
1718
- }
1719
-
1720
- if (report.pieLibPackages.length > 0) {
1721
- this.log(`\n📚 Compatible PIE Lib Packages (${report.pieLibPackages.length}):`);
1722
- for (const pkg of report.pieLibPackages.sort()) {
1723
- const usedBy = report.pieLibDetails[pkg]?.usedBy || [];
1724
- this.log(` • ${pkg} (used by: ${usedBy.join(', ')})`);
1725
- }
1726
- }
1727
-
1728
- const blockedPieLib = Object.entries(report.pieLibDetails)
1729
- .filter(([_, details]) => !details.compatible)
1730
- .sort();
1731
-
1732
- if (blockedPieLib.length > 0) {
1733
- this.log(`\n❌ Blocked PIE Lib Packages (${blockedPieLib.length}):`);
1734
- for (const [pkg, details] of blockedPieLib) {
1735
- this.log(` • ${pkg} (needed by: ${details.usedBy.join(', ')}):`);
1736
- for (const blocker of details.blockers) {
1737
- this.log(` - ${blocker}`);
1738
- }
1739
- }
1740
- }
1741
-
1742
- this.printNotReadyActionReport(report);
1743
-
1744
- this.log(`\n${'='.repeat(70)}`);
1745
- this.log(`\n💾 Report saved to: ${outputPath}`);
1746
- this.log('\n💡 Next steps:');
1747
- this.log(' 1. Review the compatibility report above');
1748
- this.log(' 2. Run sync: bun run upstream:sync');
1749
- this.log(' 3. The sync will automatically use the ESM-compatible list\n');
1750
- } else {
1751
- // Non-verbose: just show where report was saved
1752
- this.log(`\n💾 Report saved to: ${outputPath}`);
1753
- }
1754
- }
1755
-
1756
- private printNotReadyActionReport(report: CompatibilityReport): void {
1757
- const allElements = Object.keys(report.elementDetails).sort();
1758
- const blockedElements = allElements.filter((e) => !report.elementDetails[e]?.compatible);
1759
- const commonJsFreeNotEsmReady = report.esmPlayerValidationEnabled
1760
- ? report.elements.filter((e) => !report.esmPlayerReady.includes(e))
1761
- : [];
1762
- const runtimeNotReady =
1763
- report.esmRuntimeValidationEnabled && report.esmRuntimeValidation
1764
- ? Object.entries(report.esmRuntimeValidation)
1765
- .filter(([_, r]) => !r.compatible)
1766
- .map(([e]) => e)
1767
- : [];
1768
-
1769
- const blockedPieLib = Object.entries(report.pieLibDetails)
1770
- .filter(([_, details]) => !details.compatible)
1771
- .sort();
1772
-
1773
- const hasAnyNotReady =
1774
- blockedElements.length > 0 ||
1775
- commonJsFreeNotEsmReady.length > 0 ||
1776
- runtimeNotReady.length > 0 ||
1777
- blockedPieLib.length > 0;
1778
-
1779
- if (!hasAnyNotReady) return;
1780
-
1781
- this.log('\n🚧 Not ESM-ready yet (action items for upstream maintainers)');
1782
- this.log(
1783
- ' Goal: “deploy package + ESM player can load entry + controller (and transitives)”\n'
1784
- );
1785
-
1786
- if (blockedElements.length > 0) {
1787
- this.log(` ❌ CommonJS blockers (${blockedElements.length}):`);
1788
- this.log(
1789
- ' Fix: remove/upgrade/replace the listed CommonJS-only deps (direct or via @pie-lib/*).'
1790
- );
1791
- for (const element of blockedElements) {
1792
- const blockers =
1793
- report.elementDetails[element]?.blockers ?? report.blockedElements[element] ?? [];
1794
- this.log(`\n • ${element}`);
1795
- for (const blocker of blockers.slice(0, 5)) {
1796
- this.log(` - ${blocker}`);
1797
- }
1798
- if (blockers.length > 5) {
1799
- this.log(` - …and ${blockers.length - 5} more`);
1800
- }
1801
- this.log(' What to do:');
1802
- this.log(
1803
- ' - Upgrade the dependency to a version that provides ESM + proper "exports"'
1804
- );
1805
- this.log(
1806
- ' - If no ESM exists, replace the dependency or move it behind build-time code (not runtime)'
1807
- );
1808
- }
1809
- this.log('');
1810
- }
1811
-
1812
- if (report.esmPlayerValidationEnabled && commonJsFreeNotEsmReady.length > 0) {
1813
- this.log(` ⚠️ Package metadata / exports issues (${commonJsFreeNotEsmReady.length}):`);
1814
- this.log(
1815
- ' Fix: make package.json ESM-friendly so an ESM loader can resolve `@pie-element/<name>` + `/controller`.\n'
1816
- );
1817
-
1818
- for (const element of commonJsFreeNotEsmReady.sort()) {
1819
- const validation = report.esmValidation[element];
1820
- if (!validation) continue;
1821
-
1822
- const pj = validation.packageJson;
1823
- this.log(` • ${element}`);
1824
-
1825
- const issues: string[] = [];
1826
- const fixes: string[] = [];
1827
-
1828
- if (!pj.hasTypeModule) {
1829
- issues.push('Missing `"type": "module"`');
1830
- fixes.push(
1831
- 'Add `"type": "module"` (or use `.mjs` consistently) so Node/bundlers treat output as ESM.'
1832
- );
1833
- }
1834
- if (!pj.hasExportsField) {
1835
- issues.push('Missing `"exports"`');
1836
- fixes.push(
1837
- 'Add `"exports"` with `"."` (entry) and `"./controller"` (controller) subpaths pointing at `dist/*`.'
1838
- );
1839
- }
1840
- if (pj.hasExportsField && !pj.hasMainExport) {
1841
- issues.push('Missing `"exports"."."`');
1842
- fixes.push('Add `"exports": { ".": { "default": "./dist/index.js" } }` (path may vary).');
1843
- }
1844
- if (!pj.hasControllerExport) {
1845
- issues.push('Missing `"exports"."./controller"`');
1846
- fixes.push(
1847
- 'Add `"./controller": { "default": "./dist/controller/index.js" }` when a controller exists.'
1848
- );
1849
- }
1850
- if (!pj.mainExportIsEsm) {
1851
- issues.push('Main export does not point at a `.js` ESM build');
1852
- fixes.push(
1853
- 'Ensure the published entry is transpiled ESM JS (no TS, no JSX) and the export points to it.'
1854
- );
1855
- }
1856
- if (pj.hasControllerExport && !pj.controllerExportIsEsm) {
1857
- issues.push('Controller export does not point at a `.js` ESM build');
1858
- fixes.push(
1859
- 'Ensure the published controller is transpiled ESM JS (no TS, no JSX) and the export points to it.'
1860
- );
1861
- }
1862
-
1863
- for (const issue of issues.slice(0, 6)) {
1864
- this.log(` - ${issue}`);
1865
- }
1866
- if (validation.blockers.length > 0) {
1867
- this.log(` - Details: ${validation.blockers[0]}`);
1868
- }
1869
- if (validation.warnings.length > 0) {
1870
- this.log(` - Warning: ${validation.warnings[0]}`);
1871
- }
1872
- this.log(' What to do:');
1873
- for (const fix of fixes.slice(0, 4)) {
1874
- this.log(` - ${fix}`);
1875
- }
1876
- this.log('');
1877
- }
1878
- }
1879
-
1880
- if (
1881
- report.esmRuntimeValidationEnabled &&
1882
- report.esmRuntimeValidation &&
1883
- runtimeNotReady.length > 0
1884
- ) {
1885
- this.log(` ❌ Runtime probe failures (${runtimeNotReady.length}):`);
1886
- this.log(
1887
- ' Fix: make sure the published JS parses as ESM and all transitives resolve in production-like loading.\n'
1888
- );
1889
-
1890
- for (const element of runtimeNotReady.sort()) {
1891
- const r = report.esmRuntimeValidation[element];
1892
- if (!r) continue;
1893
-
1894
- this.log(` • ${element} (CDN: ${r.cdnBaseUrl}, mode: ${r.probeMode ?? 'shallow'})`);
1895
- if (r.firstFailingUrl) this.log(` - First failing URL: ${r.firstFailingUrl}`);
1896
- const firstErr = r.errors?.[0];
1897
- if (firstErr) this.log(` - Error: ${firstErr}`);
1898
-
1899
- const suggestions = this.suggestionsFromRuntimeError(firstErr);
1900
- if (suggestions.length > 0) {
1901
- this.log(' What to do:');
1902
- for (const s of suggestions) this.log(` - ${s}`);
1903
- }
1904
- this.log('');
1905
- }
1906
- }
1907
-
1908
- if (blockedPieLib.length > 0) {
1909
- this.log(` ❌ PIE lib packages blocking ESM (${blockedPieLib.length}):`);
1910
- this.log(' Fix: these must become ESM-compatible because elements import them.\n');
1911
- for (const [pkg, details] of blockedPieLib) {
1912
- this.log(` • @pie-lib/${pkg} (used by: ${details.usedBy.join(', ')})`);
1913
- for (const blocker of details.blockers.slice(0, 5)) {
1914
- this.log(` - ${blocker}`);
1915
- }
1916
- if (details.blockers.length > 5) {
1917
- this.log(` - …and ${details.blockers.length - 5} more`);
1918
- }
1919
- }
1920
- this.log('');
1921
- }
1922
- }
1923
-
1924
- private suggestionsFromRuntimeError(err: string | undefined): string[] {
1925
- if (!err) {
1926
- return [
1927
- 'Confirm the published entry and controller URLs return JavaScript (not HTML/404) and parse as ESM.',
1928
- 'Re-run with --verbose to see the exact failing URL + first error.',
1929
- ];
1930
- }
1931
-
1932
- const e = err.toLowerCase();
1933
-
1934
- if (
1935
- e.includes('jsx syntax extension is not currently enabled') ||
1936
- e.includes('unexpected token <')
1937
- ) {
1938
- return [
1939
- 'Your published output likely contains raw JSX; transpile JSX away during build (do not ship JSX in `.js/.mjs`).',
1940
- 'Verify `dist/` contains plain JS and that `package.json` exports point at those `dist/` files.',
1941
- ];
1942
- }
1943
-
1944
- if (e.includes('missing') && e.includes('specifier') && e.includes('exports')) {
1945
- return [
1946
- 'A dependency is deep-importing a path that is not exposed via `"exports"`; upgrade/replace that dependency or avoid that deep import.',
1947
- 'If this is a PIE package, ensure its `"exports"` explicitly lists the needed subpaths.',
1948
- ];
1949
- }
1950
-
1951
- if (e.includes('could not resolve') || e.includes('not found') || e.includes('404')) {
1952
- return [
1953
- 'Ensure the entry/controller paths you publish actually exist on the CDN (and match `package.json` exports).',
1954
- 'If the controller exists, publish it as a subpath export (e.g. `./controller`) and ship the referenced `dist/` files.',
1955
- ];
1956
- }
1957
-
1958
- if (
1959
- e.includes('commonjs') ||
1960
- e.includes('require is not defined') ||
1961
- e.includes('module is not defined')
1962
- ) {
1963
- return [
1964
- 'A transitive dependency is CommonJS-only; upgrade/replace it with an ESM-capable version, or ship a pre-bundled ESM build.',
1965
- ];
1966
- }
1967
-
1968
- return [
1969
- 'Inspect the first failing URL and error; fix whichever dependency/output is not valid ESM.',
1970
- 'Re-run with --verbose and (if needed) increase --runtime-max-depth / --runtime-max-modules to reproduce reliably.',
1971
- ];
1972
- }
1973
- }