Add feature to copy to publish directory (#53)

* Fix unix run header

No space is allowed between the # and the !

* Fix output option

* Remove debug log

* Fix function JSON fixup

* remove package-lock

* Fix lint warnings

* Roll back unrequired change to function JSON

* only change path sep if win

* Add copy to output feature

* Add e2e tests

* rename old tests to perf
This commit is contained in:
Christopher Anderson 2017-10-16 18:59:56 -07:00 коммит произвёл GitHub
Родитель a27e9ce458
Коммит 987079737d
44 изменённых файлов: 3740 добавлений и 864 удалений

21
.vscode/launch.json поставляемый
Просмотреть файл

@ -94,6 +94,27 @@
"stopOnEntry": false,
"type": "node"
},
{
"args": [
"pack","-c","./sample2"
],
"cwd": "${workspaceRoot}",
"env": {
},
"name": "Pack (Copy to Output)",
"outFiles": [
"${workspaceRoot}/lib/**"
],
"program": "${workspaceRoot}/src/main.ts",
"request": "launch",
"runtimeArgs": [
"--nolazy"
],
"runtimeExecutable": null,
"sourceMaps": true,
"stopOnEntry": false,
"type": "node"
},
{
"args": [
"pack","./sample2","-u"

2579
package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -22,9 +22,9 @@
"typings": "lib/index.d.ts",
"scripts": {
"clean": "rimraf lib",
"lint": "tslint --force --format verbose \"src/**/*.ts\"",
"lint": "tslint -c tslint.json 'src/**/*.ts'",
"build": "npm run clean && npm run lint && echo Using TypeScript && tsc --version && tsc --pretty",
"test": "npm run build && mocha --compilers ts:ts-node/register --recursive test/**/*-spec.ts",
"test": "npm run build && mocha --compilers ts:ts-node/register --recursive test/**/*.test.ts",
"watch": "npm run build -- --watch",
"watch:test": "npm run test -- --watch",
"e2etst": "npm run "
@ -32,6 +32,8 @@
"dependencies": {
"commander": "~2.9.0",
"debug": "~2.6.1",
"mkdirp": "^0.5.1",
"ncp": "^2.0.0",
"rimraf": "~2.5.4",
"webpack": "~3.5.6",
"winston": "~2.3.1"
@ -40,15 +42,20 @@
"@types/chai": "3.5.0",
"@types/commander": "~2.3.31",
"@types/debug": "0.0.29",
"@types/mkdirp": "^0.5.1",
"@types/mocha": "2.2.41",
"@types/ncp": "^2.0.1",
"@types/node": "6.0.31",
"@types/rimraf": "0.0.28",
"@types/supertest": "^2.0.3",
"@types/webpack": "~3.0.0",
"@types/winston": "~2.2.0",
"chai": "~3.5.0",
"mocha": "~3.0.0",
"ps-node": "^0.1.6",
"supertest": "^3.0.0",
"ts-node": "~1.0.0",
"tslint": "~4.0.0",
"tslint": "~5.7.0",
"typescript": "~2.2.0"
},
"engines": {

Просмотреть файл

@ -1,9 +0,0 @@
module.exports = function (context, req) {
context.log('"./lib/externalScriptFile" function called');
const res = {
body: {
"success":true
}
}
context.done(null, res);
};

Просмотреть файл

@ -1,18 +0,0 @@
let sql = require('tedious');
class Model {
getAll() {
const request = new sql.Request("select 'hello'", function(err, rowCount) {
// no op
});
return Promise.resolve([]);
}
add() {
return Promise.resolve({});
}
}
module.exports = {
Model: Model
}

Просмотреть файл

@ -1,4 +1,4 @@
#! /usr/bin/env node
#!/usr/bin/env node
import * as program from "commander";
import * as path from "path";
@ -21,6 +21,7 @@ async function runCli() {
.description("Will pack the specified path or the current directory if none is specified")
.option("-u, --uglify", "Uglify the project when webpacking")
.option("-o, --output <path>", "Path for output directory")
.option("-c, --copyToOutput", "Copy files to output directory")
.action(pack);
p.command("*", null, { noHelp: true, isDefault: true })
@ -53,12 +54,12 @@ async function unpack(name: string, options: any) {
let outputPath = ".funcpack";
try {
if (options.path) {
outputPath = program.opts().path;
if (options.output) {
outputPath = path.join(options.output, outputPath);
}
} catch (e) {
winston.error(e);
throw new Error("Could not parse the uglify option");
throw new Error("Could not parse the output option");
}
winston.info("Unpacking project at: " + projectRootPath);
@ -100,16 +101,28 @@ async function pack(name: string, options: any) {
let outputPath = ".funcpack";
try {
if (options.path) {
outputPath = program.opts().path;
if (options.output) {
outputPath = path.join(options.output, outputPath);
}
} catch (e) {
winston.error(e);
throw new Error("Could not parse the uglify option");
throw new Error("Could not parse the output option");
}
let copyToOutput = false;
try {
if (options.copyToOutput) {
copyToOutput = true;
}
} catch (e) {
winston.error(e);
throw new Error("Could not parse the copyToOutput option");
}
// Create new generator object with settings
const generator = new PackhostGenerator({
copyToOutput,
outputPath,
projectRootPath,
});
@ -126,10 +139,10 @@ async function pack(name: string, options: any) {
try {
winston.info("Webpacking project");
await WebpackRunner.run({
ignoredModules: config.ignoredModules,
outputPath,
projectRootPath,
uglify,
outputPath,
ignoredModules: config.ignoredModules,
});
} catch (error) {
winston.error(error);

Просмотреть файл

@ -15,6 +15,7 @@ export class PackhostGenerator {
this.options = options;
this.options.indexFileName = this.options.indexFileName || "index.js";
this.options.outputPath = this.options.outputPath || ".funcpack";
this.options.copyToOutput = this.options.copyToOutput || false;
debug("Created new PackhostGenerator for project at: %s", this.options.projectRootPath);
}
@ -85,13 +86,13 @@ export class PackhostGenerator {
directory: dir,
});
return null;
//throw new Error(`Function ${name} does not have a valid start file`);
// throw new Error(`Function ${name} does not have a valid start file`);
}
originalScriptFile = scriptFile;
}
// TODO: improve the logic for choosing entry point - failure sure not all scenarios are covered here.
// TODO: Have to overwrite this entryPoint later on. Using temporary setting for now.
// TODO: improve the logic for choosing entry point - failure sure not all scenarios are covered here.
// TODO: Have to overwrite this entryPoint later on. Using temporary setting for now.
if (fxJson._originalEntryPoint) {
debug("Found originalEntryPoint setting: %s", fxJson._originalEntryPoint);
entryPoint = fxJson._originalEntryPoint;
@ -103,11 +104,11 @@ export class PackhostGenerator {
debug("Loaded function(%s) using entryPoint: %s - scriptFile: %s", name, scriptFile, entryPoint);
return Promise.resolve({
name,
scriptFile,
entryPoint,
_originalEntryPoint: originalEntryPoint,
_originalScriptFile: originalScriptFile,
entryPoint,
name,
scriptFile,
});
}
@ -126,9 +127,13 @@ export class PackhostGenerator {
debug("Generating host file");
const exportStrings: string[] = [];
const outputDirPath = path.join(this.options.projectRootPath, this.options.outputPath);
const relPath = path.relative(outputDirPath, this.options.projectRootPath);
const rootRelPath = (path.sep === "\\") ? relPath.replace(/\\/g, "/") : relPath;
for (const [name, fx] of this.functionsMap) {
const fxvar = this.safeFunctionName(fx.name);
let exportStmt = ` "${fxvar}": require("../${fx.name}/${fx._originalScriptFile}")`;
let exportStmt = ` "${fxvar}": require("${rootRelPath}/${fx.name}/${fx._originalScriptFile}")`;
if (fx.entryPoint) {
exportStmt += `.${fx.entryPoint}`;
}
@ -150,15 +155,26 @@ export class PackhostGenerator {
debug("Updating Function JSONS");
for (const [name, fx] of this.functionsMap) {
debug("Updating function(%s)", name);
const fxJsonPath = path.resolve(this.options.projectRootPath, name, "function.json");
let fxJsonPath = path.resolve(this.options.projectRootPath, name, "function.json");
const fxvar = this.safeFunctionName(fx.name);
const fxJson = await FileHelper.readFileAsJSON(fxJsonPath);
if (this.options.copyToOutput) {
await FileHelper.cp(
path.resolve(this.options.projectRootPath, name, "function.json")
, path.resolve(this.options.projectRootPath, this.options.outputPath, name, "function.json"));
}
// TODO: This way of keeping track of the original settings is hacky
fxJson._originalEntryPoint = fx._originalEntryPoint;
fxJson._originalScriptFile = fx._originalScriptFile;
fxJson.scriptFile = `../${this.options.outputPath}/${this.options.indexFileName}`;
fxJson.scriptFile = this.options.copyToOutput ?
`../${this.options.indexFileName}` :
`../${this.options.outputPath}/${this.options.indexFileName}`;
fxJson.entryPoint = fxvar;
if (this.options.copyToOutput) {
fxJsonPath = path.resolve(this.options.projectRootPath, this.options.outputPath, name, "function.json");
}
await FileHelper.overwriteFileUtf8(fxJsonPath, JSON.stringify(fxJson, null, " "));
}
}
@ -172,6 +188,7 @@ export interface IPackhostGeneratorOptions {
projectRootPath: string;
outputPath?: string;
indexFileName?: string;
copyToOutput?: boolean;
}
export interface IFxFunction {

Просмотреть файл

@ -1,6 +1,11 @@
import * as fs from "fs";
import * as mkdirp from "mkdirp";
import { ncp } from "ncp";
import * as nodepath from "path";
import * as rimraf from "rimraf";
export type FilterCallBack = (name: string) => boolean;
export class FileHelper {
public static readdir(path: string): Promise<string[]> {
return new Promise((resolve, reject) => {
@ -110,4 +115,38 @@ export class FileHelper {
});
});
}
public static cp(source: string, destination: string, filter?: RegExp | FilterCallBack): Promise<{}> {
const options: any = {};
options.clobber = true;
options.errs = process.stderr;
if (filter) {
options.filter = filter;
}
return new Promise(async (resolve, reject) => {
if (!await FileHelper.exists(nodepath.dirname(destination))) {
await FileHelper.mkdirp(nodepath.dirname(destination));
}
ncp(source, destination, options, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
}
public static mkdirp(pathToCreate: string): Promise<string> {
return new Promise((resolve, reject) => {
mkdirp(pathToCreate, (err, made) => {
if (err) {
return reject(err);
}
resolve(made);
});
});
}
}

Просмотреть файл

@ -1,2 +1,2 @@
export * from "./fs-helper";
export * from "./config-loader";
export * from "./config-loader";

258
test/e2e/e2e.test.ts Normal file
Просмотреть файл

@ -0,0 +1,258 @@
import * as chai from "chai";
import { spawn } from "child_process";
import * as debug from "debug";
import "mocha";
import * as os from "os";
import * as path from "path";
import { FileHelper } from "../../src/utils/index";
import { FunctionHostHarness } from "../util/FunctionHostHarness";
import { ProcessHelper } from "../util/ProcessHelper";
const log = debug("azure-functions-pack:e2e.test");
const expect = chai.expect;
const sampleRoot = path.resolve(__dirname, "./sample/");
describe("e2e tests", function() {
describe("funcpack pack .", function() {
const randomNumber = Math.floor(Math.random() * 10000);
const testRoot = path.resolve(os.tmpdir(), `./AzureFunctionsPackTest${randomNumber}`);
log(`Using temp dir: ${testRoot}`);
describe("cli", function() {
before(async function() {
this.timeout(60000);
return await FileHelper.cp(sampleRoot, testRoot);
});
after(async function() {
this.timeout(60000);
if (process.env.FUNCPACK_TESTS_CLEAN) {
return await FileHelper.rimraf(testRoot);
} else {
return Promise.resolve();
}
});
it("should run successfully", async function() {
this.timeout(60000);
try {
const results = await ProcessHelper.run(["node",
path.resolve(__dirname, "../../lib/main.js"), "pack", "."], testRoot);
expect(results.didError).to.be.equal(false, "funcpack pack did not exit successfully");
return Promise.resolve();
} catch (e) {
return Promise.reject(e);
}
});
});
describe("host", function() {
let host: FunctionHostHarness;
before(async function() {
this.timeout(60000);
await ProcessHelper.killAllFunctionsHosts();
if (!await FileHelper.exists(testRoot)) {
await FileHelper.cp(sampleRoot, testRoot);
}
await ProcessHelper.run(["node",
path.resolve(__dirname, "../../lib/main.js"), "pack", "."], testRoot);
host = new FunctionHostHarness(testRoot);
await host.init();
return new Promise((resolve, reject) => {
const int = setInterval(() => {
host.test("simple")
.then((res: any) => {
log(JSON.stringify(res));
if (res.status === 200) {
clearTimeout(int);
resolve();
}
}).catch((e) => log(e));
}, 500);
});
});
after(async function() {
this.timeout(60000);
host.stop();
await ProcessHelper.killAllFunctionsHosts();
if (process.env.FUNCPACK_TESTS_CLEAN) {
return await FileHelper.rimraf(testRoot);
} else {
return Promise.resolve();
}
});
it("should ignore non-js files", function(done) {
const funcname = process.env.FUNCPACK_TESTS_V2 ? "cs-ignoreme-v2" : "cs-ignoreme";
host.test(funcname)
.expect(200, done);
});
it("should obey entryPoint setting", function(done) {
host.test("entryPoint")
.expect(200, done);
});
it("should obey excluded setting", function(done) {
host.test("excluded")
.expect(200, done);
});
it("should work with external script files", function(done) {
host.test("externalScriptFile")
.expect(200, done);
});
it("should work with large imports", function(done) {
host.test("largeimport")
.expect(200, done);
});
it("should work with local libs", function(done) {
host.test("libimport")
.expect(200, done);
});
it("should obey scriptFile setting", function(done) {
host.test("scriptFile")
.expect(200, done);
});
it("should work with simple functions", function(done) {
host.test("simple")
.expect(200, done);
});
it("should work with simple imports", function(done) {
host.test("simpleimport")
.expect(200, done);
});
});
});
describe("funcpack pack -c .", function() {
const randomNumber = Math.floor(Math.random() * 10000);
const testRoot = path.resolve(os.tmpdir(), `./AzureFunctionsPackTest${randomNumber}`);
log(`Using temp dir: ${testRoot}`);
describe("cli", function() {
before(async function() {
this.timeout(60000);
return await FileHelper.cp(sampleRoot, testRoot);
});
after(async function() {
this.timeout(60000);
if (process.env.FUNCPACK_TESTS_CLEAN) {
return await FileHelper.rimraf(testRoot);
} else {
return Promise.resolve();
}
});
it("should run successfully", async function() {
this.timeout(60000);
try {
const results = await ProcessHelper.run(["node",
path.resolve(__dirname, "../../lib/main.js"), "pack", "-c", "."], testRoot);
expect(results.didError).to.be.equal(false, "funcpack pack did not exit successfully");
return Promise.resolve();
} catch (e) {
return Promise.reject(e);
}
});
});
describe("host", function() {
let host: FunctionHostHarness;
before(async function() {
this.timeout(60000);
await ProcessHelper.killAllFunctionsHosts();
if (!await FileHelper.exists(testRoot)) {
await FileHelper.cp(sampleRoot, testRoot);
}
await ProcessHelper.run(["node",
path.resolve(__dirname, "../../lib/main.js"), "pack", "-c", "."], testRoot);
const testRunRoot = path.resolve(testRoot, ".funcpack");
log(`Starting host in ${testRunRoot}`);
host = new FunctionHostHarness(testRunRoot);
await host.init();
return new Promise((resolve, reject) => {
const int = setInterval(() => {
host.test("simple")
.then((res: any) => {
log(JSON.stringify(res, null, " "));
if (res.status === 200) {
clearTimeout(int);
resolve();
}
}).catch((e) => log(e));
}, 500);
});
});
after(async function() {
this.timeout(60000);
host.stop();
await ProcessHelper.killAllFunctionsHosts();
if (process.env.FUNCPACK_TESTS_CLEAN) {
return await FileHelper.rimraf(testRoot);
} else {
return Promise.resolve();
}
});
it("should ignore non-js files", function(done) {
const funcname = process.env.FUNCPACK_TESTS_V2 ? "cs-ignoreme-v2" : "cs-ignoreme";
host.test(funcname)
.expect(200, done);
});
it("should obey entryPoint setting", function(done) {
host.test("entryPoint")
.expect(200, done);
});
it("should obey excluded setting", function(done) {
host.test("excluded")
.expect(200, done);
});
it("should work with external script files", function(done) {
host.test("externalScriptFile")
.expect(200, done);
});
it("should work with large imports", function(done) {
host.test("largeimport")
.expect(200, done);
});
it("should work with local libs", function(done) {
host.test("libimport")
.expect(200, done);
});
it("should obey scriptFile setting", function(done) {
host.test("scriptFile")
.expect(200, done);
});
it("should work with simple functions", function(done) {
host.test("simple")
.expect(200, done);
});
it("should work with simple imports", function(done) {
host.test("simpleimport")
.expect(200, done);
});
});
});
});

Просмотреть файл

Просмотреть файл

@ -0,0 +1,12 @@
#r "Newtonsoft.Json"
using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using System.Net;
public static IActionResult Run(HttpRequest req, TraceWriter log)
{
return (ActionResult)new OkObjectResult("never pack me");
}

Просмотреть файл

@ -0,0 +1,16 @@
{
"disabled": false,
"bindings": [
{
"authLevel": "function",
"name": "req",
"type": "httpTrigger",
"direction": "in"
},
{
"name": "$return",
"type": "http",
"direction": "out"
}
]
}

Просмотреть файл

@ -3,4 +3,4 @@ using System.Net;
public static async Task<HttpResponseMessage> Run(HttpRequestMessage req, TraceWriter log)
{
return req.CreateResponse(HttpStatusCode.OK, "Never pack me");
}
}

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

1383
test/e2e/sample/package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -14,6 +14,6 @@
"tedious": "^1.14.0"
},
"devDependencies": {
"chai":"3.5.0"
"chai": "3.5.0"
}
}

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

@ -1,6 +0,0 @@
import * as chai from "chai";
import { PackhostGenerator } from "../src/packhost-generator";
const expect = chai.expect;
// TODO: Should write some tests :3

Просмотреть файл

7
test/tslint.json Normal file
Просмотреть файл

@ -0,0 +1,7 @@
{
"extends": "../tslint.json",
"rules":{
"no-console": false,
"only-arrow-functions": false
}
}

55
test/util/FunctionHost.ts Normal file
Просмотреть файл

@ -0,0 +1,55 @@
import { ChildProcess, spawn } from "child_process";
import * as debug from "debug";
import * as events from "events";
// tslint:disable-next-line:no-var-requires
const ps = require("ps-node");
type ps = any;
const log = debug("azure-functions-pack:FunctionHost");
export class FunctionHost extends events.EventEmitter {
private child: ChildProcess;
private funcRoot: string;
constructor(funcRoot: string) {
super();
this.funcRoot = funcRoot;
}
public start() {
const commands = ["func host start"];
log(`Running ${commands.join(" ")} in ${this.funcRoot}`);
const isWin = /^win/.test(process.platform);
commands.unshift(isWin ? "/c" : "-c");
this.child = spawn(isWin ? "cmd" : "sh", commands, {
cwd: this.funcRoot,
});
if (process.env.DEBUG
&& (process.env.DEBUG.includes("azure-functions-pack:*")
|| process.env.DEBUG.includes("azure-functions-pack:FunctionHost"))) {
this.child.stdout.pipe(process.stdout);
this.child.stderr.pipe(process.stderr);
}
this.child.on("error", (err: Error) => {
this.emit("error", err);
});
this.child.on("exit", (code: string) => {
this.emit("exit", code);
});
}
public stop(): Promise<{}> {
return new Promise((resolve, reject) => {
ps.kill(this.child.pid, (err: Error) => {
if (err) {
return reject(err);
}
resolve();
});
});
}
}

Просмотреть файл

@ -0,0 +1,47 @@
import * as debug from "debug";
import * as request from "supertest";
import { FunctionHost } from "./FunctionHost";
const log = debug("azure-functions-pack:FunctionHostHarness");
export class FunctionHostHarness {
private host: FunctionHost;
private request: request.SuperTest<request.Test>;
constructor(funcRoot: string) {
this.host = new FunctionHost(funcRoot);
this.request = request("http://localhost:7071");
this.host.on("error", (err: Error) => {
log(err);
});
this.host.on("exit", (code: string) => {
log(`Functions host exitted with status code: ${code}`);
});
}
public test(name: string) {
return this.request.post(`/api/${name}`);
}
public init(): Promise<{}> {
const req = this.request;
this.host.start();
return new Promise((resolve, reject) => {
const int = setInterval(() => {
req.get("/admin/host/status")
.then((res) => {
if (res.status === 200) {
clearTimeout(int);
resolve();
}
}).catch((e) => log(e));
}, 500);
});
}
public stop() {
this.host.stop();
}
}

Просмотреть файл

@ -0,0 +1,69 @@
import { ChildProcess, exec, spawn } from "child_process";
import * as debug from "debug";
// tslint:disable-next-line:no-var-requires
const ps = require("ps-node");
const log = debug("azure-functions-pack:ProcessHelper");
export class ProcessHelper {
public static run(commands: string[], cwd: string): Promise<IProcessResults> {
return new Promise<IProcessResults>((resolve, reject) => {
exec(commands.join(" "), { cwd }, (err: Error, stdout: string, stderr: string) => {
log(stdout);
log(stderr);
const results: IProcessResults = {
didError: err ? true : false,
error: err,
stderr,
stdout,
};
if (err) {
log(err);
reject(err);
return;
}
resolve(results);
});
});
}
public static killAllFunctionsHosts(): Promise<{}> {
return new Promise((resolve, reject) => {
ps.lookup({
command: "dotnet",
}, (err: any, processes: any) => {
const promises: any[] = [];
processes.forEach((p: any) => {
log(JSON.stringify(p, null, " "));
p.arguments.forEach((a: any) => {
if (a.includes("bin/Azure.Functions.Cli.dll")) {
promises.push(ProcessHelper.kill(p.pid));
}
});
});
Promise.all(promises).then(resolve).catch((e) => { log(e); resolve(); });
});
});
}
public static kill(pid: number): Promise<{}> {
return new Promise((resolve, reject) => {
ps.kill(pid, "SIGKILL", (e: Error) => {
if (e) {
resolve(e);
} else {
reject();
}
});
});
}
}
export interface IProcessResults {
exitCode?: string;
stdout?: string;
stderr?: string;
didError?: boolean;
error?: Error;
}