diff --git a/.idea/dictionaries/develar.xml b/.idea/dictionaries/develar.xml index 14d171b5276..81c5504169e 100644 --- a/.idea/dictionaries/develar.xml +++ b/.idea/dictionaries/develar.xml @@ -17,6 +17,7 @@ enoent github globaldots + globby hicolor libgcrypt makedeb @@ -31,6 +32,7 @@ promisify repos rimraf + testapp tsconfig veyor winstaller diff --git a/README.md b/README.md index 512b1158262..687cd4032d7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ Complete solution to build ready for distribution and "auto update" installers of your app for OS X, Windows and Linux. -* [Native application dependencies](http://electron.atom.io/docs/latest/tutorial/using-native-node-modules/) compilation (only if two-package.json project layout used). +* [Native application dependencies](http://electron.atom.io/docs/latest/tutorial/using-native-node-modules/) compilation (only if [two-package.json project structure](#two-packagejson-structure) used). * [Auto Update](#auto-update) ready application packaging. * [Code Signing](#code-signing) on a CI server or development machine. * [Build version management](#build-version-management). @@ -12,9 +12,30 @@ Complete solution to build ready for distribution and "auto update" installers o Real project example — [onshape-desktop-shell](https://github.com/develar/onshape-desktop-shell). +# Two package.json structure + +We strongly recommend to use **two** package.json files (it is not required, you can build project with any structure). + +1. For development + + In the root of the project. + Here you declare dependencies for your development environment and build scripts. + +2. For your application + + In the `app` directory. *Only this directory is distributed with real application.* + +Why the two package.json structure is ideal and how it solves a lot of issues +([#39](https://github.com/loopline-systems/electron-builder/issues/39), +[#182](https://github.com/loopline-systems/electron-builder/issues/182), +[#230](https://github.com/loopline-systems/electron-builder/issues/230))? + +1. Native npm modules (those written in C, not JavaScript) need to be compiled, and here we have two different compilation targets for them. Those used in application need to be compiled against electron runtime, and all `devDependencies` need to be compiled against your locally installed node.js. Thanks to having two files this is trivial. +2. When you package the app for distribution there is no need to add up to size of the app with your `devDependencies`. Here those are always not included (because reside outside the `app` directory). + # Configuration ## In short -1. Ensure that required fields are specified in the application `package.json`: + 1. Ensure that required fields are specified in the application `package.json`: Standard `name`, `description`, `version` and `author`. diff --git a/package.json b/package.json index 27244eed2d1..0d77c1fbc2e 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "electron-winstaller-fixed": "^2.0.5-beta.7", "fs-extra": "^0.26.5", "fs-extra-p": "^0.1.0", + "globby": "^4.0.0", "gm": "^1.21.1", "hosted-git-info": "^2.1.4", "lodash.template": "^4.2.2", @@ -88,8 +89,8 @@ "ts-babel": "^0.6.1", "tsconfig-glob": "^0.4.1", "tslint": "next", - "typescript": "1.9.0-dev.20160307", - "validate-commit-msg": "^2.3.1" + "typescript": "^1.9.0-dev.20160313", + "validate-commit-msg": "^2.4.0" }, "babel": { "plugins": [ diff --git a/src/linuxPackager.ts b/src/linuxPackager.ts index c5fcc98bd32..043737d59b9 100644 --- a/src/linuxPackager.ts +++ b/src/linuxPackager.ts @@ -25,7 +25,7 @@ export class LinuxPackager extends PlatformPackager { this.debOptions = Object.assign({ name: this.metadata.name, comment: this.metadata.description, - }, this.customDistOptions) + }, this.customBuildOptions) if (this.options.dist) { const tempDir = tmpDir({ @@ -45,7 +45,7 @@ export class LinuxPackager extends PlatformPackager { const tempDir = await tempDirPromise const promises: Array>> = [] - if (this.customDistOptions == null || this.customDistOptions.desktop == null) { + if (this.customBuildOptions == null || this.customBuildOptions.desktop == null) { promises.push(this.computeDesktopIconPath(tempDir)) } diff --git a/src/macPackager.ts b/src/macPackager.ts index 747a158a58f..4876e1beb6f 100644 --- a/src/macPackager.ts +++ b/src/macPackager.ts @@ -1,5 +1,5 @@ import { PlatformPackager, BuildInfo } from "./platformPackager" -import { Platform } from "./metadata" +import { Platform, PlatformSpecificBuildOptions } from "./metadata" import * as path from "path" import { Promise as BluebirdPromise } from "bluebird" import { log, spawn } from "./util" @@ -8,7 +8,10 @@ import { createKeychain, deleteKeychain, CodeSigningInfo, generateKeychainName, const __awaiter = require("./awaiter") Array.isArray(__awaiter) -export default class MacPackager extends PlatformPackager { +export interface OsXBuildOptions extends PlatformSpecificBuildOptions, appdmg.Specification { +} + +export default class MacPackager extends PlatformPackager { codeSigningInfo: Promise constructor(info: BuildInfo, cleanupTasks: Array<() => Promise>) { @@ -55,7 +58,7 @@ export default class MacPackager extends PlatformPackager new BluebirdPromise((resolve, reject) => { log("Creating DMG") - const specification: appdmg.Specification = { + const specification: appdmg.Specification = Object.assign({ title: this.appName, icon: path.join(this.buildResourcesDir, "icon.icns"), "icon-size": 80, @@ -68,15 +71,7 @@ export default class MacPackager extends PlatformPackager "x": 130, "y": 220, "type": "file" } ] - } - - if (this.customDistOptions != null) { - Object.assign(specification, this.customDistOptions) - } - - if (specification.title == null) { - specification.title = this.appName - } + }, this.customBuildOptions) specification.contents[1].path = path.join(appOutDir, this.appName + ".app") @@ -110,6 +105,6 @@ export default class MacPackager extends PlatformPackager cwd: outDir, stdio: "inherit", }) - .thenReturn(outDir + "/" + resultPath) + .thenReturn(path.join(outDir, resultPath)) } } \ No newline at end of file diff --git a/src/metadata.ts b/src/metadata.ts index 861ad51b253..46d81c1dfa0 100644 --- a/src/metadata.ts +++ b/src/metadata.ts @@ -21,7 +21,7 @@ export function getProductName(metadata: AppMetadata) { } export interface DevMetadata extends Metadata { - readonly build: DevBuildMetadata + readonly build?: DevBuildMetadata readonly directories?: MetadataDirectories } @@ -56,9 +56,15 @@ export interface MetadataDirectories { } export interface DevBuildMetadata { - readonly osx: appdmg.Specification - readonly win: any, - readonly linux: any + readonly osx?: appdmg.Specification + readonly win?: any, + readonly linux?: any + + readonly extraResources?: Array +} + +export interface PlatformSpecificBuildOptions { + readonly extraResources?: Array } export class Platform { @@ -72,4 +78,14 @@ export class Platform { toString() { return this.name } + + public static fromNodePlatform(name: string): Platform { + switch (name) { + case "darwin": return Platform.OSX + case "win32": return Platform.WINDOWS + case "linux": return Platform.LINUX + } + + throw new Error("Unknown platform: " + name) + } } \ No newline at end of file diff --git a/src/packager.ts b/src/packager.ts index 0a98d10cd95..ac80b7f6376 100644 --- a/src/packager.ts +++ b/src/packager.ts @@ -1,4 +1,4 @@ -import * as fs from "fs" +import { accessSync } from "fs" import * as path from "path" import { DEFAULT_APP_DIR_NAME, installDependencies, log, getElectronVersion, readPackageJson } from "./util" import { all, executeFinally } from "./promise" @@ -120,7 +120,7 @@ export class Packager implements BuildInfo { const absoluteAppPath = path.join(this.projectDir, customAppPath) try { - fs.accessSync(absoluteAppPath) + accessSync(absoluteAppPath) } catch (e) { if (required) { diff --git a/src/platformPackager.ts b/src/platformPackager.ts index bdc50491064..9de1ad4dc6f 100644 --- a/src/platformPackager.ts +++ b/src/platformPackager.ts @@ -1,9 +1,11 @@ import { InfoRetriever, ProjectMetadataProvider } from "./repositoryInfo" -import { AppMetadata, DevMetadata, Platform, getProductName } from "./metadata" +import { AppMetadata, DevMetadata, Platform, PlatformSpecificBuildOptions, getProductName } from "./metadata" import EventEmitter = NodeJS.EventEmitter import { Promise as BluebirdPromise } from "bluebird" import * as path from "path" import packager = require("electron-packager-tf") +import globby = require("globby") +import { copy } from "fs-extra-p" //noinspection JSUnusedLocalSymbols const __awaiter = require("./awaiter") @@ -44,7 +46,7 @@ export interface BuildInfo extends ProjectMetadataProvider { eventEmitter: EventEmitter } -export abstract class PlatformPackager implements ProjectMetadataProvider { +export abstract class PlatformPackager implements ProjectMetadataProvider { protected readonly options: PackagerOptions protected readonly projectDir: string @@ -53,7 +55,7 @@ export abstract class PlatformPackager implements ProjectMetadataProvider { readonly metadata: AppMetadata readonly devMetadata: DevMetadata - customDistOptions: DC + customBuildOptions: DC readonly appName: string @@ -67,10 +69,8 @@ export abstract class PlatformPackager implements ProjectMetadataProvider { this.buildResourcesDir = path.resolve(this.projectDir, this.relativeBuildResourcesDirname) - if (this.options.dist) { - const buildMetadata: any = info.devMetadata.build - this.customDistOptions = buildMetadata == null ? buildMetadata : buildMetadata[this.platform.buildConfigurationKey] - } + const buildMetadata: any = info.devMetadata.build + this.customBuildOptions = buildMetadata == null ? buildMetadata : buildMetadata[this.platform.buildConfigurationKey] this.appName = getProductName(this.metadata) } @@ -84,7 +84,7 @@ export abstract class PlatformPackager implements ProjectMetadataProvider { this.info.eventEmitter.emit("artifactCreated", file, this.platform) } - pack(platform: string, outDir: string, appOutDir: string, arch: string): Promise { + async pack(platform: string, outDir: string, appOutDir: string, arch: string): Promise { const version = this.metadata.version let buildVersion = version const buildNumber = process.env.TRAVIS_BUILD_NUMBER || process.env.APPVEYOR_BUILD_NUMBER || process.env.CIRCLE_BUILD_NUM @@ -116,7 +116,28 @@ export abstract class PlatformPackager implements ProjectMetadataProvider { // this option only for windows-installer delete options.iconUrl - return pack(options) + await pack(options) + + const buildMetadata: any = this.devMetadata.build + let extraResources: Array = buildMetadata == null ? null : buildMetadata.extraResources + + const platformSpecificExtraResources = this.customBuildOptions == null ? null : this.customBuildOptions.extraResources + if (platformSpecificExtraResources != null) { + extraResources = extraResources == null ? platformSpecificExtraResources : extraResources.concat(platformSpecificExtraResources) + } + + if (extraResources != null) { + const expandedPatterns = extraResources.map(it => it + .replace(/\$\{arch\}/g, arch) + .replace(/\$\{os\}/g, this.platform.buildConfigurationKey)) + await BluebirdPromise.map(await globby(expandedPatterns, {cwd: this.projectDir}), it => { + let resourcesDir = appOutDir + if (platform === "darwin") { + resourcesDir = path.join(resourcesDir, this.appName + ".app", "Contents", "Resources") + } + return copy(path.join(this.projectDir, it), path.join(resourcesDir, it)) + }) + } } abstract packageInDistributableFormat(outDir: string, appOutDir: string, arch: string): Promise diff --git a/src/winPackager.ts b/src/winPackager.ts index c30693d90f3..22b80dcd57f 100644 --- a/src/winPackager.ts +++ b/src/winPackager.ts @@ -1,15 +1,23 @@ import { downloadCertificate } from "./codeSign" import { Promise as BluebirdPromise } from "bluebird" import { PlatformPackager, BuildInfo } from "./platformPackager" -import { Platform } from "./metadata" +import { Platform, PlatformSpecificBuildOptions } from "./metadata" import * as path from "path" import { log } from "./util" import { readFile, deleteFile, stat, rename, copy, emptyDir, Stats, writeFile } from "fs-extra-p" +//noinspection JSUnusedLocalSymbols const __awaiter = require("./awaiter") -Array.isArray(__awaiter) -export default class WinPackager extends PlatformPackager { +export interface WinBuildOptions extends PlatformSpecificBuildOptions { + readonly certificateFile?: string + readonly certificatePassword?: string + + readonly icon?: string + readonly iconUrl?: string +} + +export default class WinPackager extends PlatformPackager { certFilePromise: Promise isNsis: boolean @@ -23,7 +31,7 @@ export default class WinPackager extends PlatformPackager { // "Error: EBUSY: resource busy or locked, unlink 'C:\Users\appveyor\AppData\Local\Temp\1\icon.ico'" // on appveyor (well, yes, it is a Windows bug) // Because NSIS support will be dropped some day, correct solution is not implemented - const iconPath = this.customDistOptions == null ? null : this.customDistOptions.icon + const iconPath = this.customBuildOptions == null ? null : this.customBuildOptions.icon require("../lib/win").copyAssetsToTmpFolder(iconPath || path.join(this.buildResourcesDir, "icon.ico")) } @@ -66,8 +74,8 @@ export default class WinPackager extends PlatformPackager { async packageInDistributableFormat(outDir: string, appOutDir: string, arch: string): Promise { let iconUrl = this.metadata.build.iconUrl if (!iconUrl) { - if (this.customDistOptions != null) { - iconUrl = this.customDistOptions.iconUrl + if (this.customBuildOptions != null) { + iconUrl = this.customBuildOptions.iconUrl } if (!iconUrl) { if (this.info.repositoryInfo != null) { @@ -103,7 +111,7 @@ export default class WinPackager extends PlatformPackager { certificatePassword: this.options.cscKeyPassword, fixUpPaths: false, usePackageJson: false - }, this.customDistOptions) + }, this.customBuildOptions) // we use metadata.name instead of appName because appName can contains unsafe chars const installerExePath = path.join(installerOutDir, this.metadata.name + "Setup-" + version + archSuffix + ".exe") @@ -185,7 +193,7 @@ export default class WinPackager extends PlatformPackager { icon: options.setupIcon, publisher: options.authors, verbosity: 2 - }, this.customDistOptions) + }, this.customBuildOptions) } })) } diff --git a/test/src/BuildTest.ts b/test/src/BuildTest.ts index 33ceace5c24..f3dbe366795 100644 --- a/test/src/BuildTest.ts +++ b/test/src/BuildTest.ts @@ -1,8 +1,10 @@ import test from "./helpers/avaEx" -import { assertPack, modifyPackageJson } from "./helpers/packTester" -import { move, writeJson, readJson } from "fs-extra-p" +import { assertPack, modifyPackageJson, platform } from "./helpers/packTester" +import { move, mkdirs, outputFile } from "fs-extra-p" import { Promise as BluebirdPromise } from "bluebird" import * as path from "path" +import { assertThat } from "./helpers/fileAssert" +import { Platform, PackagerOptions } from "out" //noinspection JSUnusedLocalSymbols const __awaiter = require("out/awaiter") @@ -13,18 +15,15 @@ if (process.env.TRAVIS !== "true") { } test.ifOsx("mac: two-package.json", async () => { - await assertPack("test-app", "darwin") + await assertPack("test-app", platform("darwin")) }) test.ifOsx("mac: one-package.json", async () => { - await assertPack("test-app-one", "darwin") + await assertPack("test-app-one", platform("darwin")) }) test("custom app dir", async () => { - await assertPack("test-app-one", getPossiblePlatforms(), { - // speed up tests, we don't need check every arch - arch: process.arch - }, true, (projectDir) => { + await assertPack("test-app-one", allPlatformsAndCurrentArch(), true, (projectDir) => { return BluebirdPromise.all([ modifyPackageJson(projectDir, data => { data.directories = { @@ -37,16 +36,69 @@ test("custom app dir", async () => { }) test("productName with space", async () => { - await assertPack("test-app-one", getPossiblePlatforms(), { - // speed up tests, we don't need check every arch - arch: process.arch - }, true, (projectDir) => { + await assertPack("test-app-one", allPlatformsAndCurrentArch(), true, (projectDir) => { return modifyPackageJson(projectDir, data => { data.productName = "Test App" }) }) }) +test("copy extra resource", async () => { + const platform = process.platform + const osName = Platform.fromNodePlatform(platform).buildConfigurationKey + + await assertPack("test-app", { + platform: [platform], + arch: process.arch, + dist: false + }, true, (projectDir) => { + return BluebirdPromise.all([ + modifyPackageJson(projectDir, data => { + if (data.build == null) { + data.build = {} + } + data.build.extraResources = [ + "foo", + "bar/hello.txt", + "bar/${arch}.txt", + "${os}/${arch}.txt", + ] + + data.build[osName] = { + extraResources: [ + "platformSpecific" + ] + } + }), + mkdirs(path.join(projectDir, "foo")), + outputFile(path.join(projectDir, "bar/hello.txt"), "data"), + outputFile(path.join(projectDir, `bar/${process.arch}.txt`), "data"), + outputFile(path.join(projectDir, `${osName}/${process.arch}.txt`), "data"), + outputFile(path.join(projectDir, "platformSpecific"), "platformSpecific"), + outputFile(path.join(projectDir, "ignoreMe.txt"), "ignoreMe"), + ]) + }, async (projectDir) => { + let resourcesDir = path.join(projectDir, "dist", "TestApp-" + platform + "-" + process.arch) + if (platform === "darwin") { + resourcesDir = path.join(resourcesDir, "TestApp.app", "Contents", "Resources") + } + await assertThat(path.join(resourcesDir, "foo")).isDirectory() + await assertThat(path.join(resourcesDir, "bar/hello.txt")).isFile() + await assertThat(path.join(resourcesDir, `bar/${process.arch}.txt`)).isFile() + await assertThat(path.join(resourcesDir, `${osName}/${process.arch}.txt`)).isFile() + await assertThat(path.join(resourcesDir, "platformSpecific")).isFile() + await assertThat(path.join(resourcesDir, "ignoreMe.txt")).doesNotExist() + }) +}) + +function allPlatformsAndCurrentArch(): PackagerOptions { + return { + platform: getPossiblePlatforms(), + // speed up tests, we don't need check every arch + arch: process.arch, + } +} + function getPossiblePlatforms(): Array { const isCi = process.env.CI != null if (process.platform === "darwin") { diff --git a/test/src/helpers/fileAssert.ts b/test/src/helpers/fileAssert.ts new file mode 100644 index 00000000000..99199b692c6 --- /dev/null +++ b/test/src/helpers/fileAssert.ts @@ -0,0 +1,39 @@ +import { stat } from "fs-extra-p" +import { AssertionError } from "assert" + +//noinspection JSUnusedLocalSymbols +const __awaiter = require("out/awaiter") + +export function assertThat(path: string) { + return new FileAssertions(path) +} + +class FileAssertions { + constructor (private path: string) { + } + + async isFile() { + const info = await stat(this.path) + if (!info.isFile()) { + throw new Error(`Path ${this.path} is not a file`) + } + } + + async isDirectory() { + const info = await stat(this.path) + if (!info.isDirectory()) { + throw new Error(`Path ${this.path} is not a file`) + } + } + + async doesNotExist() { + try { + await stat(this.path) + } + catch (e) { + return + } + + throw new Error(`Path ${this.path} must not exist`) + } +} \ No newline at end of file diff --git a/test/src/helpers/packTester.ts b/test/src/helpers/packTester.ts index 00984dc0b9f..e53d5197f8a 100644 --- a/test/src/helpers/packTester.ts +++ b/test/src/helpers/packTester.ts @@ -17,7 +17,11 @@ const __awaiter = require("out/awaiter") const tmpDirPrefix = "electron-builder-test-" + process.pid + "-" let tmpDirCounter = 0 -export async function assertPack(fixtureName: string, platform: string | Array, packagerOptions?: PackagerOptions, useTempDir?: boolean, tempDirCreated?: (projectDir: string) => Promise) { +export async function assertPack(fixtureName: string, + packagerOptions: PackagerOptions, + useTempDir?: boolean, + tempDirCreated?: (projectDir: string) => Promise, + packed?: (projectDir: string) => Promise) { let projectDir = path.join(__dirname, "..", "..", "fixtures", fixtureName) // const isDoNotUseTempDir = platform === "darwin" const customTmpDir = process.env.TEST_APP_TMP_DIR @@ -42,8 +46,16 @@ export async function assertPack(fixtureName: string, platform: string | Array { + const packager = new Packager(packagerOptions) const artifacts: Map> = new Map() packager.artifactCreated((file, platform) => { @@ -79,11 +85,15 @@ async function packAndCheck(projectDir: string, platforms: string[], packagerOpt await packager.build() + if (!packagerOptions.dist) { + return + } + for (let key of artifacts.keys()) { artifacts.set(key, pathSorter(artifacts.get(key))) } - const expandedPlatforms = normalizePlatforms(platforms) + const expandedPlatforms = normalizePlatforms(packagerOptions.platform) if (expandedPlatforms.includes("darwin")) { await checkOsXResult(packager, artifacts.get(Platform.OSX)) } @@ -171,4 +181,10 @@ export async function modifyPackageJson(projectDir: string, task: (data: any) => const data = await readJson(file) task(data) return await writeJson(file, data) +} + +export function platform(platform: string): PackagerOptions { + return { + platform: [platform] + } } \ No newline at end of file diff --git a/test/src/linuxPackagerTest.ts b/test/src/linuxPackagerTest.ts index b30c5e7c806..18113986eea 100644 --- a/test/src/linuxPackagerTest.ts +++ b/test/src/linuxPackagerTest.ts @@ -1,13 +1,13 @@ import test from "./helpers/avaEx" -import { assertPack } from "./helpers/packTester" +import { assertPack, platform } from "./helpers/packTester" //noinspection JSUnusedLocalSymbols const __awaiter = require("out/awaiter") test.ifNotWindows("linux", async () => { - await assertPack("test-app-one", "linux") + await assertPack("test-app-one", platform("linux")) }) test.ifNotWindows("no-author-email", async (t) => { - t.throws(assertPack("no-author-email", "linux"), /Please specify author 'email' in .*/) + t.throws(assertPack("no-author-email", platform("linux")), /Please specify author 'email' in .*/) }) diff --git a/test/src/winPackagerTest.ts b/test/src/winPackagerTest.ts index b172f0afa52..efa721562c0 100644 --- a/test/src/winPackagerTest.ts +++ b/test/src/winPackagerTest.ts @@ -1,16 +1,17 @@ import test from "./helpers/avaEx" -import { assertPack } from "./helpers/packTester" +import { assertPack, platform } from "./helpers/packTester" //noinspection JSUnusedLocalSymbols const __awaiter = require("out/awaiter") test.ifNotTravis("win", async () => { - await assertPack("test-app-one", "win32") + await assertPack("test-app-one", platform("win32")) }) // nsis is deprecated and not thread-safe - just do not run on CI to avoid failures test.ifNotCi.serial("win: nsis", async () => { - await assertPack("test-app-one", "win32", { + await assertPack("test-app-one", { + platform: ["win32"], target: ["nsis"], arch: process.arch }, true) diff --git a/test/tsconfig.json b/test/tsconfig.json index 98ccb4aeeb8..2f2e7cf7bc6 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -31,6 +31,7 @@ "../typings/command-line-args.d.ts", "../typings/electron-packager.d.ts", "../typings/gh-api.d.ts", + "../typings/globby.d.ts", "../typings/hosted-git-info.d.ts", "../typings/main/ambient/gm/gm.d.ts", "../typings/main/ambient/mime/mime.d.ts", @@ -55,6 +56,7 @@ "src/helpers/avaEx.ts", "src/helpers/codeSignData.ts", "src/helpers/expectedContents.ts", + "src/helpers/fileAssert.ts", "src/helpers/packTester.ts", "src/helpers/runTests.ts", "src/linuxPackagerTest.ts", diff --git a/tsconfig.json b/tsconfig.json index cd7ce639b8d..e0902ff03c3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,6 +33,7 @@ "typings/command-line-args.d.ts", "typings/electron-packager.d.ts", "typings/gh-api.d.ts", + "typings/globby.d.ts", "typings/hosted-git-info.d.ts", "typings/main/ambient/gm/gm.d.ts", "typings/main/ambient/mime/mime.d.ts", diff --git a/typings/globby.d.ts b/typings/globby.d.ts new file mode 100644 index 00000000000..758708d920d --- /dev/null +++ b/typings/globby.d.ts @@ -0,0 +1,9 @@ +declare module "globby" { + interface GlobOptions { + cwd?: string + } + + function globby(patterns: Array, options?: GlobOptions): Promise> + + export = globby +} \ No newline at end of file