Compare commits

..

2 Commits

Author SHA1 Message Date
Ankit Tiwari
43ac90c3c7 Merge branch 'cli/apply-text-edit' into cli/lint 2020-07-16 11:15:04 +05:30
Ankit Tiwari
7f4f90704d Implement basic lint command 2020-07-15 21:07:07 +05:30
10 changed files with 100 additions and 202 deletions

View File

@@ -11,9 +11,8 @@
"@oclif/command": "^1",
"@oclif/config": "^1",
"@oclif/plugin-help": "^3",
"@types/inquirer": "^6.5.0",
"chalk": "^4.1.0",
"foam-core": "^0.2.0",
"inquirer": "^7.3.2",
"ora": "^4.0.4",
"tslib": "^1"
},
@@ -70,4 +69,4 @@
"version": "oclif-dev readme && git add README.md"
},
"types": "lib/index.d.ts"
}
}

View File

@@ -1,168 +0,0 @@
/*eslint-disable no-unused-vars*/
import { Command, flags } from '@oclif/command';
import * as inquirer from 'inquirer';
import * as ora from 'ora';
// @todo implement this class, currently it does nothing but collect inputs
export default class Init extends Command {
static description = 'Initialize a new Foam workspace from template';
// @todo better examples
static examples = [`$ foam init`];
// @todo validate inputs
static flags = {
help: flags.help({ char: 'h' }),
name: flags.string({
char: 'n',
description: 'workspace name',
}),
scm: flags.string({
char: 's',
description: 'source control (github, git, local)'
}),
template: flags.string({
char: 't',
description: 'template'
}),
gitHubUser: flags.string({
char: 'u',
description: 'github username'
}),
gitHubPassword: flags.string({
description: 'github password'
}),
// @todo make flag
githubPages: flags.string({
char: 'p',
description: 'enable github pages'
}),
repoOwner: flags.string({
char: 'p',
description: 'github repo owner'
}),
visibility: flags.string({
char: 'v',
description: 'github repo visibility (public/private)'
}),
};
async run() {
const { flags } = this.parse(Init);
const name =
flags.name ||
(await inquirer.prompt({
name: 'name',
message: 'Give your workspace a name',
type: 'input',
default: 'foam',
})).name;
const template =
flags.template ||
(await inquirer.prompt({
name: 'template',
message: 'Choose from one of the available templates',
type: 'list',
choices: [
{ name: 'Default (foam-template)' },
{ name: 'Gatsby + GitHub Actions (foam-template-gatsby)' },
{ name: '11ty + Netlify (foam-template-eleventy)' },
{ name: 'MLH Fellowship Workspace (foam-template-mlh)' },
],
})).template;
const scm = (await inquirer.prompt([
{
name: 'scm',
message: 'How do you want to store your workspace?',
type: 'list',
default: 'GitHub',
choices: [
{ name: 'GitHub' },
{ name: 'Local git repository' },
{ name: 'Local directory (no source control)' },
],
},
])).scm;
if (scm === 'GitHub') {
const userName =
flags.gitHubUser ||
(await inquirer.prompt({
name: 'username',
message: 'GitHub username',
type: 'input'
})).username;
const password =
flags.gitHubPassword ||
(await inquirer.prompt({
name: 'password',
message: 'GitHub password',
type: 'password'
})).password;
const owner =
flags.repoOwner ||
(await inquirer.prompt({
name: 'owner',
message: 'GitHub repository owner',
type: 'input',
default: userName
})).owner;
const visibility =
flags.visibility ||
(await inquirer.prompt({
name: 'visibility',
message: 'Should the repository be public or private?',
type: 'list',
choices: [
{ name: 'Public' },
{ name: 'Private' }
],
})).visibility.toLowerCase();
const pages =
flags.githubPages ||
((await inquirer.prompt({
name: 'pages',
message: 'Publish automatically to GitHub pages?',
type: 'list',
choices: [
{ name: 'Yes' },
{ name: 'No' }
],
})).pages === 'Yes');
const sure = (await inquirer.prompt({
name: 'sure',
type: 'confirm',
message: `Create a new ${visibility} Foam in https://github.com/${owner}/${name}?`
})).sure;
if (sure) {
const spinner = ora().start();
await new Promise(resolve => {
setTimeout(() => resolve(), 1000);
});
spinner.succeed();
spinner.succeed('Foam workspace created!');
spinner.succeed('Run "code foam" to open your new workspace');
}
} else {
console.log(`Created a private Foam workspace in ./${name}`);
}
}
}

View File

@@ -59,7 +59,7 @@ Successfully generated link references and heading!
return writeFileToDisk(note.path, file);
}
return Promise.resolve(null);
return null;
})
await Promise.all(fileWritePromises);

View File

