<|start_filename|>src/utils/webpack/watch-extra-files.js<|end_filename|>
class WatchExtraFilesPlugin {
    constructor({ files = [] } = {}) {
        this.files = files;
    }

    apply(compiler) {
        compiler.hooks.afterCompile.tap(this.constructor.name, compilation => {
            this.files.map(f => compilation.fileDependencies.add(f));
        });
    }
}

module.exports = WatchExtraFilesPlugin;

<|start_filename|>src/commands/publish/apiClient.js<|end_filename|>
const { URL } = require("url");
const { BAD_REQUEST, INTERNAL_SERVER_ERROR, MOVED_TEMPORARILY } = require("http-status-codes");
const request = require("request-promise-native");

const { apiBaseUrl, apiClientId } = require("../../config/constants");
const { ClientError, ServerError } = require("../../errors");

function createError(response) {
    const { statusCode, body } = response;
    const extra = { response: response.toJSON() };

    if (statusCode >= BAD_REQUEST && statusCode < INTERNAL_SERVER_ERROR) {
        const { message, title } = body;

        return new ClientError(statusCode, `${title}${message ? `: ${message}` : ""}`, extra);
    }

    if (statusCode >= INTERNAL_SERVER_ERROR) {
        return new ServerError(statusCode, extra);
    }

    return new Error("Zeplin API error");
}

module.exports = class ApiClient {
    constructor() {
        this.requestor = request.defaults({
            baseUrl: apiBaseUrl,
            simple: false,
            resolveWithFullResponse: true,
            json: true
        });
    }

    async request(opts) {
        const response = await this.requestor(opts);
        const { body } = response;

        if (response.statusCode >= BAD_REQUEST) {
            throw createError(response);
        }

        return body;
    }

    async login({ handle, password }) {
        const { token } = await this.request({
            method: "POST",
            uri: "/users/login",
            body: {
                handle,
                password
            }
        });

        return token;
    }

    async generateAuthToken(loginToken) {
        const response = await this.requestor({
            method: "GET",
            uri: "/oauth/authorize",
            qs: {
                client_id: apiClientId,
                response_type: "token",
                scope: "write"
            },
            headers: { "Zeplin-Token": loginToken },
            followRedirect: false
        });

        const { headers: { location }, statusCode } = response;

        if (statusCode !== MOVED_TEMPORARILY) {
            throw createError(response);
        }

        const { searchParams } = new URL(location);

        return searchParams.get("access_token");
    }

    getExtensions({ authToken, owner }) {
        return this.request({
            method: "GET",
            uri: `/extensions`,
            qs: {
                owner
            },
            headers: { "Zeplin-Access-Token": authToken }
        });
    }

    createExtension({ authToken, data }) {
        const {
            packageName,
            name,
            description,
            platforms,
            version,
            packageBuffer
        } = data;

        return this.request({
            method: "POST",
            uri: "/extensions",
            formData: {
                version,
                packageName,
                name,
                description,
                projectTypes: platforms,
                package: {
                    options: {
                        filename: "package.zip"
                    },
                    value: packageBuffer
                }
            },
            headers: { "Zeplin-Access-Token": authToken }
        });
    }

    createExtensionVersion({ authToken, extensionId, version, packageBuffer }) {
        return this.request({
            method: "POST",
            uri: `/extensions/${extensionId}/versions`,
            formData: {
                version,
                package: {
                    options: {
                        filename: "package.zip"
                    },
                    value: packageBuffer
                }
            },
            headers: { "Zeplin-Access-Token": authToken }
        });
    }
};


<|start_filename|>src/template/src/fixtures/index.js<|end_filename|>
import { Context, Screen, Version, Component, ComponentVariant } from "@zeplin/extension-model";

import project from "./project.json";
import screensData from "./screens.json";
import componentsData from "./components.json";
import componentVariantsData from "./componentVariants.json";
import versionData from "./version.json";
import { zeplin } from "../../package.json";

const defaultOptions = zeplin.options?.reduce((options, option) => {
    options[option.id] = option.default;
    return options;
}, {});

const singleComponents = componentsData.map(data => new Component(data));
const variantComponents = componentVariantsData.map(data => new ComponentVariant(data)).reduce(
    (cs, variant) => cs.concat(variant.components), []
);

export const context = new Context({ project, options: defaultOptions });
export const version = new Version(versionData);
export const screens = screensData.map(data => new Screen(data));
export const components = singleComponents.concat(variantComponents);


