Skip to content

Commit

Permalink
C3: next templates refactoring (#4999)
Browse files Browse the repository at this point in the history
* add variant selection to c3 templates' `copyFiles`

* add possibility to edit destination dir for c3's `copyFiles`

* C3: refactor Next.js template code

Align the Next.js template code with the rest of the
C3 templates by removing its `templates.ts` file and
including templates files directly in the next template
directory

* add new copyFile utility

* fix: make sure not to wrongly ask users if they want to use typescript
  • Loading branch information
dario-piotrowicz authored Feb 13, 2024
1 parent 246512c commit ce6d4bc
Show file tree
Hide file tree
Showing 20 changed files with 472 additions and 395 deletions.
9 changes: 9 additions & 0 deletions .changeset/ten-jars-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"create-cloudflare": patch
---

fix: make sure not to wrongly ask users if they want to use typescript

currently if a CLI invoked by C3 asks the user if they want to use
typescript and the user opted out of it, C3 could actually again offer
typescript to the user afterwords, make sure that this does not happen
8 changes: 8 additions & 0 deletions packages/create-cloudflare/src/helpers/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import TOML from "@iarna/toml";
import { getWorkerdCompatibilityDate } from "./command";
import type { C3Context } from "types";

export const copyFile = (path: string, dest: string) => {
try {
fs.copyFileSync(path, dest);
} catch (error) {
crash(error as string);
}
};

export const writeFile = (path: string, content: string) => {
try {
fs.writeFileSync(path, content);
Expand Down
98 changes: 75 additions & 23 deletions packages/create-cloudflare/src/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,26 @@ export type TemplateConfig = {
* }
* ```
*
* Or an object with a file paths for `js` and `ts` versions:
* Or an object containing different variants:
* ```js
* {
* copyFiles: {
* js: { path: "./js"},
* ts: { path: "./ts"},
* variants: {
* js: { path: "./js"},
* ts: { path: "./ts"},
* }
* }
* }
* ```
* In such case the `js` variant will be used if the project
* uses JavaScript and the `ts` variant will be used if the project
* uses TypeScript.
*
* The above mentioned behavior is the default one and can be customized
* by providing a `selectVariant` method.
*
*/
copyFiles?: StaticFileMap | VariantInfo;
copyFiles?: CopyFiles;

/** A function invoked as the first step of project creation.
* Used to invoke framework creation cli in the internal web framework templates.
Expand Down Expand Up @@ -80,12 +88,24 @@ export type TemplateConfig = {
path?: string;
};

type CopyFiles = (StaticFileMap | VariantInfo) & {
destinationDir?: string | ((ctx: C3Context) => string);
};

// A template can have a number of variants, usually js/ts
type VariantInfo = {
path: string;
};

type StaticFileMap = Record<string, VariantInfo>;
type StaticFileMap = {
selectVariant?: (ctx: C3Context) => Promise<string>;
variants: Record<string, VariantInfo>;
};

const defaultSelectVariant = async (ctx: C3Context) => {
const typescript = await shouldUseTs(ctx);
return typescript ? "ts" : "js";
};

export type FrameworkMap = Awaited<ReturnType<typeof getFrameworkMap>>;
export type FrameworkName = keyof FrameworkMap;
Expand Down Expand Up @@ -219,19 +239,21 @@ export async function copyTemplateFiles(ctx: C3Context) {
const { copyFiles } = ctx.template;

let srcdir;
if (copyFiles.path) {
if (isVariantInfo(copyFiles)) {
// If there's only one variant, just use that.
srcdir = join(getTemplatePath(ctx), (copyFiles as VariantInfo).path);
srcdir = join(getTemplatePath(ctx), copyFiles.path);
} else {
// Otherwise, have the user select the one they want
const typescript = await shouldUseTs(ctx);
const languageTarget = typescript ? "ts" : "js";
const selectVariant = copyFiles.selectVariant ?? defaultSelectVariant;

const variant = await selectVariant(ctx);

const variantPath = (copyFiles as StaticFileMap)[languageTarget].path;
const variantPath = copyFiles.variants[variant].path;
srcdir = join(getTemplatePath(ctx), variantPath);
}

const destdir = ctx.project.path;
const copyDestDir = await getCopyFilesDestinationDir(ctx);
const destdir = join(ctx.project.path, ...(copyDestDir ? [copyDestDir] : []));

const s = spinner();
s.start(`Copying template files`);
Expand All @@ -254,6 +276,12 @@ const shouldUseTs = async (ctx: C3Context) => {
return true;
}

// If there is a generate process then we assume that a potential typescript
// setup must have been part of it, so we should not offer it here
if (ctx.template.generate) {
return false;
}

// Otherwise, prompt the user for their TS preference
return processArgument<boolean>(ctx.args, "ts", {
type: "confirm",
Expand Down Expand Up @@ -284,10 +312,14 @@ export const processRemoteTemplate = async (args: Partial<C3Args>) => {
};

const validateTemplate = (path: string, config: TemplateConfig) => {
if (typeof config.copyFiles?.path == "string") {
if (!config.copyFiles) {
return;
}

if (isVariantInfo(config.copyFiles)) {
validateTemplateSrcDirectory(resolve(path, config.copyFiles.path), config);
} else {
for (const variant of Object.values(config.copyFiles as StaticFileMap)) {
for (const variant of Object.values(config.copyFiles.variants)) {
validateTemplateSrcDirectory(resolve(path, variant.path), config);
}
}
Expand Down Expand Up @@ -322,21 +354,19 @@ const inferTemplateConfig = (path: string): TemplateConfig => {
};
};

const inferCopyFilesDefinition = (path: string) => {
const copyFiles: StaticFileMap | VariantInfo = {};
const inferCopyFilesDefinition = (path: string): CopyFiles => {
const variants: StaticFileMap["variants"] = {};
if (existsSync(join(path, "js"))) {
copyFiles["js"] = { path: "./js" };
variants["js"] = { path: "./js" };
}
if (existsSync(join(path, "ts"))) {
copyFiles["ts"] = { path: "./ts" };
}
if (Object.keys(copyFiles).length !== 0) {
return copyFiles;
variants["ts"] = { path: "./ts" };
}

return {
path: ".",
};
const copyFiles =
Object.keys(variants).length !== 0 ? { variants } : { path: "." };

return copyFiles;
};

/**
Expand Down Expand Up @@ -412,3 +442,25 @@ export const getTemplatePath = (ctx: C3Context) => {

return resolve(__dirname, "..", "templates", ctx.template.id);
};

export const isVariantInfo = (
copyFiles: CopyFiles
): copyFiles is VariantInfo => {
return "path" in (copyFiles as VariantInfo);
};

export const getCopyFilesDestinationDir = (
ctx: C3Context
): undefined | string => {
const { copyFiles } = ctx.template;

if (!copyFiles?.destinationDir) {
return undefined;
}

if (typeof copyFiles.destinationDir === "string") {
return copyFiles.destinationDir;
}

return copyFiles.destinationDir(ctx);
};
12 changes: 7 additions & 5 deletions packages/create-cloudflare/templates/common/c3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ export default {
displayName: "Example router & proxy Worker",
platform: "workers",
copyFiles: {
js: {
path: "./js",
},
ts: {
path: "./ts",
variants: {
js: {
path: "./js",
},
ts: {
path: "./ts",
},
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ export default {
displayName: '"Hello World" Durable Object',
platform: "workers",
copyFiles: {
js: {
path: "./js",
},
ts: {
path: "./ts",
variants: {
js: {
path: "./js",
},
ts: {
path: "./ts",
},
},
},
};
12 changes: 7 additions & 5 deletions packages/create-cloudflare/templates/hello-world/c3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ export default {
displayName: '"Hello World" Worker',
platform: "workers",
copyFiles: {
js: {
path: "./js",
},
ts: {
path: "./ts",
variants: {
js: {
path: "./js",
},
ts: {
path: "./ts",
},
},
},
};
57 changes: 57 additions & 0 deletions packages/create-cloudflare/templates/next/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`c3`](https://developers.cloudflare.com/pages/get-started/c3).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

## Cloudflare integration

Besides the `dev` script mentioned above `c3` has added a few extra scripts that allow you to integrate the application with the [Cloudflare Pages](https://pages.cloudflare.com/) environment, these are:
- `pages:build` to build the application for Pages using the [`@cloudflare/next-on-pages`](https://github.com/cloudflare/next-on-pages) CLI
- `pages:preview` to locally preview your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI
- `pages:deploy` to deploy your Pages application using the [Wrangler](https://developers.cloudflare.com/workers/wrangler/) CLI

> __Note:__ while the `dev` script is optimal for local development you should preview your Pages application as well (periodically or before deployments) in order to make sure that it can properly work in the Pages environment (for more details see the [`@cloudflare/next-on-pages` recommended workflow](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md#recommended-workflow))
### Bindings

Cloudflare [Bindings](https://developers.cloudflare.com/pages/functions/bindings/) are what allows you to interact with resources available in the Cloudflare Platform.

You can use bindings during development, when previewing locally your application and of course in the deployed application:

- To use bindings in dev mode you need to define them in the `next.config.js` file under `setupDevBindings`, this mode uses the `next-dev` `@cloudflare/next-on-pages` submodule. For more details see its [documentation](https://github.com/cloudflare/next-on-pages/blob/05b6256/internal-packages/next-dev/README.md).

- To use bindings in the preview mode you need to add them to the `pages:preview` script accordingly to the `wrangler pages dev` command. For more details see its [documentation](https://developers.cloudflare.com/workers/wrangler/commands/#dev-1) or the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/).

- To use bindings in the deployed application you will need to configure them in the Cloudflare [dashboard](https://dash.cloudflare.com/). For more details see the [Pages Bindings documentation](https://developers.cloudflare.com/pages/functions/bindings/).

#### KV Example

`c3` has added for you an example showing how you can use a KV binding, in order to enable the example, search for lines containing the following comment:
```ts
// KV Example:
```

and uncomment the commented lines below it.

After doing this you can run the `dev` script and visit the `/api/hello` route to see the example in action.

To then enable such example also in preview mode add a `kv` in the `pages:preview` script like so:
```diff
- "pages:preview": "npm run pages:build && wrangler pages dev .vercel/output/static --compatibility-date=2023-12-18 --compatibility-flag=nodejs_compat",
+ "pages:preview": "npm run pages:build && wrangler pages dev .vercel/output/static --compatibility-date=2023-12-18 --compatibility-flag=nodejs_compat --kv MY_KV",
```

Finally, if you also want to see the example work in the deployed application make sure to add a `MY_KV` binding to your Pages application in its [dashboard kv bindings settings section](https://dash.cloudflare.com/?to=/:account/pages/view/:pages-project/settings/functions#kv_namespace_bindings_section). After having configured it make sure to re-deploy your application.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export const runtime = 'edge'

export async function GET(request) {
let responseText = 'Hello World'

// In the edge runtime you can use Bindings that are available in your application
// (for more details see:
// - https://developers.cloudflare.com/pages/framework-guides/deploy-a-nextjs-site/#use-bindings-in-your-nextjs-application
// - https://developers.cloudflare.com/pages/functions/bindings/
// )
//
// KV Example:
// const myKv = process.env.MY_KV
// await myKv.put('suffix', ' from a KV store!')
// const suffix = await myKv.get('suffix')
// responseText += suffix

return new Response(responseText)
}
58 changes: 58 additions & 0 deletions packages/create-cloudflare/templates/next/app/js/app/not-found.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export const runtime = "edge";

export default function NotFound() {
return (
<>
<title>404: This page could not be found.</title>
<div style={styles.error}>
<div>
<style
dangerouslySetInnerHTML={{
__html: `body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}`,
}}
/>
<h1 className="next-error-h1" style={styles.h1}>
404
</h1>
<div style={styles.desc}>
<h2 style={styles.h2}>This page could not be found.</h2>
</div>
</div>
</div>
</>
);
}

const styles = {
error: {
fontFamily:
'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
height: "100vh",
textAlign: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
},

desc: {
display: "inline-block",
},

h1: {
display: "inline-block",
margin: "0 20px 0 0",
padding: "0 23px 0 0",
fontSize: 24,
fontWeight: 500,
verticalAlign: "top",
lineHeight: "49px",
},

h2: {
fontSize: 14,
fontWeight: 400,
lineHeight: "49px",
margin: 0,
},
};
Loading

0 comments on commit ce6d4bc

Please sign in to comment.