Skip to content

Commit

Permalink
feat: install a default src template (#340)
Browse files Browse the repository at this point in the history
* feat: install a default src template

A template is copied in the project if the src directory is not
present or does not contain ts files.

Fixes: #312
  • Loading branch information
ofrobots committed May 14, 2019
1 parent cadb6ad commit 4a3c511
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 15 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ When you run the `npx gts init` command, it's going to do a few things for you:
- `clean`: Removes output files.
- `compile`: Compiles the source code using TypeScript compiler.
- `pretest`, `posttest` and `prepare`: convenience integrations.
- If a source folder is not already present it will add a default template project.

### Individual files
The commands above will all run in the scope of the current folder. Some commands can be run on individual files:
Expand Down
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"README.md",
"build/src",
"build/types",
"build/template",
"prettier.config.js",
"tsconfig-google.json",
"tsconfig.json",
Expand All @@ -23,6 +24,7 @@
"clean": "rimraf ./build/",
"codecov": "c8 report --reporter=json && codecov -f coverage/*.json",
"compile": "tsc -p .",
"postcompile": "ncp template build/template",
"format-check": "prettier --list-different src/*.ts test/*.ts",
"format": "prettier --write src/*.ts test/*.ts",
"lint": "tslint -c tslint.json --project . -t codeFrame",
Expand All @@ -46,6 +48,7 @@
"diff": "^4.0.1",
"inquirer": "^6.0.0",
"meow": "^5.0.0",
"ncp": "^2.0.0",
"prettier": "^1.15.3",
"rimraf": "^2.6.2",
"tslint": "^5.12.0",
Expand All @@ -60,18 +63,20 @@
"@types/inquirer": "^6.0.0",
"@types/meow": "^5.0.0",
"@types/mocha": "^5.2.6",
"@types/ncp": "^2.0.1",
"@types/node": "^10.0.3",
"@types/prettier": "^1.15.2",
"@types/rimraf": "^2.0.2",
"@types/sinon": "^7.0.11",
"@types/tmp": "^0.1.0",
"@types/update-notifier": "^2.2.0",
"c8": "^4.1.4",
"assert-rejects": "^1.0.0",
"c8": "^4.1.5",
"codecov": "^3.0.1",
"cross-spawn": "^6.0.5",
"fs-extra": "^8.0.0",
"gts": "^1.0.0",
"inline-fixtures": "^1.0.0",
"inline-fixtures": "^1.1.0",
"js-green-licenses": "^0.5.0",
"mocha": "^6.0.0",
"sinon": "^7.3.1",
Expand Down
46 changes: 41 additions & 5 deletions src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@
* limitations under the License.
*/
import * as cp from 'child_process';
import * as fs from 'fs';
import * as inquirer from 'inquirer';
import * as path from 'path';
import { ncp } from 'ncp';
import * as util from 'util';

import {
getPkgManagerCommand,
readFilep as read,
readJsonp as readJson,
writeFileAtomicp as write,
Bag,
DefaultPackage,
} from './util';

import { Options } from './cli';
Expand All @@ -30,6 +35,8 @@ import chalk from 'chalk';

const pkg = require('../../package.json');

const ncpp = util.promisify(ncp);

const DEFAULT_PACKAGE_JSON: PackageJson = {
name: '',
version: '0.0.0',
Expand All @@ -42,10 +49,6 @@ const DEFAULT_PACKAGE_JSON: PackageJson = {
scripts: { test: 'echo "Error: no test specified" && exit 1' },
};

export interface Bag<T> {
[script: string]: T;
}

async function query(
message: string,
question: string,
Expand Down Expand Up @@ -118,9 +121,10 @@ export async function addDependencies(
options: Options
): Promise<boolean> {
let edits = false;
const deps: Bag<string> = {
const deps: DefaultPackage = {
gts: `^${pkg.version}`,
typescript: pkg.devDependencies.typescript,
'@types/node': pkg.devDependencies['@types/node'],
};

if (!packageJson.devDependencies) {
Expand Down Expand Up @@ -241,6 +245,37 @@ async function generateConfigFile(
}
}

export async function installDefaultTemplate(
options: Options
): Promise<boolean> {
const cwd = process.cwd();
const sourceDirName = path.join(__dirname, '../template');
const targetDirName = path.join(cwd, 'src');

try {
fs.mkdirSync(targetDirName);
} catch (error) {
if (error.code !== 'EEXIST') {
throw error;
}
// Else, continue and populate files into the existing directory.
}

// Only install the template if no ts files exist in target directory.
const files = fs.readdirSync(targetDirName);
const tsFiles = files.filter(file => file.toLowerCase().endsWith('.ts'));
if (tsFiles.length !== 0) {
options.logger.log(
'Target src directory already has ts files. ' +
'Template files not installed.'
);
return false;
}
await ncpp(sourceDirName, targetDirName);
options.logger.log('Default template installed.');
return true;
}

export async function init(options: Options): Promise<boolean> {
let generatedPackageJson = false;
let packageJson;
Expand Down Expand Up @@ -276,6 +311,7 @@ export async function init(options: Options): Promise<boolean> {
await generateTsConfig(options);
await generateTsLintConfig(options);
await generatePrettierConfig(options);
await installDefaultTemplate(options);

// Run `npm install` after initial setup so `npm run check` works right away.
if (!options.dryRun) {
Expand Down
12 changes: 12 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,22 @@ import * as fs from 'fs';
import * as path from 'path';
import * as rimraf from 'rimraf';
import { promisify } from 'util';
import * as ncp from 'ncp';

export const readFilep = promisify(fs.readFile);
export const rimrafp = promisify(rimraf);
export const writeFileAtomicp = promisify(require('write-file-atomic'));
export const ncpp = promisify(ncp.ncp);

export interface Bag<T> {
[script: string]: T;
}

export interface DefaultPackage extends Bag<string> {
gts: string;
typescript: string;
'@types/node': string;
}

export async function readJsonp(jsonPath: string) {
const contents = await readFilep(jsonPath, { encoding: 'utf8' });
Expand Down
16 changes: 16 additions & 0 deletions template/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
console.log("Try npm run check/fix!");

const longString = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer ut aliquet diam.';

const trailing = 'Semicolon'

const why = 'am I tabbed?';

export function doSomeStuff(withThis: string, andThat: string, andThose: string[]) {
//function on one line
if(!andThose.length) {return false;}
console.log(withThis);
console.log(andThat);
console.dir(andThose);
}
// TODO: more examples
110 changes: 107 additions & 3 deletions test/test-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@
import * as sinon from 'sinon';
import * as cp from 'child_process';
import * as assert from 'assert';
import * as fs from 'fs';
import * as path from 'path';
import { nop, readJsonp as readJson } from '../src/util';
import { accessSync } from 'fs';
import { nop, readJsonp as readJson, DefaultPackage } from '../src/util';
import { Options } from '../src/cli';
import { PackageJson } from '@npm/types';
import { withFixtures } from 'inline-fixtures';
import { withFixtures, Fixtures } from 'inline-fixtures';
import * as init from '../src/init';

const assertRejects = require('assert-rejects');

const OPTIONS: Options = {
gtsRootDir: path.resolve(__dirname, '../..'),
targetRootDir: './',
Expand Down Expand Up @@ -124,7 +128,11 @@ describe('init', () => {
});

it('addDependencies should not edit existing deps on no', async () => {
const DEPS = { gts: 'something', typescript: 'or the other' };
const DEPS: DefaultPackage = {
gts: 'something',
typescript: 'or the other',
'@types/node': 'or another',
};
const pkg: PackageJson = {
...MINIMAL_PACKAGE_JSON,
devDependencies: { ...DEPS },
Expand Down Expand Up @@ -198,4 +206,100 @@ describe('init', () => {
}
);
});

it('should install a default template if the source directory do not exists', () => {
return withFixtures({}, async dir => {
const indexPath = path.join(dir, 'src', 'index.ts');
await init.init(OPTIONS_YES);
assert.doesNotThrow(() => {
accessSync(indexPath);
});
});
});

it('should install template copy if src directory already exists and is empty', () => {
const FIXTURES = {
src: {},
};
return withFixtures(FIXTURES, async dir => {
const dirPath = path.join(dir, 'src');
const created = await init.installDefaultTemplate(OPTIONS_YES);
assert.strictEqual(created, true);
assert.doesNotThrow(() => {
accessSync(path.join(dirPath, 'index.ts'));
});
});
});

it('should install template copy if src directory already exists and contains files other than ts', () => {
const FIXTURES = {
src: {
'README.md': '# Read this',
},
};
return withFixtures(FIXTURES, async dir => {
const dirPath = path.join(dir, 'src');
const created = await init.installDefaultTemplate(OPTIONS_YES);
assert.strictEqual(created, true);
assert.doesNotThrow(() => {
// Both old and new files should exist.
accessSync(path.join(dirPath, 'README.md'));
accessSync(path.join(dirPath, 'index.ts'));
});
});
});

it('should copy the template with correct contents', () => {
const FIXTURES = {
src: {},
};
return withFixtures(FIXTURES, async dir => {
const destDir = path.join(dir, 'src');
const created = await init.installDefaultTemplate(OPTIONS_YES);
assert.strictEqual(created, true);

// make sure the target directory exists.
accessSync(destDir);

// make sure the copied file exists and has the same content.
const srcFilename = path.join(__dirname, '../template/index.ts');
const destFilename = path.join(destDir, 'index.ts');
const content = fs.readFileSync(destFilename, 'utf8');
assert.strictEqual(content, fs.readFileSync(srcFilename, 'utf8'));
});
});

// FIXME: It seems that on CirrusCI we are able to access inside
// directories where permissions may otherwise forbid access. Enable
// once this has been opened as an issue against Cirrus and fixed.
// it('should not install the default template if the source directory is not accessible', () => {
// const FIXTURES: Fixtures = {
// src: {
// mode: 0o000,
// content: {
// 'README.md': 'Hello World.',
// },
// },
// };
// return withFixtures(FIXTURES, async dir => {
// await assertRejects(init.installDefaultTemplate(OPTIONS_YES));
// });
// });

it('should not install the default template if the source directory already exists and does contain ts files', () => {
const EXISTING = 'src';
const FIXTURES: Fixtures = {
[EXISTING]: {
'main.ts': '42;',
},
};
return withFixtures(FIXTURES, async dir => {
const newPath = path.join(dir, 'src');
const created = await init.installDefaultTemplate(OPTIONS_YES);
assert.strictEqual(created, false);
assert.doesNotThrow(() => {
accessSync(newPath);
});
});
});
});

0 comments on commit 4a3c511

Please sign in to comment.