<|start_filename|>src/utils/webpack/manifest-builder.js<|end_filename|>
const fs = require("fs");
const path = require("path");

function parseShortcutRepoUrl(shortcutUrl) {
    if (shortcutUrl.startsWith("http")) {
        return shortcutUrl;
    }

    const match = shortcutUrl.match(/(?:([a-z]+):)?(.*)/i);

    if (!match) {
        return;
    }

    const [type, id] = match.slice(1);

    switch (type) {
        case "gist":
            return `https://gist.github.com/${id}`;
        case "bitbucket":
            return `https://bitbucket.org/${id}`;
        case "gitlab":
            return `https://gitlab.com/${id}`;
        case "github":
            return `https://github.com/${id}`;
        default:
            if (/[^/]+\/[^/]+/.test(id)) {
                return `https://github.com/${id}`;
            }
    }
}

function parseRepository(repoInfo) {
    if (typeof repoInfo === "string") {
        return parseShortcutRepoUrl(repoInfo);
    }

    return repoInfo.url;
}

class ManifestBuilder {
    constructor(extensionPath, bundleName) {
        this.extensionPath = extensionPath;
        this.bundleName = bundleName;
    }

    apply(compiler) {
        compiler.hooks.emit.tap(this.constructor.name, compilation => {
            const chunk = compilation.chunks.find(c => c.name === this.bundleName);

            if (!chunk) {
                return;
            }

            const manifest = {};
            const pkgInfoPath = path.join(this.extensionPath, "package.json");

            // Invalidate cached package.json content
            delete require.cache[require.resolve(pkgInfoPath)];
            const pkgInfo = require(pkgInfoPath);

            manifest.packageName = pkgInfo.name;
            manifest.name = pkgInfo.zeplin.displayName || pkgInfo.name;
            manifest.description = pkgInfo.description;
            manifest.version = pkgInfo.version;
            manifest.author = pkgInfo.author;
            manifest.options = pkgInfo.zeplin.options;
            manifest.platforms = pkgInfo.zeplin.platforms || pkgInfo.zeplin.projectTypes;
            manifest.moduleURL = `./${chunk.files[0]}`;

            if (pkgInfo.repository) {
                manifest.repository = parseRepository(pkgInfo.repository);
            }

            if (fs.existsSync(path.join(this.extensionPath, "README.md"))) {
                manifest.readmeURL = "./README.md";
            }

            const content = JSON.stringify(manifest, null, 2);

            compilation.assets["manifest.json"] = {
                source() {
                    return content;
                },
                size() {
                    return content.length;
                }
            };
        });
    }
}

module.exports = ManifestBuilder;

<|start_filename|>src/commands/publish/authenticationService.js<|end_filename|>
const fs = require("fs-extra");
const jwt = require("jsonwebtoken");
const prompts = require("prompts");

const paths = require("../../utils/paths");
const { accessToken, isCI } = require("../../config/constants");
const ApiClient = require("./apiClient");

const EXIT_CODE_FOR_SIGTERM = 130;

function readToken() {
    const rcFilePath = paths.getRcFilePath();

    if (fs.existsSync(rcFilePath)) {
        const rcFile = JSON.parse(fs.readFileSync(rcFilePath));

        return rcFile.token;
    }
}

function updateRCFile(token) {
    const rcFilePath = paths.getRcFilePath();
    const rcContent = fs.existsSync(rcFilePath) ? JSON.parse(fs.readFileSync(rcFilePath)) : {};

    rcContent.token = token;
    fs.writeFileSync(rcFilePath, JSON.stringify(rcContent));
}

function getAuthInfo(authToken) {
    if (!authToken) {
        throw new Error("Authentication token is not provided");
    }

    const { aud } = jwt.decode(authToken, { complete: false });
    if (!aud) {
        throw new Error("Invalid authentication token");
    }

    const [, userId] = aud.split(":");
    if (!userId) {
        throw new Error("Invalid authentication token");
    }

    return {
        authToken,
        userId
    };
}

