Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules/
# Ignore test-related files
/coverage.data
/coverage/
/src/utils/template/fixture/

# Build files
/dist
Expand Down
1,661 changes: 1,572 additions & 89 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"scripts": {
"start": "node ./dist/index.js",
"build": "tsup && node src/scripts/copy-template-files.js",
"test": "echo \"Error: no test specified\" && exit 1",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"prettier": "prettier --write",
"lint": "eslint --ignore-path .eslintignore --ext .js,.ts,.tsx",
"prepare": "husky",
Expand Down Expand Up @@ -62,6 +64,7 @@
"@types/shelljs": "^0.8.15",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"@vitest/coverage-v8": "^4.0.18",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"husky": "^8.0.0",
Expand All @@ -70,7 +73,8 @@
"shelljs": "^0.8.5",
"ts-node": "^10.9.2",
"tsup": "^8.0.2",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vitest": "^4.0.18"
},
"husky": {
"hooks": {
Expand Down
80 changes: 80 additions & 0 deletions src/commands/createCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

vi.mock('utils/read-config-file.js', () => ({ readConfig: vi.fn() }));

import { readConfig } from 'utils/read-config-file.js';
import createCommand from './createCommand.js';

const mockedReadConfig = vi.mocked(readConfig);

afterEach(() => {
vi.restoreAllMocks();
mockedReadConfig.mockReset();
});

describe('createCommand', () => {
const plugins = {
enabled: { install: vi.fn() },
disabled: { install: vi.fn() },
};

it('returns the command configuration', () => {
const pluginConfigFile = { name: 'config', plugins: {} };
const action = vi.fn(async () => undefined);

const command = createCommand({
requiresProjectInitialized: false,
plugins,
pluginConfigFile,
action,
});

expect(command.requiresProjectInitialized).toBe(false);
expect(command.plugins).toBe(plugins);
expect(command.pluginConfigFile).toBe(pluginConfigFile);
});

it('reads config when project initialization is required', async () => {
mockedReadConfig.mockReturnValue({ projectPath: '/project' });
const action = vi.fn(async () => undefined);
const command = createCommand({
requiresProjectInitialized: true,
plugins,
pluginConfigFile: { name: 'config', plugins: {} },
action,
});

await command.execute();

expect(mockedReadConfig).toHaveBeenCalledOnce();
expect(action).toHaveBeenCalledWith({ plugins });
});

it('filters plugins explicitly disabled in the config file', async () => {
const action = vi.fn(async () => undefined);
const command = createCommand({
requiresProjectInitialized: false,
plugins,
pluginConfigFile: { name: 'config', plugins: { disabled: false } },
action,
});

await command.execute();

expect(action).toHaveBeenCalledWith({ plugins: { enabled: plugins.enabled } });
});

it('keeps plugins absent from or enabled in the config file', async () => {
const action = vi.fn(async () => undefined);
const command = createCommand({
requiresProjectInitialized: false,
plugins,
pluginConfigFile: { name: 'config', plugins: { enabled: true } },
action,
});

await command.execute();

expect(action).toHaveBeenCalledWith({ plugins });
});
});
38 changes: 38 additions & 0 deletions src/commands/init/selectGitHubTemplate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, describe, expect, it, vi } from 'vitest';

vi.mock('utils/exec-async.js', () => ({ default: vi.fn() }));

import template from './selectGitHubTemplate.js';

describe('GitHub template selection', () => {
const directories: string[] = [];

afterEach(() => {
directories.splice(0).forEach((directory) => fs.rmSync(directory, { recursive: true, force: true }));
vi.restoreAllMocks();
});

it('creates the default README and config for None', async () => {
const projectPath = fs.mkdtempSync(path.join(os.tmpdir(), 'next-quick-template-'));
directories.push(projectPath);
const target = path.join(projectPath, 'project');

await template.action({ template: 'None' }, 'my-project', target);

expect(fs.readFileSync(path.join(target, 'README.md'), 'utf8')).toBe(
'# my-project\n\nProject initialized with NextJS CLI.'
);
expect(fs.readFileSync(path.join(target, 'config.json'), 'utf8')).toBe('{}');
});

it('reports an invalid template selection', async () => {
const error = vi.spyOn(console, 'error').mockImplementation(() => undefined);

await template.action({ template: 'unknown' }, 'my-project', '/tmp/project');

expect(error).toHaveBeenCalledWith('Invalid template selected.');
});
});
37 changes: 37 additions & 0 deletions src/utils/check-directory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import checkIfAnyDirectoryExists from './check-directory.js';

describe('checkIfAnyDirectoryExists', () => {
const directories: string[] = [];
let cwd: ReturnType<typeof vi.spyOn>;

afterEach(() => {
cwd?.mockRestore();
directories.splice(0).forEach((directory) => fs.rmSync(directory, { recursive: true, force: true }));
vi.restoreAllMocks();
});

function useTempDirectory() {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'next-quick-directory-'));
directories.push(directory);
cwd = vi.spyOn(process, 'cwd').mockReturnValue(directory);
return directory;
}

it('returns true when the current directory contains a directory', () => {
const directory = useTempDirectory();
fs.mkdirSync(path.join(directory, 'nested'));

expect(checkIfAnyDirectoryExists()).toBe(true);
});

it('returns false when the current directory contains only files', () => {
const directory = useTempDirectory();
fs.writeFileSync(path.join(directory, 'file.txt'), 'content');

expect(checkIfAnyDirectoryExists()).toBe(false);
});
});
47 changes: 47 additions & 0 deletions src/utils/env/check-env-vars.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import checkEnvVars from './check-env-vars.js';

