Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add CLI for uploading template contents to templates API #166

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/branches.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ jobs:
with:
node-version: "20.x"
- run: npm ci
- run: npm run build:cli
# Need to install a second time to get the CLI build linked up in the
# right place.
- run: npm ci
- run: npm run check:ci
6 changes: 6 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ jobs:
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
# TODO: Template API credentials
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: "20.x"
- run: npm ci
- run: npm run build:cli
# Need to install a second time to get the CLI build linked up in the
# right place.
- run: npm ci
- run: npm run check:ci
- run: npm run deploy
- run: npm run upload
34 changes: 34 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Templates CLI

A handy CLI for developing templates.

## Upload

The `upload` command uploads template contents to the Cloudflare Templates API for consumption by the Cloudflare dashboard and other template clients. This command runs in CI on merges to the `main` branch.

```
$ npx cli help upload
Usage: cli upload [options] [path-to-templates]

upload templates to the templates API

Arguments:
path-to-templates path to directory containing templates (default: ".")
```

## Lint

The `lint` command finds and fixes template style problems that aren't covered by Prettier or ESList. This linter focuses on Cloudflare-specific configuration and project structure.

```
$ npx cli help lint
Usage: cli lint [options] [path-to-templates]

find and fix template style problems

Arguments:
path-to-templates path to directory containing templates (default: ".")

Options:
--fix fix problems that can be automatically fixed
```
15 changes: 15 additions & 0 deletions cli/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import PACKAGE from "./package.json" assert { type: "json" };
import * as esbuild from "esbuild";
import fs from "node:fs";

const outfile = PACKAGE["bin"];

await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
sourcemap: true,
platform: "node",
outfile,
});

fs.writeFileSync(outfile, "#!/usr/bin/env node\n" + fs.readFileSync(outfile));
17 changes: 17 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "cli",
"description": "A handy CLI for developing templates.",
"bin": "out/cli.js",
"dependencies": {
"commander": "12.1.0"
},
"devDependencies": {
"@types/node": "22.9.1",
"esbuild": "0.24.0",
"typescript": "5.6.3"
},
"scripts": {
"build": "node build.mjs",
"check": "tsc"
}
}
34 changes: 34 additions & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Command } from "commander";
import { upload } from "./upload";
import { lint } from "./lint";

const program = new Command();

program.name("cli").description("a handy CLI for developing templates");

program
.command("upload")
.description("upload templates to the templates API")
.argument(
"[path-to-templates]",
"path to directory containing templates",
".",
)
.action((templateDirectory: string) =>
upload({ templateDirectory, apiBaseUrl: "TODO" }),
);

program
.command("lint")
.description("find and fix template style problems")
.argument(
"[path-to-templates]",
"path to directory containing templates",
".",
)
.option("--fix", "fix problems that can be automatically fixed")
.action((templateDirectory: string, options: { fix: boolean }) => {
lint({ templateDirectory, fix: options.fix });
});

program.parse();
68 changes: 68 additions & 0 deletions cli/src/lint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import path from "node:path";
import { getTemplatePaths, readJSON, writeJSON } from "./util";

export type LintConfig = {
templateDirectory: string;
fix: boolean;
};

export function lint(config: LintConfig) {
const templatePaths = getTemplatePaths(config.templateDirectory);
const results = templatePaths.flatMap((templatePath) =>
lintTemplate(templatePath, config.fix),
);
if (results.length > 0) {
results.forEach(({ filePath, problems }) => {
console.error(`Problems with ${filePath}`);
problems.forEach((problem) => {
console.log(` - ${problem}`);
});
});
process.exit(1);
}
}
const CHECKS = {
"wrangler.json": [lintWrangler],
};
const TARGET_COMPATIBILITY_DATE = "2024-11-01";

type FileDiagnostic = {
filePath: string;
problems: string[];
};

function lintTemplate(templatePath: string, fix: boolean): FileDiagnostic[] {
return Object.entries(CHECKS).flatMap(([file, linters]) => {
const filePath = path.join(templatePath, file);
const problems = linters.flatMap((linter) => linter(filePath, fix));
return problems.length > 0 ? [{ filePath, problems }] : [];
});
}

function lintWrangler(filePath: string, fix: boolean): string[] {
const wrangler = readJSON(filePath) as {
compatibility_date?: string;
observability?: { enabled: boolean };
upload_source_maps?: boolean;
};
if (fix) {
wrangler.compatibility_date = TARGET_COMPATIBILITY_DATE;
wrangler.observability = { enabled: true };
wrangler.upload_source_maps = true;
writeJSON(filePath, wrangler);
return [];
}
const problems = [];
if (wrangler.compatibility_date !== TARGET_COMPATIBILITY_DATE) {
problems.push(
`"compatibility_date" should be set to "${TARGET_COMPATIBILITY_DATE}"`,
);
}
if (wrangler.observability?.enabled !== true) {
problems.push(`"observability" should be set to { "enabled": true }`);
}
if (wrangler.upload_source_maps !== true) {
problems.push(`"upload_source_maps" should be set to true`);
}
return problems;
}
64 changes: 64 additions & 0 deletions cli/src/upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import fs from "node:fs";
import path from "node:path";
import subprocess from "node:child_process";
import { getTemplatePaths } from "./util";

export type UploadConfig = {
templateDirectory: string;
apiBaseUrl: string;
};