module.exports = class AuthenticationService {
    constructor() {
        this.apiClient = new ApiClient();
    }

    authenticate() {
        if (isCI || accessToken) {
            return getAuthInfo(accessToken);
        }

        const tokenFromFile = readToken();
        if (tokenFromFile) {
            return getAuthInfo(tokenFromFile);
        }
        return this.login();
    }

    async login() {
        const { handle, password } = await prompts([
            {
                type: "text",
                name: "handle",
                message: "Username or email address: ",
                validate: value => Boolean(value && value.length) || "Username or email address must not be empty"
            },
            {
                type: "password",
                name: "password",
                message: "Password",
                validate: value => Boolean(value && value.length) || "Password must not be empty"
            }
        ]);

        if (!password) {
            process.exit(EXIT_CODE_FOR_SIGTERM);
        }

        const token = await this.apiClient.login({ handle, password });
        const authToken = await this.apiClient.generateAuthToken(token);

        const authInfo = getAuthInfo(authToken);

        await updateRCFile(authToken);

        return authInfo;
    }
};


<|start_filename|>src/errors/index.js<|end_filename|>
module.exports = {
    ClientError: require("./clientError"),
    ServerError: require("./serverError")
};


<|start_filename|>src/config/webpack.exec.js<|end_filename|>
const merge = require("webpack-merge");
const dev = require("./webpack.dev");

module.exports = merge(dev, {
    target: "node"
});

<|start_filename|>src/commands/publish/manifest-validator.js<|end_filename|>
const Ajv = require("ajv");
const ajv = new Ajv({ allErrors: true });

const pickerOptionSchema = {
    type: "object",
    required: ["name", "value"],
    properties: {
        name: { type: "string" },
        value: { type: "string" }
    }
};

const pickerSchema = {
    type: "object",
    required: ["name", "type", "id", "default"],
    properties: {
        name: { type: "string" },
        type: { const: "picker" },
        id: { type: "string" },
        default: { type: "string" },
        options: { type: "array", items: pickerOptionSchema }
    }
};

const switchSchema = {
    type: "object",
    required: ["name", "type", "id", "default"],
    properties: {
        name: { type: "string" },
        type: { const: "switch" },
        id: { type: "string" },
        default: { type: "boolean" }
    }
};

const textOptionSchema = {
    type: "object",
    required: ["name", "type", "id", "default"],
    properties: {
        name: { type: "string" },
        type: { const: "text" },
        id: { type: "string" },
        default: { type: "string" }
    }
};

const optionsSchema = {
    type: "array",
    items: {
        anyOf: [
            pickerSchema,
            switchSchema,
            textOptionSchema
        ]
    }
};

// Credit to <NAME> https://gist.github.com/dperini/729294
const urlPattern = "^" +
    // Protocol identifier (optional)
    // Short syntax // still required
    "(?:(?:(?:https?|ftp):)?\\/\\/)" +
    // User:pass BasicAuth (optional)
    "(?:\\S+(?::\\S*)?@)?" +
    "(?:" +
    // IP address exclusion
    // Private & local networks
    "(?!(?:10|127)(?:\\.\\d{1,3}){3})" +
    "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" +
    "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" +
    // IP address dotted notation octets
    // Excludes loopback network 0.0.0.0
    // Excludes reserved space >= 192.168.3.11
    // Excludes network & broadcast addresses
    // (first & last IP address of each class)
    "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
    "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
    "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
    "|" +
    // Host & domain names, may end with dot
    // Can be replaced by a shortest alternative
    // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
    "(?:" +
    "(?:" +
    "[a-z0-9\\u00a1-\\uffff]" +
    "[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
    ")?" +
    "[a-z0-9\\u00a1-\\uffff]\\." +
    ")+" +
    // TLD identifier name, may end with dot
    "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
    ")" +
    // Port number (optional)
    "(?::\\d{2,5})?" +
    // Resource path (optional)
    "(?:[/?#]\\S*)?" +
    "$";

const manifestSchema = {
    type: "object",
    required: ["packageName", "name", "description", "version", "moduleURL", "platforms"],
    properties: {
        packageName: { type: "string" },
        name: { type: "string" },
        description: { type: "string" },
        version: { type: "string", pattern: "(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" },
        moduleURL: { type: "string" },
        readmeURL: { type: "string" },
        author: {
            type: "object",
            properties: {
                name: { type: "string" },
                email: { type: "string", format: "email" },
                url: { type: "string", pattern: urlPattern }
            }
        },
        platforms: {
            type: "array",
            items: {
                enum: ["web", "android", "ios", "osx"]
            },
            uniqueItems: true,
            minItems: 1
        },
        options: optionsSchema
    }
};

const validate = ajv.compile(manifestSchema);