describe('checkEnvVars', () => {
const directories: string[] = [];
let cwd: ReturnType<typeof vi.spyOn>;

afterEach(() => {
cwd?.mockRestore();
directories.splice(0).forEach((directory) => fs.rmSync(directory, { recursive: true, force: true }));
vi.restoreAllMocks();
});

function useTempDirectory() {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'next-quick-env-'));
directories.push(directory);
cwd = vi.spyOn(process, 'cwd').mockReturnValue(directory);
return directory;
}

it('returns false when .env.local is missing', () => {
useTempDirectory();
vi.spyOn(console, 'error').mockImplementation(() => undefined);

expect(checkEnvVars(['API_KEY'])).toBe(false);
expect(console.error).toHaveBeenCalledWith('.env.local file does not exist.');
});

it('returns true when all required variables are present', () => {
const directory = useTempDirectory();
fs.writeFileSync(path.join(directory, '.env.local'), 'API_KEY=secret\nDATABASE_URL=test');

expect(checkEnvVars(['API_KEY', 'DATABASE_URL'])).toBe(true);
});

it('reports missing variables', () => {
const directory = useTempDirectory();
fs.writeFileSync(path.join(directory, '.env.local'), 'API_KEY=secret');
vi.spyOn(console, 'error').mockImplementation(() => undefined);

expect(checkEnvVars(['API_KEY', 'DATABASE_URL'])).toBe(false);
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('DATABASE_URL'));
});
});
51 changes: 51 additions & 0 deletions src/utils/env/write-to-env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import writeToEnv from './write-to-env.js';

describe('writeToEnv', () => {
const directories: string[] = [];
const originalCwd = process.cwd();

afterEach(() => {
process.chdir(originalCwd);
directories.splice(0).forEach((directory) => fs.rmSync(directory, { recursive: true, force: true }));
vi.restoreAllMocks();
});

function useTempDirectory() {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'next-quick-write-env-'));
directories.push(directory);
process.chdir(directory);
return directory;
}

it('appends a key-value pair when .env.local exists', () => {
const directory = useTempDirectory();
fs.writeFileSync(path.join(directory, '.env.local'), 'EXISTING=value');

writeToEnv('NEW_KEY', 'new-value');

expect(fs.readFileSync(path.join(directory, '.env.local'), 'utf8')).toBe('EXISTING=value\nNEW_KEY=new-value\n');
});

it('logs an error when .env.local does not exist', () => {
useTempDirectory();
vi.spyOn(console, 'error').mockImplementation(() => undefined);

writeToEnv('NEW_KEY', 'new-value');

expect(console.error).toHaveBeenCalledWith(expect.stringContaining('file does not exist'));
});

it('logs failures while appending', () => {
const directory = useTempDirectory();
fs.mkdirSync(path.join(directory, '.env.local'));
vi.spyOn(console, 'error').mockImplementation(() => undefined);

writeToEnv('NEW_KEY', 'new-value');

expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Failed to write'));
});
});
Loading