Skip to content

Commit

Permalink
feat: implement [text_blobs]
Browse files Browse the repository at this point in the history
This implements support for `[text_blobs]` as defined by cloudflare/wrangler-legacy#1677.

Text blobs can be defined in service-worker format with configuration in `wrangler.toml` as -

```
[text_blobs]
MYTEXT = "./path/to/my-text.file"
```

The content of the file will then be available as the global `MYTEXT` inside your code. Note that this ONLY makes sense in service-worker format workers (for now).

Workers Sites now uses `[text_blobs]` internally. Previously, we were inlining the asset manifest into the worker itself, but we now attach the asset manifest to the uploaded worker. I also added an additional example of Workers Sites with a modules format worker.
  • Loading branch information
threepointone committed Feb 23, 2022
1 parent 201a6bb commit 6b04b66
Show file tree
Hide file tree
Showing 11 changed files with 342 additions and 27 deletions.
18 changes: 18 additions & 0 deletions .changeset/good-cats-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"wrangler": patch
---

feat: implement `[text_blobs]`

This implements support for `[text_blobs]` as defined by https://github.com/cloudflare/wrangler/pull/1677.

Text blobs can be defined in service-worker format with configuration in `wrangler.toml` as -

```
[text_blobs]
MYTEXT = "./path/to/my-text.file"
```

The content of the file will then be available as the global `MYTEXT` inside your code. Note that this ONLY makes sense in service-worker format workers (for now).

Workers Sites now uses `[text_blobs]` internally. Previously, we were inlining the asset manifest into the worker itself, but we now attach the asset manifest to the uploaded worker. I also added an additional example of Workers Sites with a modules format worker.
88 changes: 88 additions & 0 deletions packages/example-sites-app/src/modules.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import {
getAssetFromKV,
mapRequestToAsset,
} from "@cloudflare/kv-asset-handler";

import manifestJSON from "__STATIC_CONTENT_MANIFEST";
const assetManifest = JSON.parse(manifestJSON);

/**
* The DEBUG flag will do two things that help during development:
* 1. we will skip caching on the edge, which makes it easier to
* debug.
* 2. we will return an error message on exception in your Response rather
* than the default 404.html page.
*/
const DEBUG = false;

export default {
async fetch(request, env, ctx) {
let options = {
ASSET_NAMESPACE: env.__STATIC_CONTENT,
ASSET_MANIFEST: assetManifest,
};

/**
* You can add custom logic to how we fetch your assets
* by configuring the function `mapRequestToAsset`
*/
// options.mapRequestToAsset = handlePrefix(/^\/docs/)

try {
if (DEBUG) {
// customize caching
options.cacheControl = {
bypassCache: true,
};
}

const page = await getAssetFromKV(
{
request,
waitUntil(promise) {
return ctx.waitUntil(promise);
},
},
options
);

// allow headers to be altered
const response = new Response(page.body, page);

response.headers.set("X-XSS-Protection", "1; mode=block");
response.headers.set("X-Content-Type-Options", "nosniff");
response.headers.set("X-Frame-Options", "DENY");
response.headers.set("Referrer-Policy", "unsafe-url");
response.headers.set("Feature-Policy", "none");

return response;
} catch (e) {
// if an error is thrown try to serve the asset at 404.html
if (!DEBUG) {
try {
let notFoundResponse = await getAssetFromKV(
{
request,
waitUntil(promise) {
return ctx.waitUntil(promise);
},
},
{
ASSET_NAMESPACE: env.__STATIC_CONTENT,
ASSET_MANIFEST: assetManifest,
mapRequestToAsset: (req) =>
new Request(`${new URL(req.url).origin}/404.html`, req),
}
);

return new Response(notFoundResponse.body, {
...notFoundResponse,
status: 404,
});
} catch (e) {}
}

return new Response(e.message || e.toString(), { status: 500 });
}
},
};
File renamed without changes.
1 change: 1 addition & 0 deletions packages/wrangler/src/__tests__/dev.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ function renderDev({
durable_objects: { bindings: [] },
r2_buckets: [],
wasm_modules: {},
text_blobs: {},
unsafe: [],
},
public: publicDir,
Expand Down
122 changes: 120 additions & 2 deletions packages/wrangler/src/__tests__/publish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ export default{
expect(std.err).toMatchInlineSnapshot(`""`);
});

