diff --git a/tools/build/base/.gitignore b/tools/build/base/.gitignore index 60b7244c59..1df103ad6e 100755 --- a/tools/build/base/.gitignore +++ b/tools/build/base/.gitignore @@ -1,2 +1,8 @@ +# Installed framework core + packages (pulled by `box install` / `box update`). /vendor/*/ -/db/** + +# SQLite/H2 database files created at runtime. The db/ directory itself MUST +# ship (it carries db/.keep) so JDBC drivers have a parent dir to write into — +# only the generated database files are ignored. +/db/*.sqlite +/db/*.db diff --git a/tools/build/base/README.md b/tools/build/base/README.md index d5d3802d1d..a3d7642746 100644 --- a/tools/build/base/README.md +++ b/tools/build/base/README.md @@ -1,22 +1,31 @@ # wheels-base-template -This is a blank application written in Wheels.dev +A blank, ready-to-boot [Wheels](https://wheels.dev) application. + +> **Recommended path:** install the Wheels CLI (Homebrew / Scoop / apt / yum) +> and run `wheels new myapp`. The CLI substitutes app settings for you and is +> the canonical, fully documented workflow. This ForgeBox package exists for +> CommandBox users who prefer `box install`; it ships pre-substituted working +> defaults so it boots with no manual edits. ## As an Application -As an application, this is a starting point for a modern Wheels application with Bootstrap integration. +This is a starting point for a modern Wheels application. It uses the same +scaffold `wheels new` produces, so a `box install`-born app and a CLI-born app +are structurally identical. ## As a ForgeBox Package -As a ForgeBox package there is some interesting things going on here. Although this package doesn't contain much custom code, it does have a dependency which pulls in the core folder the framework needs to function. This folder is pulled in via this dependency: +This package contains the app skeleton only. The framework core is pulled in via +a dependency on `wheels-core`: ``` -"Dependencies":{ - "wheels-core":"^3.1.0" +"dependencies":{ + "wheels-core":"4.0.3" } ``` -The core files are put into the `vendor/wheels/` folder according to these settings. +The core files land in `vendor/wheels/` per these settings: ``` "installPaths":{ @@ -26,7 +35,8 @@ The core files are put into the `vendor/wheels/` folder according to these setti ## To Install -To install this package you'll need to have a running CommandBox installation. Then you can install this package with the following: +You'll need a running [CommandBox](https://www.ortussolutions.com/products/commandbox) +installation. Then install this package into an empty directory: ``` box @@ -34,8 +44,18 @@ mkdir myapp --cd install wheels-base-template ``` -This could be shortened to a single command run in an empty directory: +This can be shortened to a single command run in an empty directory: ``` box install wheels-base-template ``` + +Then boot the server (CommandBox reads the bundled `server.json`): + +``` +box server start +``` + +`GET /` renders the Wheels welcome page. To reload after config changes, hit +`?reload=true` (the reload password defaults to empty in development — set +`reloadPassword` in `config/settings.cfm` for other environments). diff --git a/tools/build/base/box.json b/tools/build/base/box.json index 148a5daf45..624d58af82 100644 --- a/tools/build/base/box.json +++ b/tools/build/base/box.json @@ -11,7 +11,7 @@ "mvc", "rails", "wheels", - "wheels", + "cfml", "core" ], "homepage":"https://wheels.dev/", @@ -48,9 +48,9 @@ } ], "scripts":{ - "postInstall":"pathExists .env || cp env.example .env && mv env.example .env.example", - "format":"cfformat run config/,controllers/,events/,files/,global/,miscellaneous/,models/,tests/,views/,*.cfc --overwrite", - "format:check":"cfformat check config/,controllers/,events/,files/,global/,miscellaneous/,models/,tests/,views/,*.cfc", - "format:watch":"cfformat watch path='config/,controllers/,events/,files/,global/,miscellaneous/,models/,tests/,views/,*.cfc' settingsPath='.cfformat.json'" + "postInstall":"pathExists .env || cp env.example .env", + "format":"cfformat run config/,app/,tests/,*.cfc --overwrite", + "format:check":"cfformat check config/,app/,tests/,*.cfc", + "format:watch":"cfformat watch path='config/,app/,tests/,*.cfc' settingsPath='.cfformat.json'" } } diff --git a/tools/build/base/config/app.cfm b/tools/build/base/config/app.cfm deleted file mode 100644 index 9856f737a5..0000000000 --- a/tools/build/base/config/app.cfm +++ /dev/null @@ -1,12 +0,0 @@ - - /* - Use this file to set variables for the Application.cfc's "this" scope. - - Examples: - this.name = "MyAppName"; - this.sessionTimeout = CreateTimeSpan(0,0,5,0); - */ - - this.name = "|appName|"; - // CLI-Appends-Here - diff --git a/tools/build/base/config/settings.cfm b/tools/build/base/config/settings.cfm deleted file mode 100644 index d24f2a310f..0000000000 --- a/tools/build/base/config/settings.cfm +++ /dev/null @@ -1,29 +0,0 @@ - - /* - Use this file to configure your application. - You can also use the environment specific files (e.g. /config/production/settings.cfm) to override settings set here. - Don't forget to issue a reload request (e.g. reload=true) after making changes. - See https://wheels.dev/3.1.0/guides/working-with-wheels/configuration-and-defaults for more info. - */ - - /* - If you leave these settings commented out, wheels will set the data source name to the same name as the folder the application resides in. - */ - set(coreTestDataSourceName="|datasourceName|"); - set(dataSourceName="|datasourceName|"); - // set(dataSourceUserName=""); - // set(dataSourcePassword=""); - - /* - If you comment out the following line, wheels will try to determine the URL rewrite capabilities automatically. - The "URLRewriting" setting can bet set to "on", "partial" or "off". - To run with "partial" rewriting, the "cgi.path_info" variable needs to be supported by the web server. - To run with rewriting set to "on", you need to apply the necessary rewrite rules on the web server first. - */ - set(URLRewriting="On"); - - // Reload your application with ?reload=true&password=|reloadPassword| - set(reloadPassword="|reloadPassword|"); - - // CLI-Appends-Here - diff --git a/tools/build/base/server.json b/tools/build/base/server.json index f789b25ec3..248faab492 100644 --- a/tools/build/base/server.json +++ b/tools/build/base/server.json @@ -1,5 +1,5 @@ { - "name":"|appName|", + "name":"Wheels", "web":{ "host":"localhost", "webroot": "public", @@ -9,7 +9,7 @@ } }, "app":{ - "cfengine":"|cfmlEngine|", + "cfengine":"lucee", "libDirs":"app/lib" } } diff --git a/tools/build/scripts/prepare-base.sh b/tools/build/scripts/prepare-base.sh index 1c4b2252b8..613764fbac 100755 --- a/tools/build/scripts/prepare-base.sh +++ b/tools/build/scripts/prepare-base.sh @@ -4,6 +4,25 @@ set -e # Prepare script for Wheels Base Template (ForgeBox publishing) # This script prepares the directory structure without creating ZIP files # Usage: ./prepare-base.sh +# +# SINGLE SOURCE OF TRUTH (Peter, 2026-06-12): the published ForgeBox base +# template is regenerated from cli/lucli/templates/app/ — the canonical +# `wheels new` output — NOT the repo-root demo app. This keeps CommandBox-born +# apps byte-for-byte aligned with CLI-born apps and auto-drops repo-only demo +# cruft (app/jobs/ProcessOrdersJob.cfc, public/ApplicationProxy.cfc, +# public/index.bxm, the dev /cli + /modules mappings in public/Application.cfc). +# +# CommandBox-only build artifacts (server.json, box.json, README.md, the base +# .gitignore, .mcp.json, .opencode.json) are layered on top from +# tools/build/base/ — these are the CommandBox equivalents of what `wheels new` +# users get via lucee.json/rewrite.config/_env. +# +# The LuCLI template ships unsubstituted placeholders ({{appName}}, +# {{datasourceName}}, {{reloadPassword}}, {{luceeAdminPassword}}) that the +# `wheels new` installer fills in. `box install` has NO substitution step, so +# this script substitutes them to sane working defaults at BUILD TIME — the +# published artifact carries zero placeholders and `box server start` boots with +# no manual edits. VERSION=$1 BRANCH=$2 @@ -12,59 +31,153 @@ IS_PRERELEASE=$4 echo "Preparing Wheels Base Template v${VERSION} for ForgeBox publishing" +# Locate the repo root from this script's location so the build is independent +# of the current working directory. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)" +TEMPLATE_DIR="${REPO_ROOT}/cli/lucli/templates/app" + +# Build-time substitution defaults (the dev defaults `wheels new` would yield). +APP_NAME="Wheels" +CFML_ENGINE="lucee" +DATASOURCE_NAME="wheels" +RELOAD_PASSWORD="" +LUCEE_ADMIN_PASSWORD="" + # Setup directories BUILD_DIR="build-wheels-base" +if [ ! -d "${TEMPLATE_DIR}" ]; then + echo "ERROR: LuCLI app template not found at ${TEMPLATE_DIR}" + exit 1 +fi + # Cleanup and create directories rm -rf "${BUILD_DIR}" mkdir -p "${BUILD_DIR}" echo "Current Working Directory" pwd -echo "Contents of current directory" -ls -la +echo "Template source: ${TEMPLATE_DIR}" # Create build label file BUILD_LABEL="wheels-base-template-${VERSION}-$(date +%Y%m%d%H%M%S)" echo "Built on $(date)" > "${BUILD_DIR}/${BUILD_LABEL}" -# Copy app scaffold directories from root -echo "Copying app scaffold files..." -cp -r app "${BUILD_DIR}/" -cp -r config "${BUILD_DIR}/" -cp -r db "${BUILD_DIR}/" -cp -r plugins "${BUILD_DIR}/" -cp -r public "${BUILD_DIR}/" -cp -r tests "${BUILD_DIR}/" +# Copy the canonical app scaffold (app/config/db/public/tests/vendor) from the +# LuCLI template — the same tree `wheels new` produces. +echo "Copying app scaffold from LuCLI template..." +cp -r "${TEMPLATE_DIR}/app" "${BUILD_DIR}/" +cp -r "${TEMPLATE_DIR}/config" "${BUILD_DIR}/" +cp -r "${TEMPLATE_DIR}/db" "${BUILD_DIR}/" +cp -r "${TEMPLATE_DIR}/public" "${BUILD_DIR}/" +cp -r "${TEMPLATE_DIR}/tests" "${BUILD_DIR}/" +cp -r "${TEMPLATE_DIR}/vendor" "${BUILD_DIR}/" + +# The deprecated plugins/ system is no longer part of a fresh Wheels app; the +# LuCLI template carries app/plugins/ for modern (vendor/) packages instead. +# Keep a webroot-level plugins/ marker only if the template ever ships one. + +# Generate the default Main controller + index view. `wheels new` writes these +# post-copy (cli/lucli/Module.cfc) because they are app-specific starter content, +# NOT framework structure — so they don't live in the static template. The root +# route (config/routes.cfm) points to main##index, so without these GET / throws +# Wheels.ViewNotFound. Mirror the CLI exactly so a box-install app and a +# `wheels new` app render the same welcome page. +echo "Generating default Main controller and index view..." +mkdir -p "${BUILD_DIR}/app/views/main" +printf 'component extends="Controller" {\n\n\tfunction index() {\n\t\t// Default action\n\t}\n\n}\n' \ + > "${BUILD_DIR}/app/controllers/Main.cfc" +printf '

