Skip to content

Commit 2fc7c9b

Browse files
tvanhensTyler van Hensbergen
and
Tyler van Hensbergen
authored
feat: initial implementation (#1)
* initial commit * ensure shell commands exit on error * use yarn instead of npm * enforce that yarn is used for now * add release config * feat: test release * fix: remove version to let semantic release control * feat: add pipeline config * fix: inject npmrc when publishing * fix: address pr comments * fix: inject github token * fix: add await to example app * fix: change env var name * fix: remove .npmrc step * address pr concerns * fix: use esbuild instead of ncc, support for npm & pnpm, test yarn * fix: github actions don't support temp directory * fix: shebang * fix: add stub git config * feat: add validate command to template * chore: add test for npm * make test script cleanup directory independent Co-authored-by: Tyler van Hensbergen <tyler@functionless.org>
1 parent a2ec79d commit 2fc7c9b

17 files changed

+4642
-0
lines changed

.github/workflows/check.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
on: push
2+
jobs:
3+
check-yarn:
4+
runs-on: ubuntu-latest
5+
steps:
6+
- uses: actions/checkout@v3
7+
- uses: actions/setup-node@v3
8+
with:
9+
node-version: "16.x"
10+
registry-url: "https://registry.npmjs.org"
11+
- run: yarn install
12+
- run: |
13+
git config --global user.email "test@test.com"
14+
git config --global user.name "CI Test"
15+
- run: yarn test:yarn
16+
check-npm:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v3
20+
- uses: actions/setup-node@v3
21+
with:
22+
node-version: "16.x"
23+
registry-url: "https://registry.npmjs.org"
24+
- run: yarn install
25+
- run: |
26+
git config --global user.email "test@test.com"
27+
git config --global user.name "CI Test"
28+
- run: yarn test:npm

.github/workflows/release.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
on:
2+
push:
3+
branches:
4+
- main
5+
- dev
6+
jobs:
7+
release:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v3
11+
- uses: actions/setup-node@v3
12+
with:
13+
node-version: "16.x"
14+
registry-url: "https://registry.npmjs.org"
15+
- run: yarn install
16+
- run: npx semantic-release
17+
env:
18+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
19+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
dist/

bin/test-npm.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env bash
2+
3+
set -ue pipefail
4+
5+
ORIGINAL_DIRECTORY="$(pwd)"
6+
PACKAGE_NAME="create-functionless"
7+
PACKED_NAME="${PACKAGE_NAME}.tgz"
8+
TEST_PROJECT="test-project"
9+
10+
function cleanup() {
11+
cd $ORIGINAL_DIRECTORY
12+
yarn global remove create-functionless || true
13+
rm -fr ${PACKED_NAME} || true
14+
rm -fr ${TEST_PROJECT} || true
15+
}
16+
17+
trap cleanup EXIT
18+
19+
npm run release
20+
npm pack
21+
mv ${PACKAGE_NAME}*.tgz ${PACKED_NAME}
22+
npm i -g ${PACKED_NAME}
23+
24+
# Use the create script to create a new project
25+
create-functionless ${TEST_PROJECT}
26+
cd ${TEST_PROJECT}
27+
28+
# Verify new project can synth
29+
npm run synth

bin/test-yarn.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env bash
2+
3+
set -ue pipefail
4+
5+
ORIGINAL_DIRECTORY="$(pwd)"
6+
PACKAGE_NAME="create-functionless"
7+
PACKED_NAME="${PACKAGE_NAME}.tgz"
8+
TEST_PROJECT="test-project"
9+
10+
function cleanup() {
11+
cd $ORIGINAL_DIRECTORY
12+
yarn global remove create-functionless || true
13+
rm -fr ${PACKED_NAME} || true
14+
rm -fr ${TEST_PROJECT} || true
15+
}
16+
17+
trap cleanup EXIT
18+
19+
# Clean up installs of create-functionless if they exist
20+
yarn cache clean
21+
22+
yarn run release
23+
yarn pack -f ${PACKED_NAME}
24+
yarn global add --skip-integrity-check --offline "file:$(pwd)/${PACKED_NAME}"
25+
26+
# Use the create script to create a new project
27+
yarn create --offline functionless ${TEST_PROJECT}
28+
cd ${TEST_PROJECT}
29+
30+
# Verify new project can synth
31+
yarn synth

package.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "create-functionless",
3+
"version": "0.1.0",
4+
"description": "Create a Functionless CDK app.",
5+
"files": [
6+
"dist",
7+
"templates/**/*"
8+
],
9+
"bin": {
10+
"create-functionless": "./dist/index.js"
11+
},
12+
"scripts": {
13+
"test:npm": "./bin/test-npm.sh",
14+
"test:yarn": "./bin/test-yarn.sh",
15+
"prerelease": "rimraf ./dist/",
16+
"release": "esbuild --bundle src/index.ts --outfile=dist/index.js --platform=node",
17+
"prepublishOnly": "npm run release"
18+
},
19+
"keywords": [
20+
"functionless"
21+
],
22+
"author": "Functionless",
23+
"license": "Apache-2.0",
24+
"devDependencies": {
25+
"@types/cross-spawn": "^6.0.2",
26+
"@types/mustache": "^4.2.1",
27+
"@types/node": "^18.7.14",
28+
"@types/prompts": "^2.0.14",
29+
"@types/validate-npm-package-name": "^4.0.0",
30+
"aws-cdk-lib": "^2.40.0",
31+
"chalk": "^5.0.1",
32+
"commander": "^9.4.0",
33+
"cross-spawn": "^7.0.3",
34+
"esbuild": "^0.15.6",
35+
"functionless": "^0.22.6",
36+
"mustache": "^4.2.0",
37+
"prompts": "^2.4.2",
38+
"rimraf": "^3.0.2",
39+
"semantic-release": "^19.0.5",
40+
"typescript": "^4.8.2",
41+
"validate-npm-package-name": "^4.0.0"
42+
},
43+
"release": {
44+
"branches": [
45+
"main",
46+
{
47+
"name": "dev",
48+
"prerelease": true
49+
}
50+
]
51+
}
52+
}

