Browse Source

switched icon svg imports to auto-generated explicit `import` statements

the `ensureUiComponents` function in buildutils ensures that the imports
in iconImports.ts are synced with the contents of ui-components/style/icons.
This runs as part of `jlpm integrity`. I also improved some docs
telamonian 5 years ago
parent
commit
13f017823f

+ 68 - 3
buildutils/src/ensure-package.ts

@@ -10,15 +10,21 @@ import * as ts from 'typescript';
 import { getDependency } from './get-dependency';
 import * as utils from './utils';
 
-const CSS_HEADER = `
+const HEADER_TEMPLATE = `
 /*-----------------------------------------------------------------------------
 | Copyright (c) Jupyter Development Team.
 | Distributed under the terms of the Modified BSD License.
 |----------------------------------------------------------------------------*/
 
-/* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */
+/* 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.
  *
@@ -138,7 +144,7 @@ export async function ensurePackage(
 
   // Template the CSS index file.
   if (cssImports && fs.existsSync(path.join(pkgPath, 'style/base.css'))) {
-    let cssIndex = CSS_HEADER.trim();
+    let cssIndex = generatedHeader('ensurePackage');
     cssImports.forEach(cssImport => {
       cssIndex += `\n@import url('~${cssImport}');`;
     });
@@ -290,6 +296,65 @@ export async function ensurePackage(
   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', '**/*.svg'));
+
+  // build the per-icon import code
+  let iconImportStatements: string[] = [];
+  let iconModelDeclarations: string[] = [];
+  svgs.forEach(svg => {
+    const name = utils.stem(svg);
+    // skip the debug icons
+    if (name !== 'bad' && name !== 'blank') {
+      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 './icon';\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.
  */

+ 15 - 1
buildutils/src/ensure-repo.ts

@@ -13,7 +13,11 @@
  */
 import * as path from 'path';
 import * as utils from './utils';
-import { ensurePackage, IEnsurePackageOptions } from './ensure-package';
+import {
+  ensurePackage,
+  ensureUiComponents,
+  IEnsurePackageOptions
+} from './ensure-package';
 
 type Dict<T> = { [key: string]: T };
 
@@ -341,6 +345,16 @@ export async function ensureIntegrity(): Promise<boolean> {
     }
   }
 
+  // ensure the icon svg imports
+  pkgMessages = await ensureUiComponents(pkgPaths['@jupyterlab/ui-components']);
+  if (pkgMessages.length > 0) {
+    let pkgName = '@jupyterlab/ui-components';
+    if (!messages[pkgName]) {
+      messages[pkgName] = [];
+    }
+    messages[pkgName] = messages[pkgName].concat(pkgMessages);
+  }
+
   // Handle the top level package.
   let corePath = path.resolve('.', 'package.json');
   let coreData: any = utils.readJSONFile(corePath);

+ 40 - 0
buildutils/src/utils.ts

@@ -286,3 +286,43 @@ export function ensureUnixPathSep(source: string) {
   }
   return source.replace(backSlash, '/');
 }
+
+/**
+ * Get the last portion of a path, without its extension (if any).
+ *
+ * @param path - The file path.
+ *
+ * @returns the last part of the path, sans extension.
+ */
+export function stem(path: string): string {
+  return path
+    .split('\\')
+    .pop()
+    .split('/')
+    .pop()
+    .split('.')
+    .shift();
+}
+
+/**
+ * Given a 'snake-case', 'snake_case', or 'snake case' string,
+ * will return the camel case version: 'snakeCase'.
+ *
+ * @param str: the snake-case input string.
+ *
+ * @param upper: default = false. If true, the first letter of the
+ * returned string will be capitalized.
+ *
+ * @returns the camel case version of the input string.
+ */
+export function camelCase(str: string, upper: boolean = false): string {
+  return str.replace(/(?:^\w|[A-Z]|\b\w|\s+|-+|_+)/g, function(match, index) {
+    if (+match === 0 || match[0] === '-') {
+      return '';
+    } else if (index === 0 && !upper) {
+      return match.toLowerCase();
+    } else {
+      return match.toUpperCase();
+    }
+  });
+}

+ 23 - 0
packages/ui-components/README.md

@@ -1,3 +1,26 @@
 # @jupyterlab/ui-components
 
 A JupyterLab package that provides UI elements of various types (React components, DOM elements, etc) to core JupyterLab packages and third-party extensions.
+
+## Icon usage notes
+
+The icons are organized into various categories in `./style/icons`, based on where/how they are used in Jupyterlab core. Some icons fall into multiple categories, and are noted here:
+
+- `filetype/file.svg`
+  - filetype
+  - settingeditor
+- `filetype/folder.svg`
+  - breadcrumb
+  - filetype
+  - sidebar
+- `filetype/markdown.svg`
+  - filetype
+  - settingeditor
+- `filetype/notebook.svg`
+  - filetype
+  - launcher
+  - settingeditor
+- `statusbar/terminal.svg`
+  - launcher
+  - statusbar
+  - settingeditor

+ 1 - 32
packages/ui-components/src/icon/icon.ts

@@ -1,6 +1,6 @@
 import { Token } from '@phosphor/coreutils';
 
-import { PathExt } from '@jupyterlab/coreutils';
+// import { PathExt } from '@jupyterlab/coreutils';
 
 import { IIconStyle } from '../style/icon';
 import React from 'react';
@@ -62,35 +62,4 @@ export namespace Icon {
     className?: string;
     title?: string;
   }
-
-  /**
-   * Import all svgs from a directory. The input argument should be
-   * of the form `require.context('raw-loader!<path>', true, /\.svg$/)`.
-   * <path> should be a string literal path, as this is needed by `require`.
-   */
-  export function importSvgs(r: any, exclude: string[] = []): IModel[] {
-    const excset = new Set(exclude);
-
-    return r.keys().reduce((svgs: IModel[], item: string, index: number) => {
-      const name = PathExt.stem(item);
-      if (!excset.has(name)) {
-        svgs.push({ name: name, svg: r(item).default });
-      }
-      return svgs;
-    }, []);
-  }
-
-  // create the array of default icon models
-  let icons: IModel[];
-  try {
-    // require.context is supplied by Webpack, and doesn't play nice with jest
-    icons = importSvgs(
-      require.context('raw-loader!../../style/icons', true, /\.svg$/),
-      ['bad', 'blank']
-    );
-  } catch (e) {
-    // fallback for jest tests
-    icons = [];
-  }
-  export const defaultIcons: ReadonlyArray<IModel> = icons;
 }

