UNPKG

frontity

Version:

Frontity cli and entry point to other packages

388 lines (339 loc) 12.4 kB
import { normalizeOptions, ensureProjectDir, createPackageJson, createFrontitySettings, createTsConfig, cloneStarterTheme, installDependencies, downloadFavicon, revertProgress, } from "../"; import { createPackageJson as createPackageJsonForPackage } from "../create-package"; import * as utils from "../../utils"; import * as fsExtra from "fs-extra"; import * as fetch from "node-fetch"; import * as path from "path"; import * as childProcess from "child_process"; import * as tar from "tar"; jest.mock("../../utils"); jest.mock("fs-extra"); jest.mock("path"); jest.mock("node-fetch"); jest.mock("child_process"); jest.mock("tar"); // Manually define the overload that we are mocking because TypeScript is not // able to know which one we are using when we use `.mockResolvedValue`. type Readdir = ( path: fsExtra.PathLike, options?: | { encoding: BufferEncoding | null; withFileTypes?: false } | BufferEncoding | null ) => Promise<string[]>; const mockedUtils = utils as jest.Mocked<typeof utils>; const mockedFsExtra = fsExtra as jest.Mocked<typeof fsExtra>; const mockedFetch = fetch as jest.Mocked<typeof fetch>; const mockedPath = path as jest.Mocked<typeof path>; const mockedChildProcess = childProcess as jest.Mocked<typeof childProcess>; const mockedTar = tar as jest.Mocked<typeof tar>; const mockedReaddir: jest.MockedFunction<Readdir> = fsExtra.readdir as any; beforeEach(() => { mockedPath.resolve.mockImplementation((...dirs) => dirs.join("/").replace("./", "") ); }); describe("normalizeOptions", () => { beforeEach(() => { mockedUtils.isPackageNameValid.mockReset(); }); test("returns merged options", () => { const defaultOptions = { path: "/default/path/to/project", typescript: false, packages: ["frontity", "@frontity/file-settings"], theme: "@frontity/mars-theme", }; const passedOptions = { name: "Some Random Name", path: "/path/to/project", theme: "@frontity/saturn-theme", }; const expectedOptions = { name: "some-random-name", path: passedOptions.path, typescript: defaultOptions.typescript, packages: defaultOptions.packages, theme: passedOptions.theme, }; mockedUtils.isPackageNameValid.mockReturnValue(true); const result = normalizeOptions(defaultOptions, passedOptions); expect(result).toEqual(expectedOptions); }); test("throws if `options.name` is not valid", () => { const defaultOptions = { path: "/default/path/to/project", typescript: false, packages: ["frontity", "@frontity/file-settings"], theme: "@frontity/mars-theme", }; const passedOptions = { name: "Some, Random Name", }; mockedUtils.isPackageNameValid.mockReturnValue(false); expect(() => normalizeOptions(defaultOptions, passedOptions)).toThrow( "The name of the package is not valid. Please enter a valid one (only letters and dashes)." ); }); }); describe("ensureProjectDir", () => { beforeEach(() => { mockedFsExtra.ensureDir.mockReset(); mockedReaddir.mockReset(); mockedFsExtra.pathExists.mockReset(); }); test("works when passing a non existent path", async () => { const path = "/path/to/project"; mockedFsExtra.pathExists.mockImplementation(() => Promise.resolve(false)); const dirExisted = await ensureProjectDir(path); expect(dirExisted).toBe(false); expect(mockedFsExtra.pathExists.mock.calls).toMatchSnapshot(); expect(mockedFsExtra.ensureDir.mock.calls).toMatchSnapshot(); expect(mockedReaddir).not.toHaveBeenCalled(); }); test("works when passing an existent path with an empty repo", async () => { const path = "/path/to/project"; mockedReaddir.mockResolvedValue([ "README.md", ".git", ".gitignore", "LICENSE", ]); mockedFsExtra.pathExists.mockImplementation(() => Promise.resolve(true)); const dirExisted = await ensureProjectDir(path); expect(dirExisted).toBe(true); expect(mockedFsExtra.pathExists.mock.calls).toMatchSnapshot(); expect(mockedReaddir.mock.calls).toMatchSnapshot(); expect(mockedFsExtra.ensureDir).not.toHaveBeenCalled(); }); test("throws when passing an non-empty path", async () => { const path = "/path/to/project"; mockedFsExtra.pathExists.mockImplementation(() => Promise.resolve(true)); mockedReaddir.mockResolvedValue(["file-that-should-not-exist"]); await expect(ensureProjectDir(path)).rejects.toThrow( "The directory passed to `create` function is not empty" ); }); }); describe("createPackageJson", () => { beforeEach(() => { mockedFsExtra.writeFile.mockReset(); mockedUtils.fetchPackageVersion.mockReset(); mockedUtils.fetchPackageVersion.mockResolvedValue("1.0.0"); }); test('works with a theme like "@frontity/mars-theme"', async () => { const name = "random-name"; const theme = "@frontity/mars-theme"; const path = "/path/to/project"; const typescript = false; await createPackageJson(name, theme, path, typescript); expect(mockedUtils.fetchPackageVersion.mock.calls).toMatchSnapshot(); expect(mockedFsExtra.writeFile.mock.calls).toMatchSnapshot(); }); test('works with a theme like "mars-theme"', async () => { const name = "random-name"; const theme = "random-theme"; const path = "/path/to/project"; const typescript = false; await createPackageJson(name, theme, path, typescript); expect(mockedUtils.fetchPackageVersion.mock.calls).toMatchSnapshot(); expect(mockedFsExtra.writeFile.mock.calls).toMatchSnapshot(); }); test('works when "typescript" is true', async () => { const name = "random-name"; const theme = "random-theme"; const path = "/path/to/project"; const typescript = true; await createPackageJson(name, theme, path, typescript); expect(mockedUtils.fetchPackageVersion.mock.calls).toMatchSnapshot(); expect(mockedFsExtra.writeFile.mock.calls).toMatchSnapshot(); }); }); describe("createFrontitySettings", () => { beforeEach(() => { mockedFsExtra.readFile.mockReset(); mockedFsExtra.readFile.mockResolvedValueOnce("$settings$" as any); mockedFsExtra.writeFile.mockReset(); }); test("works when extension is `js`", async () => { const name = "random-name"; const path = "/path/to/project"; const extension = "js"; const theme = "@frontity/mars-theme"; await createFrontitySettings(extension, name, path, theme); expect(mockedFsExtra.readFile).toHaveBeenCalled(); expect(mockedFsExtra.writeFile.mock.calls).toMatchSnapshot(); }); test("works when extension is `ts`", async () => { const name = "random-name"; const path = "/path/to/project"; const extension = "ts"; const theme = "@frontity/mars-theme"; await createFrontitySettings(extension, name, path, theme); expect(mockedFsExtra.readFile).toHaveBeenCalled(); expect(mockedFsExtra.writeFile.mock.calls).toMatchSnapshot(); }); }); describe("createTsConfig", () => { beforeEach(() => { mockedFsExtra.readFile.mockReset(); mockedFsExtra.readFile.mockResolvedValueOnce("$tsconfig$" as any); mockedFsExtra.writeFile.mockReset(); }); test('works as expected"', async () => { const path = "/path/to/project"; await createTsConfig(path); expect(mockedFsExtra.readFile).toHaveBeenCalled(); expect(mockedFsExtra.writeFile.mock.calls).toMatchSnapshot(); }); }); describe("cloneStarterTheme", () => { beforeEach(() => { mockedFsExtra.readFile.mockReset(); mockedFsExtra.readFile.mockResolvedValueOnce( JSON.stringify({ dependencies: { "@frontity/mars-theme": "./packages/mars-theme", }, }) as any ); mockedFsExtra.ensureDir.mockReset(); mockedUtils.isThemeNameValid.mockReset(); mockedReaddir.mockReset(); mockedReaddir.mockResolvedValueOnce(["file.tgz"]); mockedFsExtra.remove.mockReset(); mockedChildProcess.exec.mockReset(); (mockedChildProcess as any).exec.mockImplementation( ( _command: string, _options: Record<string, unknown>, resolve: (...args: any) => any ) => { resolve(); } ); mockedTar.extract.mockReset(); }); test("works as expected", async () => { const path = "/path/to/project"; const theme = "@frontity/mars-theme"; mockedUtils.isThemeNameValid.mockReturnValue(true); await cloneStarterTheme(theme, path); expect(mockedFsExtra.readFile.mock.calls).toMatchSnapshot(); expect(mockedFsExtra.ensureDir.mock.calls).toMatchSnapshot(); expect(mockedUtils.isThemeNameValid.mock.calls).toMatchSnapshot(); expect(mockedChildProcess.exec.mock.calls).toMatchSnapshot(); expect(mockedReaddir.mock.calls).toMatchSnapshot(); expect(mockedTar.extract.mock.calls).toMatchSnapshot(); expect(mockedFsExtra.remove.mock.calls).toMatchSnapshot(); }); test("throws if the theme name is not valid", async () => { const path = "/path/to/project"; const theme = "@frontity/mars-theme"; mockedUtils.isThemeNameValid.mockReturnValue(false); await expect(cloneStarterTheme(path, theme)).rejects.toThrow( "The name of the theme is not a valid npm package name." ); }); }); describe("downloadFavicon", () => { beforeEach(() => { mockedFetch.default.mockReset(); (mockedFetch as any).default.mockResolvedValue({ body: { pipe: jest.fn(), }, }); mockedFsExtra.createWriteStream.mockReset(); (mockedFsExtra as any).createWriteStream.mockReturnValue({ on: jest.fn((_event, callback) => callback()), }); }); test("works as expected", async () => { const path = "/path/to/project"; await downloadFavicon(path); expect(mockedFetch.default.mock.calls).toMatchSnapshot(); expect(mockedFsExtra.createWriteStream.mock.calls).toMatchSnapshot(); }); }); describe("installDependencies", () => { beforeEach(() => { mockedChildProcess.exec.mockReset(); (mockedChildProcess as any).exec.mockImplementation( ( _command: string, _options: Record<string, unknown>, resolve: (...args: any) => any ) => { resolve(); } ); }); test("works as expected", async () => { const path = "/path/to/project"; await installDependencies(path); expect(mockedChildProcess.exec.mock.calls).toMatchSnapshot(); }); }); describe("revertProgress", () => { beforeEach(() => { mockedReaddir.mockReset(); mockedFsExtra.remove.mockReset(); }); test("works if the project directory existed", async () => { const dirExisted = true; const path = "/path/to/project"; mockedReaddir.mockResolvedValue([ "frontity.settings.js", "package.json", "packages", "favicon.ico", "node_modules", "package-lock.json", ]); await revertProgress(dirExisted, path); expect(mockedReaddir.mock.calls).toMatchSnapshot(); expect(mockedFsExtra.remove.mock.calls).toMatchSnapshot(); }); test("works if the project directory didn't exist", async () => { const dirExisted = false; const path = "/path/to/project"; await revertProgress(dirExisted, path); expect(mockedFsExtra.remove.mock.calls).toMatchSnapshot(); }); }); describe("createPackageJson for create-package", () => { beforeEach(() => { mockedFsExtra.writeFile.mockReset(); mockedUtils.fetchPackageVersion.mockReset(); mockedUtils.fetchPackageVersion.mockResolvedValue("1.0.0"); }); test('works with a theme like "@frontity/mars-theme"', async () => { const name = "random-name"; const namespace = "test"; const theme = "@frontity/mars-theme"; const path = "/path/to/project"; await createPackageJsonForPackage(name, namespace, theme, path); expect(mockedUtils.fetchPackageVersion.mock.calls).toMatchSnapshot(); expect(mockedFsExtra.writeFile.mock.calls).toMatchSnapshot(); }); test('works with a theme like "random-theme"', async () => { const name = "random-name"; const namespace = "test"; const theme = "random-theme"; const path = "/path/to/project"; await createPackageJsonForPackage(name, namespace, theme, path); expect(mockedUtils.fetchPackageVersion.mock.calls).toMatchSnapshot(); expect(mockedFsExtra.writeFile.mock.calls).toMatchSnapshot(); }); });