module.exports = function (manifestObj) {
    const valid = validate(manifestObj);

    if (!valid) {
        return {
            valid,
            errors: ajv.errorsText(validate.errors, { dataVar: "manifest", separator: "\n" })
        };
    }

    return { valid };
};


<|start_filename|>src/commands/publish/index.js<|end_filename|>
const fs = require("fs-extra");
const chalk = require("chalk");
const path = require("path");
const Zip = require("adm-zip");
const prompts = require("prompts");

const paths = require("../../utils/paths");
const manifestValidator = require("./manifest-validator");
const ApiClient = require("./apiClient");
const AuthenticationService = require("./authenticationService");
const { isCI } = require("../../config/constants");
const { version: packageVersion } = require(paths.resolveExtensionPath("./package.json"));

const pathResolver = {
    init(root) {
        this.root = root;
    },
    resolve(relativePath) {
        if (this.root) {
            return path.resolve(this.root, relativePath);
        }

        return paths.resolveBuildPath(relativePath);
    }
};

function parseManifest() {
    const manifestPath = pathResolver.resolve("./manifest.json");

    if (!fs.existsSync(manifestPath)) {
        throw new Error("Locating manifest.json failed. Please make sure that you run `npm run build` first!");
    }

    const manifest = JSON.parse(fs.readFileSync(manifestPath));
    const { valid, errors } = manifestValidator(manifest);

    if (!valid) {
        throw new Error(`Validating manifest.json failed:\n${errors}\n\nPlease make sure that all fields in "package.json" are valid and you run \`npm run build\``);
    }

    if (packageVersion !== manifest.version) {
        throw new Error(
            "Validating manifest.json failed: Extension version does not match the version in manifest.json.\n" +
            "Please make sure that you run `npm run build` first!"
        );
    }

    return manifest;
}

function createArchive() {
    const archive = new Zip();

    archive.addLocalFolder(pathResolver.resolve("./"), "./");

    return archive;
}

async function confirm() {
    if (isCI) {
        return true;
    }

    const { answer } = await prompts({
        type: "confirm",
        name: "answer",
        message: "Are you sure to continue"
    });
    return answer;
}

