Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NPM plugin #70

Merged
merged 13 commits into from
Jun 19, 2018
10 changes: 9 additions & 1 deletion cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
const yargs = require("yargs");
const fs = require("fs");
const glob = require("glob");
const { Documentalist, KssPlugin, MarkdownPlugin, TypescriptPlugin } = require("./dist/");
const { Documentalist, KssPlugin, MarkdownPlugin, NpmPlugin, TypescriptPlugin } = require("./dist/");

const argv = yargs
.alias("v", "version")
Expand All @@ -25,6 +25,11 @@ const argv = yargs
desc: "use MarkdownPlugin for .md files",
type: "boolean",
})
.option("npm", {
default: true,
desc: "use NPM plugin for package.json files",
type: "boolean",
})
.option("ts", {
default: true,
desc: "use TypescriptPlugin for .tsx? files",
Expand All @@ -42,6 +47,9 @@ let docs = Documentalist.create();
if (argv.md) {
docs = docs.use(".md", new MarkdownPlugin());
}
if (argv.npm) {
docs = docs.use("package.json", new NpmPlugin());
}
if (argv.ts) {
docs = docs.use(/\.tsx?$/, new TypescriptPlugin({ excludePaths: ["__tests__/"] }));
}
Expand Down
51 changes: 51 additions & 0 deletions src/__tests__/npm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Copyright 2017-present Palantir Technologies, Inc. All rights reserved.
* Licensed under the BSD-3 License as modified (the “License”); you may obtain
* a copy of the license in the LICENSE and PATENTS files in the root of this
* repository.
*/

import { Documentalist } from "../documentalist";
import { NpmPlugin } from "../plugins/npm";

describe("NpmPlugin", () => {
const dm = Documentalist.create().use("package.json", new NpmPlugin());

it("npm info matches package.json", async () => {
const { npm: { documentalist } } = await dm.documentGlobs("package.json");
const pkg = require("../../package.json");
// NOTE: not using snapshot as it would change with every release due to `npm info` call.
expect(documentalist.name).toBe(pkg.name);
expect(documentalist.description).toBe(pkg.description);
expect(documentalist.latestVersion).toBe(pkg.version);
});

it("handles npm info fails", async () => {
const { npm: { doesNotExist } } = await dm.documentFiles([
{ path: "package.json", read: () => `{ "name": "doesNotExist", "version": "1.0.0" }` },
]);
expect(doesNotExist.name).toBe("doesNotExist");
expect(doesNotExist.latestVersion).toBe("1.0.0");
expect(doesNotExist.published).toBe(false);
});

it("options", async () => {
const dm2 = Documentalist.create().use(
"package.json",
new NpmPlugin({ excludeNames: [/two/i], excludePrivate: true }),
);
const { npm } = await dm2.documentFiles([
{
// excludePrivate
path: "one/package.json",
read: () => `{ "name": "packageOne", "private": true, "version": "1.0.0" }`,
},
{
// excludeNames
path: "two/package.json",
read: () => `{ "name": "packageTwo", "version": "1.0.0" }`,
},
]);
expect(Object.keys(npm)).toHaveLength(0);
});
});
5 changes: 5 additions & 0 deletions src/client/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export interface ICompiler {
*/
objectify<T>(array: T[], getKey: (item: T) => string): { [key: string]: T };

/**
* Get the relative path from `sourceBaseDir` to the given path.
*/
relativePath(path: string): string;

/**
* Render a block of content by extracting metadata (YAML front matter) and
* splitting text content into markdown-rendered HTML strings and `{ tag,
Expand Down
1 change: 1 addition & 0 deletions src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
export * from "./compiler";
export * from "./kss";
export * from "./markdown";
export * from "./npm";
export * from "./plugin";
export * from "./tags";
export * from "./typescript";
Expand Down
43 changes: 43 additions & 0 deletions src/client/npm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* Copyright 2018-present Palantir Technologies, Inc. All rights reserved.
* Licensed under the BSD-3 License as modified (the “License”); you may obtain
* a copy of the license in the LICENSE and PATENTS files in the root of this
* repository.
*/

export interface INpmPackage {
/** Package name. */
name: string;

/** Package description. */
description?: string;

/** Latest version of the package (npm `latest` dist-tag). */
latestVersion: string;

/** Next version of the package (npm `next` dist-tag). */
nextVersion?: string;

/** Whether this package is marked `private`. */
private: boolean;

/** Whether this package is published to NPM. */
published: boolean;

/** Relative path from `sourceBaseDir` to this package. */
sourcePath: string;

/** All published versions of this package. */
versions: string[];
}

/**
* The `KssPlugin` exports a `css` key that contains a map of styleguide references to markup/modifier examples.
*
* @see KSSPlugin
*/
export interface INpmPluginData {
npm: {
[packageName: string]: INpmPackage;
};
}
15 changes: 15 additions & 0 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import * as yaml from "js-yaml";
import * as marked from "marked";
import { relative } from "path";
import { IBlock, ICompiler, IHeadingTag, StringOrTag } from "./client";

/**
Expand All @@ -31,6 +32,15 @@ export interface ICompilerOptions {
* Do not include the `@` prefix in the strings.
*/
reservedTags?: string[];

/**
* Base directory for generating relative `sourcePath`s.
*
* This option _does not affect_ glob expansion, only the generation of
* `sourcePath` in plugin data.
* @default process.cwd()
*/
sourceBaseDir?: string;
}

export class Compiler implements ICompiler {
Expand All @@ -43,6 +53,11 @@ export class Compiler implements ICompiler {
}, {});
}

public relativePath = (path: string) => {
const { sourceBaseDir = process.cwd() } = this.options;
return relative(sourceBaseDir, path);
};

public renderBlock = (blockContent: string, reservedTagWords = this.options.reservedTags): IBlock => {
const { contentsRaw, metadata } = this.extractMetadata(blockContent.trim());
const contents = this.renderContents(contentsRaw, reservedTagWords);
Expand Down
3 changes: 3 additions & 0 deletions src/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import * as path from "path";
import { Documentalist } from "./documentalist";
import { NpmPlugin } from "./plugins/npm";
import { TypescriptPlugin } from "./plugins/typescript";

// Launch "Documentalist" from Debug panel in VS Code (see .vscode/launch.json).
Expand All @@ -15,7 +16,9 @@ import { TypescriptPlugin } from "./plugins/typescript";
// Set breakpoints in original .ts source and debug in the editor!
Documentalist.create()
.use(".ts", new TypescriptPlugin())
.use("package.json", new NpmPlugin())
.documentGlobs(
path.join(__dirname, "..", "package.json"),
// compile test fixtures:
path.join(__dirname, "__tests__", "__fixtures__", "*.ts"),
// compile our own source code:
Expand Down
1 change: 1 addition & 0 deletions src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@

export * from "./kss";
export * from "./markdown";
export * from "./npm";
export * from "./typescript/index";
2 changes: 1 addition & 1 deletion src/plugins/kss.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as path from "path";
import { ICompiler, IFile, IKssExample, IKssModifier, IKssPluginData, IPlugin } from "../client";

/**
* The `KSSPlugin` extracts [KSS doc comments](http://warpspire.com/kss/syntax/) from CSS code (or similar languages).
* The `KssPlugin` extracts [KSS doc comments](http://warpspire.com/kss/syntax/) from CSS code (or similar languages).
* It emits an object keyed by the "styleguide [ref]" section of the comment. The documentation, markup, and modifiers
* sections will all be emitted in the data.
*
Expand Down
10 changes: 5 additions & 5 deletions src/plugins/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,23 @@ export class MarkdownPlugin implements IPlugin<IMarkdownPluginData> {
return { nav, pages };
}

private blockToPage(filePath: string, block: IBlock): IPageData {
const reference = getReference(filePath, block);
private blockToPage(sourcePath: string, block: IBlock): IPageData {
const reference = getReference(sourcePath, block);
return {
reference,
route: reference,
sourcePath: path.relative(process.cwd(), filePath),
sourcePath,
title: getTitle(block),
...block,
};
}

/** Convert each file to IPageData and populate store. */
private buildPageStore(markdownFiles: IFile[], { renderBlock }: ICompiler) {
private buildPageStore(markdownFiles: IFile[], { relativePath, renderBlock }: ICompiler) {
const pageMap = new PageMap();
for (const file of markdownFiles) {
const block = renderBlock(file.read());
const page = this.blockToPage(file.path, block);
const page = this.blockToPage(relativePath(file.path), block);
pageMap.set(page.reference, page);
}
return pageMap;
Expand Down
88 changes: 88 additions & 0 deletions src/plugins/npm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Copyright 2018-present Palantir Technologies, Inc. All rights reserved.
* Licensed under the BSD-3 License as modified (the “License”); you may obtain
* a copy of the license in the LICENSE and PATENTS files in the root of this
* repository.
*/

import { spawn } from "child_process";
import { ICompiler, IFile, INpmPackage, INpmPluginData, IPlugin } from "../client";

export interface INpmPluginOptions {
/** Whether to exclude packages marked `private`. */
excludePrivate?: boolean;

/** Array of patterns to exclude packages by `name`. */
excludeNames?: Array<string | RegExp>;
}

/**
* The `NpmPlugin` parses `package.json` files and emits data about each NPM
* package. It uses `npm info` to look up data about published packages, and
* falls back to `package.json` info if the package is private or unpublished.
*/
export class NpmPlugin implements IPlugin<INpmPluginData> {
public constructor(private options: INpmPluginOptions = {}) {}

public async compile(packageJsons: IFile[], dm: ICompiler): Promise<INpmPluginData> {
const info = await Promise.all(packageJsons.map(pkg => this.parseNpmInfo(pkg, dm)));
const { excludeNames, excludePrivate } = this.options;
const npm = arrayToObject(
info.filter(pkg => isNotExcluded(excludeNames, pkg.name) && excludePrivate !== pkg.private),
pkg => pkg.name,
);
return { npm };
}

private parseNpmInfo = async (packageJson: IFile, dm: ICompiler): Promise<INpmPackage> => {
const sourcePath = dm.relativePath(packageJson.path);
const json = JSON.parse(packageJson.read());
const data = JSON.parse(await this.getNpmInfo(json.name));
if (data.error) {
return {
name: json.name,
published: false,
// tslint:disable-next-line:object-literal-sort-keys
description: json.description,
latestVersion: json.version,
private: json.private === true,
sourcePath,
versions: [json.version],
};
}

const distTags = data["dist-tags"] || {};
return {
name: data.name,
published: true,
// tslint:disable-next-line:object-literal-sort-keys
description: data.description,
latestVersion: distTags.latest,
nextVersion: distTags.next,
private: false,
sourcePath,
versions: data.versions,
};
};

private getNpmInfo(packageName: string) {
return new Promise<string>(resolve => {
let stdout = "";
const child = spawn("npm", ["info", "--json", packageName]);
child.stdout.setEncoding("utf8");
child.stdout.on("data", data => (stdout += data));
child.on("close", () => resolve(stdout));
});
}
}

function arrayToObject<T>(array: T[], keyFn: ((item: T) => string)) {
const obj: { [key: string]: T } = {};
array.forEach(item => (obj[keyFn(item)] = item));
return obj;
}

/** Returns true if value does not match all patterns. */
function isNotExcluded(patterns: Array<string | RegExp> = [], value?: string) {
return value === undefined || patterns.every(p => value.match(p) == null);
}