+ 62 - 0
packages/ui-components/src/icon/iconimports.ts

@@ -0,0 +1,62 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+/* This file was auto-generated by ensureUiComponents() in @jupyterlab/buildutils */
+
+import { Icon } from './icon';
+
+// icon svg import statements
+import fileSvg from '../../style/icons/filetype/file.svg';
+import folderSvg from '../../style/icons/filetype/folder.svg';
+import html5Svg from '../../style/icons/filetype/html5.svg';
+import imageSvg from '../../style/icons/filetype/image.svg';
+import jsonSvg from '../../style/icons/filetype/json.svg';
+import markdownSvg from '../../style/icons/filetype/markdown.svg';
+import notebookSvg from '../../style/icons/filetype/notebook.svg';
+import pythonSvg from '../../style/icons/filetype/python.svg';
+import rKernelSvg from '../../style/icons/filetype/r-kernel.svg';
+import reactSvg from '../../style/icons/filetype/react.svg';
+import spreadsheetSvg from '../../style/icons/filetype/spreadsheet.svg';
+import yamlSvg from '../../style/icons/filetype/yaml.svg';
+import jupyterFaviconSvg from '../../style/icons/jupyter-favicon.svg';
+import buildSvg from '../../style/icons/sidebar/build.svg';
+import extensionSvg from '../../style/icons/sidebar/extension.svg';
+import paletteSvg from '../../style/icons/sidebar/palette.svg';
+import runningSvg from '../../style/icons/sidebar/running.svg';
+import tabSvg from '../../style/icons/sidebar/tab.svg';
+import kernelSvg from '../../style/icons/statusbar/kernel.svg';
+import lineFormSvg from '../../style/icons/statusbar/line-form.svg';
+import notTrustedSvg from '../../style/icons/statusbar/not-trusted.svg';
+import terminalSvg from '../../style/icons/statusbar/terminal.svg';
+import trustedSvg from '../../style/icons/statusbar/trusted.svg';
+
+// defaultIcons definition
+export namespace IconImports {
+  export const defaultIcons: ReadonlyArray<Icon.IModel> = [
+    { name: 'file', svg: fileSvg },
+    { name: 'folder', svg: folderSvg },
+    { name: 'html5', svg: html5Svg },
+    { name: 'image', svg: imageSvg },
+    { name: 'json', svg: jsonSvg },
+    { name: 'markdown', svg: markdownSvg },
+    { name: 'notebook', svg: notebookSvg },
+    { name: 'python', svg: pythonSvg },
+    { name: 'r-kernel', svg: rKernelSvg },
+    { name: 'react', svg: reactSvg },
+    { name: 'spreadsheet', svg: spreadsheetSvg },
+    { name: 'yaml', svg: yamlSvg },
+    { name: 'jupyter-favicon', svg: jupyterFaviconSvg },
+    { name: 'build', svg: buildSvg },
+    { name: 'extension', svg: extensionSvg },
+    { name: 'palette', svg: paletteSvg },
+    { name: 'running', svg: runningSvg },
+    { name: 'tab', svg: tabSvg },
+    { name: 'kernel', svg: kernelSvg },
+    { name: 'line-form', svg: lineFormSvg },
+    { name: 'not-trusted', svg: notTrustedSvg },
+    { name: 'terminal', svg: terminalSvg },
+    { name: 'trusted', svg: trustedSvg }
+  ];
+}

