@pie-element/cli 0.1.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.
- package/.compatibility/report.json +2491 -0
- package/.turbo/turbo-build.log +1 -0
- package/CHANGELOG.md +8 -0
- package/README.md +240 -0
- package/REFACTORING.md +131 -0
- package/bin/dev.js +5 -0
- package/bin/run.js +5 -0
- package/dist/commands/dev/demo/index.d.ts +33 -0
- package/dist/commands/dev/demo/index.d.ts.map +1 -0
- package/dist/commands/dev/demo/index.js +274 -0
- package/dist/commands/dev/demo/index.js.map +1 -0
- package/dist/commands/dev/test-bundler.d.ts +18 -0
- package/dist/commands/dev/test-bundler.d.ts.map +1 -0
- package/dist/commands/dev/test-bundler.js +284 -0
- package/dist/commands/dev/test-bundler.js.map +1 -0
- package/dist/commands/mathquill/migrate.d.ts +20 -0
- package/dist/commands/mathquill/migrate.d.ts.map +1 -0
- package/dist/commands/mathquill/migrate.js +240 -0
- package/dist/commands/mathquill/migrate.js.map +1 -0
- package/dist/commands/mathquill/status.d.ts +20 -0
- package/dist/commands/mathquill/status.d.ts.map +1 -0
- package/dist/commands/mathquill/status.js +231 -0
- package/dist/commands/mathquill/status.js.map +1 -0
- package/dist/commands/packages/create-controllers.d.ts +13 -0
- package/dist/commands/packages/create-controllers.d.ts.map +1 -0
- package/dist/commands/packages/create-controllers.js +103 -0
- package/dist/commands/packages/create-controllers.js.map +1 -0
- package/dist/commands/packages/create-lib-react.d.ts +17 -0
- package/dist/commands/packages/create-lib-react.d.ts.map +1 -0
- package/dist/commands/packages/create-lib-react.js +228 -0
- package/dist/commands/packages/create-lib-react.js.map +1 -0
- package/dist/commands/packages/create-react-elements.d.ts +16 -0
- package/dist/commands/packages/create-react-elements.d.ts.map +1 -0
- package/dist/commands/packages/create-react-elements.js +259 -0
- package/dist/commands/packages/create-react-elements.js.map +1 -0
- package/dist/commands/packages/enable-publishing.d.ts +11 -0
- package/dist/commands/packages/enable-publishing.d.ts.map +1 -0
- package/dist/commands/packages/enable-publishing.js +117 -0
- package/dist/commands/packages/enable-publishing.js.map +1 -0
- package/dist/commands/packages/init-synced-elements.d.ts +16 -0
- package/dist/commands/packages/init-synced-elements.d.ts.map +1 -0
- package/dist/commands/packages/init-synced-elements.js +204 -0
- package/dist/commands/packages/init-synced-elements.js.map +1 -0
- package/dist/commands/upstream/analyze-esm.d.ts +59 -0
- package/dist/commands/upstream/analyze-esm.d.ts.map +1 -0
- package/dist/commands/upstream/analyze-esm.js +1481 -0
- package/dist/commands/upstream/analyze-esm.js.map +1 -0
- package/dist/commands/upstream/check.d.ts +15 -0
- package/dist/commands/upstream/check.d.ts.map +1 -0
- package/dist/commands/upstream/check.js +165 -0
- package/dist/commands/upstream/check.js.map +1 -0
- package/dist/commands/upstream/deps.d.ts +15 -0
- package/dist/commands/upstream/deps.d.ts.map +1 -0
- package/dist/commands/upstream/deps.js +174 -0
- package/dist/commands/upstream/deps.js.map +1 -0
- package/dist/commands/upstream/sync-mathquill.d.ts +12 -0
- package/dist/commands/upstream/sync-mathquill.d.ts.map +1 -0
- package/dist/commands/upstream/sync-mathquill.js +97 -0
- package/dist/commands/upstream/sync-mathquill.js.map +1 -0
- package/dist/commands/upstream/sync.d.ts +25 -0
- package/dist/commands/upstream/sync.d.ts.map +1 -0
- package/dist/commands/upstream/sync.js +552 -0
- package/dist/commands/upstream/sync.js.map +1 -0
- package/dist/commands/upstream/track.d.ts +15 -0
- package/dist/commands/upstream/track.d.ts.map +1 -0
- package/dist/commands/upstream/track.js +115 -0
- package/dist/commands/upstream/track.js.map +1 -0
- package/dist/commands/upstream/update.d.ts +15 -0
- package/dist/commands/upstream/update.d.ts.map +1 -0
- package/dist/commands/upstream/update.js +98 -0
- package/dist/commands/upstream/update.js.map +1 -0
- package/dist/commands/verify/controllers.d.ts +12 -0
- package/dist/commands/verify/controllers.d.ts.map +1 -0
- package/dist/commands/verify/controllers.js +138 -0
- package/dist/commands/verify/controllers.js.map +1 -0
- package/dist/commands/verify/react-build.d.ts +13 -0
- package/dist/commands/verify/react-build.d.ts.map +1 -0
- package/dist/commands/verify/react-build.js +135 -0
- package/dist/commands/verify/react-build.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/mathquill/extension-generator.d.ts +9 -0
- package/dist/lib/mathquill/extension-generator.d.ts.map +1 -0
- package/dist/lib/mathquill/extension-generator.js +107 -0
- package/dist/lib/mathquill/extension-generator.js.map +1 -0
- package/dist/lib/mathquill/khan-extractor.d.ts +16 -0
- package/dist/lib/mathquill/khan-extractor.d.ts.map +1 -0
- package/dist/lib/mathquill/khan-extractor.js +148 -0
- package/dist/lib/mathquill/khan-extractor.js.map +1 -0
- package/dist/lib/mathquill/learnosity-extractor.d.ts +15 -0
- package/dist/lib/mathquill/learnosity-extractor.d.ts.map +1 -0
- package/dist/lib/mathquill/learnosity-extractor.js +170 -0
- package/dist/lib/mathquill/learnosity-extractor.js.map +1 -0
- package/dist/lib/mathquill/migration-orchestrator.d.ts +77 -0
- package/dist/lib/mathquill/migration-orchestrator.d.ts.map +1 -0
- package/dist/lib/mathquill/migration-orchestrator.js +391 -0
- package/dist/lib/mathquill/migration-orchestrator.js.map +1 -0
- package/dist/lib/mathquill/package-updater.d.ts +9 -0
- package/dist/lib/mathquill/package-updater.d.ts.map +1 -0
- package/dist/lib/mathquill/package-updater.js +103 -0
- package/dist/lib/mathquill/package-updater.js.map +1 -0
- package/dist/lib/mathquill/pie-extractor.d.ts +14 -0
- package/dist/lib/mathquill/pie-extractor.d.ts.map +1 -0
- package/dist/lib/mathquill/pie-extractor.js +167 -0
- package/dist/lib/mathquill/pie-extractor.js.map +1 -0
- package/dist/lib/upstream/repo-utils.d.ts +9 -0
- package/dist/lib/upstream/repo-utils.d.ts.map +1 -0
- package/dist/lib/upstream/repo-utils.js +10 -0
- package/dist/lib/upstream/repo-utils.js.map +1 -0
- package/dist/lib/upstream/sync-compatibility.d.ts +29 -0
- package/dist/lib/upstream/sync-compatibility.d.ts.map +1 -0
- package/dist/lib/upstream/sync-compatibility.js +70 -0
- package/dist/lib/upstream/sync-compatibility.js.map +1 -0
- package/dist/lib/upstream/sync-constants.d.ts +50 -0
- package/dist/lib/upstream/sync-constants.d.ts.map +1 -0
- package/dist/lib/upstream/sync-constants.js +66 -0
- package/dist/lib/upstream/sync-constants.js.map +1 -0
- package/dist/lib/upstream/sync-controllers-strategy.d.ts +18 -0
- package/dist/lib/upstream/sync-controllers-strategy.d.ts.map +1 -0
- package/dist/lib/upstream/sync-controllers-strategy.js +243 -0
- package/dist/lib/upstream/sync-controllers-strategy.js.map +1 -0
- package/dist/lib/upstream/sync-demo-metadata.d.ts +15 -0
- package/dist/lib/upstream/sync-demo-metadata.d.ts.map +1 -0
- package/dist/lib/upstream/sync-demo-metadata.js +165 -0
- package/dist/lib/upstream/sync-demo-metadata.js.map +1 -0
- package/dist/lib/upstream/sync-dev-exports.d.ts +13 -0
- package/dist/lib/upstream/sync-dev-exports.d.ts.map +1 -0
- package/dist/lib/upstream/sync-dev-exports.js +115 -0
- package/dist/lib/upstream/sync-dev-exports.js.map +1 -0
- package/dist/lib/upstream/sync-externals.d.ts +29 -0
- package/dist/lib/upstream/sync-externals.d.ts.map +1 -0
- package/dist/lib/upstream/sync-externals.js +164 -0
- package/dist/lib/upstream/sync-externals.js.map +1 -0
- package/dist/lib/upstream/sync-filesystem.d.ts +21 -0
- package/dist/lib/upstream/sync-filesystem.d.ts.map +1 -0
- package/dist/lib/upstream/sync-filesystem.js +64 -0
- package/dist/lib/upstream/sync-filesystem.js.map +1 -0
- package/dist/lib/upstream/sync-imports.d.ts +284 -0
- package/dist/lib/upstream/sync-imports.d.ts.map +1 -0
- package/dist/lib/upstream/sync-imports.js +816 -0
- package/dist/lib/upstream/sync-imports.js.map +1 -0
- package/dist/lib/upstream/sync-mathquill.d.ts +19 -0
- package/dist/lib/upstream/sync-mathquill.d.ts.map +1 -0
- package/dist/lib/upstream/sync-mathquill.js +160 -0
- package/dist/lib/upstream/sync-mathquill.js.map +1 -0
- package/dist/lib/upstream/sync-package-json.d.ts +28 -0
- package/dist/lib/upstream/sync-package-json.d.ts.map +1 -0
- package/dist/lib/upstream/sync-package-json.js +66 -0
- package/dist/lib/upstream/sync-package-json.js.map +1 -0
- package/dist/lib/upstream/sync-package-manager.d.ts +50 -0
- package/dist/lib/upstream/sync-package-manager.d.ts.map +1 -0
- package/dist/lib/upstream/sync-package-manager.js +570 -0
- package/dist/lib/upstream/sync-package-manager.js.map +1 -0
- package/dist/lib/upstream/sync-pielib-strategy.d.ts +26 -0
- package/dist/lib/upstream/sync-pielib-strategy.d.ts.map +1 -0
- package/dist/lib/upstream/sync-pielib-strategy.js +345 -0
- package/dist/lib/upstream/sync-pielib-strategy.js.map +1 -0
- package/dist/lib/upstream/sync-react-strategy.d.ts +54 -0
- package/dist/lib/upstream/sync-react-strategy.d.ts.map +1 -0
- package/dist/lib/upstream/sync-react-strategy.js +733 -0
- package/dist/lib/upstream/sync-react-strategy.js.map +1 -0
- package/dist/lib/upstream/sync-strategy.d.ts +49 -0
- package/dist/lib/upstream/sync-strategy.d.ts.map +1 -0
- package/dist/lib/upstream/sync-strategy.js +2 -0
- package/dist/lib/upstream/sync-strategy.js.map +1 -0
- package/dist/lib/upstream/sync-summary.d.ts +20 -0
- package/dist/lib/upstream/sync-summary.d.ts.map +1 -0
- package/dist/lib/upstream/sync-summary.js +157 -0
- package/dist/lib/upstream/sync-summary.js.map +1 -0
- package/dist/lib/upstream/sync-transforms.d.ts +60 -0
- package/dist/lib/upstream/sync-transforms.d.ts.map +1 -0
- package/dist/lib/upstream/sync-transforms.js +97 -0
- package/dist/lib/upstream/sync-transforms.js.map +1 -0
- package/dist/lib/upstream/sync-vite-config.d.ts +13 -0
- package/dist/lib/upstream/sync-vite-config.d.ts.map +1 -0
- package/dist/lib/upstream/sync-vite-config.js +231 -0
- package/dist/lib/upstream/sync-vite-config.js.map +1 -0
- package/dist/utils/compatibility.d.ts +129 -0
- package/dist/utils/compatibility.d.ts.map +1 -0
- package/dist/utils/compatibility.js +44 -0
- package/dist/utils/compatibility.js.map +1 -0
- package/dist/utils/conversion.d.ts +25 -0
- package/dist/utils/conversion.d.ts.map +1 -0
- package/dist/utils/conversion.js +123 -0
- package/dist/utils/conversion.js.map +1 -0
- package/dist/utils/git.d.ts +7 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +63 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +29 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +63 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/package-json.d.ts +12 -0
- package/dist/utils/package-json.d.ts.map +1 -0
- package/dist/utils/package-json.js +29 -0
- package/dist/utils/package-json.js.map +1 -0
- package/package.json +59 -0
- package/packages/cli/bin/dev.js +5 -0
- package/packages/cli/bin/run.js +5 -0
- package/src/commands/dev/demo/index.ts +313 -0
- package/src/commands/dev/test-bundler.ts +345 -0
- package/src/commands/mathquill/migrate.ts +269 -0
- package/src/commands/mathquill/status.ts +267 -0
- package/src/commands/packages/create-controllers.ts +127 -0
- package/src/commands/packages/create-lib-react.ts +279 -0
- package/src/commands/packages/create-react-elements.ts +306 -0
- package/src/commands/packages/enable-publishing.ts +161 -0
- package/src/commands/packages/init-synced-elements.ts +259 -0
- package/src/commands/upstream/README.md +444 -0
- package/src/commands/upstream/analyze-esm.ts +1973 -0
- package/src/commands/upstream/check.ts +228 -0
- package/src/commands/upstream/deps.ts +222 -0
- package/src/commands/upstream/sync-mathquill.ts +108 -0
- package/src/commands/upstream/sync.ts +687 -0
- package/src/commands/upstream/track.ts +145 -0
- package/src/commands/upstream/update.ts +110 -0
- package/src/commands/verify/controllers.ts +166 -0
- package/src/commands/verify/react-build.ts +171 -0
- package/src/index.ts +1 -0
- package/src/lib/mathquill/extension-generator.ts +111 -0
- package/src/lib/mathquill/khan-extractor.ts +175 -0
- package/src/lib/mathquill/learnosity-extractor.ts +198 -0
- package/src/lib/mathquill/migration-orchestrator.ts +513 -0
- package/src/lib/mathquill/package-updater.ts +120 -0
- package/src/lib/mathquill/pie-extractor.ts +194 -0
- package/src/lib/upstream/repo-utils.ts +18 -0
- package/src/lib/upstream/sync-compatibility.ts +89 -0
- package/src/lib/upstream/sync-constants.ts +77 -0
- package/src/lib/upstream/sync-controllers-strategy.ts +291 -0
- package/src/lib/upstream/sync-demo-metadata.ts +199 -0
- package/src/lib/upstream/sync-dev-exports.ts +151 -0
- package/src/lib/upstream/sync-externals.ts +159 -0
- package/src/lib/upstream/sync-filesystem.ts +87 -0
- package/src/lib/upstream/sync-imports.ts +992 -0
- package/src/lib/upstream/sync-mathquill.ts +194 -0
- package/src/lib/upstream/sync-package-json.ts +86 -0
- package/src/lib/upstream/sync-package-manager.ts +694 -0
- package/src/lib/upstream/sync-pielib-strategy.ts +441 -0
- package/src/lib/upstream/sync-react-strategy.ts +945 -0
- package/src/lib/upstream/sync-strategy.ts +61 -0
- package/src/lib/upstream/sync-summary.ts +194 -0
- package/src/lib/upstream/sync-transforms.ts +154 -0
- package/src/lib/upstream/sync-vite-config.ts +243 -0
- package/src/types/es-module-lexer.d.ts +4 -0
- package/src/utils/compatibility.ts +182 -0
- package/src/utils/conversion.ts +153 -0
- package/src/utils/git.ts +63 -0
- package/src/utils/logger.ts +69 -0
- package/src/utils/package-json.ts +43 -0
- package/tests/sync-package-manager.test.ts +112 -0
- package/tests/transform-assign-props.test.ts +101 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,1973 @@
|
|
|
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
|
+
}
|