Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
41 changes: 41 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "create-functionless",
"version": "0.1.0",

Choose a reason for hiding this comment

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

Any reasoning around this over 0.0.0? I don't have strong opinions

Copy link
Contributor Author

Choose a reason for hiding this comment

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

semantic release will use 1.0.x as the current version if you specify 0.0.0. There's probably a way to override that with config but using 0.1.0 as the base seemed like a reasonable trade off.

"description": "Create a Functionless CDK app.",
"files": [
"dist",
"templates/**/*"
],
"scripts": {
"dev": "ncc build ./src/index.ts -w -o dist/",
"prerelease": "rimraf ./dist/",
"release": "ncc build ./src/index.ts -o dist/ --minify --no-cache --no-source-map-register",
"prepublishOnly": "npm run release"
},
"keywords": [
"functionless"
],
"author": "Functionless",
"license": "ISC",
"devDependencies": {
"@types/cross-spawn": "^6.0.2",
"@types/mustache": "^4.2.1",
"@types/node": "^18.7.14",
"@types/prompts": "^2.0.14",
"@types/validate-npm-package-name": "^4.0.0",
"@vercel/ncc": "^0.34.0",
"aws-cdk-lib": "^2.40.0",
"chalk": "^5.0.1",
"commander": "^9.4.0",
"cross-spawn": "^7.0.3",
"functionless": "^0.22.6",
"mustache": "^4.2.0",
"prompts": "^2.4.2",
"rimraf": "^3.0.2",
"typescript": "^4.8.2",
"validate-npm-package-name": "^4.0.0"
},
"bin": {
"create-functionless": "./dist/index.js"
}
}
156 changes: 156 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
#!/usr/bin/env node
import chalk from "chalk";
import { Command } from "commander";
import spawn from "cross-spawn";
import fs from "fs";
import mustache from "mustache";
import path from "path";
import prompts from "prompts";
import validateProjectName from "validate-npm-package-name";
import packageJson from "../package.json";

const program = new Command();

program.name(packageJson.name).version(packageJson.version).parse(process.argv);

async function askProjectName() {
const defaultName = "new-project";

const answer = await prompts({
type: "text",
name: "projectName",
message: "Project name:",
initial: defaultName,
validate: (name) => {
const result = validateProjectName(name);
if (result.validForNewPackages) return true;
return `Invalid project name: ${name}`;
},
});

if (typeof answer.projectName === "string") {
return answer.projectName.trim();
}

return defaultName;
}

function failOnError(response: ReturnType<typeof spawn.sync>, message: string) {
if (response.status !== 0) {
console.error(chalk.red(message));
process.exit(1);
}
}

