Make every step idempotent

This commit is contained in:
Ryan Cavanaugh
2016-03-31 18:50:19 -07:00
committed by Andrew Branch
parent 9798dd3b97
commit 15632c04c9
19 changed files with 4332 additions and 3768 deletions

View File

@@ -1,12 +1,27 @@
## Workflow
There are four steps in the typings publish process
### Parse
The *parse* step parses each folder in the source repo.
This generates a `types-data.json` file.
### Search
The *search* step generates search metadata from the `types-data.json` file.
### Generate
The *generate* step generates NPM packages on disk.
This step increments version numbers in `versions.json` if the content in the originating folder has changed.
### Publish
The *publish* step publishes all generated packages to NPM.
## Commandline arguments
### `--skipPublish`
Passing `--skipPublish` causes the `npm publish` step to be skipped.
Version numbers in `version.json` are subsequently not changed.
This is useful for debugging purposes.
### `--forceUpdate`
Passing `--forceUpdate` causes all version checks to be treated as "needs update".

View File

@@ -0,0 +1,29 @@
{
"name": "publish-typings",
"version": "0.6.0",
"description": "Publish DefinitelyTyped definitions to NPM",
"main": "index.js",
"bin": {
"publish-typings": "createSearchIndex.js"
},
"dependencies": {},
"devDependencies": {
"typescript": "^1.9.0-dev.20160331"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "node node_modules/typescript/lib/tsc.js",
"generate-search": "npm run build && npm run _generate-search",
"_generate-search": "node bin/createSearchIndex.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/RyanCavanaugh/publish-typings.git"
},
"author": "Microsoft",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/RyanCavanaugh/publish-typings/issues"
},
"homepage": "https://github.com/RyanCavanaugh/publish-typings#readme"
}

View File

@@ -0,0 +1,22 @@
function detectProjectAndLibraryNameDuplicates() {
check(info => info.libraryName, 'Library Name');
check(info => info.projectName, 'Project Name');
function check(func: (info: TypingsData) => string, key: string) {
const lookup: { [libName: string]: string[] } = {};
infos.forEach(info => {
const name = func(info);
if (name !== undefined) {
(lookup[name] || (lookup[name] = [])).push(info.typingsPackageName);
}
});
for (const k of Object.keys(lookup)) {
if (lookup[k].length > 1) {
warningLog.push(` * Duplicate ${key} descriptions "${k}"`);
lookup[k].forEach(n => warningLog.push(` * ${n}`));
}
}
}
}

View File

@@ -0,0 +1,55 @@
// https://api.npmjs.org/downloads/point/last-month/jquery,express,flarp,react
import * as fs from 'fs';
import * as request from 'request';
const rawData: SearchRecord[] = JSON.parse(fs.readFileSync('search-raw.json', 'utf-8'));
searchData.push({
packageName: info.data.projectName,
libraryName: info.data.libraryName,
globals: info.data.globals,
npmPackageName: info.data.typingsPackageName,
typePackageName: info.data.typingsPackageName,
declaredExternalModules: info.data.declaredModules
});
interface NpmResult {
[packageName: string]: {
downloads: number;
}
}
function getDownloadCounts(done: () => void) {
function next() {
const unchecked = rawData.filter(r => (r.npmPackageName !== undefined) && (r.downloads === undefined));
if (unchecked.length === 0) {
done();
} else {
// Unknown: How many can we query at once?
const nextToCheck = unchecked.slice(0, 200);
const url = 'https://api.npmjs.org/downloads/point/last-month/' + nextToCheck.map(r => r.npmPackageName).join(',');
request.get(url, (err: any, resp: any, data: string) => {
const json = JSON.parse(data);
if (err) throw err;
nextToCheck.forEach(r => {
const result = json[r.npmPackageName];
r.downloads = result ? result.downloads : 0;
});
next();
});
}
}
next();
}
function main() {
getDownloadCounts(() => {
rawData.sort((a, b) => a.downloads - b.downloads);
fs.writeFileSync('search-with-downloads.json', JSON.stringify(rawData, undefined, 4), 'utf-8');
});
}
main();

View File

