Skip to content

feat: initial implementation #1

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

Merged
merged 22 commits into from
Sep 2, 2022
Merged
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
28 changes: 28 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
on: push
jobs:
check-yarn:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16.x"
registry-url: "https://registry.npmjs.org"
- run: yarn install
- run: |
git config --global user.email "test@test.com"
git config --global user.name "CI Test"
- run: yarn test:yarn
check-npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16.x"
registry-url: "https://registry.npmjs.org"
- run: yarn install
- run: |
git config --global user.email "test@test.com"
git config --global user.name "CI Test"
- run: yarn test:npm
19 changes: 19 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
on:
push:
branches:
- main
- dev
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16.x"
registry-url: "https://registry.npmjs.org"
- run: yarn install
- run: npx semantic-release
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
29 changes: 29 additions & 0 deletions bin/test-npm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/usr/bin/env bash

set -ue pipefail

ORIGINAL_DIRECTORY="$(pwd)"
PACKAGE_NAME="create-functionless"
PACKED_NAME="${PACKAGE_NAME}.tgz"
TEST_PROJECT="test-project"

function cleanup() {
cd $ORIGINAL_DIRECTORY
yarn global remove create-functionless || true
rm -fr ${PACKED_NAME} || true
rm -fr ${TEST_PROJECT} || true
}

trap cleanup EXIT

npm run release
npm pack
mv ${PACKAGE_NAME}*.tgz ${PACKED_NAME}
npm i -g ${PACKED_NAME}
Comment on lines +21 to +22

Choose a reason for hiding this comment

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

Why don't you install directly from the tgz?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The cleanup hook needs a consistent name. pack names the file with the version so it isn't stable.


# Use the create script to create a new project
create-functionless ${TEST_PROJECT}
cd ${TEST_PROJECT}

# Verify new project can synth
npm run synth
31 changes: 31 additions & 0 deletions bin/test-yarn.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash

set -ue pipefail

ORIGINAL_DIRECTORY="$(pwd)"
PACKAGE_NAME="create-functionless"
PACKED_NAME="${PACKAGE_NAME}.tgz"
TEST_PROJECT="test-project"

function cleanup() {
cd $ORIGINAL_DIRECTORY
yarn global remove create-functionless || true
rm -fr ${PACKED_NAME} || true
rm -fr ${TEST_PROJECT} || true
}

trap cleanup EXIT

# Clean up installs of create-functionless if they exist
yarn cache clean

yarn run release
yarn pack -f ${PACKED_NAME}
yarn global add --skip-integrity-check --offline "file:$(pwd)/${PACKED_NAME}"

# Use the create script to create a new project
yarn create --offline functionless ${TEST_PROJECT}
cd ${TEST_PROJECT}

# Verify new project can synth
yarn synth
52 changes: 52 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"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/**/*"
],
"bin": {
"create-functionless": "./dist/index.js"
},
"scripts": {
"test:npm": "./bin/test-npm.sh",
"test:yarn": "./bin/test-yarn.sh",
"prerelease": "rimraf ./dist/",
"release": "esbuild --bundle src/index.ts --outfile=dist/index.js --platform=node",
"prepublishOnly": "npm run release"
},
"keywords": [
"functionless"
],
"author": "Functionless",
"license": "Apache-2.0",
"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",
"aws-cdk-lib": "^2.40.0",
"chalk": "^5.0.1",
"commander": "^9.4.0",
"cross-spawn": "^7.0.3",
"esbuild": "^0.15.6",
"functionless": "^0.22.6",
"mustache": "^4.2.0",
"prompts": "^2.4.2",
"rimraf": "^3.0.2",
"semantic-release": "^19.0.5",
"typescript": "^4.8.2",
"validate-npm-package-name": "^4.0.0"
},
"release": {
"branches": [
"main",
{
"name": "dev",
"prerelease": true
}
]
}
}
147 changes: 147 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/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 packageJson from "../package.json";
import {
askProjectName,
failOnError,
getPackageManager,
installPackages,
} from "./util";

const program = new Command();

let projectName: string | undefined;

program
.name(packageJson.name)
.version(packageJson.version)
.arguments("[projectName]")
.action((name) => {
if (typeof name === "string") {
projectName = name;
}
})
.parse(process.argv);

run().catch((err) => {
console.error(err);
process.exit(1);
});

async function run() {
const packageMangager = getPackageManager();

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")
);

if (!projectName) {
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);
};

const templateData = {
projectName,
};

await Promise.all(
templateManifest.map((path) => renderTemplate(path, templateData))
);

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 = [
"@aws-cdk/aws-appsync-alpha",
"@functionless/ast-reflection",
"@functionless/language-service",
"aws-cdk",
"aws-cdk-lib",
"aws-sdk",
"constructs",
"esbuild",
"functionless",
"typesafe-dynamodb",
"typescript",
];

installPackages(packageMangager, 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);
}
76 changes: 76 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import chalk from "chalk";
import spawn from "cross-spawn";
import prompts from "prompts";
import validateProjectName from "validate-npm-package-name";

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

export 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;
}

export type PackageManager = "yarn" | "pnpm" | "npm";

export function getPackageManager(): PackageManager {
const packageManager = process.env.npm_config_user_agent;

if (packageManager?.startsWith("yarn")) {
return "yarn";
} else if (packageManager?.startsWith("pnpm")) {
return "pnpm";
} else {
return "npm";
}
}

export function installPackages(
manager: PackageManager,
dependencies: string[]
) {
let executable = "npm";
let command = "install";

if (manager === "yarn") {
executable = "yarn";
command = "add";
}

if (manager == "pnpm") {
executable = "pnpm";
}

failOnError(
spawn.sync(executable, [command, "-D", ...dependencies], {
stdio: "inherit",
}),
"Unable to install dependencies"
);
}
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/
Loading