async function run() {
const templateName = "default";
const templateRoot = path.join(__dirname, "..", "templates", templateName);
const templateManifestPath = path.join(templateRoot, "manifest.json");
const templateManifest: string[] = JSON.parse(
await fs.promises.readFile(templateManifestPath, "utf-8")
);

const projectName = await askProjectName();

console.log();
console.log(`Creating ${chalk.green(projectName)}...`);

const root = path.resolve(projectName);

try {
await fs.promises.mkdir(root);
} catch (err: any) {
if (err.code === "EEXIST") {
console.error(`${chalk.red(`Folder already exists: ${projectName}`)}`);
}
process.exit(1);
}

const renderTemplate = async (
localPath: string,
data: Record<string, unknown>
) => {
const templateFilePath = path.join(templateRoot, localPath);
const templateContent = mustache.render(
await fs.promises.readFile(templateFilePath, "utf-8"),
data
);
// Npm won't include `.gitignore` files in a package.
// This allows you to add .template as a file ending
// and it will be removed when rendered in the end
// project.
Comment on lines +74 to +77

Choose a reason for hiding this comment

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

I don't understand what you mean by this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Take a look at https://github.com/functionless/create-functionless/pull/1/files#diff-b3dcca123bc557a15a933374361c741e8f04f515436c7aaa8f9fa89618f6c4c8

If I were to keep the name as .gitignore instead of .gitignore.template, npm would drop this file from its packaging process and rendering the template would fail due to missing a file from the manifest. As far as I can tell, this behavior cannot be overridden.

const destinationPath = path.join(root, localPath.replace(".template", ""));
await fs.promises.mkdir(path.dirname(destinationPath), {
recursive: true,
});
await fs.promises.writeFile(destinationPath, templateContent);
};

for (const path of templateManifest) {
await renderTemplate(path, {
projectName,
});
}

process.chdir(root);

console.log();
console.log("Installing packages...");
console.log();
Comment on lines +95 to +97

Choose a reason for hiding this comment

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

nit: provide a helper or use \n


const dependencies = [
"typescript",
"functionless",
"@functionless/ast-reflection",
"@functionless/language-service",
"aws-sdk",
"typesafe-dynamodb",
"aws-cdk",
"aws-cdk-lib",
"@aws-cdk/aws-appsync-alpha",
"constructs",
"esbuild",
];

failOnError(
spawn.sync("yarn", ["add", "-D", ...dependencies], {
stdio: "inherit",
}),
"Unable to install dependencies"
);

console.log();
console.log("Initializing git repository...");
console.log();

const gitErrorMessage = "Error initializing git repository.";

failOnError(
spawn.sync("git", ["init", "-q"], {
stdio: "inherit",
}),
gitErrorMessage
);

failOnError(
spawn.sync("git", ["add", "."], {
stdio: "inherit",
}),
gitErrorMessage
);

failOnError(
spawn.sync("git", ["commit", "-q", "-m", "initial commit"], {
stdio: "inherit",
}),
gitErrorMessage
);

console.log(chalk.green("Project ready!"));
console.log();
console.log(`Run ${chalk.yellow(`cd ./${projectName}`)} to get started.`);

process.exit(0);
}

run();
3 changes: 3 additions & 0 deletions templates/default/.gitignore.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.swc/
cdk.out/
node_modules/
24 changes: 24 additions & 0 deletions templates/default/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"jsc": {
"parser": {
"syntax": "typescript",
"dynamicImport": false,
"decorators": false,
"hidden": {
"jest": true
}
},
"transform": null,
"target": "es2021",
"loose": false,
"externalHelpers": false,
"experimental": {
"plugins": [["@functionless/ast-reflection", {}]]
}
},
"minify": false,
"sourceMaps": "inline",
"module": {
"type": "commonjs"
}
}
3 changes: 3 additions & 0 deletions templates/default/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"app": "node -r '@swc/register' ./src/app.ts"
}
8 changes: 8 additions & 0 deletions templates/default/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
"package.json",
"cdk.json",
"tsconfig.json",
".swcrc",
".gitignore.template",
"src/app.ts"
]
10 changes: 10 additions & 0 deletions templates/default/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "{{projectName}}",
"version": "0.1.0",
"private": true,
"scripts": {
"deploy": "cdk deploy",
"synth": "cdk synth"
},
"devDependencies": {}
}
18 changes: 18 additions & 0 deletions templates/default/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { App, Stack } from "aws-cdk-lib";
import { Function, StepFunction } from "functionless";

const app = new App();

const stack = new Stack(app, "MyStack");

const sayFunction = new Function(
stack,
"SayFunction",
async (event: { message: string }) => {
console.log(event.message);
}
);

new StepFunction(stack, "Workflow", async (event: { name: string }) => {
sayFunction({ message: `Hello ${event.name}` });
});
15 changes: 15 additions & 0 deletions templates/default/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {

Choose a reason for hiding this comment

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

Is this all we want?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure. I used the defaults tsc --init generates plus the extra config for the language server. It's easy enough to adjust. Most users are going to tweak their ts-config anyway. I tend to prefer minimal defaults as a consumer.

"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"plugins": [
{
"name": "@functionless/language-service"
}
]
}
}
10 changes: 10 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"target": "es2019",
"moduleResolution": "node",
"resolveJsonModule": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
}
}
Loading