Skip to content

Commit

Permalink
Set up formatting check and local formatting script (#5530)
Browse files Browse the repository at this point in the history
  • Loading branch information
hsubox76 authored Oct 15, 2021
1 parent 04295f2 commit 192ac86
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 270 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/format.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Formatting Check (Run yarn format locally if this fails)

on: pull_request

env:
GITHUB_PULL_REQUEST_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
GITHUB_PULL_REQUEST_BASE_SHA: ${{ github.event.pull_request.base.sha }}

jobs:
format:
name: Run license and prettier formatting tasks
runs-on: ubuntu-latest

steps:
- name: Checkout Repo
uses: actions/checkout@master
with:
# get all history for the diff
fetch-depth: 0
- name: Set up Node (14)
uses: actions/setup-node@v2
with:
node-version: 14.x
- name: Yarn install
run: yarn
- name: Run formatting script
run: yarn format
- name: Check for changes (fail if so)
run: git diff --exit-code
- name: Formatting needs to be updated. See message below.
if: ${{ failure() }}
run: echo "Something was changed by formatting. Run \`yarn format\` locally to do a prettier/license pass. Use \`yarn format --help\` to see options."
11 changes: 3 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,15 @@
"docgen:compat": "yarn docgen:compat:js; yarn docgen:compat:node",
"docgen:compat:js": "node scripts/docgen-compat/generate-docs.js --api js",
"docgen:compat:node": "node scripts/docgen-compat/generate-docs.js --api node",
"prettier": "prettier --config .prettierrc --write '**/*.{ts,js}'",
"lint": "lerna run --scope @firebase/* lint",
"lint:fix": "lerna run --scope @firebase/* lint:fix",
"size-report": "ts-node-script scripts/size_report/report_binary_size.ts",
"modular-export-size-report": "ts-node-script scripts/size_report/report_modular_export_binary_size.ts",
"api-report": "lerna run --scope @firebase/* api-report",
"postinstall": "yarn --cwd repo-scripts/changelog-generator build",
"sa": "ts-node-script repo-scripts/size-analysis/cli.ts",
"api-documenter-devsite": "ts-node-script repo-scripts/api-documenter/src/start.ts"
"api-documenter-devsite": "ts-node-script repo-scripts/api-documenter/src/start.ts",
"format": "ts-node ./scripts/format/format.ts"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -155,10 +155,5 @@
"watch": "1.0.2",
"webpack": "4.46.0",
"yargs": "17.2.0"
},
"husky": {
"hooks": {
"pre-commit": "node tools/gitHooks/precommit.js"
}
}
}
}
98 changes: 98 additions & 0 deletions scripts/format/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* @license
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { doPrettier } from './prettier';
import { doLicense } from './license';
import { resolve } from 'path';
import simpleGit from 'simple-git/promise';
import chalk from 'chalk';
import glob from 'glob';
import { join } from 'path';
import yargs from 'yargs/yargs';
import { hideBin } from 'yargs/helpers';

// Computed Deps
const root = resolve(__dirname, '../..');
const git = simpleGit(root);

const { path: targetPath, all: runOnAll } = yargs(hideBin(process.argv))
.option('all', {
describe: 'Run on all js/ts files in repo',
type: 'boolean'
})
.option('path', {
describe: 'Specific directory to run on',
type: 'string'
})
.help()
.usage(
`Runs prettier formatting and updates license headers. ` +
`If no arguments are provided it will run formatting on any ` +
`files changed since master.`
)
.parseSync();

const format = async () => {
let changedFiles: string[] | undefined;
try {
if (!runOnAll) {
// If a file pattern is provided, get the individual files.
if (targetPath) {
changedFiles = await new Promise(resolve => {
glob(join(targetPath, '/**/*'), (err, res) => resolve(res));
});
} else {
// Otherwise get all files changed since master.
const baseSha = process.env.GITHUB_PULL_REQUEST_BASE_SHA || 'master';
const diff = await git.diff([
'--name-only',
'--diff-filter=d',
baseSha
]);
changedFiles = diff.split('\n');

if (changedFiles.length === 0) {
console.log(chalk`{green No files changed since ${baseSha}.}`);
return;
}
}

// Only run on .js or .ts files.
changedFiles = changedFiles!.filter(line => line.match(/\.(js|ts)$/));

if (changedFiles.length === 0) {
console.log(chalk`{green No .js or .ts files found in list.`);
return;
}
}

await doPrettier(changedFiles);

await doLicense(changedFiles);

process.exit();
} catch (err) {
console.error(chalk`
{red Formatting failed, error body below}
`);
console.error(err);
return process.exit(1);
}
};

format();
76 changes: 33 additions & 43 deletions tools/gitHooks/license.js → scripts/format/license.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,13 @@
* limitations under the License.
*/

const { resolve } = require('path');
const simpleGit = require('simple-git/promise');
const fs = require('mz/fs');
const ora = require('ora');
const chalk = require('chalk');

const root = resolve(__dirname, '../..');
const git = simpleGit(root);
import fs from 'mz/fs';
import chalk from 'chalk';
import glob from 'glob';