@@ -1,179 +0,0 @@
import { TypingsData, DefinitionFileKind } from './definition-parser';
import * as fs from 'fs';
import * as crypto from 'crypto';
import * as path from 'path';
import * as child_process from 'child_process';
const settings: PublishSettings = JSON.parse(fs.readFileSync('./settings.json', 'utf-8'));
namespace Versions {
const versionFilename = 'versions.json';
interface VersionMap {
[typingsPackageName: string]: {
lastVersion: number;
lastContentHash: string;
};
}
export function performUpdate(key: string, content: string, update: (version: number) => boolean) {
let data: VersionMap = fs.existsSync(versionFilename) ? JSON.parse(fs.readFileSync(versionFilename, 'utf-8')) : {};
const forceUpdate = process.argv.some(arg => arg === '--forceUpdate');
const hashValue = computeHash(key);
let entry = data[key];
if (entry === undefined) {
data[key] = entry = { lastVersion: 0, lastContentHash: '' };
}
if (entry.lastContentHash !== hashValue || forceUpdate) {
const vNext = entry.lastVersion + (forceUpdate ? 2 : 1);
if(update(vNext)) {
data[key] = { lastVersion: vNext, lastContentHash: hashValue };
fs.writeFileSync(versionFilename, JSON.stringify(data, undefined, 4));
}
return true;
}
return false;
}
export function computeHash(content: string) {
const h = crypto.createHash('sha256');
h.update(content, 'utf-8');
return h.digest('base64');
}
}
function mkdir(p: string) {
try {
fs.statSync(p);
} catch(e) {
fs.mkdirSync(p);
}
}
function patchDefinitionFile(input: string): string {
const pathToLibrary = /\/\/\/ <reference path="..\/(\w.+)\/.+"/gm;
let output = input.replace(pathToLibrary, '/// <reference library="$1"');
return output;
}
export function publish(typing: TypingsData): { log: string[] } {
const log: string[] = [];
log.push(`Possibly publishing ${typing.libraryName}`);
let allContent = '';
// Make the file ordering deterministic so the hash doesn't jump around for no reason
typing.files.sort();
for(const file of typing.files) {
allContent = allContent + fs.readFileSync(path.join(typing.root, file), 'utf-8');
}
const actualPackageName = typing.packageName.toLowerCase();
const didUpdate = Versions.performUpdate(actualPackageName, allContent, version => {
log.push('Generate package.json and README.md; ensure output path exists');
const packageJson = JSON.stringify(createPackageJSON(typing, version), undefined, 4);
const readme = createReadme(typing);
const outputPath = path.join(settings.outputPath, actualPackageName);
mkdir(outputPath);
fs.writeFileSync(path.join(outputPath, 'package.json'), packageJson, 'utf-8');
fs.writeFileSync(path.join(outputPath, 'README.md'), readme, 'utf-8');
typing.files.forEach(file => {
log.push(`Copy and patch ${file}`);
let content = fs.readFileSync(path.join(typing.root, file), 'utf-8');
content = patchDefinitionFile(content);
fs.writeFileSync(path.join(outputPath, file), content);
});
const args: string[] = ['npm', 'publish', path.resolve(outputPath), '--access public'];
if (settings.tag) {
args.push(`--tag ${settings.tag}`);
}
const cmd = args.join(' ');
log.push(`Run ${cmd}`);
try {
const skipPublish = process.argv.some(arg => arg === '--skipPublish');
if (skipPublish) return false;
const result = <string>child_process.execSync(cmd, { encoding: 'utf-8' });
log.push(`Ran successfully`);
log.push(result);
return true;
} catch(e) {
log.push(`!!! Publish failed`);
log.push(JSON.stringify(e));
return false;
}
});
if (!didUpdate) {
log.push('Package was already up-to-date');
}
return { log };
}
function createPackageJSON(typing: TypingsData, fileVersion: number) {
const dependencies: any = {};
typing.moduleDependencies.forEach(d => dependencies[d] = '*');
typing.libraryDependencies.forEach(d => dependencies[`@${settings.scopeName}/${d}`] = '*');
let version = `${typing.libraryMajorVersion}.${typing.libraryMinorVersion}.${fileVersion}`;
if (settings.prereleaseTag) {
version = `${version}-${settings.prereleaseTag}`;
}
return ({
name: `@${settings.scopeName}/${typing.packageName.toLowerCase()}`,
version,
description: `Type definitions for ${typing.libraryName} from ${typing.sourceRepoURL}`,
main: '',
scripts: {},
author: typing.authors,
license: 'MIT',
typings: typing.definitionFilename,
dependencies
});
}
function createReadme(typing: TypingsData) {
const lines: string[] = [];
lines.push(`This package contains type definitions for ${typing.libraryName}.`)
if (typing.projectName) {
lines.push('');
lines.push(`The project URL or description is ${typing.projectName}`);
}
if (typing.authors) {
lines.push('');
lines.push(`These definitions were written by ${typing.authors}.`);
}
lines.push('');
lines.push(`Typings were exported from ${typing.sourceRepoURL} in the ${typing.packageName} directory.`);
lines.push('');
lines.push(`Additional Details`)
lines.push(` * Last updated: ${(new Date()).toUTCString()}`);
lines.push(` * Typings kind: ${typing.kind}`);
lines.push(` * Library Dependencies: ${typing.libraryDependencies.length ? typing.libraryDependencies.join(', ') : 'none'}`);
lines.push(` * Module Dependencies: ${typing.moduleDependencies.length ? typing.moduleDependencies.join(', ') : 'none'}`);
lines.push(` * Global values: ${typing.globals.length ? typing.globals.join(', ') : 'none'}`);
lines.push('');
return lines.join('\r\n');
}

View File

@@ -0,0 +1,17 @@
import * as common from './lib/common';
import * as generator from './lib/package-generator';
const typeData = <common.TypesDataFile>common.readDataFile(common.typesDataFilename);
if (typeData === undefined) {
console.log('Run parse-definitions first!');
} else {
const log: string[] = [];
Object.keys(typeData).forEach(packageName => {
const typing = typeData[packageName];
const result = generator.generatePackage(typing);
log.push(` * ${packageName}`);
result.log.forEach(line => log.push(` * ${line}`));
});
common.writeLogSync('package-generator.md', log);
}

View File

