├── .prettierignore ├── .gitignore ├── bin └── create-notion-app.js ├── .husky ├── commit-msg └── pre-commit ├── src ├── utils │ ├── to-json.ts │ ├── async-pipeline.ts │ ├── async-exec.ts │ ├── is-error-like.ts │ ├── index.ts │ ├── install-dependencies.ts │ ├── download-and-extract-repo.ts │ ├── try-init-git.ts │ ├── must-be-empty.ts │ └── format-project.ts ├── create.ts ├── index.ts └── require-invite-code.ts ├── lint-staged.config.js ├── commitlint.config.js ├── .vscode ├── extensions.json ├── tasks.json └── settings.json ├── tsconfig.json ├── .github ├── dependabot.yml └── workflows │ ├── pr-sources.yml │ ├── ci.yml │ └── pr-metadata.yml ├── cspell.json ├── CONTRIBUTING.md ├── webpack.config.js ├── README.md └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /bin/create-notion-app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require("../out/main.js"); 3 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn tsc --noEmit 5 | yarn lint-staged 6 | -------------------------------------------------------------------------------- /src/utils/to-json.ts: -------------------------------------------------------------------------------- 1 | export function toJson(obj: unknown): string { 2 | return JSON.stringify(obj, null, 2); 3 | } 4 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*": ["prettier --write --ignore-unknown", "cspell --no-must-find-files"], 3 | }; 4 | -------------------------------------------------------------------------------- /src/utils/async-pipeline.ts: -------------------------------------------------------------------------------- 1 | import stream from "stream"; 2 | import { promisify } from "util"; 3 | 4 | export const pipeline = promisify(stream.pipeline); 5 | -------------------------------------------------------------------------------- /src/utils/async-exec.ts: -------------------------------------------------------------------------------- 1 | import child_process from "child_process"; 2 | import { promisify } from "util"; 3 | 4 | export const exec = promisify(child_process.exec); 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-angular"], 3 | ignores: [(commit) => /^build\((deps|deps-dev)\): bump/.test(commit)], 4 | }; 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "amodio.tsl-problem-matcher", 4 | "esbenp.prettier-vscode", 5 | "streetsidesoftware.code-spell-checker", 6 | "vivaxy.vscode-conventional-commits" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node14", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { "~/*": ["src/*"] } 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "out", "*.config.js"] 9 | } 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: npm 5 | directory: / 6 | schedule: 7 | interval: monthly 8 | - package-ecosystem: github-actions 9 | directory: / 10 | schedule: 11 | interval: monthly 12 | -------------------------------------------------------------------------------- /src/utils/is-error-like.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorLike { 2 | message: string; 3 | } 4 | 5 | export function isErrorLike(err: unknown): err is ErrorLike { 6 | return ( 7 | typeof err === "object" && 8 | err !== null && 9 | typeof (err as { message?: unknown }).message === "string" 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePaths": ["package.json", "cspell.json", ".vscode", "out"], 3 | "words": [ 4 | "commitlint", 5 | "hgcheck", 6 | "hgignore", 7 | "intellij", 8 | "minh", 9 | "mkdocs", 10 | "nbundle", 11 | "nolookalikes", 12 | "npmignore", 13 | "phuc", 14 | "pnpm" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./to-json"; 2 | export * from "./is-error-like"; 3 | export * from "./async-pipeline"; 4 | export * from "./async-exec"; 5 | export * from "./must-be-empty"; 6 | export * from "./download-and-extract-repo"; 7 | export * from "./format-project"; 8 | export * from "./install-dependencies"; 9 | export * from "./try-init-git"; 10 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "start", 7 | "problemMatcher": "$ts-webpack-watch", 8 | "label": "npm: start", 9 | "detail": "Start building & watching in development mode", 10 | "isBackground": true, 11 | "group": { 12 | "kind": "build", 13 | "isDefault": true 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/install-dependencies.ts: -------------------------------------------------------------------------------- 1 | import ms from "ms"; 2 | 3 | import { exec } from "./async-exec"; 4 | 5 | export async function installDependencies( 6 | projectDirectory: string 7 | ): Promise { 8 | const internal = setInterval(() => { 9 | console.log( 10 | "Hang on, packages are still being installed, your connection may be a bit slow…" 11 | ); 12 | }, ms("20s")); 13 | await exec(`yarn install`, { cwd: projectDirectory }); 14 | clearInterval(internal); 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "javascript.suggestionActions.enabled": false, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.formatOnSave": true, 6 | "files.trimTrailingWhitespace": true, 7 | "files.trimFinalNewlines": true, 8 | "files.insertFinalNewline": true, 9 | "files.exclude": { 10 | "node_modules": true, 11 | "yarn.lock": true, 12 | ".husky": true 13 | }, 14 | "conventionalCommits.gitmoji": false 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/pr-sources.yml: -------------------------------------------------------------------------------- 1 | name: PR / Sources 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | validate-sources: 9 | name: Validate sources 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | cache: yarn 17 | - run: yarn install --frozen-lockfile 18 | - run: yarn tsc --noEmit 19 | - run: yarn prettier --check . 20 | - run: yarn cspell --no-must-find-files '**' 21 | - run: yarn build 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `create-notion-app` 2 | 3 | ## Requirements 4 | 5 | - Node 14+ 6 | 7 | - Yarn 1.22+ 8 | 9 | ## Setup 10 | 11 | 1. Install requirements 12 | 13 | 2. Clone the repository 14 | 15 | 3. Run `yarn` to install dependencies 16 | 17 | ## Develop 18 | 19 | - Run `yarn start` to start building & watching in development mode 20 | 21 | - Commit adhering to [Angular commit convention](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit) (use `yarn commit` or [Conventional Commits in Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=vivaxy.vscode-conventional-commits) to commit interactively) 22 | 23 | - Submit a PR and make sure required status checks pass 24 | 25 | - When a PR is merged or code is pushed to `main`, Github automatically builds & publishes a new release if there are relevant changes 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build-release: 9 | name: Build & release 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 16 16 | cache: yarn 17 | - run: yarn install --frozen-lockfile 18 | - name: Run yarn commitlint --from=head_commit.message 19 | run: echo "${{ github.event.head_commit.message }}" | yarn commitlint 20 | - run: yarn tsc --noEmit 21 | - run: yarn prettier --check . 22 | - run: yarn cspell --no-must-find-files '**' 23 | - run: yarn build 24 | - run: yarn semantic-release --branches ${{ github.ref_name }} 25 | env: 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const TsconfigPathsWebpackPlugin = require("tsconfig-paths-webpack-plugin"); 2 | 3 | const path = require("path"); 4 | const res = (...segments) => path.resolve(__dirname, ...segments); 5 | const src = (...segments) => res("src", ...segments); 6 | 7 | module.exports = (_, argv) => ({ 8 | mode: argv.mode, 9 | entry: src("index.ts"), 10 | output: { 11 | filename: "[name].js", 12 | path: res("out"), 13 | clean: true, 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.(c|m)js$/i, 19 | resolve: { fullySpecified: false }, 20 | }, 21 | { 22 | test: /\.ts?$/i, 23 | use: ["ts-loader"], 24 | }, 25 | ], 26 | }, 27 | resolve: { 28 | extensions: [".js", ".cjs", ".mjs", ".ts", ".json"], 29 | plugins: [ 30 | new TsconfigPathsWebpackPlugin({ configFile: res("tsconfig.json") }), 31 | ], 32 | }, 33 | devtool: argv.mode === "development" ? "eval" : false, 34 | target: "async-node14", 35 | externalsPresets: { node: true }, 36 | }); 37 | -------------------------------------------------------------------------------- /src/utils/download-and-extract-repo.ts: -------------------------------------------------------------------------------- 1 | import got from "got"; 2 | import tar from "tar"; 3 | import retry from "async-retry"; 4 | 5 | import { pipeline } from "./async-pipeline"; 6 | 7 | export interface Repo { 8 | username: string; 9 | name: string; 10 | branch: string; 11 | filePath?: string; 12 | } 13 | 14 | export async function downloadAndExtractRepo( 15 | destinationDirectory: string, 16 | { username, name, branch, filePath }: Repo 17 | ): Promise { 18 | return retry( 19 | () => 20 | pipeline( 21 | got.stream( 22 | `https://codeload.github.com/${username}/${name}/tar.gz/${branch}` 23 | ), 24 | tar.extract( 25 | { 26 | cwd: destinationDirectory, 27 | strip: filePath ? filePath.split("/").length + 1 : 1, 28 | }, 29 | [ 30 | `${name}-${branch.replace(/\//g, "-")}${ 31 | filePath ? `/${filePath}` : "" 32 | }`, 33 | ] 34 | ) 35 | ), 36 | { 37 | retries: 3, 38 | } 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/utils/try-init-git.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "./async-exec"; 2 | 3 | async function isInGitRepository(projectDirectory: string): Promise { 4 | try { 5 | await exec("git rev-parse --is-inside-work-tree", { 6 | cwd: projectDirectory, 7 | }); 8 | return true; 9 | } catch { 10 | return false; 11 | } 12 | } 13 | 14 | async function isInMercurialRepository( 15 | projectDirectory: string 16 | ): Promise { 17 | try { 18 | await exec("hg --cwd . root", { 19 | cwd: projectDirectory, 20 | }); 21 | return true; 22 | } catch { 23 | return false; 24 | } 25 | } 26 | 27 | export async function tryGitInit(projectDirectory: string): Promise { 28 | try { 29 | await exec("git --version", { 30 | cwd: projectDirectory, 31 | }); 32 | if ( 33 | (await isInGitRepository(projectDirectory)) || 34 | (await isInMercurialRepository(projectDirectory)) 35 | ) { 36 | return false; 37 | } 38 | await exec("git init", { 39 | cwd: projectDirectory, 40 | }); 41 | return true; 42 | } catch { 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/pr-metadata.yml: -------------------------------------------------------------------------------- 1 | name: PR / Metadata 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | types: [opened, synchronize, reopened, edited] 7 | 8 | permissions: 9 | pull-requests: write 10 | contents: write 11 | 12 | jobs: 13 | validate-metadata: 14 | name: Validate metadata 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 16 21 | cache: yarn 22 | - run: yarn install --frozen-lockfile 23 | - name: Run yarn commitlint --from=pull_request.title 24 | run: echo "${{ github.event.pull_request.title }}" | yarn commitlint 25 | auto-merge: 26 | name: Auto-merge 27 | if: ${{ github.actor == 'dependabot[bot]' }} 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: dependabot/fetch-metadata@v1.3.6 31 | id: dependabot-metadata 32 | with: 33 | github-token: "${{ secrets.GITHUB_TOKEN }}" 34 | - run: gh pr merge --auto --squash ${{ github.event.pull_request.html_url }} 35 | if: ${{ steps.dependabot-metadata.outputs.update-type != 'version-update:semver-major' }} 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /src/utils/must-be-empty.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs/promises"; 3 | import chalk from "chalk"; 4 | 5 | export async function mustBeEmpty(directory: string): Promise { 6 | const validFiles = [ 7 | ".DS_Store", 8 | ".git", 9 | ".gitattributes", 10 | ".gitignore", 11 | ".gitlab-ci.yml", 12 | ".hg", 13 | ".hgcheck", 14 | ".hgignore", 15 | ".idea", 16 | ".npmignore", 17 | ".travis.yml", 18 | "LICENSE", 19 | "Thumbs.db", 20 | "docs", 21 | "mkdocs.yml", 22 | "npm-debug.log", 23 | "yarn-debug.log", 24 | "yarn-error.log", 25 | ]; 26 | 27 | const files = await fs.readdir(directory); 28 | const conflicts = files 29 | .filter((file) => !validFiles.includes(file)) 30 | // Support IntelliJ IDEA-based editors 31 | .filter((file) => !/\.iml$/.test(file)); 32 | if (conflicts.length === 0) return; 33 | 34 | console.error( 35 | `\nThe directory ${chalk.green( 36 | directory 37 | )} contains files that might conflict:\n` 38 | ); 39 | for (const file of conflicts) { 40 | try { 41 | const stats = await fs.lstat(path.join(directory, file)); 42 | if (stats.isDirectory()) { 43 | console.error(` ${chalk.blue(file)}/`); 44 | } else { 45 | console.error(` ${file}`); 46 | } 47 | } catch { 48 | console.error(` ${file}`); 49 | } 50 | } 51 | console.error( 52 | "\nEither try using another directory or remove the files listed above.\n" 53 | ); 54 | process.exit(1); 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > This project is in early access and requires an invite code to use. 2 | > 3 | > ⤷ [Sign up for early access](https://phuctm97.gumroad.com/l/nbundle-waitlist). 4 | 5 | --- 6 | 7 | **⚠️ All `1.0.x` releases are `alpha` releases, are not stable, and may have breaking changes.** 8 | 9 | --- 10 | 11 | # Create Notion App · [![npm Version](https://img.shields.io/npm/v/create-notion-app?logo=npm)](https://www.npmjs.com/package/create-notion-app) [![CI](https://github.com/nbundle/create-notion-app/actions/workflows/ci.yml/badge.svg)](https://github.com/nbundle/create-notion-app/actions/workflows/ci.yml) 12 | 13 | Create [nbundle-powered][nbundle] [Notion] apps with one command: 14 | 15 | ```shell 16 | yarn create notion-app 17 | ``` 18 | 19 | Or for a TypeScript project: 20 | 21 | ```shell 22 | yarn create notion-app --ts 23 | ``` 24 | 25 | ## Options 26 | 27 | `create-notion-app` comes with the following options: 28 | 29 | | Option | Description | 30 | | -------------------------- | ------------------------------------------------------------------------- | 31 | | **-t, --ts, --typescript** | Initialize as a TypeScript project | 32 | | **-d, --devtool** | Use default devtools (prettier, husky, lint-staged, commitlint, & cspell) | 33 | 34 | ## Contributing 35 | 36 | See [CONTRIBUTING.md](CONTRIBUTING.md). 37 | 38 | 39 | 40 | [nbundle]: https://developers.nbundle.com 41 | [notion]: https://www.notion.so 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-notion-app", 3 | "version": "0.0.0-SNAPSHOT", 4 | "description": "Create nbundle-powered Notion apps with one command", 5 | "keywords": [ 6 | "notion", 7 | "notion-api", 8 | "notion-client", 9 | "notion-app", 10 | "notion-integration", 11 | "notion-extension", 12 | "notion-plugin", 13 | "notion-addon", 14 | "notion-automation", 15 | "nbundle", 16 | "nbundle-app", 17 | "nbundle-integration", 18 | "nbundle-extension", 19 | "nbundle-plugin", 20 | "nbundle-addon" 21 | ], 22 | "license": "UNLICENSED", 23 | "repository": "https://github.com/nbundle/create-notion-app", 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "files": [ 28 | "out", 29 | "bin" 30 | ], 31 | "main": "out/main.js", 32 | "bin": { 33 | "create-notion-app": "bin/create-notion-app.js" 34 | }, 35 | "engines": { 36 | "node": ">=14.0.0", 37 | "yarn": "^1.22.0" 38 | }, 39 | "scripts": { 40 | "prepare": "husky install", 41 | "start": "webpack --mode development --watch --color", 42 | "build": "webpack --mode production --color" 43 | }, 44 | "devDependencies": { 45 | "@commitlint/cli": "^17.0.2", 46 | "@commitlint/config-angular": "^17.0.0", 47 | "@commitlint/prompt-cli": "^17.0.0", 48 | "@tsconfig/node14": "^1.0.1", 49 | "@types/async-retry": "^1.4.4", 50 | "@types/got": "^9.6.12", 51 | "@types/ms": "^0.7.31", 52 | "@types/nanoid-dictionary": "^4.2.0", 53 | "@types/node": "^14.18.12", 54 | "@types/prompts": "^2.0.14", 55 | "@types/tar": "^6.1.1", 56 | "async-retry": "^1.3.3", 57 | "capital-case": "^1.0.4", 58 | "cspell": "^6.1.2", 59 | "email-validator": "^2.0.4", 60 | "got": "^12.1.0", 61 | "husky": "^8.0.1", 62 | "lint-staged": "^13.0.1", 63 | "ms": "^2.1.3", 64 | "nanoid": "^4.0.0", 65 | "nanoid-dictionary": "^4.3.0", 66 | "node-fetch": "^3.2.6", 67 | "open": "^8.4.0", 68 | "param-case": "^3.0.4", 69 | "prettier": "^2.6.2", 70 | "prompts": "^2.4.2", 71 | "semantic-release": "^19.0.3", 72 | "tar": "^6.1.11", 73 | "ts-loader": "^9.3.0", 74 | "tsconfig-paths-webpack-plugin": "^4.0.0", 75 | "typescript": "^4.7.3", 76 | "update-check": "^1.5.4", 77 | "webpack": "^5.73.0", 78 | "webpack-cli": "^4.9.2" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/utils/format-project.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import path from "path"; 3 | import fetch from "node-fetch"; 4 | import { customAlphabet } from "nanoid/async"; 5 | import { lowercase, numbers } from "nanoid-dictionary"; 6 | import { capitalCase } from "capital-case"; 7 | 8 | import { toJson } from "./to-json"; 9 | 10 | const nanoid = customAlphabet(lowercase + numbers, 5); 11 | 12 | async function createAppName(): Promise { 13 | const name = `nbundle-app-${await nanoid()}`; 14 | const res = await fetch(`https://developers.nbundle.com/api/v1/apps/${name}`); 15 | if (res.status === 404) return name; 16 | if (res.ok) return createAppName(); 17 | throw new Error("Couldn't generate app name."); 18 | } 19 | 20 | export interface FormatProjectOptions { 21 | devtool?: boolean; 22 | } 23 | 24 | export async function formatProject( 25 | projectDirectory: string, 26 | options?: FormatProjectOptions 27 | ): Promise { 28 | await Promise.all( 29 | [ 30 | "LICENSE", 31 | "yarn.lock", 32 | ...(options?.devtool 33 | ? [] 34 | : [ 35 | ".github", 36 | ".vscode", 37 | ".husky", 38 | "commitlint.config.js", 39 | "cspell.json", 40 | "lint-staged.config.js", 41 | ".prettierignore", 42 | ]), 43 | ].map((file) => 44 | fs.rm(path.join(projectDirectory, file), { recursive: true }) 45 | ) 46 | ); 47 | 48 | if (options?.devtool) { 49 | const cspellJsonPath = path.join(projectDirectory, "cspell.json"); 50 | const cspell = JSON.parse(await fs.readFile(cspellJsonPath, "utf8")); 51 | cspell.words = cspell.words.filter( 52 | (w: string) => !["minh", "phuc"].includes(w) 53 | ); 54 | await fs.writeFile(cspellJsonPath, toJson(cspell), "utf8"); 55 | } 56 | 57 | const name = path.basename(projectDirectory); 58 | const pkgJsonPath = path.join(projectDirectory, "package.json"); 59 | const pkg = JSON.parse(await fs.readFile(pkgJsonPath, "utf8")); 60 | pkg.name = await createAppName(); 61 | pkg.productName = capitalCase(name); 62 | if (pkg.scripts.prepare) delete pkg.scripts.prepare; 63 | if (!options?.devtool) { 64 | pkg.devDependencies = Object.fromEntries( 65 | Object.entries(pkg.devDependencies).filter(([key]) => 66 | key.startsWith("@nbundle/") 67 | ) 68 | ); 69 | } 70 | await fs.writeFile( 71 | pkgJsonPath, 72 | toJson({ 73 | name: pkg.name, 74 | version: pkg.version, 75 | productName: pkg.productName, 76 | description: pkg.description, 77 | ...pkg, 78 | }), 79 | "utf8" 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /src/create.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises"; 2 | import chalk from "chalk"; 3 | 4 | import { 5 | mustBeEmpty, 6 | downloadAndExtractRepo, 7 | formatProject, 8 | installDependencies, 9 | tryGitInit, 10 | toJson, 11 | } from "./utils"; 12 | import path from "path"; 13 | 14 | export interface CreateOptions { 15 | typescript?: boolean; 16 | devtool?: boolean; 17 | } 18 | 19 | export default async function create( 20 | projectDirectory: string, 21 | options: CreateOptions 22 | ): Promise { 23 | await fs.mkdir(projectDirectory, { recursive: true }); 24 | 25 | await mustBeEmpty(projectDirectory); 26 | 27 | const example = `example${options.typescript ? "-ts" : ""}`; 28 | 29 | console.log( 30 | `\nDownloading template ${chalk.cyan(example)}. This might take a moment.\n` 31 | ); 32 | 33 | await downloadAndExtractRepo(projectDirectory, { 34 | username: "nbundle", 35 | name: example, 36 | branch: "main", 37 | }); 38 | 39 | await formatProject(projectDirectory, options); 40 | 41 | if ((await tryGitInit(projectDirectory)) && options.devtool) { 42 | const pkgJsonPath = path.join(projectDirectory, "package.json"); 43 | const pkg = JSON.parse(await fs.readFile(pkgJsonPath, "utf8")); 44 | if (pkg.devDependencies.husky) { 45 | pkg.scripts.prepare = "husky install"; 46 | await fs.writeFile(pkgJsonPath, toJson(pkg), "utf8"); 47 | } 48 | } 49 | 50 | console.log("Installing packages. This might take up to a few minutes."); 51 | 52 | await installDependencies(projectDirectory); 53 | 54 | console.log(); 55 | 56 | const packageManager = "yarn"; 57 | const useYarn = packageManager === "yarn"; 58 | 59 | const name = path.basename(projectDirectory); 60 | let cdPath: string; 61 | if (path.join(process.cwd(), name) === projectDirectory) { 62 | cdPath = name; 63 | } else { 64 | cdPath = projectDirectory; 65 | } 66 | console.log( 67 | `${chalk.green("Success!")} Created ${chalk.blue(projectDirectory)}\n` 68 | ); 69 | console.log("Inside that directory, you can run several commands:\n"); 70 | console.log( 71 | chalk.cyan(` ${packageManager} ${useYarn ? "" : "run "}develop`) 72 | ); 73 | console.log(" Starts the development server.\n"); 74 | console.log(chalk.cyan(` ${packageManager} ${useYarn ? "" : "run "}build`)); 75 | console.log(" Builds the app for production.\n"); 76 | console.log(chalk.cyan(` ${packageManager} preview`)); 77 | console.log(" Runs the built app in production mode.\n"); 78 | console.log("We suggest that you begin by typing:\n"); 79 | console.log(chalk.cyan(" cd"), cdPath); 80 | console.log( 81 | ` ${chalk.cyan(`${packageManager} ${useYarn ? "" : "run "}develop`)}\n` 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs/promises"; 3 | import chalk from "chalk"; 4 | import updateCheck from "update-check"; 5 | import prompts from "prompts"; 6 | import { paramCase } from "param-case"; 7 | import { Command, Option } from "commander"; 8 | 9 | import requireInviteCode from "./require-invite-code"; 10 | import create from "./create"; 11 | 12 | async function getPkg() { 13 | const pkgJson = await fs.readFile( 14 | path.resolve(__dirname, "..", "package.json"), 15 | "utf8" 16 | ); 17 | return JSON.parse(pkgJson); 18 | } 19 | 20 | async function checkForUpdates(): Promise { 21 | const pkg = await getPkg(); 22 | if (pkg.version === "0.0.0-SNAPSHOT") return; 23 | const res = await updateCheck(pkg); 24 | if (res?.latest) { 25 | console.log( 26 | `\n${chalk.yellow.bold( 27 | `A new version of ${chalk.green.bold( 28 | pkg.name 29 | )} is available and required!` 30 | )}\n\nRun ${chalk.cyan( 31 | `yarn global add ${pkg.name}` 32 | )} to update then try again.\n` 33 | ); 34 | process.exit(1); 35 | } 36 | } 37 | 38 | async function run() { 39 | await requireInviteCode(); 40 | 41 | const program = new Command(); 42 | const pkg = await getPkg(); 43 | program 44 | .name(pkg.name) 45 | .description(pkg.description) 46 | .version(pkg.version, "-v, -V, --version") 47 | .arguments("[]") 48 | .usage(`[] [options]`) 49 | .option("-t, --ts, --typescript", "initialize as a TypeScript project") 50 | .addOption( 51 | new Option( 52 | "-t, --typescript", 53 | "initialize as a TypeScript project" 54 | ).hideHelp() 55 | ) 56 | .option( 57 | "-d, --devtool", 58 | "use default devtools (prettier, husky, lint-staged, commitlint, & cspell)" 59 | ) 60 | .action(async (optionalProjectDirectory, opts) => { 61 | let projectDirectory = optionalProjectDirectory; 62 | if (!projectDirectory) { 63 | const { projectName } = await prompts({ 64 | name: "projectName", 65 | type: "text", 66 | message: "What is your project name?", 67 | }); 68 | if (!projectName) { 69 | console.error(chalk.red("Project name is required.")); 70 | process.exit(1); 71 | } 72 | projectDirectory = projectName; 73 | } 74 | if (projectDirectory) { 75 | projectDirectory = paramCase(projectDirectory); 76 | } 77 | await create(path.resolve(projectDirectory), { 78 | typescript: opts.ts || opts.typescript, 79 | devtool: opts.devtool, 80 | }); 81 | }); 82 | await program.parseAsync(); 83 | } 84 | 85 | checkForUpdates() 86 | .then(run) 87 | .catch((err) => { 88 | console.error(err); 89 | process.exit(1); 90 | }); 91 | -------------------------------------------------------------------------------- /src/require-invite-code.ts: -------------------------------------------------------------------------------- 1 | import prompts from "prompts"; 2 | import chalk from "chalk"; 3 | import fetch from "node-fetch"; 4 | import open from "open"; 5 | import emailValidator from "email-validator"; 6 | 7 | export default async function requireInviteCode(): Promise { 8 | const { whatToDo } = await prompts({ 9 | name: "whatToDo", 10 | type: "select", 11 | message: 12 | "nbundle for Developers is in early access and requires an invite code.", 13 | choices: [ 14 | { 15 | title: "I don't have any invite codes yet, sign me up", 16 | value: "no-code", 17 | }, 18 | { 19 | title: "I have an invite code, continue", 20 | value: "has-code", 21 | }, 22 | ], 23 | initial: 0, 24 | }); 25 | if (!whatToDo) { 26 | console.error( 27 | chalk.red( 28 | "Invite code is required. You can sign up for early access at https://developers.nbundle.com/early-access." 29 | ) 30 | ); 31 | process.exit(1); 32 | } 33 | 34 | if (whatToDo === "no-code") { 35 | await open("https://developers.nbundle.com/early-access"); 36 | console.log( 37 | "Open https://developers.nbundle.com/early-access to sign up for early access." 38 | ); 39 | process.exit(0); 40 | } 41 | 42 | const { code } = await prompts({ 43 | name: "code", 44 | type: "text", 45 | message: "What is your invite code?", 46 | validate: (value) => { 47 | const trimmedValue = value.trim(); 48 | if (trimmedValue.length === 0) return "Please enter an invite code."; 49 | return true; 50 | }, 51 | }); 52 | if (!code) { 53 | console.error( 54 | chalk.red( 55 | "Invite code is required. You can sign up for early access at https://developers.nbundle.com/early-access." 56 | ) 57 | ); 58 | process.exit(1); 59 | } 60 | 61 | const preVerify = await fetch( 62 | "https://developers.nbundle.com/api/v1/invite-verifications", 63 | { 64 | method: "POST", 65 | headers: { 66 | "Content-Type": "application/json", 67 | }, 68 | body: JSON.stringify({ code }), 69 | } 70 | ); 71 | if (!preVerify.ok) { 72 | const json = (await preVerify.json()) as { message: string }; 73 | console.error( 74 | chalk.red( 75 | `${ 76 | preVerify.status === 404 ? "Invite code is invalid." : json.message 77 | } Please try again or sign up for early access at https://developers.nbundle.com/early-access if you haven't already.` 78 | ) 79 | ); 80 | process.exit(1); 81 | } 82 | const preVerifyJson = (await preVerify.json()) as { email: string }; 83 | 84 | const { email } = await prompts({ 85 | name: "email", 86 | type: "text", 87 | message: `What is your email address that got this invite code? (${preVerifyJson.email})`, 88 | validate: async (value): Promise => { 89 | const trimmedValue = value.trim(); 90 | if (trimmedValue.length === 0) return "Email is required."; 91 | if (!emailValidator.validate(trimmedValue)) return "Email is invalid."; 92 | return true; 93 | }, 94 | }); 95 | if (!email) { 96 | console.error( 97 | chalk.red( 98 | "Email is required to verify if you're the owner of the invite code. Please try again or sign up for early access at https://developers.nbundle.com/early-access if you haven't already." 99 | ) 100 | ); 101 | process.exit(1); 102 | } 103 | 104 | const verify = await fetch( 105 | "https://developers.nbundle.com/api/v1/invite-verifications", 106 | { 107 | method: "POST", 108 | headers: { 109 | "Content-Type": "application/json", 110 | }, 111 | body: JSON.stringify({ code, email }), 112 | } 113 | ); 114 | if (!verify.ok) { 115 | const json = (await verify.json()) as { message: string }; 116 | console.error( 117 | chalk.red( 118 | `${ 119 | verify.status === 404 ? "Invite code is invalid." : json.message 120 | } Please try again or sign up for early access at https://developers.nbundle.com/early-access if you haven't already.` 121 | ) 122 | ); 123 | process.exit(1); 124 | } 125 | } 126 | --------------------------------------------------------------------------------