mirror of
https://github.com/ApfelTeeSaft/R2-Explorer.git
synced 2026-08-26 19:33:39 +00:00
Remove create-r2-explorer in favor of using cloudflare c3
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
dist
|
||||
node_modules
|
||||
packages/create-r2-explorer/*
|
||||
packages/github-action/*
|
||||
packages/worker/bin/*
|
||||
packages/worker/config/*
|
||||
|
||||
@@ -38,3 +38,6 @@ packages/github-action/src
|
||||
packages/dashboard/.env
|
||||
packages/dashboard/.quasar
|
||||
.dev.vars
|
||||
|
||||
template/package-lock.json
|
||||
template/node_modules/
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,10 +23,6 @@ export default defineConfig({
|
||||
src: ['../LICENSE', '../README.md'],
|
||||
dest: '.',
|
||||
},
|
||||
{
|
||||
src: '../create-r2-explorer/dist/cli.js',
|
||||
dest: './bin',
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
Generated
-90
@@ -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':
|
||||
|
||||
@@ -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,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: {
|
||||
|
||||
@@ -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/
|
||||
|
||||
Reference in New Issue
Block a user