const licenseHeader = `/**
* @license
* Copyright 2021 Google LLC
* Copyright ${new Date().getFullYear()} Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -45,15 +41,15 @@ const licenseHeader = `/**
const copyrightPattern = /Copyright \d{4} Google (Inc\.|LLC)/;
const oldCopyrightPattern = /(\s*\*\s*Copyright \d{4}) Google Inc\./;

async function readFiles(paths) {
async function readFiles(paths: string[]) {
const fileContents = await Promise.all(paths.map(path => fs.readFile(path)));
return fileContents.map((buffer, idx) => ({
contents: String(buffer),
path: paths[idx]
}));
}

function addLicenceTag(contents) {
function addLicenseTag(contents: string) {
const lines = contents.split('\n');
let newLines = [];
for (const line of lines) {
Expand All @@ -66,7 +62,7 @@ function addLicenceTag(contents) {
return newLines.join('\n');
}

function rewriteCopyrightLine(contents) {
function rewriteCopyrightLine(contents: string) {
const lines = contents.split('\n');
let newLines = lines.map(line => {
return line.replace(oldCopyrightPattern, (_, leader) => {
Expand All @@ -76,13 +72,25 @@ function rewriteCopyrightLine(contents) {
return newLines.join('\n');
}

async function doLicense(changedFiles) {
const licenseSpinner = ora(' Validating License Headers').start();
export async function doLicense(changedFiles?: string[]) {
let count = 0;

const paths = changedFiles.filter(line => line.match(/(js|ts)$/));
if (paths.length === 0) return;
let filesToChange: string[] = changedFiles ?? [];

const files = await readFiles(paths);
if (!changedFiles) {
filesToChange = await new Promise(resolve => {
glob(
'+(packages|repo-scripts)/**/*.+(ts|js)',
{ ignore: ['**/node_modules/**', './node_modules/**', '**/dist/**'] },
(err, res) => resolve(res)
);
});
}

console.log(
chalk`{green Validating license headers in ${filesToChange.length} files.}`
);
const files = await readFiles(filesToChange);

await Promise.all(
files.map(({ contents, path }) => {
Expand All @@ -91,50 +99,32 @@ async function doLicense(changedFiles) {
// Files with no license block at all.
if (result.match(copyrightPattern) == null) {
result = licenseHeader + result;
console.log(`Adding license to ${path}.`);
}

// Files with no @license tag.
if (result.match(/@license/) == null) {
result = addLicenceTag(result);
result = addLicenseTag(result);
console.log(`Adding @license tag to ${path}.`);
}

// Files with the old form of copyright notice.
if (result.match(oldCopyrightPattern) != null) {
result = rewriteCopyrightLine(result);
console.log(`Updating old copyright notice found in ${path}.`);
}

if (contents !== result) {
count++;
return fs.writeFile(path, result, 'utf8');
} else {
return Promise.resolve();
}
})
);

licenseSpinner.stopAndPersist({
symbol: '✅'
});

// Diff unstaged (license writes) against staged.
const stageDiff = await git.diff(['--name-only']);

if (!stageDiff) {
console.log(chalk`\n{red License pass caused no changes.}\n`);
return;
if (count === 0) {
console.log(chalk`{green No files needed license changes.}`);
} else {
console.log(
`License script modified ${stageDiff.split('\n').length - 1} files.`
);
console.log(chalk`{green ${count} files had license headers updated.}`);
}

const gitSpinner = ora(' Git staging license text modifications.').start();
await git.add('.');

gitSpinner.stopAndPersist({
symbol: '▶️'
});
}

module.exports = {
doLicense
};
95 changes: 95 additions & 0 deletions scripts/format/prettier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* @license
* Copyright 2017 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { resolve } from 'path';
import { exec, spawn } from 'child-process-promise';
import chalk from 'chalk';

const root = resolve(__dirname, '../..');
const packageJson = require(root + '/package.json');

async function checkVersion() {
const { stdout } = await exec('yarn prettier --version', {
cwd: root
});
const lines = stdout.split('\n');
let runtimeVersion;
for (const line of lines) {
if (line.match(/^\d+\.\d+\.\d+$/)) {
runtimeVersion = line;
break;
}
}
if (!runtimeVersion) {
console.warn('Was not able to find runtime version of prettier.');
return;
}
const packageVersion = packageJson.devDependencies.prettier;
if (packageVersion !== runtimeVersion) {
const mismatchText =
`Installed version of prettier (${runtimeVersion}) does not match ` +
`required version (${packageVersion}).`;
const versionMismatchMessage = chalk`
{red ${mismatchText}}
{yellow Please re-run {reset 'yarn'} from the root of the repo and try again.}
`;
throw new Error(versionMismatchMessage);
}
}

export async function doPrettier(changedFiles?: string[]) {
try {
await checkVersion();
} catch (e) {
console.error(e);
return process.exit(1);
}

let prettierArgs = [
'prettier',
'--config',
`${resolve(root, '.prettierrc')}`,
'--write'
];

if (changedFiles) {
prettierArgs = [...prettierArgs, ...changedFiles];
console.log(
chalk`{green Validating ${changedFiles.length} files with Prettier}`
);
} else {
prettierArgs.push('{,!(node_modules)/}**/*.{js,ts}');
console.log(chalk`{green Validating all .js and .ts files with Prettier}`);
}

try {
await spawn('yarn', prettierArgs, {
stdio: 'inherit',
cwd: root
});
} catch (e) {
if ((e as NodeJS.ErrnoException).code === 'E2BIG') {
console.error(
chalk`{red Too many files, use a smaller pattern or use the --all flag.}`
);
process.exit();
} else {
throw e;
}
}
}
Loading

0 comments on commit 192ac86

Please sign in to comment.