UNPKG

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