Skip to content

Commit

Permalink
Add 'generate-index' function to GLSP CLI (#1197)
Browse files Browse the repository at this point in the history
* Add 'generate-index' function to GLSP CLI

- Turn CLI package into ES Module so we can easily use 'globby'
- Minor fix in 'check-headers' if second line does not contain year
  • Loading branch information
martin-fleck-at authored Apr 17, 2024
1 parent b5aff4f commit 063af47
Show file tree
Hide file tree
Showing 22 changed files with 286 additions and 83 deletions.
10 changes: 10 additions & 0 deletions dev-packages/cli/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* eslint-disable header/header */
/* eslint-disable no-undef */
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: '@eclipse-glsp',
rules: {
// turn import issues off as eslint cannot handle ES modules easily
'import/no-unresolved': 'off'
}
};
24 changes: 24 additions & 0 deletions dev-packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,30 @@ Options:
-h, --help display help for command
```

## generateIndex

Use this command to create an index file of all sources for a given directory and all it's sub directories.

```console
$ glsp generateIndex -h
Usage: glsp generateIndex [options] <rootDir>

Generate index files in a given source directory.

Arguments:
rootDir The source directory for index generation.

Options:
-s, --singleIndex Generate a single index file in the source directory instead of indices in each sub-directory (default: false)
-f, --forceOverwrite Overwrite existing index files and remove them if there are no entries (default: false)
-m, --match [match patterns...] File patterns to consider during indexing (default: ["**/*.ts","**/*.tsx"])
-i, --ignore [ignore patterns...] File patterns to ignore during indexing (default: ["**/*.spec.ts","**/*.spec.tsx","**/*.d.ts"])
-s, --style <importStyle> Import Style (choices: "commonjs", "esm", default: "commonjs")
--ignoreFile <ignoreFile> The file that is used to specify patterns that should be ignored during indexing (default: ".indexignore")
-v, --verbose Generate verbose output during generation (default: false)
-h, --help display help for command
```

## More information

For more information, please visit the [Eclipse GLSP Umbrella repository](https://github.com/eclipse-glsp/glsp) and the [Eclipse GLSP Website](https://www.eclipse.org/glsp/).
Expand Down
2 changes: 1 addition & 1 deletion dev-packages/cli/bin/glsp
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#!/usr/bin/env node
require('../lib/app.js');
import('../lib/app.js');
2 changes: 2 additions & 0 deletions dev-packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"url": "https://projects.eclipse.org/projects/ecd.glsp"
}
],
"type": "module",
"bin": {
"glsp": "bin/glsp"
},
Expand All @@ -43,6 +44,7 @@
"dependencies": {
"commander": "^10.0.1",
"glob": "^10.3.10",
"globby": "13.2.2",
"node-fetch": "^2.6.11",
"node-jq": "^4.3.1",
"readline-sync": "^1.4.10",
Expand Down
14 changes: 8 additions & 6 deletions dev-packages/cli/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { CheckHeaderCommand } from './commands/check-header';
import { CoverageReportCommand } from './commands/coverage-report';
import { ReleaseCommand } from './commands/release/release';
import { UpdateNextCommand } from './commands/update-next';
import { baseCommand } from './util/command-util';
import { CheckHeaderCommand } from './commands/check-header.js';
import { CoverageReportCommand } from './commands/coverage-report.js';
import { GenerateIndex } from './commands/generate-index.js';
import { ReleaseCommand } from './commands/release/release.js';
import { UpdateNextCommand } from './commands/update-next.js';
import { baseCommand } from './util/command-util.js';

export const COMMAND_VERSION = '1.1.0-next';

Expand All @@ -28,6 +29,7 @@ const app = baseCommand() //
.addCommand(CoverageReportCommand)
.addCommand(ReleaseCommand)
.addCommand(CheckHeaderCommand)
.addCommand(UpdateNextCommand);
.addCommand(UpdateNextCommand)
.addCommand(GenerateIndex);

app.parse(process.argv);
52 changes: 31 additions & 21 deletions dev-packages/cli/src/commands/check-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ import * as fs from 'fs';
import { glob } from 'glob';
import * as minimatch from 'minimatch';
import * as readline from 'readline-sync';
import * as sh from 'shelljs';
import { baseCommand, configureShell, getShellConfig } from '../util/command-util';
import { getChangesOfLastCommit, getLastModificationDate, getUncommittedChanges } from '../util/git-util';
import sh from 'shelljs';
import { baseCommand, configureShell, getShellConfig } from '../util/command-util.js';
import { getChangesOfLastCommit, getLastModificationDate, getUncommittedChanges } from '../util/git-util.js';

import * as path from 'path';
import { LOGGER } from '../util/logger.js';
import { validateGitDirectory } from '../util/validation-util.js';

import { LOGGER } from '../util/logger';
import { validateGitDirectory } from '../util/validation-util';
import path = require('path');
export interface HeaderCheckOptions {
type: CheckType;
exclude: string[];
Expand Down Expand Up @@ -140,19 +141,22 @@ function validate(rootDir: string, files: string[], options: HeaderCheckOptions)
printFileProgress(i + 1 + noHeadersLength, allFilesLength, `Validating ${file}`);
const copyrightLine = sh.head({ '-n': 2 }, file).stdout.trim().split('\n')[1];
const copyRightYears = copyrightLine.match(YEAR_RANGE_REGEX)!;
const currentStartYear = Number.parseInt(copyRightYears[0], 10);
const currentEndYear = copyRightYears[1] ? Number.parseInt(copyRightYears[1], 10) : undefined;
const result: DateValidationResult = {
currentStartYear,
currentEndYear,
expectedEndYear: defaultEndYear ?? getLastModificationDate(file, rootDir, AUTO_FIX_MESSAGE)!.getFullYear(),
file,
violation: 'none'
};

validateEndYear(result);

results.push(result);
if (!copyRightYears) {
const result: ValidationResult = { file, violation: 'noYear', line: copyrightLine };
results.push(result);
} else {
const currentStartYear = Number.parseInt(copyRightYears[0], 10);
const currentEndYear = copyRightYears[1] ? Number.parseInt(copyRightYears[1], 10) : undefined;
const result: DateValidationResult = {
currentStartYear,
currentEndYear,
expectedEndYear: defaultEndYear ?? getLastModificationDate(file, rootDir, AUTO_FIX_MESSAGE)!.getFullYear(),
file,
violation: 'none'
};
validateEndYear(result);
results.push(result);
}
});

results.sort((a, b) => a.file.localeCompare(b.file));
Expand Down Expand Up @@ -202,7 +206,10 @@ export function handleValidationResults(rootDir: string, results: ValidationResu
fs.writeFileSync(path.join(rootDir, 'headerCheck.json'), JSON.stringify(results, undefined, 2));
}

if (violations.length > 0 && (options.autoFix || readline.keyInYN('Do you want automatically fix copyright year range violations?'))) {
if (
violations.length > 0 &&
(options.autoFix || readline.keyInYN('Do you want to automatically fix copyright year range violations?'))
) {
const toFix = violations.filter(violation => isDateValidationResult(violation)) as DateValidationResult[];
fixViolations(rootDir, toFix, options);
}
Expand All @@ -223,6 +230,8 @@ function toPrintMessage(result: ValidationResult): string {
return `${error} ${message}! Expected end year '${expected}' but is '${actual}'`;
} else if (result.violation === 'noOrMissingHeader') {
return `${error} No or invalid copyright header!`;
} else if (result.violation === 'noYear') {
return `${error} No year found!${result.line ? ' (line: ' + result.line + ')' : ''}`;
}

return `${info} OK`;
Expand Down Expand Up @@ -255,6 +264,7 @@ function fixViolations(rootDir: string, violations: DateValidationResult[], opti
interface ValidationResult {
file: string;
violation: Violation;
line?: string;
}

interface DateValidationResult extends ValidationResult {
Expand All @@ -267,4 +277,4 @@ function isDateValidationResult(object: ValidationResult): object is DateValidat
return 'currentStartYear' in object && 'expectedEndYear' in object;
}

type Violation = 'none' | 'noOrMissingHeader' | 'invalidEndYear';
type Violation = 'none' | 'noOrMissingHeader' | 'invalidEndYear' | 'noYear';
8 changes: 4 additions & 4 deletions dev-packages/cli/src/commands/coverage-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

import * as fs from 'fs';
import * as path from 'path';
import * as sh from 'shelljs';
import { baseCommand, fatalExec, getShellConfig } from '../util/command-util';
import { LOGGER } from '../util/logger';
import { validateDirectory } from '../util/validation-util';
import sh from 'shelljs';
import { baseCommand, fatalExec, getShellConfig } from '../util/command-util.js';
import { LOGGER } from '../util/logger.js';
import { validateDirectory } from '../util/validation-util.js';

export interface CoverageCmdOptions {
coverageScript: string;
Expand Down
154 changes: 154 additions & 0 deletions dev-packages/cli/src/commands/generate-index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/********************************************************************************
* Copyright (c) 2023 EclipseSource and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { createOption } from 'commander';
import * as fs from 'fs';
import { Options as GlobbyOptions, globbySync } from 'globby';
import * as os from 'os';
import * as path from 'path';
import sh from 'shelljs';
import { baseCommand } from '../util/command-util.js';
import { LOGGER, configureLogger } from '../util/logger.js';
import { validateDirectory } from '../util/validation-util.js';

export interface GenerateIndexCmdOptions {
singleIndex: boolean;
forceOverwrite: boolean;
match: string[] | boolean;
ignore: string[] | boolean;
ignoreFile: string;
style: 'commonjs' | 'esm';
verbose: boolean;
}

export const GenerateIndex = baseCommand() //
.name('generateIndex')
.description('Generate index files in a given source directory.')
.argument('<rootDir...>', 'The source directory for index generation.')
.option('-s, --singleIndex', 'Generate a single index file in the source directory instead of indices in each sub-directory', false)
.option('-f, --forceOverwrite', 'Overwrite existing index files and remove them if there are no entries', false)
.option('-m, --match [match patterns...]', 'File patterns to consider during indexing', ['**/*.ts', '**/*.tsx'])
.option('-i, --ignore [ignore patterns...]', 'File patterns to ignore during indexing', ['**/*.spec.ts', '**/*.spec.tsx', '**/*.d.ts'])
.addOption(createOption('-s, --style <importStyle>', 'Import Style').choices(['commonjs', 'esm']).default('commonjs'))
.option('--ignoreFile <ignoreFile>', 'The file that is used to specify patterns that should be ignored during indexing', '.indexignore')
.option('-v, --verbose', 'Generate verbose output during generation', false)
.action(generateIndices);

export async function generateIndices(rootDirs: string[], options: GenerateIndexCmdOptions): Promise<void> {
const dirs = rootDirs.map(rootDir => validateDirectory(path.resolve(rootDir)));
dirs.forEach(dir => generateIndex(dir, options));
}

export async function generateIndex(rootDir: string, options: GenerateIndexCmdOptions): Promise<void> {
configureLogger(options.verbose);
LOGGER.debug('Run generateIndex for', rootDir, 'with the following options:', options);
sh.cd(rootDir);
const cwd = process.cwd();

// we want to match all given patterns and subdirectories and ignore all given patterns and (generated) index files
const pattern = typeof options.match === 'boolean' ? ['**/'] : [...options.match, '**/'];
const ignore = typeof options.ignore === 'boolean' ? ['**/index.ts'] : [...options.ignore, '**/index.ts'];
const globbyOptions: GlobbyOptions = {
ignore,
cwd,
onlyFiles: options.singleIndex,
markDirectories: true, // directories have '/' at the end
ignoreFiles: '**/' + options.ignoreFile // users can add this file in their directories to ignore files for indexing
};
LOGGER.debug('Search for children using the following globby options', globbyOptions);
const files = globbySync(pattern, globbyOptions);
LOGGER.debug('All children considered in the input directory', files);

const relativeRootDirectory = '';
if (options.singleIndex) {
writeIndex(relativeRootDirectory, files.filter(isFile), options);
} else {
// sort by length so we deal with sub-directories before we deal with their parents to determine whether they are empty
const directories = [...files.filter(isDirectory), relativeRootDirectory].sort((left, right) => right.length - left.length);
const directoryChildren = new Map<string, string[]>();
for (const directory of directories) {
const children = files.filter(file => isDirectChild(directory, file, () => !!directoryChildren.get(file)?.length));
directoryChildren.set(directory, children);
writeIndex(directory, children, options);
}
}
}

export function isDirectChild(parent: string, child: string, childHasChildren: () => boolean): boolean {
return isChildFile(parent, child) || (isChildDirectory(parent, child) && childHasChildren());
}

export function isDirectory(file: string): boolean {
return file.endsWith('/');
}

export function isFile(file: string): boolean {
return !isDirectory(file);
}

export function getLevel(file: string): number {
return file.split('/').length;
}

export function isChild(parent: string, child: string): boolean {
return child.startsWith(parent);
}

export function isChildDirectory(parent: string, child: string): boolean {
return isDirectory(child) && isChild(parent, child) && getLevel(child) === getLevel(parent) + 1;
}

export function isChildFile(parent: string, child: string): boolean {
return isFile(child) && isChild(parent, child) && getLevel(child) === getLevel(parent);
}

export function writeIndex(directory: string, exports: string[], options: GenerateIndexCmdOptions): void {
const indexFile = path.join(process.cwd(), directory, 'index.ts');
if (exports.length === 0) {
if (options.forceOverwrite && fs.existsSync(indexFile)) {
LOGGER.info('Remove index file', indexFile);
fs.rmSync(indexFile);
}
return;
}
const exists = fs.existsSync(indexFile);
if (exists && !options.forceOverwrite) {
LOGGER.info("Do not overwrite existing index file. Use '-f' to force an overwrite.", indexFile);
return;
}

const headerContent = exists ? extractReusableContent(fs.readFileSync(indexFile, { encoding: 'utf-8' })) : '';
const exportContent = exports.map(exported => createExport(directory, exported, options)).sort();
const content = headerContent + exportContent.join(os.EOL) + os.EOL; // end with an empty line
LOGGER.info((exists ? 'Overwrite' : 'Write') + ' index file', indexFile);
LOGGER.debug(' ' + content.split(os.EOL).join(os.EOL + ' '));
fs.writeFileSync(indexFile, content, { flag: 'w' });
}

export function createExport(directory: string, relativePath: string, options: GenerateIndexCmdOptions): string {
// remove directory prefix, file extension and directory ending '/'
const parentPrefix = directory.length;
const suffix = isFile(relativePath) ? path.extname(relativePath).length : 1;
const relativeName = relativePath.substring(parentPrefix, relativePath.length - suffix);
const exportName = options.style === 'esm' && isFile(relativePath) ? relativeName + '.js' : relativeName;
const exportLine = `export * from './${exportName}';`;
return exportLine;
}

export function extractReusableContent(fileContent: string): string {
// all code before any actual export lines are considered re-usable
return fileContent.match(/^(.*?)(?=^export)/ms)?.[0] ?? '';
}
10 changes: 5 additions & 5 deletions dev-packages/cli/src/commands/release/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ import fetch from 'node-fetch';
import { resolve } from 'path';
import * as readline from 'readline-sync';
import * as semver from 'semver';
import * as sh from 'shelljs';
import { fatalExec, getShellConfig } from '../../util/command-util';
import { getLatestGithubRelease, getLatestTag, hasGitChanges, isGitRepository } from '../../util/git-util';
import { LOGGER } from '../../util/logger';
import { validateVersion } from '../../util/validation-util';
import sh from 'shelljs';
import { fatalExec, getShellConfig } from '../../util/command-util.js';
import { getLatestGithubRelease, getLatestTag, hasGitChanges, isGitRepository } from '../../util/git-util.js';
import { LOGGER } from '../../util/logger.js';
import { validateVersion } from '../../util/validation-util.js';

export const VERDACCIO_REGISTRY = 'http://localhost:4873/';

Expand Down
6 changes: 3 additions & 3 deletions dev-packages/cli/src/commands/release/release-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import * as sh from 'shelljs';
import { getShellConfig } from '../../util/command-util';
import { LOGGER } from '../../util/logger';
import { checkoutAndCd, commitAndTag, lernaSetVersion, publish, ReleaseOptions, updateLernaForDryRun, yarnInstall } from './common';
import { getShellConfig } from '../../util/command-util.js';
import { LOGGER } from '../../util/logger.js';
import { checkoutAndCd, commitAndTag, lernaSetVersion, publish, ReleaseOptions, updateLernaForDryRun, yarnInstall } from './common.js';

let REPO_ROOT: string;

Expand Down
Loading

0 comments on commit 063af47

Please sign in to comment.