diff --git a/fixtures/src_template/expected/docker/development.dockerfile b/fixtures/src_template/expected/docker/development.dockerfile index a4e5b267..78e24ba0 100644 --- a/fixtures/src_template/expected/docker/development.dockerfile +++ b/fixtures/src_template/expected/docker/development.dockerfile @@ -4,13 +4,6 @@ FROM crystallang/crystal:1.16.1 RUN apt-get update && \ apt-get install -y wget -# Apt installs: -# - Postgres cli tools are required for lucky-cli. -# - tmux is required for the Overmind process manager. -RUN apt-get update && \ - apt-get install -y postgresql-client tmux && \ - rm -rf /var/lib/apt/lists/* - # Install lucky cli WORKDIR /lucky/cli RUN git clone https://github.com/luckyframework/lucky_cli . && \ diff --git a/fixtures/src_template__api_only/expected/docker/development.dockerfile b/fixtures/src_template__api_only/expected/docker/development.dockerfile index a4e5b267..78e24ba0 100644 --- a/fixtures/src_template__api_only/expected/docker/development.dockerfile +++ b/fixtures/src_template__api_only/expected/docker/development.dockerfile @@ -4,13 +4,6 @@ FROM crystallang/crystal:1.16.1 RUN apt-get update && \ apt-get install -y wget -# Apt installs: -# - Postgres cli tools are required for lucky-cli. -# - tmux is required for the Overmind process manager. -RUN apt-get update && \ - apt-get install -y postgresql-client tmux && \ - rm -rf /var/lib/apt/lists/* - # Install lucky cli WORKDIR /lucky/cli RUN git clone https://github.com/luckyframework/lucky_cli . && \ diff --git a/fixtures/src_template__generate_auth/expected/docker/dev_entrypoint.sh b/fixtures/src_template__generate_auth/expected/docker/dev_entrypoint.sh index d16ef6ab..084fb44b 100755 --- a/fixtures/src_template__generate_auth/expected/docker/dev_entrypoint.sh +++ b/fixtures/src_template__generate_auth/expected/docker/dev_entrypoint.sh @@ -27,8 +27,10 @@ if ! [ -d bin ] ; then echo 'Creating bin directory' mkdir bin fi -echo 'Installing npm packages...' + +echo 'Installing yarn packages...' yarn install + if ! shards check ; then echo 'Installing shards...' shards install diff --git a/fixtures/src_template__js_bun_bundler/expected/.crystal-version b/fixtures/src_template__js_bun_bundler/expected/.crystal-version new file mode 100644 index 00000000..41c11ffb --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/.crystal-version @@ -0,0 +1 @@ +1.16.1 diff --git a/fixtures/src_template__js_bun_bundler/expected/.env b/fixtures/src_template__js_bun_bundler/expected/.env new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__js_bun_bundler/expected/.github/workflows/ci.yml b/fixtures/src_template__js_bun_bundler/expected/.github/workflows/ci.yml new file mode 100644 index 00000000..310ca50d --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/.github/workflows/ci.yml @@ -0,0 +1,113 @@ +name: test-project CI + +on: + push: + branches: "*" + pull_request: + branches: "*" + +jobs: + check-format: + strategy: + fail-fast: false + matrix: + crystal_version: + - 1.16.1 + experimental: + - false + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} + steps: + - uses: actions/checkout@v4 + - name: Install Crystal + uses: crystal-lang/install-crystal@v1 + with: + crystal: ${{ matrix.crystal_version }} + - name: Format + run: crystal tool format --check + + specs: + strategy: + fail-fast: false + matrix: + crystal_version: + - 1.16.1 + experimental: + - false + runs-on: ubuntu-latest + env: + LUCKY_ENV: test + DB_HOST: localhost + continue-on-error: ${{ matrix.experimental }} + services: + postgres: + image: postgres:14-alpine + env: + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + # Set health checks to wait until postgres has started + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + - name: Install Crystal + uses: crystal-lang/install-crystal@v1 + with: + crystal: ${{ matrix.crystal_version }} + + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - name: Set up Yarn cache + uses: actions/cache@v4 + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Set up Node cache + uses: actions/cache@v4 + id: node-cache # use this to check for `cache-hit` (`steps.node-cache.outputs.cache-hit != 'true'`) + with: + path: '**/node_modules' + key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Set up Crystal cache + uses: actions/cache@v4 + id: crystal-cache + with: + path: | + ~/.cache/crystal + lib + key: ${{ runner.os }}-crystal-${{ hashFiles('**/shard.lock') }} + restore-keys: | + ${{ runner.os }}-crystal- + + - name: Install shards + if: steps.crystal-cache.outputs.cache-hit != 'true' + run: shards check || shards install + + - name: Install yarn packages + if: steps.node-cache.outputs.cache-hit != 'true' + run: yarn install --frozen-lockfile --no-progress + - name: Compiling assets + run: yarn prod + - name: Build lucky_tasks + run: crystal build tasks.cr -o ./lucky_tasks + + - name: Prepare database + run: | + ./lucky_tasks db.create + ./lucky_tasks db.migrate + ./lucky_tasks db.seed.required_data + + - name: Run tests + run: crystal spec -Dwith_sec_tests \ No newline at end of file diff --git a/fixtures/src_template__js_bun_bundler/expected/Procfile b/fixtures/src_template__js_bun_bundler/expected/Procfile new file mode 100644 index 00000000..e524d70d --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/Procfile @@ -0,0 +1,2 @@ +web: bin/app +release: lucky db.migrate diff --git a/fixtures/src_template__js_bun_bundler/expected/Procfile.dev b/fixtures/src_template__js_bun_bundler/expected/Procfile.dev new file mode 100644 index 00000000..17ab4309 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/Procfile.dev @@ -0,0 +1,3 @@ +system_check: crystal script/system_check.cr +web: lucky watch --reload-browser +assets: yarn watch diff --git a/fixtures/src_template__js_bun_bundler/expected/README.md b/fixtures/src_template__js_bun_bundler/expected/README.md new file mode 100644 index 00000000..da2d97cc --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/README.md @@ -0,0 +1,23 @@ +# test-project + +This is a project written using [Lucky](https://luckyframework.org). Enjoy! + +### Setting up the project + +1. [Install required dependencies](https://luckyframework.org/guides/getting-started/installing#install-required-dependencies) +1. Update database settings in `config/database.cr` +1. Run `script/setup` +1. Run `lucky dev` to start the app + +### Using Docker for development + +1. [Install Docker](https://docs.docker.com/engine/install/) +1. Run `docker compose up` + +The Docker container will boot all of the necessary components needed to run your Lucky application. +To configure the container, update the `docker-compose.yml` file, and the `docker/development.dockerfile` file. + + +### Learning Lucky + +Lucky uses the [Crystal](https://crystal-lang.org) programming language. You can learn about Lucky from the [Lucky Guides](https://luckyframework.org/guides/getting-started/why-lucky). diff --git a/fixtures/src_template__js_bun_bundler/expected/config/application.cr b/fixtures/src_template__js_bun_bundler/expected/config/application.cr new file mode 100644 index 00000000..c807149a --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/config/application.cr @@ -0,0 +1,24 @@ +# This file may be used for custom Application configurations. +# It will be loaded before other config files. +# +# Read more on configuration: +# https://luckyframework.org/guides/getting-started/configuration#configuring-your-own-code + +# Use this code as an example: +# +# ``` +# module Application +# Habitat.create do +# setting support_email : String +# setting lock_with_basic_auth : Bool +# end +# end +# +# Application.configure do |settings| +# settings.support_email = "support@myapp.io" +# settings.lock_with_basic_auth = LuckyEnv.staging? +# end +# +# # In your application, call +# # `Application.settings.support_email` anywhere you need it. +# ``` diff --git a/fixtures/src_template__js_bun_bundler/expected/config/colors.cr b/fixtures/src_template__js_bun_bundler/expected/config/colors.cr new file mode 100644 index 00000000..761ae940 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/config/colors.cr @@ -0,0 +1,4 @@ +# This enables the color output when in development or test +# Check out the Colorize docs for more information +# https://crystal-lang.org/api/Colorize.html +Colorize.enabled = LuckyEnv.development? || LuckyEnv.test? diff --git a/fixtures/src_template__js_bun_bundler/expected/config/cookies.cr b/fixtures/src_template__js_bun_bundler/expected/config/cookies.cr new file mode 100644 index 00000000..2d5055f2 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/config/cookies.cr @@ -0,0 +1,25 @@ +require "./server" + +Lucky::Session.configure do |settings| + settings.key = "_test_project_session" +end + +Lucky::CookieJar.configure do |settings| + settings.on_set = ->(cookie : HTTP::Cookie) { + # If ForceSSLHandler is enabled, only send cookies over HTTPS + cookie.secure(Lucky::ForceSSLHandler.settings.enabled) + + # By default, don't allow reading cookies with JavaScript + cookie.http_only(true) + + # Restrict cookies to a first-party or same-site context + cookie.samesite(:lax) + + # Set all cookies to the root path by default + cookie.path("/") + + # You can set other defaults for cookies here. For example: + # + # cookie.expires(1.year.from_now).domain("mydomain.com") + } +end diff --git a/fixtures/src_template__js_bun_bundler/expected/config/database.cr b/fixtures/src_template__js_bun_bundler/expected/config/database.cr new file mode 100644 index 00000000..f614299a --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/config/database.cr @@ -0,0 +1,29 @@ +database_name = "test_project_#{LuckyEnv.environment}" + +AppDatabase.configure do |settings| + if LuckyEnv.production? + settings.credentials = Avram::Credentials.parse(ENV["DATABASE_URL"]) + else + settings.credentials = Avram::Credentials.parse?(ENV["DATABASE_URL"]?) || Avram::Credentials.new( + database: database_name, + hostname: ENV["DB_HOST"]? || "localhost", + port: ENV["DB_PORT"]?.try(&.to_i) || 5432, + # Some common usernames are "postgres", "root", or your system username (run 'whoami') + username: ENV["DB_USERNAME"]? || "postgres", + # Some Postgres installations require no password. Use "" if that is the case. + password: ENV["DB_PASSWORD"]? || "postgres" + ) + end +end + +Avram.configure do |settings| + settings.database_to_migrate = AppDatabase + + # In production, allow lazy loading (N+1). + # In development and test, raise an error if you forget to preload associations + settings.lazy_load_enabled = LuckyEnv.production? + + # Always parse `Time` values with these specific formats. + # Used for both database values, and datetime input fields. + # settings.time_formats << "%F" +end diff --git a/fixtures/src_template__js_bun_bundler/expected/config/email.cr b/fixtures/src_template__js_bun_bundler/expected/config/email.cr new file mode 100644 index 00000000..7c875449 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/config/email.cr @@ -0,0 +1,26 @@ +require "carbon_sendgrid_adapter" + +BaseEmail.configure do |settings| + if LuckyEnv.production? + # If you don't need to send emails, set the adapter to DevAdapter instead: + # + # settings.adapter = Carbon::DevAdapter.new + # + # If you do need emails, get a key from SendGrid and set an ENV variable + send_grid_key = send_grid_key_from_env + settings.adapter = Carbon::SendGridAdapter.new(api_key: send_grid_key) + elsif LuckyEnv.development? + settings.adapter = Carbon::DevAdapter.new(print_emails: true) + else + settings.adapter = Carbon::DevAdapter.new + end +end + +private def send_grid_key_from_env + ENV["SEND_GRID_KEY"]? || raise_missing_key_message +end + +private def raise_missing_key_message + puts "Missing SEND_GRID_KEY. Set the SEND_GRID_KEY env variable to 'unused' if not sending emails, or set the SEND_GRID_KEY ENV var.".colorize.red + exit(1) +end diff --git a/fixtures/src_template__js_bun_bundler/expected/config/env.cr b/fixtures/src_template__js_bun_bundler/expected/config/env.cr new file mode 100644 index 00000000..3f364072 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/config/env.cr @@ -0,0 +1,33 @@ +# Environments are managed using `LuckyEnv`. By default, development, production +# and test are supported. See +# https://luckyframework.org/guides/getting-started/configuration for details. +# +# The default environment is development unless the environment variable +# LUCKY_ENV is set. +# +# Example: +# ``` +# LuckyEnv.environment # => "development" +# LuckyEnv.development? # => true +# LuckyEnv.production? # => false +# LuckyEnv.test? # => false +# ``` +# +# New environments can be added using the `LuckyEnv.add_env` macro. +# +# Example: +# ``` +# LuckyEnv.add_env :staging +# LuckyEnv.staging? # => false +# ``` +# +# To determine whether or not a `LuckyTask` is currently running, you can use +# the `LuckyEnv.task?` predicate. +# +# Example: +# ``` +# LuckyEnv.task? # => false +# ``` + +# Add a staging environment. +# LuckyEnv.add_env :staging diff --git a/fixtures/src_template__js_bun_bundler/expected/config/error_handler.cr b/fixtures/src_template__js_bun_bundler/expected/config/error_handler.cr new file mode 100644 index 00000000..c6b736e3 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/config/error_handler.cr @@ -0,0 +1,3 @@ +Lucky::ErrorHandler.configure do |settings| + settings.show_debug_output = !LuckyEnv.production? +end diff --git a/fixtures/src_template__js_bun_bundler/expected/config/log.cr b/fixtures/src_template__js_bun_bundler/expected/config/log.cr new file mode 100644 index 00000000..225d25cc --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/config/log.cr @@ -0,0 +1,50 @@ +require "file_utils" + +if LuckyEnv.test? + # Logs to `tmp/test.log` so you can see what's happening without having + # a bunch of log output in your spec results. + FileUtils.mkdir_p("tmp") + + backend = Log::IOBackend.new(File.new("tmp/test.log", mode: "w")) + backend.formatter = Lucky::PrettyLogFormatter.proc + Log.dexter.configure(:debug, backend) +elsif LuckyEnv.production? + # Lucky uses JSON in production so logs can be searched more easily + # + # If you want logs like in develpoment use 'Lucky::PrettyLogFormatter.proc'. + backend = Log::IOBackend.new + backend.formatter = Dexter::JSONLogFormatter.proc + Log.dexter.configure(:info, backend) +else + # Use a pretty formatter printing to STDOUT in development + backend = Log::IOBackend.new + backend.formatter = Lucky::PrettyLogFormatter.proc + Log.dexter.configure(:debug, backend) + DB::Log.level = :info +end + +# Lucky only logs when before/after pipes halt by redirecting, or rendering a +# response. Pipes that run without halting are not logged. +# +# If you want to log every pipe that runs, set the log level to ':info' +Lucky::ContinuedPipeLog.dexter.configure(:none) + +# Lucky only logs failed queries by default. +# +# Set the log to ':info' to log all queries +Avram::QueryLog.dexter.configure(:none) + +# Subscribe to Pulsar events to log when queries are made, +# queries fail, or save operations fail. Remove this to +# disable these log events without disabling all logging. +Avram.initialize_logging + +# Skip logging static assets requests in development +Lucky::LogHandler.configure do |settings| + if LuckyEnv.development? + settings.skip_if = ->(context : HTTP::Server::Context) { + context.request.method.downcase == "get" && + context.request.resource.starts_with?(/\/css\/|\/js\/|\/assets\/|\/favicon\.ico/) + } + end +end diff --git a/fixtures/src_template__js_bun_bundler/expected/config/route_helper.cr b/fixtures/src_template__js_bun_bundler/expected/config/route_helper.cr new file mode 100644 index 00000000..ede1f328 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/config/route_helper.cr @@ -0,0 +1,10 @@ +# This is used when generating URLs for your application +Lucky::RouteHelper.configure do |settings| + if LuckyEnv.production? + # Example: https://my_app.com + settings.base_uri = ENV.fetch("APP_DOMAIN") + else + # Set domain to the default host/port in development/test + settings.base_uri = "http://localhost:#{Lucky::ServerSettings.port}" + end +end diff --git a/fixtures/src_template__js_bun_bundler/expected/config/server.cr b/fixtures/src_template__js_bun_bundler/expected/config/server.cr new file mode 100644 index 00000000..6421b76d --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/config/server.cr @@ -0,0 +1,67 @@ +# Here is where you configure the Lucky server +# +# Look at config/route_helper.cr if you want to change the domain used when +# generating links with `Action.url`. +Lucky::Server.configure do |settings| + if LuckyEnv.production? + settings.secret_key_base = secret_key_from_env + settings.host = "0.0.0.0" + settings.port = ENV["PORT"].to_i + settings.gzip_enabled = true + # By default certain content types will be gzipped. + # For a full list look in + # https://github.com/luckyframework/lucky/blob/main/src/lucky/server.cr + # To add additional extensions do something like this: + # settings.gzip_content_types << "content/type" + + settings js_bundle_system = Lucky::JSBundlers::YarnBundler + else + settings.secret_key_base = "1234567890" + # Change host/port in config/watch.yml + # Alternatively, you can set the DEV_PORT env to set the port for local development + settings.host = Lucky::ServerSettings.host + settings.port = Lucky::ServerSettings.port + end + + # By default Lucky will serve static assets in development and production. + # + # However you could use a CDN when in production like this: + # + # Lucky::Server.configure do |settings| + # if LuckyEnv.production? + # settings.asset_host = "https://mycdnhost.com" + # else + # settings.asset_host = "" + # end + # end + settings.asset_host = "" # Lucky will serve assets +end + +Lucky::ForceSSLHandler.configure do |settings| + # To force SSL in production, uncomment the lines below. + # This will cause http requests to be redirected to https: + # + # settings.enabled = LuckyEnv.production? + # settings.strict_transport_security = {max_age: 1.year, include_subdomains: true} + # + # Or, leave it disabled: + settings.enabled = false +end + +# Set a unique ID for each HTTP request. +# To enable the request ID, uncomment the lines below. +# You can set your own custom String, or use a random UUID. +# Lucky::RequestIdHandler.configure do |settings| +# settings.set_request_id = ->(context : HTTP::Server::Context) { +# UUID.random.to_s +# } +# end + +private def secret_key_from_env + ENV["SECRET_KEY_BASE"]? || raise_missing_secret_key_in_production +end + +private def raise_missing_secret_key_in_production + puts "Please set the SECRET_KEY_BASE environment variable. You can generate a secret key with 'lucky gen.secret_key'".colorize.red + exit(1) +end diff --git a/fixtures/src_template__js_bun_bundler/expected/config/watch.yml b/fixtures/src_template__js_bun_bundler/expected/config/watch.yml new file mode 100644 index 00000000..3a59b410 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/config/watch.yml @@ -0,0 +1,3 @@ +host: 127.0.0.1 +port: 3000 +reload_port: 3001 diff --git a/fixtures/src_template__js_bun_bundler/expected/db/migrations/.keep b/fixtures/src_template__js_bun_bundler/expected/db/migrations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__js_bun_bundler/expected/docker-compose.yml b/fixtures/src_template__js_bun_bundler/expected/docker-compose.yml new file mode 100644 index 00000000..d00779db --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/docker-compose.yml @@ -0,0 +1,45 @@ +version: "3.8" +services: + lucky: + build: + context: . + dockerfile: docker/development.dockerfile + environment: + DATABASE_URL: postgres://lucky:password@postgres:5432/lucky + DEV_HOST: "0.0.0.0" + volumes: + - .:/app + - node_modules:/app/node_modules + - shards_lib:/app/lib + - app_bin:/app/bin + - build_cache:/root/.cache + depends_on: + - postgres + ports: + - 3000:3000 # This is the Lucky Server port + - 3001:3001 # This is the Lucky watcher reload port + + entrypoint: ["docker/dev_entrypoint.sh"] + + postgres: + image: postgres:14-alpine + environment: + POSTGRES_USER: lucky + POSTGRES_PASSWORD: password + POSTGRES_DB: lucky + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + # The postgres database container is exposed on the host at port 6543 to + # allow connecting directly to it with postgres clients. The port differs + # from the postgres default to avoid conflict with existing postgres + # servers. Connect to a running postgres container with: + # postgres://lucky:password@localhost:6543/lucky + - 6543:5432 + +volumes: + postgres_data: + node_modules: + shards_lib: + app_bin: + build_cache: diff --git a/fixtures/src_template__js_bun_bundler/expected/docker/dev_entrypoint.sh b/fixtures/src_template__js_bun_bundler/expected/docker/dev_entrypoint.sh new file mode 100755 index 00000000..890673da --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/docker/dev_entrypoint.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +set -euo pipefail + +# This is the entrypoint script used for development docker workflows. +# By default it will: +# - Install dependencies. +# - Run migrations. +# - Start the dev server. +# It also accepts any commands to be run instead. + + +warnfail () { + echo "$@" >&2 + exit 1 +} + +case ${1:-} in + "") # If no arguments are provided, start lucky dev server. + ;; + + *) # If any arguments are provided, execute them instead. + exec "$@" +esac + +if ! [ -d bin ] ; then + echo 'Creating bin directory' + mkdir bin +fi + +echo 'Installing bun packages...' +bun install + +if ! shards check ; then + echo 'Installing shards...' + shards install +fi + +echo 'Waiting for postgres to be available...' +./docker/wait-for-it.sh -q postgres:5432 + +if ! psql -d "$DATABASE_URL" -c '\d migrations' > /dev/null ; then + echo 'Finishing database setup...' + lucky db.migrate +fi + +echo 'Starting lucky dev server...' +exec lucky dev diff --git a/fixtures/src_template__js_bun_bundler/expected/docker/development.dockerfile b/fixtures/src_template__js_bun_bundler/expected/docker/development.dockerfile new file mode 100644 index 00000000..8158fc4b --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/docker/development.dockerfile @@ -0,0 +1,33 @@ +FROM crystallang/crystal:1.16.1 + +# Install utilities required to make this Dockerfile run +RUN apt-get update && \ + apt-get install -y wget +# Add the nodesource ppa to apt. Update this to change the nodejs version. +RUN wget https://deb.nodesource.com/setup_16.x -O- | bash + +# Apt installs: +# - nodejs (from above ppa) is required for front-end apps. +# - Postgres cli tools are required for lucky-cli. +# - tmux is required for the Overmind process manager. +RUN apt-get update && \ + apt-get install -y nodejs postgresql-client tmux && \ + rm -rf /var/lib/apt/lists/* + +# NPM global installs: +# NPM global installs: +# - Bun is the default js package manager. +RUN npm install -g bun + +# Install lucky cli +WORKDIR /lucky/cli +RUN git clone https://github.com/luckyframework/lucky_cli . && \ + git checkout v1.3.0 && \ + shards build --without-development && \ + cp bin/lucky /usr/bin + +WORKDIR /app +ENV DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/postgres +EXPOSE 3000 +EXPOSE 3001 + diff --git a/fixtures/src_template__js_bun_bundler/expected/docker/wait-for-it.sh b/fixtures/src_template__js_bun_bundler/expected/docker/wait-for-it.sh new file mode 100755 index 00000000..06e0638c --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/docker/wait-for-it.sh @@ -0,0 +1,189 @@ +#!/usr/bin/bash +# +# Pulled from https://github.com/vishnubob/wait-for-it on 2022-02-28. +# Licensed under the MIT license as of 81b1373f. +# +# Below this line, wait-for-it is the original work of the author. +# +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi + diff --git a/fixtures/src_template__js_bun_bundler/expected/script/helpers/function_helpers.cr b/fixtures/src_template__js_bun_bundler/expected/script/helpers/function_helpers.cr new file mode 100644 index 00000000..8abcb6a2 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/script/helpers/function_helpers.cr @@ -0,0 +1,32 @@ +require "colorize" + +# These are helper methods provided to help keep your code +# clean. Add new methods, or alter these as needed. + +def notice(message : String) : Nil + puts "\n▸ #{message}" +end + +def print_done : Nil + puts "✔ Done" +end + +def print_error(message : String) : Nil + puts "There is a problem with your system setup:\n".colorize.red.bold + puts "#{message}\n".colorize.red.bold + Process.exit(1) +end + +def command_not_found(command : String) : Bool + Process.find_executable(command).nil? +end + +def command_not_running(command : String, *args) : Bool + output = IO::Memory.new + code = Process.run(command, args, output: output).exit_code + code > 0 +end + +def run_command(command : String, *args) : Nil + Process.run(command, args, output: STDOUT, error: STDERR, input: STDIN) +end diff --git a/fixtures/src_template__js_bun_bundler/expected/script/setup.cr b/fixtures/src_template__js_bun_bundler/expected/script/setup.cr new file mode 100644 index 00000000..401ae5dd --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/script/setup.cr @@ -0,0 +1,35 @@ +require "./helpers/*" + +notice "Running System Check" + +require "./system_check" + +print_done + +notice "Installing node dependencies" +run_command "yarn", "install", "--no-progress" + +notice "Compiling assets" +run_command "yarn", "dev" + +print_done + +notice "Installing shards" +run_command "shards", "install" + +if !File.exists?(".env") + notice "No .env found. Creating one." + File.touch ".env" + print_done +end + +notice "Setting up the database" + +run_command "lucky", "db.setup" + +notice "Seeding the database with required and sample records" +run_command "lucky", "db.seed.required_data" +run_command "lucky", "db.seed.sample_data" + +print_done +notice "Run 'lucky dev' to start the app" diff --git a/fixtures/src_template__js_bun_bundler/expected/script/system_check.cr b/fixtures/src_template__js_bun_bundler/expected/script/system_check.cr new file mode 100644 index 00000000..c56ce3e9 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/script/system_check.cr @@ -0,0 +1,21 @@ +require "./helpers/*" + +# Use this script to check the system for required tools and process that your app needs. +# A few helper functions are provided to keep the code simple. See the +# script/helpers/function_helpers.cr file for more examples. +# +# A few examples you might use here: +# * 'lucky db.verify_connection' to test postgres can be connected +# * Checking that elasticsearch, redis, or postgres is installed and/or booted +# * Note: Booting additional processes for things like mail, background jobs, etc... +# should go in your Procfile.dev. + +if command_not_found "yarn" + print_error "Yarn is not installed\n See https://yarnpkg.com/lang/en/docs/install/ for install instructions." +end + +# CUSTOM PRE-BOOT CHECKS +# example: +# if command_not_running "redis-cli", "ping" +# print_error "Redis is not running." +# end diff --git a/fixtures/src_template__js_bun_bundler/expected/spec/setup/clean_database.cr b/fixtures/src_template__js_bun_bundler/expected/spec/setup/clean_database.cr new file mode 100644 index 00000000..a1bc631c --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/spec/setup/clean_database.cr @@ -0,0 +1,3 @@ +Spec.before_each do + AppDatabase.truncate +end diff --git a/fixtures/src_template__js_bun_bundler/expected/spec/setup/reset_emails.cr b/fixtures/src_template__js_bun_bundler/expected/spec/setup/reset_emails.cr new file mode 100644 index 00000000..140ab416 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/spec/setup/reset_emails.cr @@ -0,0 +1,3 @@ +Spec.before_each do + Carbon::DevAdapter.reset +end diff --git a/fixtures/src_template__js_bun_bundler/expected/spec/setup/setup_database.cr b/fixtures/src_template__js_bun_bundler/expected/spec/setup/setup_database.cr new file mode 100644 index 00000000..393c6da3 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/spec/setup/setup_database.cr @@ -0,0 +1,2 @@ +Db::Create.new(quiet: true).call +Db::Migrate.new(quiet: true).call diff --git a/fixtures/src_template__js_bun_bundler/expected/spec/setup/start_app_server.cr b/fixtures/src_template__js_bun_bundler/expected/spec/setup/start_app_server.cr new file mode 100644 index 00000000..ff0bfeeb --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/spec/setup/start_app_server.cr @@ -0,0 +1,10 @@ +app_server = AppServer.new + +spawn do + app_server.listen +end + +Spec.after_suite do + LuckyFlow.shutdown + app_server.close +end diff --git a/fixtures/src_template__js_bun_bundler/expected/spec/spec_helper.cr b/fixtures/src_template__js_bun_bundler/expected/spec/spec_helper.cr new file mode 100644 index 00000000..187f4753 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/spec/spec_helper.cr @@ -0,0 +1,24 @@ +ENV["LUCKY_ENV"] = "test" +ENV["DEV_PORT"] = "5001" +require "spec" +require "lucky_flow" +require "lucky_flow/ext/lucky" +require "lucky_flow/ext/avram" +require "../src/app" +require "./support/flows/base_flow" +require "./support/**" +require "../db/migrations/**" + +# Add/modify files in spec/setup to start/configure programs or run hooks +# +# By default there are scripts for setting up and cleaning the database, +# configuring LuckyFlow, starting the app server, etc. +require "./setup/**" + +include Carbon::Expectations +include Lucky::RequestExpectations +include LuckyFlow::Expectations + +Avram::Migrator::Runner.new.ensure_migrated! +Avram::SchemaEnforcer.ensure_correct_column_mappings! +Habitat.raise_if_missing_settings! diff --git a/fixtures/src_template__js_bun_bundler/expected/spec/support/.keep b/fixtures/src_template__js_bun_bundler/expected/spec/support/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__js_bun_bundler/expected/spec/support/api_client.cr b/fixtures/src_template__js_bun_bundler/expected/spec/support/api_client.cr new file mode 100644 index 00000000..ef251251 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/spec/support/api_client.cr @@ -0,0 +1,8 @@ +class ApiClient < Lucky::BaseHTTPClient + app AppServer.new + + def initialize + super + headers("Content-Type": "application/json") + end +end diff --git a/fixtures/src_template__js_bun_bundler/expected/spec/support/factories/.keep b/fixtures/src_template__js_bun_bundler/expected/spec/support/factories/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__js_bun_bundler/expected/src/actions/api_action.cr b/fixtures/src_template__js_bun_bundler/expected/src/actions/api_action.cr new file mode 100644 index 00000000..fac02c8b --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/actions/api_action.cr @@ -0,0 +1,11 @@ +# Include modules and add methods that are for all API requests +abstract class ApiAction < Lucky::Action + # APIs typically do not need to send cookie/session data. + # Remove this line if you want to send cookies in the response header. + disable_cookies + accepted_formats [:json] + + # By default all actions are required to use underscores to separate words. + # Add 'include Lucky::SkipRouteStyleCheck' to your actions if you wish to ignore this check for specific routes. + include Lucky::EnforceUnderscoredRoute +end diff --git a/fixtures/src_template__js_bun_bundler/expected/src/actions/errors/show.cr b/fixtures/src_template__js_bun_bundler/expected/src/actions/errors/show.cr new file mode 100644 index 00000000..d01ed541 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/actions/errors/show.cr @@ -0,0 +1,63 @@ +# This class handles error responses and reporting. +# +# https://luckyframework.org/guides/http-and-routing/error-handling +class Errors::Show < Lucky::ErrorAction + DEFAULT_MESSAGE = "Something went wrong." + default_format :html + dont_report [Lucky::RouteNotFoundError, Avram::RecordNotFoundError] + + def render(error : Lucky::RouteNotFoundError | Avram::RecordNotFoundError) + if html? + error_html "Sorry, we couldn't find that page.", status: 404 + else + error_json "Not found", status: 404 + end + end + + # When the request is JSON and an InvalidOperationError is raised, show a + # helpful error with the param that is invalid, and what was wrong with it. + def render(error : Avram::InvalidOperationError) + if html? + error_html DEFAULT_MESSAGE, status: 500 + else + error_json \ + message: error.renderable_message, + details: error.renderable_details, + param: error.invalid_attribute_name, + status: 400 + end + end + + # Always keep this below other 'render' methods or it may override your + # custom 'render' methods. + def render(error : Lucky::RenderableError) + if html? + error_html DEFAULT_MESSAGE, status: error.renderable_status + else + error_json error.renderable_message, status: error.renderable_status + end + end + + # If none of the 'render' methods return a response for the raised Exception, + # Lucky will use this method. + def default_render(error : Exception) : Lucky::Response + if html? + error_html DEFAULT_MESSAGE, status: 500 + else + error_json DEFAULT_MESSAGE, status: 500 + end + end + + private def error_html(message : String, status : Int) + context.response.status_code = status + html_with_status Errors::ShowPage, status, message: message, status_code: status + end + + private def error_json(message : String, status : Int, details = nil, param = nil) + json ErrorSerializer.new(message: message, details: details, param: param), status: status + end + + private def report(error : Exception) : Nil + # Send to Rollbar, send an email, etc. + end +end diff --git a/fixtures/src_template__js_bun_bundler/expected/src/actions/home/index.cr b/fixtures/src_template__js_bun_bundler/expected/src/actions/home/index.cr new file mode 100644 index 00000000..2d39a100 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/actions/home/index.cr @@ -0,0 +1,5 @@ +class Home::Index < BrowserAction + get "/" do + html Lucky::WelcomePage + end +end diff --git a/fixtures/src_template__js_bun_bundler/expected/src/actions/mixins/.keep b/fixtures/src_template__js_bun_bundler/expected/src/actions/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__js_bun_bundler/expected/src/app.cr b/fixtures/src_template__js_bun_bundler/expected/src/app.cr new file mode 100644 index 00000000..9d3ee3c7 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/app.cr @@ -0,0 +1,26 @@ +require "./shards" + +# Load the asset manifest +Lucky::AssetHelpers.load_manifest "public/mix-manifest.json" + +require "../config/server" +require "./app_database" +require "../config/**" +require "./models/base_model" +require "./models/mixins/**" +require "./models/**" +require "./queries/mixins/**" +require "./queries/**" +require "./operations/mixins/**" +require "./operations/**" +require "./serializers/base_serializer" +require "./serializers/**" +require "./emails/base_email" +require "./emails/**" +require "./actions/mixins/**" +require "./actions/**" +require "./components/base_component" +require "./components/**" +require "./pages/**" +require "../db/migrations/**" +require "./app_server" diff --git a/fixtures/src_template__js_bun_bundler/expected/src/app_database.cr b/fixtures/src_template__js_bun_bundler/expected/src/app_database.cr new file mode 100644 index 00000000..0efd4f50 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/app_database.cr @@ -0,0 +1,2 @@ +class AppDatabase < Avram::Database +end diff --git a/fixtures/src_template__js_bun_bundler/expected/src/app_server.cr b/fixtures/src_template__js_bun_bundler/expected/src/app_server.cr new file mode 100644 index 00000000..8ec16c3c --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/app_server.cr @@ -0,0 +1,26 @@ +class AppServer < Lucky::BaseAppServer + # Learn about middleware with HTTP::Handlers: + # https://luckyframework.org/guides/http-and-routing/http-handlers + def middleware : Array(HTTP::Handler) + [ + Lucky::RequestIdHandler.new, + Lucky::ForceSSLHandler.new, + Lucky::HttpMethodOverrideHandler.new, + Lucky::LogHandler.new, + Lucky::ErrorHandler.new(action: Errors::Show), + Lucky::RemoteIpHandler.new, + Lucky::RouteHandler.new, + Lucky::StaticCompressionHandler.new("./public", file_ext: "gz", content_encoding: "gzip"), + Lucky::StaticFileHandler.new("./public", fallthrough: false, directory_listing: false), + Lucky::RouteNotFoundHandler.new, + ] of HTTP::Handler + end + + def protocol + "http" + end + + def listen + server.listen(host, port, reuse_port: false) + end +end diff --git a/fixtures/src_template__js_bun_bundler/expected/src/emails/base_email.cr b/fixtures/src_template__js_bun_bundler/expected/src/emails/base_email.cr new file mode 100644 index 00000000..656f4f11 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/emails/base_email.cr @@ -0,0 +1,15 @@ +# Learn about sending emails +# https://luckyframework.org/guides/emails/sending-emails-with-carbon +abstract class BaseEmail < Carbon::Email + # You can add defaults using the 'inherited' hook + # + # Example: + # + # macro inherited + # from default_from + # end + # + # def default_from + # Carbon::Address.new("support@app.com") + # end +end diff --git a/fixtures/src_template__js_bun_bundler/expected/src/models/base_model.cr b/fixtures/src_template__js_bun_bundler/expected/src/models/base_model.cr new file mode 100644 index 00000000..6bafeb84 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/models/base_model.cr @@ -0,0 +1,5 @@ +abstract class BaseModel < Avram::Model + def self.database : Avram::Database.class + AppDatabase + end +end diff --git a/fixtures/src_template__js_bun_bundler/expected/src/models/mixins/.keep b/fixtures/src_template__js_bun_bundler/expected/src/models/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__js_bun_bundler/expected/src/operations/.keep b/fixtures/src_template__js_bun_bundler/expected/src/operations/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__js_bun_bundler/expected/src/operations/mixins/.keep b/fixtures/src_template__js_bun_bundler/expected/src/operations/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__js_bun_bundler/expected/src/queries/.keep b/fixtures/src_template__js_bun_bundler/expected/src/queries/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__js_bun_bundler/expected/src/queries/mixins/.keep b/fixtures/src_template__js_bun_bundler/expected/src/queries/mixins/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__js_bun_bundler/expected/src/serializers/.keep b/fixtures/src_template__js_bun_bundler/expected/src/serializers/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__js_bun_bundler/expected/src/serializers/base_serializer.cr b/fixtures/src_template__js_bun_bundler/expected/src/serializers/base_serializer.cr new file mode 100644 index 00000000..1702f271 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/serializers/base_serializer.cr @@ -0,0 +1,9 @@ +abstract class BaseSerializer + include Lucky::Serializable + + def self.for_collection(collection : Enumerable, *args, **named_args) : Array(self) + collection.map do |object| + new(object, *args, **named_args) + end + end +end diff --git a/fixtures/src_template__js_bun_bundler/expected/src/serializers/error_serializer.cr b/fixtures/src_template__js_bun_bundler/expected/src/serializers/error_serializer.cr new file mode 100644 index 00000000..b7b55283 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/serializers/error_serializer.cr @@ -0,0 +1,14 @@ +# This is the default error serializer generated by Lucky. +# Feel free to customize it in any way you like. +class ErrorSerializer < BaseSerializer + def initialize( + @message : String, + @details : String? = nil, + @param : String? = nil, # so you can track which param (if any) caused the problem + ) + end + + def render + {message: @message, param: @param, details: @details} + end +end diff --git a/fixtures/src_template__js_bun_bundler/expected/src/shards.cr b/fixtures/src_template__js_bun_bundler/expected/src/shards.cr new file mode 100644 index 00000000..1afc72cb --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/shards.cr @@ -0,0 +1,8 @@ +# Load .env file before any other config or app code +require "lucky_env" +LuckyEnv.load?(".env") + +# Require your shards here +require "lucky" +require "avram/lucky" +require "carbon" diff --git a/fixtures/src_template__js_bun_bundler/expected/src/start_server.cr b/fixtures/src_template__js_bun_bundler/expected/src/start_server.cr new file mode 100644 index 00000000..9df5d1fd --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/start_server.cr @@ -0,0 +1,16 @@ +require "./app" + +Habitat.raise_if_missing_settings! + +if LuckyEnv.development? + Avram::Migrator::Runner.new.ensure_migrated! + Avram::SchemaEnforcer.ensure_correct_column_mappings! +end + +app_server = AppServer.new + +Signal::INT.trap do + app_server.close +end + +app_server.listen diff --git a/fixtures/src_template__js_bun_bundler/expected/src/test_project.cr b/fixtures/src_template__js_bun_bundler/expected/src/test_project.cr new file mode 100644 index 00000000..68e1a8d2 --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/src/test_project.cr @@ -0,0 +1,6 @@ +# Typically you will not use or modify this file. 'shards build' and some +# other crystal tools will sometimes use this. +# +# When this file is compiled/run it will require and run 'start_server', +# which as its name implies will start the server for you app. +require "./start_server" diff --git a/fixtures/src_template__js_bun_bundler/expected/tasks.cr b/fixtures/src_template__js_bun_bundler/expected/tasks.cr new file mode 100644 index 00000000..5a892d4d --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/tasks.cr @@ -0,0 +1,25 @@ +# This file loads your app and all your tasks when running 'lucky' +# +# Run 'lucky --help' to see all available tasks. +# +# Learn to create your own tasks: +# https://luckyframework.org/guides/command-line-tasks/custom-tasks + +# See `LuckyEnv#task?` +ENV["LUCKY_TASK"] = "true" + +# Load Lucky and the app (actions, models, etc.) +require "./src/app" +require "lucky_task" + +# You can add your own tasks here in the ./tasks folder +require "./tasks/**" + +# Load migrations +require "./db/migrations/**" + +# Load Lucky tasks (dev, routes, etc.) +require "lucky/tasks/**" +require "avram/lucky/tasks" + +LuckyTask::Runner.run diff --git a/fixtures/src_template__js_bun_bundler/expected/tasks/.keep b/fixtures/src_template__js_bun_bundler/expected/tasks/.keep new file mode 100644 index 00000000..e69de29b diff --git a/fixtures/src_template__js_bun_bundler/expected/tasks/db/seed/required_data.cr b/fixtures/src_template__js_bun_bundler/expected/tasks/db/seed/required_data.cr new file mode 100644 index 00000000..d866040f --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/tasks/db/seed/required_data.cr @@ -0,0 +1,30 @@ +require "../../../spec/support/factories/**" + +# Add seeds here that are *required* for your app to work. +# For example, you might need at least one admin user or you might need at least +# one category for your blog posts for the app to work. +# +# Use `Db::Seed::SampleData` if your only want to add sample data helpful for +# development. +class Db::Seed::RequiredData < LuckyTask::Task + summary "Add database records required for the app to work" + + def call + # Using a Avram::Factory: + # + # Use the defaults, but override just the email + # UserFactory.create &.email("me@example.com") + + # Using a SaveOperation: + # + # SaveUser.create!(email: "me@example.com", name: "Jane") + # + # You likely want to be able to run this file more than once. To do that, + # only create the record if it doesn't exist yet: + # + # unless UserQuery.new.email("me@example.com").first? + # SaveUser.create!(email: "me@example.com", name: "Jane") + # end + puts "Done adding required data" + end +end diff --git a/fixtures/src_template__js_bun_bundler/expected/tasks/db/seed/sample_data.cr b/fixtures/src_template__js_bun_bundler/expected/tasks/db/seed/sample_data.cr new file mode 100644 index 00000000..231d7e8d --- /dev/null +++ b/fixtures/src_template__js_bun_bundler/expected/tasks/db/seed/sample_data.cr @@ -0,0 +1,30 @@ +require "../../../spec/support/factories/**" + +# Add sample data helpful for development, e.g. (fake users, blog posts, etc.) +# +# Use `Db::Seed::RequiredData` if you need to create data *required* for your +# app to work. +class Db::Seed::SampleData < LuckyTask::Task + summary "Add sample database records helpful for development" + + def call + # Using an Avram::Factory: + # + # Use the defaults, but override just the email + # UserFactory.create &.email("me@example.com") + + # Using a SaveOperation: + # ``` + # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123") + # ``` + # + # You likely want to be able to run this file more than once. To do that, + # only create the record if it doesn't exist yet: + # ``` + # if UserQuery.new.email("me@example.com").none? + # SignUpUser.create!(email: "me@example.com", password: "test123", password_confirmation: "test123") + # end + # ``` + puts "Done adding sample data" + end +end diff --git a/fixtures/src_template__sec_tester/expected/docker/dev_entrypoint.sh b/fixtures/src_template__sec_tester/expected/docker/dev_entrypoint.sh index d16ef6ab..084fb44b 100755 --- a/fixtures/src_template__sec_tester/expected/docker/dev_entrypoint.sh +++ b/fixtures/src_template__sec_tester/expected/docker/dev_entrypoint.sh @@ -27,8 +27,10 @@ if ! [ -d bin ] ; then echo 'Creating bin directory' mkdir bin fi -echo 'Installing npm packages...' + +echo 'Installing yarn packages...' yarn install + if ! shards check ; then echo 'Installing shards...' shards install diff --git a/spec/unit/browser_src_template_spec.cr b/spec/unit/browser_src_template_spec.cr index 0ba2281a..c62986e9 100644 --- a/spec/unit/browser_src_template_spec.cr +++ b/spec/unit/browser_src_template_spec.cr @@ -12,13 +12,13 @@ describe BrowserSrcTemplate do it "generates browser src template" do generate_snapshot("browser_src_template") do - BrowserSrcTemplate.new(generate_auth: true) + BrowserSrcTemplate.new(generate_auth: true, js_bundle_system: "yarn") end end it "generates browser src template without generate auth" do generate_snapshot("browser_src_template__generate_auth") do - BrowserSrcTemplate.new(generate_auth: false) + BrowserSrcTemplate.new(generate_auth: false, js_bundle_system: "yarn") end end end diff --git a/spec/unit/src_template_spec.cr b/spec/unit/src_template_spec.cr index 0ba63efb..92396a17 100644 --- a/spec/unit/src_template_spec.cr +++ b/spec/unit/src_template_spec.cr @@ -16,7 +16,8 @@ describe SrcTemplate do "test-project", generate_auth: true, api_only: true, - with_sec_tester: true + with_sec_tester: true, + js_bundle_system: "yarn", ).tap do |instance| instance.secret_key_base = "1234567890" instance.crystal_version = "1.16.1" @@ -31,7 +32,8 @@ describe SrcTemplate do "test-project", generate_auth: true, api_only: false, - with_sec_tester: false + with_sec_tester: false, + js_bundle_system: "yarn", ).tap do |instance| instance.secret_key_base = "1234567890" instance.crystal_version = "1.16.1" @@ -46,7 +48,8 @@ describe SrcTemplate do "test-project", generate_auth: false, api_only: true, - with_sec_tester: false + with_sec_tester: false, + js_bundle_system: "yarn", ).tap do |instance| instance.secret_key_base = "1234567890" instance.crystal_version = "1.16.1" @@ -61,7 +64,24 @@ describe SrcTemplate do "test-project", generate_auth: false, api_only: false, - with_sec_tester: true + with_sec_tester: true, + js_bundle_system: "yarn", + ).tap do |instance| + instance.secret_key_base = "1234567890" + instance.crystal_version = "1.16.1" + instance.lucky_cli_version = "1.3.0" + end + end + end + + it "generates src template with js bundler bun option" do + generate_snapshot("src_template__js_bun_bundler") do + SrcTemplate.new( + "test-project", + generate_auth: false, + api_only: false, + with_sec_tester: true, + js_bundle_system: "bun", ).tap do |instance| instance.secret_key_base = "1234567890" instance.crystal_version = "1.16.1" diff --git a/src/browser_app_skeleton/package.json.ecr b/src/browser_app_skeleton/package.json.ecr index 659331fc..15ac69cb 100644 --- a/src/browser_app_skeleton/package.json.ecr +++ b/src/browser_app_skeleton/package.json.ecr @@ -6,15 +6,15 @@ "modern-normalize": "^2.0.0" }, "scripts": { - "heroku-postbuild": "yarn prod", - "dev": "yarn run mix", - "watch": "yarn run mix watch", - "prod": "yarn run mix --production" + "heroku-postbuild": "<%= js_run_command %> prod", + "dev": "<%= js_run_command %> mix", + "watch": "<%= js_run_command %> mix watch", + "prod": "<%= js_run_command %> mix --production" }, "devDependencies": { "@babel/compat-data": "^7.23.5", - "browser-sync": "^2.29.3", - "compression-webpack-plugin": "^10.0.0", + "browser-sync": "^3.0.4", + "compression-webpack-plugin": "^11.0.0", "laravel-mix": "^6.0.49", "postcss": "^8.4.32", "resolve-url-loader": "^5.0.0", diff --git a/src/generators/web.cr b/src/generators/web.cr index a0eecc06..f8778069 100644 --- a/src/generators/web.cr +++ b/src/generators/web.cr @@ -4,7 +4,7 @@ class LuckyCli::Generators::Web include LuckyCli::GeneratorHelpers getter project_name : String - getter? api_only, generate_auth, with_sec_tester + getter? api_only, generate_auth, with_sec_tester, js_bundle_system private getter? default_directory : Bool private getter full_project_directory : String @@ -13,6 +13,7 @@ class LuckyCli::Generators::Web @api_only : Bool, @generate_auth : Bool, @with_sec_tester : Bool = false, + @js_bundle_system : String = "yarn", project_directory : String = ".", ) @full_project_directory = File.expand_path(project_directory) @@ -120,14 +121,17 @@ class LuckyCli::Generators::Web project_name, generate_auth: generate_auth?, api_only: api_only?, - with_sec_tester: with_sec_tester? + with_sec_tester: with_sec_tester?, + js_bundle_system: js_bundle_system?, ) .render(Path[project_dir]) end private def add_browser_app_structure_to_src - BrowserSrcTemplate.new(generate_auth: generate_auth?) - .render(Path[project_dir]) + BrowserSrcTemplate.new( + generate_auth: generate_auth?, + js_bundle_system: js_bundle_system?, + ).render(Path[project_dir]) end private def add_base_auth_to_src diff --git a/src/lucky_cli/browser_src_template.cr b/src/lucky_cli/browser_src_template.cr index d7156b85..eb182b9d 100644 --- a/src/lucky_cli/browser_src_template.cr +++ b/src/lucky_cli/browser_src_template.cr @@ -3,7 +3,23 @@ require "random/secure" class BrowserSrcTemplate getter? generate_auth - def initialize(@generate_auth : Bool) + def initialize( + @generate_auth : Bool, + @js_bundle_system : String, + ) + end + + def js_run_command : String + case @js_bundle_system + when "yarn" + "yarn run" + when "npm" + "npm run" + when "bun" + "bun run" + else + raise "Unknown JavaScript bundle system: #{@js_bundle_system}. Supported systems are: yarn, npm, bun." + end end def render(path : Path) diff --git a/src/lucky_cli/src_template.cr b/src/lucky_cli/src_template.cr index d3db38fa..ee6c0bf0 100644 --- a/src/lucky_cli/src_template.cr +++ b/src/lucky_cli/src_template.cr @@ -2,7 +2,7 @@ require "random/secure" class SrcTemplate getter project_name - getter? api_only, generate_auth, with_sec_tester + getter? api_only, generate_auth, with_sec_tester, js_bundle_system getter crystal_project_name : String property(secret_key_base) { Random::Secure.base64(32) } property(crystal_version) { Crystal::VERSION } @@ -13,6 +13,7 @@ class SrcTemplate @generate_auth : Bool, @api_only : Bool, @with_sec_tester : Bool, + @js_bundle_system : String, ) @crystal_project_name = @project_name.gsub("-", "_") end @@ -25,6 +26,19 @@ class SrcTemplate !api_only? end + def js_install_command : String + case js_bundle_system? + when "yarn" + "yarn install" + when "npm" + "npm install" + when "bun" + "bun install" + else + raise "Unknown JavaScript bundle system: #{js_bundle_system?}. Supported systems are: yarn, npm, bun." + end + end + def render(path : Path) LuckyTemplate.write!(path, template_folder) end diff --git a/src/lucky_cli/wizard/labeled_choice_question.cr b/src/lucky_cli/wizard/labeled_choice_question.cr new file mode 100644 index 00000000..853d0ce2 --- /dev/null +++ b/src/lucky_cli/wizard/labeled_choice_question.cr @@ -0,0 +1,22 @@ +class LabeledChoiceQuestion + getter question_text : String + getter choices : Array(String) + + def initialize(@question_text : String, @choices : Array(String)) + end + + def self.ask(*args, **named_args) : String + new(*args, **named_args).ask + end + + def ask : String + print question_text.colorize.bold.to_s + " (#{choices.join("/")})? ".colorize.green.bold.to_s + answer = gets.try(&.strip.gsub("'", "").downcase) + if choices.includes?(answer) + answer.to_s + else + puts "\nMust be one of: #{choices.join(", ")}".colorize.red + ask + end + end +end diff --git a/src/lucky_cli/wizard/web.cr b/src/lucky_cli/wizard/web.cr index cf5d8a86..71a18597 100644 --- a/src/lucky_cli/wizard/web.cr +++ b/src/lucky_cli/wizard/web.cr @@ -12,6 +12,7 @@ class LuckyCli::Wizard::Web print_welcome_to_lucky project_name = ask_for_project_name api_only = ask_about_api_only + js_bundle_system = ask_about_js_bundle_system(api_only) generate_auth = ask_about_auth(api_only) puts "\n" @@ -21,7 +22,8 @@ class LuckyCli::Wizard::Web LuckyCli::Generators::Web.run( project_name: project_name, api_only: api_only, - generate_auth: generate_auth + generate_auth: generate_auth, + js_bundle_system: js_bundle_system, ) end @@ -56,6 +58,33 @@ class LuckyCli::Wizard::Web ) end + private def ask_about_js_bundle_system(api_only : Bool) : String + if api_only + puts <<-HELP_TEXT.colorize.dim + + Lucky can be generated with no JavaScript bundle system + + ● No JavaScript files or Webpack configuration + ● No CSS files or PostCSS configuration + + HELP_TEXT + return "none" + end + + puts <<-HELP_TEXT.colorize.dim + + Lucky can be generated with a JavaScript bundle system + + ● Bundles JavaScript and CSS files + ● Supports NPM, Yarn, and Bun + + HELP_TEXT + LabeledChoiceQuestion.ask( + "Which JavaScript bundle system do you want to use?", + choices: ["npm", "yarn", "bun", "none"] + ) + end + private def ask_about_auth(api_only : Bool) : Bool if api_only puts <<-HELP_TEXT.colorize.dim diff --git a/src/web_app_skeleton/Procfile.dev.ecr b/src/web_app_skeleton/Procfile.dev.ecr index 53563d99..85cc616e 100644 --- a/src/web_app_skeleton/Procfile.dev.ecr +++ b/src/web_app_skeleton/Procfile.dev.ecr @@ -3,5 +3,9 @@ system_check: crystal script/system_check.cr web: lucky watch <%- else -%> web: lucky watch --reload-browser +<%- if js_bundle_system? == "yarn" -%> assets: yarn watch +<%- elsif js_bundle_system? == "bun" -%> +assets: bun watch +<%- end -%> <%- end -%> diff --git a/src/web_app_skeleton/docker/dev_entrypoint.sh.ecr b/src/web_app_skeleton/docker/dev_entrypoint.sh.ecr index 94d15ffd..36db0e7f 100755 --- a/src/web_app_skeleton/docker/dev_entrypoint.sh.ecr +++ b/src/web_app_skeleton/docker/dev_entrypoint.sh.ecr @@ -28,8 +28,10 @@ if ! [ -d bin ] ; then mkdir bin fi <%- if browser? -%> -echo 'Installing npm packages...' -yarn install + +echo 'Installing <%= js_bundle_system? %> packages...' +<%= js_install_command %> + <%- end -%> if ! shards check ; then echo 'Installing shards...' diff --git a/src/web_app_skeleton/docker/development.dockerfile.ecr b/src/web_app_skeleton/docker/development.dockerfile.ecr index 13abd8a5..c8668c14 100644 --- a/src/web_app_skeleton/docker/development.dockerfile.ecr +++ b/src/web_app_skeleton/docker/development.dockerfile.ecr @@ -5,7 +5,7 @@ RUN apt-get update && \ apt-get install -y wget <%- if browser? -%> # Add the nodesource ppa to apt. Update this to change the nodejs version. -RUN wget https://deb.nodesource.com/setup_16.x -O- | bash +RUN wget https://deb.nodesource.com/setup_20.x -O- | bash # Apt installs: # - nodejs (from above ppa) is required for front-end apps. @@ -16,18 +16,16 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* # NPM global installs: +<%- if js_bundle_system? == "yarn" -%> # - Yarn is the default package manager for the node component of a lucky # browser app. # - Mix is the default asset compiler. RUN npm install -g yarn mix -<%- else -%> - -# Apt installs: -# - Postgres cli tools are required for lucky-cli. -# - tmux is required for the Overmind process manager. -RUN apt-get update && \ - apt-get install -y postgresql-client tmux && \ - rm -rf /var/lib/apt/lists/* +<%- elsif js_bundle_system? == "bun" -%> +# NPM global installs: +# - Bun is the default js package manager. +RUN npm install -g bun +<%- end -%> <%- end -%> # Install lucky cli diff --git a/src/web_app_skeleton/script/setup.cr.ecr b/src/web_app_skeleton/script/setup.cr.ecr index a83d4ce2..ca9bc80b 100755 --- a/src/web_app_skeleton/script/setup.cr.ecr +++ b/src/web_app_skeleton/script/setup.cr.ecr @@ -7,11 +7,19 @@ require "./system_check" print_done <%- if browser? -%> +<%- if js_bundle_system? == "yarn" -%> notice "Installing node dependencies" run_command "yarn", "install", "--no-progress" notice "Compiling assets" run_command "yarn", "dev" +<%- elsif js_bundle_system? == "bun" -%> +notice "Installing node dependencies" +run_command "bun", "install", "--no-progress" + +notice "Compiling assets" +run_command "bun", "dev" +<%- end -%> print_done <%- end -%> diff --git a/src/web_app_skeleton/script/system_check.cr.ecr b/src/web_app_skeleton/script/system_check.cr.ecr index 39fbd24d..cb24cc52 100755 --- a/src/web_app_skeleton/script/system_check.cr.ecr +++ b/src/web_app_skeleton/script/system_check.cr.ecr @@ -11,9 +11,19 @@ require "./helpers/*" # should go in your Procfile.dev. <%- if browser? -%> +<%- if js_bundle_system? == "npm" -%> +if command_not_found "yarn" + print_error "Npm is not installed\n See https://yarnpkg.com/lang/en/docs/install/ for install instructions." +end +<%- elsif js_bundle_system? == "yarn" -%> if command_not_found "yarn" print_error "Yarn is not installed\n See https://yarnpkg.com/lang/en/docs/install/ for install instructions." end +<%- elsif js_bundle_system? == "bun" -%> +if command_not_found "bun" + print_error "Bun is not installed\n See https://bun.sh/docs/installation for install instructions." +end +<%- end -%> <%- end -%> # CUSTOM PRE-BOOT CHECKS