UNPKG

16.3 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.loadProjectInfo = void 0;
4const fs = require("node:fs");
5const path = require("node:path");
6const spec = require("@jsii/spec");
7const spec_1 = require("@jsii/spec");
8const log4js = require("log4js");
9const semver = require("semver");
10const ts = require("typescript");
11const find_utils_1 = require("./common/find-utils");
12const jsii_diagnostic_1 = require("./jsii-diagnostic");
13const tsconfig_1 = require("./tsconfig");
14const utils_1 = require("./utils");
15// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
16const spdx = require('spdx-license-list/simple');
17const LOG = log4js.getLogger('jsii/package-info');
18function 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 // Check peerDependencies are also in devDependencies
35 // You need this to write tests properly. There are probably cases where
36 // it makes sense to have this different, so most of what this checking
37 // produces is warnings, not errors.
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 // When `true`, `jsii-pacmak` will use the `Jsii$Default` implementation in code generation even for dependencies.
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 // user-provided tsconfig
108 tsconfig: pkg.jsii?.tsconfig,
109 validateTsconfig: _validateTsconfigRuleSet(pkg.jsii?.validateTsconfig ?? 'strict'),
110 };
111 return { projectInfo, diagnostics };
112}
113exports.loadProjectInfo = loadProjectInfo;
114function _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}
124function _sourceMapPreferences({ declarationMap, inlineSourceMap, inlineSources, sourceMap } = {}) {
125 // If none of the options are specified, use the default configuration from jsii <= 1.58.0, which
126 // means inline source maps with embedded source information.
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}
140class DependencyResolver {
141 constructor() {
142 this.cache = new Map();
143 }
144 /**
145 * Discover the dependency tree starting at 'root', validating versions as we go along
146 *
147 * This primes the data structures in this class and should be called first.
148 *
149 * Return the resolved jsii dependency paths
150 */
151 discoverDependencyTree(root, dependencies) {
152 const ret = {};
153 for (const [name, declaration] of Object.entries(dependencies)) {
154 // eslint-disable-next-line no-await-in-loop
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 * From a set of resolved paths, recursively return all assemblies
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 // Only recurse if we haven't seen this assembly yet
207 if (this.cache.has(jsiiFile)) {
208 return this.cache.get(jsiiFile);
209 }
210 const assembly = (0, spec_1.loadAssemblyFromFile)(jsiiFile);
211 // Continue loading any dependencies declared in the asm
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}
223function _required(value, message) {
224 if (value == null) {
225 throw new Error(message);
226 }
227 return value;
228}
229function _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}
241function _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}
251function _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}
267function _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}
279function _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}
285function _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}
300function _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 * Resolves an NPM package specifier to a version range
311 *
312 * If it was already a version range, return it. If it the
313 * package references a local file, return the version that
314 * package is at.
315 */
316function _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 // Rendering as a caret version to maintain uniformity against the "standard".
324 // eslint-disable-next-line @typescript-eslint/no-require-imports,@typescript-eslint/no-var-requires
325 version: `^${JSON.parse(fs.readFileSync(path.join(localPackage, 'package.json'), 'utf-8')).version}`,
326 localPackage,
327 };
328}
329/**
330 * Merges two metadata blocks together.
331 *
332 * @param base the base values
333 * @param user the user-supplied values, which can override the `base` values
334 *
335 * @returns the merged metadata block
336 */
337function 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}
360function _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}
387function 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//# sourceMappingURL=project-info.js.map
\No newline at end of file