@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
783 lines (674 loc) • 19.8 kB
JavaScript
"use strict";
/**
* Haskell Backend Template Base Generator
* Shared functionality for all Haskell web frameworks
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.HaskellBackendGenerator = void 0;
const backend_template_generator_1 = require("../shared/backend-template-generator");
const fs_1 = require("fs");
const path = __importStar(require("path"));
class HaskellBackendGenerator extends backend_template_generator_1.BackendTemplateGenerator {
constructor(framework) {
const config = {
language: 'Haskell',
framework,
packageManager: 'cabal',
buildTool: 'stack',
testFramework: 'hspec',
features: [
'Pure functional programming',
'Type-safe development',
'Lazy evaluation',
'Strong static typing',
'Pattern matching',
'Monadic composition',
'STM for concurrency',
'Type-level programming',
'Property-based testing',
'GHC optimizations'
],
dependencies: {},
devDependencies: {},
scripts: {
'build': 'stack build',
'test': 'stack test',
'run': 'stack run',
'repl': 'stack ghci',
'clean': 'stack clean',
'install': 'stack install',
'watch': 'stack build --file-watch --fast',
'benchmark': 'stack bench',
'haddock': 'stack haddock',
'format': 'ormolu --mode inplace **/*.hs'
},
dockerConfig: {
baseImage: 'haskell:9.6-slim',
workDir: '/app',
exposedPorts: [3000],
buildSteps: [
'COPY stack.yaml package.yaml* ./',
'RUN stack setup',
'RUN stack build --only-dependencies',
'COPY . .',
'RUN stack build --copy-bins'
],
runCommand: '/usr/local/bin/app-exe',
multistage: true
},
envVars: {
'PORT': '3000',
'HOST': '0.0.0.0',
'ENV': 'development',
'LOG_LEVEL': 'info',
'DATABASE_URL': 'postgresql://user:password@localhost:5432/haskell_db',
'REDIS_URL': 'redis://localhost:6379',
'JWT_SECRET': 'your-secret-key',
'CORS_ORIGIN': '*'
}
};
super(config);
}
async generateLanguageFiles(projectPath, options) {
// Generate stack.yaml
await this.generateStackConfig(projectPath);
// Generate package.yaml or .cabal file
await this.generatePackageConfig(projectPath, options);
// Generate .gitignore
await this.generateHaskellGitignore(projectPath);
// Generate HLint configuration
await this.generateHLintConfig(projectPath);
// Generate stylish-haskell config
await this.generateStylishHaskellConfig(projectPath);
// Create directory structure
const directories = [
'app',
'src',
'src/API',
'src/Config',
'src/Database',
'src/Models',
'src/Services',
'src/Types',
'src/Utils',
'test',
'test/Unit',
'test/Integration',
'bench',
'scripts'
];
for (const dir of directories) {
await fs_1.promises.mkdir(path.join(projectPath, dir), { recursive: true });
}
}
async generateStackConfig(projectPath) {
const stackContent = `# Stack configuration for ${this.config.framework} application
resolver: lts-21.25 # GHC 9.4.8
packages:
- .
extra-deps:
${this.getExtraDeps().map(dep => `- ${dep}`).join('\n')}
# Override default flag values for local packages and extra-deps
flags: {}
# Extra package databases containing global packages
extra-package-dbs: []
# Docker image settings
docker:
enable: false
# Allow newer versions of packages
allow-newer: true
# Build settings
build:
library-profiling: false
executable-profiling: false
copy-bins: true
# GHC options
ghc-options:
"$everything": -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints
# Nix integration (optional)
nix:
enable: false
`;
await fs_1.promises.writeFile(path.join(projectPath, 'stack.yaml'), stackContent);
}
async generatePackageConfig(projectPath, options) {
const packageContent = `name: ${options.name}
version: 0.1.0.0
github: "githubuser/${options.name}"
license: BSD3
author: "Author name here"
maintainer: "example@example.com"
copyright: "2024 Author name here"
extra-source-files:
- README.md
- CHANGELOG.md
# Metadata used when publishing your package
synopsis: ${this.config.framework} web application
category: Web
# To avoid duplicated efforts in documentation and dealing with the
# complications of embedding Haddock markup inside cabal files, it is
# common to point users to the README.md file.
description: Please see the README on GitHub at <https://github.com/githubuser/${options.name}#readme>
dependencies:
- base >= 4.7 && < 5
${this.getFrameworkDependencies().map(dep => `- ${dep}`).join('\n')}
default-extensions:
- OverloadedStrings
- RecordWildCards
- LambdaCase
- TupleSections
- TypeApplications
- DataKinds
- TypeOperators
- FlexibleContexts
- FlexibleInstances
- MultiParamTypeClasses
- ScopedTypeVariables
- DeriveGeneric
- GeneralizedNewtypeDeriving
- DerivingStrategies
- StandaloneDeriving
- DeriveAnyClass
ghc-options:
- -Wall
- -Wcompat
- -Widentities
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wmissing-export-lists
- -Wmissing-home-modules
- -Wpartial-fields
- -Wredundant-constraints
library:
source-dirs: src
executables:
${options.name}-exe:
main: Main.hs
source-dirs: app
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- ${options.name}
tests:
${options.name}-test:
main: Spec.hs
source-dirs: test
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- ${options.name}
- hspec
- hspec-wai
- hspec-wai-json
- QuickCheck
- quickcheck-instances
benchmarks:
${options.name}-bench:
main: Main.hs
source-dirs: bench
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- ${options.name}
- criterion
`;
await fs_1.promises.writeFile(path.join(projectPath, 'package.yaml'), packageContent);
}
async generateHaskellGitignore(projectPath) {
const gitignoreContent = `# Haskell
dist
dist-*
cabal-dev
*.o
*.hi
*.hie
*.chi
*.chs.h
*.dyn_o
*.dyn_hi
.hpc
.hsenv
.cabal-sandbox/
cabal.sandbox.config
*.prof
*.aux
*.hp
*.eventlog
.stack-work/
cabal.project.local
cabal.project.local~
.HTF/
.ghc.environment.*
# Stack
stack.yaml.lock
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Project specific
.env
.env.local
.env.*.local
logs/
*.log
`;
await fs_1.promises.writeFile(path.join(projectPath, '.gitignore'), gitignoreContent);
}
async generateHLintConfig(projectPath) {
const hlintContent = `# HLint configuration file
# https://github.com/ndmitchell/hlint
# Specify additional command line arguments
- arguments: [--color]
# Warnings currently triggered by our code
- ignore: {name: "Use newtype instead of data"}
- ignore: {name: "Redundant do"}
- ignore: {name: "Use <$>"}
- ignore: {name: "Use list comprehension"}
# Ignore some builtin hints
- ignore: {name: "Use camelCase"}
- ignore: {name: "Eta reduce"}
# Custom hints
- warn: {lhs: "mappend", rhs: "(<>)"}
- warn: {lhs: "map f (map g x)", rhs: "map (f . g) x"}
- warn: {lhs: "concat (map f x)", rhs: "concatMap f x"}
# Modules to ignore
- ignore: {name: "Use module export list", within: ["Main"]}
# Extensions we allow
- extensions:
- default: true
- name: [OverloadedStrings, RecordWildCards, ViewPatterns]
`;
await fs_1.promises.writeFile(path.join(projectPath, '.hlint.yaml'), hlintContent);
}
async generateStylishHaskellConfig(projectPath) {
const stylishContent = `# stylish-haskell configuration
steps:
- simple_align:
cases: true
top_level_patterns: true
records: true
- imports:
align: global
list_align: after_alias
pad_module_names: true
long_list_align: inline
empty_list_align: inherit
list_padding: 4
separate_lists: true
space_surround: false
- language_pragmas:
style: vertical
align: true
remove_redundant: true
- trailing_whitespace: {}
columns: 100
newline: native
language_extensions:
- OverloadedStrings
- RecordWildCards
- TypeApplications
- DataKinds
- TypeOperators
`;
await fs_1.promises.writeFile(path.join(projectPath, '.stylish-haskell.yaml'), stylishContent);
}
async generateCommonFiles(projectPath, options) {
await super.generateCommonFiles(projectPath, options);
// Generate Haskell-specific common files
await this.generateCabalConfig(projectPath);
await this.generateTestSetup(projectPath);
await this.generateBenchmarkSetup(projectPath);
await this.generateMakefile(projectPath);
}
async generateCabalConfig(projectPath) {
const cabalConfigContent = `-- Cabal configuration
repository stackage
url: https://github.com/commercialhaskell/all-cabal-files/archive/hackage.tar.gz
jobs: $ncpus
documentation: true
doc-index-file: $datadir/doc/$arch-$os-$compiler/index.html
`;
await fs_1.promises.writeFile(path.join(projectPath, 'cabal.config'), cabalConfigContent);
}
async generateTestSetup(projectPath) {
const specContent = `{-# LANGUAGE OverloadedStrings #-}
import Test.Hspec
import Test.QuickCheck
import Control.Exception (evaluate)
main :: IO ()
main = hspec $ do
describe "Prelude.head" $ do
it "returns the first element of a list" $ do
head [23 ..] \`shouldBe\` (23 :: Int)
it "returns the first element of an *arbitrary* list" $
property $ \\x xs -> head (x:xs) == (x :: Int)
it "throws an exception if used with an empty list" $ do
evaluate (head []) \`shouldThrow\` anyException
`;
await fs_1.promises.writeFile(path.join(projectPath, 'test', 'Spec.hs'), specContent);
}
async generateBenchmarkSetup(projectPath) {
const benchContent = `import Criterion.Main
-- Our benchmark harness.
main :: IO ()
main = defaultMain [
bgroup "example" [ bench "1" $ whnf (\\x -> x + 1) (1 :: Int)
, bench "2" $ whnf (\\x -> x + 2) (1 :: Int)
]
]
`;
await fs_1.promises.writeFile(path.join(projectPath, 'bench', 'Main.hs'), benchContent);
}
async generateMakefile(projectPath) {
const makefileContent = `.PHONY: all build test bench clean run watch format lint setup
all: build
setup:
stack setup
stack build --dependencies-only --test --no-run-tests
build:
stack build --fast
test:
stack test
bench:
stack bench
clean:
stack clean
run:
stack run
watch:
stack build --file-watch --fast
format:
find src app test -name "*.hs" -exec ormolu --mode inplace {} \\;
lint:
hlint src app test
ghci:
stack ghci
docs:
stack haddock
install:
stack install
docker-build:
docker build -t ${this.config.framework.toLowerCase()}-app .
docker-run:
docker run -p 3000:3000 ${this.config.framework.toLowerCase()}-app
`;
await fs_1.promises.writeFile(path.join(projectPath, 'Makefile'), makefileContent);
}
async generateTestStructure(projectPath, options) {
// Test structure is already created in generateLanguageFiles
// Additional test files can be added here if needed
}
async generateHealthCheck(projectPath) {
// Health check is typically implemented in the framework files
// Can be overridden by specific frameworks if needed
}
async generateAPIDocs(projectPath) {
// API docs are framework-specific and implemented in framework generators
}
async generateDockerFiles(projectPath, options) {
const dockerContent = this.getDockerfileContent(options);
await fs_1.promises.writeFile(path.join(projectPath, 'Dockerfile'), dockerContent);
// Docker Compose file
const dockerComposeContent = `version: '3.8'
services:
app:
build: .
ports:
- "\${PORT:-3000}:3000"
environment:
- PORT=3000
- ENV=production
- DATABASE_URL=postgresql://postgres:postgres@db:5432/haskell_db
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
restart: unless-stopped
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=haskell_db
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
`;
await fs_1.promises.writeFile(path.join(projectPath, 'docker-compose.yml'), dockerComposeContent);
}
async generateDocumentation(projectPath, options) {
const readmeContent = `# ${options.name}
A ${this.config.framework} web application built with Haskell.
## Prerequisites
- GHC 9.4+ or Stack
- PostgreSQL 12+
- Redis (optional)
## Getting Started
\`\`\`bash
# Install dependencies
stack setup
stack build
# Run tests
stack test
# Start development server
stack run
# Build for production
stack build --copy-bins
\`\`\`
## Project Structure
\`\`\`
.
├── app/ # Application entry point
├── src/ # Source code
├── test/ # Test files
├── bench/ # Benchmarks
├── stack.yaml # Stack configuration
└── package.yaml # Package configuration
\`\`\`
## Development
\`\`\`bash
# Watch mode
stack build --file-watch --fast
# REPL
stack ghci
# Format code
make format
# Lint code
make lint
\`\`\`
## Testing
\`\`\`bash
# Run all tests
stack test
# Run with coverage
stack test --coverage
# Run specific test
stack test --test-arguments="-m TestName"
\`\`\`
## Deployment
\`\`\`bash
# Build Docker image
docker build -t ${options.name} .
# Run with Docker Compose
docker-compose up -d
\`\`\`
`;
await fs_1.promises.writeFile(path.join(projectPath, 'README.md'), readmeContent);
}
getLanguageSpecificIgnorePatterns() {
return [
'dist/',
'dist-*/',
'.stack-work/',
'.cabal-sandbox/',
'cabal.sandbox.config',
'*.hi',
'*.o',
'*.prof',
'*.hp',
'*.eventlog',
'.hpc/',
'.hsenv/',
'.HTF/',
'.ghc.environment.*',
'stack.yaml.lock'
];
}
getLanguagePrerequisites() {
return 'Haskell Stack or GHC 9.4+';
}
getInstallCommand() {
return 'stack setup && stack build';
}
getDevCommand() {
return 'stack run';
}
getProdCommand() {
return './app-exe';
}
getTestCommand() {
return 'stack test';
}
getCoverageCommand() {
return 'stack test --coverage';
}
getLintCommand() {
return 'hlint src app test';
}
getBuildCommand() {
return 'stack build --copy-bins';
}
getSetupAction() {
return 'haskell/actions/setup@v2';
}
async generateBuildScript(projectPath, options) {
const buildScriptContent = `#!/bin/bash
# Build script for Haskell ${this.config.framework} application
set -e
echo "Building Haskell ${this.config.framework} application..."
# Setup Stack
echo "Setting up Stack..."
stack setup
# Install dependencies
echo "Installing dependencies..."
stack build --dependencies-only --test --no-run-tests
# Run tests
echo "Running tests..."
stack test
# Build application
echo "Building application..."
stack build --copy-bins
# Generate documentation
echo "Generating documentation..."
stack haddock
echo "Build complete!"
echo "Run 'stack run' to start the application"
`;
await fs_1.promises.mkdir(path.join(projectPath, 'scripts'), { recursive: true });
await fs_1.promises.writeFile(path.join(projectPath, 'scripts', 'build.sh'), buildScriptContent);
await fs_1.promises.chmod(path.join(projectPath, 'scripts', 'build.sh'), 0o755);
}
getDockerfileContent(options) {
return `# Multi-stage Dockerfile for Haskell ${this.config.framework} application
# Build stage
FROM haskell:9.6 AS build
RUN mkdir -p /app
WORKDIR /app
# Install dependencies
RUN apt-get update && apt-get install -y \\
libpq-dev \\
&& rm -rf /var/lib/apt/lists/*
# Copy stack configuration
COPY stack.yaml package.yaml ./
# Setup GHC and dependencies
RUN stack setup
RUN stack build --dependencies-only
# Copy source code
COPY . .
# Build application
RUN stack build --copy-bins
# Runtime stage
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \\
ca-certificates \\
libpq5 \\
libgmp10 \\
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN useradd -m -u 1000 haskell
WORKDIR /app
# Copy binary from build stage
COPY --from=build /root/.local/bin/${options.name}-exe /usr/local/bin/app
# Copy any necessary runtime files
COPY --from=build /app/static ./static
COPY --from=build /app/config ./config
# Switch to non-root user
USER haskell
# Expose port
EXPOSE 3000
# Set environment variables
ENV PORT=3000
ENV HOST=0.0.0.0
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
CMD curl -f http://localhost:3000/health || exit 1
# Run the application
CMD ["app"]
`;
}
}
exports.HaskellBackendGenerator = HaskellBackendGenerator;