Skip to content

Commit

Permalink
feat(api): Adds unstable_pages module to JS API
Browse files Browse the repository at this point in the history
  • Loading branch information
jrf0110 committed Jan 10, 2023
1 parent 90cf62e commit 795bc16
Show file tree
Hide file tree
Showing 9 changed files with 449 additions and 347 deletions.
5 changes: 5 additions & 0 deletions .changeset/green-planets-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"wrangler": patch
---

Adds unstable_pages module to JS API
1 change: 1 addition & 0 deletions packages/wrangler/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { unstable_dev } from "./dev";
export type { UnstableDevWorker, UnstableDevOptions } from "./dev";
export { unstable_pages } from "./pages";
5 changes: 5 additions & 0 deletions packages/wrangler/src/api/pages/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { publish } from "./publish";

export const unstable_pages = {
publish,
};
297 changes: 297 additions & 0 deletions packages/wrangler/src/api/pages/publish.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import { existsSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { cwd } from "node:process";
import { File, FormData } from "undici";
import { fetchResult } from "../../cfetch";
import { FatalError } from "../../errors";
import { logger } from "../../logger";
import { buildFunctions } from "../../pages/buildFunctions";
import {
FunctionsNoRoutesError,
getFunctionsNoRoutesWarning,
} from "../../pages/errors";
import { validateRoutes } from "../../pages/functions/routes-validation";
import { upload } from "../../pages/upload";
import type { Project, Deployment } from "@cloudflare/types";

interface PagesPublishOptions {
/**
* Path to static assets to publish to Pages
*/
directory: string;
/**
* The Cloudflare Account ID that owns the project that's
* being published
*/
accountId: string;
/**
* The name of the project to be published
*/
projectName: string;
/**
* Branch name to use. Defaults to production branch
*/
branch?: string;
/**
* Whether or not to skip local file upload result caching
*/
skipCaching?: boolean;
/**
* Commit message associated to deployment
*/
commitMessage?: string;
/**
* Commit hash associated to deployment
*/
commitHash?: string;
/**
* Whether or not the deployment should be considered to be
* in a dirty commit state
*/
commitDirty?: boolean;
/**
* Path to the project's functions directory. Default uses
* the current working directory + /functions since this is
* typically called in a CLI
*/
functionsDirectory?: string;

// TODO: Allow passing in the API key and plumb it through
// to the API calls so that the publish function does not
// rely on the `CLOUDFLARE_API_KEY` environment variable
}

/**
* Publish a directory to an account/project.
* NOTE: You will need the `CLOUDFLARE_API_KEY` environment
* variable set
*/
export async function publish({
directory,
accountId,
projectName,
branch,
skipCaching,
commitMessage,
commitHash,
commitDirty,
functionsDirectory: customFunctionsDirectory,
}: PagesPublishOptions) {
let _headers: string | undefined,
_redirects: string | undefined,
_routesGenerated: string | undefined,
_routesCustom: string | undefined,
_workerJS: string | undefined;

try {
_headers = readFileSync(join(directory, "_headers"), "utf-8");
} catch {}

try {
_redirects = readFileSync(join(directory, "_redirects"), "utf-8");
} catch {}

try {
/**
* Developers can specify a custom _routes.json file, for projects with Pages
* Functions or projects in Advanced Mode
*/
_routesCustom = readFileSync(join(directory, "_routes.json"), "utf-8");
} catch {}

try {
_workerJS = readFileSync(join(directory, "_worker.js"), "utf-8");
} catch {}

// Grab the bindings from the API, we need these for shims and other such hacky inserts
const project = await fetchResult<Project>(
`/accounts/${accountId}/pages/projects/${projectName}`
);
let isProduction = true;
if (branch) {
isProduction = project.production_branch === branch;
}

/**
* Evaluate if this is an Advanced Mode or Pages Functions project. If Advanced Mode, we'll
* go ahead and upload `_worker.js` as is, but if Pages Functions, we need to attempt to build
* Functions first and exit if it failed
*/
let builtFunctions: string | undefined = undefined;
const functionsDirectory =
customFunctionsDirectory || join(cwd(), "functions");
const routesOutputPath = !existsSync(join(directory, "_routes.json"))
? join(tmpdir(), `_routes-${Math.random()}.json`)
: undefined;

// Routing configuration displayed in the Functions tab of a deployment in Dash
let filepathRoutingConfig: string | undefined;

if (!_workerJS && existsSync(functionsDirectory)) {
const outfile = join(tmpdir(), `./functionsWorker-${Math.random()}.js`);
const outputConfigPath = join(
tmpdir(),
`functions-filepath-routing-config-${Math.random()}.json`
);

try {
await buildFunctions({
outfile,
outputConfigPath,
functionsDirectory,
onEnd: () => {},
buildOutputDirectory: dirname(outfile),
routesOutputPath,
local: false,
d1Databases: Object.keys(
project.deployment_configs[isProduction ? "production" : "preview"]
.d1_databases ?? {}
),
});

builtFunctions = readFileSync(outfile, "utf-8");
filepathRoutingConfig = readFileSync(outputConfigPath, "utf-8");
} catch (e) {
if (e instanceof FunctionsNoRoutesError) {
logger.warn(
getFunctionsNoRoutesWarning(functionsDirectory, "skipping")
);
} else {
throw e;
}
}
}

const manifest = await upload({
directory,
accountId,
projectName,
skipCaching: skipCaching ?? false,
});

const formData = new FormData();

formData.append("manifest", JSON.stringify(manifest));

if (branch) {
formData.append("branch", branch);
}

if (commitMessage) {
formData.append("commit_message", commitMessage);
}

if (commitHash) {
formData.append("commit_hash", commitHash);
}

if (commitDirty !== undefined) {
formData.append("commit_dirty", commitDirty);
}

if (_headers) {
formData.append("_headers", new File([_headers], "_headers"));
logger.log(`✨ Uploading _headers`);
}

if (_redirects) {
formData.append("_redirects", new File([_redirects], "_redirects"));
logger.log(`✨ Uploading _redirects`);
}

if (filepathRoutingConfig) {
formData.append(
"functions-filepath-routing-config.json",
new File(
[filepathRoutingConfig],
"functions-filepath-routing-config.json"
)
);
}

/**
* Advanced Mode
* https://developers.cloudflare.com/pages/platform/functions/#advanced-mode
*
* When using a _worker.js file, the entire /functions directory is ignored
* – this includes its routing and middleware characteristics.
*/
if (_workerJS) {
formData.append("_worker.js", new File([_workerJS], "_worker.js"));
logger.log(`✨ Uploading _worker.js`);

if (_routesCustom) {
// user provided a custom _routes.json file
try {
const routesCustomJSON = JSON.parse(_routesCustom);
validateRoutes(routesCustomJSON, join(directory, "_routes.json"));

formData.append(
"_routes.json",
new File([_routesCustom], "_routes.json")
);
logger.log(`✨ Uploading _routes.json`);
logger.warn(
`_routes.json is an experimental feature and is subject to change. Please use with care.`
);
} catch (err) {
if (err instanceof FatalError) {
throw err;
}
}
}
}

/**
* Pages Functions
* https://developers.cloudflare.com/pages/platform/functions/
*/
if (builtFunctions && !_workerJS) {
// if Functions were build successfully, proceed to uploading the build file
formData.append("_worker.js", new File([builtFunctions], "_worker.js"));
logger.log(`✨ Uploading Functions`);

if (_routesCustom) {
// user provided a custom _routes.json file
try {
const routesCustomJSON = JSON.parse(_routesCustom);
validateRoutes(routesCustomJSON, join(directory, "_routes.json"));

formData.append(
"_routes.json",
new File([_routesCustom], "_routes.json")
);
logger.log(`✨ Uploading _routes.json`);
logger.warn(
`_routes.json is an experimental feature and is subject to change. Please use with care.`
);
} catch (err) {
if (err instanceof FatalError) {
throw err;
}
}
} else if (routesOutputPath) {
// no custom _routes.json file found, so fallback to the generated one
try {
_routesGenerated = readFileSync(routesOutputPath, "utf-8");

if (_routesGenerated) {
formData.append(
"_routes.json",
new File([_routesGenerated], "_routes.json")
);
}
} catch {}
}
}

const deploymentResponse = await fetchResult<Deployment>(
`/accounts/${accountId}/pages/projects/${projectName}/deployments`,
{
method: "POST",
body: formData,
}
);
return deploymentResponse;
}
4 changes: 2 additions & 2 deletions packages/wrangler/src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import process from "process";
import { hideBin } from "yargs/helpers";
import { unstable_dev } from "./api";
import { unstable_dev, unstable_pages } from "./api";
import { FatalError } from "./errors";
import { main } from ".";

Expand All @@ -24,5 +24,5 @@ if (typeof jest === "undefined" && require.main === module) {
* It makes it possible to import wrangler from 'wrangler',
* and call wrangler.unstable_dev().
*/
export { unstable_dev };
export { unstable_dev, unstable_pages };
export type { UnstableDevWorker, UnstableDevOptions };

0 comments on commit 795bc16

Please sign in to comment.