├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE
├── README.md
├── cli-webworker
└── worker.ts
├── cli
├── .vscode
│ └── settings.json
├── api_kv_namespace.ts
├── api_r2_bucket.ts
├── bundle.ts
├── cli.ts
├── cli_ae_proxy.ts
├── cli_analytics.ts
├── cli_analytics_durable_objects.ts
├── cli_analytics_r2.ts
├── cli_cfapi.ts
├── cli_command.ts
├── cli_common.ts
├── cli_d1.ts
├── cli_pubsub.ts
├── cli_pubsub_publish.ts
├── cli_pubsub_subscribe.ts
├── cli_push.ts
├── cli_push_deploy.ts
├── cli_push_lambda.ts
├── cli_push_supabase.ts
├── cli_r2.ts
├── cli_r2_abort_multipart_upload.ts
├── cli_r2_complete_multipart_upload.ts
├── cli_r2_copy_object.ts
├── cli_r2_create_bucket.ts
├── cli_r2_create_multipart_upload.ts
├── cli_r2_delete_bucket.ts
├── cli_r2_delete_bucket_encryption.ts
├── cli_r2_delete_object.ts
├── cli_r2_delete_objects.ts
├── cli_r2_generate_credentials.ts
├── cli_r2_get_bucket_encryption.ts
├── cli_r2_get_bucket_location.ts
├── cli_r2_get_head_object.ts
├── cli_r2_head_bucket.ts
├── cli_r2_list_buckets.ts
├── cli_r2_list_multipart_uploads.ts
├── cli_r2_list_objects.ts
├── cli_r2_list_objects_v1.ts
├── cli_r2_presign.ts
├── cli_r2_put_bucket_encryption.ts
├── cli_r2_put_large_object.ts
├── cli_r2_put_object.ts
├── cli_r2_upload_part.ts
├── cli_r2_upload_part_copy.ts
├── cli_serve.ts
├── cli_site.ts
├── cli_site_generate.ts
├── cli_site_serve.ts
├── cli_tail.ts
├── cli_version.ts
├── config_loader.ts
├── config_loader_test.ts
├── deno_bundle.ts
├── deno_check.ts
├── deno_graph.ts
├── deno_info.ts
├── deps_cli.ts
├── docker_cli.ts
├── docker_registry_api.ts
├── flag_parser.ts
├── flag_parser_test.ts
├── fs_util.ts
├── in_memory_r2_bucket.ts
├── in_memory_rpc_channels.ts
├── jsonc.ts
├── module_watcher.ts
├── repo_dir.ts
├── rpc_host_d1_database.ts
├── rpc_host_d1_database_test.ts
├── rpc_host_durable_object_storage.ts
├── rpc_host_durable_object_storage_test.ts
├── rpc_host_sockets.ts
├── rpc_host_web_sockets.ts
├── site
│ ├── design.html
│ ├── design.ts
│ ├── page.ts
│ ├── sidebar.ts
│ ├── site_config.schema.json
│ ├── site_config.ts
│ ├── site_config_validation.ts
│ ├── site_model.ts
│ └── toc.ts
├── spawn.ts
├── sqlite_d1_database.ts
├── sqlite_d1_database_test.ts
├── sqlite_dbpath_for_instance.ts
├── versions.ts
├── versions_test.ts
├── wasm_crypto.ts
└── worker_manager.ts
├── common
├── analytics
│ ├── cfgql_client.ts
│ ├── durable_objects_costs.ts
│ ├── graphql.ts
│ ├── r2_costs.ts
│ └── r2_costs_test.ts
├── aws
│ ├── aws_lambda.ts
│ ├── emailer.ts
│ ├── lambda_runtime.d.ts
│ ├── lambda_runtime.ts
│ └── lambda_runtime_zip.ts
├── bytes.ts
├── check.ts
├── cloudflare_api.ts
├── cloudflare_email.ts
├── cloudflare_pipeline_transform.ts
├── cloudflare_sockets.ts
├── cloudflare_sockets_deno_conn_factory.ts
├── cloudflare_workers_runtime.ts
├── cloudflare_workers_types.d.ts
├── config.schema.json
├── config.ts
├── config_validation.ts
├── console.ts
├── constants.ts
├── crypto_keys.ts
├── deno_workarounds.ts
├── denoflare_response.ts
├── deploy
│ ├── deno_deploy_app.ts
│ ├── deno_deploy_common_api.ts
│ ├── deno_deploy_dash_api.ts
│ └── deno_deploy_rest_api.ts
├── deps_xml.ts
├── fake_web_socket.ts
├── fetch_util.ts
├── import_binary.ts
├── import_text.ts
├── import_wasm.ts
├── incoming_request_cf_properties.ts
├── jwt.ts
├── local_durable_objects.ts
├── local_durable_objects_test.ts
├── local_web_sockets.ts
├── module_worker_execution.ts
├── mqtt
│ ├── README.md
│ ├── deno_tcp_connection.ts
│ ├── mod_deno.ts
│ ├── mod_iso.ts
│ ├── mqtt.ts
│ ├── mqtt_client.ts
│ ├── mqtt_connection.ts
│ ├── mqtt_messages.ts
│ └── web_socket_connection.ts
├── mutex.ts
├── noop_analytics_engine.ts
├── noop_cf_global_caches.ts
├── noop_d1_database.ts
├── noop_email_sender.ts
├── noop_queue.ts
├── oauth.ts
├── r2
│ ├── abort_multipart_upload.ts
│ ├── complete_multipart_upload.ts
│ ├── copy_object.ts
│ ├── create_bucket.ts
│ ├── create_multipart_upload.ts
│ ├── delete_bucket.ts
│ ├── delete_bucket_encryption.ts
│ ├── delete_object.ts
│ ├── delete_objects.ts
│ ├── get_bucket_encryption.ts
│ ├── get_bucket_location.ts
│ ├── get_head_object.ts
│ ├── head_bucket.ts
│ ├── known_element.ts
│ ├── list_buckets.ts
│ ├── list_multipart_uploads.ts
│ ├── list_objects.ts
│ ├── list_objects_v2.ts
│ ├── put_bucket_encryption.ts
│ ├── put_object.ts
│ ├── r2.ts
│ ├── upload_part.ts
│ └── upload_part_copy.ts
├── rpc_channel.ts
├── rpc_cloudflare_sockets.ts
├── rpc_fetch.ts
├── rpc_kv_namespace.ts
├── rpc_r2_bucket.ts
├── rpc_r2_model.ts
├── rpc_script.ts
├── rpc_stub_d1_database.ts
├── rpc_stub_durable_object_storage.ts
├── rpc_stub_web_sockets.ts
├── script_worker_execution.ts
├── sets.ts
├── sha1.ts
├── signal.ts
├── sleep.ts
├── storage
│ ├── durable_object_storage_test.ts
│ ├── in_memory_alarms.ts
│ ├── in_memory_durable_object_storage.ts
│ ├── sqlite_durable_object_storage.ts
│ ├── sqlite_durable_object_storage_test.ts
│ └── web_storage_durable_object_storage.ts
├── supabase
│ ├── eszip.ts
│ ├── supabase_api.ts
│ ├── supabase_app.ts
│ └── supabase_import_template.js
├── tail.ts
├── tail_connection.ts
├── tail_pretty.ts
├── uint8array_.ts
├── unimplemented_cloudflare_stubs.ts
├── uuid_v4.ts
├── worker_execution.ts
├── xml_parser.ts
├── xml_parser_test.ts
├── xml_util.ts
└── xml_util_test.ts
├── denoflare.code-workspace
├── devcli
├── deps.ts
├── devcli.ts
├── devcli_auth.ts
├── devcli_generate_npm.ts
├── devcli_pubsub.ts
├── devcli_r2.ts
├── devcli_regenerate_docs.ts
└── tsc.ts
├── examples
├── hello-wasm-worker
│ ├── deps.ts
│ ├── hello.ts
│ ├── hello.wasm
│ └── sub
│ │ ├── hello2.wasm
│ │ └── sub.ts
├── hello-worker
│ ├── deps.ts
│ └── hello.ts
├── image-demo-worker
│ ├── .vscode
│ │ └── settings.json
│ ├── 404.ts
│ ├── README.md
│ ├── app_manifest.d.ts
│ ├── cli.ts
│ ├── cli_transform_generator.ts
│ ├── deps_cli.ts
│ ├── deps_worker.ts
│ ├── ext
│ │ ├── photon_rs_bg.js
│ │ ├── photon_rs_bg.wasm
│ │ ├── pngs.ts
│ │ ├── pngs_bg.js
│ │ └── pngs_bg.wasm
│ ├── favicon.ico
│ ├── favicons.ts
│ ├── homepage.js
│ ├── homepage.ts
│ ├── html.ts
│ ├── img.ts
│ ├── theme.ts
│ ├── transforms.json
│ ├── transforms.ts
│ ├── twitter.ts
│ ├── twitter_image.png
│ ├── worker.ts
│ └── worker_env.d.ts
├── keyspace-worker
│ ├── deps.ts
│ ├── env.ts
│ ├── html.ts
│ └── worker.ts
├── mqtt-demo-worker
│ ├── deps.ts
│ ├── static
│ │ ├── mqtt-demo.html
│ │ └── mqtt-demo.js
│ └── worker.ts
├── multiplat-worker
│ ├── README.md
│ ├── deps.ts
│ ├── multiplat.ts
│ └── static
│ │ ├── deno.png
│ │ ├── example.txt
│ │ └── hello.wasm
├── r2-presigned-url-worker
│ ├── deps.ts
│ ├── worker.ts
│ └── worker_env.d.ts
├── r2-public-read-worker
│ ├── deps.ts
│ ├── listing.ts
│ ├── worker.ts
│ └── worker_env.d.ts
├── raw-worker
│ └── worker.ts
├── tiered-websockets-worker
│ ├── client.ts
│ ├── colo_from_trace.ts
│ ├── colo_tier_do.ts
│ ├── deps.ts
│ ├── globals.d.ts
│ ├── isolate_id.ts
│ ├── server.ts
│ ├── tiered_worker.ts
│ └── tiered_worker_env.d.ts
├── webtail-app
│ ├── .vscode
│ │ ├── settings.json
│ │ └── tasks.json
│ ├── app_constants.ts
│ ├── demo_mode.ts
│ ├── deno.jsonc
│ ├── deps_app.ts
│ ├── material.ts
│ ├── qps_controller.ts
│ ├── static_data.ts
│ ├── tail_controller.ts
│ ├── views
│ │ ├── analytics_view.ts
│ │ ├── circular_progress_view.ts
│ │ ├── console_view.ts
│ │ ├── filter_editor_view.ts
│ │ ├── header_view.ts
│ │ ├── icons.ts
│ │ ├── modal_view.ts
│ │ ├── profile_editor_view.ts
│ │ ├── sidebar_view.ts
│ │ └── welcome_panel.ts
│ ├── webtail_app.ts
│ └── webtail_app_vm.ts
└── webtail-worker
│ ├── app_manifest.d.ts
│ ├── cli.ts
│ ├── deps_cli.ts
│ ├── deps_worker.ts
│ ├── favicons.ts
│ ├── material.ts
│ ├── static
│ └── webtail_app.js
│ ├── twitter.ts
│ └── webtail_worker.ts
└── npm
└── denoflare-mqtt
├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── cjs
├── main.js
└── package.json
├── esm
├── main.js
└── package.json
├── main.d.ts
└── package.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: [ 'https://denoflare.dev/#support-this-project' ]
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Skymethod LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # denoflare
2 | Develop, test, and deploy Cloudflare Workers with Deno.
3 |
4 | ## Project documentation
5 | Docs content site: [https://denoflare.dev](https://denoflare.dev) (generated by `denoflare` itself, hosted on Cloudflare Pages)
6 |
7 | Docs content source repo: https://github.com/skymethod/denoflare-docs/
8 |
--------------------------------------------------------------------------------
/cli-webworker/worker.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { consoleLog } from '../common/console.ts';
4 | import { RpcChannel } from '../common/rpc_channel.ts';
5 | import { addRequestHandlerForRunScript } from '../common/rpc_script.ts';
6 |
7 | (function() {
8 | consoleLog('worker: start');
9 |
10 | const rpcChannel = new RpcChannel('worker', self.postMessage.bind(self));
11 | self.onmessage = async function(event) {
12 | if (await rpcChannel.receiveMessage(event.data)) return;
13 | consoleLog('worker: onmessage', event.data);
14 | };
15 | self.onmessageerror = function(event) {
16 | consoleLog('worker: onmessageerror', event);
17 | };
18 |
19 | addRequestHandlerForRunScript(rpcChannel);
20 |
21 | })();
22 |
--------------------------------------------------------------------------------
/cli/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.lint": true,
4 | "deno.unstable": true,
5 | "deno.suggest.imports.hosts": {
6 | "https://github.com": false,
7 | "https://deno.land": false
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/cli/cli.ts:
--------------------------------------------------------------------------------
1 | import { serve, SERVE_COMMAND } from './cli_serve.ts';
2 | import { tail, TAIL_COMMAND } from './cli_tail.ts';
3 | import { push, PUSH_COMMAND } from './cli_push.ts';
4 | import { pushLambda, PUSH_LAMBDA_COMMAND } from './cli_push_lambda.ts';
5 | import { site, SITE_COMMAND } from './cli_site.ts';
6 | import { parseFlags } from './flag_parser.ts';
7 | import { CLI_VERSION } from './cli_version.ts';
8 | import { analytics, ANALYTICS_COMMAND } from './cli_analytics.ts';
9 | import { cfapi, CFAPI_COMMAND } from './cli_cfapi.ts';
10 | import { r2, R2_COMMAND } from './cli_r2.ts';
11 | import { pubsub, PUBSUB_COMMAND } from './cli_pubsub.ts';
12 | import { CliCommand } from './cli_command.ts';
13 | import { denoflareCliCommand } from './cli_common.ts';
14 | import { d1, D1_COMMAND } from './cli_d1.ts';
15 | import { AE_PROXY_COMMAND, aeProxy } from './cli_ae_proxy.ts';
16 | import { PUSH_DEPLOY_COMMAND, pushDeploy } from './cli_push_deploy.ts';
17 | import { PUSH_SUPABASE_COMMAND, pushSupabase } from './cli_push_supabase.ts';
18 |
19 | const { args, options } = parseFlags(Deno.args);
20 |
21 | const VERSION_COMMAND = denoflareCliCommand('version', 'Dump cli version');
22 |
23 | export const DENOFLARE_COMMAND = CliCommand.of(['denoflare'], undefined, { version: CLI_VERSION })
24 | .subcommand(SERVE_COMMAND, serve)
25 | .subcommand(PUSH_COMMAND, push)
26 | .subcommand(PUSH_LAMBDA_COMMAND, pushLambda)
27 | .subcommand(PUSH_DEPLOY_COMMAND, pushDeploy)
28 | .subcommand(PUSH_SUPABASE_COMMAND, pushSupabase)
29 | .subcommand(TAIL_COMMAND, tail)
30 | .subcommand(SITE_COMMAND, site)
31 | .subcommand(ANALYTICS_COMMAND, analytics)
32 | .subcommand(CFAPI_COMMAND, cfapi)
33 | .subcommand(R2_COMMAND, r2)
34 | .subcommand(PUBSUB_COMMAND, pubsub)
35 | .subcommand(D1_COMMAND, d1)
36 | .subcommand(AE_PROXY_COMMAND, aeProxy)
37 | .subcommand(VERSION_COMMAND, () => console.log(CLI_VERSION))
38 | ;
39 |
40 | if (import.meta.main) {
41 | await DENOFLARE_COMMAND.routeSubcommand(args, options);
42 | }
43 |
--------------------------------------------------------------------------------
/cli/cli_analytics.ts:
--------------------------------------------------------------------------------
1 | import { denoflareCliCommand } from './cli_common.ts';
2 | import { analyticsDurableObjects, ANALYTICS_DURABLE_OBJECTS_COMMAND } from './cli_analytics_durable_objects.ts';
3 | import { analyticsR2, ANALYTICS_R2_COMMAND } from './cli_analytics_r2.ts';
4 |
5 | export const ANALYTICS_COMMAND = denoflareCliCommand('analytics', 'Dump stats via the Cloudflare GraphQL Analytics API')
6 | .subcommand(ANALYTICS_DURABLE_OBJECTS_COMMAND, analyticsDurableObjects)
7 | .subcommand(ANALYTICS_R2_COMMAND, analyticsR2)
8 | .docsLink('/cli/analytics')
9 | ;
10 |
11 | export async function analytics(args: (string | number)[], options: Record): Promise {
12 | await ANALYTICS_COMMAND.routeSubcommand(args, options);
13 | }
14 |
15 | export function dumpTable(rows: (string | number)[][], opts: { leftAlignColumns?: number[] } = {}) {
16 | const { leftAlignColumns = [] } = opts;
17 | const sizes: number[] = [];
18 | for (const row of rows) {
19 | for (let i = 0; i < row.length; i++) {
20 | const size = `${row[i]}`.length;
21 | sizes[i] = typeof sizes[i] === 'number' ? Math.max(sizes[i], size) : size;
22 | }
23 | }
24 | for (const row of rows) {
25 | const pieces = [];
26 | for (let i = 0; i < row.length; i++) {
27 | const size = sizes[i];
28 | const val = `${row[i]}`;
29 | const piece = leftAlignColumns.includes(i) ? val.padEnd(size, ' ') : val.padStart(size, ' ');
30 | pieces.push(piece);
31 | }
32 | console.log(pieces.join(' '));
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/cli/cli_pubsub_publish.ts:
--------------------------------------------------------------------------------
1 | import { Mqtt } from '../common/mqtt/mqtt.ts';
2 | import { MqttClient } from '../common/mqtt/mod_deno.ts';
3 | import { DISCONNECT } from '../common/mqtt/mqtt_messages.ts';
4 | import { denoflareCliCommand } from './cli_common.ts';
5 | import { commandOptionsForPubsub, parseCloudflareEndpoint, parsePubsubOptions } from './cli_pubsub.ts';
6 |
7 | export const PUBLISH_COMMAND = denoflareCliCommand(['pubsub', 'publish'], 'Publish a message to a Pub/Sub broker')
8 | .option('text', 'string', 'Plaintext message payload')
9 | .option('file', 'string', 'Path to file with the message payload')
10 | .option('topic', 'required-string', 'Topic on which to publish the message')
11 | .option('n', 'integer', 'Times to repeat the message')
12 | .option('maxMessagesPerSecond', 'integer', 'Maximum rate of message to send per second')
13 | .include(commandOptionsForPubsub())
14 | .docsLink('/cli/pubsub#publish')
15 | ;
16 |
17 | export async function publish(args: (string | number)[], options: Record) {
18 | if (PUBLISH_COMMAND.dumpHelp(args, options)) return;
19 |
20 | const { verbose, text, file, topic, n, maxMessagesPerSecond = 10 } = PUBLISH_COMMAND.parse(args, options); // 10 = current beta limit: https://developers.cloudflare.com/pub-sub/platform/limits/
21 |
22 | if ((text ?? file) === undefined) throw new Error(`Specify a payload with --text or --file`);
23 | const basePayload = text ? text : await Deno.readFile(file!);
24 |
25 | if (verbose) {
26 | Mqtt.DEBUG = true;
27 | }
28 |
29 | const { endpoint, clientId, password, keepAlive, debugMessages } = await parsePubsubOptions(options);
30 |
31 | const { hostname, port, protocol } = parseCloudflareEndpoint(endpoint);
32 |
33 | const client = new MqttClient({ hostname, port, protocol, maxMessagesPerSecond });
34 | client.onMqttMessage = message => {
35 | if (debugMessages) console.log(JSON.stringify(message, undefined, 2));
36 | if (message.type === DISCONNECT) {
37 | console.log('disconnect', message.reason);
38 | }
39 | };
40 |
41 | console.log('connecting');
42 | await client.connect({ clientId, password, keepAlive });
43 | {
44 | const { clientId, keepAlive } = client;
45 | console.log('connected', { clientId, keepAlive });
46 | }
47 |
48 | for (let i = 0; i < (n ?? 1); i++) {
49 | console.log(`publishing${n ? ` ${i + 1} of ${n}` : ''}`);
50 | let payload = basePayload;
51 | if (n !== undefined && typeof payload === 'string' && payload.includes('$n')) {
52 | payload = payload.replace('$n', String(i + 1));
53 | }
54 | await client.publish({ topic, payload });
55 | }
56 |
57 | console.log('disconnecting');
58 | await client.disconnect();
59 |
60 | console.log('disconnected');
61 | }
62 |
--------------------------------------------------------------------------------
/cli/cli_pubsub_subscribe.ts:
--------------------------------------------------------------------------------
1 | import { Bytes } from '../common/bytes.ts';
2 | import { Mqtt } from '../common/mqtt/mqtt.ts';
3 | import { MqttClient } from '../common/mqtt/mod_deno.ts';
4 | import { DISCONNECT } from '../common/mqtt/mqtt_messages.ts';
5 | import { denoflareCliCommand } from './cli_common.ts';
6 | import { commandOptionsForPubsub, parseCloudflareEndpoint, parsePubsubOptions } from './cli_pubsub.ts';
7 |
8 | export const SUBSCRIBE_COMMAND = denoflareCliCommand(['pubsub', 'subscribe'], 'Subscribe to a Pub/Sub broker')
9 | .option('topic', 'required-string', 'Topic on which to subscribe')
10 | .include(commandOptionsForPubsub())
11 | .docsLink('/cli/pubsub#subscribe')
12 | ;
13 |
14 | export async function subscribe(args: (string | number)[], options: Record) {
15 | if (SUBSCRIBE_COMMAND.dumpHelp(args, options)) return;
16 |
17 | const { verbose, topic } = SUBSCRIBE_COMMAND.parse(args, options);
18 |
19 | if (verbose) {
20 | Mqtt.DEBUG = true;
21 | }
22 |
23 | const { endpoint, clientId, password, keepAlive, debugMessages } = await parsePubsubOptions(options);
24 |
25 | const { hostname, port, protocol } = parseCloudflareEndpoint(endpoint);
26 |
27 | const client = new MqttClient({ hostname, port, protocol });
28 | client.onMqttMessage = message => {
29 | if (debugMessages) console.log(JSON.stringify(message, undefined, 2));
30 | if (message.type === DISCONNECT) {
31 | console.log('disconnect', message.reason);
32 | }
33 | };
34 |
35 | client.onReceive = opts => {
36 | const { topic, payload, contentType } = opts;
37 | const display = typeof payload === 'string' ? payload : `(${payload.length} bytes)${payload.length < 1000 ? ` ${new Bytes(payload).utf8()}` : ''}`;
38 | console.log(`[topic: ${topic}]${contentType ? ` [content-type: ${contentType}]` : ''} ${display}`);
39 | };
40 |
41 | console.log('connecting');
42 | await client.connect({ clientId, password, keepAlive });
43 | {
44 | const { clientId, keepAlive } = client;
45 | console.log('connected', { clientId, keepAlive });
46 | }
47 |
48 | console.log('subscribing');
49 | await client.subscribe({ topicFilter: topic });
50 |
51 | console.log('listening');
52 | await client.completion();
53 | console.log('completed');
54 | }
55 |
--------------------------------------------------------------------------------
/cli/cli_r2_abort_multipart_upload.ts:
--------------------------------------------------------------------------------
1 | import { abortMultipartUpload as abortMultipartUploadR2, R2 } from '../common/r2/r2.ts';
2 | import { denoflareCliCommand } from './cli_common.ts';
3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
4 |
5 | export const ABORT_MULTIPART_UPLOAD_COMMAND = denoflareCliCommand(['r2', 'abort-multipart-upload'], 'Abort an existing multipart upload')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .arg('key', 'string', 'Key of the object')
8 | .arg('uploadId', 'string', 'Id of the existing multipart upload to abort')
9 | .include(commandOptionsForR2())
10 | .docsLink('/cli/r2#abort-multipart-upload')
11 | ;
12 |
13 | export async function abortMultipartUpload(args: (string | number)[], options: Record) {
14 | if (ABORT_MULTIPART_UPLOAD_COMMAND.dumpHelp(args, options)) return;
15 |
16 | const { bucket, key, uploadId, verbose } = ABORT_MULTIPART_UPLOAD_COMMAND.parse(args, options);
17 |
18 | if (verbose) {
19 | R2.DEBUG = true;
20 | }
21 |
22 | const { origin, region, context, urlStyle } = await loadR2Options(options);
23 |
24 | await abortMultipartUploadR2({ bucket, key, uploadId, origin, region, urlStyle }, context);
25 | }
26 |
--------------------------------------------------------------------------------
/cli/cli_r2_complete_multipart_upload.ts:
--------------------------------------------------------------------------------
1 | import { CompletedPart, completeMultipartUpload as completeMultipartUploadR2, R2 } from '../common/r2/r2.ts';
2 | import { CliStats, denoflareCliCommand } from './cli_common.ts';
3 | import { commandOptionsForR2, loadR2Options, surroundWithDoubleQuotesIfNecessary } from './cli_r2.ts';
4 |
5 | export const COMPLETE_MULTIPART_UPLOAD_COMMAND = denoflareCliCommand(['r2', 'complete-multipart-upload'], 'Complete an existing multipart upload')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .arg('key', 'string', 'Key of the object')
8 | .arg('uploadId', 'string', 'Id of the existing multipart upload to complete')
9 | .arg('part', 'strings', 'partNumber:etag')
10 | .include(commandOptionsForR2())
11 | .docsLink('/cli/r2#complete-multipart-upload')
12 | ;
13 |
14 | export async function completeMultipartUpload(args: (string | number)[], options: Record) {
15 | if (COMPLETE_MULTIPART_UPLOAD_COMMAND.dumpHelp(args, options)) return;
16 |
17 | const { bucket, key, uploadId, part: partSpecs, verbose } = COMPLETE_MULTIPART_UPLOAD_COMMAND.parse(args, options);
18 |
19 | if (verbose) {
20 | R2.DEBUG = true;
21 | }
22 |
23 | const parts = partSpecs.map(parsePartSpec);
24 |
25 | const { origin, region, context, urlStyle } = await loadR2Options(options);
26 |
27 | const result = await completeMultipartUploadR2({ bucket, key, uploadId, parts, origin, region, urlStyle }, context);
28 | console.log(JSON.stringify(result, undefined, 2));
29 |
30 | const millis = Date.now() - CliStats.launchTime;
31 | console.log(`completed in ${millis}ms`);
32 | }
33 |
34 | //
35 |
36 | function parsePartSpec(partSpec: unknown): CompletedPart {
37 | if (typeof partSpec !== 'string') throw new Error(`Invalid part: ${partSpec}`);
38 | const [ partNumberStr, etagStr ] = partSpec.split(':');
39 | if (typeof partNumberStr !== 'string' || typeof etagStr !== 'string') throw new Error(`Invalid part: ${partSpec}`);
40 | const partNumber = parseInt(partNumberStr);
41 | const etag = surroundWithDoubleQuotesIfNecessary(etagStr)!;
42 | return { partNumber, etag };
43 | }
44 |
--------------------------------------------------------------------------------
/cli/cli_r2_create_bucket.ts:
--------------------------------------------------------------------------------
1 | import { createBucket as createBucketR2, R2 } from '../common/r2/r2.ts';
2 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
3 | import { denoflareCliCommand } from './cli_common.ts';
4 |
5 | export const CREATE_BUCKET_COMMAND = denoflareCliCommand(['r2', 'create-bucket'], 'Create a new R2 bucket')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .include(commandOptionsForR2())
8 | .docsLink('/cli/r2#create-bucket')
9 | ;
10 |
11 | export async function createBucket(args: (string | number)[], options: Record) {
12 | if (CREATE_BUCKET_COMMAND.dumpHelp(args, options)) return;
13 |
14 | const { verbose, bucket } = CREATE_BUCKET_COMMAND.parse(args, options);
15 |
16 | if (verbose) {
17 | R2.DEBUG = true;
18 | }
19 |
20 | const { origin, region, context, urlStyle } = await loadR2Options(options);
21 |
22 | const { location } = await createBucketR2({ bucket, origin, region, urlStyle }, context);
23 | console.log(location);
24 | }
25 |
--------------------------------------------------------------------------------
/cli/cli_r2_create_multipart_upload.ts:
--------------------------------------------------------------------------------
1 | import { createMultipartUpload as createMultipartUploadR2, R2 } from '../common/r2/r2.ts';
2 | import { CliStats, denoflareCliCommand } from './cli_common.ts';
3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
4 |
5 | export const CREATE_MULTIPART_UPLOAD_COMMAND = denoflareCliCommand(['r2', 'create-multipart-upload'], 'Start a multipart upload and return an upload ID')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .arg('key', 'string', 'Key of the object to upload')
8 | .option('cacheControl', 'string', 'Specify caching behavior along the request/reply chain')
9 | .option('contentDisposition', 'string', 'Specify presentational information for the object')
10 | .option('contentEncoding', 'string', 'Specify what content encodings have been applied to the object')
11 | .option('contentLanguage', 'string', 'Specify the language the object is in')
12 | .option('contentType', 'string', 'A standard MIME type describing the format of the contents')
13 | .option('expires', 'string', 'The date and time at which the object is no longer cacheable')
14 | .option('custom', 'name-value-pairs', 'Custom metadata for the object')
15 | .option('ifMatch', 'string', 'Put the object only if its entity tag (ETag) is the same as the one specified')
16 | .option('ifNoneMatch', 'string', 'Put the object only if its entity tag (ETag) is different from the one specified')
17 | .option('ifModifiedSince', 'string', 'Put the object only if it has been modified since the specified time')
18 | .option('ifUnmodifiedSince', 'string', 'Put the object only if it has not been modified since the specified time')
19 | .include(commandOptionsForR2())
20 | .docsLink('/cli/r2#create-multipart-upload')
21 | ;
22 |
23 | export async function createMultipartUpload(args: (string | number)[], options: Record) {
24 | if (CREATE_MULTIPART_UPLOAD_COMMAND.dumpHelp(args, options)) return;
25 |
26 | const { bucket, key, verbose, cacheControl, contentDisposition, contentEncoding, contentLanguage, contentType, expires, custom: customMetadata, ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince } = CREATE_MULTIPART_UPLOAD_COMMAND.parse(args, options);
27 |
28 | if (verbose) {
29 | R2.DEBUG = true;
30 | }
31 |
32 | const { origin, region, context, urlStyle } = await loadR2Options(options);
33 |
34 | const result = await createMultipartUploadR2({
35 | bucket, key, origin, region, urlStyle, cacheControl, contentDisposition, contentEncoding, contentLanguage, expires, contentType, customMetadata, ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince
36 | }, context);
37 | console.log(JSON.stringify(result, undefined, 2));
38 |
39 | const millis = Date.now() - CliStats.launchTime;
40 | console.log(`copied in ${millis}ms`);
41 | }
42 |
--------------------------------------------------------------------------------
/cli/cli_r2_delete_bucket.ts:
--------------------------------------------------------------------------------
1 | import { deleteBucket as deleteBucketR2, R2 } from '../common/r2/r2.ts';
2 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
3 | import { denoflareCliCommand } from './cli_common.ts';
4 |
5 | export const DELETE_BUCKET_COMMAND = denoflareCliCommand(['r2', 'delete-bucket'], 'Delete an R2 bucket')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .include(commandOptionsForR2())
8 | .docsLink('/cli/r2#delete-bucket')
9 | ;
10 |
11 | export async function deleteBucket(args: (string | number)[], options: Record) {
12 | if (DELETE_BUCKET_COMMAND.dumpHelp(args, options)) return;
13 |
14 | const { verbose, bucket } = DELETE_BUCKET_COMMAND.parse(args, options);
15 |
16 | if (verbose) {
17 | R2.DEBUG = true;
18 | }
19 |
20 | const { origin, region, context, urlStyle } = await loadR2Options(options);
21 |
22 | await deleteBucketR2({ bucket, origin, region, urlStyle }, context);
23 | }
24 |
--------------------------------------------------------------------------------
/cli/cli_r2_delete_bucket_encryption.ts:
--------------------------------------------------------------------------------
1 | import { deleteBucketEncryption as deleteBucketEncryptionR2, R2 } from '../common/r2/r2.ts';
2 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
3 | import { denoflareCliCommand } from './cli_common.ts';
4 |
5 | export const DELETE_BUCKET_ENCRYPTION_COMMAND = denoflareCliCommand(['r2', 'delete-bucket-encryption'], 'Reset encryption config for a bucket')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .include(commandOptionsForR2())
8 | .docsLink('/cli/r2#delete-bucket-encryption')
9 | ;
10 |
11 | export async function deleteBucketEncryption(args: (string | number)[], options: Record) {
12 | if (DELETE_BUCKET_ENCRYPTION_COMMAND.dumpHelp(args, options)) return;
13 |
14 | const { verbose, bucket } = DELETE_BUCKET_ENCRYPTION_COMMAND.parse(args, options);
15 |
16 | if (verbose) {
17 | R2.DEBUG = true;
18 | }
19 |
20 | const { origin, region, context, urlStyle } = await loadR2Options(options);
21 |
22 | const result = await deleteBucketEncryptionR2({ bucket, origin, region, urlStyle }, context);
23 | console.log(JSON.stringify(result, undefined, 2));
24 | }
25 |
--------------------------------------------------------------------------------
/cli/cli_r2_delete_object.ts:
--------------------------------------------------------------------------------
1 | import { deleteObject as deleteObjectR2, R2 } from '../common/r2/r2.ts';
2 | import { denoflareCliCommand, parseOptionalStringOption } from './cli_common.ts';
3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
4 |
5 | export const DELETE_OBJECT_COMMAND = denoflareCliCommand(['r2', 'delete-object'], 'Delete R2 object for a given key')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .arg('key', 'string', 'Key of the object to delete')
8 | .option('versionId', 'string', 'Returns the version ID of the delete marker created as a result of the DELETE operation')
9 | .include(commandOptionsForR2())
10 | .docsLink('/cli/r2#delete-object')
11 | ;
12 |
13 | export async function deleteObject(args: (string | number)[], options: Record) {
14 | if (DELETE_OBJECT_COMMAND.dumpHelp(args, options)) return;
15 |
16 | const { bucket, key, verbose } = DELETE_OBJECT_COMMAND.parse(args, options);
17 |
18 | if (verbose) {
19 | R2.DEBUG = true;
20 | }
21 |
22 | const versionId = parseOptionalStringOption('version-id', options);
23 |
24 | const { origin, region, context, urlStyle } = await loadR2Options(options);
25 |
26 | await deleteObjectR2({ bucket, key, origin, region, versionId, urlStyle }, context);
27 | }
28 |
--------------------------------------------------------------------------------
/cli/cli_r2_delete_objects.ts:
--------------------------------------------------------------------------------
1 | import { deleteObjects as deleteObjectsR2, R2 } from '../common/r2/r2.ts';
2 | import { denoflareCliCommand } from './cli_common.ts';
3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
4 |
5 | export const DELETE_OBJECTS_COMMAND = denoflareCliCommand(['r2', 'delete-objects'], 'Delete R2 objects for the given keys')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .arg('key', 'strings', 'Keys of the objects to delete')
8 | .option('quiet', 'boolean', 'Enable quiet mode, response will only include keys where the delete action encountered an error')
9 | .include(commandOptionsForR2())
10 | .docsLink('/cli/r2#delete-objects')
11 | ;
12 |
13 | export async function deleteObjects(args: (string | number)[], options: Record) {
14 | if (DELETE_OBJECTS_COMMAND.dumpHelp(args, options)) return;
15 |
16 | const { bucket, key: items, verbose, quiet } = DELETE_OBJECTS_COMMAND.parse(args, options);
17 |
18 | if (verbose) {
19 | R2.DEBUG = true;
20 | }
21 |
22 | const { origin, region, context, urlStyle } = await loadR2Options(options);
23 |
24 | const result = await deleteObjectsR2({ bucket, items, origin, region, quiet, urlStyle }, context);
25 | console.log(JSON.stringify(result, undefined, 2));
26 | }
27 |
--------------------------------------------------------------------------------
/cli/cli_r2_generate_credentials.ts:
--------------------------------------------------------------------------------
1 | import { Bytes } from '../common/bytes.ts';
2 | import { denoflareCliCommand } from './cli_common.ts';
3 |
4 | export const GENERATE_CREDENTIALS_COMMAND = denoflareCliCommand(['r2', 'generate-credentials'], 'Generate private R2-looking credentials for any use')
5 | .docsLink('/cli/r2#generate-credentials')
6 | ;
7 |
8 | export function generateCredentials(args: (string | number)[], options: Record) {
9 | if (GENERATE_CREDENTIALS_COMMAND.dumpHelp(args, options)) return;
10 |
11 | GENERATE_CREDENTIALS_COMMAND.parse(args, options);
12 |
13 | const accessKeyId = new Bytes(crypto.getRandomValues(new Uint8Array(16))).hex();
14 | const secretAccessKey = new Bytes(crypto.getRandomValues(new Uint8Array(32))).hex();
15 | console.log(JSON.stringify({ accessKeyId, secretAccessKey }, undefined, 2));
16 | }
17 |
--------------------------------------------------------------------------------
/cli/cli_r2_get_bucket_encryption.ts:
--------------------------------------------------------------------------------
1 | import { getBucketEncryption as getBucketEncryptionR2, R2 } from '../common/r2/r2.ts';
2 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
3 | import { denoflareCliCommand } from './cli_common.ts';
4 |
5 | export const GET_BUCKET_ENCRYPTION_COMMAND = denoflareCliCommand(['r2', 'get-bucket-encryption'], 'Gets encryption config for a bucket')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .include(commandOptionsForR2())
8 | .docsLink('/cli/r2#get-bucket-encryption')
9 | ;
10 |
11 | export async function getBucketEncryption(args: (string | number)[], options: Record) {
12 | if (GET_BUCKET_ENCRYPTION_COMMAND.dumpHelp(args, options)) return;
13 |
14 | const { verbose, bucket } = GET_BUCKET_ENCRYPTION_COMMAND.parse(args, options);
15 |
16 | if (verbose) {
17 | R2.DEBUG = true;
18 | }
19 |
20 | const { origin, region, context, urlStyle } = await loadR2Options(options);
21 |
22 | const result = await getBucketEncryptionR2({ bucket, origin, region, urlStyle }, context);
23 | console.log(JSON.stringify(result, undefined, 2));
24 | }
25 |
--------------------------------------------------------------------------------
/cli/cli_r2_get_bucket_location.ts:
--------------------------------------------------------------------------------
1 | import { getBucketLocation as getBucketLocationR2, R2 } from '../common/r2/r2.ts';
2 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
3 | import { denoflareCliCommand } from './cli_common.ts';
4 |
5 | export const GET_BUCKET_LOCATION_COMMAND = denoflareCliCommand(['r2', 'get-bucket-location'], 'Returns the region the bucket resides in')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .include(commandOptionsForR2())
8 | .docsLink('/cli/r2#get-bucket-location')
9 | ;
10 |
11 | export async function getBucketLocation(args: (string | number)[], options: Record) {
12 | if (GET_BUCKET_LOCATION_COMMAND.dumpHelp(args, options)) return;
13 |
14 | const { verbose, bucket } = GET_BUCKET_LOCATION_COMMAND.parse(args, options);
15 |
16 | if (verbose) {
17 | R2.DEBUG = true;
18 | }
19 |
20 | const { origin, region, context, urlStyle } = await loadR2Options(options);
21 |
22 | const result = await getBucketLocationR2({ bucket, origin, region, urlStyle }, context);
23 | console.log(JSON.stringify(result, undefined, 2));
24 | }
25 |
--------------------------------------------------------------------------------
/cli/cli_r2_head_bucket.ts:
--------------------------------------------------------------------------------
1 | import { computeHeadersString, headBucket as headBucketR2, R2 } from '../common/r2/r2.ts';
2 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
3 | import { denoflareCliCommand } from './cli_common.ts';
4 |
5 | export const HEAD_BUCKET_COMMAND = denoflareCliCommand(['r2', 'head-bucket'], 'Determine if an R2 bucket exists')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .include(commandOptionsForR2())
8 | .docsLink('/cli/r2#head-bucket')
9 | ;
10 |
11 | export async function headBucket(args: (string | number)[], options: Record) {
12 | if (HEAD_BUCKET_COMMAND.dumpHelp(args, options)) return;
13 |
14 | const { verbose, bucket } = HEAD_BUCKET_COMMAND.parse(args, options);
15 |
16 | if (verbose) {
17 | R2.DEBUG = true;
18 | }
19 |
20 | const { origin, region, context, urlStyle } = await loadR2Options(options);
21 |
22 | const response = await headBucketR2({ bucket, origin, region, urlStyle }, context);
23 | console.log(`${response.status} ${computeHeadersString(response.headers)}`);
24 | }
25 |
--------------------------------------------------------------------------------
/cli/cli_r2_list_buckets.ts:
--------------------------------------------------------------------------------
1 | import { listBuckets as listBucketsR2, R2 } from '../common/r2/r2.ts';
2 | import { denoflareCliCommand } from './cli_common.ts';
3 | import { loadR2Options, commandOptionsForR2 } from './cli_r2.ts';
4 |
5 | export const LIST_BUCKETS_COMMAND = denoflareCliCommand(['r2', 'list-buckets'], 'List all R2 buckets')
6 | .include(commandOptionsForR2({ hideUrlStyle: true }))
7 | .docsLink('/cli/r2#list-buckets')
8 | ;
9 |
10 | export async function listBuckets(args: (string | number)[], options: Record) {
11 | if (LIST_BUCKETS_COMMAND.dumpHelp(args, options)) return;
12 |
13 | const { verbose } = LIST_BUCKETS_COMMAND.parse(args, options);
14 |
15 | if (verbose) {
16 | R2.DEBUG = true;
17 | }
18 |
19 | const { origin, region, context } = await loadR2Options(options);
20 |
21 | const result = await listBucketsR2({ origin, region }, context);
22 | console.log(JSON.stringify(result, undefined, 2));
23 | }
24 |
--------------------------------------------------------------------------------
/cli/cli_r2_list_multipart_uploads.ts:
--------------------------------------------------------------------------------
1 | import { listMultipartUploads as listMultipartUploadsR2, R2 } from '../common/r2/r2.ts';
2 | import { loadR2Options, commandOptionsForR2 } from './cli_r2.ts';
3 | import { denoflareCliCommand } from './cli_common.ts';
4 |
5 | export const LIST_MULTIPART_UPLOADS_COMMAND = denoflareCliCommand(['r2', 'list-multipart-uploads'], 'List in-progress multipart uploads within an R2 bucket')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .option('delimiter', 'string', 'The character used to group keys', { hint: 'char' })
8 | .option('encodingType', 'enum', 'Encoding used to encode keys in the response', { value: 'url', description: 'Url encoding' })
9 | .option('keyMarker', 'string', 'Together with upload-id-marker, specifies the multipart upload after which listing should begin')
10 | .option('maxUploads', 'integer', 'Limit the number of multipart uploads to return', { min: 0, max: 1000 })
11 | .option('prefix', 'string', 'Limit to uploads for keys that begin with the specified prefix')
12 | .option('uploadIdMarker', 'string', 'Together with key-marker, specifies the upload after which listing should begin')
13 | .include(commandOptionsForR2())
14 | .docsLink('/cli/r2#list-multipart-uploads')
15 | ;
16 |
17 | export async function listMultipartUploads(args: (string | number)[], options: Record) {
18 | if (LIST_MULTIPART_UPLOADS_COMMAND.dumpHelp(args, options)) return;
19 |
20 | const { bucket, verbose, delimiter, encodingType, keyMarker, maxUploads, prefix, uploadIdMarker } = LIST_MULTIPART_UPLOADS_COMMAND.parse(args, options);
21 |
22 | if (verbose) {
23 | R2.DEBUG = true;
24 | }
25 |
26 | const { origin, region, context, urlStyle } = await loadR2Options(options);
27 |
28 | const result = await listMultipartUploadsR2({ bucket, origin, region, delimiter, encodingType, keyMarker, maxUploads, prefix, uploadIdMarker, urlStyle }, context);
29 | console.log(JSON.stringify(result, undefined, 2));
30 | }
31 |
--------------------------------------------------------------------------------
/cli/cli_r2_list_objects.ts:
--------------------------------------------------------------------------------
1 | import { listObjectsV2, R2 } from '../common/r2/r2.ts';
2 | import { loadR2Options, commandOptionsForR2 } from './cli_r2.ts';
3 | import { denoflareCliCommand } from './cli_common.ts';
4 |
5 | export const LIST_OBJECTS_COMMAND = denoflareCliCommand(['r2', 'list-objects'], 'List objects within an R2 bucket (list-objects-v2)')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .option('maxKeys', 'integer', 'Limit the number of keys to return', { min: 0, max: 1000 })
8 | .option('continuationToken', 'string', 'Continue the listing on this bucket with a previously returned token (token is obfuscated and is not a real key)')
9 | .option('startAfter', 'string', 'Start listing after this specified key, can be any key in the bucket', { hint: 'key' })
10 | .option('prefix', 'string', 'Limit to keys that begin with the specified prefix')
11 | .option('delimiter', 'string', 'The character used to group keys', { hint: 'char' })
12 | .option('encodingType', 'enum', 'Encoding used to encode keys in the response', { value: 'url', description: 'Url encoding' })
13 | .option('fetchOwner', 'boolean', 'If set, return the owner info for each item')
14 | .include(commandOptionsForR2())
15 | .docsLink('/cli/r2#list-objects')
16 | ;
17 |
18 | export async function listObjects(args: (string | number)[], options: Record) {
19 | if (LIST_OBJECTS_COMMAND.dumpHelp(args, options)) return;
20 |
21 | const { bucket, verbose, maxKeys, continuationToken, startAfter, prefix, delimiter, encodingType, fetchOwner } = LIST_OBJECTS_COMMAND.parse(args, options);
22 |
23 | if (verbose) {
24 | R2.DEBUG = true;
25 | }
26 |
27 | const { origin, region, context, urlStyle } = await loadR2Options(options);
28 |
29 | const result = await listObjectsV2({ bucket, origin, region, maxKeys, continuationToken, delimiter, prefix, startAfter, encodingType, fetchOwner, urlStyle }, context);
30 | console.log(JSON.stringify(result, undefined, 2));
31 | }
32 |
--------------------------------------------------------------------------------
/cli/cli_r2_list_objects_v1.ts:
--------------------------------------------------------------------------------
1 | import { listObjects, R2 } from '../common/r2/r2.ts';
2 | import { denoflareCliCommand } from './cli_common.ts';
3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
4 |
5 | export const LIST_OBJECTS_V1_COMMAND = denoflareCliCommand(['r2', 'list-objects-v1'], 'List objects within a bucket (deprecated v1 version)')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .option('maxKeys', 'integer', 'Limit the number of keys to return', { min: 0, max: 1000 })
8 | .option('marker', 'string', 'Start listing after this specified key, can be any key in the bucket')
9 | .option('prefix', 'string', 'Limit to keys that begin with the specified prefix')
10 | .option('delimiter', 'string', 'The character used to group keys', { hint: 'char' })
11 | .option('encodingType', 'enum', 'Encoding used to encode keys in the response', { value: 'url', description: 'Url encoding' })
12 | .include(commandOptionsForR2())
13 | .docsLink('/cli/r2#list-objects-v1')
14 | ;
15 |
16 | export async function listObjectsV1(args: (string | number)[], options: Record) {
17 | if (LIST_OBJECTS_V1_COMMAND.dumpHelp(args, options)) return;
18 |
19 | const { bucket, verbose, maxKeys, marker, prefix, delimiter, encodingType } = LIST_OBJECTS_V1_COMMAND.parse(args, options);
20 |
21 | if (verbose) {
22 | R2.DEBUG = true;
23 | }
24 |
25 | const { origin, region, context, urlStyle } = await loadR2Options(options);
26 |
27 | const result = await listObjects({ bucket, origin, region, urlStyle, maxKeys, marker, delimiter, prefix, encodingType }, context);
28 | console.log(JSON.stringify(result, undefined, 2));
29 | }
30 |
--------------------------------------------------------------------------------
/cli/cli_r2_put_bucket_encryption.ts:
--------------------------------------------------------------------------------
1 | import { putBucketEncryption as putBucketEncryptionR2, R2 } from '../common/r2/r2.ts';
2 | import { denoflareCliCommand, parseOptionalBooleanOption, parseOptionalStringOption } from './cli_common.ts';
3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
4 |
5 | export const PUT_BUCKET_ENCRYPTION_COMMAND = denoflareCliCommand(['r2', 'put-bucket-encryption'], 'Sets encryption config for a bucket')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .include(commandOptionsForR2())
8 | .docsLink('/cli/r2#put-bucket-encryption')
9 | ;
10 |
11 | export async function putBucketEncryption(args: (string | number)[], options: Record) {
12 | if (PUT_BUCKET_ENCRYPTION_COMMAND.dumpHelp(args, options)) return;
13 |
14 | const { verbose, bucket } = PUT_BUCKET_ENCRYPTION_COMMAND.parse(args, options);
15 |
16 | if (verbose) {
17 | R2.DEBUG = true;
18 | }
19 |
20 | const sseAlgorithm = parseOptionalStringOption('sse-algorithm', options); if (sseAlgorithm === undefined) throw new Error(`--sse-algorithm is required`);
21 | const bucketKeyEnabled = parseOptionalBooleanOption('bucket-key-enabled', options); if (bucketKeyEnabled === undefined) throw new Error(`--bucket-key-enabled is required`);
22 |
23 | const { origin, region, context, urlStyle } = await loadR2Options(options);
24 |
25 | await putBucketEncryptionR2({ bucket, sseAlgorithm, bucketKeyEnabled, origin, region, urlStyle }, context);
26 | }
27 |
--------------------------------------------------------------------------------
/cli/cli_r2_put_large_object.ts:
--------------------------------------------------------------------------------
1 | import { Bytes } from '../common/bytes.ts';
2 | import { createMultipartUpload, uploadPart, abortMultipartUpload, completeMultipartUpload, R2, CompletedPart } from '../common/r2/r2.ts';
3 | import { CliStats, denoflareCliCommand } from './cli_common.ts';
4 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
5 |
6 | const PUT_LARGE_OBJECT_COMMAND = denoflareCliCommand(['cli', 'r2', 'put-large-object'], 'Upload a large file in multiple chunks')
7 | .arg('bucket', 'string', 'Name of the R2 bucket')
8 | .arg('key', 'string', 'Name of the R2 object key')
9 | .option('file', 'required-string', 'Local path to the file', { hint: 'path' })
10 | .include(commandOptionsForR2())
11 | ;
12 |
13 |
14 | export async function putLargeObject(args: (string | number)[], options: Record) {
15 | if (PUT_LARGE_OBJECT_COMMAND.dumpHelp(args, options)) return;
16 |
17 | const { verbose, bucket, key, file } = PUT_LARGE_OBJECT_COMMAND.parse(args, options);
18 |
19 | if (verbose) {
20 | R2.DEBUG = true;
21 | }
22 |
23 | const { origin, region, context, urlStyle } = await loadR2Options(options);
24 |
25 | const start = Date.now();
26 | const bytes = await Deno.readFile(file);
27 | console.log(`Read ${bytes.length} bytes in ${Date.now() - start}ms`);
28 |
29 | const { uploadId } = await createMultipartUpload({ bucket, key, origin, region, urlStyle }, context);
30 | let completed = false;
31 | try {
32 | const partSize = 1024 * 1024 * 200;
33 | const partsNum = Math.ceil(bytes.length / partSize);
34 | const parts: CompletedPart[] = [];
35 | for (let i = 0; i < partsNum; i++) {
36 | const start = i * partSize;
37 | const end = Math.min((i + 1) * partSize, bytes.length);
38 | const part = new Bytes(bytes.slice(start, end));
39 | const partNumber = i + 1;
40 | console.log(`Uploading part ${partNumber} of ${partsNum}`);
41 | const start2 = Date.now();
42 | const { etag } = await uploadPart({ bucket, key, uploadId, partNumber, body: part, origin, region, urlStyle }, context);
43 | console.log(`Put ${part.length} bytes in ${Date.now() - start2}ms`);
44 | parts.push({ partNumber, etag });
45 | }
46 |
47 | console.log(`Completing upload`);
48 | await completeMultipartUpload({ bucket, key, uploadId, parts, origin, region, urlStyle }, context);
49 | completed = true;
50 | } finally {
51 | if (!completed) {
52 | console.log(`Aborting upload`);
53 | await abortMultipartUpload({ bucket, key, uploadId, origin, region, urlStyle }, context);
54 | }
55 | }
56 |
57 | const millis = Date.now() - CliStats.launchTime;
58 | console.log(`put ${bytes.length} total bytes in ${millis}ms`);
59 | }
60 |
--------------------------------------------------------------------------------
/cli/cli_r2_upload_part.ts:
--------------------------------------------------------------------------------
1 | import { computeAwsCallBodyLength, uploadPart as uploadPartR2, R2 } from '../common/r2/r2.ts';
2 | import { CliStats, denoflareCliCommand } from './cli_common.ts';
3 | import { commandOptionsForLoadBodyFromOptions, commandOptionsForR2, loadBodyFromOptions, loadR2Options } from './cli_r2.ts';
4 |
5 | export const UPLOAD_PART_COMMAND = denoflareCliCommand(['r2', 'upload-part'], 'Upload part of a multipart upload')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .arg('key', 'string', 'Key of the object')
8 | .option('uploadId', 'required-string', 'Id of the existing multipart upload to complete')
9 | .option('partNumber', 'required-integer', 'Number of the part', { min: 1, max: 10000 })
10 | .include(commandOptionsForLoadBodyFromOptions)
11 | .include(commandOptionsForR2())
12 | .docsLink('/cli/r2#upload-part')
13 | ;
14 |
15 | export async function uploadPart(args: (string | number)[], options: Record) {
16 | if (UPLOAD_PART_COMMAND.dumpHelp(args, options)) return;
17 |
18 | const { bucket, key, uploadId, partNumber, verbose } = UPLOAD_PART_COMMAND.parse(args, options);
19 |
20 | if (verbose) {
21 | R2.DEBUG = true;
22 | }
23 |
24 | const { origin, region, context, urlStyle } = await loadR2Options(options);
25 |
26 | const { body, contentMd5 } = await loadBodyFromOptions(options, context.unsignedPayload);
27 |
28 | const result = await uploadPartR2({ bucket, key, uploadId, partNumber, body, origin, region, contentMd5, urlStyle }, context);
29 | const millis = Date.now() - CliStats.launchTime;
30 | console.log(JSON.stringify(result));
31 | console.log(`put ${computeAwsCallBodyLength(body)} bytes in ${millis}ms`);
32 | }
33 |
--------------------------------------------------------------------------------
/cli/cli_r2_upload_part_copy.ts:
--------------------------------------------------------------------------------
1 | import { uploadPartCopy as uploadPartCopyR2, R2 } from '../common/r2/r2.ts';
2 | import { CliStats, denoflareCliCommand } from './cli_common.ts';
3 | import { commandOptionsForR2, loadR2Options } from './cli_r2.ts';
4 |
5 | export const UPLOAD_PART_COPY_COMMAND = denoflareCliCommand(['r2', 'upload-part-copy'], 'Copy R2 part from a given source bucket and key')
6 | .arg('bucket', 'string', 'Name of the R2 bucket')
7 | .arg('key', 'string', 'Key of the object')
8 | .option('uploadId', 'required-string', 'Id of the existing multipart upload to complete')
9 | .option('partNumber', 'required-integer', 'Number of the part', { min: 1, max: 10000 })
10 | .option('sourceBucket', 'string', 'R2 Bucket of the source object (default: destination bucket)')
11 | .option('sourceKey', 'required-string', 'Key of the source object')
12 | .option('sourceRange', 'string', 'The range of bytes to copy from the source object')
13 | .optionGroup()
14 | .option('ifMatch', 'string', 'Copies the object part if its entity tag (ETag) matches the specified tag')
15 | .option('ifNoneMatch', 'string', 'Copies the object part if its entity tag (ETag) is different than the specified ETag')
16 | .option('ifModifiedSince', 'string', 'Copies the object part if it has been modified since the specified time')
17 | .option('ifUnmodifiedSince', 'string', `Copies the object part if it hasn't been modified since the specified time`)
18 | .include(commandOptionsForR2())
19 | .docsLink('/cli/r2#upload-part-copy')
20 | ;
21 |
22 | export async function uploadPartCopy(args: (string | number)[], options: Record) {
23 | if (UPLOAD_PART_COPY_COMMAND.dumpHelp(args, options)) return;
24 |
25 | const { bucket, key, uploadId, partNumber, verbose, sourceKey, sourceBucket, sourceRange, ifMatch, ifNoneMatch, ifModifiedSince, ifUnmodifiedSince } = UPLOAD_PART_COPY_COMMAND.parse(args, options);
26 |
27 | if (verbose) {
28 | R2.DEBUG = true;
29 | }
30 |
31 | const { origin, region, context, urlStyle } = await loadR2Options(options);
32 |
33 | const result = await uploadPartCopyR2({
34 | bucket, key, uploadId, partNumber, origin, region, urlStyle,
35 | sourceBucket: sourceBucket ?? bucket, sourceKey, sourceRange, ifMatch, ifModifiedSince, ifNoneMatch, ifUnmodifiedSince,
36 | }, context);
37 | console.log(JSON.stringify(result, undefined, 2));
38 |
39 | const millis = Date.now() - CliStats.launchTime;
40 | console.log(`copied in ${millis}ms`);
41 | }
42 |
--------------------------------------------------------------------------------
/cli/cli_site.ts:
--------------------------------------------------------------------------------
1 | import { generate, SITE_GENERATE_COMMAND } from './cli_site_generate.ts';
2 | import { serve, SITE_SERVE_COMMAND } from './cli_site_serve.ts';
3 | import { denoflareCliCommand } from './cli_common.ts';
4 |
5 | export const SITE_COMMAND = denoflareCliCommand('site', 'Develop and deploy a static docs site to Cloudflare Pages')
6 | .subcommand(SITE_GENERATE_COMMAND, generate)
7 | .subcommand(SITE_SERVE_COMMAND, serve)
8 | .docsLink('/cli/site')
9 | ;
10 |
11 | export async function site(args: (string | number)[], options: Record): Promise {
12 | await SITE_COMMAND.routeSubcommand(args, options);
13 | }
14 |
--------------------------------------------------------------------------------
/cli/cli_site_generate.ts:
--------------------------------------------------------------------------------
1 | import { CliStats, denoflareCliCommand } from './cli_common.ts';
2 | import { ensureDir, resolve } from './deps_cli.ts';
3 | import { fileExists } from './fs_util.ts';
4 | import { RepoDir } from './repo_dir.ts';
5 | import { InputFileInfo, SiteModel } from './site/site_model.ts';
6 |
7 | export const SITE_GENERATE_COMMAND = denoflareCliCommand(['site', 'generate'], 'Develop and deploy a static docs site to Cloudflare Pages')
8 | .arg('repoDir', 'string', 'Local path to the git repo to use as the source input for generation')
9 | .arg('outputDir', 'string', 'Local path to the directory to use for generated output')
10 | .docsLink('/cli/site/generate')
11 | ;
12 |
13 | export async function generate(args: (string | number)[], options: Record) {
14 | if (SITE_GENERATE_COMMAND.dumpHelp(args, options)) return;
15 |
16 | const { verbose, repoDir: repoDirOpt, outputDir: outputDirOpt } = SITE_GENERATE_COMMAND.parse(args, options);
17 |
18 | const repoDir = await RepoDir.of(resolve(Deno.cwd(), repoDirOpt));
19 |
20 | if (await fileExists(outputDirOpt)) throw new Error(`Bad output-dir, exists as file: ${outputDirOpt}`);
21 | const outputDir = resolve(Deno.cwd(), outputDirOpt);
22 |
23 | const siteModel = new SiteModel(repoDir.path);
24 |
25 | // 3-7ms to here
26 | let start = Date.now();
27 | console.log('Building site...');
28 | const inputFiles: InputFileInfo[] = (await repoDir.listFiles()).map(v => ({ path: v.path, version: '0' }));
29 | await siteModel.setInputFiles(inputFiles);
30 | console.log(`Built site, took ${Date.now() - start}ms`);
31 |
32 | start = Date.now();
33 | if (verbose) console.log(`Ensuring dir exists: ${outputDir}`);
34 | await ensureDir(outputDir);
35 | console.log(`Writing output`);
36 | await siteModel.writeOutput(outputDir);
37 | console.log(`Wrote output to ${outputDir}, took ${Date.now() - start}ms`);
38 | console.log(`Done in ${Date.now() - CliStats.launchTime}ms`);
39 | }
40 |
--------------------------------------------------------------------------------
/cli/cli_site_serve.ts:
--------------------------------------------------------------------------------
1 | import { denoflareCliCommand } from './cli_common.ts';
2 | import { resolve } from './deps_cli.ts';
3 | import { computeFileInfoVersion } from './fs_util.ts';
4 | import { RepoDir } from './repo_dir.ts';
5 | import { InputFileInfo, SiteModel } from './site/site_model.ts';
6 |
7 | const DEFAULT_PORT = 8099;
8 |
9 | export const SITE_SERVE_COMMAND = denoflareCliCommand(['site', 'serve'], 'Host a static Cloudflare Pages site in a local Deno web server')
10 | .arg('repoDir', 'string', 'Local path to the git repo to use as the source input')
11 | .option('port', 'integer', `Local port to use for the http server (default: ${DEFAULT_PORT})`)
12 | .option('watch', 'boolean', `If set, rebuild the site when file system changes are detected in `)
13 | .docsLink('/cli/site/serve')
14 | ;
15 |
16 | export async function serve(args: (string | number)[], options: Record) {
17 | if (SITE_SERVE_COMMAND.dumpHelp(args, options)) return;
18 |
19 | const { verbose, repoDir: repoDirOpt, port: portOpt, watch } = SITE_SERVE_COMMAND.parse(args, options);
20 |
21 | if (verbose) RepoDir.VERBOSE = true;
22 |
23 | const port = portOpt ?? DEFAULT_PORT;
24 | const localOrigin = `http://localhost:${port}`;
25 |
26 | const repoDir = await RepoDir.of(resolve(Deno.cwd(), repoDirOpt));
27 | const siteModel = new SiteModel(repoDir.path, { localOrigin });
28 |
29 | const buildSite = async () => {
30 | const start = Date.now();
31 | console.log('Building site...');
32 | const inputFiles: InputFileInfo[] = (await repoDir.listFiles({ stat: true })).map(v => ({ path: v.path, version: computeFileInfoVersion(v.info!) }));
33 | await siteModel.setInputFiles(inputFiles);
34 | console.log(`Built site, took ${Date.now() - start}ms`);
35 | };
36 | if (watch) {
37 | let timeoutId: number | undefined;
38 | repoDir.startWatching(_events => {
39 | clearTimeout(timeoutId);
40 | timeoutId = setTimeout(async () => {
41 | await buildSite();
42 | }, 50);
43 | });
44 | }
45 |
46 | await buildSite();
47 |
48 | const server = Deno.serve({ port }, async request => await siteModel.handle(request));
49 | console.log(`Local server running on ${localOrigin}`);
50 | await server.finished;
51 | }
52 |
--------------------------------------------------------------------------------
/cli/cli_version.ts:
--------------------------------------------------------------------------------
1 | export const CLI_VERSION = '0.7.0+';
2 |
--------------------------------------------------------------------------------
/cli/config_loader_test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from 'https://deno.land/std@0.224.0/assert/assert_equals.ts';
2 | import { assertRejects } from 'https://deno.land/std@0.224.0/assert/assert_rejects.ts';
3 |
4 | import { resolveBindings, resolveProfile } from './config_loader.ts';
5 | import { Binding, Config } from '../common/config.ts';
6 |
7 | Deno.test({
8 | name: 'resolveBindings',
9 | fn: async () => {
10 | const opts = makeOpts();
11 | const input: Record = {
12 | text1: { value: '${pushId}' },
13 | text2: { value: '${localPort}' },
14 | text3: { value: '${aws:foo}' },
15 | text4: { value: 'asdf' },
16 | text5: { value: 'pre${env:foo}post' },
17 | kv1: { kvNamespace: '${env:foo}' },
18 | };
19 | const output = await resolveBindings(input, opts);
20 | assertEquals(input, input); // ensure input not modified
21 | assertEquals(output, {
22 | text1: { value: 'the-push-id' },
23 | text2: { value: '123' },
24 | text3: { value: 'ak:sak' },
25 | text4: { value: 'asdf' },
26 | text5: { value: 'prebarpost' },
27 | kv1: { kvNamespace: 'bar' },
28 | });
29 | assertRejects(async () => {
30 | await resolveBindings({ bad: { value: '${asdf}' } }, opts);
31 | })
32 | }
33 | });
34 |
35 | Deno.test({
36 | name: 'resolveProfile',
37 | fn: async () => {
38 | const opts = makeOpts();
39 | const config: Config = { profiles: { first: { accountId: 'account-id', apiToken: '${env:API_TOKEN}' } } };
40 | const profile = await resolveProfile(config, {}, undefined, opts);
41 | assertEquals(profile as unknown, { accountId: 'account-id', apiToken: 'api-token' });
42 | }
43 | });
44 |
45 | //
46 |
47 | function makeOpts() {
48 | const awsCredentialsLoader = async (profile: string) => {
49 | await Promise.resolve();
50 | if (profile === 'foo') return { accessKeyId: 'ak', secretAccessKey: 'sak' };
51 | throw new Error(`Profile ${profile} not found`);
52 | };
53 | const envLoader = (name: string) => new Map([['foo', 'bar'], ['API_TOKEN', 'api-token']]).get(name);
54 | return { localPort: 123, pushId: 'the-push-id', awsCredentialsLoader, envLoader };
55 | }
56 |
--------------------------------------------------------------------------------
/cli/deno_check.ts:
--------------------------------------------------------------------------------
1 | import { DenoDiagnostic, DenoDiagnosticCategory, parseDiagnostics } from './deno_bundle.ts';
2 | import { spawn } from './spawn.ts';
3 |
4 | export interface DenoCheckResult {
5 | readonly success: boolean;
6 | readonly diagnostics: DenoDiagnostic[];
7 | }
8 |
9 | export async function denoCheck(rootSpecifier: string, opts: { all?: boolean, compilerOptions?: { lib?: string[] } } = {}): Promise {
10 | const { all, compilerOptions } = opts;
11 |
12 | let config: string | undefined;
13 | try {
14 | if (compilerOptions?.lib) {
15 | config = await Deno.makeTempFile({ prefix: 'denoflare-deno-check', suffix: '.json'});
16 | await Deno.writeTextFile(config, JSON.stringify({ compilerOptions }));
17 | }
18 | const { out, err, success } = await runDenoCheck(rootSpecifier, { all, config });
19 | console.log({ out, err, success });
20 |
21 | let diagnostics: DenoDiagnostic[] = [];
22 | if (err.length > 0 || !success) {
23 | diagnostics = parseDiagnostics(err);
24 | if (!success && err && diagnostics.length === 0) {
25 | // unparsed error (e.g. deno bundle not supporting npm specifiers)
26 | diagnostics.push({
27 | category: DenoDiagnosticCategory.Error,
28 | code: 0,
29 | messageText: err,
30 | });
31 | }
32 | }
33 |
34 | return { success, diagnostics };
35 | } finally {
36 | if (config) {
37 | await Deno.remove(config);
38 | }
39 | }
40 | }
41 |
42 | //
43 |
44 | type RunDenoCheckResult = { code: number, success: boolean, out: string, err: string };
45 |
46 | async function runDenoCheck(rootSpecifier: string, opts: { all?: boolean, config?: string } = {}): Promise {
47 | const { all, config } = opts;
48 | const args = [
49 | 'check',
50 | '--allow-import',
51 | '--no-lock',
52 | ...(all ? [ '--all' ] : []),
53 | ...(config ? ['--config', config] : []),
54 | rootSpecifier,
55 | ];
56 | const { code, success, stdout, stderr } = await spawn(Deno.execPath(), {
57 | args,
58 | env: {
59 | NO_COLOR: '1', // to make parsing the output easier
60 | }
61 | });
62 | const out = new TextDecoder().decode(stdout);
63 | const err = new TextDecoder().decode(stderr);
64 | return { code, success, out, err };
65 | }
66 |
--------------------------------------------------------------------------------
/cli/deno_graph.ts:
--------------------------------------------------------------------------------
1 | import { createGraph, fromFileUrl, ModuleGraphJson, toFileUrl, isAbsolute, join, normalize } from './deps_cli.ts';
2 |
3 | export async function computeDenoGraphLocalPaths(path: string): Promise {
4 | const graph = await computeDenoGraph(path);
5 | return findLocalPaths(graph);
6 | }
7 |
8 | export async function computeDenoGraph(path: string): Promise {
9 | return await createGraph(computeRootSpecifier(path));
10 | }
11 |
12 | //
13 |
14 | function computeRootSpecifier(path: string): string {
15 | if (path.startsWith('file://')) return path;
16 | const absolutePath = toAbsolutePath(path);
17 | return toFileUrl(absolutePath).toString();
18 | }
19 |
20 | function toAbsolutePath(path: string): string {
21 | return isAbsolute(path) ? path : normalize(join(Deno.cwd(), path));
22 | }
23 |
24 | function findLocalPaths(graph: ModuleGraphJson): string[] {
25 | const rt = new Set();
26 | for (const root of graph.roots) {
27 | const rootPath = fromFileUrl(root);
28 | rt.add(rootPath);
29 | }
30 | for (const { specifier } of graph.modules) {
31 | if (specifier.startsWith('file://')) {
32 | rt.add(fromFileUrl(specifier));
33 | }
34 | }
35 | return [...rt].sort();
36 | }
37 |
--------------------------------------------------------------------------------
/cli/deno_info.ts:
--------------------------------------------------------------------------------
1 | import { fromFileUrl } from './deps_cli.ts';
2 |
3 | export async function computeDenoInfoLocalPaths(path: string): Promise {
4 | const info = await computeDenoInfo(path);
5 | return findLocalPaths(info);
6 | }
7 |
8 | export async function computeDenoInfo(path: string): Promise {
9 | const p = Deno.run({
10 | cmd: [Deno.execPath(), 'info', '--json', path],
11 | stdout: 'piped',
12 | stderr: 'piped',
13 | });
14 |
15 | // await its completion
16 | const status = await p.status();
17 | const stdout = new TextDecoder().decode(await p.output());
18 | const stderr = new TextDecoder().decode(await p.stderrOutput());
19 | if (!status.success) {
20 | throw new Error(`deno info failed: ${JSON.stringify({ status, stdout, stderr})}`);
21 | }
22 | const obj = JSON.parse(stdout);
23 | // console.log(JSON.stringify(obj, undefined, 2));
24 | return obj as DenoInfo;
25 | }
26 |
27 | //
28 |
29 | function findLocalPaths(info: DenoInfo): string[] {
30 | const rt = new Set();
31 | const rootPath = fromFileUrl(info.root);
32 | rt.add(rootPath);
33 | for (const moduleInfo of info.modules) {
34 | rt.add(moduleInfo.local);
35 | }
36 | return [...rt].sort();
37 | }
38 |
39 | //
40 |
41 | export interface DenoInfo {
42 | readonly root: string; // e.g. file:///...
43 | readonly modules: readonly ModuleInfo[];
44 | readonly size: number;
45 | }
46 |
47 | export interface ModuleInfo {
48 | readonly specifier: string; // e.g. file:/// or https://...
49 | readonly dependencies: readonly DependencyInfo[];
50 | readonly size: number;
51 | readonly mediaType: string; // e.g. Dts, TypeScript
52 | readonly local: string; // e.g. /path/to/source
53 | readonly checksum: string; // e.g. f182ca01c68d2d914db5016db3d74c123651286ba6b95e26f0231e84b867f896
54 | readonly emit?: string; // e.g. /path/to/deno/cache
55 | readonly error?: string; // e.g. Cannot resolve module...
56 | }
57 |
58 | export interface DependencyInfo {
59 | readonly specifier: string; // e.g. ../asdf.ts or https://
60 | readonly code: string; // e.g. file:///asdf or https://
61 | }
62 |
--------------------------------------------------------------------------------
/cli/deps_cli.ts:
--------------------------------------------------------------------------------
1 | export { basename, dirname, join, fromFileUrl, resolve, toFileUrl, extname, relative, isAbsolute, normalize, parse as parsePath, globToRegExp } from 'https://deno.land/std@0.224.0/path/mod.ts';
2 | export { ensureDir, walk, emptyDir } from 'https://deno.land/std@0.224.0/fs/mod.ts';
3 | export { SEPARATOR as systemSeparator } from 'https://deno.land/std@0.224.0/path/constants.ts';
4 | export { sortBy } from 'https://deno.land/std@0.224.0/collections/sort_by.ts';
5 | export { createGraph } from 'https://deno.land/x/deno_graph@0.43.3/mod.ts';
6 | export type { ModuleGraphJson } from 'https://deno.land/x/deno_graph@0.43.3/lib/types.d.ts';
7 | export { gzip } from 'https://deno.land/x/compress@v0.4.5/zlib/mod.ts';
8 | export { parse as _parseJsonc } from 'https://cdn.skypack.dev/jsonc-parser@3.2.0';
9 | export { default as marked } from 'https://cdn.skypack.dev/marked@3.0.8?dts';
10 | export { html } from 'https://deno.land/x/html_escape@v1.1.5/html.ts';
11 | export { default as hljs } from 'https://cdn.skypack.dev/highlight.js@11.7.0';
12 | export { serve } from 'https://deno.land/std@0.224.0/http/server.ts';
13 | export { mapValues } from 'https://deno.land/std@0.224.0/collections/map_values.ts';
14 | export { TextLineStream } from 'https://deno.land/std@0.224.0/streams/text_line_stream.ts';
15 |
--------------------------------------------------------------------------------
/cli/flag_parser.ts:
--------------------------------------------------------------------------------
1 | export function parseFlags(denoArgs: string[]): { args: (string | number)[], options: Record } {
2 | const args: (string | number)[] = [];
3 | const options: Record = {};
4 |
5 | const addOption = (name: string, value: string | number | boolean) => {
6 | const existing = options[name];
7 | if (existing === undefined) {
8 | options[name] = value;
9 | } else if (Array.isArray(existing)) {
10 | existing.push(value);
11 | } else {
12 | options[name] = [ existing, value ];
13 | }
14 | };
15 |
16 | let currentOptionName: string | undefined;
17 | for (const denoArg of denoArgs) {
18 | const optionName = tryParseOptionName(denoArg);
19 | if (optionName) {
20 | if (currentOptionName) {
21 | addOption(currentOptionName, true);
22 | }
23 | currentOptionName = optionName;
24 | continue;
25 | }
26 | const value = /^true$/i.test(denoArg) ? true
27 | : /^false$/i.test(denoArg) ? false
28 | : /^-?\d+$/.test(denoArg) ? parseInt(denoArg)
29 | : /^-?\d+\.\d+$/.test(denoArg) ? parseFloat(denoArg)
30 | : denoArg;
31 | if (currentOptionName) {
32 | addOption(currentOptionName, value);
33 | currentOptionName = undefined;
34 | } else {
35 | if (Object.keys(options).length > 0) throw new Error(`Named options must come after positional arguments`);
36 | args.push(typeof value === 'boolean' ? String(value) : value);
37 | }
38 | }
39 | if (currentOptionName) {
40 | addOption(currentOptionName, true);
41 | }
42 |
43 | return { args, options };
44 | }
45 |
46 | //
47 |
48 | function tryParseOptionName(denoArg: string): string | undefined {
49 | const m = /^--([^\s]+)$/.exec(denoArg);
50 | return m ? m[1] : undefined;
51 | }
52 |
--------------------------------------------------------------------------------
/cli/flag_parser_test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from 'https://deno.land/std@0.224.0/assert/assert_equals.ts';
2 | import { assertThrows } from 'https://deno.land/std@0.224.0/assert/assert_throws.ts';
3 |
4 | import { parseFlags } from './flag_parser.ts';
5 |
6 | Deno.test({
7 | name: 'parseFlags',
8 | fn: () => {
9 | const good: [ string[], (string | number)[], Record ][] = [
10 | [ [], [], {} ],
11 | [ [ 'one' ], [ 'one' ], {} ],
12 | [ [ '--bool' ], [], { bool: true} ],
13 | [ [ 'arg', '--foo', 'bar' ], [ 'arg' ], { foo: 'bar'} ],
14 | [ [ '--foo', 'bar', '--foo' ], [], { foo: ['bar', true]} ],
15 | [ [ '--foo', 'bar', '--foo', 'baz' ], [], { foo: ['bar', 'baz']} ],
16 | [ [ '--bool', 'true' ], [], { bool: true } ],
17 | [ [ '--bool', 'True' ], [], { bool: true } ],
18 | [ [ '--bool', 'false' ], [], { bool: false } ],
19 | [ [ '--bool', 'FALSE' ], [], { bool: false } ],
20 | [ [ '--int', '0' ], [], { int: 0 } ],
21 | [ [ '--int', '-0' ], [], { int: 0 } ],
22 | [ [ '--int', '123456' ], [], { int: 123456 } ],
23 | [ [ '--float', '0.123456' ], [], { float: 0.123456 } ],
24 | [ [ '--float', '-123.123456' ], [], { float: -123.123456 } ],
25 | [ [ '--string', '-123.123456.0' ], [], { string: '-123.123456.0' } ],
26 | [ [ '--string', '-123.123456.0' ], [], { string: '-123.123456.0' } ],
27 | [ [ '', 'a b' ], ['', 'a b'], { } ],
28 | [ [ 'cfapi', 'list-scripts', '--per-page', '1', '--verbose' ], [ 'cfapi', 'list-scripts' ], { 'per-page': 1, verbose: true } ],
29 | ];
30 | for (const [ input, args, options] of good) {
31 | assertEquals(parseFlags(input), { args, options });
32 | }
33 | const bad: (string[])[] = [
34 | ['--foo', 'bar', 'baz' ],
35 | ];
36 | for (const input of bad) {
37 | assertThrows(() => parseFlags(input));
38 | }
39 | }
40 | });
41 |
--------------------------------------------------------------------------------
/cli/fs_util.ts:
--------------------------------------------------------------------------------
1 | export async function fileExists(path: string): Promise {
2 | return await exists(path, info => info.isFile);
3 | }
4 |
5 | export async function directoryExists(path: string): Promise {
6 | return await exists(path, info => info.isDirectory);
7 | }
8 |
9 | export function computeFileInfoVersion(info: Deno.FileInfo): string {
10 | return `${info.size}|${info.mtime instanceof Date ? (info.mtime.getTime()) : ''}`;
11 | }
12 |
13 | //
14 |
15 | async function exists(path: string, test: (info: Deno.FileInfo) => boolean) {
16 | try {
17 | const info = await Deno.stat(path);
18 | return test(info);
19 | } catch (e) {
20 | if (e instanceof Deno.errors.NotFound) {
21 | return false;
22 | } else {
23 | throw e;
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/cli/in_memory_rpc_channels.ts:
--------------------------------------------------------------------------------
1 | import { RpcChannel } from '../common/rpc_channel.ts';
2 |
3 | export class InMemoryRpcChannels {
4 | readonly host: RpcChannel;
5 | readonly stub: RpcChannel;
6 |
7 | constructor(tagBase: string) {
8 | this.host = new RpcChannel(`${tagBase}-host`, async (message, _transfer) => {
9 | await this.stub.receiveMessage(message);
10 | });
11 | this.stub = new RpcChannel(`${tagBase}-stub`, async (message, _transfer) => {
12 | await this.host.receiveMessage(message);
13 | });
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/cli/jsonc.ts:
--------------------------------------------------------------------------------
1 | import { _parseJsonc } from './deps_cli.ts';
2 |
3 | // deno-lint-ignore no-explicit-any
4 | export function parseJsonc(input: string, errors?: ParseError[], options?: ParseOptions): any {
5 | return _parseJsonc(input, errors, options);
6 | }
7 |
8 | export function formatParseError(error: ParseError, input: string): string {
9 | const { error: code, offset, length } = error;
10 | let problem = escapeForPrinting(input.substring(offset, offset + length));
11 | if (problem.length < 10) {
12 | const contextSize = 10;
13 | problem = escapeForPrinting(input.substring(Math.max(offset - contextSize, 0), Math.min(offset + length + contextSize, input.length)));
14 | }
15 | return `${parseErrorCodeToString(code)} at ${offset}: ${problem}`;
16 | }
17 |
18 | export function parseErrorCodeToString(code: ParseErrorCode): string {
19 | switch (code) {
20 | case ParseErrorCode.InvalidSymbol: return 'InvalidSymbol';
21 | case ParseErrorCode.InvalidNumberFormat: return 'InvalidNumberFormat';
22 | case ParseErrorCode.PropertyNameExpected: return 'PropertyNameExpected';
23 | case ParseErrorCode.ValueExpected: return 'ValueExpected';
24 | case ParseErrorCode.ColonExpected: return 'ColonExpected';
25 | case ParseErrorCode.CommaExpected: return 'CommaExpected';
26 | case ParseErrorCode.CloseBraceExpected: return 'CloseBraceExpected';
27 | case ParseErrorCode.CloseBracketExpected: return 'CloseBracketExpected';
28 | case ParseErrorCode.EndOfFileExpected: return 'EndOfFileExpected';
29 | case ParseErrorCode.InvalidCommentToken: return 'InvalidCommentToken';
30 | case ParseErrorCode.UnexpectedEndOfComment: return 'UnexpectedEndOfComment';
31 | case ParseErrorCode.UnexpectedEndOfString: return 'UnexpectedEndOfString';
32 | case ParseErrorCode.UnexpectedEndOfNumber: return 'UnexpectedEndOfNumber';
33 | case ParseErrorCode.InvalidUnicode: return 'InvalidUnicode';
34 | case ParseErrorCode.InvalidEscapeCharacter: return 'InvalidEscapeCharacter';
35 | case ParseErrorCode.InvalidCharacter: return 'InvalidCharacter';
36 | }
37 | return ``;
38 | }
39 |
40 | //
41 |
42 | function escapeForPrinting(input: string): string {
43 | return input.replaceAll('\r', '\\r').replaceAll('\n', '\\n');
44 | }
45 |
46 | //
47 |
48 | export interface ParseOptions {
49 | disallowComments?: boolean;
50 | allowTrailingComma?: boolean;
51 | allowEmptyContent?: boolean;
52 | }
53 |
54 | export interface ParseError {
55 | error: ParseErrorCode;
56 | offset: number;
57 | length: number;
58 | }
59 |
60 | export const enum ParseErrorCode {
61 | InvalidSymbol = 1,
62 | InvalidNumberFormat = 2,
63 | PropertyNameExpected = 3,
64 | ValueExpected = 4,
65 | ColonExpected = 5,
66 | CommaExpected = 6,
67 | CloseBraceExpected = 7,
68 | CloseBracketExpected = 8,
69 | EndOfFileExpected = 9,
70 | InvalidCommentToken = 10,
71 | UnexpectedEndOfComment = 11,
72 | UnexpectedEndOfString = 12,
73 | UnexpectedEndOfNumber = 13,
74 | InvalidUnicode = 14,
75 | InvalidEscapeCharacter = 15,
76 | InvalidCharacter = 16
77 | }
78 |
--------------------------------------------------------------------------------
/cli/module_watcher.ts:
--------------------------------------------------------------------------------
1 | import { computeDenoGraphLocalPaths } from './deno_graph.ts';
2 |
3 | export class ModuleWatcher {
4 | static VERBOSE = false;
5 |
6 | private readonly entryPointPath: string;
7 | private readonly includes: string[];
8 | private readonly modificationCallback: () => void;
9 |
10 | private watcher: Deno.FsWatcher | undefined;
11 |
12 | constructor(entryPointPath: string, modificationCallback: () => void, includes?: string[]) {
13 | this.entryPointPath = entryPointPath;
14 | this.includes = includes || [];
15 | this.modificationCallback = modificationCallback;
16 | this.initWatcher().catch(e => console.error('Error in initWatcher', e.stack || e));
17 | }
18 |
19 | close() {
20 | if (this.watcher) {
21 | this.watcher.close();
22 | this.watcher = undefined;
23 | }
24 | }
25 |
26 | //
27 |
28 | private dispatchModification() {
29 | this.modificationCallback();
30 | }
31 |
32 | private async initWatcher() {
33 | const paths = await computeDenoGraphLocalPaths(this.entryPointPath);
34 | paths.push(...this.includes);
35 | if (ModuleWatcher.VERBOSE) console.log('watching', paths);
36 | const watcher = Deno.watchFs(paths);
37 | this.watcher = watcher;
38 | let timeoutId: number | undefined;
39 | for await (const event of watcher) {
40 | if (event.kind === 'modify') {
41 | // a single file modification sends two modify events, so coalesce them
42 | clearTimeout(timeoutId);
43 | timeoutId = setTimeout(() => this.dispatchModification(), 500);
44 | }
45 | }
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/cli/rpc_host_d1_database_test.ts:
--------------------------------------------------------------------------------
1 | import { makeRpcStubD1DatabaseProvider } from '../common/rpc_stub_d1_database.ts';
2 | import { InMemoryRpcChannels } from './in_memory_rpc_channels.ts';
3 | import { makeRpcHostD1Database } from './rpc_host_d1_database.ts';
4 | import { SqliteD1Database } from './sqlite_d1_database.ts';
5 | import { runSimpleD1DatabaseScenario } from './sqlite_d1_database_test.ts';
6 |
7 | Deno.test({
8 | name: 'makeRpcHostD1Database',
9 | ignore: (await Deno.permissions.query({ name: 'net' })).state !== 'granted',
10 | async fn() {
11 | const channels = new InMemoryRpcChannels('test');
12 | makeRpcHostD1Database(channels.host, v => SqliteD1Database.provider(() => ':memory:')(v));
13 | const provider = makeRpcStubD1DatabaseProvider(channels.stub);
14 | const db = provider(crypto.randomUUID())
15 | await runSimpleD1DatabaseScenario(db);
16 | }
17 | });
18 |
--------------------------------------------------------------------------------
/cli/rpc_host_durable_object_storage_test.ts:
--------------------------------------------------------------------------------
1 | import { makeRpcStubDurableObjectStorageProvider } from '../common/rpc_stub_durable_object_storage.ts';
2 | import { runSimpleStorageTestScenario } from '../common/storage/durable_object_storage_test.ts';
3 | import { InMemoryRpcChannels } from './in_memory_rpc_channels.ts';
4 | import { makeRpcHostDurableObjectStorage } from './rpc_host_durable_object_storage.ts';
5 |
6 | Deno.test('makeRpcHostDurableObjectStorage', async () => {
7 | const channels = new InMemoryRpcChannels('test');
8 | makeRpcHostDurableObjectStorage(channels.host);
9 | const provider = makeRpcStubDurableObjectStorageProvider(channels.stub);
10 | const storage = await provider('class1', 'id1', { storage: 'memory', container: 'test.rpc' }, () => {});
11 | await runSimpleStorageTestScenario(storage);
12 | });
13 |
--------------------------------------------------------------------------------
/cli/site/site_config.ts:
--------------------------------------------------------------------------------
1 | export interface SiteConfig {
2 |
3 | // leave these out if no org
4 | readonly organization?: string; // short first (bold) part of org name
5 | readonly organizationSuffix?: string; // short second part of org name
6 | readonly organizationSvg?: string; // content repo /path/to/organization.svg must use fill="currentColor"
7 | readonly organizationUrl?: string; // abs url to org
8 |
9 | readonly product: string; // (required) product name for sidebar, etc
10 | readonly productRepo?: string; // e.g. "ghuser/project-repo", used for gh link in header
11 | readonly productSvg?: string; // content repo /path/to/product.svg must use fill="currentColor"
12 | readonly contentRepo?: string; // e.g. "ghuser/docs-repo", used for edit this page in footer
13 |
14 | readonly themeColor?: string; // #rrggbb
15 | readonly themeColorDark?: string; // #rrggbb
16 |
17 | readonly search?: SiteSearchConfig; // Algolia DocSearch options, if applicable
18 |
19 | readonly siteMetadata: SiteMetadata; // (required for title, description)
20 | }
21 |
22 | export interface SiteMetadata {
23 | readonly title: string; // (required) (html title, og:title, twitter:title) = ·
24 | readonly description: string; // (required) (html meta description, og:description, twitter:description) =
25 | readonly twitterUsername?: string; // @asdf for twitter:site
26 | readonly image?: string; // abs or relative url to twitter:image
27 | readonly imageAlt?: string; // alt text for twitter:image
28 | readonly origin?: string; // abs url to site (origin)
29 | readonly faviconIco?: string; // relative url favicon ico
30 | readonly faviconSvg?: string; // relative url favicon svg
31 | readonly faviconMaskSvg?: string; // relative url favicon mask svg
32 | readonly faviconMaskColor?: string; // #rrggbb favicon mask color (required if faviconMaskSvg provided)
33 | readonly manifest?: Record; // override default web app manifest members
34 | }
35 |
36 | export interface SiteSearchConfig {
37 | readonly indexName: string; // (required) Name of the Algolia DocSearch index name for this site
38 | readonly apiKey: string; // (required) Api key provided by Algolia for this site
39 | readonly appId: string; // (required) Application ID provided by Algolia for this site
40 | }
41 |
--------------------------------------------------------------------------------
/cli/site/toc.ts:
--------------------------------------------------------------------------------
1 | export interface TocNode {
2 | readonly title: string;
3 | readonly anchorId: string;
4 | readonly children?: readonly TocNode[];
5 | }
6 |
7 | export interface Heading {
8 | readonly level: 2 | 3 | 4 | 5 | 6 | 7;
9 | readonly id: string;
10 | readonly text: string;
11 | }
12 |
13 | export function computeToc(headings: Heading[]): TocNode[] {
14 | return headings.map(v => ({ title: v.text, anchorId: v.id }));
15 | }
16 |
--------------------------------------------------------------------------------
/cli/spawn.ts:
--------------------------------------------------------------------------------
1 | // deno-lint-ignore-file no-explicit-any
2 |
3 | // 1.24.0 version
4 | export interface SpawnOutput {
5 | get stdout(): Uint8Array;
6 | get stderr(): Uint8Array;
7 | success: boolean;
8 | code: number;
9 | signal: Deno.Signal | null;
10 | }
11 |
12 | export async function spawn(command: string | URL, options?: { args?: string[], env?: Record }): Promise {
13 | // use Deno.Command if 1.28+, spawn is deprecated
14 | if ('Command' in Deno) {
15 | const c = new (Deno as any).Command(command, options);
16 | return await c.output();
17 | }
18 |
19 | // use Deno.spawn if < 1.28, not deprecated
20 | if ('spawn' in Deno) {
21 | const rt = await (Deno as any).spawn(command, options) as any;
22 |
23 | // pre-1.24.0, success, code, and signal were under a .status property
24 | // https://deno.com/blog/v1.24#updates-to-new-subprocess-api
25 | if (typeof rt.status === 'object') {
26 | const { success, code, signal } = rt.status;
27 | if (typeof success === 'boolean' && typeof code === 'number' && signal !== undefined) {
28 | rt.success = success;
29 | rt.code = code;
30 | rt.signal = signal;
31 | delete rt.status;
32 | } else {
33 | throw new Error(`Unexpected pre-1.24.0 spawn output status object`);
34 | }
35 | }
36 | return rt;
37 | }
38 |
39 | throw new Error(`Unable to spawn subprocess: did not find 'Deno.Command' or 'Deno.spawn'`);
40 | }
41 |
--------------------------------------------------------------------------------
/cli/sqlite_d1_database_test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from 'https://deno.land/std@0.224.0/assert/assert_equals.ts';
2 | import { SqliteD1Database } from './sqlite_d1_database.ts';
3 | import { assert } from 'https://deno.land/std@0.224.0/assert/assert.ts';
4 | import { D1Database } from '../common/cloudflare_workers_types.d.ts';
5 |
6 | Deno.test({
7 | name: 'SqliteD1Database',
8 | ignore: (await Deno.permissions.query({ name: 'net' })).state !== 'granted',
9 | async fn() {
10 | const db = SqliteD1Database.provider(() => ':memory:')(crypto.randomUUID()) as SqliteD1Database;
11 | await runSimpleD1DatabaseScenario(db);
12 | db.close();
13 | }
14 | });
15 |
16 | export async function runSimpleD1DatabaseScenario(db: D1Database) {
17 | await db.prepare('create table t1(id int primary key, name text)').all();
18 | {
19 | const result = await db.prepare(`insert into t1(id, name) values(1, 'one')`).all();
20 | assert(result.success);
21 | assert(result.meta.changed_db);
22 | assertEquals(result.meta.changes, 1);
23 | assertEquals(result.results, []);
24 | }
25 | {
26 | const result = await db.prepare(`select * from t1`).all();
27 | assert(result.success);
28 | assert(!result.meta.changed_db);
29 | assertEquals(result.meta.changes, 0);
30 | assertEquals(result.results, [ { id: 1, name: 'one' } ]);
31 | }
32 | {
33 | const result = await db.prepare(`select * from t1`).raw();
34 | assertEquals(result, [ [ 1, 'one' ] ]);
35 | }
36 | {
37 | const result = await db.prepare(`select * from t1`).raw({ columnNames: true });
38 | assertEquals(result, [ [ 'id', 'name' ], [ 1, 'one' ] ]);
39 | }
40 | {
41 | const result = await db.prepare(`select * from t1`).first();
42 | assertEquals(result, { id: 1, name: 'one' });
43 | }
44 | {
45 | const result = await db.prepare(`select * from t1`).first('id');
46 | assertEquals(result, 1);
47 | }
48 | {
49 | const stmt = db.prepare(`insert into t1(id, name) values(?, ?)`);
50 | const result = await db.batch([ stmt.bind(2, 'two'), stmt.bind(3, 'three') ]);
51 | assertEquals(result.length, 2);
52 | const [ first, second ] = result;
53 | assertEquals(first.meta.last_row_id, 2);
54 | assertEquals(second.meta.last_row_id, 3);
55 | }
56 | {
57 | const result = await db.prepare(`select count(*) c from t1`).first('c');
58 | assertEquals(result, 3);
59 | }
60 | {
61 | const result = await db.exec(`delete from t1\nselect * from sqlite_schema;\nselect * from sqlite_schema ;\r\n`);
62 | assertEquals(result.count, 3);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/cli/sqlite_dbpath_for_instance.ts:
--------------------------------------------------------------------------------
1 | // only import if used
2 | // import { DenoDir } from 'https://esm.sh/jsr/@deno/cache-dir@0.11.1';
3 | import { ComputeDbPathForInstance } from '../common/storage/sqlite_durable_object_storage.ts';
4 | import { join } from './deps_cli.ts';
5 |
6 | export const sqliteDbPathForInstance: ComputeDbPathForInstance = async ({ container, className, id }: { container: string, className: string, id: string }) => {
7 | return join(await computeSqliteDosqlDirectory(), `${container}-${className}-${id}.db`);
8 | }
9 |
10 | export async function computeSqliteDosqlDirectory(): Promise {
11 | const { DenoDir } = await import('https://esm.sh/jsr/@deno/cache-dir@0.11.1' + '');
12 | const root = DenoDir.tryResolveRootPath(undefined);
13 | if (root === undefined) throw new Error(`Unable to resolve deno cache dir`);
14 | const denoflareDosqlDir = join(root, 'denoflare', 'dosql');
15 | await Deno.mkdir(denoflareDosqlDir, { recursive: true });
16 | return denoflareDosqlDir;
17 | }
18 |
--------------------------------------------------------------------------------
/cli/versions.ts:
--------------------------------------------------------------------------------
1 | export function versionCompare(lhs: string, rhs: string): number {
2 | if (lhs === rhs) return 0;
3 | const lhsTokens = lhs.split('.');
4 | const rhsTokens = rhs.split('.');
5 | for (let i = 0; i < Math.max(lhsTokens.length, rhsTokens.length); i++) {
6 | const lhsNum = parseInt(lhsTokens[i] ?? '0');
7 | const rhsNum = parseInt(rhsTokens[i] ?? '0');
8 | if (lhsNum < rhsNum) return -1;
9 | if (lhsNum > rhsNum) return 1;
10 | }
11 | return 0;
12 | }
13 |
--------------------------------------------------------------------------------
/cli/versions_test.ts:
--------------------------------------------------------------------------------
1 | import { assert } from 'https://deno.land/std@0.224.0/assert/assert.ts';
2 |
3 | import { versionCompare } from './versions.ts';
4 |
5 | Deno.test({
6 | name: 'versions',
7 | fn: () => {
8 | assert(versionCompare('1.0.0', '1.0.0') === 0);
9 | assert(versionCompare('1.21.3', '1.22.0') < 0);
10 | assert(versionCompare('1.21.3', '1.22') < 0);
11 | assert(versionCompare('1.21.4', '1.21') > 0);
12 | assert(versionCompare('1.21.4', '1.21.3') > 0);
13 | assert(versionCompare('1.21.3', '2.0') < 0);
14 | }
15 | });
16 |
--------------------------------------------------------------------------------
/cli/wasm_crypto.ts:
--------------------------------------------------------------------------------
1 | import { Bytes } from '../common/bytes.ts';
2 | import { versionCompare } from './versions.ts';
3 |
4 | const { crypto: WasmCrypto } = await import(versionCompare(Deno.version.deno, '2.0.0') >= 0 ? 'jsr:@std/crypto@1.0.3': 'https://deno.land/std@0.224.0/crypto/mod.ts');
5 |
6 | export async function computeStreamingSha256(stream: ReadableStream): Promise {
7 | const arr = await WasmCrypto.subtle.digest('SHA-256', stream);
8 | return new Bytes(new Uint8Array(arr));
9 | }
10 |
11 | export async function computeStreamingMd5(stream: ReadableStream): Promise {
12 | const arr = await WasmCrypto.subtle.digest('MD5', stream);
13 | return new Bytes(new Uint8Array(arr));
14 | }
15 |
16 | export async function computeMd5(input: Bytes | string): Promise {
17 | const bytes = typeof input === 'string' ? Bytes.ofUtf8(input) : input;
18 | const arr = await WasmCrypto.subtle.digest('MD5', bytes.array());
19 | return new Bytes(new Uint8Array(arr));
20 | }
21 |
--------------------------------------------------------------------------------
/common/analytics/r2_costs_test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from 'https://deno.land/std@0.224.0/assert/assert_equals.ts';
2 | import { computeCosts } from './r2_costs.ts';
3 |
4 | const DEBUG = false;
5 |
6 | Deno.test({
7 | name: 'r2.computeCosts',
8 | fn: () => {
9 | {
10 | const { classAOperationsCost, classBOperationsCost, storageGbMo, storageCost, totalCost } = computeCosts({ classAOperations: 499751, classBOperations: 430084, storageGb: 500, excludeFreeUsage: false });
11 | if (DEBUG) console.log({ classAOperationsCost, classBOperationsCost, storageGbMo, storageCost, totalCost });
12 | assertEquals(Math.round(totalCost * 100) / 100, 2.65)
13 | }
14 | {
15 | const { classAOperationsCost, classBOperationsCost, storageGbMo, storageCost, totalCost } = computeCosts({ classAOperations: 499751, classBOperations: 430084, storageGb: 500, excludeFreeUsage: true });
16 | if (DEBUG) console.log({ classAOperationsCost, classBOperationsCost, storageGbMo, storageCost, totalCost });
17 | assertEquals(Math.round(totalCost * 100) / 100, 0.1);
18 | }
19 | }
20 | });
21 |
--------------------------------------------------------------------------------
/common/aws/emailer.ts:
--------------------------------------------------------------------------------
1 | import { Bytes } from '../bytes.ts';
2 | import { AwsCall, AwsCallContext, signAwsCallV4 } from '../r2/r2.ts';
3 |
4 | export type EmailerOpts = { readonly awsCallContext: AwsCallContext, readonly emailFrom: string, readonly emailTo: string, readonly region?: string, readonly endpoint?: string };
5 |
6 | export type Email = { readonly subject: string, readonly text: string };
7 |
8 | export type SendEmailResponse = { readonly status: number };
9 |
10 | export class Emailer {
11 |
12 | private readonly opts: EmailerOpts;
13 |
14 | constructor(opts: EmailerOpts) {
15 | this.opts = opts;
16 | }
17 |
18 | async send(email: Email) {
19 | const { subject, text } = email;
20 | const { emailFrom: source, emailTo: to, awsCallContext: context, region, endpoint } = this.opts;
21 | const response = await sendEmail({ source, to, subject, text, context, region, endpoint });
22 | if (response.status !== 200) throw new Error(`Error status ${response.status} sending email, expected 200`);
23 | }
24 |
25 | }
26 |
27 | export async function sendEmail({ source, to, subject, text, context, region = 'us-east-1', endpoint = 'https://email.us-east-1.amazonaws.com' }: { source: string, to: string, subject: string, text: string, context: AwsCallContext, region?: string, endpoint?: string }): Promise {
28 | const params = new URLSearchParams();
29 | params.set('Action', 'SendEmail');
30 | params.set('Destination.ToAddresses.member.1', to);
31 | params.set('Message.Body.Text.Charset', 'UTF-8');
32 | params.set('Message.Body.Text.Data', text);
33 | params.set('Message.Subject.Charset', 'UTF-8');
34 | params.set('Message.Subject.Data', subject);
35 | params.set('Source', source);
36 |
37 | const body = Bytes.ofUtf8(params.toString());
38 |
39 | const call: AwsCall = {
40 | method: 'POST',
41 | service: 'email',
42 | region,
43 | url: new URL(endpoint),
44 | headers: new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' }),
45 | body,
46 | };
47 |
48 | const { signedHeaders, bodyInfo } = await signAwsCallV4(call, context);
49 |
50 | const request = new Request(call.url.toString(), { method: call.method, headers: signedHeaders, body: bodyInfo.body });
51 | const { status } = await fetch(request);
52 | return { status };
53 | }
54 |
--------------------------------------------------------------------------------
/common/aws/lambda_runtime.d.ts:
--------------------------------------------------------------------------------
1 | export interface LambdaWorkerContext {
2 | readonly lambda: LambdaWorkerInfo;
3 | waitUntil(promise: Promise): void;
4 | passThroughOnException(): void;
5 | }
6 |
7 | export interface LambdaWorkerInfo {
8 | readonly request: LambdaHttpRequest;
9 | readonly times: LambdaWorkerTimes;
10 | readonly env: Record;
11 | readonly awsRequestId: string;
12 | readonly invokedFunctionArn: string;
13 | readonly traceId: string;
14 | }
15 |
16 | export interface LambdaWorkerTimes {
17 | readonly bootstrap: number;
18 | readonly start: number;
19 | readonly init: number;
20 | readonly request: number;
21 | readonly dispatch: number;
22 | readonly deadline: number;
23 | }
24 |
25 | // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html#http-api-develop-integrations-lambda.proxy-format
26 |
27 | export interface LambdaHttpRequest {
28 | readonly version: string; // 2.0
29 | readonly routeKey: string; // $default
30 | readonly rawPath: string; // /path
31 | readonly rawQueryString: string // foo=bar or ""
32 | readonly cookies?: string[];
33 | readonly headers: Record,
34 | readonly requestContext: {
35 | readonly accountId: string; // anonymous
36 | readonly apiId: string; // <32-loweralphanum>
37 | readonly domainName: string; // <32-loweralphanum>.lambda-url..on.aws
38 | readonly domainPrefix: string; // <32-loweralphanum>
39 | readonly http: {
40 | readonly method: string; // GET
41 | readonly path: string; // /path
42 | readonly protocol: string; // HTTP/1.1
43 | readonly sourceIp: string;
44 | readonly userAgent: string;
45 | },
46 | readonly requestId: string; // v4 guid
47 | readonly routeKey: string; // $default
48 | readonly stage: string; // $default
49 | readonly time: string; // 08/Jul/2023:01:33:58 +0000
50 | readonly timeEpoch: number; // 1688780038228
51 | },
52 | readonly body?: string;
53 | readonly isBase64Encoded: boolean;
54 | }
55 |
--------------------------------------------------------------------------------
/common/cloudflare_email.ts:
--------------------------------------------------------------------------------
1 | import { EmailMessageConstructable, EmailMessage } from './cloudflare_workers_types.d.ts';
2 |
3 | export function cloudflareEmail(): CloudflareEmail {
4 | // deno-lint-ignore no-explicit-any
5 | const provider = (globalThis as any).__cloudflareEmailProvider;
6 | if (typeof provider === 'function') return provider();
7 | return { EmailMessage: StubEmailMessage };
8 | }
9 |
10 | export interface CloudflareEmail {
11 | EmailMessage: EmailMessageConstructable,
12 | }
13 |
14 | class StubEmailMessage implements EmailMessage {
15 | readonly from: string;
16 | readonly to: string;
17 | get headers(): Headers { throw new Error(); }
18 | get raw(): ReadableStream { throw new Error(); }
19 | get rawSize(): number { throw new Error(); }
20 |
21 | constructor(from: string, to: string, _raw: ReadableStream | string) {
22 | this.from = from;
23 | this.to = to;
24 | }
25 |
26 | setReject(reason: string): void {
27 | throw new Error(`setReject(${JSON.stringify({ reason })})`);
28 | }
29 |
30 | forward(rcptTo: string, headers?: Headers | undefined): Promise {
31 | throw new Error(`forward(${JSON.stringify({ rcptTo, headers })})`);
32 | }
33 |
34 | reply(message: EmailMessage): Promise {
35 | throw new Error(`forward(${JSON.stringify({ message })})`);
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/common/cloudflare_pipeline_transform.ts:
--------------------------------------------------------------------------------
1 | import type { PipelineTransform } from './cloudflare_workers_types.d.ts';
2 |
3 | export function cloudflarePipelineTransform() {
4 | return { PipelineTransform: StubPipelineTransform }
5 | }
6 |
7 | class StubPipelineTransform implements PipelineTransform {
8 | constructor(ctx: unknown, env: unknown) {
9 | }
10 |
11 | transformJson(_data: object[]): Promise
\\s+()', 'si').exec(text);
18 | if (m) {
19 | const table = m[1];
20 | const pattern = /(.*?)<\/p>.*?(.*?)<\/p>.*?
(.*?)<\/p>.*?
(.*?)<\/p>.*?<\/tr>/gs;
21 | while (null != (m = pattern.exec(table))) {
22 | const codeStr = despan(m[1]);
23 | const name = fixWhitespace(despan(m[3].trim()));
24 | const description = fixWhitespace(m[4]);
25 | const code = parseInt(codeStr);
26 | console.log(` ${code}: [ '${name}', '${description}' ],`);
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/devcli/devcli_r2.ts:
--------------------------------------------------------------------------------
1 | //
2 |
3 | import { ApiR2Bucket } from '../cli/api_r2_bucket.ts';
4 | import { CLI_USER_AGENT } from '../cli/cli_common.ts';
5 | import { loadConfig, resolveProfile } from '../cli/config_loader.ts';
6 | import { R2 } from '../common/r2/r2.ts';
7 |
8 | export async function tmp(args: (string | number)[], options: Record) {
9 | const [ bucketName, key ] = args;
10 | if (typeof bucketName !== 'string') throw new Error();
11 | if (typeof key !== 'string') throw new Error();
12 |
13 | const verbose = !!options.verbose;
14 | if (verbose) {
15 | R2.DEBUG = true;
16 | }
17 |
18 | const config = await loadConfig(options);
19 | const profile = await resolveProfile(config, options);
20 | const bucket = await ApiR2Bucket.ofProfile(profile, bucketName, CLI_USER_AGENT);
21 | const { body } = await fetch('https://yahoo.com');
22 | const res = await bucket.put(key, body);
23 | console.log(res);
24 | // if (res) console.log(await res.text());
25 | }
26 |
--------------------------------------------------------------------------------
/devcli/tsc.ts:
--------------------------------------------------------------------------------
1 | import { walk } from 'https://deno.land/std@0.224.0/fs/mod.ts'; // isolated for sharing
2 | import { spawn } from '../cli/spawn.ts';
3 |
4 | export type TscResult = { code: number, success: boolean, out: string, err: string, output: Record };
5 |
6 | export async function runTsc(opts: { files: string[], compilerOptions: Record, tscPath?: string }): Promise {
7 | const { files, compilerOptions, tscPath = '/usr/local/bin/tsc' } = opts;
8 | const tsconfigFile = await Deno.makeTempFile({ prefix: 'run-tsc-tsconfig', suffix: '.json'});
9 | const outDir = await Deno.makeTempDir({ prefix: 'run-tsc-outdir', suffix: '.json'});
10 | compilerOptions.outDir = outDir;
11 | try {
12 | await Deno.writeTextFile(tsconfigFile, JSON.stringify({ files, compilerOptions }, undefined, 2));
13 | const { code, success, stdout, stderr } = await spawn(tscPath, {
14 | args: [
15 | '--project', tsconfigFile,
16 | ],
17 | env: {
18 | NO_COLOR: '1', // to make parsing the output easier
19 | }
20 | });
21 | const out = new TextDecoder().decode(stdout);
22 | const err = new TextDecoder().decode(stderr);
23 | const output: Record = {};
24 | for await (const entry of walk(outDir, { maxDepth: 1 })) {
25 | if (!entry.isFile) continue;
26 | const { name, path } = entry;
27 | output[name] = await Deno.readTextFile(path);
28 | }
29 | return { code, success, out, err, output };
30 | } finally {
31 | await Deno.remove(tsconfigFile);
32 | await Deno.remove(outDir, { recursive: true });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/hello-wasm-worker/deps.ts:
--------------------------------------------------------------------------------
1 | export { importWasm } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_wasm.ts';
2 |
--------------------------------------------------------------------------------
/examples/hello-wasm-worker/hello.ts:
--------------------------------------------------------------------------------
1 | import { importWasm } from './deps.ts';
2 | import { callSub } from './sub/sub.ts'; // also works with relative module specifiers within relative imports
3 | const module = await importWasm(import.meta.url, './hello.wasm'); // rewritten to: import module from './hello.wasm';
4 |
5 | export default {
6 |
7 | fetch(): Response {
8 | try {
9 | // call hello.wasm
10 | const instance = new WebAssembly.Instance(module);
11 | const result = (instance.exports.main as CallableFunction)();
12 |
13 | // call hello2.wasm
14 | const subresult = callSub();
15 |
16 | const html = `Result from hello.wasm call: ${result}
Result from hello2.wasm call: ${subresult}
`;
17 | return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
18 | } catch (e) {
19 | const html = `${e}`;
20 | return new Response(html, { status: 500, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
21 | }
22 | }
23 |
24 | };
25 |
--------------------------------------------------------------------------------
/examples/hello-wasm-worker/hello.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/hello-wasm-worker/hello.wasm
--------------------------------------------------------------------------------
/examples/hello-wasm-worker/sub/hello2.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/hello-wasm-worker/sub/hello2.wasm
--------------------------------------------------------------------------------
/examples/hello-wasm-worker/sub/sub.ts:
--------------------------------------------------------------------------------
1 | import { importWasm } from '../deps.ts';
2 | const module = await importWasm(import.meta.url, './hello2.wasm'); // rewritten to: import module from './sub/hello2.wasm';
3 |
4 | export function callSub(): number {
5 | // call hello2.wasm
6 | const instance = new WebAssembly.Instance(module);
7 | return (instance.exports.main as CallableFunction)();
8 | }
9 |
--------------------------------------------------------------------------------
/examples/hello-worker/deps.ts:
--------------------------------------------------------------------------------
1 | export type { IncomingRequestCf } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts';
2 |
--------------------------------------------------------------------------------
/examples/hello-worker/hello.ts:
--------------------------------------------------------------------------------
1 | import { IncomingRequestCf } from './deps.ts';
2 |
3 | // simplest possible module worker, echo back the client's city, as determined by Cloudflare
4 | export default {
5 |
6 | fetch(request: IncomingRequestCf): Response {
7 | const html = `Hello ${'city' in request.cf ? request.cf.city : 'nowhere'}!
`;
8 | return new Response(html, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
9 | }
10 |
11 | };
12 |
--------------------------------------------------------------------------------
/examples/image-demo-worker/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.lint": true,
4 | "deno.unstable": true
5 | }
--------------------------------------------------------------------------------
/examples/image-demo-worker/404.ts:
--------------------------------------------------------------------------------
1 | export function compute404() {
2 | const title = 'Not found';
3 | const body = `
4 |
5 |
6 |
Not found
7 |
8 | `;
9 | return { title, body };
10 | }
11 |
--------------------------------------------------------------------------------
/examples/image-demo-worker/README.md:
--------------------------------------------------------------------------------
1 | # Transform Images in a Cloudflare Worker
2 |
3 | This example Cloudflare Worker fetches an image, applies transforms from the [Photon](https://silvia-odwyer.github.io/photon/) wasm library, then encodes to PNG using wasm from the [pngs](https://github.com/denosaurs/pngs) deno module.
4 |
5 | You can deploy the [module worker](worker.ts) to your own Cloudflare account by adding the script def to your [`.denoflare`](https://denoflare.dev/cli/configuration) file...
6 |
7 | ```jsonc
8 | "image-demo": {
9 | "path": "/path/to/this/image-demo-worker/worker.ts",
10 | "bindings": {
11 | "unsplashAppName": { "value": "image_demo_worker" },
12 | "unsplashIxid": { "value": "MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8" },
13 | },
14 | "usageModel": "unbound",
15 | },
16 | ```
17 | ... then pushing to Cloudflare
18 | ```sh
19 | denoflare push image-demo
20 | ```
21 |
22 | It runs as an `unbundled` worker, but you will eventually hit the Workers CPU limits with some of the transforms, so it's recommended to use `unbound` as the usage model.
23 |
24 | The size of the uploaded script + favicons/metadata images + all wasm modules is 734kb compressed.
25 |
26 | ---
27 | ## WebAssembly notes
28 | This worker uses Denoflare's new [WebAssembly import support](https://denoflare.dev/reference/wasm), the unmodified .wasm files from their respective third party libraries, and slightly tweaked js binding files (see the `/ext` subdir).
29 |
30 | The WebAssembly libraries are loaded and used in the [`/img`](img.ts) endpoint.
31 |
--------------------------------------------------------------------------------
/examples/image-demo-worker/app_manifest.d.ts:
--------------------------------------------------------------------------------
1 | export interface AppManifest {
2 | /**
3 | * https://w3c.github.io/manifest/#short_name-member
4 | * maximum of 12 characters recommended per https://developer.chrome.com/extensions/manifest/name
5 | * used: android launcher icon title
6 | */
7 | 'short_name': string,
8 |
9 | /**
10 | * https://w3c.github.io/manifest/#name-member
11 | * maximum of 45 characters per https://developer.chrome.com/extensions/manifest/name
12 | * used: app install banner android offline splash screen
13 | */
14 | name: string,
15 |
16 | /**
17 | * https://w3c.github.io/manifest/#description-member
18 | * used ??
19 | */
20 | description: string,
21 |
22 | /**
23 | * https://w3c.github.io/manifest/#icons-member
24 | */
25 | icons: readonly AppManifestIcon[],
26 |
27 | /**
28 | * https://w3c.github.io/manifest/#theme_color-member
29 | */
30 | 'theme_color': string,
31 |
32 | /**
33 | * https://w3c.github.io/manifest/#background_color-member
34 | * should match page, also used for splash screen
35 | */
36 | 'background_color': string,
37 |
38 | /**
39 | * https://w3c.github.io/manifest/#display-member
40 | */
41 | display: 'fullscreen' | 'standalone' | 'minimal-ui' | 'browser',
42 |
43 | /**
44 | * https://w3c.github.io/manifest/#start_url-member
45 | */
46 | 'start_url': string,
47 |
48 | /**
49 | * https://w3c.github.io/manifest/#lang-member
50 | */
51 | lang: 'en-US',
52 |
53 | /**
54 | * https://w3c.github.io/manifest/#dir-member
55 | */
56 | dir: 'ltr',
57 |
58 | /**
59 | * https://w3c.github.io/manifest/#scope-member
60 | */
61 | scope?: string,
62 |
63 | }
64 |
65 | export interface AppManifestIcon {
66 |
67 | /**
68 | * url to image resource
69 | */
70 | src: string;
71 |
72 | /**
73 | * e.g. "512x512", or "16x16 32x32 48x48"
74 | *
75 | * https://w3c.github.io/manifest/#declaring-multiple-icons
76 | */
77 | sizes?: string;
78 |
79 | /**
80 | * MIME type
81 | *
82 | * https://w3c.github.io/manifest/#declaring-multiple-icons
83 | */
84 | type?: string;
85 |
86 | /**
87 | * https://w3c.github.io/manifest/#purpose-member
88 | */
89 | purpose?: 'monochrome' | 'maskable' | 'any';
90 | }
91 |
--------------------------------------------------------------------------------
/examples/image-demo-worker/cli.ts:
--------------------------------------------------------------------------------
1 | import { parseFlags } from './deps_cli.ts';
2 | import { computeTransforms } from './cli_transform_generator.ts';
3 |
4 | const args = parseFlags(Deno.args);
5 |
6 | if (args._.length > 0) {
7 | await imageDemo(args._, args);
8 | Deno.exit(0);
9 | }
10 |
11 | dumpHelp();
12 |
13 | Deno.exit(1);
14 |
15 | //
16 |
17 | async function imageDemo(args: (string | number)[], options: Record) {
18 | const command = args[0];
19 | const fn = { dumpTransforms, unsplash }[command];
20 | if (options.help || !fn) {
21 | dumpHelp();
22 | return;
23 | }
24 | await fn(args.slice(1), options);
25 | }
26 |
27 | async function dumpTransforms(_args: (string | number)[]) {
28 | const transforms = await computeTransforms();
29 | console.log(JSON.stringify(transforms, undefined, 2));
30 | }
31 |
32 | async function unsplash(args: (string | number)[], options: Record) {
33 | const photoId = args[0];
34 | if (typeof photoId !== 'string') throw new Error(`Must provide photo id arg`);
35 |
36 | const accessKeyId = options['access-key-id'];
37 | if (typeof accessKeyId !== 'string') throw new Error(`Must provide --access-key-id`);
38 | const res = await fetch(`https://api.unsplash.com/photos/${photoId}`, { headers: { authorization: `Client-ID ${accessKeyId}`} });
39 | if (res.status !== 200 || res.headers.get('content-type') !== 'application/json') {
40 | console.log(res);
41 | console.log(await res.text());
42 | return;
43 | }
44 | const obj = await res.json();
45 | console.log(JSON.stringify(obj, undefined, 2));
46 | }
47 |
48 | function dumpHelp() {
49 | const lines = [
50 | `image-demo-cli`,
51 | 'Tools for developing image-demo',
52 | '',
53 | 'USAGE:',
54 | '',
55 | 'FLAGS:',
56 | ' -h, --help Prints help information',
57 | ' --verbose Toggle verbose output (when applicable)',
58 | '',
59 | 'ARGS:',
60 | ];
61 | for (const line of lines) {
62 | console.log(line);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/examples/image-demo-worker/deps_cli.ts:
--------------------------------------------------------------------------------
1 | export { parse as parseFlags } from 'https://deno.land/std@0.179.0/flags/mod.ts';
2 |
--------------------------------------------------------------------------------
/examples/image-demo-worker/deps_worker.ts:
--------------------------------------------------------------------------------
1 | export { Bytes } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/bytes.ts';
2 | export type { IncomingRequestCf } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts';
3 | export { importText } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_text.ts';
4 | export { importBinary } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_binary.ts';
5 | export { importWasm } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_wasm.ts';
6 |
--------------------------------------------------------------------------------
/examples/image-demo-worker/ext/photon_rs_bg.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/image-demo-worker/ext/photon_rs_bg.wasm
--------------------------------------------------------------------------------
/examples/image-demo-worker/ext/pngs.ts:
--------------------------------------------------------------------------------
1 | // adapted from https://deno.land/x/pngs@0.1.1/mod.ts
2 |
3 | /* OLD
4 | import init, {
5 | decode as wasmDecode,
6 | encode as wasmEncode,
7 | source,
8 | } from "./wasm.js";
9 |
10 | await init(source);
11 | */
12 |
13 | // NEW
14 |
15 | import { importWasm } from '../deps_worker.ts';
16 | const pngsModule = await importWasm(import.meta.url, './pngs_bg.wasm');
17 | import pngsInit, { decode as wasmDecode, encode as wasmEncode } from './pngs_bg.js';
18 | await pngsInit(pngsModule);
19 |
20 | //
21 |
22 | type ValueOf = T[keyof T];
23 |
24 | export const ColorType = {
25 | Grayscale: 0,
26 | RGB: 2,
27 | Indexed: 3,
28 | GrayscaleAlpha: 4,
29 | RGBA: 6,
30 | };
31 |
32 | export const BitDepth = {
33 | One: 1,
34 | Two: 2,
35 | Four: 4,
36 | Eight: 8,
37 | Sixteen: 16,
38 | };
39 |
40 | export const Compression = {
41 | Default: 0,
42 | Fast: 1,
43 | Best: 2,
44 | Huffman: 3,
45 | Rle: 4,
46 | };
47 |
48 | export const FilterType = {
49 | NoFilter: 0,
50 | Sub: 1,
51 | Up: 2,
52 | Avg: 3,
53 | Paeth: 4,
54 | };
55 |
56 | export interface DecodeResult {
57 | image: Uint8Array;
58 | width: number;
59 | height: number;
60 | colorType: ValueOf;
61 | bitDepth: ValueOf;
62 | lineSize: number;
63 | }
64 |
65 | export function encode(
66 | image: Uint8Array,
67 | width: number,
68 | height: number,
69 | options?: {
70 | palette?: Uint8Array;
71 | trns?: Uint8Array;
72 | color?: ValueOf;
73 | depth?: ValueOf;
74 | compression?: ValueOf;
75 | filter?: ValueOf;
76 | stripAlpha?: boolean;
77 | },
78 | ): Uint8Array {
79 | if (options?.stripAlpha) {
80 | image = image.filter((_, i) => (i + 1) % 4);
81 | }
82 |
83 | return wasmEncode(
84 | image,
85 | width,
86 | height,
87 | options?.palette,
88 | options?.trns,
89 | options?.color ?? ColorType.RGBA,
90 | options?.depth ?? BitDepth.Eight,
91 | options?.compression,
92 | options?.filter,
93 | );
94 | }
95 |
96 | export function decode(image: Uint8Array): DecodeResult {
97 | const res = wasmDecode(image);
98 |
99 | return {
100 | image: new Uint8Array(res.image),
101 | width: res.width,
102 | height: res.height,
103 | colorType: res.colorType,
104 | bitDepth: res.bitDepth,
105 | lineSize: res.lineSize,
106 | };
107 | }
--------------------------------------------------------------------------------
/examples/image-demo-worker/ext/pngs_bg.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/image-demo-worker/ext/pngs_bg.wasm
--------------------------------------------------------------------------------
/examples/image-demo-worker/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/image-demo-worker/favicon.ico
--------------------------------------------------------------------------------
/examples/image-demo-worker/favicons.ts:
--------------------------------------------------------------------------------
1 | import { Theme } from './theme.ts';
2 | import { importBinary } from './deps_worker.ts';
3 |
4 | export const FAVICON_SVG = `
5 |
6 | `;
10 |
11 | export const FAVICON_VERSION = '2';
12 |
13 | export const FAVICON_ICO = await importBinary(import.meta.url, './favicon.ico');
14 |
--------------------------------------------------------------------------------
/examples/image-demo-worker/html.ts:
--------------------------------------------------------------------------------
1 | export function encodeHtml(value: string): string {
2 | return value.replace(/&/g, '&')
3 | .replace(/\"/g, '"')
4 | .replace(/'/g, ''')
5 | .replace(//g, '>');
7 | }
8 |
9 | export interface HtmlContribution {
10 | readonly title: string;
11 | readonly headContribution?: string;
12 | readonly body: string;
13 | }
14 |
--------------------------------------------------------------------------------
/examples/image-demo-worker/theme.ts:
--------------------------------------------------------------------------------
1 | export class Theme {
2 | // tailwind sky
3 | static readonly primaryColor200Hex = '#0284c7';
4 | static readonly primaryColor300Hex = '#0369a1'; // primary
5 | static readonly primaryColor900Hex = '#0c4a6e'; // darkest
6 |
7 | static readonly backgroundColorHex = '#101010';
8 | }
9 |
--------------------------------------------------------------------------------
/examples/image-demo-worker/transforms.ts:
--------------------------------------------------------------------------------
1 | export interface Transform {
2 | readonly name: string;
3 | readonly parameters: readonly TransformParameter[];
4 | }
5 |
6 | export interface TransformParameter {
7 | readonly name: string;
8 | readonly type: TransformParameterType;
9 | }
10 |
11 | export type TransformParameterType = FloatParameterType | IntParameterType | EnumParameterType | RgbParameterType | ChannelParameterType;
12 |
13 | export interface FloatParameterType {
14 | kind: 'float';
15 | min: number;
16 | max: number;
17 | default?: number;
18 | }
19 |
20 | export interface IntParameterType {
21 | kind: 'int';
22 | min: number;
23 | max: number;
24 | default?: number;
25 | }
26 |
27 | export interface EnumParameterType {
28 | kind: 'enum';
29 | values: readonly string[];
30 | }
31 |
32 | export interface RgbParameterType {
33 | kind: 'rgb';
34 | }
35 |
36 | export interface ChannelParameterType {
37 | kind: 'channel';
38 | // 0 = red, 1 = green, 2 = blue
39 | }
40 |
--------------------------------------------------------------------------------
/examples/image-demo-worker/twitter.ts:
--------------------------------------------------------------------------------
1 | import { importBinary } from './deps_worker.ts';
2 |
3 | // https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/summary-card-with-large-image
4 |
5 | // Images for this Card support an aspect ratio of 2:1 with minimum dimensions of 300x157 or maximum of 4096x4096 pixels.
6 | // Images must be less than 5MB in size. JPG, PNG, WEBP and GIF formats are supported.
7 | // Only the first frame of an animated GIF will be used. SVG is not supported.
8 | export const TWITTER_IMAGE_PNG = await importBinary(import.meta.url, './twitter_image.png');
9 | export const TWITTER_IMAGE_VERSION = '2';
10 |
--------------------------------------------------------------------------------
/examples/image-demo-worker/twitter_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/image-demo-worker/twitter_image.png
--------------------------------------------------------------------------------
/examples/image-demo-worker/worker_env.d.ts:
--------------------------------------------------------------------------------
1 | export interface WorkerEnv {
2 | readonly twitter?: string;
3 | readonly unsplashAppName?: string;
4 | readonly unsplashIxid?: string;
5 | readonly imgOrigin?: string;
6 | readonly authorName?: string;
7 | readonly authorHref?: string;
8 | }
9 |
--------------------------------------------------------------------------------
/examples/keyspace-worker/deps.ts:
--------------------------------------------------------------------------------
1 | export type { IncomingRequestCf } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts';
2 | export { Bytes } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/bytes.ts';
3 | export type { Kv, KvCommitError, KvCommitResult, KvKey, KvService, KvU64 } from 'https://raw.githubusercontent.com/skymethod/kv-connect-kit/f895fd50d2f9f37309aab0f0bb3080e3a9a2a5cf/kv_types.ts';
4 | export { makeRemoteService } from 'https://raw.githubusercontent.com/skymethod/kv-connect-kit/f895fd50d2f9f37309aab0f0bb3080e3a9a2a5cf/client.ts';
5 |
--------------------------------------------------------------------------------
/examples/keyspace-worker/env.ts:
--------------------------------------------------------------------------------
1 | export type Env = {
2 | adminIp?: string,
3 | remoteKvUrl?: string,
4 | remoteKvAccessToken?: string,
5 | denoOrigin?: string,
6 | cloudflareOrigin?: string,
7 | denoCfAnalyticsToken?: string,
8 | cloudflareCfAnalyticsToken?: string,
9 | };
10 |
--------------------------------------------------------------------------------
/examples/mqtt-demo-worker/deps.ts:
--------------------------------------------------------------------------------
1 | export { importText } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_text.ts';
2 |
--------------------------------------------------------------------------------
/examples/mqtt-demo-worker/worker.ts:
--------------------------------------------------------------------------------
1 | import { importText } from './deps.ts';
2 | const mqttDemoHtml = await importText(import.meta.url, './static/mqtt-demo.html');
3 | const mqttDemoJs = await importText(import.meta.url, './static/mqtt-demo.js');
4 |
5 | export default {
6 |
7 | fetch(request: Request): Response {
8 | const { method } = request;
9 | if (method !== 'GET') return new Response(`method not allowed: ${method}`, { status: 405 });
10 |
11 | const { pathname } = new URL(request.url);
12 | if (pathname === '/') return new Response(mqttDemoHtml, { headers: { 'content-type': 'text/html; charset=utf-8' } });
13 | if (pathname === '/mqtt-demo.js') return new Response(mqttDemoJs, { headers: { 'content-type': 'text/javascript; charset=utf-8' } });
14 |
15 | return new Response('not found', { status: 404 });
16 | }
17 |
18 | };
19 |
--------------------------------------------------------------------------------
/examples/multiplat-worker/deps.ts:
--------------------------------------------------------------------------------
1 | export { importWasm } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_wasm.ts';
2 | export { importText } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_text.ts';
3 | export { importBinary } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_binary.ts';
4 | export type { IncomingRequestCf } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts';
5 |
--------------------------------------------------------------------------------
/examples/multiplat-worker/static/deno.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/multiplat-worker/static/deno.png
--------------------------------------------------------------------------------
/examples/multiplat-worker/static/example.txt:
--------------------------------------------------------------------------------
1 | Lorem ipsum dolor sit amet
--------------------------------------------------------------------------------
/examples/multiplat-worker/static/hello.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skymethod/denoflare/57eba6c752bb61edee12f5352bb15ea9a1fb81f0/examples/multiplat-worker/static/hello.wasm
--------------------------------------------------------------------------------
/examples/r2-presigned-url-worker/deps.ts:
--------------------------------------------------------------------------------
1 | export type { IncomingRequestCf, R2Bucket, R2Object, R2ObjectBody, R2Range, R2GetOptions, R2Conditional, R2Objects, R2ListOptions } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts';
2 | export { Bytes } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/bytes.ts';
3 | export { computeExpectedAwsSignature, tryParseAmazonDate, R2 } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/r2/r2.ts';
4 | export type { AwsCall } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/r2/r2.ts';
5 |
--------------------------------------------------------------------------------
/examples/r2-presigned-url-worker/worker_env.d.ts:
--------------------------------------------------------------------------------
1 | import { R2Bucket } from './deps.ts';
2 |
3 | export interface WorkerEnv {
4 | readonly bucket: R2Bucket;
5 | readonly flags?: string;
6 | readonly allowIps?: string;
7 | readonly denyIps?: string;
8 | readonly credentials?: string;
9 | readonly maxSkewMinutes?: string; // default: 15
10 | readonly maxExpiresMinutes?: string; // default: 7 days
11 | readonly virtualHostname?: string; // e.g. custombucketname.example.com, useful if using vhost-style presigning
12 | }
13 |
--------------------------------------------------------------------------------
/examples/r2-public-read-worker/deps.ts:
--------------------------------------------------------------------------------
1 | export type { IncomingRequestCf, R2Bucket, R2Object, R2ObjectBody, R2Range, R2GetOptions, R2Conditional, R2Objects, R2ListOptions } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts';
2 | export { encodeXml } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/xml_util.ts';
3 |
--------------------------------------------------------------------------------
/examples/r2-public-read-worker/worker_env.d.ts:
--------------------------------------------------------------------------------
1 | import { R2Bucket } from './deps.ts';
2 |
3 | export interface WorkerEnv {
4 | readonly bucket: R2Bucket;
5 | readonly flags?: string;
6 | readonly allowIps?: string;
7 | readonly denyIps?: string;
8 | readonly directoryListingLimit?: string; // default: 1000 (max) to workaround r2 bug
9 | readonly allowCorsOrigins?: string; // e.g. * or https://origin1.com, https://origin2.com
10 | readonly allowCorsTypes?: string; // if allowed cors origin, further restricts by file extension (.mp4, .m3u8, .ts) or content-type (video/mp4, application/x-mpegurl, video/mp2t)
11 | }
12 |
--------------------------------------------------------------------------------
/examples/tiered-websockets-worker/client.ts:
--------------------------------------------------------------------------------
1 | export class Client {
2 | private readonly socket: WebSocket;
3 |
4 | constructor(socket: WebSocket, clientId: string, republish: (data: Record) => void) {
5 | socket.addEventListener('message', event => {
6 | const msg = JSON.parse(event.data);
7 | republish({ t: 'message', msg, clientId });
8 | });
9 | socket.addEventListener('close', event => {
10 | const { code, reason, wasClean } = event;
11 | republish({ t: 'close', clientId, code, reason, wasClean });
12 | });
13 | socket.addEventListener('error', event => {
14 | republish({ t: 'error', clientId, event });
15 | });
16 | this.socket = socket;
17 |
18 | // cf sockets are assumed open after accept!
19 | this.send({ t: 'open', clientId });
20 | }
21 |
22 | send(data: Record) {
23 | this.socket.send(JSON.stringify(data));
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/examples/tiered-websockets-worker/colo_from_trace.ts:
--------------------------------------------------------------------------------
1 | export class ColoFromTrace {
2 |
3 | private _colo: string | undefined;
4 |
5 | async get(): Promise {
6 | if (!this._colo) {
7 | this._colo = await this.computeColo();
8 | }
9 | return this._colo;
10 | }
11 |
12 | //
13 |
14 | private async computeColo(): Promise {
15 | const res = await fetch('https://1.1.1.1/cdn-cgi/trace');
16 | if (res.status !== 200) return res.status.toString();
17 | const text = await res.text();
18 | const lines = text.split('\n');
19 | for (const line of lines) {
20 | if (line.startsWith('colo=')) return line.substring('colo='.length);
21 | }
22 | return 'nocolo';
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/examples/tiered-websockets-worker/colo_tier_do.ts:
--------------------------------------------------------------------------------
1 | import { ColoFromTrace } from './colo_from_trace.ts';
2 | import { DurableObjectState } from './deps.ts';
3 | import { getIsolateId } from './isolate_id.ts';
4 | import { Server } from './server.ts';
5 |
6 | export class ColoTierDO {
7 | private readonly state: DurableObjectState;
8 |
9 | constructor(state: DurableObjectState) {
10 | this.state = state;
11 | }
12 |
13 | async fetch(request: Request): Promise {
14 | console.log(request.url);
15 | const colo = await COLO_FROM_TRACE.get();
16 | const durableObjectName = request.headers.get('do-name');
17 | const isolateId = 'colo-tier-' + durableObjectName + '-' + getIsolateId(colo);
18 | const server = this.ensureServer(isolateId);
19 | const url = new URL(request.url);
20 | const { pathname } = url;
21 | this.ensureTicking();
22 | console.log('logprops:', { colo, durableObjectClass: 'ColoTierDO', durableObjectId: this.state.id.toString(), durableObjectName });
23 |
24 | const wsResponse = server.tryHandle(pathname, request, isolateId);
25 | if (wsResponse) return wsResponse;
26 |
27 | return new Response('not found', { status: 404 });
28 | }
29 |
30 | //
31 |
32 | _server: Server | undefined;
33 |
34 | private ensureServer(serverId: string): Server {
35 | if (this._server) return this._server;
36 | this._server = new Server(serverId, true);
37 | return this._server;
38 | }
39 |
40 | _ticking = false;
41 |
42 | private ensureTicking() {
43 | if (this._ticking) return;
44 | this._ticking = true;
45 | setInterval(() => {
46 | const now = new Date().toISOString();
47 | this._server?.broadcast({ t: 'tick', now });
48 | }, 5000);
49 | }
50 |
51 | }
52 |
53 | const COLO_FROM_TRACE = new ColoFromTrace();
54 |
--------------------------------------------------------------------------------
/examples/tiered-websockets-worker/deps.ts:
--------------------------------------------------------------------------------
1 | export type { IncomingRequestCf, DurableObjectState, DurableObjectNamespace, ModuleWorkerContext, CloudflareWebSocketExtensions } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts';
2 |
--------------------------------------------------------------------------------
/examples/tiered-websockets-worker/globals.d.ts:
--------------------------------------------------------------------------------
1 | import { CloudflareWebSocketExtensions } from './deps.ts';
2 |
3 | export {};
4 |
5 | declare global {
6 |
7 | // https://developers.cloudflare.com/workers/runtime-apis/websockets
8 |
9 | interface Response {
10 | // non-standard member
11 | webSocket?: WebSocket & CloudflareWebSocketExtensions;
12 | }
13 |
14 | // non-standard class, only on CF
15 | class WebSocketPair {
16 | 0: WebSocket & CloudflareWebSocketExtensions;
17 | 1: WebSocket & CloudflareWebSocketExtensions;
18 | }
19 |
20 | interface ResponseInit {
21 | // non-standard member
22 | webSocket?: WebSocket;
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/examples/tiered-websockets-worker/isolate_id.ts:
--------------------------------------------------------------------------------
1 | export function getIsolateId(colo: string): string {
2 | if (_isolateId) return _isolateId;
3 | const tokens = crypto.randomUUID().split('-');
4 | _isolateId = colo + '-' + tokens[3] + tokens[4];
5 | return _isolateId;
6 | }
7 |
8 | //
9 |
10 | let _isolateId: string | undefined;
11 |
--------------------------------------------------------------------------------
/examples/tiered-websockets-worker/server.ts:
--------------------------------------------------------------------------------
1 | export class Server {
2 | private readonly sockets = new Map();
3 | private readonly debug: boolean;
4 |
5 | readonly id: string;
6 |
7 | constructor(id: string, debug: boolean = false) {
8 | this.id = id;
9 | this.debug = debug;
10 | }
11 |
12 | broadcast(obj: Record) {
13 | obj.serverId = this.id;
14 | obj.clients = this.sockets.size;
15 | const json = JSON.stringify(obj);
16 | for (const socket of this.sockets.values()) {
17 | socket.send(json);
18 | }
19 | }
20 |
21 | tryHandle(pathname: string, request: Request, debug: string): Response | undefined {
22 | const m = /^\/ws\/(.+)$/.exec(pathname);
23 | if (!m) return undefined;
24 | const upgrade = request.headers.get('upgrade') || undefined;
25 | if (upgrade !== 'websocket') return new Response('expected upgrade: websocket', { status: 400 });
26 | const clientId = m[1];
27 | const pair = new WebSocketPair();
28 | const socket = pair[1];
29 | this.sockets.set(clientId, socket);
30 | socket.accept();
31 | socket.addEventListener('message', event => {
32 | try {
33 | const obj = JSON.parse(event.data);
34 | if (obj.t === 'open') {
35 | if (typeof obj.clientId !== 'string') throw new Error(`Bad clientId: ${obj.clientId}`);
36 | this.broadcast({ t: 'opened', clientId, clients: this.sockets.size, debug });
37 | }
38 | } catch (e) {
39 | this.broadcast({ t: 'messageError', clientId, msg: `Error handling event.data: ${event.data}, ${(e as Error).stack || e}` });
40 | }
41 | })
42 | socket.addEventListener('close', event => {
43 | const { code, reason, wasClean } = event;
44 | this.sockets.delete(clientId);
45 | this.broadcast({ t: 'closed', clientId, clients: this.sockets.size, code, reason, wasClean });
46 | });
47 | socket.addEventListener('error', event => {
48 | this.sockets.delete(clientId);
49 | this.broadcast({ t: 'errored', clientId, clients: this.sockets.size, event });
50 | });
51 | return new Response(null, { status: 101, webSocket: pair[0], headers: { 'debug': debug } });
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/examples/tiered-websockets-worker/tiered_worker_env.d.ts:
--------------------------------------------------------------------------------
1 | import { DurableObjectNamespace } from './deps.ts';
2 |
3 | export interface TieredWorkerEnv {
4 | readonly ColoTierDO: DurableObjectNamespace;
5 | }
6 |
--------------------------------------------------------------------------------
/examples/webtail-app/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true
3 | }
--------------------------------------------------------------------------------
/examples/webtail-app/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "type": "deno",
6 | "command": "bundle",
7 | "problemMatcher": [
8 | "$deno"
9 | ],
10 | "args": [
11 | "--config",
12 | "deno.jsonc",
13 | "--watch",
14 | "webtail_app.ts",
15 | "../webtail-worker/static/webtail_app.js"
16 | ],
17 | "group": {
18 | "kind": "build",
19 | "isDefault": true
20 | },
21 | "label": "deno: bundle webtail_app.ts"
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/examples/webtail-app/app_constants.ts:
--------------------------------------------------------------------------------
1 | export class AppConstants {
2 | static readonly WEBSOCKET_PING_INTERVAL_SECONDS = 10; // send an empty message down the ws to detect bad connections faster
3 | static readonly INACTIVE_TAIL_SECONDS = 5; // reclaim inactive tails older than this
4 | }
5 |
--------------------------------------------------------------------------------
/examples/webtail-app/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom", "dom.iterable", "esnext"
5 | ]
6 | },
7 | "lock": false,
8 | "fmt": { "singleQuote": true }
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/examples/webtail-app/deps_app.ts:
--------------------------------------------------------------------------------
1 | export { css, html, LitElement, svg, type SVGTemplateResult, CSSResult, type TemplateResult, render } from 'https://cdn.skypack.dev/lit@2.6.1?dts';
2 | export type { Tail } from '../../common/cloudflare_api.ts';
3 | export { createTail, CloudflareApiError, listScripts, listTails, CloudflareApi } from '../../common/cloudflare_api.ts';
4 | export { setSubtract, setEqual, setIntersect, setUnion } from '../../common/sets.ts';
5 | export { TailConnection } from '../../common/tail_connection.ts';
6 | export { formatLocalYyyyMmDdHhMmSs, dumpMessagePretty, parseLogProps } from '../../common/tail_pretty.ts';
7 | export { generateUuid } from '../../common/uuid_v4.ts';
8 | export type { AdditionalLog } from '../../common/tail_pretty.ts';
9 | export type { ErrorInfo, TailConnectionCallbacks, UnparsedMessage } from '../../common/tail_connection.ts';
10 | export type { TailMessage, TailOptions, TailFilter, HeaderFilter } from '../../common/tail.ts';
11 | export { isTailMessageCronEvent, parseHeaderFilter } from '../../common/tail.ts';
12 | export { CfGqlClient } from '../../common/analytics/cfgql_client.ts';
13 | export { computeDurableObjectsCostsTable } from '../../common/analytics/durable_objects_costs.ts';
14 | export type { DurableObjectsCostsTable, DurableObjectsDailyCostsTable } from '../../common/analytics/durable_objects_costs.ts';
15 |
--------------------------------------------------------------------------------
/examples/webtail-app/qps_controller.ts:
--------------------------------------------------------------------------------
1 | export interface QpsControllerCallbacks {
2 | onQpsChanged(qps: number): void;
3 | }
4 |
5 | export class QpsController {
6 | private readonly sortedEventTimes: number[] = [];
7 | private readonly callbacks: QpsControllerCallbacks;
8 | private readonly n: number;
9 |
10 | private _qps = 0;
11 | get qps(): number { return this._qps; }
12 |
13 | constructor(n: number, callbacks: QpsControllerCallbacks) {
14 | this.callbacks = callbacks;
15 | this.n = n;
16 | }
17 |
18 | addEvent(eventTime: number) {
19 | const qps = computeQps(this.n, this.sortedEventTimes, eventTime);
20 | if (qps === this._qps) return;
21 | this._qps = qps;
22 | this.callbacks.onQpsChanged(qps);
23 | }
24 |
25 | }
26 |
27 | //
28 |
29 | function computeQps(n: number, sortedEventTimes: number[], eventTime: number) {
30 | add(eventTime, sortedEventTimes);
31 | while (sortedEventTimes.length > n) {
32 | sortedEventTimes.shift();
33 | }
34 | // currentAvgRate = (N - 1) / (time difference between last N events)
35 | const num = sortedEventTimes.length;
36 | if (num < 2) return 0;
37 | const timeDiffSeconds = (sortedEventTimes[sortedEventTimes.length - 1] - sortedEventTimes[0]) / 1000;
38 | return (num - 1) / timeDiffSeconds;
39 | }
40 |
41 | function add(el: number, arr: number[]) {
42 | arr.splice(findLoc(el, arr) + 1, 0, el);
43 | return arr;
44 | }
45 |
46 | function findLoc(el: number, arr: number[], st?: number, en?: number) {
47 | st = st || 0;
48 | en = en || arr.length;
49 | for (let i = 0; i < arr.length; i++) {
50 | if (arr[i] > el) {
51 | return i - 1;
52 | }
53 | }
54 | return en;
55 | }
56 |
--------------------------------------------------------------------------------
/examples/webtail-app/static_data.ts:
--------------------------------------------------------------------------------
1 | export interface StaticData {
2 | readonly version?: string;
3 | readonly flags?: string;
4 | }
5 |
--------------------------------------------------------------------------------
/examples/webtail-app/views/icons.ts:
--------------------------------------------------------------------------------
1 | import { html, svg, SVGTemplateResult } from '../deps_app.ts';
2 | import { Material } from '../material.ts';
3 |
4 | export function actionIcon(icon: SVGTemplateResult, opts: { text?: string, onclick?: () => void } = {}) {
5 | const { text, onclick } = opts;
6 | return html` { e.preventDefault(); onclick && onclick(); }}>${icon}${text || ''}
`;
7 | }
8 |
9 | export const CLEAR_ICON = svg``;
10 | export const EDIT_ICON = svg``;
11 | export const ADD_ICON = svg``;
12 | export const CHECK_BOX_UNCHECKED_ICON = svg``;
13 | export const CHECK_BOX_CHECKED_ICON = svg``;
14 |
--------------------------------------------------------------------------------
/examples/webtail-app/views/modal_view.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { css, html } from '../deps_app.ts';
4 | import { WebtailAppVM } from '../webtail_app_vm.ts';
5 | import { FILTER_EDITOR_HTML, initFilterEditor } from './filter_editor_view.ts';
6 | import { initProfileEditor, PROFILE_EDITOR_HTML } from './profile_editor_view.ts';
7 | import { initWelcomePanel, WELCOME_PANEL_HTML } from './welcome_panel.ts';
8 |
9 | export const MODAL_HTML = html`
10 |
17 | `;
18 |
19 | export const MODAL_CSS = css`
20 | .modal {
21 | display: none;
22 | position: fixed;
23 | z-index: 1;
24 | left: 0;
25 | top: 0;
26 | width: 100%;
27 | height: 100%;
28 | background-color: rgba(0, 0, 0, 0.4);
29 | }
30 |
31 | .modal-content {
32 | margin: 10% auto;
33 | width: 80%;
34 | max-width: 40rem;
35 | background-color: var(--background-color);
36 | }
37 |
38 | @media screen and (max-width: 600px) {
39 | .modal-content {
40 | margin: 10% 0;
41 | width: 100%;
42 | }
43 | }
44 |
45 | `;
46 |
47 | export function initModal(document: HTMLDocument, vm: WebtailAppVM): () => void {
48 |
49 | const modal = document.getElementById('modal') as HTMLDivElement;
50 | const updateProfileEditor = initProfileEditor(document, vm);
51 | const updateFilterEditor = initFilterEditor(document, vm);
52 | const updateWelcomePanel = initWelcomePanel(document, vm);
53 |
54 | const closeModal = () => {
55 | if (!vm.profileForm.showing && !vm.filterForm.showing && !vm.welcomeShowing && !vm.aboutShowing) return;
56 | if (vm.profileForm.progressVisible) return; // don't allow close if busy
57 | vm.profileForm.showing = false;
58 | vm.filterForm.showing = false;
59 | vm.aboutShowing = false;
60 | vm.onChange();
61 | };
62 |
63 | // Click outside modal content -> close modal
64 | window.addEventListener('click', event => {
65 | if (event.target == modal) {
66 | closeModal();
67 | }
68 | });
69 |
70 | // esc -> close modal
71 | document.addEventListener('keydown', event => {
72 | event = event || window.event;
73 | if (event.key === 'Escape') {
74 | closeModal();
75 | }
76 | });
77 |
78 | return () => {
79 | updateProfileEditor();
80 | updateFilterEditor();
81 | updateWelcomePanel();
82 | modal.style.display = (vm.profileForm.showing || vm.filterForm.showing || vm.welcomeShowing || vm.aboutShowing) ? 'block' : 'none';
83 | };
84 | }
85 |
--------------------------------------------------------------------------------
/examples/webtail-worker/app_manifest.d.ts:
--------------------------------------------------------------------------------
1 | export interface AppManifest {
2 | /**
3 | * https://w3c.github.io/manifest/#short_name-member
4 | * maximum of 12 characters recommended per https://developer.chrome.com/extensions/manifest/name
5 | * used: android launcher icon title
6 | */
7 | 'short_name': string,
8 |
9 | /**
10 | * https://w3c.github.io/manifest/#name-member
11 | * maximum of 45 characters per https://developer.chrome.com/extensions/manifest/name
12 | * used: app install banner android offline splash screen
13 | */
14 | name: string,
15 |
16 | /**
17 | * https://w3c.github.io/manifest/#description-member
18 | * used ??
19 | */
20 | description: string,
21 |
22 | /**
23 | * https://w3c.github.io/manifest/#icons-member
24 | */
25 | icons: readonly AppManifestIcon[],
26 |
27 | /**
28 | * https://w3c.github.io/manifest/#theme_color-member
29 | */
30 | 'theme_color': string,
31 |
32 | /**
33 | * https://w3c.github.io/manifest/#background_color-member
34 | * should match page, also used for splash screen
35 | */
36 | 'background_color': string,
37 |
38 | /**
39 | * https://w3c.github.io/manifest/#display-member
40 | */
41 | display: 'fullscreen' | 'standalone' | 'minimal-ui' | 'browser',
42 |
43 | /**
44 | * https://w3c.github.io/manifest/#start_url-member
45 | */
46 | 'start_url': string,
47 |
48 | /**
49 | * https://w3c.github.io/manifest/#lang-member
50 | */
51 | lang: 'en-US',
52 |
53 | /**
54 | * https://w3c.github.io/manifest/#dir-member
55 | */
56 | dir: 'ltr',
57 |
58 | /**
59 | * https://w3c.github.io/manifest/#scope-member
60 | */
61 | scope?: string,
62 |
63 | }
64 |
65 | export interface AppManifestIcon {
66 |
67 | /**
68 | * url to image resource
69 | */
70 | src: string;
71 |
72 | /**
73 | * e.g. "512x512", or "16x16 32x32 48x48"
74 | *
75 | * https://w3c.github.io/manifest/#declaring-multiple-icons
76 | */
77 | sizes?: string;
78 |
79 | /**
80 | * MIME type
81 | *
82 | * https://w3c.github.io/manifest/#declaring-multiple-icons
83 | */
84 | type?: string;
85 |
86 | /**
87 | * https://w3c.github.io/manifest/#purpose-member
88 | */
89 | purpose?: 'monochrome' | 'maskable' | 'any';
90 | }
91 |
--------------------------------------------------------------------------------
/examples/webtail-worker/cli.ts:
--------------------------------------------------------------------------------
1 | import { Bytes, parseFlags } from './deps_cli.ts';
2 |
3 | const { args, options } = parseFlags(Deno.args);
4 |
5 | if (args.length > 0) {
6 | await webtail(args, options);
7 | Deno.exit(0);
8 | }
9 |
10 | dumpHelp();
11 |
12 | Deno.exit(1);
13 |
14 | //
15 |
16 | async function webtail(args: (string | number)[], options: Record) {
17 | const command = args[0];
18 | const fn = { b64 }[command];
19 | if (options.help || !fn) {
20 | dumpHelp();
21 | return;
22 | }
23 | await fn(args.slice(1));
24 | }
25 |
26 | async function b64(args: (string | number)[]) {
27 | const path = args[0];
28 | if (typeof path !== 'string') throw new Error('Must provide path to file');
29 | const contents = await Deno.readFile(path);
30 | const b64 = new Bytes(contents).base64();
31 | console.log(b64);
32 | }
33 |
34 |
35 | function dumpHelp() {
36 | const lines = [
37 | `webtail-cli`,
38 | 'Tools for developing webtail',
39 | '',
40 | 'USAGE:',
41 | ' deno run --allow-net examples/webtail-worker/cli.ts [FLAGS] [OPTIONS] [--] build',
42 | ' deno run --allow-net --allow-read examples/webtail-worker/cli.ts [FLAGS] [OPTIONS] [--] b64 ',
43 | '',
44 | 'FLAGS:',
45 | ' -h, --help Prints help information',
46 | ' --verbose Toggle verbose output (when applicable)',
47 | '',
48 | 'ARGS:',
49 | ' b64 Dump out the b64 of a given file',
50 | ];
51 | for (const line of lines) {
52 | console.log(line);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/examples/webtail-worker/deps_cli.ts:
--------------------------------------------------------------------------------
1 | export { basename, dirname, join, fromFileUrl, resolve, toFileUrl, extname, relative, isAbsolute, normalize, parse as parsePath, globToRegExp } from 'https://deno.land/std@0.224.0/path/mod.ts';
2 | export { ModuleWatcher } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/cli/module_watcher.ts';
3 | export { Bytes } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/bytes.ts';
4 | export { bundle } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/cli/bundle.ts';
5 | export { parseFlags } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/cli/flag_parser.ts';
6 |
--------------------------------------------------------------------------------
/examples/webtail-worker/deps_worker.ts:
--------------------------------------------------------------------------------
1 | export { Bytes } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/bytes.ts';
2 | export type { IncomingRequestCf, ModuleWorkerContext } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/cloudflare_workers_types.d.ts';
3 | export { encodeXml } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/xml_util.ts';
4 | export { importText } from 'https://raw.githubusercontent.com/skymethod/denoflare/v0.7.0/common/import_text.ts';
5 |
--------------------------------------------------------------------------------
/examples/webtail-worker/material.ts:
--------------------------------------------------------------------------------
1 | export class Material {
2 | static readonly primaryColor200Hex = '#cfabfd';
3 | static readonly primaryColor300Hex = '#bb86fc'; // primary
4 | static readonly primaryColor900Hex = '#5f27ca'; // darkest
5 |
6 | static readonly backgroundColorHex = '#121212';
7 | static sansSerifFontFamily = '-apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, Ubuntu, roboto, noto, segoe ui, arial, sans-serif';
8 | }
9 |
--------------------------------------------------------------------------------
/npm/denoflare-mqtt/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/npm/denoflare-mqtt/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.exclude": {
3 | "**/.DS_Store": true,
4 | "**/node_modules": true,
5 | "cjs": true,
6 | "esm": true,
7 | "main.d.ts": true,
8 | },
9 | }
--------------------------------------------------------------------------------
/npm/denoflare-mqtt/cjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "commonjs"
3 | }
4 |
--------------------------------------------------------------------------------
/npm/denoflare-mqtt/esm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module"
3 | }
4 |
--------------------------------------------------------------------------------
/npm/denoflare-mqtt/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "denoflare-mqtt",
3 | "version": "0.0.2",
4 | "description": "Lightweight MQTT v5 client for Deno, Node, and the browser.",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/skymethod/denoflare.git"
8 | },
9 | "main": "./cjs/main.js",
10 | "module": "./esm/main.js",
11 | "exports": {
12 | ".": {
13 | "require": "./cjs/main.js",
14 | "default": "./esm/main.js"
15 | }
16 | },
17 | "types": "main.d.ts",
18 | "keywords": [ "mqtt" ],
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/skymethod/denoflare/issues"
22 | },
23 | "homepage": "https://github.com/skymethod/denoflare/tree/master/npm/denoflare-mqtt"
24 | }
25 |
--------------------------------------------------------------------------------