Skip to content
Draft
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
92 changes: 92 additions & 0 deletions packages/build-tools/template-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Template Generator

This tool generates a new Effectstream project based on the selected options.

## Usage

```sh
TEMPLATE_PATH=`pwd` deno task -f @effectstream/template-generator start
cd my-project
chmod +x patch.sh
deno install --allow-scripts && ./patch.sh
deno task build:evm
deno task build:midnight
# deno task -f @[project-name]/database pgtyped:update
# deno task -f @[project-name]/midnight-contracts midnight-contract:deploy


# deno task build:cardano
# deno task build:avail
# deno task build:bitcoin
deno task dev
```

## Test
```sh
TEMPLATE_CONFIG_FILE_ALL=true
or
TEMPLATE_CONFIG_FILE_ALL_FAST=true
```
To skip the interactive prompt and generate the project with all options.

## Packages (& Roadmap)

### General
- [ ] Config Parameters
- [ ] Add links to documentation in each step
- [ ] Maybe this could also scaffold the templates?
- [ ] How do we test this? (We need a non-interactive cli, and ci-cd pipeline)
- [ ] Make this work in the /template folder
- [ ] Documentation

### Contracts/Avail
- [ ] Create base project structure
- [ ] Add empty contract

### Contracts/Bitcoin
- [ ] Pending on implementation.

### Contracts/Cardano
- [ ] Pending on implementation.

### Contracts/EVM
- [ ] Create Contract Management System
- [ ] Add Empty Contract
- [ ] Add ERC20/721/1155 contract
- [ ] Add Effectstream L2 contract
- [ ] Add Inverse* contract

### Contracts/Midnight
Including Contracts/Midnight-Contracts

- [ ] Create Contact Management System

### Shared/Data-Types

### Client/API
- [ ] Add Generic API Endpoint Reading from database

### Client/Database
- [ ] Add Generic Example Tables

### Client/Batcher
- [ ] Add Generic EVM Batcher Adapter
- [ ] Enable disable code depending on selected chains

### Client/Node
- [ ] LocalhostConfig Enable Sections depending on selected chains
- [ ] LocalhostConfig Enable Dynamic code depending on selected contracts
- [ ] StateMachine create state for grammar
- [ ] Grammar create sections depending on selected contracts

### Frontend/Standalone
- [ ] Working example frontend reading from API
- [ ] Working example including @effectstream/wallets

### Frontend/Integrated Vite-Deno
- [ ] Working example frontend reading from API
- [ ] Working example including @effectstream/wallets

### Root files
- [ ] Readme should contain instructions per chain/contract selected

