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

[C3] Improve type support in Qwik template #5052

Merged
merged 14 commits into from
Feb 21, 2024
Merged
7 changes: 7 additions & 0 deletions .changeset/two-ants-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"create-cloudflare": patch
---

feature: Add script to Qwik template for building Env type definitions.

When creating a project with the Qwik template, the `QwikCityPlatform` type will be updated to contain a definition for the `env` property. These types can be re-generated with a newly added `build-cf-types` script.
3 changes: 3 additions & 0 deletions packages/create-cloudflare/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ templates/**/pnpm-lock.yaml

# the build step renames .gitignore files to __dot__gitignore
templates/**/__dot__gitignore

scripts/snippets/**
!scripts/snippets/.gitkeep
jculvey marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions packages/create-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
],
"scripts": {
"build": "node -r esbuild-register scripts/build.ts",
"dev:codemod": "node -r esbuild-register scripts/codemod.ts",
"check:lint": "eslint .",
"check:type": "tsc",
"lint": "eslint",
Expand Down
62 changes: 62 additions & 0 deletions packages/create-cloudflare/scripts/codemod.ts
jculvey marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { join } from "path";
import { parseFile, parseTs } from "helpers/codemod";
import { writeFile } from "helpers/files";
import * as recast from "recast";

/**
* Writing code-mods often requires some trial and error. Since they are often
* applied later on in a c3 run, manual testing can become a hassle. This script was meant
* to help test and develop transforms in isolation without having to write a throw-away script.
*
* Replace your codemod below and run the script with `pnpm run dev:codemod`.
*
* Test files can be kept in the `./snippets` directory, where you will also find the output from
* the last run.
*
*/

/**
* This function mocks the `transformFile` API but outputs it to the console and writes it
* to a dedicated output file for easier testing.
*/
export const testTransform = (
filePath: string,
methods: recast.types.Visitor
) => {
const ast = parseFile(join(__dirname, filePath));

if (ast) {
recast.visit(ast, methods);
const code = recast.print(ast).code;
console.log(code);
writeFile(join(__dirname, "snippets", "output"), code);
}
};

// Use this function to experiment with a codemod in isolation
const testCodemod = () => {
// const b = recast.types.builders;
// const snippets = loadSnippets(join(__dirname, "snippets"));

testTransform("snippets/test.ts", {
visitIdentifier(n) {
n.node.name = "Potato";

return false;
},
});
};
testCodemod();

// This function can be used to inspect the AST of a particular snippet
const printSnippet = () => {
const snippet = `
if(true) {
console.log("potato");
}
`;

const program = parseTs(snippet).program;
console.log(program.body[0].consequent);
};
// printSnippet();
Empty file.
41 changes: 39 additions & 2 deletions packages/create-cloudflare/src/helpers/codemod.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import path from "path";
import { existsSync, lstatSync, readdirSync } from "fs";
import path, { extname, join } from "path";
import { crash } from "@cloudflare/cli";
import * as recast from "recast";
import * as esprimaParser from "recast/parsers/esprima";
import * as typescriptParser from "recast/parsers/typescript";
import { getTemplatePath } from "../templates";
import { readFile, writeFile } from "./files";
import type { Program } from "esprima";
import type { C3Context } from "types";

