123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430 |
- /*-----------------------------------------------------------------------------
- | Copyright (c) Jupyter Development Team.
- | Distributed under the terms of the Modified BSD License.
- |----------------------------------------------------------------------------*/
- import * as fs from 'fs-extra';
- import * as glob from 'glob';
- import * as path from 'path';
- import * as ts from 'typescript';
- import { getDependency } from './get-dependency';
- import * as utils from './utils';
- const HEADER_TEMPLATE = `
- /*-----------------------------------------------------------------------------
- | Copyright (c) Jupyter Development Team.
- | Distributed under the terms of the Modified BSD License.
- |----------------------------------------------------------------------------*/
- /* This file was auto-generated by {{funcName}}() in @jupyterlab/buildutils */
- `;
- function generatedHeader(funcName: string): string {
- return HEADER_TEMPLATE.split('{{funcName}}')
- .join(funcName)
- .trim();
- }
- /**
- * Ensure the integrity of a package.
- *
- * @param options - The options used to ensure the package.
- *
- * @returns A list of changes that were made to ensure the package.
- */
- export async function ensurePackage(
- options: IEnsurePackageOptions
- ): Promise<string[]> {
- let { data, pkgPath } = options;
- let deps: { [key: string]: string } = data.dependencies || {};
- let devDeps: { [key: string]: string } = data.devDependencies || {};
- let seenDeps = options.depCache || {};
- let missing = options.missing || [];
- let unused = options.unused || [];
- let messages: string[] = [];
- let locals = options.locals || {};
- let cssImports = options.cssImports || [];
- let differentVersions = options.differentVersions || [];
- // Verify dependencies are consistent.
- let promises = Object.keys(deps).map(async name => {
- if (differentVersions.indexOf(name) !== -1) {
- // Skip processing packages that can have different versions
- return;
- }
- if (!(name in seenDeps)) {
- seenDeps[name] = await getDependency(name);
- }
- if (deps[name] !== seenDeps[name]) {
- messages.push(`Updated dependency: ${name}@${seenDeps[name]}`);
- }
- deps[name] = seenDeps[name];
- });
- await Promise.all(promises);
- // Verify devDependencies are consistent.
- promises = Object.keys(devDeps).map(async name => {
- if (differentVersions.indexOf(name) !== -1) {
- // Skip processing packages that can have different versions
- return;
- }
- if (!(name in seenDeps)) {
- seenDeps[name] = await getDependency(name);
- }
- if (devDeps[name] !== seenDeps[name]) {
- messages.push(`Updated devDependency: ${name}@${seenDeps[name]}`);
- }
- devDeps[name] = seenDeps[name];
- });
- await Promise.all(promises);
- // For TypeScript files, verify imports match dependencies.
- let filenames: string[] = [];
- filenames = glob.sync(path.join(pkgPath, 'src/*.ts*'));
- filenames = filenames.concat(glob.sync(path.join(pkgPath, 'src/**/*.ts*')));
- if (!fs.existsSync(path.join(pkgPath, 'tsconfig.json'))) {
- if (utils.writePackageData(path.join(pkgPath, 'package.json'), data)) {
- messages.push('Updated package.json');
- }
- return messages;
- }
- let imports: string[] = [];
- // Extract all of the imports from the TypeScript files.
- filenames.forEach(fileName => {
- let sourceFile = ts.createSourceFile(
- fileName,
- fs.readFileSync(fileName).toString(),
- (ts.ScriptTarget as any).ES6,
- /*setParentNodes */ true
- );
- imports = imports.concat(getImports(sourceFile));
- });
- // Make sure we are not importing CSS in a core package.
- if (data.name.indexOf('example') === -1) {
- imports.forEach(importStr => {
- if (importStr.indexOf('.css') !== -1) {
- messages.push('CSS imports are not allowed source files');
- }
- });
- }
- let names: string[] = Array.from(new Set(imports)).sort();
- names = names.map(function(name) {
- let parts = name.split('/');
- if (name.indexOf('@') === 0) {
- return parts[0] + '/' + parts[1];
- }
- return parts[0];
- });
- // Look for imports with no dependencies.
- promises = names.map(async name => {
- if (missing.indexOf(name) !== -1) {
- return;
- }
- if (name === '.' || name === '..') {
- return;
- }
- if (!deps[name]) {
- if (!(name in seenDeps)) {
- seenDeps[name] = await getDependency(name);
- }
- deps[name] = seenDeps[name];
- messages.push(`Added dependency: ${name}@${seenDeps[name]}`);
- }
- });
- await Promise.all(promises);
- // Template the CSS index file.
- if (cssImports && fs.existsSync(path.join(pkgPath, 'style/base.css'))) {
- let cssIndex = generatedHeader('ensurePackage');
- cssImports.forEach(cssImport => {
- cssIndex += `\n@import url('~${cssImport}');`;
- });
- cssIndex += "\n\n@import url('./base.css');\n";
- const cssPath = path.join(pkgPath, 'style/index.css');
- const prev = fs.readFileSync(cssPath, { encoding: 'utf8' });
- if (prev !== cssIndex) {
- messages.push(`Updated ./${data.style}`);
- fs.writeFileSync(cssPath, cssIndex);
- }
- }
- // Look for unused packages
- Object.keys(deps).forEach(name => {
- if (options.noUnused === false) {
- return;
- }
- if (unused.indexOf(name) !== -1) {
- return;
- }
- const isTest = data.name.indexOf('test') !== -1;
- if (isTest) {
- const testLibs = ['jest', 'ts-jest', '@jupyterlab/testutils'];
- if (testLibs.indexOf(name) !== -1) {
- return;
- }
- }
- if (names.indexOf(name) === -1) {
- let version = data.dependencies[name];
- messages.push(
- `Unused dependency: ${name}@${version}: remove or add to list of known unused dependencies for this package`
- );
- }
- });
- // Handle typedoc config output.
- const tdOptionsPath = path.join(pkgPath, 'tdoptions.json');
- if (fs.existsSync(tdOptionsPath)) {
- const tdConfigData = utils.readJSONFile(tdOptionsPath);
- const pkgDirName = pkgPath.split('/').pop();
- tdConfigData['out'] = `../../docs/api/${pkgDirName}`;
- utils.writeJSONFile(tdOptionsPath, tdConfigData);
- }
- // Handle references.
- let references: { [key: string]: string } = Object.create(null);
- Object.keys(deps).forEach(name => {
- if (!(name in locals)) {
- return;
- }
- const target = locals[name];
- if (!fs.existsSync(path.join(target, 'tsconfig.json'))) {
- return;
- }
- let ref = path.relative(pkgPath, locals[name]);
- references[name] = ref.split(path.sep).join('/');
- });
- if (
- data.name.indexOf('example-') === -1 &&
- Object.keys(references).length > 0
- ) {
- const tsConfigPath = path.join(pkgPath, 'tsconfig.json');
- const tsConfigData = utils.readJSONFile(tsConfigPath);
- tsConfigData.references = [];
- Object.keys(references).forEach(name => {
- tsConfigData.references.push({ path: references[name] });
- });
- utils.writeJSONFile(tsConfigPath, tsConfigData);
- }
- // Get a list of all the published files.
- // This will not catch .js or .d.ts files if they have not been built,
- // but we primarily use this to check for files that are published as-is,
- // like styles, assets, and schemas.
- const published = new Set<string>(
- data.files
- ? data.files.reduce((acc: string[], curr: string) => {
- return acc.concat(glob.sync(path.join(pkgPath, curr)));
- }, [])
- : []
- );
- // Ensure that the `schema` directories match what is in the `package.json`
- const schemaDir = data.jupyterlab && data.jupyterlab.schemaDir;
- const schemas = glob.sync(
- path.join(pkgPath, schemaDir || 'schema', '*.json')
- );
- if (schemaDir && !schemas.length) {
- messages.push(`No schemas found in ${path.join(pkgPath, schemaDir)}.`);
- } else if (!schemaDir && schemas.length) {
- messages.push(`Schemas found, but no schema indicated in ${pkgPath}`);
- }
- for (let schema of schemas) {
- if (!published.has(schema)) {
- messages.push(`Schema ${schema} not published in ${pkgPath}`);
- }
- }
- // Ensure that the `style` directories match what is in the `package.json`
- const styles = glob.sync(path.join(pkgPath, 'style', '**/*.*'));
- for (let style of styles) {
- if (!published.has(style)) {
- messages.push(`Style file ${style} not published in ${pkgPath}`);
- }
- }
- // If we have styles, ensure that 'style' field is declared
- if (styles.length > 0) {
- if (data.style === undefined) {
- data.style = 'style/index.css';
- }
- }
- // Ensure that sideEffects are declared, and that any styles are covered
- if (styles.length > 0) {
- if (data.sideEffects === undefined) {
- messages.push(
- `Side effects not declared in ${pkgPath}, and styles are present.`
- );
- } else if (data.sideEffects === false) {
- messages.push(`Style files not included in sideEffects in ${pkgPath}`);
- }
- }
- // Ensure dependencies and dev dependencies.
- data.dependencies = deps;
- data.devDependencies = devDeps;
- if (Object.keys(data.dependencies).length === 0) {
- delete data.dependencies;
- }
- if (Object.keys(data.devDependencies).length === 0) {
- delete data.devDependencies;
- }
- // Make sure there are no gitHead keys, which are only temporary keys used
- // when a package is actually being published.
- delete data.gitHead;
- // Ensure there is a minimal prepublishOnly script
- if (!data.private && !data.scripts.prepublishOnly) {
- messages.push(`prepublishOnly script missing in ${pkgPath}`);
- data.scripts.prepublishOnly = 'npm run build';
- }
- if (utils.writePackageData(path.join(pkgPath, 'package.json'), data)) {
- messages.push('Updated package.json');
- }
- return messages;
- }
- /**
- * An extra ensure function just for the @jupyterlab/ui-components package.
- * Ensures that the icon svg import statements are synced with the contents
- * of ui-components/style/icons.
- *
- * @param pkgPath - The path to the @jupyterlab/ui-components package.
- *
- * @returns A list of changes that were made to ensure the package.
- */
- export async function ensureUiComponents(pkgPath: string): Promise<string[]> {
- let messages: string[] = [];
- const tab = ' ';
- const iconSrcDir = path.join(pkgPath, 'src/icon');
- const svgs = glob.sync(path.join(pkgPath, 'style/icons', '**/*.svg'));
- // build the per-icon import code
- let iconImportStatements: string[] = [];
- let iconModelDeclarations: string[] = [];
- svgs.forEach(svg => {
- const name = utils.stem(svg);
- const nameCamel = utils.camelCase(name) + 'Svg';
- iconImportStatements.push(
- `import ${nameCamel} from '${path.relative(iconSrcDir, svg)}';`
- );
- iconModelDeclarations.push(
- tab + tab + `{ name: '${name}', svg: ${nameCamel} }`
- );
- });
- // generate the actual iconImports file
- let iconImports = generatedHeader('ensureUiComponents') + '\n\n';
- iconImports += "import { Icon } from './interfaces';\n\n";
- iconImports += '// icon svg import statements\n';
- iconImports += iconImportStatements.join('\n') + '\n\n';
- iconImports += '// defaultIcons definition\n';
- iconImports += 'export namespace IconImports {\n';
- iconImports +=
- tab + 'export const defaultIcons: ReadonlyArray<Icon.IModel> = [\n';
- iconImports += iconModelDeclarations.join(',\n') + '\n';
- iconImports += tab + '];\n';
- iconImports += '}\n';
- // write the iconImports file
- const iconImportsPath = path.join(iconSrcDir, 'iconImports.ts');
- const prev = fs.readFileSync(iconImportsPath, { encoding: 'utf8' });
- if (prev !== iconImports) {
- messages.push(`Updated ./${iconImportsPath}`);
- fs.writeFileSync(iconImportsPath, iconImports);
- }
- return messages;
- }
- /**
- * The options used to ensure a package.
- */
- export interface IEnsurePackageOptions {
- /**
- * The path to the package.
- */
- pkgPath: string;
- /**
- * The package data.
- */
- data: any;
- /**
- * The cache of dependency versions by package.
- */
- depCache?: { [key: string]: string };
- /**
- * A list of dependencies that can be unused.
- */
- unused?: string[];
- /**
- * A list of dependencies that can be missing.
- */
- missing?: string[];
- /**
- * A map of local package names and their relative path.
- */
- locals?: { [key: string]: string };
- /**
- * Whether to enforce that dependencies get used. Default is true.
- */
- noUnused?: boolean;
- /**
- * The css import list for the package.
- */
- cssImports?: string[];
- /**
- * Packages which are allowed to have multiple versions pulled in
- */
- differentVersions?: string[];
- }
- /**
- * Extract the module imports from a TypeScript source file.
- *
- * @param sourceFile - The path to the source file.
- *
- * @returns An array of package names.
- */
- function getImports(sourceFile: ts.SourceFile): string[] {
- let imports: string[] = [];
- handleNode(sourceFile);
- function handleNode(node: any): void {
- switch (node.kind) {
- case ts.SyntaxKind.ImportDeclaration:
- imports.push(node.moduleSpecifier.text);
- break;
- case ts.SyntaxKind.ImportEqualsDeclaration:
- imports.push(node.moduleReference.expression.text);
- break;
- default:
- // no-op
- }
- ts.forEachChild(node, handleNode);
- }
- return imports;
- }
|