it("when using a service worker type, it should inline an asset manifest, and bind to a namespace", async () => {
it("when using a service worker type, it should add an asset manifest as a text_blob, and bind to a namespace", async () => {
const assets = [
{ filePath: "assets/file-1.txt", content: "Content of file-1" },
{ filePath: "assets/file-2.txt", content: "Content of file-2" },
Expand All @@ -620,13 +620,21 @@ export default{
writeAssets(assets);
mockUploadWorkerRequest({
expectedType: "sw",
expectedEntry: `const __STATIC_CONTENT_MANIFEST = {"file-1.txt":"assets/file-1.2ca234f380.txt","file-2.txt":"assets/file-2.5938485188.txt"};`,
expectedModules: {
__STATIC_CONTENT_MANIFEST:
'{"file-1.txt":"assets/file-1.2ca234f380.txt","file-2.txt":"assets/file-2.5938485188.txt"}',
},
expectedBindings: [
{
name: "__STATIC_CONTENT",
namespace_id: "__test-name-workers_sites_assets-id",
type: "kv_namespace",
},
{
name: "__STATIC_CONTENT_MANIFEST",
part: "__STATIC_CONTENT_MANIFEST",
type: "text_blob",
},
],
});
mockSubDomainRequest();
Expand Down Expand Up @@ -1617,6 +1625,116 @@ export default{
});
});

describe("[text_blobs]", () => {
it("should be able to define text blobs for service-worker format workers", async () => {
writeWranglerToml({
text_blobs: {
TESTTEXTBLOBNAME: "./path/to/text.file",
},
});
writeWorkerSource({ type: "sw" });
fs.mkdirSync("./path/to", { recursive: true });
fs.writeFileSync("./path/to/text.file", "SOME TEXT CONTENT");
mockUploadWorkerRequest({
expectedType: "sw",
expectedModules: { TESTTEXTBLOBNAME: "SOME TEXT CONTENT" },
expectedBindings: [
{
name: "TESTTEXTBLOBNAME",
part: "TESTTEXTBLOBNAME",
type: "text_blob",
},
],
});
mockSubDomainRequest();
await runWrangler("publish index.js");
expect(std.out).toMatchInlineSnapshot(`
"Uploaded
test-name
(TIMINGS)
Published
test-name
(TIMINGS)
test-name.test-sub-domain.workers.dev"
`);
expect(std.err).toMatchInlineSnapshot(`""`);
expect(std.warn).toMatchInlineSnapshot(`""`);
});

it("should error when defining text blobs for modules format workers", async () => {
writeWranglerToml({
text_blobs: {
TESTTEXTBLOBNAME: "./path/to/text.file",
},
});
writeWorkerSource({ type: "esm" });
fs.mkdirSync("./path/to", { recursive: true });
fs.writeFileSync("./path/to/text.file", "SOME TEXT CONTENT");

await expect(
runWrangler("publish index.js")
).rejects.toThrowErrorMatchingInlineSnapshot(
`"You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure \`[build.upload.rules]\` in your wrangler.toml"`
);
expect(std.out).toMatchInlineSnapshot(`""`);
expect(std.err).toMatchInlineSnapshot(`
"You cannot configure [text_blobs] with an ES module worker. Instead, import the file directly in your code, and optionally configure \`[build.upload.rules]\` in your wrangler.toml
%s
If you think this is a bug then please create an issue at https://github.com/cloudflare/wrangler2/issues/new."
`);
expect(std.warn).toMatchInlineSnapshot(`""`);
});

it("should resolve text blobs relative to the wrangler.toml file", async () => {
fs.mkdirSync("./path/to/and/the/path/to/", { recursive: true });
fs.writeFileSync(
"./path/to/wrangler.toml",
TOML.stringify({
compatibility_date: "2022-01-12",
name: "test-name",
text_blobs: {
TESTTEXTBLOBNAME: "./and/the/path/to/text.file",
},
}),

"utf-8"
);

writeWorkerSource({ type: "sw" });
fs.writeFileSync(
"./path/to/and/the/path/to/text.file",
"SOME TEXT CONTENT"
);
mockUploadWorkerRequest({
expectedType: "sw",
expectedModules: { TESTTEXTBLOBNAME: "SOME TEXT CONTENT" },
expectedBindings: [
{
name: "TESTTEXTBLOBNAME",
part: "TESTTEXTBLOBNAME",
type: "text_blob",
},
],
});
mockSubDomainRequest();
await runWrangler("publish index.js --config ./path/to/wrangler.toml");
expect(std.out).toMatchInlineSnapshot(`
"Uploaded
test-name
(TIMINGS)
Published
test-name
(TIMINGS)
test-name.test-sub-domain.workers.dev"
`);
expect(std.err).toMatchInlineSnapshot(`""`);
expect(std.warn).toMatchInlineSnapshot(`""`);
});
});

describe("vars bindings", () => {
it("should support json bindings", async () => {
writeWranglerToml({
Expand Down
37 changes: 33 additions & 4 deletions packages/wrangler/src/api/form_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface WorkerMetadata {
| { type: "plain_text"; name: string; text: string }
| { type: "json"; name: string; json: unknown }
| { type: "wasm_module"; name: string; part: string }
| { type: "text_blob"; name: string; part: string }
| {
type: "durable_object_namespace";
name: string;
Expand All @@ -54,14 +55,15 @@ export function toFormData(worker: CfWorkerInit): FormData {
const formData = new FormData();
const {
main,
modules,
bindings,
migrations,
usage_model,
compatibility_date,
compatibility_flags,
} = worker;

let { modules } = worker;

const metadataBindings: WorkerMetadata["bindings"] = [];

bindings.kv_namespaces?.forEach(({ id, binding }) => {
Expand Down Expand Up @@ -114,11 +116,28 @@ export function toFormData(worker: CfWorkerInit): FormData {
);
}

for (const [name, filePath] of Object.entries(bindings.text_blobs || {})) {
metadataBindings.push({
name,
type: "text_blob",
part: name,
});

if (name !== "__STATIC_CONTENT_MANIFEST") {
formData.set(
name,
new File([readFileSync(filePath)], filePath, {
type: "text/plain",
})
);
}
}

if (main.type === "commonjs") {
// This is a service-worker format worker.
// So we convert all `.wasm` modules into `wasm_module` bindings.
for (const [index, module] of Object.entries(modules || [])) {
for (const module of Object.values([...(modules || [])])) {
if (module.type === "compiled-wasm") {
// Convert all `.wasm` modules into `wasm_module` bindings.
// The "name" of the module is a file path. We use it
// to instead be a "part" of the body, and a reference
// that we can use inside our source. This identifier has to be a valid
Expand All @@ -139,7 +158,17 @@ export function toFormData(worker: CfWorkerInit): FormData {
})
);
// And then remove it from the modules collection
modules?.splice(parseInt(index, 10), 1);
modules = modules?.filter((m) => m !== module);
} else if (module.name === "__STATIC_CONTENT_MANIFEST") {
// Add the manifest to the form data.
formData.set(
module.name,
new File([module.content], module.name, {
type: "text/plain",
})
);
// And then remove it from the modules collection
modules = modules?.filter((m) => m !== module);
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions packages/wrangler/src/api/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ interface CfWasmModuleBindings {
[key: string]: string;
}

/**
* A binding to a text blob (in service worker format)
*/

interface CfTextBlobBindings {
[key: string]: string;
}

/**
* A Durable Object.
*/
Expand Down Expand Up @@ -145,6 +153,7 @@ export interface CfWorkerInit {
vars: CfVars | undefined;
kv_namespaces: CfKvNamespace[] | undefined;
wasm_modules: CfWasmModuleBindings | undefined;
text_blobs: CfTextBlobBindings | undefined;
durable_objects: { bindings: CfDurableObject[] } | undefined;
r2_buckets: CfR2Bucket[] | undefined;
unsafe: CfUnsafeBinding[] | undefined;
Expand Down

0 comments on commit 6b04b66

Please sign in to comment.