async function validateReadme({ hasOptions }) {
    const readmePath = pathResolver.resolve("./README.md");

    if (!fs.existsSync(readmePath)) {
        throw new Error("Locating README.md failed. Please make sure that you create a readme file and run `npm run build`!");
    }

    const readme = (await fs.readFile(readmePath))
        .toString("utf8")
        .replace(/<!--(.|\n)*?-->/g, "");

    if (!readme.match(/^## Output/m)) {
        console.log(chalk.yellow("Output section could not be found in README.md"));
        if (!await confirm()) {
            throw new Error("Output section could not be found in README.md. Please make sure that you add this section and run `npm run build`!");
        }
    }

    if (hasOptions && !readme.match(/^## Options/m)) {
        console.log(chalk.yellow("Options section could not be found in README.md"));
        if (!await confirm()) {
            throw new Error("Options section could not be found in README.md. Please make sure that you add this section and run `npm run build`!");
        }
    }
}

module.exports = async function (buildPath) {
    console.log("Publishing the extension...\n");

    pathResolver.init(buildPath);

    try {
        const {
            packageName,
            version,
            name,
            description,
            platforms,
            options
        } = parseManifest();

        await validateReadme({ hasOptions: Boolean(options) });

        const authenticationService = new AuthenticationService();
        const apiClient = new ApiClient();
        const { authToken, userId } = await authenticationService.authenticate();
        const { extensions } = await apiClient.getExtensions({ authToken, owner: userId });

        const extension = extensions.find(e => e.packageName === packageName);
        const packageBuffer = createArchive().toBuffer();

        if (!extension) {
            const data = {
                packageName,
                version,
                name,
                description,
                platforms: platforms.join(","),
                packageBuffer
            };
            await apiClient.createExtension({ data, authToken });
            console.log(`${chalk.bold(name)} (${version}) is now submitted. 🏄‍️\n`);
        } else {
            await apiClient.createExtensionVersion({ authToken, extensionId: extension._id, version, packageBuffer });
            console.log(`Version ${chalk.bold(version)} of ${chalk.bold(name)} is now submitted. 🏄‍️\n`);
        }

        console.log(`Big hugs for your contribution, you'll be notified via email once it's published on ${chalk.underline("https://extensions.zeplin.io")}.`);
    } catch (error) {
        console.log(chalk.red("Publishing extension failed:"));
        console.error(error.message || error);
        throw error;
    }
};

<|start_filename|>src/index.js<|end_filename|>
#!/usr/bin/env node
const fs = require("fs-extra");
const path = require("path");
const chalk = require("chalk");
const updateNotifier = require("update-notifier");
const commander = require("commander");

const { resolveBuildPath } = require("./utils/paths");
const { defaultHostName, defaultPort } = require("./config/constants");
const { name, version } = require(path.join(__dirname, "../package.json"));

const seconds = 60;
const minutes = 60;
const hours = 24;
const days = 5;
const updateCheckInterval = days * hours * minutes * seconds;

function beforeCommand() {
    const notifier = updateNotifier({
        pkg: { name, version },
        shouldNotifyInNpmScript: true,
        updateCheckInterval
    });
    notifier.notify();
}

const program = new commander.Command(name).version(version);
const TEST_ARGS_INDEX = 3;

program
    .command("create <dir>")
    .description("Create empty Zeplin extension at directory.")
    .option("-y --yes", "Create extension without prompt for configuration")
    .action((extensionDir, { yes }) => {
        const create = require("./commands/create");
        const root = path.resolve(process.cwd(), extensionDir);

        return create({ root, disablePrompt: yes });
    });

program
    .command("build")
    .description("Create build, targeting production environment.")
    .option("-d --dev", "Target development environment")
    .action(options => {
        const build = require("./commands/build");

        build(require(`./config/webpack.${options.dev ? "dev" : "prod"}`));
    });

program
    .command("clean")
    .description("Clean build directory.")
    .action(() => {
        fs.remove(resolveBuildPath());
    });

program
    .command("start")
    .description("Start local server, serving the extension.")
    .option("-h --host <host>", "Host name", defaultHostName)
    .option("-p --port <port>", "Port", defaultPort)
    .option("-a --allowed-hosts <allowed-hosts>", "Allowed hosts")
    .action(command => {
        const start = require("./commands/start");

        start(command.host, command.port, command.allowedHosts);
    });

program
    .command("exec [function-name]")
    .description("Execute extension function with sample data.")
    .option("--no-build", "Use existing build.")
    .option("--defaults <default-options>", `Set default extension option values (e.g, flag=false,prefix=\\"pre\\")`)
    .action((fnName, options) => {
        const exec = require("./commands/exec");
        let defaultOptions;

        if (options.defaults) {
            defaultOptions = {};

            options.defaults.split(",").forEach(keyValue => {
                const [key, value] = keyValue.split("=");

                defaultOptions[key] = JSON.parse(value);
            });
        }

        exec(require("./config/webpack.exec"), fnName, defaultOptions, options.build);
    });

program
    .command("publish")
    .description(`Publish extension, submitting it for review to be listed on ${chalk.underline("https://extensions.zeplin.io.")}`)
    .option("--path <build-path>", `Path for the extension build to be published`)
    .action(async command => {
        const publish = require("./commands/publish");
        try {
            await publish(command.path);
        } catch (_) {
            process.exitCode = 1;
        }
    });

program
    .command("test")
    .description(`Test via jest`)
    .allowUnknownOption()
    .action(command => {
        const test = require("./commands/test");

        test(process.argv.slice(TEST_ARGS_INDEX));
    });

program.on("command:*", () => {
    program.outputHelp();
});

beforeCommand();
program.parse(process.argv);

<|start_filename|>src/errors/serverError.js<|end_filename|>
module.exports = class ServerError extends Error {
    constructor(status, extra) {
        const message = `(${status}) Server error`;
        super(message);

        Error.captureStackTrace(this, this.constructor);

        this.status = status;
        this.message = message;
        this.name = "ServerError";
        this.extra = extra;
    }
};

<|start_filename|>src/sample-data/index.js<|end_filename|>
const project = require("./project.json");
const version = require("./version.json");
const screens = require("./screens.json");
const components = require("./components.json");
const componentVariants = require("./componentVariants.json");

module.exports = {
    componentVariants,
    components,
    project,
    screens,
    screenVersion: version
};

<|start_filename|>src/config/constants.js<|end_filename|>

const { isCI } = require("ci-info");

/* eslint-disable no-process-env */

module.exports = {
    defaultHostName: "localhost",
    defaultPort: 7070,
    buildDirName: "dist",
    bundleName: "main",
    isCI,
    accessToken: process.env.ZEM_ACCESS_TOKEN,
    apiBaseUrl: process.env.API_BASE_URL || "https://api.zeplin.io",
    apiClientId: process.env.API_CLIENT_ID || "5bbc983af6c410493afb03f1"
};

<|start_filename|>src/commands/create.js<|end_filename|>
const chalk = require("chalk");
const fs = require("fs-extra");
const path = require("path");
const { spawn } = require("child_process");
const prompts = require("prompts");
const { title } = require("case");

const { name } = require("../../package.json");

const JSON_INDENT = 2;
const EXIT_CODE_FOR_SIGTERM = 130;

const installDeps = () => new Promise((resolve, reject) => {
    const npmArgs = ["install", "--save", name];
    const child = spawn("npm", npmArgs, { stdio: "inherit", shell: true });

    child.on("close", exitCode => {
        if (exitCode !== 0) {
            reject();
            return;
        }

        resolve();
    });
});

const getDefaultConfig = packageName => ({
    packageName,
    description: "",
    displayName: title(packageName),
    platforms: []
});

const getConfig = async defaultPackageName => {
    const response = await prompts([
        {
            type: "text",
            name: "packageName",
            message: "Package name:",
            initial: defaultPackageName
        },
        {
            type: "text",
            name: "description",
            message: "Description:"
        },
        {
            type: "text",
            name: "displayName",
            message: "Display name:",
            initial: title(defaultPackageName)
        },
        {
            type: "multiselect",
            name: "platforms",
            message: "Platforms:",
            min: 1,
            choices: [
                { name: "Web", value: "web" },
                { name: "Android", value: "android" },
                { name: "iOS", value: "ios" },
                { name: "macOS", value: "osx" }
            ],
            instructions: false,
            hint: `Press ${chalk.blue("←")}/${chalk.blue("→")}/${chalk.green("[space]")} to select choices`
        }
    ]);

    if (!response.platforms) {
        process.exit(EXIT_CODE_FOR_SIGTERM);
    }

    return response;
};

const createPackageJson = (root, { packageName, description, displayName, platforms }) => {
    const packageJson = {
        name: packageName,
        version: "0.1.0",
        description,
        sideEffects: false,
        scripts: {
            start: "zem start",
            build: "zem build",
            clean: "zem clean",
            exec: "zem exec",
            test: "zem test",
            publish: "zem publish"
        },
        zeplin: {
            displayName,
            platforms
        }
    };

    return fs.writeFile(path.resolve(root, "package.json"), JSON.stringify(packageJson, null, JSON_INDENT));
};

const generateReadme = async options => {
    const readmeTemplate = (await fs.readFile("./README.md"))
        .toString("utf8");

    const overrideOptions = {
        description: options.description || "<!--- Sample description --->"
    };

    const readme = Object
        .entries(Object.assign({}, options, overrideOptions))
        .reduce(
            (template, [key, value]) => template.replace(new RegExp(`{{${key}}}`, "g"), value),
            readmeTemplate
        );
    return fs.writeFile("./README.md", readme);
};

const create = async ({ root, disablePrompt }) => {
    if (fs.existsSync(root)) {
        console.log(chalk.red("Creating extension failed, directory already exists."));
        process.exit(1);
    }

    const defaultPackageName = path.basename(root);

    const config = disablePrompt ? getDefaultConfig(defaultPackageName) : await getConfig(defaultPackageName);

    await fs.mkdir(root);

    const templatePath = path.join(__dirname, "../template");

    await Promise.all([
        createPackageJson(root, config),
        fs.copy(templatePath, root)
    ]);

    process.chdir(root);

    await Promise.all([
        installDeps(),
        generateReadme(config)
    ]);

    console.log(`\n✅ Created extension at ${chalk.blue(root)}.\n`);
    console.log(`To get started, see documentation at ${chalk.underline("https://github.com/zeplin/zeplin-extension-documentation")}.`);
};

module.exports = create;


<|start_filename|>src/commands/exec.js<|end_filename|>
const fs = require("fs");
const chalk = require("chalk");
const build = require("./build");
const highlightSyntax = require("../utils/highlight-syntax");
const sampleData = require("../sample-data");
const { bundleName } = require("../config/constants");
const { resolveBuildPath, resolveExtensionPath } = require("../utils/paths");
const {
    Context,
    Screen,
    Component,
    ComponentVariant,
    Version
} = require("@zeplin/extension-model");

function getManifestDefaults() {
    const manifest = require(resolveExtensionPath("dist/manifest.json"));

    if (!manifest.options) {
        return {};
    }

    return manifest.options.reduce((defaultOptions, option) => {
        defaultOptions[option.id] = option.default;

        return defaultOptions;
    }, {});
}

function printCodeData(codeData, title) {
    if (title) {
        console.log(chalk.bold(title));
    }

    if (!codeData) {
        return;
    }

    let output;

    if (typeof codeData === "string") {
        output = codeData;
    } else {
        const { code, language } = codeData;

        try {
            output = highlightSyntax(code, language);
        } catch (error) {
            output = code;
        }
    }

    console.log(output);
}

function callExtensionFunction(extension, fnName, ...args) {
    return Promise.resolve()
        .then(() => {
            if (typeof extension[fnName] !== "function") {
                return;
            }

            return extension[fnName](...args);
        }).catch(error => chalk.red(error.stack));
}

function executeScreen(extension, context) {
    const version = new Version(sampleData.screenVersion);
    const screens = sampleData.screens.map(s => new Screen(s));

    return Promise.all(
        screens.map(screen => callExtensionFunction(extension, "screen", context, version, screen))
    ).then(results => {
        console.log(chalk.underline.bold("\nScreens:"));

        results.forEach((codeData, index) => {
            printCodeData(codeData, `${screens[index].name}:`);
        });
    });
}

function executeComponent(extension, context) {
    const singleComponents = sampleData.components.map(c => new Component(c));
    const variantComponents = sampleData.componentVariants.map(
        variantData => new ComponentVariant(variantData)
    ).reduce((cs, variant) => cs.concat(variant.components), []);
    const components = singleComponents.concat(variantComponents);

    return Promise.all(
        components.map(component => callExtensionFunction(extension, "component", context, component.latestVersion, component))
    ).then(results => {
        console.log(chalk.underline.bold("\nComponents:"));

        results.forEach((codeData, index) => {
            printCodeData(codeData, `${components[index].name}:`);
        });
    });
}

function executeLayer(extension, context) {
    const version = new Version(sampleData.screenVersion);

    return Promise.all(
        version
            .layers
            .map(layer => callExtensionFunction(extension, "layer", context, layer))
    ).then(results => {
        console.log(chalk.underline.bold("\nLayers:"));

        results.forEach((codeData, index) => {
            printCodeData(codeData, `${version.layers[index].name}:`);
        });
    });
}

function executeColors(extension, context) {
    return callExtensionFunction(extension, "colors", context).then(codeData => {
        console.log(chalk.underline.bold("\nColors (Project):"));

        printCodeData(codeData);
    });
}

function executeTextStyles(extension, context) {
    return callExtensionFunction(extension, "textStyles", context).then(codeData => {
        console.log(chalk.underline.bold("\nText styles (Project):"));

        printCodeData(codeData);
    });
}

function executeSpacing(extension, context) {
    return callExtensionFunction(extension, "spacing", context).then(codeData => {
        console.log(chalk.underline.bold("\nSpacing (Project):"));

        printCodeData(codeData);
    });
}

const EXTENSION_FUNCTIONS = {
    layer: executeLayer,
    colors: executeColors,
    textStyles: executeTextStyles,
    spacing: executeSpacing,
    screen: executeScreen,
    component: executeComponent
};

function executeFunction(extension, fnName, context) {
    const fn = EXTENSION_FUNCTIONS[fnName];

    if (!fn) {
        console.log(chalk.yellow(`Function “${fnName}” not defined.`));
        return;
    }
    fn(extension, context);
}

function executeExtension(extension, fnName, defaultOptions = {}) {
    const options = Object.assign(getManifestDefaults(), defaultOptions);
    const context = new Context({
        options,
        project: sampleData.project
    });

    if (fnName) {
        executeFunction(extension, fnName, context);
        return;
    }

    executeFunction(extension, "colors", context);
    executeFunction(extension, "textStyles", context);
    executeFunction(extension, "spacing", context);
    executeFunction(extension, "component", context);
    executeFunction(extension, "screen", context);
    executeFunction(extension, "layer", context);
}

module.exports = function (webpackConfig, fnName, defaultOptions, shouldBuild) {
    const extensionModulePath = resolveBuildPath(bundleName);
    let moduleBuild;

    if (!shouldBuild && fs.existsSync(`${extensionModulePath}.js`)) {
        moduleBuild = Promise.resolve();
    } else {
        moduleBuild = build(webpackConfig, { throwOnError: true, printStats: false });
    }

    return moduleBuild.then(() => {
        try {
            const extension = require(extensionModulePath);

            console.log(`Executing extension${fnName ? ` function ${chalk.blue(fnName)}` : ""} with sample data...`);

            executeExtension(extension, fnName, defaultOptions);
        } catch (error) {
            console.error(chalk.red("Execution failed:"), error);
        }
    }).catch(error => {
        console.log(chalk.red("Execution failed: cannot build the extension"));
        console.error(error.stats.toJson().errors.join("\n"));
    });
};

<|start_filename|>src/config/webpack.common.js<|end_filename|>
const fs = require("fs");
const ManifestBuilder = require("../utils/webpack/manifest-builder");
const SimpleCopyPlugin = require("../utils/webpack/simple-copy-plugin");
const WatchExtraFilesPlugin = require("../utils/webpack/watch-extra-files");
const { resolveBuildPath, resolveExtensionPath } = require("../utils/paths");
const { bundleName } = require("./constants");

const extensionPath = resolveExtensionPath();
const buildPath = resolveBuildPath();
const readmePath = resolveExtensionPath("README.md");

const copies = {
    [bundleName]: [
        { from: readmePath, to: "README.md" }
    ]
};
const { eslintConfig, main: entryPoint } = require(resolveExtensionPath("package.json"));
const eslintEnabled = eslintConfig || fs.readdirSync(extensionPath).find(f => f.startsWith(".eslintrc"));
const jsLoaders = [{
    loader: "babel-loader",
    options: {
        presets: [
            [
                "@babel/preset-env",
                {
                    useBuiltIns: "usage",
                    corejs: 3,
                    modules: false, // Should be false to run tree shaking. See: https://webpack.js.org/guides/tree-shaking/
                    targets: {
                        chrome: 62,
                        safari: 11,
                        firefox: 59,
                        edge: 15
                    }
                }
            ]
        ]
    }
}];

if (eslintEnabled) {
    jsLoaders.push("eslint-loader");
}

module.exports = {
    mode: "none",
    entry: { [bundleName]: entryPoint || "./src/index.js" },
    output: {
        path: buildPath,
        library: "extension",
        libraryExport: "default",
        libraryTarget: "umd",
        globalObject: "typeof self !== 'undefined' ? self : this"
    },
    module: {
        rules: [{
            test: /\.js$/,
            exclude: /node_modules/,
            use: jsLoaders
        }]
    },
    plugins: [
        new SimpleCopyPlugin(copies),
        new WatchExtraFilesPlugin({
            files: [resolveExtensionPath("package.json"), readmePath]
        }),
        new ManifestBuilder(extensionPath, bundleName)
    ]
};


<|start_filename|>src/commands/start.js<|end_filename|>
const chalk = require("chalk");
const WebpackDevServer = require("webpack-dev-server");
const webpack = require("webpack");
const webpackConfig = require("../config/webpack.dev");
const devServerConfig = require("../config/dev-server");
const transformConfig = require("../utils/webpack/transform-config");

function createCompiler(config) {
    let compiler;
    try {
        compiler = webpack(config);
    } catch (err) {
        console.log(chalk.red("Compilation failed:"));
        console.error(err.message || err);
        process.exit(1);
    }

    compiler.hooks.done.tap("logStatPlugin", stats => {
        console.log(stats.toString({
            errors: true,
            colors: true
        }));
    });

    return compiler;
}

module.exports = function (host, port, allowedHosts) {
    const compiler = createCompiler(transformConfig(webpackConfig));
    const serverConfig = Object.assign({}, devServerConfig);

    if (allowedHosts) {
        serverConfig.allowedHosts = allowedHosts.split(",");
    }

    const server = new WebpackDevServer(compiler, devServerConfig);

    server.listen(port, host, err => {
        if (err) {
            return console.log(err);
        }

        console.log(`Extension is served from ${chalk.blue.bold(`http://${host}:${port}/manifest.json`)}\n`);
    });

    const closeServer = () => {
        server.close();
        process.exit();
    };

    process.on("SIGTERM", closeServer);
    process.on("SIGINT", closeServer);
    process.on("warning", warning => {
        console.log(warning.stack);
    });
};