42 changes: 42 additions & 0 deletions packages/build-tools/template-generator/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@effectstream/template-generator",
"version": "0.3.0",
"license": "MIT",
"exports": {
".": "./mod.ts"
},
"alias": {
"npm:/react-dom@18.3.1": "react-dom"
},
"strict": true,
"imports": {
"@std/path": "jsr:@std/path@^1.1.3",
"@effectstream/evm-hardhat": "../../chains/evm-hardhat/deno.json",
"react": "npm:react@18.3.1",
"react-dom": "npm:react-dom@18.3.1",
"ink": "npm:ink@5.0.1",
"ink-text-input": "npm:ink-text-input@6.0.0",
"ink-select-input": "npm:ink-select-input@6.2.0",
"ink-big-text": "npm:ink-big-text@2.0.0",
"ink-gradient": "npm:ink-gradient@3.0.0",
"@types/react": "npm:@types/react@18.3.1"
},
"tasks": {
"check": "deno check src/template-generator.tsx",
"start": "deno run -A src/template-generator.tsx"
},
"compilerOptions": {
"types": [
"react",
"react-dom",
"@types/react"
],
"lib": [
"dom",
"dom.iterable",
"deno.ns"
],
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
1 change: 1 addition & 0 deletions packages/build-tools/template-generator/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './src/template-generator.tsx';
66 changes: 66 additions & 0 deletions packages/build-tools/template-generator/src/file-operations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as path from "jsr:@std/path";

// Copy files
// sourceDir - source directory
// targetDir - target directory
//
// replacements - simple [TAG] -> `code` replacement
// codeBlocks - enable/disable /** TAG */ ... /** TAG */
// codeInsertions - replace /** TAG */ with `code`
// replaceFileNames - x => y
export async function copyFiles(
sourceDir: string,
targetDir: string,
replacements: Record<string, string> = {},
codeBlocks: Record<string, boolean> = {},
codeInsertions: Record<string, string> = {},
replaceFileNames: Record<string, string> = {},
): Promise<void> {
const files = await Deno.readDir(sourceDir);
await Deno.mkdir(targetDir, { recursive: true });

for await (const file of files) {
if (file.isDirectory) {
continue;
}
let finalName = file.name.replace(".rename", "");
if (replaceFileNames[finalName]) {
finalName = replaceFileNames[finalName];
}

let content = await Deno.readTextFile(path.join(sourceDir, file.name));

// Enable/disable entire inlined code blocks
for (const [codeBlock, enabled] of Object.entries(codeBlocks)) {
// Search for block of code between /** TAG */ ... /** TAG */
const r = `\\/\\*\\* ${codeBlock} \\*\\/([\\s\\S]+?)\\/\\*\\* ${codeBlock} \\*\\/`;
const regex = new RegExp(r, 'g');
if (enabled) {
content = content.replace(regex, `$1`);
} else {
content = content.replace(regex, '');
}
}

// Insert dynamically generated code blocks
for (const [r, code] of Object.entries(codeInsertions)) {
// Search for tags /** TAG */
const regex = new RegExp(`\\/\\*\\* ${r} \\*\\/`, 'g');
content = content.replace(regex, code);
}

// Replace placeholders with actual values
for (const [r, replacement] of Object.entries(replacements)) {
// Replace tags in [TAG] format
const regex = new RegExp(`\\[${r}\\]`, 'g');
content = content.replace(regex, replacement);
}

await Deno.writeTextFile(path.join(targetDir, finalName), content);
}
}

// // Get current directory - this has to be JSR compatible.
// export function currentDir(): string {
// return path.dirname(path.fromFileUrl(import.meta.url));
// }
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { GeneratedFile } from './generated-file.ts';

export class DenoJsonFile extends GeneratedFile {
constructor(filePath: string, private content: any) {
super(filePath);
if (!this.content.name) {
throw new Error('Name is required');
}
if (!this.content.exports) {
this.content.exports = {
// This is a placeholder
".": "./src/mod.ts",
};
}
}

getContent(): string {
return JSON.stringify(this.content, null, 2);
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { GeneratedFile } from './generated-file.ts';
import { Chain, Frontend, PAIMA_SCOPE, EFFECTSTREAM_VERSION } from '../options.ts';

export class RootDenoJsonFile extends GeneratedFile {
private content: object;

constructor(filePath: string, projectName: string, chains: Chain[], frontend: Frontend) {
super(filePath);

const tasks: Record<string, string> = {};
const imports: Record<string, string> = {};

// Conditional tasks
if (chains.includes('evm')) {
tasks['build:evm'] = `deno task -f @${projectName}/evm-contracts build:mod`;
}
if (chains.includes('midnight')) {
tasks['build:midnight'] = `deno task -r compact`;
}

tasks['dev'] = `deno task -f @${projectName}/node dev`;

if (frontend === 'intergrated-vite-deno') {
tasks['start:frontend'] = `deno task -f @${projectName}/frontend dev`;
}

tasks['check'] = `deno task -f @${projectName}/node check`;

// Conditional imports
imports[`${PAIMA_SCOPE}/tui`] = `jsr:${PAIMA_SCOPE}/tui@${EFFECTSTREAM_VERSION}`;
if (chains.includes('midnight')) {
imports[`${PAIMA_SCOPE}/midnight-contracts`] = `jsr:${PAIMA_SCOPE}/midnight-contracts@${EFFECTSTREAM_VERSION}`;
}
if (chains.includes('evm')) {
imports[`${PAIMA_SCOPE}/evm-contracts`] = `jsr:${PAIMA_SCOPE}/evm-contracts@${EFFECTSTREAM_VERSION}`;
}
imports['@std/path'] = 'jsr:@std/path@^1.1.3';

this.content = {
workspace: ['./packages/**/*'],
nodeModulesDir: 'auto',
tasks,
imports,
lint: {
rules: {
exclude: [
'no-this-alias',
'require-yield',
'no-explicit-any',
'ban-types',
'no-unused-vars',
'no-slow-types',
],
},
},
};
}

getContent(): string {
return JSON.stringify(this.content, null, 2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import path from 'node:path';
import fs from 'node:fs/promises';

export abstract class GeneratedFile {
constructor(public filePath: string) {}

protected abstract getContent(): string;

public async write(mode?: number): Promise<void> {
const dir = path.dirname(this.filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(this.filePath, this.getContent(), { mode });
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { GeneratedFile } from './generated-file.ts';

export class GitignoreFile extends GeneratedFile {
constructor(filePath: string) {
super(filePath);
}

getContent(): string {
return `
**/node_modules
forge-std
logs/

# e2e tests
/cache
/out

# Frontend
.vite

# Batcher file storage
batcher-data/
midnight-level-db/

# Docusaurus
.docusaurus/

# System files
.DS_Store

# Temporary files
packages/client/node/tmux.conf
packages/client/node/install.sh
`.trim();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { GeneratedFile } from './generated-file.ts';

export class HtmlFile extends GeneratedFile {
constructor(filePath: string, private title: string, private scriptSrc: string) {
super(filePath);
}

getContent(): string {
return `<!DOCTYPE html>
<html>
<head>
<title>${this.title}</title>
</head>
<body>
<script src="${this.scriptSrc}"></script>
</body>
</html>`;
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { GeneratedFile } from './generated-file.ts';

export class MarkdownFile extends GeneratedFile {
protected lines: string[] = [];

constructor(filePath: string) {
super(filePath);
}

addHeader(text: string, level: 1 | 2 | 3 | 4 | 5 | 6 = 1): this {
this.lines.push(`${'#'.repeat(level)} ${text}`);
return this;
}

addText(text: string): this {
this.lines.push(text);
return this;
}

addCodeBlock(code: string, language = 'sh'): this {
this.lines.push(`\`\`\`${language}\n${code}\n\`\`\``);
return this;
}

getContent(): string {
return this.lines.join('\n\n');
}
}


Loading
Loading