src/index.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
#!/usr/bin/env node
2+
import chalk from "chalk";
3+
import { Command } from "commander";
4+
import spawn from "cross-spawn";
5+
import fs from "fs";
6+
import mustache from "mustache";
7+
import path from "path";
8+
9+
import packageJson from "../package.json";
10+
import {
11+
askProjectName,
12+
failOnError,
13+
getPackageManager,
14+
installPackages,
15+
} from "./util";
16+
17+
const program = new Command();
18+
19+
let projectName: string | undefined;
20+
21+
program
22+
.name(packageJson.name)
23+
.version(packageJson.version)
24+
.arguments("[projectName]")
25+
.action((name) => {
26+
if (typeof name === "string") {
27+
projectName = name;
28+
}
29+
})
30+
.parse(process.argv);
31+
32+
run().catch((err) => {
33+
console.error(err);
34+
process.exit(1);
35+
});
36+
37+
async function run() {
38+
const packageMangager = getPackageManager();
39+
40+
const templateName = "default";
41+
const templateRoot = path.join(__dirname, "..", "templates", templateName);
42+
const templateManifestPath = path.join(templateRoot, "manifest.json");
43+
const templateManifest: string[] = JSON.parse(
44+
await fs.promises.readFile(templateManifestPath, "utf-8")
45+
);
46+
47+
if (!projectName) {
48+
projectName = await askProjectName();
49+
}
50+
51+
console.log();
52+
console.log(`Creating ${chalk.green(projectName)}...`);
53+
54+
const root = path.resolve(projectName);
55+
56+
try {
57+
await fs.promises.mkdir(root);
58+
} catch (err: any) {
59+
if (err.code === "EEXIST") {
60+
console.error(`${chalk.red(`Folder already exists: ${projectName}`)}`);
61+
}
62+
process.exit(1);
63+
}
64+
65+
const renderTemplate = async (
66+
localPath: string,
67+
data: Record<string, unknown>
68+
) => {
69+
const templateFilePath = path.join(templateRoot, localPath);
70+
const templateContent = mustache.render(
71+
await fs.promises.readFile(templateFilePath, "utf-8"),
72+
data
73+
);
74+
// Npm won't include `.gitignore` files in a package.
75+
// This allows you to add .template as a file ending
76+
// and it will be removed when rendered in the end
77+
// project.
78+
const destinationPath = path.join(root, localPath.replace(".template", ""));
79+
await fs.promises.mkdir(path.dirname(destinationPath), {
80+
recursive: true,
81+
});
82+
await fs.promises.writeFile(destinationPath, templateContent);
83+
};
84+
85+
const templateData = {
86+
projectName,
87+
};
88+
89+
await Promise.all(
90+
templateManifest.map((path) => renderTemplate(path, templateData))
91+
);
92+
93+
process.chdir(root);
94+
95+
console.log();
96+
console.log("Installing packages...");
97+
console.log();
98+
99+
const dependencies = [
100+
"@aws-cdk/aws-appsync-alpha",
101+
"@functionless/ast-reflection",
102+
"@functionless/language-service",
103+
"aws-cdk",
104+
"aws-cdk-lib",
105+
"aws-sdk",
106+
"constructs",
107+
"esbuild",
108+
"functionless",
109+
"typesafe-dynamodb",
110+
"typescript",
111+
];
112+
113+
installPackages(packageMangager, dependencies);
114+
115+
console.log();
116+
console.log("Initializing git repository...");
117+
console.log();
118+
119+
const gitErrorMessage = "Error initializing git repository.";
120+
121+
failOnError(
122+
spawn.sync("git", ["init", "-q"], {
123+
stdio: "inherit",
124+
}),
125+
gitErrorMessage
126+
);
127+
128+
failOnError(
129+
spawn.sync("git", ["add", "."], {
130+
stdio: "inherit",
131+
}),
132+
gitErrorMessage
133+
);
134+
135+
failOnError(
136+
spawn.sync("git", ["commit", "-q", "-m", "initial commit"], {
137+
stdio: "inherit",
138+
}),
139+
gitErrorMessage
140+
);
141+
142+
console.log(chalk.green("Project ready!"));
143+
console.log();
144+
console.log(`Run ${chalk.yellow(`cd ./${projectName}`)} to get started.`);
145+
146+
process.exit(0);
147+
}