/*
CODEMOD TIPS & TRICKS
Expand Down Expand Up @@ -55,7 +58,7 @@ export const parseFile = (filePath: string) => {
const fileContents = readFile(path.resolve(filePath));

if (fileContents) {
return recast.parse(fileContents, { parser }) as Program;
return recast.parse(fileContents, { parser }).program as Program;
}
jculvey marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
crash(`Error parsing file: ${filePath}`);
Expand All @@ -76,3 +79,37 @@ export const transformFile = (
writeFile(filePath, recast.print(ast).code);
}
};

export const loadSnippets = (parentFolder: string) => {
const snippetsPath = join(parentFolder, "snippets");

if (!existsSync(snippetsPath)) {
return {};
}

if (!lstatSync(snippetsPath).isDirectory) {
return {};
}

const files = readdirSync(snippetsPath);

return (
files
// don't try loading directories
.filter((fileName) => lstatSync(join(snippetsPath, fileName)).isFile)
// only load js or ts files
.filter((fileName) => [".js", ".ts"].includes(extname(fileName)))
.reduce((acc, snippetPath) => {
const [file, ext] = snippetPath.split(".");
const key = `${file}${ext === "js" ? "Js" : "Ts"}`;
return {
...acc,
[key]: parseFile(join(snippetsPath, snippetPath))?.body,
};
}, {}) as Record<string, recast.types.ASTNode[]>
);
};
Comment on lines +96 to +110
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT - personally I find use of filters and reduce actually makes the code less readable and in fact less performant due to the recreation of arrays and objects on each iteration.

How about:

	const result: Record<string, Program["body"]|undefined> = {}
	for(const file of files) {
		if (!lstatSync(join(snippetsPath, file)).isFile) {
		  continue;
		}
		const ext = extname(file);
		if (!['.js', '.ts'].includes(ext)) {
			continue;
		}
		const baseName = basename(ext);
		const key = `${baseName}${ext === "js" ? "Js" : "Ts"}`;
		result[key] = parseFile(join(snippetsPath, file))?.body;
	}

	return result;


export const loadTemplateSnippets = (ctx: C3Context) => {
return loadSnippets(getTemplatePath(ctx));
};
1 change: 1 addition & 0 deletions packages/create-cloudflare/templates/nuxt/c3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const config: TemplateConfig = {
scripts: {
deploy: `${npm} run build && wrangler pages deploy ./dist`,
preview: `${npm} run build && wrangler pages dev ./dist`,
"build-cf-types": `wrangler types`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Feels a bit incongruous to this PR?

},
}),
};
Expand Down
71 changes: 49 additions & 22 deletions packages/create-cloudflare/templates/qwik/c3.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { endSection } from "@cloudflare/cli";
import { brandColor } from "@cloudflare/cli/colors";
import { spinner } from "@cloudflare/cli/interactive";
import { parseTs, transformFile } from "helpers/codemod";
import { loadTemplateSnippets, parseTs, transformFile } from "helpers/codemod";
import { runCommand, runFrameworkGenerator } from "helpers/command";
import { usesTypescript } from "helpers/files";
import { detectPackageManager } from "helpers/packages";
import * as recast from "recast";
import { quoteShellArgs } from "../../src/common";
import type { TemplateConfig } from "../../src/templates";
import type * as recast from "recast";
import type { C3Context } from "types";

const { npm, npx } = detectPackageManager();
Expand All @@ -23,6 +23,7 @@ const configure = async (ctx: C3Context) => {
await runCommand(cmd);

addBindingsProxy(ctx);
populateCloudflareEnv();
};

const addBindingsProxy = (ctx: C3Context) => {
Expand All @@ -35,43 +36,67 @@ const addBindingsProxy = (ctx: C3Context) => {
const s = spinner();
s.start("Updating `vite.config.ts`");

// Insert the env declaration after the last import (but before the rest of the body)
const envDeclaration = `
let env = {};

if(process.env.NODE_ENV === 'development') {
const { getPlatformProxy } = await import('wrangler');
const platformProxy = await getPlatformProxy();
env = platformProxy.env;
}
`;
const snippets = loadTemplateSnippets(ctx);

transformFile("vite.config.ts", {
// Insert the env declaration after the last import (but before the rest of the body)
visitProgram: function (n) {
const lastImportIndex = n.node.body.findLastIndex(
(t) => t.type === "ImportDeclaration"
);
n.get("body").insertAt(lastImportIndex + 1, envDeclaration);
const lastImport = n.get("body", lastImportIndex);
lastImport.insertAfter(...snippets.getPlatformProxyTs);

return this.traverse(n);
},
// Pass the `platform` object from the declaration to the `qwikCity` plugin
visitCallExpression: function (n) {
const callee = n.node.callee as recast.types.namedTypes.Identifier;
if (callee.name !== "qwikCity") {
return this.traverse(n);
}

n.node.arguments = [parseTs("{ platform }")];
jculvey marked this conversation as resolved.
Show resolved Hide resolved

return false;
},
});

// Populate the `qwikCity` plugin with the platform object containing the `env` defined above.
const platformObject = parseTs(`{ platform: { env } }`);
s.stop(`${brandColor("updated")} \`vite.config.ts\``);
};

transformFile("vite.config.ts", {
visitCallExpression: function (n) {
const callee = n.node.callee as recast.types.namedTypes.Identifier;
if (callee.name === "qwikCity") {
n.node.arguments = [platformObject];
const populateCloudflareEnv = () => {
const entrypointPath = "src/entry.cloudflare-pages.tsx";

const s = spinner();
s.start(`Updating \`${entrypointPath}\``);

transformFile(entrypointPath, {
visitTSInterfaceDeclaration: function (n) {
const b = recast.types.builders;
const id = n.node.id as recast.types.namedTypes.Identifier;
if (id.name !== "QwikCityPlatform") {
this.traverse(n);
}

this.traverse(n);
const newBody = [
["env", "Env"],
// Qwik doesn't supply `cf` to the platform object. Should they do so, uncomment this
// ["cf", "CfProperties"],
petebacondarwin marked this conversation as resolved.
Show resolved Hide resolved
].map(([varName, type]) =>
b.tsPropertySignature(
b.identifier(varName),
b.tsTypeAnnotation(b.tsTypeReference(b.identifier(type)))
)
);

n.node.body.body = newBody;

return false;
},
});

s.stop(`${brandColor("updated")} \`vite.config.ts\``);
s.stop(`${brandColor("updated")} \`${entrypointPath}\``);
};

const config: TemplateConfig = {
Expand All @@ -89,6 +114,8 @@ const config: TemplateConfig = {
transformPackageJson: async () => ({
scripts: {
deploy: `${npm} run build && wrangler pages deploy ./dist`,
preview: `${npm} run build && wrangler pages dev ./dist`,
"build-cf-types": `wrangler types`,
},
}),
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
let platform = {};

if(process.env.NODE_ENV === 'development') {
const { getPlatformProxy } = await import('wrangler');
platform = await getPlatformProxy();
}
jculvey marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Generated by Wrangler on Fri Feb 16 2024 15:52:18 GMT-0600 (Central Standard Time)
// After adding bindings to `wrangler.toml`, regenerate this interface via `npm build-cf-types`
interface Env {}
1 change: 1 addition & 0 deletions packages/create-cloudflare/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"exclude": [
"node_modules",
"dist",
"scripts/snippets/*",
"e2e-tests/fixtures/*",
// exclude all template files other than the top level ones so
// that we can catch `c3.ts`. For example, any top level files in
Expand Down
Loading