UNPKG

19.4 kBJavaScriptView Raw
1"use strict";
2/* -----------------------------------------------------------------------------
3| Copyright (c) Jupyter Development Team.
4| Distributed under the terms of the Modified BSD License.
5|----------------------------------------------------------------------------*/
6var __importStar = (this && this.__importStar) || function (mod) {
7 if (mod && mod.__esModule) return mod;
8 var result = {};
9 if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
10 result["default"] = mod;
11 return result;
12};
13Object.defineProperty(exports, "__esModule", { value: true });
14const fs = __importStar(require("fs-extra"));
15const glob = __importStar(require("glob"));
16const path = __importStar(require("path"));
17const prettier = __importStar(require("prettier"));
18const ts = __importStar(require("typescript"));
19const get_dependency_1 = require("./get-dependency");
20const utils = __importStar(require("./utils"));
21const HEADER_TEMPLATE = `
22/*-----------------------------------------------------------------------------
23| Copyright (c) Jupyter Development Team.
24| Distributed under the terms of the Modified BSD License.
25|----------------------------------------------------------------------------*/
26
27/* This file was auto-generated by {{funcName}}() in @jupyterlab/buildutils */
28`;
29const ICON_IMPORTS_TEMPLATE = `
30import { LabIcon } from './labicon';
31
32// icon svg import statements
33{{svgImportStatements}}
34
35// LabIcon instance construction
36{{labiconConstructions}}
37`;
38const ICON_CSS_CLASSES_TEMPLATE = `
39/**
40 * (DEPRECATED) Support for consuming icons as CSS background images
41 */
42
43/* Icons urls */
44
45:root {
46 {{iconCSSUrls}}
47}
48
49/* Icon CSS class declarations */
50
51{{iconCSSDeclarations}}
52`;
53/**
54 * Ensure the integrity of a package.
55 *
56 * @param options - The options used to ensure the package.
57 *
58 * @returns A list of changes that were made to ensure the package.
59 */
60async function ensurePackage(options) {
61 const { data, pkgPath } = options;
62 const deps = data.dependencies || {};
63 const devDeps = data.devDependencies || {};
64 const seenDeps = options.depCache || {};
65 const missing = options.missing || [];
66 const unused = options.unused || [];
67 const messages = [];
68 const locals = options.locals || {};
69 const cssImports = options.cssImports || [];
70 const differentVersions = options.differentVersions || [];
71 // Verify dependencies are consistent.
72 let promises = Object.keys(deps).map(async (name) => {
73 if (differentVersions.indexOf(name) !== -1) {
74 // Skip processing packages that can have different versions
75 return;
76 }
77 if (!(name in seenDeps)) {
78 seenDeps[name] = await get_dependency_1.getDependency(name);
79 }
80 if (deps[name] !== seenDeps[name]) {
81 messages.push(`Updated dependency: ${name}@${seenDeps[name]}`);
82 }
83 deps[name] = seenDeps[name];
84 });
85 await Promise.all(promises);
86 // Verify devDependencies are consistent.
87 promises = Object.keys(devDeps).map(async (name) => {
88 if (differentVersions.indexOf(name) !== -1) {
89 // Skip processing packages that can have different versions
90 return;
91 }
92 if (!(name in seenDeps)) {
93 seenDeps[name] = await get_dependency_1.getDependency(name);
94 }
95 if (devDeps[name] !== seenDeps[name]) {
96 messages.push(`Updated devDependency: ${name}@${seenDeps[name]}`);
97 }
98 devDeps[name] = seenDeps[name];
99 });
100 await Promise.all(promises);
101 // For TypeScript files, verify imports match dependencies.
102 let filenames = [];
103 filenames = glob.sync(path.join(pkgPath, 'src/*.ts*'));
104 filenames = filenames.concat(glob.sync(path.join(pkgPath, 'src/**/*.ts*')));
105 const tsConfigPath = path.join(pkgPath, 'tsconfig.json');
106 if (!fs.existsSync(tsConfigPath)) {
107 if (utils.writePackageData(path.join(pkgPath, 'package.json'), data)) {
108 messages.push('Updated package.json');
109 }
110 return messages;
111 }
112 // Make sure typedoc config files are consistent
113 if (fs.existsSync(path.join(pkgPath, 'typedoc.json'))) {
114 const name = data.name.split('/');
115 utils.writeJSONFile(path.join(pkgPath, 'typedoc.json'), {
116 excludeNotExported: true,
117 mode: 'file',
118 out: `../../docs/api/${name[name.length - 1]}`,
119 theme: '../../typedoc-theme'
120 });
121 }
122 let imports = [];
123 // Extract all of the imports from the TypeScript files.
124 filenames.forEach(fileName => {
125 const sourceFile = ts.createSourceFile(fileName, fs.readFileSync(fileName).toString(), ts.ScriptTarget.ES6,
126 /* setParentNodes */ true);
127 imports = imports.concat(getImports(sourceFile));
128 });
129 // Make sure we are not importing CSS in a core package.
130 if (data.name.indexOf('example') === -1) {
131 imports.forEach(importStr => {
132 if (importStr.indexOf('.css') !== -1) {
133 messages.push('CSS imports are not allowed source files');
134 }
135 });
136 }
137 let names = Array.from(new Set(imports)).sort();
138 names = names.map(function (name) {
139 const parts = name.split('/');
140 if (name.indexOf('@') === 0) {
141 return parts[0] + '/' + parts[1];
142 }
143 return parts[0];
144 });
145 // Look for imports with no dependencies.
146 promises = names.map(async (name) => {
147 if (missing.indexOf(name) !== -1) {
148 return;
149 }
150 if (name === '.' || name === '..') {
151 return;
152 }
153 if (!deps[name]) {
154 if (!(name in seenDeps)) {
155 seenDeps[name] = await get_dependency_1.getDependency(name);
156 }
157 deps[name] = seenDeps[name];
158 messages.push(`Added dependency: ${name}@${seenDeps[name]}`);
159 }
160 });
161 await Promise.all(promises);
162 // Template the CSS index file.
163 if (cssImports && fs.existsSync(path.join(pkgPath, 'style/base.css'))) {
164 const funcName = 'ensurePackage';
165 let cssIndexContents = utils.fromTemplate(HEADER_TEMPLATE, { funcName }, { end: '' });
166 cssImports.forEach(cssImport => {
167 cssIndexContents += `\n@import url('~${cssImport}');`;
168 });
169 cssIndexContents += "\n\n@import url('./base.css');\n";
170 // write out cssIndexContents, if needed
171 const cssIndexPath = path.join(pkgPath, 'style/index.css');
172 if (!fs.existsSync(cssIndexPath)) {
173 fs.ensureFileSync(cssIndexPath);
174 }
175 messages.push(...ensureFile(cssIndexPath, cssIndexContents, false));
176 }
177 // Look for unused packages
178 Object.keys(deps).forEach(name => {
179 if (options.noUnused === false) {
180 return;
181 }
182 if (unused.indexOf(name) !== -1) {
183 return;
184 }
185 const isTest = data.name.indexOf('test') !== -1;
186 if (isTest) {
187 const testLibs = ['jest', 'ts-jest', '@jupyterlab/testutils'];
188 if (testLibs.indexOf(name) !== -1) {
189 return;
190 }
191 }
192 if (names.indexOf(name) === -1) {
193 const version = data.dependencies[name];
194 messages.push(`Unused dependency: ${name}@${version}: remove or add to list of known unused dependencies for this package`);
195 }
196 });
197 // Handle typedoc config output.
198 const tdOptionsPath = path.join(pkgPath, 'tdoptions.json');
199 if (fs.existsSync(tdOptionsPath)) {
200 const tdConfigData = utils.readJSONFile(tdOptionsPath);
201 const pkgDirName = pkgPath.split('/').pop();
202 tdConfigData['out'] = `../../docs/api/${pkgDirName}`;
203 utils.writeJSONFile(tdOptionsPath, tdConfigData);
204 }
205 // Handle references.
206 const references = Object.create(null);
207 Object.keys(deps).forEach(name => {
208 if (!(name in locals)) {
209 return;
210 }
211 const target = locals[name];
212 if (!fs.existsSync(path.join(target, 'tsconfig.json'))) {
213 return;
214 }
215 const ref = path.relative(pkgPath, locals[name]);
216 references[name] = ref.split(path.sep).join('/');
217 });
218 if (data.name.indexOf('example-') === -1 &&
219 Object.keys(references).length > 0) {
220 const tsConfigData = utils.readJSONFile(tsConfigPath);
221 tsConfigData.references = [];
222 Object.keys(references).forEach(name => {
223 tsConfigData.references.push({ path: references[name] });
224 });
225 utils.writeJSONFile(tsConfigPath, tsConfigData);
226 }
227 // Inherit from the base tsconfig.
228 if (fs.existsSync(tsConfigPath)) {
229 const tsConfigData = utils.readJSONFile(tsConfigPath);
230 let prefix = '';
231 let dirName = pkgPath;
232 while (!fs.existsSync(path.join(dirName, 'tsconfigbase.json'))) {
233 dirName = path.dirname(dirName);
234 prefix += '../';
235 }
236 tsConfigData.extends = path.join(prefix, 'tsconfigbase');
237 utils.writeJSONFile(tsConfigPath, tsConfigData);
238 }
239 // Handle references in tsconfig.test.json if it exists
240 const tsConfigTestPath = path.join(pkgPath, 'tsconfig.test.json');
241 if (fs.existsSync(tsConfigTestPath)) {
242 const testReferences = Object.assign({}, references);
243 // Add a reference to self to build the local package as well.
244 testReferences['.'] = '.';
245 Object.keys(devDeps).forEach(name => {
246 if (!(name in locals)) {
247 return;
248 }
249 const target = locals[name];
250 if (!fs.existsSync(path.join(target, 'tsconfig.json'))) {
251 return;
252 }
253 const ref = path.relative(pkgPath, locals[name]);
254 testReferences[name] = ref.split(path.sep).join('/');
255 });
256 const tsConfigTestData = utils.readJSONFile(tsConfigTestPath);
257 tsConfigTestData.references = [];
258 Object.keys(testReferences).forEach(name => {
259 tsConfigTestData.references.push({ path: testReferences[name] });
260 });
261 Object.keys(references).forEach(name => {
262 tsConfigTestData.references.push({ path: testReferences[name] });
263 });
264 utils.writeJSONFile(tsConfigTestPath, tsConfigTestData);
265 }
266 // Get a list of all the published files.
267 // This will not catch .js or .d.ts files if they have not been built,
268 // but we primarily use this to check for files that are published as-is,
269 // like styles, assets, and schemas.
270 const published = new Set(data.files
271 ? data.files.reduce((acc, curr) => {
272 return acc.concat(glob.sync(path.join(pkgPath, curr)));
273 }, [])
274 : []);
275 // Ensure that the `schema` directories match what is in the `package.json`
276 const schemaDir = data.jupyterlab && data.jupyterlab.schemaDir;
277 const schemas = glob.sync(path.join(pkgPath, schemaDir || 'schema', '*.json'));
278 if (schemaDir && !schemas.length) {
279 messages.push(`No schemas found in ${path.join(pkgPath, schemaDir)}.`);
280 }
281 else if (!schemaDir && schemas.length) {
282 messages.push(`Schemas found, but no schema indicated in ${pkgPath}`);
283 }
284 for (const schema of schemas) {
285 if (!published.has(schema)) {
286 messages.push(`Schema ${schema} not published in ${pkgPath}`);
287 }
288 }
289 // Ensure that the `style` directories match what is in the `package.json`
290 const styles = glob.sync(path.join(pkgPath, 'style', '**/*.*'));
291 for (const style of styles) {
292 if (!published.has(style)) {
293 messages.push(`Style file ${style} not published in ${pkgPath}`);
294 }
295 }
296 // If we have styles, ensure that 'style' field is declared
297 if (styles.length > 0) {
298 if (data.style === undefined) {
299 data.style = 'style/index.css';
300 }
301 }
302 // Ensure that sideEffects are declared, and that any styles are covered
303 if (styles.length > 0) {
304 if (data.sideEffects === undefined) {
305 messages.push(`Side effects not declared in ${pkgPath}, and styles are present.`);
306 }
307 else if (data.sideEffects === false) {
308 messages.push(`Style files not included in sideEffects in ${pkgPath}`);
309 }
310 }
311 // Ensure dependencies and dev dependencies.
312 data.dependencies = deps;
313 data.devDependencies = devDeps;
314 if (Object.keys(data.dependencies).length === 0) {
315 delete data.dependencies;
316 }
317 if (Object.keys(data.devDependencies).length === 0) {
318 delete data.devDependencies;
319 }
320 // Make sure there are no gitHead keys, which are only temporary keys used
321 // when a package is actually being published.
322 delete data.gitHead;
323 // Ensure that there is a public access set, if the package is not private.
324 if (data.private !== true) {
325 data['publishConfig'] = { access: 'public' };
326 }
327 // Ensure there is a minimal prepublishOnly script
328 if (!data.private && !data.scripts.prepublishOnly) {
329 messages.push(`prepublishOnly script missing in ${pkgPath}`);
330 data.scripts.prepublishOnly = 'npm run build';
331 }
332 if (utils.writePackageData(path.join(pkgPath, 'package.json'), data)) {
333 messages.push('Updated package.json');
334 }
335 return messages;
336}
337exports.ensurePackage = ensurePackage;
338/**
339 * An extra ensure function just for the @jupyterlab/ui-components package.
340 * Ensures that the icon svg import statements are synced with the contents
341 * of ui-components/style/icons.
342 *
343 * @param pkgPath - The path to the @jupyterlab/ui-components package.
344 * @param dorequire - If true, use `require` function in place of `import`
345 * statements when loading the icon svg files
346 *
347 * @returns A list of changes that were made to ensure the package.
348 */
349async function ensureUiComponents(pkgPath, dorequire = false) {
350 const funcName = 'ensureUiComponents';
351 const pkgName = utils.stem(pkgPath);
352 const messages = [];
353 const svgPaths = glob.sync(path.join(pkgPath, 'style/icons', '**/*.svg'));
354 /* support for glob import of icon svgs */
355 const iconSrcDir = path.join(pkgPath, 'src/icon');
356 // build the per-icon import code
357 const _svgImportStatements = [];
358 const _labiconConstructions = [];
359 svgPaths.forEach(svgPath => {
360 const svgName = utils.stem(svgPath);
361 const svgImportPath = path
362 .relative(iconSrcDir, svgPath)
363 .split(path.sep)
364 .join('/');
365 const svgstrRef = utils.camelCase(svgName) + 'Svgstr';
366 const iconRef = utils.camelCase(svgName) + 'Icon';
367 const iconName = [pkgName, utils.stem(svgPath)].join(':');
368 if (dorequire) {
369 // load the icon svg using `require`
370 _labiconConstructions.push(`export const ${iconRef} = new LabIcon({ name: '${iconName}', svgstr: require('${svgImportPath}').default });`);
371 }
372 else {
373 // load the icon svg using `import`
374 _svgImportStatements.push(`import ${svgstrRef} from '${svgImportPath}';`);
375 _labiconConstructions.push(`export const ${iconRef} = new LabIcon({ name: '${iconName}', svgstr: ${svgstrRef} });`);
376 }
377 });
378 // sort the statements and then join them
379 const svgImportStatements = _svgImportStatements.sort().join('\n');
380 const labiconConstructions = _labiconConstructions.sort().join('\n');
381 // generate the actual contents of the iconImports file
382 const iconImportsPath = path.join(iconSrcDir, 'iconimports.ts');
383 const iconImportsContents = utils.fromTemplate(HEADER_TEMPLATE + ICON_IMPORTS_TEMPLATE, { funcName, svgImportStatements, labiconConstructions });
384 messages.push(...ensureFile(iconImportsPath, iconImportsContents, false));
385 /* support for deprecated icon CSS classes */
386 const iconCSSDir = path.join(pkgPath, 'style');
387 // build the per-icon import code
388 const _iconCSSUrls = [];
389 const _iconCSSDeclarations = [];
390 svgPaths.forEach(svgPath => {
391 const svgName = utils.stem(svgPath);
392 const urlName = 'jp-icon-' + svgName;
393 const className = 'jp-' + utils.camelCase(svgName, true) + 'Icon';
394 _iconCSSUrls.push(`--${urlName}: url('${path
395 .relative(iconCSSDir, svgPath)
396 .split(path.sep)
397 .join('/')}');`);
398 _iconCSSDeclarations.push(`.${className} {background-image: var(--${urlName})}`);
399 });
400 // sort the statements and then join them
401 const iconCSSUrls = _iconCSSUrls.sort().join('\n');
402 const iconCSSDeclarations = _iconCSSDeclarations.sort().join('\n');
403 // generate the actual contents of the iconCSSClasses file
404 const iconCSSClassesPath = path.join(iconCSSDir, 'deprecated.css');
405 const iconCSSClassesContent = utils.fromTemplate(HEADER_TEMPLATE + ICON_CSS_CLASSES_TEMPLATE, { funcName, iconCSSUrls, iconCSSDeclarations });
406 messages.push(...ensureFile(iconCSSClassesPath, iconCSSClassesContent));
407 return messages;
408}
409exports.ensureUiComponents = ensureUiComponents;
410/**
411 * Ensure that contents of a file match a supplied string. If they do match,
412 * do nothing and return an empty array. If they don't match, overwrite the
413 * file and return an array with an update message.
414 *
415 * @param fpath: The path to the file being checked. The file must exist,
416 * or else this function does nothing.
417 *
418 * @param contents: The desired file contents.
419 *
420 * @param prettify: default = true. If true, format the contents with
421 * `prettier` before comparing/writing. Set to false only if you already
422 * know your code won't be modified later by the `prettier` git commit hook.
423 *
424 * @returns a string array with 0 or 1 messages.
425 */
426function ensureFile(fpath, contents, prettify = true) {
427 const messages = [];
428 if (!fs.existsSync(fpath)) {
429 // bail
430 messages.push(`Tried to ensure the contents of ${fpath}, but the file does not exist`);
431 return messages;
432 }
433 // (maybe) run the newly generated contents through prettier before comparing
434 let formatted = prettify
435 ? prettier.format(contents, { filepath: fpath, singleQuote: true })
436 : contents;
437 const prev = fs.readFileSync(fpath, { encoding: 'utf8' });
438 if (prev.indexOf('\r') !== -1) {
439 // Normalize line endings to match current content
440 formatted = formatted.replace(/\n/g, '\r\n');
441 }
442 if (prev !== formatted) {
443 // Write out changes and notify
444 fs.writeFileSync(fpath, formatted);
445 const msgpath = fpath.startsWith('/') ? fpath : `./${fpath}`;
446 messages.push(`Updated ${msgpath}`);
447 }
448 return messages;
449}
450/**
451 * Extract the module imports from a TypeScript source file.
452 *
453 * @param sourceFile - The path to the source file.
454 *
455 * @returns An array of package names.
456 */
457function getImports(sourceFile) {
458 const imports = [];
459 handleNode(sourceFile);
460 function handleNode(node) {
461 switch (node.kind) {
462 case ts.SyntaxKind.ImportDeclaration:
463 imports.push(node.moduleSpecifier.text);
464 break;
465 case ts.SyntaxKind.ImportEqualsDeclaration:
466 imports.push(node.moduleReference.expression.text);
467 break;
468 default:
469 // no-op
470 }
471 ts.forEachChild(node, handleNode);
472 }
473 return imports;
474}
475//# sourceMappingURL=ensure-package.js.map
\No newline at end of file