@@ -0,0 +1,83 @@
import { Command, flags } from '@oclif/command';
import * as ora from 'ora';
import * as chalk from 'chalk';
import { initializeNoteGraph, Note, NoteLink } from 'foam-core';
import * as fs from 'fs'
interface InvalidReference {
note: Note,
link: NoteLink
}
export default class Lint extends Command {
static description = 'Shows lint errors and warnings across all the markdown files in the given workspaces';
// TODO:
static examples = [
`$ foam-cli Lint path-to-foam-workspace
Successfully generated link references and heading!
`,
]
static flags = {
help: flags.help({ char: 'h' }),
}
static args = [{ name: 'workspacePath' }]
async run() {
const spinner = ora('Reading Files').start();
const { args, flags } = this.parse(Lint)
const { workspacePath = './' } = args;
if (fs.existsSync(workspacePath) && fs.lstatSync(workspacePath).isDirectory()) {
const graph = await initializeNoteGraph(workspacePath);
const notes = graph.getNotes();
spinner.text = `${notes.length} files found`;
// TODO: Figure out why there is an undefined note
const orphanedNotes = notes.filter(note => note && graph.getBacklinks(note.id).length === 0);
// Find out invalid references
// ⚠️ Warning: Dirty code ahead. This is just a proof of concept.
// @ts-ignore
const invalidLinks: InvalidReference[] = notes.filter(Boolean).map(note => {
return graph
.getNoteLinks(note.id)
.map(link => {
const target = graph.getNote(link.to);
return !target ? { note: note, link: link } : false;
})
.filter(Boolean)
}).reduce((acc, curr) => ([...acc, ...curr]), []) // flatten the array
const warnings = `${orphanedNotes.map(note => {
return `→ "${note.title}" is an orphan note.`;
}).join('\n')}`;
const errors = `${invalidLinks.map(item => {
return `→ Link "${item.link.to}" in "${item.note.title}" points to a non-existing note [${item.link.position.start.line}, ${item.link.position.start.column}] `;
}).join('\n')}`;
spinner.stop()
this.log(chalk.yellowBright('⚠️ Warnings:'));
this.log(warnings);
this.log(chalk.redBright('❌ Errors:'));
console.log(errors);
}
else {
spinner.fail('Directory does not exist!');
}
}
}

View File

@@ -49,7 +49,7 @@ Successfully generated link references and heading!
if (kebabCasedFileName) {
return renameFile(note.path, kebabCasedFileName);
}
return Promise.resolve(null);
return null;
})
await Promise.all(fileRename);
@@ -80,7 +80,7 @@ Successfully generated link references and heading!
return writeFileToDisk(note.path, file);
}
return Promise.resolve(null);
return null;
}))
await Promise.all(fileWritePromises);

View File

@@ -1,10 +1,5 @@
import * as fs from 'fs'
/**
*
* @param fileUri absolute path for the file that needs to renamed
* @param newFileName "new file name" without the extension
*/
export const renameFile = async (fileUri: string, newFileName: string) => {
const fileName = fileUri.split('/').pop();
const extension = fileName?.split('.').pop();

View File

@@ -3,8 +3,6 @@ import { renameFile } from '../src/utils/rename-file'
import * as fs from 'fs';
import mockFS from 'mock-fs';
const doesFileExist = (path) => fs.promises.access(path).then(() => true).catch(() => false);
describe('renameFile', () => {
const fileUri = './test/oldFileName.md';
@@ -18,11 +16,10 @@ describe('renameFile', () => {
});
it('should rename existing file', async () => {
expect(await doesFileExist(fileUri)).toBe(true);
expect(fs.existsSync(fileUri)).toBe(true);
renameFile(fileUri, 'new-file-name');
expect(await doesFileExist(fileUri)).toBe(false);
expect(await doesFileExist('./test/new-file-name.md')).toBe(true);
expect(fs.existsSync(fileUri)).toBe(false);
expect(fs.existsSync('./test/new-file-name.md')).toBe(true);
});
});

View File

@@ -18,7 +18,7 @@ describe('writeFileToDisk', () => {
it('should overrwrite existing file in the disk with the new data', async () => {
const expected = `content in the new file`;
await writeFileToDisk(fileUri, expected);
const actual = await fs.promises.readFile(fileUri, { encoding: 'utf8' });
const actual = fs.readFileSync(fileUri, { encoding: 'utf8' });
expect(actual).toBe(expected);
});
})

View File

@@ -85,6 +85,13 @@ export class NoteGraph {
throw new Error(`Note with ID [${noteId}] not found`);
}
// Note: This is temporary until we figure out how to put
// position inside Link (needed for linting)
public getNoteLinks(noteId: ID): NoteLink[] {
const note = this.getNote(noteId);
return note ? note.links : [];
}
public getAllLinks(noteId: ID): Link[] {
return (this.graph.nodeEdges(noteId) || []).map(edge =>
convertEdgeToLink(edge, this.graph)

View File

@@ -2350,14 +2350,6 @@
resolved "https://registry.yarnpkg.com/@types/graphlib/-/graphlib-2.1.6.tgz#5c7b515bfadc08d737f2e84fadbd151117c73207"
integrity sha512-os2Xj+pV/iwLkLX17LWuXdPooA4Jf4xg8WSdKPUi0tCSseP95oikcA1irOgVl3K2QYnoXrjJT3qVZeQ1uskB7g==
"@types/inquirer@^6.5.0":
version "6.5.0"
resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be"
integrity sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==
dependencies:
"@types/through" "*"
rxjs "^6.4.0"
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@@ -2452,13 +2444,6 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/through@*":
version "0.0.30"
resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895"
integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==
dependencies:
"@types/node" "*"
"@types/unist@^2.0.0", "@types/unist@^2.0.2":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
@@ -5952,7 +5937,7 @@ inquirer@^6.2.0, inquirer@^6.2.2:
strip-ansi "^5.1.0"
through "^2.3.6"
inquirer@^7.0.0, inquirer@^7.0.4, inquirer@^7.3.2:
inquirer@^7.0.0, inquirer@^7.0.4:
version "7.3.2"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.2.tgz#25245d2e32dc9f33dbe26eeaada231daa66e9c7c"
integrity sha512-DF4osh1FM6l0RJc5YWYhSDB6TawiBRlbV9Cox8MWlidU218Tb7fm3lQTULyUJDfJ0tjbzl0W4q651mrCCEM55w==