1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.loadProjectInfo = void 0;
|
4 | const fs = require("node:fs");
|
5 | const path = require("node:path");
|
6 | const spec = require("@jsii/spec");
|
7 | const spec_1 = require("@jsii/spec");
|
8 | const log4js = require("log4js");
|
9 | const semver = require("semver");
|
10 | const ts = require("typescript");
|
11 | const find_utils_1 = require("./common/find-utils");
|
12 | const jsii_diagnostic_1 = require("./jsii-diagnostic");
|
13 | const tsconfig_1 = require("./tsconfig");
|
14 | const utils_1 = require("./utils");
|
15 |
|
16 | const spdx = require('spdx-license-list/simple');
|
17 | const LOG = log4js.getLogger('jsii/package-info');
|
18 | function loadProjectInfo(projectRoot) {
|
19 | const packageJsonPath = path.join(projectRoot, 'package.json');
|
20 | const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
21 | const diagnostics = [];
|
22 | let bundleDependencies;
|
23 | for (const name of pkg.bundleDependencies ?? pkg.bundledDependencies ?? []) {
|
24 | const version = pkg.dependencies?.[name];
|
25 | if (!version) {
|
26 | throw new Error(`The "package.json" file has "${name}" in "bundleDependencies", but it is not declared in "dependencies"`);
|
27 | }
|
28 | if (pkg.peerDependencies && name in pkg.peerDependencies) {
|
29 | throw new Error(`The "package.json" file has "${name}" in "bundleDependencies", and also in "peerDependencies"`);
|
30 | }
|
31 | bundleDependencies = bundleDependencies ?? {};
|
32 | bundleDependencies[name] = _resolveVersion(version, projectRoot).version;
|
33 | }
|
34 |
|
35 |
|
36 |
|
37 |
|
38 | const devDependencies = pkg.devDependencies ?? {};
|
39 | for (const [name, rng] of Object.entries(pkg.peerDependencies ?? {})) {
|
40 | const range = new semver.Range(_resolveVersion(rng, projectRoot).version);
|
41 | const minVersion = semver.minVersion(range)?.raw;
|
42 | if (!(name in devDependencies) || devDependencies[name] !== `${minVersion}`) {
|
43 | diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_0006_MISSING_DEV_DEPENDENCY.createDetached(name, `${rng}`, `${minVersion}`, `${devDependencies[name]}`));
|
44 | continue;
|
45 | }
|
46 | }
|
47 | const bundled = new Set(Object.keys(bundleDependencies ?? {}));
|
48 | const dependencies = filterDictByKey(pkg.dependencies ?? {}, (depName) => !bundled.has(depName));
|
49 | const peerDependencies = pkg.peerDependencies ?? {};
|
50 | const resolver = new DependencyResolver();
|
51 | const resolved = resolver.discoverDependencyTree(projectRoot, {
|
52 | ...dependencies,
|
53 | ...peerDependencies,
|
54 | });
|
55 | const transitiveDependencies = resolver.assemblyClosure(resolved);
|
56 | const metadata = mergeMetadata({
|
57 | jsii: {
|
58 | pacmak: {
|
59 |
|
60 | hasDefaultInterfaces: true,
|
61 | },
|
62 | },
|
63 | }, pkg.jsii?.metadata);
|
64 | const projectInfo = {
|
65 | projectRoot,
|
66 | packageJson: pkg,
|
67 | name: _required(pkg.name, 'The "package.json" file must specify the "name" attribute'),
|
68 | version: _required(pkg.version, 'The "package.json" file must specify the "version" attribute'),
|
69 | deprecated: pkg.deprecated,
|
70 | stability: _validateStability(pkg.stability, pkg.deprecated),
|
71 | author: _toPerson(_required(pkg.author, 'The "package.json" file must specify the "author" attribute'), 'author'),
|
72 | repository: _toRepository(_required(pkg.repository, 'The "package.json" file must specify the "repository" attribute')),
|
73 | license: _validateLicense(pkg.license),
|
74 | keywords: pkg.keywords,
|
75 | main: _required(pkg.main, 'The "package.json" file must specify the "main" attribute'),
|
76 | types: _required(pkg.types, 'The "package.json" file must specify the "types" attribute'),
|
77 | dependencies,
|
78 | peerDependencies,
|
79 | dependencyClosure: transitiveDependencies,
|
80 | bundleDependencies,
|
81 | targets: {
|
82 | ..._required(pkg.jsii, 'The "package.json" file must specify the "jsii" attribute').targets,
|
83 | js: { npm: pkg.name },
|
84 | },
|
85 | metadata,
|
86 | jsiiVersionFormat: _validateVersionFormat(pkg.jsii?.versionFormat ?? 'full'),
|
87 | description: pkg.description,
|
88 | homepage: pkg.homepage,
|
89 | contributors: pkg.contributors?.map((contrib, index) => _toPerson(contrib, `contributors[${index}]`, 'contributor')),
|
90 | excludeTypescript: pkg.jsii?.excludeTypescript ?? [],
|
91 | projectReferences: pkg.jsii?.projectReferences,
|
92 | tsc: {
|
93 | outDir: pkg.jsii?.tsc?.outDir,
|
94 | rootDir: pkg.jsii?.tsc?.rootDir,
|
95 | baseUrl: pkg.jsii?.tsc?.baseUrl,
|
96 | paths: pkg.jsii?.tsc?.paths,
|
97 | forceConsistentCasingInFileNames: pkg.jsii?.tsc?.forceConsistentCasingInFileNames,
|
98 | noImplicitOverride: pkg.jsii?.tsc?.noImplicitOverride,
|
99 | noPropertyAccessFromIndexSignature: pkg.jsii?.tsc?.noPropertyAccessFromIndexSignature,
|
100 | noUncheckedIndexedAccess: pkg.jsii?.tsc?.noUncheckedIndexedAccess,
|
101 | ..._sourceMapPreferences(pkg.jsii?.tsc),
|
102 | types: pkg.jsii?.tsc?.types,
|
103 | },
|
104 | bin: pkg.bin,
|
105 | exports: pkg.exports,
|
106 | diagnostics: _loadDiagnostics(pkg.jsii?.diagnostics),
|
107 |
|
108 | tsconfig: pkg.jsii?.tsconfig,
|
109 | validateTsconfig: _validateTsconfigRuleSet(pkg.jsii?.validateTsconfig ?? 'strict'),
|
110 | };
|
111 | return { projectInfo, diagnostics };
|
112 | }
|
113 | exports.loadProjectInfo = loadProjectInfo;
|
114 | function _guessRepositoryType(url) {
|
115 | if (url.endsWith('.git')) {
|
116 | return 'git';
|
117 | }
|
118 | const parts = /^([^:]+):\/\//.exec(url);
|
119 | if (parts?.[1] !== 'http' && parts?.[1] !== 'https') {
|
120 | return parts[1];
|
121 | }
|
122 | throw new Error(`The "package.json" file must specify the "repository.type" attribute (could not guess from ${url})`);
|
123 | }
|
124 | function _sourceMapPreferences({ declarationMap, inlineSourceMap, inlineSources, sourceMap } = {}) {
|
125 |
|
126 |
|
127 | if (declarationMap == null && inlineSourceMap == null && inlineSources == null && sourceMap == null) {
|
128 | declarationMap = false;
|
129 | inlineSourceMap = true;
|
130 | inlineSources = true;
|
131 | sourceMap = undefined;
|
132 | }
|
133 | return {
|
134 | declarationMap,
|
135 | inlineSourceMap,
|
136 | inlineSources,
|
137 | sourceMap,
|
138 | };
|
139 | }
|
140 | class DependencyResolver {
|
141 | constructor() {
|
142 | this.cache = new Map();
|
143 | }
|
144 | |
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 | discoverDependencyTree(root, dependencies) {
|
152 | const ret = {};
|
153 | for (const [name, declaration] of Object.entries(dependencies)) {
|
154 |
|
155 | let resolved;
|
156 | try {
|
157 | resolved = this.resolveDependency(root, name, declaration);
|
158 | }
|
159 | catch (e) {
|
160 | LOG.error(`Unable to find a JSII dependency named "${name}" as "${declaration}". If you meant to include a non-JSII dependency, try adding it to bundledDependencies instead.`);
|
161 | throw e;
|
162 | }
|
163 | const actualVersion = resolved.dependencyInfo.assembly.version;
|
164 | if (!semver.satisfies(actualVersion, declaration)) {
|
165 | throw new Error(`Declared dependency on version ${declaration} of ${name}, but version ${actualVersion} was found`);
|
166 | }
|
167 | ret[name] = resolved.resolvedFile;
|
168 | }
|
169 | return ret;
|
170 | }
|
171 | |
172 |
|
173 |
|
174 | assemblyClosure(resolved) {
|
175 | const closure = new Map();
|
176 | const queue = Array.from(Object.values(resolved));
|
177 | while (queue.length > 0) {
|
178 | const next = queue.shift();
|
179 | const depInfo = this.cache.get(next);
|
180 | if (!depInfo) {
|
181 | throw new Error(`Path ${next} not seen before`);
|
182 | }
|
183 | if (closure.has(next)) {
|
184 | continue;
|
185 | }
|
186 | closure.set(next, depInfo.assembly);
|
187 | queue.push(...Object.values(depInfo.resolvedDependencies));
|
188 | }
|
189 | return Array.from(closure.values());
|
190 | }
|
191 | resolveDependency(root, name, declaration) {
|
192 | const { version: versionString, localPackage } = _resolveVersion(declaration, root);
|
193 | const version = new semver.Range(versionString);
|
194 | if (!version) {
|
195 | throw new Error(`Invalid semver expression for ${name}: ${versionString}`);
|
196 | }
|
197 | const jsiiFile = _tryResolveAssembly(name, localPackage, root);
|
198 | LOG.debug(`Resolved dependency ${name} to ${jsiiFile}`);
|
199 | return {
|
200 | resolvedVersion: versionString,
|
201 | resolvedFile: jsiiFile,
|
202 | dependencyInfo: this.loadAssemblyAndRecurse(jsiiFile),
|
203 | };
|
204 | }
|
205 | loadAssemblyAndRecurse(jsiiFile) {
|
206 |
|
207 | if (this.cache.has(jsiiFile)) {
|
208 | return this.cache.get(jsiiFile);
|
209 | }
|
210 | const assembly = (0, spec_1.loadAssemblyFromFile)(jsiiFile);
|
211 |
|
212 | const resolvedDependencies = assembly.dependencies
|
213 | ? this.discoverDependencyTree(path.dirname(jsiiFile), assembly.dependencies)
|
214 | : {};
|
215 | const depInfo = {
|
216 | assembly,
|
217 | resolvedDependencies,
|
218 | };
|
219 | this.cache.set(jsiiFile, depInfo);
|
220 | return depInfo;
|
221 | }
|
222 | }
|
223 | function _required(value, message) {
|
224 | if (value == null) {
|
225 | throw new Error(message);
|
226 | }
|
227 | return value;
|
228 | }
|
229 | function _toPerson(value, field, defaultRole = field) {
|
230 | if (typeof value === 'string') {
|
231 | value = (0, utils_1.parsePerson)(value);
|
232 | }
|
233 | return {
|
234 | name: _required(value.name, `The "package.json" file must specify the "${field}.name" attribute`),
|
235 | roles: value.roles ? [...new Set(value.roles)] : [defaultRole],
|
236 | email: value.email,
|
237 | url: value.url,
|
238 | organization: value.organization ? value.organization : undefined,
|
239 | };
|
240 | }
|
241 | function _toRepository(value) {
|
242 | if (typeof value === 'string') {
|
243 | value = (0, utils_1.parseRepository)(value);
|
244 | }
|
245 | return {
|
246 | url: _required(value.url, 'The "package.json" file must specify the "repository.url" attribute'),
|
247 | type: value.type || _guessRepositoryType(value.url),
|
248 | directory: value.directory,
|
249 | };
|
250 | }
|
251 | function _tryResolveAssembly(mod, localPackage, searchPath) {
|
252 | if (localPackage) {
|
253 | const result = (0, spec_1.findAssemblyFile)(localPackage);
|
254 | if (!fs.existsSync(result)) {
|
255 | throw new Error(`Assembly does not exist: ${result}`);
|
256 | }
|
257 | return result;
|
258 | }
|
259 | try {
|
260 | const dependencyDir = (0, find_utils_1.findDependencyDirectory)(mod, searchPath);
|
261 | return (0, spec_1.findAssemblyFile)(dependencyDir);
|
262 | }
|
263 | catch (e) {
|
264 | throw new Error(`Unable to locate jsii assembly for "${mod}". If this module is not jsii-enabled, it must also be declared under bundledDependencies: ${e}`);
|
265 | }
|
266 | }
|
267 | function _validateLicense(id) {
|
268 | if (id == null) {
|
269 | throw new Error('No "license" was specified in "package.json", see valid license identifiers at https://spdx.org/licenses/');
|
270 | }
|
271 | if (id === 'UNLICENSED') {
|
272 | return id;
|
273 | }
|
274 | if (!spdx.has(id)) {
|
275 | throw new Error(`Invalid license identifier "${id}", see valid license identifiers at https://spdx.org/licenses/`);
|
276 | }
|
277 | return id;
|
278 | }
|
279 | function _validateVersionFormat(format) {
|
280 | if (format !== 'short' && format !== 'full') {
|
281 | throw new Error(`Invalid jsii.versionFormat "${format}", it must be either "short" or "full" (the default)`);
|
282 | }
|
283 | return format;
|
284 | }
|
285 | function _validateStability(stability, deprecated) {
|
286 | if (!stability && deprecated) {
|
287 | stability = spec.Stability.Deprecated;
|
288 | }
|
289 | else if (deprecated && stability !== spec.Stability.Deprecated) {
|
290 | console.warn(`Package is deprecated (${deprecated}), but it's stability is ${stability} and not ${spec.Stability.Deprecated}`);
|
291 | }
|
292 | if (!stability) {
|
293 | return undefined;
|
294 | }
|
295 | if (!Object.values(spec.Stability).includes(stability)) {
|
296 | throw new Error(`Invalid stability "${stability}", it must be one of ${Object.values(spec.Stability).join(', ')}`);
|
297 | }
|
298 | return stability;
|
299 | }
|
300 | function _validateTsconfigRuleSet(ruleSet) {
|
301 | if (ruleSet == null) {
|
302 | return undefined;
|
303 | }
|
304 | if (!Object.values(tsconfig_1.TypeScriptConfigValidationRuleSet).includes(ruleSet)) {
|
305 | throw new Error(`Invalid validateTsconfig "${ruleSet}", it must be one of ${Object.values(tsconfig_1.TypeScriptConfigValidationRuleSet).join(', ')}`);
|
306 | }
|
307 | return ruleSet;
|
308 | }
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 |
|
315 |
|
316 | function _resolveVersion(dep, searchPath) {
|
317 | const matches = /^file:(.+)$/.exec(dep);
|
318 | if (!matches) {
|
319 | return { version: dep };
|
320 | }
|
321 | const localPackage = path.resolve(searchPath, matches[1]);
|
322 | return {
|
323 |
|
324 |
|
325 | version: `^${JSON.parse(fs.readFileSync(path.join(localPackage, 'package.json'), 'utf-8')).version}`,
|
326 | localPackage,
|
327 | };
|
328 | }
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
337 | function mergeMetadata(base, user) {
|
338 | if (user == null) {
|
339 | return base;
|
340 | }
|
341 | return mergeObjects(base, user);
|
342 | function mergeObjects(baseObj, override) {
|
343 | const result = {};
|
344 | const allKeys = Array.from(new Set([...Object.keys(baseObj), ...Object.keys(override)])).sort();
|
345 | for (const key of allKeys) {
|
346 | const baseValue = baseObj[key];
|
347 | const overrideValue = override[key];
|
348 | if (typeof baseValue === 'object' && typeof overrideValue === 'object') {
|
349 | if (overrideValue != null) {
|
350 | result[key] = mergeObjects(baseValue, overrideValue);
|
351 | }
|
352 | }
|
353 | else {
|
354 | result[key] = overrideValue ?? baseValue;
|
355 | }
|
356 | }
|
357 | return result;
|
358 | }
|
359 | }
|
360 | function _loadDiagnostics(entries) {
|
361 | if (entries === undefined || Object.keys(entries).length === 0) {
|
362 | return undefined;
|
363 | }
|
364 | const result = {};
|
365 | for (const code of Object.keys(entries)) {
|
366 | let category;
|
367 | switch (entries[code].trim().toLowerCase()) {
|
368 | case 'error':
|
369 | category = ts.DiagnosticCategory.Error;
|
370 | break;
|
371 | case 'warning':
|
372 | category = ts.DiagnosticCategory.Warning;
|
373 | break;
|
374 | case 'suggestion':
|
375 | category = ts.DiagnosticCategory.Suggestion;
|
376 | break;
|
377 | case 'message':
|
378 | category = ts.DiagnosticCategory.Message;
|
379 | break;
|
380 | default:
|
381 | throw new Error(`Invalid category '${entries[code]}' for code '${code}'`);
|
382 | }
|
383 | result[code] = category;
|
384 | }
|
385 | return result;
|
386 | }
|
387 | function filterDictByKey(xs, predicate) {
|
388 | const ret = {};
|
389 | for (const [key, value] of Object.entries(xs)) {
|
390 | if (predicate(key)) {
|
391 | ret[key] = value;
|
392 | }
|
393 | }
|
394 | return ret;
|
395 | }
|
396 |
|
\ | No newline at end of file |