Skip to content
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ templates/**/package-lock.json
/playwright-report/
/blob-report/
/playwright/.cache/

# We generate this on publish
boilerplate
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const compat = new FlatCompat({

export default [
{
ignores: ['**/dist', '**/templates']
ignores: ['dist', 'templates', 'boilerplate']
},
{
...love,
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"files": [
"dist/index.js",
"README.md",
"LICENSE"
"LICENSE",
"boilerplate"
],
"scripts": {
"format": "prettier . --write",
Expand All @@ -22,7 +23,9 @@
"e2e": "NODE_ENV=development playwright test",
"e2e:snapshots": "NODE_ENV=development playwright test --update-snapshots --reporter=list",
"e2e:ci": "playwright test --reporter=html",
"e2e:ci:snapshots": "playwright test --update-snapshots --reporter=html"
"e2e:ci:snapshots": "playwright test --update-snapshots --reporter=html",
"copy:boilerplate": "./scripts/cli-to-boilerplate.sh",
"prepublishOnly": "npm run copy:boilerplate"
},
"repository": {
"type": "git",
Expand Down
19 changes: 19 additions & 0 deletions scripts/cli-to-boilerplate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash

set -e

NPM_ROOT=$(npm root -g)
SRC="$NPM_ROOT/@junobuild/cli/templates/eject"

DEST="./boilerplate"
DEST_FUNCTIONS="$DEST/functions"

if [ -d "$DEST" ]; then
rm -rf "$DEST"
fi

mkdir -p "$DEST_FUNCTIONS"

cp -r "$SRC/"* "$DEST_FUNCTIONS/"

echo "✅ Boilerplate copied to $DEST"
2 changes: 2 additions & 0 deletions src/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const NODE_18 = 18;
export const JUNO_CDN_URL = 'https://cdn.juno.build';
export const CLI_PACKAGE = '@junobuild/cli';

export const BOILERPLATE_PATH = '../boilerplate';
37 changes: 33 additions & 4 deletions src/services/generate.services.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import {nonNullish} from '@dfinity/utils';
import {downloadFromURL, gunzipFile} from '@junobuild/cli-tools';
import {readFile} from 'fs/promises';
import {red} from 'kleur';
import {writeFile} from 'node:fs/promises';
import {basename, join, parse} from 'node:path';
import ora from 'ora';
import {JUNO_CDN_URL} from '../constants/constants';
import {BOILERPLATE_PATH, JUNO_CDN_URL} from '../constants/constants';
import {GITHUB_ACTION_DEPLOY} from '../templates/github-actions';
import type {PopulateInput} from '../types/generator';
import type {PopulateInput, ServerlessFunctions} from '../types/generator';
import {untarFile, type UntarOutputFile} from '../utils/compress.utils';
import {
copyFiles,
createParentFolders,
getLocalTemplatePath,
getRelativeTemplatePath
} from '../utils/fs.utils';
import {createDirectory, getLocalFiles, type LocalFileDescriptor} from '../utils/populate.utils';

export const generate = async ({gitHubAction, ...rest}: PopulateInput) => {
export const generate = async ({gitHubAction, serverlessFunctions, ...rest}: PopulateInput) => {
const spinner = ora(`Creating project...`).start();

try {
Expand All @@ -25,6 +27,10 @@ export const generate = async ({gitHubAction, ...rest}: PopulateInput) => {

await populateFn(rest);

if (nonNullish(serverlessFunctions)) {
await populateServerlessFunctions({serverlessFunctions, ...rest});
}

if (gitHubAction) {
await populateGitHubAction(rest);
}
Expand All @@ -35,7 +41,7 @@ export const generate = async ({gitHubAction, ...rest}: PopulateInput) => {
}
};

type PopulateInputFn = Omit<PopulateInput, 'gitHubAction'>;
type PopulateInputFn = Omit<PopulateInput, 'gitHubAction' | 'serverlessFunctions'>;

const populateFromCDN = async ({where, template, localDevelopment}: PopulateInputFn) => {
// Windows uses backslash, we cannot build the URL path without replacing those with slash.
Expand Down Expand Up @@ -139,3 +145,26 @@ const updatePackageJson = async ({where, template}: PopulateInputFn) => {

await writeFile(pkgJson, result, 'utf8');
};

const populateServerlessFunctions = async ({
where,
serverlessFunctions
}: PopulateInputFn & {serverlessFunctions: ServerlessFunctions}) => {
const serverlessFunctionsSrc =
serverlessFunctions === 'ts'
? 'typescript'
: serverlessFunctions === 'js'
? 'javascript'
: serverlessFunctions;

const source = join(BOILERPLATE_PATH, 'functions', serverlessFunctionsSrc);

const target =
serverlessFunctions === 'rust' ? join(where ?? '') : join(where ?? '', 'src', 'satellite');

if (serverlessFunctions !== 'rust') {
createParentFolders(target);
}

await copyFiles({source, target});
};
5 changes: 4 additions & 1 deletion src/services/project.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {confirm} from '../utils/prompts.utils';
import {initArgs} from './args.services';
import {dependencies} from './deps.services';
import {generate} from './generate.services';
import {promptDestination, promptGitHubAction} from './prompt.services';
import {promptDestination, promptGitHubAction, promptServerlessFunctions} from './prompt.services';
import {initTemplate} from './template.services';

export const checkForExistingProject = async (): Promise<{initProject: boolean}> => {
Expand Down Expand Up @@ -43,6 +43,8 @@ export const initNewProject = async (args: string[]): Promise<GeneratorInput> =>

const template = nonNullish(userInputs.template) ? userInputs.template : await initTemplate();

const serverlessFunctions = await promptServerlessFunctions(template);

const gitHubAction = await promptGitHubAction();

const localDevelopment = template.kind === 'app';
Expand All @@ -52,6 +54,7 @@ export const initNewProject = async (args: string[]): Promise<GeneratorInput> =>
template,
gitHubAction,
localDevelopment,
serverlessFunctions,
verbose: hasArgs({args, options: ['-v', '--verbose']})
};

Expand Down
44 changes: 43 additions & 1 deletion src/services/prompt.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import {isNullish} from '@dfinity/utils';
import {red} from 'kleur';
import prompts from 'prompts';
import {TEMPLATES} from '../constants/templates';
import type {GeneratorInput, PopulateTemplate, ProjectKind} from '../types/generator';
import type {
GeneratorInput,
PopulateTemplate,
ProjectKind,
ServerlessFunctions
} from '../types/generator';
import type {Template, TemplateFramework, TemplateKeyOption} from '../types/template';
import {assertAnswerCtrlC, confirm} from '../utils/prompts.utils';

Expand Down Expand Up @@ -146,3 +151,40 @@ export const promptProjectKind = async (): Promise<ProjectKind> => {
export const promptGitHubAction = async (): Promise<boolean> => {
return await confirm(`Would you like to set up a GitHub Action for deployment?`);
};

export const promptServerlessFunctions = async ({
typeChecking
}: PopulateTemplate): Promise<ServerlessFunctions | undefined> => {
const {type}: {type: ServerlessFunctions | 'none' | undefined} = await prompts({
type: 'select',
name: 'type',
message: 'Would you like to include serverless functions in your project?',
choices: [
{
title: 'Rust',
value: 'rust'
},
...(typeChecking
? [
{
title: 'TypeScript (experimental)',
value: 'ts'
}
]
: [
{
title: 'JavaScript (experimental)',
value: 'js'
}
]),
{
title: 'None',
value: 'none'
}
]
});

assertAnswerCtrlC(type);

return type === 'none' ? undefined : type;
};
3 changes: 3 additions & 0 deletions src/types/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ export interface GeneratorInput {
template: PopulateTemplate;
gitHubAction: boolean;
localDevelopment: boolean;
serverlessFunctions: ServerlessFunctions | undefined;
verbose?: boolean;
}

export type ProjectKind = 'website' | 'app';

export type ServerlessFunctions = 'rust' | 'ts' | 'js';

export type PopulateInput = {
where: string | null;
} & Omit<GeneratorInput, 'destination'>;
Expand Down
5 changes: 5 additions & 0 deletions src/utils/fs.utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {existsSync, mkdirSync} from 'node:fs';
import {cp} from 'node:fs/promises';
import {dirname, join} from 'node:path';
import {fileURLToPath} from 'node:url';
import type {TemplateKey} from '../types/template';
Expand All @@ -20,3 +21,7 @@ export const createParentFolders = (target: string) => {
mkdirSync(folder, {recursive: true});
}
};

export const copyFiles = async ({source, target}: {source: string; target: string}) => {
await cp(join(__dirname, source), target, {recursive: true});
};
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@
"outDir": "dist",
"resolveJsonModule": true
},
"exclude": ["templates/**/*"]
"exclude": ["templates/**/*", "boilerplate/**/*"]
}