Skip to content
Merged
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 @@ -2,6 +2,7 @@
src/**/*.js
src/**/*.js.map
src/**/*.d.ts
!src/prettier/schema.d.ts

# IDEs
.idea/
Expand Down
11 changes: 11 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# Ignores TypeScript files, but keeps definitions.
*.ts
!*.d.ts

*.js.map
!src/**/files/**/*.js
!src/**/files/**/*.ts

.vscode/*

sandbox
docs

*.circleci
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@
"angular"
],
"main": "src/prettier/index.js",
"author": "Kevin Schuchard",
"bin": "src/prettier/index.js",
"author": "Kevin Schuchard <schuchard.kevin@gmail.com>",
"bugs": "https://github.com/schuchard/prettier-schematic/issues",
"license": "MIT",
"schematics": "./src/collection.json",
"engines": {
"node": ">=8.11.0"
},
"dependencies": {
"@angular-devkit/core": "^0.6.8",
"@angular-devkit/schematics": "^0.6.8",
Expand All @@ -38,7 +43,7 @@
"commitizen": "^2.10.1",
"cz-conventional-changelog": "^2.1.0",
"npm-run-all": "^4.1.3",
"semantic-release": "^15.5.4"
"semantic-release": "15.5.5"
},
"config": {
"commitizen": {
Expand Down
12 changes: 9 additions & 3 deletions src/collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"prettier": {
"description": "A blank schematic.",
"factory": "./prettier/index"
}
"description": "AddsPrettier to the application.",
"factory": "./prettier/index",
"schema": "./prettier/schema.json"
},
"ng-add": {
"description": "AddsPrettier to the application.",
"factory": "./prettier/index",
"schema": "./prettier/schema.json"
},
}
}
40 changes: 35 additions & 5 deletions src/prettier/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
mergeWith,
} from '@angular-devkit/schematics';
import { Observable, of } from 'rxjs';
import { map, concatMap } from 'rxjs/operators';
import { map, concatMap, filter } from 'rxjs/operators';

import {
PrettierOptions,
Expand All @@ -19,24 +19,39 @@ import {
removeConflictingTsLintRules,
} from '../utility/prettier-util';
import { addPackageJsonDependency, NodeDependencyType } from '../utility/dependencies';
import { getLatestNodeVersion, NpmRegistryPackage, getFileAsJson } from '../utility/util';
import {
getLatestNodeVersion,
NpmRegistryPackage,
getFileAsJson,
addPropertyToPackageJson,
} from '../utility/util';

export default function(options: PrettierOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
const cliOptions = getDefaultOptions(context, options, new PrettierSettings());

return chain([
addDependencies(),
addDependencies(cliOptions),
addPrettierFiles(cliOptions),
modifyTsLint(),
updateEditorConfig(cliOptions),
addLintStagedConfig(cliOptions),
])(tree, context);
};
}

function addDependencies(): Rule {
function addDependencies(options: PrettierOptions): Rule {
return (tree: Tree): Observable<Tree> => {
return of('prettier').pipe(
const lintStagedDep = ['lint-staged', 'husky'];

return of('prettier', 'lint-staged', 'husky').pipe(
filter((pkg) => {
if (options.lintStaged === 'false') {
// remove lint-staged deps
return !lintStagedDep.some((p) => p === pkg);
}
return true;
}),
concatMap((pkg: string) => getLatestNodeVersion(pkg)),
map((packageFromRegistry: NpmRegistryPackage) => {
const { name, version } = packageFromRegistry;
Expand Down Expand Up @@ -118,3 +133,18 @@ function updateEditorConfig(options: PrettierOptions): Rule {
return tree;
};
}

function addLintStagedConfig(options: PrettierOptions) {
return (tree: Tree, context: SchematicContext) => {
if (options.lintStaged !== 'false') {
addPropertyToPackageJson(tree, context, 'scripts', {
precommit: 'lint-staged',
});

addPropertyToPackageJson(tree, context, 'lint-staged', {
'*.{ts,tsx}': ['prettier --parser typescript --write', 'git add'],
});
}
return tree;
};
}
46 changes: 46 additions & 0 deletions src/prettier/schema.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
export interface Schema {
// Specify the line length that the printer will wrap on.
printWidth?: number;

// Specify the number of spaces per indentation-level.
tabWidth?: boolean;

// Indent lines with tabs instead of spaces.
useTabs?: boolean;

// Print semicolons at the ends of statements.
semi?: true;

// Use single quotes instead of double quotes.
singleQuote?: boolean;

// Print trailing commas wherever possible when multi-line. (A single-line array, for example, never gets trailing commas.)
trailingComma?: string;

// Print spaces between brackets in object literals.
bracketSpacing?: true;

// Put the > of a multi-line JSX element at the end of the last line instead of being alone on the next line (does not apply to self closing elements).
jsxBracketSameLine?: boolean;

// Include parentheses around a sole arrow function parameter.
arrowParens?: string;

// Format only a segment of a file.
rangeStart?: number;

// Format only a segment of a file.
rangeEnd?: Infinity;

// Prettier can restrict itself to only format files that contain a special comment, called a pragma, at the top of the file. This is very useful when gradually transitioning large, unformatted codebases to prettier.
requirePragma?: boolean;

// Prettier can insert a special @format marker at the top of files specifying that the file has been formatted with prettier. This works well when used in tandem with the --require-pragma option.
insertPragma?: boolean;

// By default, Prettier will wrap markdown text as-is since some services use a linebreak-sensitive renderer, e.g. GitHub comment and BitBucket. In some cases you may want to rely on editor/viewer soft wrapping instead, so this option allows you to opt out with \"never\".
proseWrap?: string;

// Run Prettier against staged git files.
lintStaged?: boolean;
}
90 changes: 90 additions & 0 deletions src/prettier/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
{
"$schema": "http://json-schema.org/schema",
"id": "SchuchardSchematicsPrettier",
"title": "Angular Prettier Options Schema",
"type": "object",
"properties": {
"printWidth": {
"type": "number",
"description": "Specify the line length that the printer will wrap on.",
"default": "80"
},
"tabWidth": {
"type": "number",
"description": "Specify the number of spaces per indentation-level.",
"default": "false"
},
"useTabs": {
"type": "boolean",
"description": "Indent lines with tabs instead of spaces.",
"default": "false"
},
"semi": {
"type": "boolean",
"description": "Print semicolons at the ends of statements.",
"default": "true"
},
"singleQuote": {
"type": "boolean",
"description": "Use single quotes instead of double quotes.",
"default": "false"
},
"trailingComma": {
"type": "string",
"description":
"Print trailing commas wherever possible when multi-line. (A single-line array, for example, never gets trailing commas.)",
"default": "none"
},
"bracketSpacing": {
"type": "boolean",
"description": "Print spaces between brackets in object literals.",
"default": "true"
},
"jsxBracketSameLine": {
"type": "boolean",
"description":
"Put the > of a multi-line JSX element at the end of the last line instead of being alone on the next line (does not apply to self closing elements).",
"default": "false"
},
"arrowParens": {
"type": "string",
"description": "Include parentheses around a sole arrow function parameter.",
"default": "avoid"
},
"rangeStart": {
"type": "number",
"description": "Format only a segment of a file.",
"default": "0"
},
"rangeEnd": {
"type": "number",
"description": "Format only a segment of a file.",
"default": "Infinity"
},
"requirePragma": {
"type": "boolean",
"description":
"Prettier can restrict itself to only format files that contain a special comment, called a pragma, at the top of the file. This is very useful when gradually transitioning large, unformatted codebases to prettier.",
"default": "false"
},
"insertPragma": {
"type": "boolean",
"description":
"Prettier can insert a special @format marker at the top of files specifying that the file has been formatted with prettier. This works well when used in tandem with the --require-pragma option.",
"default": "false"
},
"proseWrap": {
"type": "string",
"description":
"By default, Prettier will wrap markdown text as-is since some services use a linebreak-sensitive renderer, e.g. GitHub comment and BitBucket. In some cases you may want to rely on editor/viewer soft wrapping instead, so this option allows you to opt out with \"never\".",
"default": "preserve"
},
"lintStaged": {
"type": "boolean",
"description": "Run Prettier against staged git files.",
"default": "true"
}
},
"required": [],
"additionalProperties": false
}
2 changes: 2 additions & 0 deletions src/utility/prettier-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface PrettierOptions {
requirePragma?: boolean;
insertPragma?: boolean;
proseWrap?: 'preserve' | 'always' | 'never';
lintStaged?: 'true' | 'false';
[index: string]: any;
}

Expand All @@ -37,6 +38,7 @@ export class PrettierSettings implements PrettierOptions {
requirePragma = false;
insertPragma = false;
proseWrap = 'preserve' as 'preserve';
lintStaged = 'true' as 'true';
}

export function getDefaultOptions(
Expand Down
75 changes: 73 additions & 2 deletions src/utility/util.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { get } from 'http';
import { Tree, SchematicsException } from '@angular-devkit/schematics';
import { parseJson, JsonParseMode, JsonValue } from '@angular-devkit/core';
import { Tree, SchematicsException, SchematicContext } from '@angular-devkit/schematics';
import {
parseJson,
JsonParseMode,
JsonValue,
JsonAstObject,
parseJsonAst,
} from '@angular-devkit/core';
import { findPropertyInAstObject, insertPropertyInAstObjectInOrder, appendPropertyInAstObject } from './json-utils';

export interface NpmRegistryPackage {
name: string;
version: string;
}

export enum Config {
PackageJsonPath = 'package.json',
JsonIndentLevel = 4,
}

export function getLatestNodeVersion(packageName: string): Promise<NpmRegistryPackage> {
const DEFAULT_VERSION = 'latest';

Expand Down Expand Up @@ -39,3 +51,62 @@ export function getFileAsJson(host: Tree, path: string): JsonValue {

return parseJson(content, JsonParseMode.Loose);
}

export function addPropertyToPackageJson(
tree: Tree,
context: SchematicContext,
propertyName: string,
propertyValue: { [key: string]: any }
) {
const packageJsonAst = readPackageJson(tree);
const pkgNode = findPropertyInAstObject(packageJsonAst, propertyName);
const recorder = tree.beginUpdate('package.json');

if (!pkgNode) {
// outer node missing, add key/value
appendPropertyInAstObject(
recorder,
packageJsonAst,
propertyName,
propertyValue,
Config.JsonIndentLevel
);
} else if (pkgNode.kind === 'object') {
// property exists, update values
for (let [key, value] of Object.entries(propertyValue)) {
const innerNode = findPropertyInAstObject(pkgNode, key);

if (!innerNode) {
// script not found, add it
context.logger.debug(`creating ${key} with ${value}`);

insertPropertyInAstObjectInOrder(recorder, pkgNode, key, value, Config.JsonIndentLevel);
} else {
// script found, overwrite value
context.logger.debug(`overwriting ${key} with ${value}`);

const { end, start } = innerNode;

recorder.remove(start.offset, end.offset - start.offset);
recorder.insertRight(start.offset, JSON.stringify(value));
}
}
}

tree.commitUpdate(recorder);
}

export function readPackageJson(tree: Tree): JsonAstObject {
const buffer = tree.read(Config.PackageJsonPath);
if (buffer === null) {
throw new SchematicsException('Could not read package.json.');
}
const content = buffer.toString();

const packageJson = parseJsonAst(content, JsonParseMode.Strict);
if (packageJson.kind != 'object') {
throw new SchematicsException('Invalid package.json. Was expecting an object');
}

return packageJson;
}
Loading