export async function upload(config: UploadConfig) {
const templatePaths = getTemplatePaths(config.templateDirectory);
const results = await Promise.allSettled(
templatePaths.map((templatePath) => uploadTemplate(templatePath, config)),
);
if (results.some((result) => result.status === "rejected")) {
results.forEach((result, i) => {
if (result.status === "rejected") {
console.error(`Upload ${templatePaths[i]} failed: ${result.reason}`);
}
});
process.exit(1);
}
}

async function uploadTemplate(templatePath: string, config: UploadConfig) {
const files = collectTemplateFiles(templatePath);
console.info(`Uploading ${templatePath}:`);
const body = new FormData();
files.forEach((file) => {
console.info(` ${file.name}`);
body.set(file.name, file);
});
const url = `${config.apiBaseUrl}/upload/path`;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to fill in the real API path there once that's finalized

const response = await fetch(url, { method: "POST", body });
if (!response.ok) {
throw new Error(
`Error response from ${url} (${response.status}): ${await response.text()}`,
);
}
}

function collectTemplateFiles(templatePath: string): File[] {
return fs
.readdirSync(templatePath, { recursive: true })
.map((file) => ({
name: file.toString(),
filePath: path.join(templatePath, file.toString()),
}))
.filter(
({ filePath }) =>
!fs.statSync(filePath).isDirectory() && !gitIgnored(filePath),
)
.map(({ name, filePath }) => new File([fs.readFileSync(filePath)], name));
}

function gitIgnored(filePath: string): boolean {
try {
subprocess.execSync(`git check-ignore ${filePath}`);
return true;
} catch {
return false;
}
}
23 changes: 23 additions & 0 deletions cli/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import fs from "node:fs";
import path from "node:path";

const TEMPLATE_DIRECTORY_SUFFIX = "-template";

export function getTemplatePaths(templateDirectory: string): string[] {
return fs
.readdirSync(templateDirectory)
.filter(
(file) =>
file.endsWith(TEMPLATE_DIRECTORY_SUFFIX) &&
fs.statSync(file).isDirectory(),
)
.map((template) => path.join(templateDirectory, template));
}

export function readJSON(filePath: string): unknown {
return JSON.parse(fs.readFileSync(filePath, { encoding: "utf-8" }));
}

export function writeJSON(filePath: string, object: unknown) {
fs.writeFileSync(filePath, JSON.stringify(object, undefined, 2) + "\n");
}
14 changes: 14 additions & 0 deletions cli/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "esnext",
"lib": ["esnext"],
"module": "nodenext",
"types": ["@types/node"],
"noEmit": true,
"isolatedModules": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"strict": true
},
"include": ["src"]
}
16 changes: 13 additions & 3 deletions d1-template/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# D1 Template
# Worker + D1 Database

[Visit](https://d1-template.templates.workers.dev)
Cloudflare's native serverless SQL database.

TODO
## Develop Locally

Use this template with [C3](https://developers.cloudflare.com/pages/get-started/c3/) (the `create-cloudflare` CLI):

```
npm create cloudflare@latest -- --template=cloudflare/templates/d1-template
```

## Preview Deployment

A live public deployment of this template is available at [https://d1-template.templates.workers.dev](https://d1-template.templates.workers.dev)
2 changes: 1 addition & 1 deletion d1-template/wrangler.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"compatibility_date": "2024-11-15",
"compatibility_date": "2024-11-01",
"main": "src/index.ts",
"name": "d1-template",
"upload_source_maps": true,
Expand Down
23 changes: 23 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @ts-check

import eslint from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.strictTypeChecked,
{
languageOptions: {
parserOptions: {
project: ["./*-template/tsconfig.json", "./cli/tsconfig.json"],
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
"@typescript-eslint/restrict-template-expressions": "off",
},
},
{
ignores: ["**/*.js", "**/*.mjs"],
},
);
16 changes: 13 additions & 3 deletions image-classification-template/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Image Classification Template
# Image Classification App

[Visit](https://image-classification-template.templates.workers.dev)
Identify and label objects found in images.

TODO
## Develop Locally

Use this template with [C3](https://developers.cloudflare.com/pages/get-started/c3/) (the `create-cloudflare` CLI):

```
npm create cloudflare@latest -- --template=cloudflare/templates/image-classification-template
```

## Preview Deployment

A live public deployment of this template is available at [https://image-classification-template.templates.workers.dev](https://image-classification-template.templates.workers.dev)
2 changes: 1 addition & 1 deletion image-classification-template/wrangler.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"compatibility_date": "2024-11-15",
"compatibility_date": "2024-11-01",
"main": "src/index.ts",
"name": "image-classification-template",
"upload_source_maps": true,
Expand Down
16 changes: 13 additions & 3 deletions llm-template/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# LLM Template
# LLM App

[Visit](https://llm-template.templates.workers.dev)
Perform natural language processing tasks using a Large Language Model (LLM) with Workers.

TODO
## Develop Locally

Use this template with [C3](https://developers.cloudflare.com/pages/get-started/c3/) (the `create-cloudflare` CLI):

```
npm create cloudflare@latest -- --template=cloudflare/templates/llm-template
```

## Preview Deployment

A live public deployment of this template is available at [https://llm-template.templates.workers.dev](https://llm-template.templates.workers.dev)
Loading