Skip to content

Commit 23e238b

Browse files
authored
Merge pull request #36 from anthonychu/preview
Merge `preview` branch
2 parents 4d70d92 + 992ff52 commit 23e238b

File tree

4 files changed

+142
-88
lines changed

4 files changed

+142
-88
lines changed

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@ This is a worker that lets you run Deno on [Azure Functions](https://docs.micros
2727

2828
The project includes a CLI `denofunc` to make it easy to create, run, and deploy your Deno Azure Functions apps.
2929

30-
> **Note:** Due to some issues with the Deno bundler, we currently do not deploy a bundled app. Expect cold starts to be worse until we can bundle again.
31-
3230
### 3 commands to get started
3331

3432
```bash
@@ -39,7 +37,7 @@ denofunc init
3937
denofunc start
4038

4139
# deploy the app
42-
denofunc publish $functionAppName
40+
denofunc publish $functionAppName [--slot $slotName]
4341
```
4442

4543
For more information, try the [quickstart](#getting-started) below.
@@ -61,7 +59,7 @@ Check out the [new project template](https://github.com/anthonychu/azure-functio
6159
* Linux, macOS, Windows
6260
* [Deno](https://deno.land/x/install/)
6361
- Tested on:
64-
- `1.5.4`
62+
- `1.10.2`
6563
* [Azure Functions Core Tools V3](https://github.com/Azure/azure-functions-core-tools#azure-functions-core-tools) - needed for running the app locally and deploying it
6664
* [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest#install) - needed to deploy the app
6765
* `denofunc` CLI - see [below](#install-the-denofunc-cli)

denofunc.ts

Lines changed: 133 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,67 @@
1-
const { args } = Deno;
21
import {
32
parse,
43
readZip,
54
ensureDir,
65
move,
76
walk,
7+
semver,
88
} from "./deps.ts";
99

10-
const shouldBundle = false;
10+
const baseExecutableFileName = "worker";
11+
const bundleFileName = "worker.bundle.js";
12+
const commonDenoOptions = ["--allow-env", "--allow-net", "--allow-read"];
1113
const parsedArgs = parse(Deno.args);
1214

13-
if (parsedArgs["help"]) {
15+
const bundleStyles = ["executable", "jsbundle", "none"];
16+
const STYLE_EXECUTABLE = 0;
17+
const STYLE_JSBUNDLE = 1;
18+
const STYLE_NONE = 2;
19+
20+
if (parsedArgs._[0] === "help") {
1421
printHelp();
1522
Deno.exit();
1623
}
1724

18-
if (args.length >= 1 && args[0] === "init") {
19-
const templateDownloadBranch: string | undefined = args[1];
25+
if (parsedArgs._.length >= 1 && parsedArgs._[0] === "init") {
26+
const templateDownloadBranch: string | undefined = parsedArgs?._[1]?.toString();
2027
await initializeFromTemplate(templateDownloadBranch);
2128
} else if (
22-
args.length === 1 && args[0] === "start" ||
23-
args.length === 2 && `${args[0]} ${args[1]}` === "host start"
29+
parsedArgs._.length === 1 && parsedArgs._[0] === "start" ||
30+
parsedArgs._.length === 2 && parsedArgs._.join(' ') === "host start"
2431
) {
2532
await generateFunctions();
2633
await createJSBundle();
2734
await runFunc("start");
28-
} else if (args.length === 2 && args[0] === "publish") {
29-
const platform = await getAppPlatform(args[1]);
30-
await updateHostJson(platform);
31-
await downloadBinary(platform);
35+
} else if (parsedArgs._[0] === "publish" && parsedArgs._.length === 2) {
36+
const bundleStyle = parsedArgs["bundle-style"] // use specified bundle style
37+
|| (semver.satisfies(Deno.version.deno, ">=1.6.0") // default style depends on Deno runtime version
38+
? bundleStyles[STYLE_EXECUTABLE] // for v1.6.0 or later
39+
: bundleStyles[STYLE_JSBUNDLE] // for others
40+
);
41+
if (!bundleStyles.includes(bundleStyle)) {
42+
console.error(`The value \`${parsedArgs["bundle-style"]}\` of \`--bundle-style\` option is not acceptable.`)
43+
Deno.exit(1);
44+
} else if (semver.satisfies(Deno.version.deno, "<1.6.0") && bundleStyle === bundleStyles[STYLE_EXECUTABLE]) {
45+
console.error(`Deno version v${Deno.version.deno} doesn't support \`${bundleStyles[STYLE_EXECUTABLE]}\` for bundle style.`);
46+
Deno.exit(1);
47+
}
48+
const appName = parsedArgs._[1].toString();
49+
const slotName = parsedArgs["slot"]?.toString();
50+
const platform = await getAppPlatform(appName, slotName);
51+
if (!["windows", "linux"].includes(platform)) {
52+
console.error(`The value \`${platform}\` for the function app \`${appName + (slotName ? `/${slotName}` : "")}\` is not valid.`);
53+
Deno.exit(1);
54+
}
55+
await updateHostJson(platform, bundleStyle);
3256
await generateFunctions();
33-
await createJSBundle();
34-
await publishApp(args[1]);
57+
58+
if (bundleStyle === bundleStyles[STYLE_EXECUTABLE]) {
59+
await generateExecutable(platform);
60+
} else {
61+
await downloadBinary(platform);
62+
if (bundleStyle === bundleStyles[STYLE_JSBUNDLE]) await createJSBundle();
63+
}
64+
await publishApp(appName, slotName);
3565
} else {
3666
printHelp();
3767
}
@@ -67,24 +97,49 @@ async function listFiles(dir: string) {
6797
return files;
6898
}
6999

100+
async function generateExecutable(platformArg?: string) {
101+
try {
102+
await Deno.remove('./bin', { recursive: true });
103+
await Deno.remove(`./${bundleFileName}`);
104+
} catch { }
105+
106+
const platform = platformArg || Deno.build.os;
107+
await Deno.mkdir(`./bin/${platform}`, { recursive: true });
108+
109+
const cmd = [
110+
"deno",
111+
"compile",
112+
"--unstable",
113+
...(semver.satisfies(Deno.version.deno, ">=1.7.1 <1.10.0") ? ["--lite"] : []), // `--lite` option is implemented only between v1.7.1 and v1.9.x
114+
...commonDenoOptions,
115+
"--output",
116+
`./bin/${platform}/${baseExecutableFileName}`,
117+
...(['windows', 'linux'].includes(platform)
118+
? ['--target', platform === 'windows' ? 'x86_64-pc-windows-msvc' : 'x86_64-unknown-linux-gnu']
119+
: []
120+
),
121+
"worker.ts"
122+
];
123+
console.info(`Running command: ${cmd.join(" ")}`);
124+
const generateProcess = Deno.run({ cmd });
125+
await generateProcess.status();
126+
}
127+
70128
async function createJSBundle() {
71-
if (shouldBundle) {
72-
const bundleFileName = "worker.bundle.js";
73-
const cmd = ["deno", "bundle", "--unstable", "worker.ts", bundleFileName];
74-
console.info(`Running command: ${cmd.join(" ")}`);
75-
const generateProcess = Deno.run({ cmd });
76-
await generateProcess.status();
77-
}
129+
const cmd = ["deno", "bundle", "--unstable", "worker.ts", bundleFileName];
130+
console.info(`Running command: ${cmd.join(" ")}`);
131+
const generateProcess = Deno.run({ cmd });
132+
await generateProcess.status();
78133
}
79134

80-
async function getAppPlatform(appName: string): Promise<string> {
81-
console.info(`Checking platform type of : ${appName} ...`);
135+
async function getAppPlatform(appName: string, slotName?: string): Promise<string> {
136+
console.info(`Checking platform type of : ${appName + (slotName ? `/${slotName}` : "")} ...`);
82137
const azResourceCmd = [
83138
"az",
84139
"resource",
85140
"list",
86141
"--resource-type",
87-
"Microsoft.web/sites",
142+
`Microsoft.web/sites${slotName ? "/slots" : ""}`,
88143
"-o",
89144
"json",
90145
];
@@ -100,32 +155,8 @@ async function getAppPlatform(appName: string): Promise<string> {
100155

101156
try {
102157
const resource = resources.find((resource: any) =>
103-
resource.name === appName
104-
);
105-
106-
if ((resource.kind as string).includes("linux")) {
107-
return "linux";
108-
}
109-
110-
const azFunctionCmd = [
111-
"az",
112-
"functionapp",
113-
"config",
114-
"show",
115-
"--ids",
116-
resource.id,
117-
"-o",
118-
"json",
119-
];
120-
const azFunctionProcess = await runWithRetry(
121-
{ cmd: azFunctionCmd, stdout: "piped" },
122-
"az.cmd",
123-
);
124-
const azFunctionOutput = await azFunctionProcess.output();
125-
const config = JSON.parse(
126-
new TextDecoder().decode(azFunctionOutput),
158+
resource.name === (appName + (slotName ? `/${slotName}` : ""))
127159
);
128-
azFunctionProcess.close();
129160

130161
const azFunctionAppSettingsCmd = [
131162
"az",
@@ -135,8 +166,12 @@ async function getAppPlatform(appName: string): Promise<string> {
135166
"set",
136167
"--ids",
137168
resource.id,
169+
...(slotName
170+
? ["--slot", slotName]
171+
: []
172+
),
138173
"--settings",
139-
"WEBSITE_LOAD_USER_PROFILE=1",
174+
"FUNCTIONS_WORKER_RUNTIME=custom",
140175
"-o",
141176
"json",
142177
];
@@ -147,27 +182,36 @@ async function getAppPlatform(appName: string): Promise<string> {
147182
await azFunctionAppSettingsProcess.status();
148183
azFunctionAppSettingsProcess.close();
149184

150-
return "windows";
185+
return (resource.kind as string).includes("linux") ? "linux" : "windows";
151186
} catch {
152-
throw new Error(`Not found: ${appName}`);
187+
throw new Error(`Not found: ${appName + (slotName ? `/${slotName}` : "")}`);
153188
}
154189
}
155190

156-
async function updateHostJson(platform: string) {
157-
// update `defaultExecutablePath` in host.json
191+
async function updateHostJson(platform: string, bundleStyle: string) {
192+
// update `defaultExecutablePath` and `arguments` in host.json
158193
const hostJsonPath = "./host.json";
159194
if (!(await fileExists(hostJsonPath))) {
160195
throw new Error(`\`${hostJsonPath}\` not found`);
161196
}
162197

163198
const hostJSON: any = await readJson(hostJsonPath);
164-
hostJSON.customHandler.description.defaultExecutablePath = platform === "windows"
165-
? "D:\\home\\site\\wwwroot\\bin\\windows\\deno.exe"
166-
: "/home/site/wwwroot/bin/linux/deno",
167-
await writeJson(hostJsonPath, hostJSON); // returns a promise
199+
if (!hostJSON.customHandler) hostJSON.customHandler = {};
200+
hostJSON.customHandler.description = {
201+
defaultExecutablePath: `bin/${platform}/${bundleStyle === bundleStyles[STYLE_EXECUTABLE] ? baseExecutableFileName : "deno"}${platform === "windows" ? ".exe" : ""}`,
202+
arguments: bundleStyle === bundleStyles[STYLE_EXECUTABLE]
203+
? []
204+
: [
205+
"run",
206+
...commonDenoOptions,
207+
bundleStyle === bundleStyles[STYLE_JSBUNDLE] ? bundleFileName : "worker.ts"
208+
]
209+
};
210+
211+
await writeJson(hostJsonPath, hostJSON); // returns a promise
168212
}
169213

170-
function writeJson(path: string, data: object): void {
214+
function writeJson(path: string, data: object): void {
171215
Deno.writeTextFileSync(path, JSON.stringify(data, null, 2));
172216
}
173217

@@ -193,12 +237,14 @@ async function downloadBinary(platform: string) {
193237
await Deno.remove(entry);
194238
}
195239
}
240+
try {
241+
await Deno.remove(`./${bundleFileName}`);
242+
} catch { }
196243

197244
const binZipPath = `${binDir}/deno.zip`;
198245
if (!(await fileExists(binPath))) {
199246
const downloadUrl =
200-
`https://github.com/denoland/deno/releases/download/v${Deno.version.deno}/deno-x86_64-${
201-
archive[platform]
247+
`https://github.com/denoland/deno/releases/download/v${Deno.version.deno}/deno-x86_64-${archive[platform]
202248
}.zip`;
203249
console.info(`Downloading deno binary from: ${downloadUrl} ...`);
204250
// download deno binary (that gets deployed to Azure)
@@ -270,9 +316,7 @@ async function generateFunctions() {
270316
cmd: [
271317
"deno",
272318
"run",
273-
"--allow-net",
274-
"--allow-env",
275-
"--allow-read",
319+
...commonDenoOptions,
276320
"--allow-write",
277321
"--unstable",
278322
"--no-check",
@@ -305,8 +349,7 @@ async function runWithRetry(
305349
} catch (ex) {
306350
if (Deno.build.os === "windows") {
307351
console.info(
308-
`Could not start ${
309-
runOptions.cmd[0]
352+
`Could not start ${runOptions.cmd[0]
310353
} from path, searching for executable...`,
311354
);
312355
const whereCmd = ["where.exe", backupCommand];
@@ -335,30 +378,30 @@ async function runWithRetry(
335378
}
336379
}
337380

338-
async function publishApp(appName: string) {
339-
await runFunc(
381+
async function publishApp(appName: string, slotName?: string) {
382+
const runFuncArgs = [
340383
"azure",
341384
"functionapp",
342385
"publish",
343-
appName,
344-
"--force"
345-
);
386+
appName
387+
];
388+
await runFunc(...(slotName ? runFuncArgs.concat(["--slot", slotName]) : runFuncArgs));
346389
}
347390

348391
function printLogo() {
349392
const logo = `
350-
@@@@@@@@@@@,
351-
@@@@@@@@@@@@@@@@@@@ %%%%%%%%%%%%
352-
@@@@@@ @@@@@@@@@@ %%%%%%%%%%%%
353-
@@@@@ @ @ *@@@@@ @ %%%%%%%%%%%% @
354-
@@@ @@@@@ @@ %%%%%%%%%%%% @@
355-
@@@@@ @@@@@ @@@ %%%%%%%%%%%%%%%%%%%%%% @@@
356-
@@@@@@@@@@@@@@@ @@@@ @@ %%%%%%%%%%%%%%%%%%%% @@
357-
@@@@@@@@@@@@@@ @@@@ @@ %%%%%%%% @@
358-
@@@@@@@@@@@@@@ @@@ @@ %%%%%% @@
359-
@@@@@@@@@@@@@ @ @@ %%%% @@
360-
@@@@@@@@@@@ %%%%
361-
@@@@@@@ %%
393+
@@@@@@@@@@@,
394+
@@@@@@@@@@@@@@@@@@@ %%%%%%
395+
@@@@@@ @@@@@@@@@@ %%%%%%
396+
@@@@@ @ @ *@@@@@ @ %%%%%% @
397+
@@@ @@@@@ @@ %%%%%% @@
398+
@@@@@ @@@@@ @@@ %%%%%%%%%%% @@@
399+
@@@@@@@@@@@@@@@ @@@@ @@ %%%%%%%%%% @@
400+
@@@@@@@@@@@@@@ @@@@ @@ %%%% @@
401+
@@@@@@@@@@@@@@ @@@ @@ %%% @@
402+
@@@@@@@@@@@@@ @ @@ %% @@
403+
@@@@@@@@@@@ %%
404+
@@@@@@@ %
362405
`;
363406
console.info(logo);
364407
}
@@ -378,7 +421,14 @@ denofunc init
378421
denofunc start
379422
Generate functions artifacts and start Azure Functions Core Tools
380423
381-
denofunc publish <function_app_name>
424+
denofunc publish <function_app_name> [options]
382425
Publish to Azure
426+
options:
427+
--slot <slot_name> Specify name of the deployment slot
428+
--bundle-style executable|jsbundle|none Select bundle style on deployment
429+
430+
executable: Bundle as one executable(default option for Deno v1.6.0 or later).
431+
jsbundle: Bundle as one javascript worker & Deno runtime
432+
none: No bundle
383433
`);
384434
}

deps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export {
66
walk,
77
} from "https://deno.land/std@0.79.0/fs/mod.ts";
88
export * from "./worker_deps.ts";
9+
export * as semver from "https://deno.land/x/semver@v1.3.0/mod.ts";

mod.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,14 @@ export class AzureFunctionsWorker {
135135

136136
const result = await Promise.resolve(registration.handler(context));
137137

138+
// Merge `context.res` into `context.bindings`
139+
// `context.res` is the special property for HTTP response
140+
// https://docs.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=v2#response-object
141+
if (context.res) context.bindings.res = context.res;
142+
138143
ctx.response.body = {
139144
Logs: context.log.logs,
140-
Outputs: context,
145+
Outputs: context.bindings,
141146
ReturnValue: result,
142147
};
143148
ctx.response.headers.set("content-type", "application/json");

0 commit comments

Comments
 (0)