@@ -0,0 +1,149 @@
import path = require('path');
import fs = require('fs');
import crypto = require('crypto');
export const home = path.join(__dirname, '..', '..');
export const settings: PublishSettings = JSON.parse(fs.readFileSync(path.join(home, 'settings.json'), 'utf-8'));
export const typesDataFilename = 'types-data.json';
export const versionsFilename = 'versions.json';
export interface TypesDataFile {
[folderName: string]: TypingsData;
}
export interface TypingsData {
kind: string;
moduleDependencies: string[];
libraryDependencies: string[];
// e.g. https://github.com/DefinitelyTyped
sourceRepoURL: string;
// The name of the primary definition file, e.g. 'jquery.d.ts'
definitionFilename: string;
// The name of the library (human readable, e.g. might be 'Moment.js' even though packageName is 'moment')
libraryName: string;
// The NPM name to publish this under, e.g. 'jquery'. May not be lower-cased yet.
typingsPackageName: string;
// Parsed from 'Definitions by:'
authors: string;
// Optionally-present name or URL of the project, e.g. 'http://cordova.apache.org'
projectName: string;
// Names introduced into the global scope by this definition set
globals: string[];
// External modules declared by this package. Includes the containing folder name when applicable (e.g. proper module)
declaredModules: string[];
// The major version of the library (e.g. '1' for 1.0, '2' for 2.0)
libraryMajorVersion: string;
// The minor version of the library
libraryMinorVersion: string;
// The full path to the containing folder of all files, e.g. 'C:/github/DefinitelyTyped'
root: string;
// Files that should be published with this definition, e.g. ['jquery.d.ts', 'jquery-extras.d.ts']
files: string[];
// A hash computed from all files from this definition
contentHash: string;
}
export enum DefinitionFileKind {
// Dunno
Unknown,
// UMD module file
UMD,
// File has global variables or interfaces, but not any external modules
Global,
// File has top-level export declarations
ProperModule,
// File has a single declare module "foo" but no global interfaces or variables
DeclareModule,
// Some combination of Global and DeclareModule
Mixed,
// More than one 'declare module "foo"''
MultipleModules,
// Augments an external module
ModuleAugmentation,
// Old-style UMD
OldUMD
}
export enum RejectionReason {
TooManyFiles,
BadFileFormat,
ReferencePaths
}
export interface TypingParseFailResult {
rejectionReason: RejectionReason;
log: string[];
warnings: string[];
}
export interface TypingParseSucceedResult {
data: TypingsData;
log: string[];
warnings: string[];
}
export function isSuccess(t: TypingParseSucceedResult | TypingParseFailResult): t is TypingParseSucceedResult {
return (t as TypingParseSucceedResult).data !== undefined;
}
export function isFail(t: TypingParseSucceedResult | TypingParseFailResult): t is TypingParseFailResult {
return (t as TypingParseFailResult).rejectionReason !== undefined;
}
export function mkdir(p: string) {
try {
fs.statSync(p);
} catch (e) {
fs.mkdirSync(p);
}
}
export function writeLogSync(logName: string, contents: string[]) {
const logDir = path.join(home, 'logs');
mkdir(logDir);
fs.writeFileSync(path.join(logDir, logName), contents.join('\r\n'), 'utf-8');
}
export function writeDataFile(filename: string, content: {}) {
const dataDir = path.join(home, 'data');
mkdir(dataDir);
if (typeof content !== 'string') {
content = JSON.stringify(content, undefined, 4);
}
fs.writeFileSync(path.join(dataDir, filename), content, 'utf-8');
}
export function readDataFile(filename: string): {} {
const dataDir = path.join(home, 'data');
const fullPath = path.join(dataDir, filename);
if (fs.existsSync(fullPath)) {
return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
} else {
return undefined;
}
}
export function computeHash(content: string) {
const h = crypto.createHash('sha256');
h.update(content, 'utf-8');
return <string>h.digest('hex');
}
export function getOutputPath(typing: TypingsData) {
const outputPath = path.join(settings.outputPath, typing.typingsPackageName);
return outputPath;
}

View File