src/util.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import chalk from "chalk";
2+
import spawn from "cross-spawn";
3+
import prompts from "prompts";
4+
import validateProjectName from "validate-npm-package-name";
5+
6+
export function failOnError(
7+
response: ReturnType<typeof spawn.sync>,
8+
message: string
9+
) {
10+
if (response.status !== 0) {
11+
console.error(chalk.red(message));
12+
process.exit(1);
13+
}
14+
}
15+
16+
export async function askProjectName() {
17+
const defaultName = "new-project";
18+
19+
const answer = await prompts({
20+
type: "text",
21+
name: "projectName",
22+
message: "Project name:",
23+
initial: defaultName,
24+
validate: (name) => {
25+
const result = validateProjectName(name);
26+
if (result.validForNewPackages) {
27+
return true;
28+
}
29+
return `Invalid project name: ${name}`;
30+
},
31+
});
32+
33+
if (typeof answer.projectName === "string") {
34+
return answer.projectName.trim();
35+
}
36+
37+
return defaultName;
38+
}
39+
40+
export type PackageManager = "yarn" | "pnpm" | "npm";
41+
42+
export function getPackageManager(): PackageManager {
43+
const packageManager = process.env.npm_config_user_agent;
44+
45+
if (packageManager?.startsWith("yarn")) {
46+
return "yarn";
47+
} else if (packageManager?.startsWith("pnpm")) {
48+
return "pnpm";
49+
} else {
50+
return "npm";
51+
}
52+
}
53+
54+
export function installPackages(
55+
manager: PackageManager,
56+
dependencies: string[]
57+
) {
58+
let executable = "npm";
59+
let command = "install";
60+
61+
if (manager === "yarn") {
62+
executable = "yarn";
63+
command = "add";
64+
}
65+
66+
if (manager == "pnpm") {
67+
executable = "pnpm";
68+
}
69+
70+
failOnError(
71+
spawn.sync(executable, [command, "-D", ...dependencies], {
72+
stdio: "inherit",
73+
}),
74+
"Unable to install dependencies"
75+
);
76+
}

templates/default/.gitignore.template

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.swc/
2+
cdk.out/
3+
node_modules/

0 commit comments

Comments
 (0)