+ 2 - 1
packages/ui-components/src/icon/iconregistry.tsx

@@ -7,6 +7,7 @@ import { classes } from 'typestyle/lib';
 import { Text } from '@jupyterlab/coreutils';
 
 import { IIconRegistry, Icon } from './icon';
+import { IconImports } from './iconimports';
 import { iconStyle, iconStyleFlat } from '../style/icon';
 
 import badSvg from '../../style/icons/bad.svg';
@@ -19,7 +20,7 @@ export class IconRegistry implements IIconRegistry {
   constructor(options: IconRegistry.IOptions = {}) {
     this._debug = !!options.debug;
 
-    let icons = options.initialIcons || Icon.defaultIcons;
+    let icons = options.initialIcons || IconImports.defaultIcons;
     this.addIcon(...icons);
 
     // add the bad state and blank icons

+ 1 - 0
packages/ui-components/src/icon/index.ts

@@ -2,5 +2,6 @@
 // Distributed under the terms of the Modified BSD License.
 
 export * from './icon';
+export * from './iconImports';
 export * from './iconregistry';
 export * from './tabbarsvg';

+ 43 - 2
packages/ui-components/src/svg.d.ts

@@ -1,15 +1,56 @@
 // Copyright (c) Jupyter Development Team.
 // Distributed under the terms of the Modified BSD License.
 
-// for use with raw-loader in webpack
+// including this file in a package allows for the use of import statements
+// with svg files. Example: `import xSvg from 'path/xSvg.svg'`
+
+// for use with raw-loader in Webpack.
+// The svg will be imported as a raw string
+
 declare module '*.svg' {
   const value: string;
   export default value;
 }
 
-// for use with svg-react-loader in webpack
+// for use with svg-react-loader in Webpack.
+// The svg will be imported as a ReactElement
+
 // declare module '*.svg' {
 //   import { HTMLAttributes } from 'react';
 //   const value: React.ComponentType<HTMLAttributes<SVGElement>>;
 //   export default value;
 // }
+
+// as an alternative to importing svgs one at a time, you can do a glob import
+// using `context.requires`. This is a Webpack only extension. Implementation:
+
+// /**
+//  * Import all svgs from a directory. The input argument should be
+//  * of the form `require.context('raw-loader!<path>', true, /\.svg$/)`.
+//  * <path> should be a string literal path, as this is needed by `require`.
+//  */
+// export function importSvgs(r: any, exclude: string[] = []): IModel[] {
+//   const excset = new Set(exclude);
+//
+//   return r.keys().reduce((svgs: IModel[], item: string, index: number) => {
+//     const name = PathExt.stem(item);
+//     if (!excset.has(name)) {
+//       svgs.push({ name: name, svg: r(item).default });
+//     }
+//     return svgs;
+//   }, []);
+// }
+//
+// // create the array of default icon models
+// let icons: IModel[];
+// try {
+//   // require.context is supplied by Webpack, and doesn't play nice with jest
+//   icons = importSvgs(
+//     require.context('raw-loader!../../style/icons', true, /\.svg$/),
+//     ['bad', 'blank']
+//   );
+// } catch (e) {
+//   // fallback for jest tests
+//   icons = [];
+// }
+// export const defaultIcons: ReadonlyArray<IModel> = icons;

+ 0 - 1
packages/ui-components/style/icons/sidebar/notes.md

@@ -1 +0,0 @@
-- The filebrowser sidebar icon can be found at `packages/ui-components/style/icons/filetype/folder.svg`