@@ -2,88 +2,10 @@ import * as ts from 'typescript';
import * as fs from 'fs';
import * as path from 'path';
export enum DefinitionFileKind {
// Dunno
Unknown,
// UMD module file
UMD,
// File has global variables or interfaces, but not any external modules
Global,
// File has top-level export declarations
ProperModule,
// File has a single declare module "foo" but no global interfaces or variables
DeclareModule,
// Some combination of Global and DeclareModule
Mixed,
// More than one 'declare module "foo"''
MultipleModules,
// Augments an external module
ModuleAugmentation,
// Old-style UMD
OldUMD
}
import { TypingsData, DefinitionFileKind, RejectionReason, TypingParseSucceedResult, TypingParseFailResult, computeHash } from './common';
export enum RejectionReason {
TooManyFiles,
BadFileFormat,
ReferencePaths
}
export interface TypingParseFailResult {
rejectionReason: RejectionReason;
log: string[];
}
export interface TypingParseSucceedResult {
data: TypingsData;
log: string[];
}
export function isSuccess(t: TypingParseSucceedResult | TypingParseFailResult): t is TypingParseSucceedResult {
return (t as TypingParseSucceedResult).data !== undefined;
}
export function isFail(t: TypingParseSucceedResult | TypingParseFailResult): t is TypingParseFailResult {
return (t as TypingParseFailResult).rejectionReason !== undefined;
}
export interface TypingsData {
kind: string;
moduleDependencies: string[];
libraryDependencies: string[];
// e.g. https://github.com/DefinitelyTyped
sourceRepoURL: string;
// The name of the primary definition file, e.g. 'jquery.d.ts'
definitionFilename: string;
// The name of the library (human readable, e.g. might be 'Moment.js' even though packageName is 'moment')
libraryName: string;
// The NPM name to publish this under, e.g. 'jquery'. May not be lower-cased yet.
packageName: string;
// Parsed from 'Definitions by:'
authors: string;
// Optionally-present name or URL of the project, e.g. 'http://cordova.apache.org'
projectName: string;
// Names introduced into the global scope by this definition set
globals: string[];
// The major version of the library (e.g. '1' for 1.0, '2' for 2.0)
libraryMajorVersion: string;
// The minor version of the library
libraryMinorVersion: string;
// The full path to the containing folder of all files, e.g. 'C:/github/DefinitelyTyped'
root: string;
// Files that should be published with this definition, e.g. ['jquery.d.ts', 'jquery-extras.d.ts']
files: string[];
function stripQuotes(s: string) {
return s.substr(1, s.length - 2);
}
const augmentedGlobals = ['Array', ' Function', 'String', 'Number', 'Window', 'Date', 'StringConstructor', 'NumberConstructor', 'Math', 'HTMLElement'];
@@ -107,10 +29,6 @@ function isSupportedFileKind(kind: DefinitionFileKind) {
}
}
function stripQuotes(s: string) {
return s.substr(1, s.length - 2);
}
enum DeclarationFlags {
None = 0,
Value = 1 << 0,
@@ -153,6 +71,8 @@ function getNamespaceFlags(ns: ts.ModuleDeclaration): DeclarationFlags {
export function getTypingInfo(directory: string): TypingParseFailResult | TypingParseSucceedResult {
const log: string[] = [];
const warnings: string[] = [];
const folderName = path.basename(directory);
log.push(`Reading contents of ${directory}`);
const files = fs.readdirSync(directory);
@@ -162,12 +82,10 @@ export function getTypingInfo(directory: string): TypingParseFailResult | Typing
// * -tests.ts (tests)
// * .d.ts.tscparams (for testing)
// "// Type definitions for JSFL v3.2"
log.push(`Found ${files.length} files`);
const declFiles = files.filter(f => /\.d\.ts$/.test(f));
const candidates = [path.basename(directory) + ".d.ts", "index.d.ts"];
const candidates = [folderName + ".d.ts", "index.d.ts"];
log.push(`Found ${declFiles.length} .d.ts files (${declFiles.join(', ')})`);
let entryPointFilename: string;
@@ -184,10 +102,12 @@ export function getTypingInfo(directory: string): TypingParseFailResult | Typing
}
}
}
declFiles.sort();
if (entryPointFilename === undefined) {
log.push('Exiting, found either zero or more than one .d.ts file and none of ' + candidates.join(' or '));
return { log, rejectionReason: RejectionReason.TooManyFiles };
warnings.push('Found either zero or more than one .d.ts file and none of ' + candidates.join(' or '));
return { log, warnings, rejectionReason: RejectionReason.TooManyFiles };
}
const entryPointContent = readFile(entryPointFilename);
@@ -198,6 +118,7 @@ export function getTypingInfo(directory: string): TypingParseFailResult | Typing
const moduleDependencies: string[] = [];
const referencedLibraries: string[] = [];
const declaredModules: string[] = [];
let globalSymbols: { [name: string]: ts.SymbolFlags } = {};
function recordSymbol(name: string, flags: DeclarationFlags) {
@@ -257,7 +178,9 @@ export function getTypingInfo(directory: string): TypingParseFailResult | Typing
} else {
const nameKind = (node as ts.ModuleDeclaration).name.kind;
if (nameKind === ts.SyntaxKind.StringLiteral) {
log.push(`Found ambient external module ${(node as ts.ModuleDeclaration).name.getText()}`);
const name = stripQuotes((node as ts.ModuleDeclaration).name.getText());
declaredModules.push(name);
log.push(`Found ambient external module ${name}`);
ambientModuleCount++;
} else {
const moduleName = (node as ts.ModuleDeclaration).name.getText();
@@ -374,9 +297,18 @@ export function getTypingInfo(directory: string): TypingParseFailResult | Typing
}
}
if (declaredModules.length === 1 && fileKind !== DefinitionFileKind.ModuleAugmentation && declaredModules[0].toLowerCase() !== folderName.toLowerCase()) {
warnings.push(`Declared module \`${declaredModules[0]}\` is in folder with incorrect name \`${folderName}\``);
}
if (declaredModules.length === 0 && fileKind === DefinitionFileKind.ProperModule) {
declaredModules.push(folderName);
}
if (!isSupportedFileKind(fileKind)) {
log.push(`Exiting, ${DefinitionFileKind[fileKind]} is not a supported file kind`);
return { log, rejectionReason: RejectionReason.BadFileFormat };
warnings.push(`${DefinitionFileKind[fileKind]} is not a supported file kind`);
return { log, warnings, rejectionReason: RejectionReason.BadFileFormat };
}
function regexMatch(rx: RegExp, defaultValue: string): string {
@@ -393,11 +325,14 @@ export function getTypingInfo(directory: string): TypingParseFailResult | Typing
const sourceRepoURL = 'https://www.github.com/DefinitelyTyped/DefinitelyTyped';
if (packageName !== packageName.toLowerCase()) {
log.push(`!!! WARNING: ${packageName} !== ${packageName.toLowerCase()}`);
warnings.push(`Package name ${packageName} should be strictly lowercase`);
}
const allContent = declFiles.map(d => d + '**' + readFile(d)).join('||');
return {
log,
warnings,
data: {
authors,
definitionFilename: entryPointFilename,
@@ -406,13 +341,15 @@ export function getTypingInfo(directory: string): TypingParseFailResult | Typing
libraryMajorVersion,
libraryMinorVersion,
libraryName,
packageName,
typingsPackageName: folderName.toLowerCase(),
projectName,
sourceRepoURL,
kind: DefinitionFileKind[fileKind],
globals: Object.keys(globalSymbols).filter(k => !!(globalSymbols[k] & DeclarationFlags.Value)),
declaredModules,
root: path.resolve(directory),
files: declFiles
files: declFiles,
contentHash: computeHash(allContent)
}
};

View File

@@ -0,0 +1,153 @@
import { TypingsData, DefinitionFileKind, mkdir, settings, getOutputPath } from './common';
import * as fs from 'fs';
import * as crypto from 'crypto';
import * as path from 'path';
import * as child_process from 'child_process';
import * as request from 'request';
export function generatePackage(typing: TypingsData): { log: string[] } {
const log: string[] = [];
const fileVersion = Versions.computeVersion(typing);
const outputPath = getOutputPath(typing);
log.push(`Create output path ${outputPath}`);
mkdir(outputPath);
log.push(`Clear out old files`);
fs.readdirSync(outputPath).forEach(file => {
fs.unlinkSync(path.join(outputPath, file));
});
log.push('Generate package.json, metadata.json, and README.md');
const packageJson = createPackageJSON(typing, fileVersion);
const metadataJson = createMetadataJSON(typing);
const readme = createReadme(typing);
log.push('Write metadata files to disk');
writeOutputFile('package.json', packageJson);
writeOutputFile('types-metadata.json', metadataJson);
writeOutputFile('README.md', readme);
typing.files.forEach(file => {
log.push(`Copy and patch ${file}`);
let content = fs.readFileSync(path.join(typing.root, file), 'utf-8');
content = patchDefinitionFile(content);
writeOutputFile(file, content);
});
Versions.recordVersionUpdate(typing);
return { log };
function writeOutputFile(filename: string, content: string) {
fs.writeFileSync(path.join(outputPath, filename), content, 'utf-8');
}
}
function patchDefinitionFile(input: string): string {
const pathToLibrary = /\/\/\/ <reference path="..\/(\w.+)\/.+"/gm;
let output = input.replace(pathToLibrary, '/// <reference library="$1"');
return output;
}
function createMetadataJSON(typing: TypingsData): string {
const clone: typeof typing = JSON.parse(JSON.stringify(typing));
delete clone.root;
return JSON.stringify(clone, undefined, 4);
}
function createPackageJSON(typing: TypingsData, fileVersion: number): string {
const dependencies: any = {};
typing.moduleDependencies.forEach(d => dependencies[d] = '*');
typing.libraryDependencies.forEach(d => dependencies[`@${settings.scopeName}/${d}`] = '*');
let version = `${typing.libraryMajorVersion}.${typing.libraryMinorVersion}.${fileVersion}`;
if (settings.prereleaseTag) {
version = `${version}-${settings.prereleaseTag}`;
}
return JSON.stringify({
name: `@${settings.scopeName}/${typing.typingsPackageName.toLowerCase()}`,
version,
description: `Type definitions for ${typing.libraryName} from ${typing.sourceRepoURL}`,
main: '',
scripts: {},
author: typing.authors,
license: 'MIT',
typings: typing.definitionFilename,
dependencies
}, undefined, 4);
}
function createReadme(typing: TypingsData) {
const lines: string[] = [];
lines.push(`This package contains type definitions for ${typing.libraryName}.`)
if (typing.projectName) {
lines.push('');
lines.push(`The project URL or description is ${typing.projectName}`);
}
if (typing.authors) {
lines.push('');
lines.push(`These definitions were written by ${typing.authors}.`);
}
lines.push('');
lines.push(`Typings were exported from ${typing.sourceRepoURL} in the ${typing.typingsPackageName} directory.`);
lines.push('');
lines.push(`Additional Details`)
lines.push(` * Last updated: ${(new Date()).toUTCString()}`);
lines.push(` * Typings kind: ${typing.kind}`);
lines.push(` * Library Dependencies: ${typing.libraryDependencies.length ? typing.libraryDependencies.join(', ') : 'none'}`);
lines.push(` * Module Dependencies: ${typing.moduleDependencies.length ? typing.moduleDependencies.join(', ') : 'none'}`);
lines.push(` * Global values: ${typing.globals.length ? typing.globals.join(', ') : 'none'}`);
lines.push('');
return lines.join('\r\n');
}
namespace Versions {
const versionFilename = 'versions.json';
interface VersionMap {
[typingsPackageName: string]: {
lastVersion: number;
lastContentHash: string;
};
}
let _versionData: VersionMap = undefined;
function loadVersions() {
if(_versionData === undefined) {
_versionData = fs.existsSync(versionFilename) ? JSON.parse(fs.readFileSync(versionFilename, 'utf-8')) : {};
}
return _versionData;
}
function saveVersions(data: VersionMap) {
fs.writeFileSync(versionFilename, JSON.stringify(data, undefined, 4));
}
export function recordVersionUpdate(typing: TypingsData) {
const key = typing.typingsPackageName;
const data = loadVersions();
data[key] = { lastVersion: computeVersion(typing), lastContentHash: typing.contentHash };
saveVersions(data);
}
function getLastVersion(typing: TypingsData) {
const key = typing.typingsPackageName;
const data = loadVersions();
const entry = data[key];
return entry || { lastVersion: 0, lastContentHash: '' };
}
export function computeVersion(typing: TypingsData): number {
const forceUpdate = process.argv.some(arg => arg === '--forceUpdate');
const lastVersion = getLastVersion(typing);
const increment = (forceUpdate || (lastVersion.lastContentHash !== typing.contentHash)) ? 1 : 0;
return lastVersion.lastVersion + increment;
}
}

View File

@@ -0,0 +1,74 @@
import { TypingsData, DefinitionFileKind, mkdir, getOutputPath, settings } from './common';
import * as fs from 'fs';
import * as path from 'path';
import * as child_process from 'child_process';
import * as request from 'request';
interface NpmRegistryResult {
versions: {
[key: string]: {};
}
error: string;
}
export function publishPackage(typing: TypingsData, done: (log: string[], errors: string[]) => void) {
const log: string[] = [];
const errors: string[] = [];
const outputPath = getOutputPath(typing);
log.push(`Possibly publishing ${typing.libraryName}`);
// Read package.json for version number we would be publishing
const localVersion: string = JSON.parse(fs.readFileSync(path.join(outputPath, 'package.json'), 'utf-8')).version;
log.push(`Local version from package.json is ${localVersion}`);
// Hit e.g. http://registry.npmjs.org/@ryancavanaugh%2fjquery for version data
const registryUrl = `http://registry.npmjs.org/@${settings.scopeName}%2F${typing.typingsPackageName}`;
log.push(`Fetch registry data from ${registryUrl}`);
// See if this version already exists
request.get(registryUrl, (err: any, resp: any, bodyString: string) => {
const body: NpmRegistryResult = JSON.parse(bodyString);
if (body.error === "Not found") {
// OK, just haven't published this one before
log.push('Registry indicates this is a new package');
} else if (body.error) {
// Critical failure
log.push('Unexpected response, refer to error log');
errors.push(`NPM registry failure for ${registryUrl}: Unexpected error content ${body.error})`);
done(log, errors);
return;
} else {
const remoteVersionExists = body.versions[localVersion] !== undefined;
if (remoteVersionExists) {
log.push(`Remote version already exists`);
done(log, errors);
return;
} else {
log.push(`Remote version does not exist`);
}
}
// Made it to here, so proceed with update
const args: string[] = ['npm', 'publish', path.resolve(outputPath), '--access public'];
if (settings.tag) {
args.push(`--tag ${settings.tag}`);
}
const cmd = args.join(' ');
log.push(`Run ${cmd}`);
try {
const result = <string>child_process.execSync(cmd, { encoding: 'utf-8' });
log.push(`Ran successfully`);
log.push(result);
} catch(e) {
errors.push(`Publish failed: ${JSON.stringify(e)}`);
log.push('Publish failed, refer to error log');
}
done(log, errors);
});
}

View File

@@ -0,0 +1,99 @@
import * as parser from './lib/definition-parser';
import { TypingsData, DefinitionFileKind, RejectionReason, TypingParseSucceedResult, TypingParseFailResult,
settings, isSuccess, isFail, writeLogSync, writeDataFile, typesDataFilename } from './lib/common';
import fs = require('fs');
import path = require('path');
function processDir(folderPath: string, name: string): { data: TypingsData, log: string[], warnings: string[], outcome: string } {
let data: TypingsData;
let outcome: string;
const info = parser.getTypingInfo(folderPath);
const log = info.log;
if (isSuccess(info)) {
data = info.data;
outcome = `Succeeded (${info.data.kind})`;
} else if (isFail(info)) {
data = undefined;
outcome = `Failed (${RejectionReason[info.rejectionReason]})`;
}
return { data, log, warnings: info.warnings, outcome: outcome };
}
function filterPaths(paths: string[]): { name: string; path: string; }[] {
return paths
// Remove hidden paths
.filter(s => s.substr(0, 1) !== '_' && s.substr(0, 1) !== '.')
// Sort by name
.sort()
// Combine paths
.map(s => ({ name: s, path: path.join(settings.definitelyTypedPath, s) }))
// Remove non-folders
.filter(s => fs.statSync(s.path).isDirectory());
}
function main() {
const summaryLog: string[] = [];
const detailedLog: string[] = [];
summaryLog.push('# Typing Publish Report Summary');
summaryLog.push(`Started at ${(new Date()).toUTCString()}`);
// TypesData
fs.readdir(settings.definitelyTypedPath, (err, paths) => {
const folders = filterPaths(paths);
summaryLog.push(`Found ${folders.length} typings folders in ${settings.definitelyTypedPath}`);
const outcomes: { [name: string]: number} = {};
const warningLog: string[] = [];
const typings: { [name: string]: TypingsData } = {};
folders.forEach(s => {
const result = processDir(s.path, s.name);
// Record outcome
outcomes[result.outcome] = (outcomes[result.outcome] || 0) + 1;
detailedLog.push(`# ${s.name}`);
// Push warnings
if (result.warnings.length > 0) {
warningLog.push(` * ${s.name}`);
result.warnings.forEach(w => {
warningLog.push(` * ${w}`);
detailedLog.push(`**Warning**: ${w}`);
});
}
if (result.data !== undefined) {
typings[s.name] = result.data;
}
// Flush detailed log
result.log.forEach(e => detailedLog.push(e));
});
summaryLog.push('\r\n### Overall Results\r\n');
summaryLog.push(' * Pass / fail');
const outcomeKeys = Object.keys(outcomes);
outcomeKeys.sort();
outcomeKeys.forEach(k => {
summaryLog.push(` * ${k}: ${outcomes[k]}`);
});
summaryLog.push('\r\n### Warnings\r\n');
warningLog.forEach(w => summaryLog.push(w));
writeLogSync('parser-log-summary.md', summaryLog);
writeLogSync('parser-log-details.md', detailedLog);
writeDataFile(typesDataFilename, typings);
});
}
main();

View File

@@ -0,0 +1,40 @@
import * as common from './lib/common';
import * as publisher from './lib/package-publisher';
const typeData = <common.TypesDataFile>common.readDataFile(common.typesDataFilename);
if (typeData === undefined) {
console.log('Run parse-definitions and generate-packages first!');
} else {
main();
}
function main() {
const log: string[] = [];
const publishQueue = Object.keys(typeData);
next();
function next() {
common.writeLogSync('publishing.md', log);
if (publishQueue.length === 0) {
console.log('Done!');
return;
}
const packageName = publishQueue.shift();
console.log(`Publishing ${packageName}...`);
const typing = typeData[packageName];
publisher.publishPackage(typing, (publishLog, errors) => {
log.push(` * ${packageName}`);
publishLog.forEach(line => log.push(` * ${line}`));
errors.forEach(err => {
log.push(` * ERROR: ${err}`);
console.log(` Error! ${err}`);
});
next();
});
}
}

View File

@@ -1,112 +0,0 @@
import * as parser from './definition-parser';
import * as publisher from './definition-publisher';
import fs = require('fs');
import path = require('path');
const settings: PublishSettings = JSON.parse(fs.readFileSync('./settings.json', 'utf-8'));
const versionFile = 'versions.json';
const summaryLog: string[] = [];
const detailedLog: string[] = [];
const outcomes: { [s: string]: number } = {};
const kinds: { [s: string]: number } = {};
function recordKind(s: string) {
kinds[s] = (kinds[s] || 0) + 1;
}
function recordOutcome(s: string) {
outcomes[s] = (outcomes[s] || 0) + 1;
}
function processDir(folderPath: string, name: string) {
detailedLog.push(`## ${name}`);
const info = parser.getTypingInfo(folderPath);
if (parser.isSuccess(info)) {
detailedLog.push('### File Parse Succeeded');
detailedLog.push(`Detected a ${info.data.kind} typing definition.`);
detailedLog.push('```js');
detailedLog.push(JSON.stringify(info.data, undefined, 4));
detailedLog.push('```');
recordOutcome(`Succeeded (${info.data.kind})`);
recordKind(info.data.kind);
detailedLog.push('### Publish');
const publishLog = publisher.publish(info.data);
for(const line of publishLog.log) {
detailedLog.push(` > ${line}\r\n\r\n`);
}
} else if(parser.isFail(info)) {
detailedLog.push('### File Parse Failed');
switch (info.rejectionReason) {
case parser.RejectionReason.BadFileFormat:
recordOutcome('Failed: Bad file format');
detailedLog.push('Bad file format');
break;
case parser.RejectionReason.ReferencePaths:
recordOutcome('Failed: Reference paths not allowed');
detailedLog.push('Reference paths are not allowed (use library references instead)');
break;
case parser.RejectionReason.TooManyFiles:
recordOutcome('Failed: Too many files');
detailedLog.push('Failed: Only one .d.ts file per folder is currently supported');
break;
default:
recordOutcome('??');
}
}
detailedLog.push('### Parser Log');
for(const line of info.log) detailedLog.push('> ' + line + '\r\n');
detailedLog.push('');
}
function main() {
summaryLog.push('# Typing Publish Report Summary');
summaryLog.push(`Started at ${(new Date()).toUTCString()}`);
try {
fs.mkdirSync(settings.outputPath);
} catch(e) { }
fs.readdir(settings.definitelyTypedPath, (err, paths) => {
const folders = paths
// Remove hidden paths
.filter(s => s.substr(0, 1) !== '_' && s.substr(0, 1) !== '.')
// Combine paths
.map(s => ({ name: s, path: path.join(settings.definitelyTypedPath, s) }))
// Remove non-folders
.filter(s => fs.statSync(s.path).isDirectory());
folders.sort();
console.log(`Found ${folders.length} typings folders.`);
folders.forEach(s => processDir(s.path, s.name));
summaryLog.push('\r\n### Overall Results\r\n');
summaryLog.push(' * Pass / fail');
const outcomeKeys = Object.keys(outcomes);
outcomeKeys.sort();
outcomeKeys.forEach(k => {
summaryLog.push(` * ${k}: ${outcomes[k]}`);
});
summaryLog.push(' * Typing Kind');
const typingKeys = Object.keys(kinds);
typingKeys.sort();
typingKeys.forEach(k => {
summaryLog.push(` * ${k}: ${kinds[k]}`);
});
const logmd = summaryLog.join('\r\n') + '\r\n\r\n# Detailed Report\r\n\r\n' + detailedLog.join('\r\n');
fs.writeFile('log.md', logmd, 'utf-8');
});
}
main();

View File

@@ -0,0 +1,16 @@
interface SearchRecord {
// types package name
typePackageName: string;
// npm package name
npmPackageName: string;
// globals
globals: string[];
// modules
declaredExternalModules: string[];
// project name
packageName: string;
// library name
libraryName: string;
// downloads in the last month from NPM
downloads?: number;
}

View File

@@ -1,10 +1,17 @@
{
"files": [
"src/run.ts",
"src/definition-publisher.ts",
"src/definition-parser.ts",
"src/settings.d.ts",
"typings/tsd.d.ts"
"src/searchRecord.d.ts",
"typings/tsd.d.ts",
"src/lib/definition-parser.ts",
"src/lib/package-generator.ts",
"src/lib/package-publisher.ts",
"src/lib/common.ts",
"src/parse-definitions.ts",
"src/generate-packages.ts",
"src/publish-packages.ts"
],
"compilerOptions": {
"module": "commonjs",

View File

@@ -0,0 +1,16 @@
// Type definitions for form-data
// Project: https://github.com/felixge/node-form-data
// Definitions by: Carlos Ballesteros Velasco <https://github.com/soywiz>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// Imported from: https://github.com/soywiz/typescript-node-definitions/form-data.d.ts
declare module "form-data" {
export class FormData {
append(key: string, value: any, options?: any): FormData;
getHeaders(): Object;
// TODO expand pipe
pipe(to: any): any;
submit(params: string|Object, callback: (error: any, response: any) => void): any;
}
}

View File

@@ -0,0 +1,262 @@
// Type definitions for request
// Project: https://github.com/mikeal/request
// Definitions by: Carlos Ballesteros Velasco <https://github.com/soywiz>, bonnici <https://github.com/bonnici>, Bart van der Schoor <https://github.com/Bartvds>, Joe Skeen <http://github.com/joeskeen>, Christopher Currens <https://github.com/ccurrens>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// Imported from: https://github.com/soywiz/typescript-node-definitions/d.ts
/// <reference path="../node/node.d.ts" />
/// <reference path="../form-data/form-data.d.ts" />
declare module 'request' {
import stream = require('stream');
import http = require('http');
import https = require('https');
import FormData = require('form-data');
import url = require('url');
import fs = require('fs');
namespace request {
export interface RequestAPI<TRequest extends Request,
TOptions extends CoreOptions,
TUriUrlOptions> {
defaults(options: TOptions): RequestAPI<TRequest, TOptions, RequiredUriUrl>;
defaults(options: RequiredUriUrl & TOptions): DefaultUriUrlRequestApi<TRequest, TOptions, OptionalUriUrl>;
(uri: string, options?: TOptions, callback?: RequestCallback): TRequest;
(uri: string, callback?: RequestCallback): TRequest;
(options: TUriUrlOptions & TOptions, callback?: RequestCallback): TRequest;
get(uri: string, options?: TOptions, callback?: RequestCallback): TRequest;
get(uri: string, callback?: RequestCallback): TRequest;
get(options: TUriUrlOptions & TOptions, callback?: RequestCallback): TRequest;
post(uri: string, options?: TOptions, callback?: RequestCallback): TRequest;
post(uri: string, callback?: RequestCallback): TRequest;
post(options: TUriUrlOptions & TOptions, callback?: RequestCallback): TRequest;
put(uri: string, options?: TOptions, callback?: RequestCallback): TRequest;
put(uri: string, callback?: RequestCallback): TRequest;
put(options: TUriUrlOptions & TOptions, callback?: RequestCallback): TRequest;
head(uri: string, options?: TOptions, callback?: RequestCallback): TRequest;
head(uri: string, callback?: RequestCallback): TRequest;
head(options: TUriUrlOptions & TOptions, callback?: RequestCallback): TRequest;
patch(uri: string, options?: TOptions, callback?: RequestCallback): TRequest;
patch(uri: string, callback?: RequestCallback): TRequest;
patch(options: TUriUrlOptions & TOptions, callback?: RequestCallback): TRequest;
del(uri: string, options?: TOptions, callback?: RequestCallback): TRequest;
del(uri: string, callback?: RequestCallback): TRequest;
del(options: TUriUrlOptions & TOptions, callback?: RequestCallback): TRequest;
forever(agentOptions: any, optionsArg: any): TRequest;
jar(): CookieJar;
cookie(str: string): Cookie;
initParams: any;
debug: boolean;
}
interface DefaultUriUrlRequestApi<TRequest extends Request,
TOptions extends CoreOptions,
TUriUrlOptions> extends RequestAPI<TRequest, TOptions, TUriUrlOptions> {
defaults(options: TOptions): DefaultUriUrlRequestApi<TRequest, TOptions, OptionalUriUrl>;
(): TRequest;
get(): TRequest;
post(): TRequest;
put(): TRequest;
head(): TRequest;
patch(): TRequest;
del(): TRequest;
}
interface CoreOptions {
baseUrl?: string;
callback?: (error: any, response: http.IncomingMessage, body: any) => void;
jar?: any; // CookieJar
formData?: any; // Object
form?: any; // Object or string
auth?: AuthOptions;
oauth?: OAuthOptions;
aws?: AWSOptions;
hawk?: HawkOptions;
qs?: any;
json?: any;
multipart?: RequestPart[] | Multipart;
agent?: http.Agent | https.Agent;
agentOptions?: any;
agentClass?: any;
forever?: any;
host?: string;
port?: number;
method?: string;
headers?: Headers;
body?: any;
followRedirect?: boolean | ((response: http.IncomingMessage) => boolean);
followAllRedirects?: boolean;
maxRedirects?: number;
encoding?: string;
pool?: any;
timeout?: number;
proxy?: any;
strictSSL?: boolean;
gzip?: boolean;
preambleCRLF?: boolean;
postambleCRLF?: boolean;
key?: Buffer;
cert?: Buffer;
passphrase?: string;
ca?: Buffer;
har?: HttpArchiveRequest;
useQuerystring?: boolean;
}
interface UriOptions {
uri: string;
}
interface UrlOptions {
url: string;
}
export type RequiredUriUrl = UriOptions | UrlOptions;
interface OptionalUriUrl {
uri?: string;
url?: string;
}
export type OptionsWithUri = UriOptions & CoreOptions;
export type OptionsWithUrl = UrlOptions & CoreOptions;
export type Options = OptionsWithUri | OptionsWithUrl;
export interface RequestCallback {
(error: any, response: http.IncomingMessage, body: any): void;
}
export interface HttpArchiveRequest {
url?: string;
method?: string;
headers?: NameValuePair[];
postData?: {
mimeType?: string;
params?: NameValuePair[];
}
}
export interface NameValuePair {
name: string;
value: string;
}
export interface Multipart {
chunked?: boolean;
data?: {
'content-type'?: string,
body: string
}[];
}
export interface RequestPart {
headers?: Headers;
body: any;
}
export interface Request extends stream.Stream {
readable: boolean;
writable: boolean;
getAgent(): http.Agent;
//start(): void;
//abort(): void;
pipeDest(dest: any): void;
setHeader(name: string, value: string, clobber?: boolean): Request;
setHeaders(headers: Headers): Request;
qs(q: Object, clobber?: boolean): Request;
form(): FormData.FormData;
form(form: any): Request;
multipart(multipart: RequestPart[]): Request;
json(val: any): Request;
aws(opts: AWSOptions, now?: boolean): Request;
auth(username: string, password: string, sendInmediately?: boolean, bearer?: string): Request;
oauth(oauth: OAuthOptions): Request;
jar(jar: CookieJar): Request;
on(event: string, listener: Function): this;
on(event: 'request', listener: (req: http.ClientRequest) => void): this;
on(event: 'response', listener: (resp: http.IncomingMessage) => void): this;
on(event: 'data', listener: (data: Buffer | string) => void): this;
on(event: 'error', listener: (e: Error) => void): this;
on(event: 'complete', listener: (resp: http.IncomingMessage, body?: string | Buffer) => void): this;
write(buffer: Buffer, cb?: Function): boolean;
write(str: string, cb?: Function): boolean;
write(str: string, encoding: string, cb?: Function): boolean;
write(str: string, encoding?: string, fd?: string): boolean;
end(): void;
end(chunk: Buffer, cb?: Function): void;
end(chunk: string, cb?: Function): void;
end(chunk: string, encoding: string, cb?: Function): void;
pause(): void;
resume(): void;
abort(): void;
destroy(): void;
toJSON(): Object;
}
export interface Headers {
[key: string]: any;
}
export interface AuthOptions {
user?: string;
username?: string;
pass?: string;
password?: string;
sendImmediately?: boolean;
bearer?: string;
}
export interface OAuthOptions {
callback?: string;
consumer_key?: string;
consumer_secret?: string;
token?: string;
token_secret?: string;
verifier?: string;
}
export interface HawkOptions {
credentials: any;
}
export interface AWSOptions {
secret: string;
bucket?: string;
}
export interface CookieJar {
setCookie(cookie: Cookie, uri: string | url.Url, options?: any): void
getCookieString(uri: string | url.Url): string
getCookies(uri: string | url.Url): Cookie[]
}
export interface CookieValue {
name: string;
value: any;
httpOnly: boolean;
}
export interface Cookie extends Array<CookieValue> {
constructor(name: string, req: Request): void;
str: string;
expires: Date;
path: string;
toString(): string;
}
}
var request: request.RequestAPI<request.Request, request.CoreOptions, request.RequiredUriUrl>;
export = request;
}

View File

@@ -1,3 +1,4 @@
/// <reference path="node/node.d.ts" />
/// <reference path="typescript/typescript.d.ts" />
/// <reference path="request/request.d.ts" />

File diff suppressed because it is too large Load Diff