Welcome to %s

\n

Your Wheels application is running. Edit this file at app/views/main/index.cfm

\n' \ + "${APP_NAME}" > "${BUILD_DIR}/app/views/main/index.cfm" + +# Materialize directory-keeper markers so empty scaffold dirs survive packaging. +# CommandBox `package publish` keeps .keep / .gitkeep files, so no rename is +# needed — but normalize .gitkeep -> .keep is intentionally NOT done; both ship. +echo "Ensuring db/ ships with a keeper marker..." +touch "${BUILD_DIR}/db/.keep" + +# Translate the LuCLI underscore-prefixed dotfile templates into the CommandBox +# equivalents. _env becomes env.example (box.json's postInstall copies it to +# .env on first install). _gitignore is superseded by the CommandBox-specific +# tools/build/base/.gitignore copied below. +if [ -f "${TEMPLATE_DIR}/_env" ]; then + cp "${TEMPLATE_DIR}/_env" "${BUILD_DIR}/env.example" +fi + mkdir -p "${BUILD_DIR}/vendor" -cp vendor/wheels/.keep "${BUILD_DIR}/vendor/.keep" 2>/dev/null || touch "${BUILD_DIR}/vendor/.keep" +touch "${BUILD_DIR}/vendor/.keep" -# Copy AI documentation files +# Copy AI documentation files from the repo root. echo "Copying AI documentation..." -cp -r .ai "${BUILD_DIR}/" -cp CLAUDE.md "${BUILD_DIR}/" -cp -r .claude "${BUILD_DIR}/" -cp -r .opencode "${BUILD_DIR}/" 2>/dev/null || true +cp -r "${REPO_ROOT}/.ai" "${BUILD_DIR}/" +cp "${REPO_ROOT}/CLAUDE.md" "${BUILD_DIR}/" +cp -r "${REPO_ROOT}/.claude" "${BUILD_DIR}/" +cp -r "${REPO_ROOT}/.opencode" "${BUILD_DIR}/" 2>/dev/null || true # Apache 2.0 §4(d) requires NOTICE to propagate to derivatives. -cp LICENSE "${BUILD_DIR}/" -cp NOTICE "${BUILD_DIR}/" +cp "${REPO_ROOT}/LICENSE" "${BUILD_DIR}/" +cp "${REPO_ROOT}/NOTICE" "${BUILD_DIR}/" # Copy VS Code snippets echo "Copying VS Code snippets..." mkdir -p "${BUILD_DIR}/.vscode" -cp .vscode/wheels.code-snippets "${BUILD_DIR}/.vscode/" -cp .vscode/wheels-test.code-snippets "${BUILD_DIR}/.vscode/" - -# Copy template files, overwriting defaults -cp tools/build/base/.gitignore "${BUILD_DIR}/.gitignore" -cp tools/build/base/box.json "${BUILD_DIR}/box.json" -cp tools/build/base/README.md "${BUILD_DIR}/README.md" -cp tools/build/base/server.json "${BUILD_DIR}/server.json" -cp tools/build/base/config/app.cfm "${BUILD_DIR}/config/app.cfm" -cp tools/build/base/config/settings.cfm "${BUILD_DIR}/config/settings.cfm" - -# Copy .env file -cp .env "${BUILD_DIR}/.env" +cp "${REPO_ROOT}/.vscode/wheels.code-snippets" "${BUILD_DIR}/.vscode/" +cp "${REPO_ROOT}/.vscode/wheels-test.code-snippets" "${BUILD_DIR}/.vscode/" + +# Layer the CommandBox-specific build overrides on top. These are the CommandBox +# counterparts of the LuCLI server config (lucee.json/rewrite.config): a +# CommandBox server.json (webroot/cfengine/rewrites pointing at the template's +# public/urlrewrite.xml), the ForgeBox box.json (dependency + installPaths), the +# package README, the base .gitignore (must NOT exclude db/), and editor/MCP +# config. +echo "Applying CommandBox build overrides..." +cp "${REPO_ROOT}/tools/build/base/.gitignore" "${BUILD_DIR}/.gitignore" +cp "${REPO_ROOT}/tools/build/base/box.json" "${BUILD_DIR}/box.json" +cp "${REPO_ROOT}/tools/build/base/README.md" "${BUILD_DIR}/README.md" +cp "${REPO_ROOT}/tools/build/base/server.json" "${BUILD_DIR}/server.json" +cp "${REPO_ROOT}/tools/build/base/.mcp.json" "${BUILD_DIR}/.mcp.json" +cp "${REPO_ROOT}/tools/build/base/.opencode.json" "${BUILD_DIR}/.opencode.json" + +# Substitute template placeholders to working defaults so the published artifact +# has NO |tokens| / {{tokens}}. `box install` has no substitution step, so this +# is what makes `box server start` boot with zero manual edits. +# +# The app-level tokens ({{appName}}, {{datasourceName}}, {{reloadPassword}}, +# {{luceeAdminPassword}}) appear only in config/app.cfm, config/settings.cfm, +# app/views/layout.cfm and env.example — NOT in app/snippets/*.txt (those carry +# code-gen tokens like {{belongsToRelationships}} that must ship verbatim), so +# substituting these four specific tokens is safe across the whole tree. +# +# The legacy |token| forms come only from the CommandBox overrides (server.json, +# and any historical config copies); substitute them too. +echo "Substituting template placeholders to build defaults..." +substitute_placeholders() { + local file="$1" + sed -i.bak \ + -e "s/{{appName}}/${APP_NAME}/g" \ + -e "s/{{datasourceName}}/${DATASOURCE_NAME}/g" \ + -e "s/{{reloadPassword}}/${RELOAD_PASSWORD}/g" \ + -e "s/{{luceeAdminPassword}}/${LUCEE_ADMIN_PASSWORD}/g" \ + -e "s/|appName|/${APP_NAME}/g" \ + -e "s/|cfmlEngine|/${CFML_ENGINE}/g" \ + -e "s/|datasourceName|/${DATASOURCE_NAME}/g" \ + -e "s/|reloadPassword|/${RELOAD_PASSWORD}/g" \ + "$file" && rm "${file}.bak" +} +# Restrict substitution to config + server + env + the layout view; never touch +# app/snippets/*.txt code-gen templates. +for f in \ + "${BUILD_DIR}/config/app.cfm" \ + "${BUILD_DIR}/config/settings.cfm" \ + "${BUILD_DIR}/config/routes.cfm" \ + "${BUILD_DIR}/config/environment.cfm" \ + "${BUILD_DIR}/app/views/layout.cfm" \ + "${BUILD_DIR}/server.json" \ + "${BUILD_DIR}/env.example"; do + if [ -f "$f" ]; then + substitute_placeholders "$f" + fi +done # Replace version placeholders echo "Replacing version placeholders..." diff --git a/vendor/wheels/tests/specs/buildArtifactLicenseSpec.cfc b/vendor/wheels/tests/specs/buildArtifactLicenseSpec.cfc index 530e64e7f1..d916834eb2 100644 --- a/vendor/wheels/tests/specs/buildArtifactLicenseSpec.cfc +++ b/vendor/wheels/tests/specs/buildArtifactLicenseSpec.cfc @@ -12,6 +12,15 @@ component extends="wheels.WheelsTest" { // asserting the canonical `cp LICENSE` / `cp NOTICE` lines mirrors the // regression-guard pattern used by buildInfoSpec.cfc and // routesViewBrowserFixturesSpec.cfc. + // + // Two source-operand forms are accepted: the bare `cp LICENSE "${BUILD_DIR}/"` + // (core/cli/starterApp run from the repo root, so a relative LICENSE + // resolves) and the CWD-independent `cp "${REPO_ROOT}/LICENSE" "${BUILD_DIR}/"` + // that prepare-base.sh uses (#3196 — the LuCLI-source rebuild derives + // REPO_ROOT from BASH_SOURCE and reads every source file by absolute path, + // so a bare LICENSE would be a lone CWD-dependent outlier). Both forms still + // land LICENSE/NOTICE in BUILD_DIR; the regex remains tight enough that + // deleting the copy line fails the guard. function run() { @@ -36,7 +45,7 @@ component extends="wheels.WheelsTest" { it("copies LICENSE into its BUILD_DIR", () => { var src = fileRead(repoRoot & "/" & relPath); var hasLicense = reFindNoCase( - "cp[[:space:]]+LICENSE[[:space:]]+""\$\{BUILD_DIR\}", + "cp[[:space:]]+(""?\$\{REPO_ROOT\}/)?LICENSE""?[[:space:]]+""\$\{BUILD_DIR\}", src ) > 0; expect(hasLicense).toBeTrue( @@ -47,7 +56,7 @@ component extends="wheels.WheelsTest" { it("copies NOTICE into its BUILD_DIR", () => { var src = fileRead(repoRoot & "/" & relPath); var hasNotice = reFindNoCase( - "cp[[:space:]]+NOTICE[[:space:]]+""\$\{BUILD_DIR\}", + "cp[[:space:]]+(""?\$\{REPO_ROOT\}/)?NOTICE""?[[:space:]]+""\$\{BUILD_DIR\}", src ) > 0; expect(hasNotice).toBeTrue(