Remove create-r2-explorer in favor of using cloudflare c3

This commit is contained in:
Gabriel Massadas
2024-12-01 21:46:49 +00:00
parent 66f509566e
commit 1e7c97e367
36 changed files with 14 additions and 2072 deletions
-1
View File
@@ -1,6 +1,5 @@
dist
node_modules
packages/create-r2-explorer/*
packages/github-action/*
packages/worker/bin/*
packages/worker/config/*
+3
View File
@@ -38,3 +38,6 @@ packages/github-action/src
packages/dashboard/.env
packages/dashboard/.quasar
.dev.vars
template/package-lock.json
template/node_modules/
-10
View File
@@ -15,16 +15,6 @@ npm run build
wrangler pages publish --branch main --project-name r2-explorer-dashboard dist/
```
## Publish create-r2-explorer package
Increase version in `packages/create-r2-explorer/package.json`
```bash
cd packages/create-r2-explorer/
npm run build
npm publish --access public
```
## Manually publish worker
```bash
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 Gabriel Massadas
Copyright (c) 2023-2024 Gabriel Massadas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+1 -1
View File
@@ -42,7 +42,7 @@ Read this in other languages: [Español](READMEes.md), [Português](READMEpt.md)
Run this command to get an example project setup
```bash
npm create r2-explorer@latest
npm create cloudflare@latest r2-explorer -- --template "G4brym/R2-Explorer/template"
```
## Upgrading your installation
+1 -1
View File
@@ -39,7 +39,7 @@ Leer esto en otros idiomas: [English](README.md), [Português](READMEpt.md), [Fr
Ejecuta este comando para configurar un proyecto de ejemplo
```bash
npm create r2-explorer@latest
npm create cloudflare@latest r2-explorer -- --template "G4brym/R2-Explorer/template"
```
## Actualizar tu instalación
+1 -1
View File
@@ -39,7 +39,7 @@ Lisez ceci dans d'autres langues : [English](README.md), [Español](READMEes.md)
Exécutez cette commande pour configurer un projet exemple
```bash
npm create r2-explorer@latest
npm create cloudflare@latest r2-explorer -- --template "G4brym/R2-Explorer/template"
```
## Mise à jour de votre installation
+1 -1
View File
@@ -39,7 +39,7 @@ Leia isso em outros idiomas: [English](README.md), [Español](READMEes.md), [Fra
Execute este comando para configurar um projeto de exemplo
```bash
npm create r2-explorer@latest
npm create cloudflare@latest r2-explorer -- --template "G4brym/R2-Explorer/template"
```
## Atualizar sua instalação
-7
View File
@@ -1,7 +0,0 @@
export default {
configVersion: 1,
id: "r2explorer",
displayName: "R2Explorer interface for your Cloudflare R2 Buckets!",
platform: "workers",
copyFiles: { path: './template' }
};
+1 -2
View File
@@ -5,10 +5,9 @@
"scripts": {
"prepare": "husky install",
"lint": "(eslint . || (eslint . --fix; exit 1)) && (prettier --check '*.{json,js}' 'packages/*/src/**/*.{js,ts}' || (prettier -w '*.{json,js}' 'packages/*/src/**/*.{js,ts}'; exit 1))",
"build-create": "pnpm run --filter create-r2-explorer build",
"build-dashboard": "pnpm run --filter r2-explorer-dashboard build",
"build-worker": "pnpm run --filter r2-explorer build",
"build": "pnpm build-create && pnpm build-dashboard && pnpm build-worker",
"build": "pnpm build-dashboard && pnpm build-worker",
"deploy-dashboard": "pnpm run --filter r2-explorer-dashboard deploy",
"deploy-dashboard-dev": "pnpm run --filter r2-explorer-dashboard deploy-dev",
"deploy-dev-worker": "pnpm run --filter r2-explorer-dev-worker deploy",
-26
View File
@@ -1,26 +0,0 @@
# create-r2-explorer
A CLI for creating and deploying new [R2 Explorer](https://github.com/G4brym/R2-Explorer) applications to
[Cloudflare](https://developers.cloudflare.com/).
This package is a fork from [create-cloudflare](https://www.npmjs.com/package/create-cloudflare).
## Usage
### Setup via Interactive CLI
To create new applications via interactive CLI, run:
```bash
npm create r2-explorer@latest
# or
pnpm create r2-explorer@latest
# or
yarn create r2-explorer
```
### Community
- Join us [on Discord](https://discord.cloudflare.com) in the workers-libraries and workers-discussions channels
- File an issue [on Github](https://github.com/G4brym/R2-Explorer/issues/new)
-69
View File
@@ -1,69 +0,0 @@
{
"name": "create-r2-explorer",
"version": "1.0.1",
"description": "A CLI for creating and deploying new R2 Explorer applications to Cloudflare.",
"keywords": [
"cloudflare",
"workers",
"cloudflare workers",
"serverless",
"serverless application",
"r2-explorer",
"cloudflare-r2"
],
"repository": {
"type": "git",
"url": "https://github.com/G4brym/R2-Explorer.git",
"directory": "packages/create-r2-explorer"
},
"license": "MIT",
"author": "Gabriel Massadas",
"exports": {
".": "./dist/cli.js"
},
"main": "./dist/cli.js",
"bin": "./dist/cli.js",
"files": [
"dist",
"template"
],
"scripts": {
"build": "node scripts/build.js",
"prepublishOnly": "npm run build",
"watch": "node scripts/build.js --watch"
},
"devDependencies": {
"@babel/parser": "^7.21.3",
"@babel/types": "^7.21.4",
"@clack/prompts": "^0.6.3",
"@cloudflare/workers-types": "^4.20230419.0",
"@types/command-exists": "^1.2.0",
"@types/cross-spawn": "^6.0.2",
"@types/esprima": "^4.0.3",
"@types/node": "^18.15.3",
"@types/which-pm-runs": "^1.0.0",
"@types/yargs": "^17.0.22",
"@typescript-eslint/parser": "^5.55.0",
"chalk": "^5.2.0",
"command-exists": "^1.2.9",
"cross-spawn": "^7.0.3",
"dns2": "^2.1.0",
"esbuild": "^0.17.12",
"execa": "^7.1.1",
"haikunator": "^2.1.2",
"log-update": "^5.0.1",
"open": "^8.4.0",
"pnpm": "^8.6.11",
"typescript": "^5.0.2",
"which-pm-runs": "^1.1.0",
"wrangler": "*",
"yargs": "^17.7.1",
"yarn": "^1.22.19",
"@clack/core": "^0.3.4",
"semver": "^7.6.0",
"undici": "^6.7.1"
},
"engines": {
"node": ">=16.13.0"
}
}
@@ -1,37 +0,0 @@
const { build, context } = require("esbuild");
const { cp } = require("fs/promises");
const run = async () => {
const argv = process.argv.slice(2);
const watchMode = argv[0] === "--watch";
const config = {
entryPoints: ["./src/cli.ts"],
bundle: true,
outdir: "./dist",
platform: "node",
format: "cjs",
};
const runBuild = async () => {
await build(config);
await build({...config, outdir: undefined, outfile: '../worker/bin/r2-explorer.js'});
};
const runWatch = async () => {
let ctx = await context(config);
await ctx.watch();
console.log("Watching...");
};
if (watchMode) {
await runWatch();
} else {
await runBuild();
}
};
run().catch((error) => {
console.error(error);
process.exit(1);
});
-69
View File
@@ -1,69 +0,0 @@
#!/usr/bin/env node
import {crash, logRaw, startSection} from "helpers/cli";
import {dim} from "helpers/colors";
import {processArgument} from "helpers/interactive";
import yargs from "yargs";
import {hideBin} from "yargs/helpers";
import {version} from "../package.json";
import {validateProjectDirectory} from "./common";
import {runWorkersGenerator} from "./workers";
import {CArgs} from "types";
export const C_DEFAULTS = {
projectName: 'my-r2-explorer',
deploy: undefined,
open: true,
};
export const main = async (argv: string[]) => {
const args = await parseArgs(argv);
printBanner();
const projectName = await processArgument<string>(args, "projectName", {
type: "text",
question: `In which directory do you want to create your application?`,
helpText: "also used as application name",
defaultValue: C_DEFAULTS.projectName,
label: "dir",
validate: (value) =>
validateProjectDirectory(String(value) || C_DEFAULTS.projectName),
format: (val) => `./${val}`,
});
const validatedArgs: CArgs = {
...args,
projectName,
};
await runWorkersGenerator(validatedArgs);
};
const printBanner = () => {
logRaw(dim(`\nusing create-r2-explorer version ${version}\n`));
startSection(`Create an application with Cloudflare`, "Step 1 of 3");
};
export const parseArgs = async (argv: string[]): Promise<Partial<CArgs>> => {
const args = await yargs(hideBin(argv))
.scriptName("create-r2-explorer")
.usage("$0 [args]")
.positional("name", {type: "string"})
.option("deploy", {type: "boolean"})
.option("open", {
type: "boolean",
default: true,
description:
"opens your browser after your deployment, set --no-open to disable",
})
.version(version)
.help().argv;
return {
...C_DEFAULTS,
projectName: args._[0] as string | undefined,
...args,
};
};
main(process.argv).catch((e) => crash(e));
-217
View File
@@ -1,217 +0,0 @@
import {existsSync, mkdirSync, readdirSync} from "fs";
import {basename, dirname, resolve} from "path";
import {chdir} from "process";
import {crash, endSection, log, logRaw, newline, openInBrowser, shapes, startSection,} from "helpers/cli";
import {bgGreen, blue, brandColor, dim, gray} from "helpers/colors";
import {listAccounts, runCommand, wranglerLogin,} from "helpers/command";
import {inputPrompt, processArgument, spinner} from "helpers/interactive";
import {detectPackageManager} from "helpers/packages";
import {C_DEFAULTS} from "./cli";
import type {CArgs, PagesGeneratorContext} from "types";
import {poll} from "helpers/poll";
const {npm} = detectPackageManager();
export const validateProjectDirectory = (relativePath: string) => {
const path = resolve(relativePath);
const existsAlready = existsSync(path);
const isEmpty = existsAlready && readdirSync(path).length === 0; // allow existing dirs _if empty_ to ensure c3 is non-destructive
if (existsAlready && !isEmpty) {
return `Directory \`${relativePath}\` already exists and is not empty. Please choose a new name.`;
}
};
export const setupProjectDirectory = (args: CArgs) => {
// Crash if the directory already exists
const path = resolve(args.projectName);
const err = validateProjectDirectory(path);
if (err) {
crash(err);
}
const directory = dirname(path);
const name = basename(path);
// If the target is a nested directory, create the parent
mkdirSync(directory, {recursive: true});
// Change to the parent directory
chdir(directory);
return {name, path};
};
export const offerToDeploy = async (ctx: PagesGeneratorContext) => {
startSection(`Deploy with Cloudflare`, `Step 3 of 3`);
const label = `deploy via \`${npm} run ${
ctx.framework?.config.deployCommand ?? "deploy"
}\``;
ctx.args.deploy = await processArgument(ctx.args, "deploy", {
type: "confirm",
question: "Do you want to deploy your application?",
label,
defaultValue: C_DEFAULTS.deploy,
});
if (!ctx.args.deploy) return;
const loginSuccess = await wranglerLogin();
if (!loginSuccess) return;
await chooseAccount(ctx);
};
export const runDeploy = async (ctx: PagesGeneratorContext) => {
if (ctx.args.deploy === false) return;
if (!ctx.account?.id) {
crash("Failed to read Cloudflare account.");
return;
}
const deployCmd = `${npm} run ${
ctx.framework?.config.deployCommand ?? "deploy"
}`;
const result = await runCommand(deployCmd, {
silent: true,
cwd: ctx.project.path,
env: {CLOUDFLARE_ACCOUNT_ID: ctx.account.id, NODE_ENV: "production"},
startText: `Deploying your application`,
doneText: `${brandColor("deployed")} ${dim(`via \`${deployCmd}\``)}`,
});
const deployedUrlRegex = /https:\/\/.+\.(pages|workers)\.dev/;
const deployedUrlMatch = result.match(deployedUrlRegex);
if (deployedUrlMatch) {
ctx.deployedUrl = deployedUrlMatch[0];
} else {
crash("Failed to find deployment url.");
}
// if a pages url (<sha1>.<project>.pages.dev), remove the sha1
if (ctx.deployedUrl?.endsWith(".pages.dev")) {
const [proto, hostname] = ctx.deployedUrl.split("://");
const hostnameWithoutSHA1 = hostname.split(".").slice(-3).join("."); // only keep the last 3 parts (discard the 4th, i.e. the SHA1)
ctx.deployedUrl = `${proto}://${hostnameWithoutSHA1}`;
}
};
export const chooseAccount = async (ctx: PagesGeneratorContext) => {
const s = spinner();
s.start(`Selecting Cloudflare account ${dim("retrieving accounts")}`);
const accounts = await listAccounts();
let accountId: string;
if (Object.keys(accounts).length == 1) {
const accountName = Object.keys(accounts)[0];
accountId = accounts[accountName];
s.stop(`${brandColor("account")} ${dim(accountName)}`);
} else {
s.stop(
`${brandColor("account")} ${dim("more than one account available")}`
);
const accountOptions = Object.entries(accounts).map(([name, id]) => ({
label: name,
value: id,
}));
accountId = await inputPrompt({
type: "select",
question: "Which account do you want to use?",
options: accountOptions,
label: "account",
defaultValue: accountOptions[0].value,
});
}
const accountName = Object.keys(accounts).find(
(name) => accounts[name] == accountId
) as string;
ctx.account = {id: accountId, name: accountName};
};
export const printSummary = async (ctx: PagesGeneratorContext) => {
const nextSteps = [
[`Navigate to the new directory`, `cd ${ctx.project.name}`],
[
`Run the development server`,
`${npm} run ${ctx.framework?.config.devCommand ?? "start"}`,
],
[
`Deploy your application`,
`${npm} run ${ctx.framework?.config.deployCommand ?? "deploy"}`,
],
[
`Read the documentation`,
`https://developers.cloudflare.com/${
ctx.framework
? ctx.framework.config.type === "workers"
? "workers"
: "pages"
: "workers"
}`,
],
[`Stuck? Join us at`, `https://discord.gg/cloudflaredev`],
];
if (ctx.deployedUrl) {
const msg = [
`${gray(shapes.leftT)}`,
`${bgGreen(" SUCCESS ")}`,
`${dim(`View your deployed application at`)}`,
`${blue(ctx.deployedUrl)}`,
].join(" ");
logRaw(msg);
} else {
const msg = [
`${gray(shapes.leftT)}`,
`${bgGreen(" APPLICATION CREATED ")}`,
`${dim(`Deploy your application with`)}`,
`${blue(
`${npm} run ${ctx.framework?.config.deployCommand ?? "deploy"}`
)}`,
].join(" ");
logRaw(msg);
}
newline();
nextSteps.forEach((entry) => {
log(`${dim(entry[0])} ${blue(entry[1])}`);
});
newline();
if (ctx.deployedUrl) {
const success = await poll(ctx.deployedUrl);
if (success) {
if (ctx.args.open) {
await openInBrowser(ctx.deployedUrl);
}
}
}
endSection("See you again soon!");
process.exit(0);
};
export async function getProductionBranch(cwd: string) {
try {
const productionBranch = await runCommand(
// "git branch --show-current", // git@^2.22
"git rev-parse --abbrev-ref HEAD", // git@^1.6.3
{
silent: true,
cwd,
useSpinner: false,
captureOutput: true,
}
);
return productionBranch.trim();
} catch (err) {
}
return "main";
}
@@ -1,127 +0,0 @@
import { exit } from "process";
import open from "open";
import { brandColor, dim, gray, white, red, hidden, bgRed } from "./colors";
export const shapes = {
diamond: "◇",
dash: "─",
radioInactive: "○",
radioActive: "●",
bar: "│",
leftT: "├",
rigthT: "┤",
arrows: {
left: "",
right: "",
},
corners: {
tl: "╭",
bl: "╰",
tr: "╮",
br: "╯",
},
};
export const status = {
error: bgRed(` ERROR `),
warning: bgRed(` WARNING `),
info: bgRed(` INFO `),
success: bgRed(` SUCCESS `),
};
// Returns a string containing n non-trimmable spaces
// This is useful for places where clack trims lines of output
// but we need leading spaces
export const space = (n = 1) => {
return hidden("\u200A".repeat(n));
};
// Primitive for printing to stdout. Use this instead of
// console.log or printing to stdout directly
export const logRaw = (msg: string) => {
process.stdout.write(`${msg}\n`);
};
// A simple stylized log for use within a prompt
export const log = (msg: string) => {
const lines = msg.split("\n").map((ln) => `${gray(shapes.bar)} ${white(ln)}`);
logRaw(lines.join("\n"));
};
export const newline = () => {
log("");
};
// Log a simple status update with a style similar to the clack spinner
export const updateStatus = (msg: string) => {
logRaw(`${gray(shapes.leftT)} ${msg}`);
newline();
};
export const startSection = (heading: string, subheading?: string) => {
logRaw(
`${gray(shapes.corners.tl)} ${brandColor(heading)} ${
subheading ? dim(subheading) : ""
}`
);
newline();
};
export const endSection = (heading: string, subheading?: string) => {
logRaw(
`${gray(shapes.corners.bl)} ${brandColor(heading)} ${
subheading ? dim(subheading) : ""
}\n`
);
};
export const cancel = (msg: string) => {
newline();
logRaw(`${gray(shapes.corners.bl)} ${white.bgRed(` X `)} ${dim(msg)}`);
};
export const warn = (msg: string) => {
newline();
logRaw(`${gray(shapes.corners.bl)} ${status.warning} ${dim(msg)}`);
};
// Strip the ansi color characters out of the line when calculating
// line length, otherwise the padding will be thrown off
// Used from https://github.com/natemoo-re/clack/blob/main/packages/prompts/src/index.ts
export const stripAnsi = (str: string) => {
const pattern = [
"[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)",
"(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))",
].join("|");
const regex = RegExp(pattern, "g");
return str.replace(regex, "");
};
export const crash = (msg?: string): never => {
if (msg) {
process.stderr.write(red(msg));
process.stderr.write("\n");
}
exit(1);
};
/**
* An extremely simple wrapper around the open command.
* Specifically, it adds an 'error' event handler so that when this function
* is called in environments where we can't open the browser (e.g. GitHub Codespaces,
* StackBlitz, remote servers), it doesn't just crash the process.
*
* @param url the URL to point the browser at
*/
export async function openInBrowser(url: string): Promise<void> {
updateStatus("Opening browser");
const childProcess = await open(url);
childProcess.on("error", () => {
warn("Failed to open browser");
});
}
@@ -1,57 +0,0 @@
import path from "path";
import * as recast from "recast";
import * as esprimaParser from "recast/parsers/esprima";
import * as typescriptParser from "recast/parsers/typescript";
import { crash } from "./cli";
import { readFile, writeFile } from "./files";
import type { Program } from "esprima";
// Parse an input string as javascript and return an ast
export const parseJs = (src: string) => {
try {
return recast.parse(src, { parser: esprimaParser });
} catch (error) {
crash("Error parsing js template.");
}
};
// Parse an input string as typescript and return an ast
export const parseTs = (src: string) => {
try {
return recast.parse(src, { parser: typescriptParser });
} catch (error) {
crash("Error parsing ts template.");
}
};
// Parse a provided file with recast and return an ast
// Selects the correct parser based on the file extension
export const parseFile = (filePath: string) => {
const lang = path.extname(filePath).slice(1);
const parser = lang === "js" ? esprimaParser : typescriptParser;
try {
const fileContents = readFile(path.resolve(filePath));
if (fileContents) {
return recast.parse(fileContents, { parser }) as Program;
}
} catch (error) {
crash(`Error parsing file: ${filePath}`);
}
return null;
};
// Transform a file with the provided transformer methods and write it back to disk
export const transformFile = (
filePath: string,
methods: recast.types.Visitor
) => {
const ast = parseFile(filePath);
if (ast) {
recast.visit(ast, methods);
writeFile(filePath, recast.print(ast).code);
}
};
@@ -1,32 +0,0 @@
import chalk from "chalk";
const { white, gray, dim, hidden, bold } = chalk;
const brandColor = chalk.hex("#BD5B08");
const black = chalk.hex("#111");
const blue = chalk.hex("#0E838F");
const bgBlue = black.bgHex("#0E838F");
const red = chalk.hex("#AB2526");
const bgRed = black.bgHex("#AB2526");
const green = chalk.hex("#218529");
const bgGreen = black.bgHex("#218529");
const yellow = chalk.hex("#7F7322");
const bgYellow = black.bgHex("#7F7322");
export {
blue,
bgBlue,
green,
bgGreen,
red,
bgRed,
yellow,
bgYellow,
brandColor,
dim,
white,
gray,
hidden,
bold,
};
@@ -1,330 +0,0 @@
import { existsSync, rmSync } from "fs";
import path from "path";
import { spawn } from "cross-spawn";
import { endSection, stripAnsi } from "./cli";
import { brandColor, dim } from "./colors";
import { spinner } from "./interactive";
import { detectPackageManager } from "./packages";
import type { PagesGeneratorContext } from "types";
/**
* Command can be either:
* - a string, like `git commit -m "Changes"`
* - a string array, like ['git', 'commit', '-m', '"Initial commit"']
*
* The string version is a convenience but is unsafe if your args contain spaces
*/
type Command = string | string[];
type RunOptions = {
startText?: string;
doneText?: string | ((output: string) => string);
silent?: boolean;
captureOutput?: boolean;
useSpinner?: boolean;
env?: NodeJS.ProcessEnv;
cwd?: string;
/** If defined this function is called to all you to transform the output from the command into a new string. */
transformOutput?: (output: string) => string;
/** If defined, this function is called to return a string that is used if the `transformOutput()` fails. */
fallbackOutput?: (error: unknown) => string;
};
type MultiRunOptions = RunOptions & {
commands: Command[];
startText: string;
};
type PrintOptions<T> = {
promise: Promise<T> | (() => Promise<T>);
useSpinner?: boolean;
startText: string;
doneText?: string | ((output: T) => string);
};
export const runCommand = async (
command: Command,
opts: RunOptions = {}
): Promise<string> => {
if (typeof command === "string") {
command = command.trim().replace(/\s+/g, ` `).split(" ");
}
return printAsyncStatus({
useSpinner: opts.useSpinner ?? opts.silent,
startText: opts.startText || command.join(" ").trim(),
doneText: opts.doneText,
promise() {
const [executable, ...args] = command;
const cmd = spawn(executable, [...args], {
// TODO: ideally inherit stderr, but npm install uses this for warnings
// stdio: [ioMode, ioMode, "inherit"],
stdio: opts.silent ? "pipe" : "inherit",
env: {
...process.env,
...opts.env,
},
cwd: opts.cwd,
});
let output = ``;
if (opts.captureOutput ?? opts.silent) {
cmd.stdout?.on("data", (data) => {
output += data;
});
cmd.stderr?.on("data", (data) => {
output += data;
});
}
return new Promise<string>((resolve, reject) => {
cmd.on("close", (code) => {
try {
if (code !== 0) {
throw new Error(output, { cause: code });
}
// Process any captured output
const transformOutput =
opts.transformOutput ?? ((result: string) => result);
const processedOutput = transformOutput(stripAnsi(output));
// Send the captured (and processed) output back to the caller
resolve(processedOutput);
} catch (e) {
// Something went wrong.
// Perhaps the command or the transform failed.
// If there is a fallback use the result of calling that
if (opts.fallbackOutput) {
resolve(opts.fallbackOutput(e));
} else {
reject(new Error(output, { cause: e }));
}
}
});
cmd.on("error", (code) => {
reject(code);
});
});
},
});
};
// run multiple commands in sequence (not parallel)
export async function runCommands({ commands, ...opts }: MultiRunOptions) {
return printAsyncStatus({
useSpinner: opts.useSpinner ?? opts.silent,
startText: opts.startText,
doneText: opts.doneText,
async promise() {
const results = [];
for (const command of commands) {
results.push(await runCommand(command, { ...opts, useSpinner: false }));
}
return results.join("\n");
},
});
}
export const printAsyncStatus = async <T>({
promise,
...opts
}: PrintOptions<T>): Promise<T> => {
let s: ReturnType<typeof spinner> | undefined;
if (opts.useSpinner) {
s = spinner();
}
s?.start(opts?.startText);
if (typeof promise === "function") {
promise = promise();
}
try {
const output = await promise;
const doneText =
typeof opts.doneText === "function"
? opts.doneText(output)
: opts.doneText;
s?.stop(doneText);
} catch (err) {
s?.stop((err as Error).message);
} finally {
s?.stop();
}
return promise;
};
export const retry = async <T>(times: number, fn: () => Promise<T>) => {
let error: unknown = null;
while (times > 0) {
try {
return await fn();
} catch (e) {
error = e;
times--;
}
}
throw error;
};
// Prints the section header & footer while running the `cmd`
export const runFrameworkGenerator = async (
ctx: PagesGeneratorContext,
cmd: string
) => {
endSection(
`Continue with ${ctx.framework?.config.displayName}`,
`via \`${cmd.trim()}\``
);
if (process.env.VITEST) {
const flags = ctx.framework?.config.testFlags ?? [];
cmd = `${cmd} ${flags.join(" ")}`;
}
await runCommand(cmd);
};
type InstallConfig = {
startText?: string;
doneText?: string;
dev?: boolean;
};
export const installPackages = async (
packages: string[],
config: InstallConfig
) => {
const { npm } = detectPackageManager();
let saveFlag;
let cmd;
switch (npm) {
case "yarn":
cmd = "add";
saveFlag = config.dev ? "-D" : "";
break;
case "npm":
case "pnpm":
default:
cmd = "install";
saveFlag = config.dev ? "--save-dev" : "--save";
break;
}
await runCommand(`${npm} ${cmd} ${saveFlag} ${packages.join(" ")}`, {
...config,
silent: true,
});
};
export const npmInstall = async () => {
const { npm } = detectPackageManager();
await runCommand(`${npm} install`, {
silent: true,
startText: "Installing dependencies",
doneText: `${brandColor("installed")} ${dim(`via \`${npm} install\``)}`,
});
};
// Resets the package manager context for a project by clearing out existing dependencies
// and lock files then re-installing.
export const resetPackageManager = async (ctx: PagesGeneratorContext) => {
const { npm } = detectPackageManager();
// Only do this when using pnpm or yarn
if (npm === "npm") return;
const nodeModulesPath = path.join(ctx.project.path, "node_modules");
rmSync(nodeModulesPath, { recursive: true });
const lockfilePath = path.join(ctx.project.path, "package-lock.json");
rmSync(lockfilePath);
await runCommand(`${npm} install`, {
silent: true,
startText: "Installing dependencies",
doneText: `${brandColor("installed")} ${dim(`via \`${npm} install\``)}`,
});
};
export const installWrangler = async () => {
const { npm } = detectPackageManager();
// Exit early if already installed
if (existsSync(path.resolve("node_modules", "wrangler"))) {
return;
}
await installPackages([`wrangler`], {
dev: true,
startText: `Installing wrangler ${dim(
"A command line tool for building Cloudflare Workers"
)}`,
doneText: `${brandColor("installed")} ${dim(
`via \`${npm} install wrangler --save-dev\``
)}`,
});
};
export const isLoggedIn = async () => {
const { npx } = detectPackageManager();
try {
const output = await runCommand(`${npx} wrangler whoami`, {
silent: true,
});
return /You are logged in/.test(output);
} catch (error) {
return false;
}
};
export const wranglerLogin = async () => {
const { npx } = detectPackageManager();
const s = spinner();
s.start(`Logging into Cloudflare ${dim("checking authentication status")}`);
const alreadyLoggedIn = await isLoggedIn();
s.stop(brandColor(alreadyLoggedIn ? "logged in" : "not logged in"));
if (alreadyLoggedIn) return true;
s.start(`Logging into Cloudflare ${dim("This will open a browser window")}`);
// We're using a custom spinner since this is a little complicated.
// We want to vary the done status based on the output
const output = await runCommand(`${npx} wrangler login`, {
silent: true,
});
const success = /Successfully logged in/.test(output);
const verb = success ? "allowed" : "denied";
s.stop(`${brandColor(verb)} ${dim("via `wrangler login`")}`);
return success;
};
export const listAccounts = async () => {
const { npx } = detectPackageManager();
const output = await runCommand(`${npx} wrangler whoami`, {
silent: true,
});
const accounts: Record<string, string> = {};
output.split("\n").forEach((line) => {
const match = line.match(/│\s+(.+)\s+│\s+(\w{32})\s+│/);
if (match) {
accounts[match[1].trim()] = match[2].trim();
}
});
return accounts;
};
@@ -1,108 +0,0 @@
import fs, { existsSync } from "fs";
import { crash } from "./cli";
import type { PagesGeneratorContext } from "types";
export const writeFile = (path: string, content: string) => {
try {
fs.writeFileSync(path, content);
} catch (error) {
crash(error as string);
}
};
export const readFile = (path: string) => {
try {
return fs.readFileSync(path, "utf-8");
} catch (error) {
return crash(error as string);
}
};
export const readJSON = (path: string) => {
const contents = readFile(path);
return contents ? JSON.parse(contents) : contents;
};
export const writeJSON = (
path: string,
object: object,
stringifySpace?: number | string
) => {
writeFile(path, JSON.stringify(object, null, stringifySpace));
};
// Probes a list of paths and returns the first one that exists
// If one isn't found, throws an error with the given message
export const probePaths = (
paths: string[],
errorMsg = "Failed to find required file."
) => {
for (const path of paths) {
if (existsSync(path)) {
return path;
}
}
crash(errorMsg);
process.exit(1); // hack to make typescript happy
};
export const usesTypescript = (projectRoot = ".") => {
return existsSync(`${projectRoot}/tsconfig.json`);
};
const eslintRcExts = ["js", "cjs", "yaml", "yml", "json"] as const;
type EslintRcFileName = `.eslintrc.${typeof eslintRcExts[number]}`;
type EslintUsageInfo =
| {
used: true;
configType: EslintRcFileName | "eslint.config.js" | "package.json";
}
| {
used: false;
};
/*
checks if eslint is used and if so returns the configuration type
(for the various configuration types see:
- https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file-formats
- https://eslint.org/docs/latest/use/configure/configuration-files-new )
*/
export const usesEslint = (ctx: PagesGeneratorContext): EslintUsageInfo => {
for (const ext of eslintRcExts) {
const eslintRcFilename = `.eslintrc.${ext}` as EslintRcFileName;
if (existsSync(`${ctx.project.path}/${eslintRcFilename}`)) {
return {
used: true,
configType: eslintRcFilename,
};
}
}
if (existsSync(`${ctx.project.path}/eslint.config.js`)) {
return {
used: true,
configType: "eslint.config.js",
};
}
try {
const pkgJson = readJSON(`${ctx.project.path}/package.json`);
if (pkgJson.eslintConfig) {
return {
used: true,
configType: "package.json",
};
}
} catch {}
return { used: false };
};
// Generate a compatibility date flag
export const compatDateFlag = () => {
const date = new Date();
return `--compatibility-date=${date.toISOString().slice(0, 10)}`;
};
@@ -1,321 +0,0 @@
import { TextPrompt, SelectPrompt, ConfirmPrompt } from "@clack/core";
import { isCancel } from "@clack/prompts";
import logUpdate from "log-update";
import { shapes, cancel, space, status, newline, logRaw } from "./cli";
import { blue, dim, gray, brandColor, bold } from "./colors";
import type { C3Arg, CArgs } from "types";
const grayBar = gray(shapes.bar);
const blCorner = gray(shapes.corners.bl);
const leftT = gray(shapes.leftT);
export type Option = {
label: string;
value: string;
};
export type BasePromptConfig = {
// Displayed to the user while prompting for this input
question: string;
// Further clarifies the question
helpText?: string;
// The value to use by default
defaultValue?: string | boolean;
// The status label to be shown after submitting
label: string;
// Pretty-prints the value in the interactive prompt
format?: (value: C3Arg) => string;
// Returns a user displayed error if the value is invalid
validate?: (value: C3Arg) => string | void;
};
export type TextPromptConfig = BasePromptConfig & {
type: "text";
};
export type SelectPromptConfig =
| BasePromptConfig & {
type: "select";
options: Option[];
};
export type ConfirmPromptConfig =
| BasePromptConfig & {
type: "confirm";
activeText?: string;
inactiveText?: string;
};
export type PromptConfig =
| TextPromptConfig
| ConfirmPromptConfig
| SelectPromptConfig;
export const processArgument = async <T>(
args: Partial<CArgs>,
name: keyof CArgs,
promptConfig: PromptConfig
) => {
let value = args[name];
const renderSubmitted = getRenderers(promptConfig).submit;
// If the value has already been set via args, use that
if (value !== undefined) {
promptConfig.validate?.(value);
const lines = renderSubmitted({ value });
logRaw(lines.join("\n"));
return value as T;
}
value = await inputPrompt(promptConfig);
return value as T;
};
type RenderProps =
| Omit<SelectPrompt<Option>, "prompt">
| Omit<TextPrompt, "prompt">
| Omit<ConfirmPrompt, "prompt">;
export const inputPrompt = async (promptConfig: PromptConfig) => {
const renderers = getRenderers(promptConfig);
let prompt: SelectPrompt<Option> | TextPrompt | ConfirmPrompt;
// Looks up the needed renderer by the current state ('initial', 'submitted', etc.)
const dispatchRender = (props: RenderProps): string | void => {
const renderedLines = renderers[props.state](props);
return renderedLines.join("\n");
};
if (promptConfig.type === "select") {
prompt = new SelectPrompt({
...promptConfig,
options: (promptConfig as SelectPromptConfig).options,
initialValue: String(promptConfig.defaultValue),
render() {
return dispatchRender(this);
},
});
} else if (promptConfig.type === "confirm") {
prompt = new ConfirmPrompt({
...promptConfig,
initialValue: Boolean(promptConfig.defaultValue),
active: promptConfig.activeText || "",
inactive: promptConfig.inactiveText || "",
render() {
return dispatchRender(this);
},
});
} else {
prompt = new TextPrompt({
...promptConfig,
defaultValue: String(promptConfig.defaultValue),
render() {
return dispatchRender(this);
},
});
}
const input = (await prompt.prompt()) as string;
if (isCancel(input)) {
cancel("Operation cancelled.");
process.exit(0);
}
return input;
};
type Renderer = (props: {
state?: string;
error?: string;
cursor?: number;
value: C3Arg;
}) => string[];
const renderSubmit = (config: PromptConfig, value: string) => {
const { question, label } = config;
const content =
config.type === "confirm"
? `${grayBar} ${brandColor(value)} ${dim(label)}`
: `${grayBar} ${brandColor(label)} ${dim(value)}`;
return [`${leftT} ${question}`, content, `${grayBar}`];
};
const handleCancel = () => {
cancel("Operation cancelled.");
process.exit(0);
};
const getRenderers = (config: PromptConfig) => {
switch (config.type) {
case "select":
return getSelectRenderers(config);
case "confirm":
return getConfirmRenderers(config);
case "text":
return getTextRenderers(config);
}
};
const getTextRenderers = (config: TextPromptConfig) => {
const {
defaultValue,
question,
helpText: _helpText,
format: _format,
} = config;
const helpText = _helpText ?? "";
const format = _format ?? ((val: C3Arg) => String(val));
return {
initial: () => [
`${blCorner} ${bold(question)} ${dim(helpText)}`,
`${space(2)}${gray(format(defaultValue))}`,
``, // extra line for readability
],
active: ({ value }: { value: C3Arg }) => [
`${blCorner} ${bold(question)} ${dim(helpText)}`,
`${space(2)}${format(value || dim(defaultValue))}`,
``, // extra line for readability
],
error: ({ value, error }: { value: C3Arg; error: string }) => [
`${leftT} ${status.error} ${dim(error)}`,
`${grayBar}`,
`${blCorner} ${question} ${dim(helpText)}`,
`${space(2)}${format(value)}`,
``, // extra line for readability
],
submit: ({ value }: { value: C3Arg }) =>
renderSubmit(config, format(value)),
cancel: handleCancel,
};
};
const getSelectRenderers = (config: SelectPromptConfig) => {
const { options, question, helpText: _helpText } = config;
const helpText = _helpText ?? "";
const defaultRenderer: Renderer = ({ cursor }) => {
const renderOption = (opt: Option, i: number) => {
const { label: optionLabel } = opt;
const active = i === cursor;
const text = active ? blue.underline(optionLabel) : dim(optionLabel);
const indicator = active
? blue(shapes.radioActive)
: dim(shapes.radioInactive);
return `${space(2)}${indicator} ${text}`;
};
return [
`${blCorner} ${bold(question)} ${dim(helpText)}`,
`${options.map(renderOption).join(`\n`)}`,
``, // extra line for readability
];
};
return {
initial: defaultRenderer,
active: defaultRenderer,
confirm: defaultRenderer,
error: defaultRenderer,
submit: ({ value }: { value: C3Arg }) =>
renderSubmit(
config,
options.find((o) => o.value === value)?.label as string
),
cancel: handleCancel,
};
};
const getConfirmRenderers = (config: ConfirmPromptConfig) => {
const { activeText, inactiveText, question, helpText: _helpText } = config;
const helpText = _helpText ?? "";
const active = activeText || "Yes";
const inactive = inactiveText || "No";
const defaultRenderer: Renderer = ({ value }) => {
const yesColor = value ? blue : dim;
const noColor = value ? dim : blue;
return [
`${blCorner} ${bold(question)} ${dim(helpText)}`,
`${space(2)}${yesColor(active)} / ${noColor(inactive)}`,
];
};
return {
initial: defaultRenderer,
active: defaultRenderer,
confirm: defaultRenderer,
error: defaultRenderer,
submit: ({ value }: { value: C3Arg }) =>
renderSubmit(config, value ? "yes" : "no"),
cancel: handleCancel,
};
};
export const spinnerFrames = ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"];
const ellipsisFrames = ["", ".", "..", "...", " ..", " .", ""];
export const spinner = () => {
// Alternative animations we considered. Keeping around in case we
// introduce different animations for different use cases.
// const frames = ["▁", "▃", "▄", "▅", "▆", "▇", "▆", "▅", "▄", "▃"];
// const frames = ["■", "□", "▪", "▫"];
// const frames = ["✶", "✸", "✹", "✺", "✹", "✷"];
// const frames = ["◜", "◠", "◝", "◞", "◡", "◟"];
// const frames = ["◐", "◓", "◑", "◒"];
// const frames = ["㊂", "㊀", "㊁"];
const color = brandColor;
const frameRate = 120;
let loop: NodeJS.Timer | null = null;
let startMsg: string;
let currentMsg: string;
function clearLoop() {
if (loop) {
clearTimeout(loop);
}
loop = null;
}
return {
start(msg: string, helpText?: string) {
helpText ||= ``;
currentMsg = msg;
startMsg = `${currentMsg} ${dim(helpText)}`;
let index = 0;
clearLoop();
loop = setInterval(() => {
index++;
const spinnerFrame = spinnerFrames[index % spinnerFrames.length];
const ellipsisFrame = ellipsisFrames[index % ellipsisFrames.length];
if (msg) {
logUpdate(`${color(spinnerFrame)} ${currentMsg} ${ellipsisFrame}`);
}
}, frameRate);
},
update(msg: string) {
currentMsg = msg;
},
stop(msg?: string) {
// Write the final message and clear the loop
logUpdate.clear();
if (msg) {
logUpdate(`${leftT} ${startMsg}\n${grayBar} ${msg}`);
logUpdate.done();
newline();
}
clearLoop();
},
};
};
@@ -1,65 +0,0 @@
import semver from "semver";
import whichPmRuns from "which-pm-runs";
import { devDependencies } from "../../package.json";
/*
A helper function for determining which pm command to use based on which one the user
invoked this CLI with.
The entries of the return type are used for the following operations:
- npm: running commands with the package manager (ex. `npm install` or `npm run build`)
- npx: executing code local to the working directory (ex. `npx wrangler whoami`)
- dlx: executing packages that are not installed locally (ex. `pnpm dlx create-solid`)
*/
export const detectPackageManager = () => {
let { name, version } = whichPmRuns() ?? { name: "npm", version: "0.0.0" };
if (process.env.TEST_PM) {
switch (process.env.TEST_PM) {
case "pnpm":
name = "pnpm";
version = devDependencies["pnpm"].replace("^", "");
break;
case "yarn":
name = "yarn";
version = devDependencies["yarn"].replace("^", "");
break;
}
}
switch (name) {
case "pnpm":
if (semver.gt(version, "6.0.0")) {
return {
npm: "pnpm",
npx: "pnpm",
dlx: "pnpm dlx",
};
}
return {
npm: "pnpm",
npx: "pnpx",
dlx: "pnpx",
};
case "yarn":
if (semver.gt(version, "2.0.0")) {
return {
npm: "yarn",
npx: "yarn",
dlx: "yarn dlx",
};
}
return {
npm: "yarn",
npx: "yarn",
dlx: "yarn",
};
case "npm":
default:
return {
npm: "npm",
npx: "npx",
dlx: "npx",
};
}
};
@@ -1,136 +0,0 @@
import dns2 from "dns2";
import { request } from "undici";
import { blue, brandColor, dim } from "./colors";
import { spinner } from "./interactive";
import type { DnsAnswer, DnsResponse } from "dns2";
const TIMEOUT = 1000 * 60 * 5;
const POLL_INTERVAL = 1000;
/*
A helper to wait until the newly deployed domain is available.
We do this by first polling DNS until the new domain is resolvable, and then polling
via HTTP until we get a successful response.
Note that when polling DNS we make queries against specific nameservers to avoid negative
caching. Similarly, we poll via HTTP using the 'no-cache' header for the same reason.
*/
export const poll = async (url: string): Promise<boolean> => {
const start = Date.now();
const domain = new URL(url).host;
const s = spinner();
s.start("Waiting for DNS to propagate");
// Start out by sleeping for 10 seconds since it's unlikely DNS changes will
// have propogated before then
await sleep(10 * 1000);
await pollDns(domain, start, s);
if (await pollHttp(url, start, s)) return true;
s.stop(
`${brandColor(
"timed out"
)} while waiting for ${url} - try accessing it in a few minutes.`
);
return false;
};
const pollDns = async (
domain: string,
start: number,
s: ReturnType<typeof spinner>
) => {
while (Date.now() - start < TIMEOUT) {
s.update(`Waiting for DNS to propagate (${secondsSince(start)}s)`);
if (await isDomainResolvable(domain)) {
s.stop(`${brandColor("DNS propagation")} ${dim("complete")}.`);
return;
}
await sleep(POLL_INTERVAL);
}
};
const pollHttp = async (
url: string,
start: number,
s: ReturnType<typeof spinner>
) => {
s.start("Waiting for deployment to become available");
while (Date.now() - start < TIMEOUT) {
s.update(
`Waiting for deployment to become available (${secondsSince(start)}s)`
);
try {
const { statusCode } = await request(url, {
reset: true,
headers: { "Cache-Control": "no-cache" },
});
if (statusCode === 200) {
s.stop(
`${brandColor("deployment")} ${dim("is ready at:")} ${blue(url)}`
);
return true;
}
} catch (e) {
// Re-throw if the error is not ENOTFOUND
if ((e as { code: string }).code !== "ENOTFOUND") {
throw e;
}
}
await sleep(POLL_INTERVAL);
}
};
// Determines if the domain is resolvable via DNS. Until this condition is true,
// any HTTP requests will result in an NXDOMAIN error.
export const isDomainResolvable = async (domain: string) => {
try {
const nameServers = await lookupSubdomainNameservers(domain);
// If the subdomain nameservers aren't resolvable yet, keep polling
if (nameServers.length === 0) return false;
// Once they are resolvable, query these nameservers for the domain's 'A' record
const dns = new dns2({ nameServers });
const res = await dns.resolve(domain, "A");
return res.answers.length > 0;
} catch (error) {
return false;
}
};
// Looks up the nameservers that are responsible for this particular domain
export const lookupSubdomainNameservers = async (domain: string) => {
const nameServers = await lookupDomainLevelNameservers(domain);
const dns = new dns2({ nameServers });
const res = (await dns.resolve(domain, "NS")) as DnsResponse;
return (
res.authorities
// Filter out non-authoritative authorities (ones that don't have an 'ns' property)
.filter((r) => Boolean(r.ns))
// Return only the hostnames of the authoritative servers
.map((r) => r.ns)
);
};
// Looks up the nameservers responsible for handling `pages.dev` or `workers.dev` domains
export const lookupDomainLevelNameservers = async (domain: string) => {
// Get the last 2 parts of the domain (ie. `pages.dev` or `workers.dev`)
const baseDomain = domain.split(".").slice(-2).join(".");
const dns = new dns2({});
const nameservers = await dns.resolve(baseDomain, "NS");
return (nameservers.answers as DnsAnswer[]).map((n) => n.ns);
};
async function sleep(ms: number) {
return new Promise((res) => setTimeout(res, ms));
}
function secondsSince(start: number): number {
return Math.round((Date.now() - start) / 1000);
}
-9
View File
@@ -1,9 +0,0 @@
export type CArgs = {
projectName: string;
deploy?: boolean;
open?: boolean;
};
export type CArg = CArgs[keyof CArgs];
type UpdaterPackageScript = (cmd: string) => string;
@@ -1,91 +0,0 @@
import { readFile, writeFile, mkdtemp, cp, rm, readdir } from "fs/promises";
import { tmpdir } from "os";
import { resolve, join } from "path";
import { chdir } from "process";
import { endSection, updateStatus, startSection } from "helpers/cli";
import { brandColor, dim } from "helpers/colors";
import {
npmInstall,
runCommand,
} from "helpers/command";
import { processArgument } from "helpers/interactive";
import { C_DEFAULTS } from "./cli";
import {
chooseAccount,
gitCommit,
offerGit,
offerToDeploy,
printSummary,
runDeploy,
setupProjectDirectory,
} from "./common";
import type { CArgs, PagesGeneratorContext as Context } from "types";
export const runWorkersGenerator = async (args: CArgs) => {
const { name, path } = setupProjectDirectory(args);
const ctx: Context = {
project: { name, path },
args,
};
await copyFiles(ctx);
await updateFiles(ctx);
endSection("Application created");
startSection("Installing dependencies", "Step 2 of 3");
chdir(ctx.project.path);
await npmInstall();
endSection("Dependencies Installed");
await offerToDeploy(ctx);
await runDeploy(ctx);
await printSummary(ctx);
};
async function getTemplate() {
return resolve(
// eslint-disable-next-line no-restricted-globals
__dirname,
"..",
"template",
);
}
async function copyFiles(ctx: Context) {
const path = await getTemplate();
const destdir = ctx.project.path;
// copy template files
updateStatus('Copying files from template');
await cp(path, destdir, { recursive: true });
}
async function updateFiles(ctx: Context) {
// build file paths
const paths = {
packagejson: resolve(ctx.project.path, "package.json"),
wranglertoml: resolve(ctx.project.path, "wrangler.toml"),
};
// read files
const contents = {
packagejson: JSON.parse(await readFile(paths.packagejson, "utf-8")),
wranglertoml: await readFile(paths.wranglertoml, "utf-8"),
};
// update files
if (contents.packagejson.name === "<TBD>") {
contents.packagejson.name = ctx.project.name;
}
contents.wranglertoml = contents.wranglertoml
.replace(/^name\s*=\s*"<TBD>"/m, `name = "${ctx.project.name}"`)
// write files
await writeFile(
paths.packagejson,
JSON.stringify(contents.packagejson, null, 2)
);
await writeFile(paths.wranglertoml, contents.wranglertoml);
}
@@ -1,171 +0,0 @@
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
# wrangler project
.dev.vars
@@ -1,43 +0,0 @@
# My Personal R2 Explorer
This is my personal [R2 Explorer](https://github.com/G4brym/R2-Explorer) application self hosted on my Cloudflare
account.
You can read the full [documentation here](https://r2explorer.dev/).
## Get started
1. Sign up for [Cloudflare Workers](https://workers.dev). The free tier is more than enough for this, you will only pay
for the [R2 storage](https://developers.cloudflare.com/r2/pricing/).
2. Clone this project and install dependencies with `npm install`
3. Run `wrangler login` to login to your Cloudflare account in wrangler
4. Run `wrangler deploy` to publish to Cloudflare Workers
### Optional Steps
1. Read only mode is enabled by default, disable it
in `src/index.ts`, [documentation here](https://r2explorer.dev/getting-started/configuration/#disabling-readonly-mode).
2. Learn how to secure your R2 Explorer in the
official [documentation here](https://r2explorer.dev/getting-started/security/).
## Updating the instance
Install latest version
```bash
npm install r2-explorer@latest --save
```
Deploy to Cloudflare Workers
```bash
wrangler deploy
```
## Deploying
Deploy to Cloudflare Workers
```bash
wrangler deploy
```
@@ -1,15 +0,0 @@
{
"name": "<TBD>",
"version": "0.1.0",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"start": "wrangler dev"
},
"dependencies": {
"r2-explorer": "^1.0.0"
},
"devDependencies": {
"wrangler": "^3.0.0"
}
}
@@ -1,3 +0,0 @@
import { R2Explorer } from 'r2-explorer';
export default R2Explorer({ readonly: true });
@@ -1,3 +0,0 @@
name = "<TBD>"
main = "src/index.ts"
compatibility_date = "2023-05-12"
-20
View File
@@ -1,20 +0,0 @@
{
"root": true,
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist", "templates", "**/templates/**"],
"compilerOptions": {
"target": "ESNext",
"module": "CommonJS",
"moduleResolution": "node",
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"outDir": "dist",
"types": ["node"],
"resolveJsonModule": true,
"paths": {
"helpers/*": ["./src/helpers/*"],
"types": ["./src/types.ts"]
}
}
}
-4
View File
@@ -23,10 +23,6 @@ export default defineConfig({
src: ['../LICENSE', '../README.md'],
dest: '.',
},
{
src: '../create-r2-explorer/dist/cli.js',
dest: './bin',
},
],
}),
],
-90
View File
@@ -30,96 +30,6 @@ importers:
specifier: ^4.7.4
version: 4.9.5
packages/create-r2-explorer:
devDependencies:
'@babel/parser':
specifier: ^7.21.3
version: 7.22.10
'@babel/types':
specifier: ^7.21.4
version: 7.22.10
'@clack/core':
specifier: ^0.3.4
version: 0.3.4
'@clack/prompts':
specifier: ^0.6.3
version: 0.6.3
'@cloudflare/workers-types':
specifier: ^4.20230419.0
version: 4.20230814.0
'@types/command-exists':
specifier: ^1.2.0
version: 1.2.0
'@types/cross-spawn':
specifier: ^6.0.2
version: 6.0.2
'@types/esprima':
specifier: ^4.0.3
version: 4.0.3
'@types/node':
specifier: ^18.15.3
version: 18.17.6
'@types/which-pm-runs':
specifier: ^1.0.0
version: 1.0.0
'@types/yargs':
specifier: ^17.0.22
version: 17.0.24
'@typescript-eslint/parser':
specifier: ^5.55.0
version: 5.62.0([email protected])([email protected])
chalk:
specifier: ^5.2.0
version: 5.3.0
command-exists:
specifier: ^1.2.9
version: 1.2.9
cross-spawn:
specifier: ^7.0.3
version: 7.0.3
dns2:
specifier: ^2.1.0
version: 2.1.0
esbuild:
specifier: ^0.17.12
version: 0.17.19
execa:
specifier: ^7.1.1
version: 7.2.0
haikunator:
specifier: ^2.1.2
version: 2.1.2
log-update:
specifier: ^5.0.1
version: 5.0.1
open:
specifier: ^8.4.0
version: 8.4.2
pnpm:
specifier: ^8.6.11
version: 8.6.12
semver:
specifier: ^7.6.0
version: 7.6.0
typescript:
specifier: ^5.0.2
version: 5.1.6
undici:
specifier: ^6.7.1
version: 6.7.1
which-pm-runs:
specifier: ^1.1.0
version: 1.1.0
wrangler:
specifier: '*'
version: 3.5.1
yargs:
specifier: ^17.7.1
version: 17.7.2
yarn:
specifier: ^1.22.19
version: 1.22.19
packages/dashboard:
dependencies:
'@quasar/extras':
+2 -2
View File
@@ -1,5 +1,5 @@
{
"name": "<TBD>",
"name": "r2-explorer",
"version": "1.0.0",
"private": true,
"scripts": {
@@ -8,7 +8,7 @@
"start": "wrangler dev"
},
"dependencies": {
"r2-explorer": "^1.0.0"
"r2-explorer": "^1.0.7"
},
"devDependencies": {
"wrangler": "^3.91.0"
+1
View File
@@ -1,5 +1,6 @@
import { R2Explorer } from 'r2-explorer';
// Read all available configurations here: https://r2explorer.dev/getting-started/configuration/
export default R2Explorer({
readonly: true,
// basicAuth: {
+2 -2
View File
@@ -1,6 +1,6 @@
name = "<TBD>"
name = "r2-explorer"
main = "src/index.ts"
compatibility_date = "<TBD>"
compatibility_date = "2024-12-01"
# Bind R2 Buckets to your application.
# Docs: https://r2explorer.dev/getting-started/add-r2-buckets/