diff --git a/.commitlintrc.json b/.commitlintrc.json new file mode 100644 index 0000000..c30e5a9 --- /dev/null +++ b/.commitlintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["@commitlint/config-conventional"] +} diff --git a/.cspell-project-words.txt b/.cspell-project-words.txt new file mode 100644 index 0000000..dad626a --- /dev/null +++ b/.cspell-project-words.txt @@ -0,0 +1,62 @@ +Lando +Landofile +Stanislav +appserver +behaviour +bmewburn +bootstrapvue +commitlint +davidanson +dbaeumer +ddev +devpod +docroot +drush +druxt +druxtjs +endswith +extensionless +gdlib +giget +hverlin +instantiator +intelephense +interruptible +knip +lando +libasound +libfreetype +libgbm +libicu +libjpeg +libnotify +libnss +libonig +libsqlite +libxss +libxtst +libzip +lndo +makedirs +mbstring +namelist +nohup +nuxt +nuxtjs +nvmrc +opcache +openssl +openvscode +phpsab +phpstan +phpunit +repository +setuptools +shivammathur +starterkit +startswith +tokenless +trixie +valeryan +vfox +xauth diff --git a/.cspell.json b/.cspell.json new file mode 100644 index 0000000..f39803a --- /dev/null +++ b/.cspell.json @@ -0,0 +1,22 @@ +{ + "files": ["**/*.{js,mjs,cjs,vue,json,jsonc,md,yml,yaml}"], + "dictionaryDefinitions": [{ "name": "project-words", "path": "./.cspell-project-words.txt" }], + "dictionaries": ["project-words", "bash", "misc", "node", "softwareTerms"], + "ignorePaths": [ + ".git/", + "node_modules/", + "nuxt/", + "drupal/", + "coverage/", + "*.lock", + "package-lock.json", + "*.svg", + "*.png", + "*.jpg", + "*.gif", + "*.ico", + "*.min.*", + ".devcontainer/extensions/*/*.vsix" + ], + "useGitignore": true +} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b6abbf8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,83 @@ +{ + "name": "Druxt Quickstart (Serverless)", + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "16.17.0" + }, + "ghcr.io/devcontainers/features/php:1": { + "version": "8.4", + "installComposer": true + }, + // Installed through gh-release rather than the mise feature: that + // feature asks for the "latest" jdx/mise release and does not expose + // a tag filter, and mise now publishes vfox-* releases from the same + // repository. The resolver picked vfox-v2026.8.15, found no matching + // asset, and every container build failed. The regex keeps us on + // mise's own v-tags while still tracking the newest one. + "ghcr.io/devcontainers-extra/features/gh-release:1": { + "repo": "jdx/mise", + "binaryNames": "mise", + // mise ships the same build as .tar.gz, .tar.xz, .tar.zst and an + // extensionless binary; without this the resolver finds four + // matches and refuses to choose. This is the value the mise + // feature passes. + "assetRegex": ".tar.gz$", + "releaseTagRegex": "^v[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + // Pin the backend port: .devtools/start otherwise auto-discovers the + // first free port from 8888 up, and a container that picked 8889 would + // sit behind a forward that only maps 8888. + "containerEnv": { + "WEBSERVER_PORT": "8888", + // Node and PHP are provided by the devcontainer features above - + // mise.toml's pins are for host users. Without this, `mise install` + // (including the mise extension's missing-tools check) tries to + // BUILD PHP from source via the vfox-php plugin inside the + // container, which is slow, redundant, and fails on missing build + // deps (gdlib). mise tasks (`mise run setup` etc.) are unaffected. + "MISE_DISABLE_TOOLS": "node,php" + }, + "forwardPorts": [8888, 3000], + "portsAttributes": { + "8888": { + "label": "Drupal backend (drupal/.devtools)", + "onAutoForward": "silent" + }, + "3000": { + "label": "Nuxt frontend", + "onAutoForward": "silent" + } + }, + "customizations": { + "vscode": { + "settings": { + // Absolute path to where the mise feature installs the binary + // (gh-release feature default binLocation). The extension's + // default bare "mise" value trips its workspace-binary security + // check - a bare name is treated as workspace-relative, fails + // to hash (ENOENT), and re-prompts for approval on every config + // reload. An absolute machine-scope path never prompts. + "mise.binPath": "/usr/local/bin/mise" + }, + "extensions": [ + "hverlin.mise-vscode", + "davidanson.vscode-markdownlint", + "streetsidesoftware.code-spell-checker", + "EditorConfig.EditorConfig", + "redhat.vscode-yaml", + "bmewburn.vscode-intelephense-client", + "ValeryanM.vscode-phpsab", + "SanderRonde.phpstan-vscode", + "xdebug.php-debug", + "Stanislav.vscode-drupal", + "Vue.volar", + "dbaeumer.vscode-eslint" + ] + } + }, + "postCreateCommand": "bash .devcontainer/post-create.sh", + "remoteUser": "vscode" +} diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 0000000..ffdabaf --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# +# Dev container setup. The official PHP feature (ghcr.io/devcontainers/ +# features/php) builds PHP from source with no "extensions" option - +# gd isn't compiled in at all. Drupal core needs it regardless of what +# any composer.json platform override was told (its own installer does +# an independent runtime check). No sodium needed here - unlike +# quickstart/quickstart-druxt-site-tome, this repo has no simple_oauth/ +# consumers (and so no lcobucci/jwt) at all. +set -euo pipefail + +echo "==> Installing system dependencies" +sudo apt-get update +# python3-setuptools: trixie ships Python 3.13, which dropped the +# distutils stdlib module entirely - old node-gyp (bundled with this +# repo's pinned Node 16's npm) still imports it, and setuptools ships a +# compatible shim. Same fix this project's own CI pipeline already +# needed for the same reason. +sudo apt-get install -y python3 python3-setuptools build-essential libjpeg-dev libpng-dev libwebp-dev libfreetype-dev zlib1g-dev + +CONF_DIR=$(php --ini | grep 'Scan for additional .ini files' | sed 's/.*: *//') +# The PHP feature ships Xdebug globally active - every php/composer run +# stalls trying to reach a debug client and logs a warning. Off by +# default; `npm run xdebug` re-enables it per-process with -d flags. +echo 'xdebug.mode=off' | sudo tee "$CONF_DIR/zz-xdebug-off.ini" > /dev/null + + +echo "==> Building gd (not compiled in by the PHP feature at all)" +# gd is not a real standalone PECL package for current PHP versions - +# it is a bundled core extension that only gets built via a configure +# flag (--with-jpeg/--with-webp/--with-freetype) during PHP's own +# compile. `pecl install gd` fails with "No releases available" even +# with a fully up-to-date channel, because there is nothing there to +# install. Build ext/gd directly from PHP's own source tree instead - +# the same technique the PHP feature itself uses as its xdebug fallback. +PHP_FULL_VERSION=$(php -r 'echo PHP_VERSION;') +# mktemp, not a fixed /tmp path: predictable names are open to symlink +# swaps by any local process between download and extract. +PHP_SRC_TMP="$(mktemp -d)" +trap 'rm -rf "$PHP_SRC_TMP"' EXIT +GD_BUILD_DIR="$PHP_SRC_TMP/gd" +mkdir -p "$GD_BUILD_DIR" +curl -fsSL "https://www.php.net/distributions/php-${PHP_FULL_VERSION}.tar.gz" -o "$PHP_SRC_TMP/php-src.tar.gz" +tar -xzf "$PHP_SRC_TMP/php-src.tar.gz" -C "$GD_BUILD_DIR" --strip-components=3 "php-${PHP_FULL_VERSION}/ext/gd" +( + cd "$GD_BUILD_DIR" + phpize + ./configure --with-jpeg --with-webp --with-freetype + make -j"$(nproc)" + make install +) +echo 'extension=gd' | sudo tee "$CONF_DIR/gd.ini" > /dev/null + +# Fail fast if the gd build above didn't actually take - composer and +# Drupal's installer both hard-require it, and a broken build surfacing +# here beats a confusing failure mid-provision. +php -r "exit(extension_loaded('gd') ? 0 : 1);" || { echo "gd extension failed to load" >&2; exit 1; } + + +echo "==> Trusting this repo's mise.toml" +mise trust + +echo "==> Running npm install (triggers the full setup pipeline)" +# --loglevel=error: the root devDependencies are lint tooling that +# requires Node 22 (CI runs them there); installing them under the +# app's pinned Node 16 works fine but emits a wall of EBADENGINE +# warnings that drowns the real setup output on first run. +npm install --loglevel=error diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..322a4b8 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Backend URL the Nuxt frontend connects to. +# +# You normally never edit this by hand: +# - `npm run setup` / `make setup` starts the local PHP backend and +# rewrites BASE_URL to it, e.g. http://127.0.0.1:8888 +# - For DDEV, keep the project URL below instead. +BASE_URL=http://quickstart-druxt-serverless.ddev.site diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4f66068..c16f9ec 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -11,6 +11,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -23,10 +24,11 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots or video to help explain your problem. **Your Environment (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] + +- Device: [e.g. iPhone 6] +- OS: [e.g. iOS] +- Browser: [e.g. Chrome, Safari] +- Version [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 64b807a..4cb6a50 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,17 +1,19 @@ ## Types of changes + + - [ ] Bug fix (a non-breaking change which fixes an issue) - [ ] New feature (a non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - ## Description + +## Screenshots/Media -## Screenshots/Media: diff --git a/.github/semantic.yml b/.github/semantic.yml new file mode 100644 index 0000000..6d75f2f --- /dev/null +++ b/.github/semantic.yml @@ -0,0 +1,8 @@ +# Semantic Pull Requests: validate the pull request TITLE, not the +# commits inside it. +# +# The app's default accepts a PR when *either* the title or the commits +# are conventional. These repos squash-merge, so the title becomes the +# merge commit's subject - a well-committed PR with a prose title passed +# the check, then broke `lint:commit` on develop for every later push. +titleOnly: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..dc5a441 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,350 @@ +name: CI + +on: + push: + pull_request: + +# Least-privilege: nothing here needs write access to the repo. +permissions: + contents: read + +jobs: + # Root-level tooling only (package.json at the repo root) - none of this + # needs PHP/Composer or nuxt/'s own dependencies. + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + fetch-depth: 2 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + # 22, not this repo's pinned 16 (or the other jobs' 20) - the + # lint tools themselves need it: cspell requires >=22.18.0, + # markdownlint-cli2 >=22, commitlint >=22.12.0. This job only + # runs root-level tooling, never touches the app's own runtime. + node-version: '22' + cache: 'npm' + + - run: npm install + + - name: ESLint (scripts/) + run: npm run lint:js + + - name: Prettier + run: npm run lint:format + + - name: cspell + run: npm run lint:cspell + + - name: markdownlint + run: npm run lint:md + + - name: knip (unused/unlisted dependencies) + run: npm run lint:knip + + - name: npm audit (production dependencies) + run: npm run lint:audit + + - name: renovate-config-validator + run: npm run lint:renovate + + # This repository is public and is what people copy to start a + # site, so a URL only the author can reach is a defect in the + # published artefact, not a stray comment. + - name: Private host references + run: npm run lint:private + + # Only checks the most recent commit - good enough to catch the + # common case (a single non-conforming commit message) without the + # complexity of resolving a full PR commit range. + - name: commitlint + run: npx commitlint --from HEAD~1 --to HEAD --verbose + + - name: JSON sweep + run: | + rc=0; count=0 + while IFS= read -r -d '' f; do + count=$((count + 1)) + if python3 -m json.tool "$f" > /dev/null 2>&1; then + echo " [PASS] $f" + else + echo " [FAIL] $f" + rc=1 + fi + done < <(find . -name '*.json' -not -path '*/.git/*' -not -path '*/node_modules/*' \ + -not -path '*/nuxt/*' -not -path '*/drupal/*' -not -path '*/.vscode/*' -not -path '*/.devcontainer/*' -print0) + echo "--- $count JSON file(s) checked ---" + exit "$rc" + + - name: Shell syntax sweep + run: | + rc=0; count=0 + while IFS= read -r -d '' f; do + count=$((count + 1)) + if bash -n "$f" 2>/dev/null; then + echo " [PASS] $f" + else + echo " [FAIL] $f" + rc=1 + fi + done < <(find . -name '*.sh' -not -path '*/.git/*' -not -path '*/node_modules/*' \ + -not -path '*/nuxt/*' -not -path '*/drupal/*' -print0) + echo "--- $count shell file(s) checked ---" + exit "$rc" + + - name: yamllint + run: | + pip install yamllint -q + yamllint -d "{extends: default, rules: {line-length: {max: 200, level: warning}}}" .gitlab-ci.yml .github/workflows/ci.yml + + # Prose-quality lint for README.md - see .vale.ini for scoping/exceptions. + - name: Install Vale + run: | + vale_version="3.17.1" + vale_sha256="db947f89f2292e6a0381a61de155f6a5f5cb4cb460ca178ea412ef605559cefd" + curl -sL --max-time 60 "https://github.com/vale-cli/vale/releases/download/v${vale_version}/vale_${vale_version}_Linux_64-bit.tar.gz" -o /tmp/vale.tar.gz + echo "${vale_sha256} /tmp/vale.tar.gz" | sha256sum -c - || { echo "vale checksum mismatch" >&2; exit 1; } + sudo tar -xzf /tmp/vale.tar.gz -C /usr/local/bin vale + sudo chmod +x /usr/local/bin/vale + + - name: Install ai-tells style package + run: | + ai_tells_version="1.31.0" + ai_tells_sha256="bc1267248f13e65928475c439ad7ae1bf806a20d09254c08d7d8c4a9c8b811f0" + curl -sL --max-time 60 "https://github.com/tbhb/vale-ai-tells/releases/download/v${ai_tells_version}/ai-tells.zip" -o /tmp/ai-tells.zip + echo "${ai_tells_sha256} /tmp/ai-tells.zip" | sha256sum -c - || { echo "ai-tells checksum mismatch" >&2; exit 1; } + python3 -c " + import zipfile, os + with zipfile.ZipFile('/tmp/ai-tells.zip') as z: + for name in z.namelist(): + if name.startswith('ai-tells/styles/') and not name.endswith('/'): + target = os.path.join('styles', name[len('ai-tells/styles/'):]) + os.makedirs(os.path.dirname(target), exist_ok=True) + with open(target, 'wb') as f: + f.write(z.read(name)) + " + + - name: Vale + run: vale README.md + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: mbstring, pdo_sqlite, intl, gd, xml, zip, opcache + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + # setup-node v5 turned caching on by itself when package.json + # names a package manager, and keys it on the root lockfile + # alone. This job installs from nuxt/, so name both. + cache: npm + cache-dependency-path: | + package-lock.json + nuxt/package-lock.json + + - name: Validate and install Composer dependencies + working-directory: drupal + run: composer validate --strict && composer install --no-interaction --no-progress + + # Not `npm run build`/`generate` - Druxt fetches the JSON:API index + # at build time, so building needs a live Drupal backend. That + # happens in test_generate, via .devtools/. + - name: Install Nuxt dependencies + working-directory: nuxt + run: npm install + + - name: Create .env + run: cp .env.example .env + + test_generate: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: mbstring, pdo_sqlite, intl, gd, xml, zip, opcache + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + # setup-node v5 turned caching on by itself when package.json + # names a package manager, and keys it on the root lockfile + # alone. This job installs from nuxt/, so name both. + cache: npm + cache-dependency-path: | + package-lock.json + nuxt/package-lock.json + + - name: Install Composer dependencies + working-directory: drupal + run: composer install --no-interaction --no-progress + + - name: Assemble + working-directory: drupal + run: .devtools/assemble + + - name: Provision + working-directory: drupal + run: .devtools/provision + + - name: Start backend + working-directory: drupal + run: .devtools/start + + - name: Install Nuxt dependencies + working-directory: nuxt + run: npm install + + # This repo's whole point is full static output with no live + # backend at runtime - the meaningful test is that `nuxt generate` + # actually succeeds against a real backend and produces real + # static files. + - name: Generate full static output + working-directory: nuxt + run: npm run generate + + - name: Verify static output + working-directory: nuxt + run: | + test -s dist/index.html + ls dist/_nuxt/*.js > /dev/null + + - name: Upload static output on failure + if: failure() + uses: actions/upload-artifact@v7 + with: + name: nuxt-dist + path: nuxt/dist + + # node:test and node:assert only, so this needs no install and no PHP. + test_scripts: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: '22' + + - name: Run the script tests + run: npm run test:scripts + + # The documented consumer flow (README: `npx giget@1 ... --install`) is + # not what the jobs above exercise - they work from the git checkout + # and call .devtools/ directly. This job consumes the same artifact a + # giget user gets: a tarball of this exact commit, no .git, no + # preinstalled dependencies - and lets the root postinstall stand the + # whole site up. + # + # `git archive` produces that tarball from the commit under test, which + # giget cannot do - it only fetches refs already pushed to GitHub. + test_consumer_install: + runs-on: ubuntu-latest + steps: + # The documented minimum, so a pass means the lock and the + # provisioning actually work there - not just on a newer PHP. + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: mbstring, pdo_sqlite, intl, gd, xml, zip, opcache + + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + + - name: Build the tarball a giget consumer gets + run: git archive --prefix=site/ HEAD | tar -x -C /tmp + + # `env -u CI`: postinstall deliberately steps aside on CI machines + # - this job's whole point is to behave like a consumer machine. + - name: Root npm install (giget's --install step) + working-directory: /tmp/site + run: env -u CI npm install + + - name: Backend is up and provisioned + working-directory: /tmp/site + run: | + . ./.env + curl -sf "${BASE_URL}/jsonapi" | grep -q '"jsonapi"' + + - name: Full static output generates against the live backend + working-directory: /tmp/site/nuxt + run: npm run generate + + # What the root install promises on machines without a working PHP: a + # consumer's `npm install` must never fail, `npm run setup` must + # fail loudly, and a too-old PHP must be rejected by the preflight. + test_install_guardrails: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + + - name: Hide the runner's preinstalled PHP + run: while command -v php >/dev/null 2>&1; do sudo mv "$(command -v php)" "$(command -v php).hidden"; done + + - name: npm install without PHP succeeds with guidance + run: | + env -u CI npm install > /tmp/install.log 2>&1 + grep -q 'The backend needs PHP' /tmp/install.log + + - name: npm run setup without PHP fails with guidance + run: | + if npm run setup > /tmp/setup.log 2>&1; then echo "setup should have failed"; exit 1; fi + grep -q 'Missing required tools' /tmp/setup.log + + - name: Old PHP is rejected by the version preflight + run: | + SHIM=$(mktemp -d) + printf '#!/bin/sh\necho 8.2.29\n' > "$SHIM/php" + printf '#!/bin/sh\nexit 0\n' > "$SHIM/composer" + chmod +x "$SHIM/php" "$SHIM/composer" + if PATH="$SHIM:$PATH" npm run setup > /tmp/old-php.log 2>&1; then echo "setup should have failed"; exit 1; fi + grep -q 'too old' /tmp/old-php.log + + # The preflight exits the process, so without postinstall screening + # the version first this is what would fail `npm install` outright. + - name: Old PHP still leaves npm install green + run: | + SHIM=$(mktemp -d) + printf '#!/bin/sh\necho 8.2.29\n' > "$SHIM/php" + printf '#!/bin/sh\nexit 0\n' > "$SHIM/composer" + chmod +x "$SHIM/php" "$SHIM/composer" + PATH="$SHIM:$PATH" env -u CI npm install > /tmp/old-php-install.log 2>&1 + grep -q 'The backend needs PHP' /tmp/old-php-install.log diff --git a/.github/workflows/environments.yml b/.github/workflows/environments.yml new file mode 100644 index 0000000..fb67869 --- /dev/null +++ b/.github/workflows/environments.yml @@ -0,0 +1,115 @@ +name: Environments + +# The README offers four ways to run this starterkit. Three of them boot +# containers, which the main CI never touches - it only exercises the +# Docker-free path. These jobs cover the other three, so a broken DDEV +# command or Landofile is found here rather than by someone following +# the README. +# +# They are slow (container build plus a full Composer install), so they +# run when the files they cover change, once a week to catch upstream +# drift, and on demand - not on every push. +on: + push: + paths: &environment-paths + - '.devcontainer/**' + - '.env.example' + - 'drupal/.ddev/**' + - 'drupal/.devtools/**' + - 'drupal/.lando.yml' + - 'package.json' + - 'drupal/composer.json' + - 'drupal/composer.lock' + - 'scripts/**' + - '.github/workflows/environments.yml' + pull_request: + paths: *environment-paths + schedule: + - cron: '0 4 * * 1' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: environments-${{ github.ref }} + cancel-in-progress: true + +jobs: + ddev: + name: DDEV + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + # Starts the project too (autostart defaults to true). + - uses: ddev/github-action-setup-ddev@v1 + with: + ddevDir: drupal + + - name: Install Drupal + working-directory: drupal + run: ddev drupal-install + + # From inside the web container: no dependency on the runner + # resolving *.ddev.site. -sS so a failure still prints why. + - name: JSON:API responds + working-directory: drupal + run: ddev exec 'curl -sS http://localhost/jsonapi' | tee /dev/stderr | grep -q '"jsonapi"' + + lando: + name: Lando + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + # Installs Lando only - unlike the DDEV action it does not start + # the app, so `lando start` is a step of its own below. + - uses: lando/setup-lando@v3 + + - name: Start Lando + working-directory: drupal + run: lando start + + - name: Install Drupal + working-directory: drupal + run: lando drupal-install + + # Checked from the host over the Lando URL, which is the path a + # user actually takes. `via: nginx` puts the web server in its own + # container, so localhost inside appserver serves nothing. + - name: JSON:API responds + working-directory: drupal + run: | + URL=$(lando info --format json 2>/dev/null \ + | jq -r '.[] | select(.service == "appserver") | .urls[]?' \ + | grep '^http://' | head -1) + URL="${URL:-http://druxt-quickstart-serverless.lndo.site}" + echo "Checking $URL/jsonapi" + curl -sS "$URL/jsonapi" | tee /dev/stderr | grep -q '"jsonapi"' + + devcontainer: + name: Dev container + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + + # Builds devcontainer.json for real and runs post-create.sh, which + # is the same first-run a DevPod or VS Code user gets. + - uses: devcontainers/ci@v0.3 + with: + push: never + runCmd: | + set -e + npm run devtools -- start + . ./.env + curl -sf "${BASE_URL}/jsonapi" | grep -q '"jsonapi"' diff --git a/.gitignore b/.gitignore index b40a599..67da1bf 100644 --- a/.gitignore +++ b/.gitignore @@ -104,6 +104,8 @@ dist .vscode/* # VSCode Xdebug settings !.vscode/launch.json +# VSCode recommended extensions +!.vscode/extensions.json # Intellij idea *.iml @@ -129,4 +131,8 @@ dist Network Trash Folder Temporary Items .apdisk -CHANGELOG.md + +# Vale style package, fetched by `vale sync` (local) or pinned+verified by +# CI - never committed, see .vale.ini. +/styles/ +.setup.lock/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..75ef2c8 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,317 @@ +stages: + - lint + - build + - test + +variables: + PHP_APT_PACKAGES: 'git unzip libsqlite3-dev libzip-dev libicu-dev libpng-dev libjpeg62-turbo-dev libfreetype6-dev libonig-dev libxml2-dev' + PHP_EXTENSIONS: 'mbstring pdo_sqlite intl gd xml zip opcache' + +cache: + paths: + - drupal/vendor/ + - nuxt/node_modules/ + +# +# --- Lint Stage --- +# +# Root-level tooling only (package.json at the repo root) - none of this +# needs PHP/Composer or nuxt/'s own dependencies, so it stays off the +# heavier .php-node-setup image entirely. +.node-lint: + # 22, not this repo's pinned 16 - the lint + # tools themselves need it: cspell requires >=22.18.0, markdownlint-cli2 + # >=22, commitlint >=22.12.0. This template only runs root-level + # tooling, never touches the app's own runtime. + image: node:22 + cache: + key: + files: + - package-lock.json + paths: + - node_modules/ + before_script: + - npm install + +lint:js: + extends: .node-lint + stage: lint + interruptible: true + script: + - npm run lint:js + +lint:format: + extends: .node-lint + stage: lint + interruptible: true + script: + - npm run lint:format + +lint:cspell: + extends: .node-lint + stage: lint + interruptible: true + script: + - npm run lint:cspell + +lint:md: + extends: .node-lint + stage: lint + interruptible: true + script: + - npm run lint:md + +lint:knip: + extends: .node-lint + stage: lint + interruptible: true + script: + - npm run lint:knip + +lint:audit: + extends: .node-lint + stage: lint + interruptible: true + script: + - npm run lint:audit + +lint:renovate: + extends: .node-lint + stage: lint + interruptible: true + script: + - npm run lint:renovate + +# This repository is public and is what people copy to start a site, so a +# URL only the author can reach is a defect in the published artefact, +# not a stray comment. +lint:private: + extends: .node-lint + stage: lint + interruptible: true + script: + - npm run lint:private + +# Only checks the most recent commit - good enough to catch the common +# case (a single non-conforming commit message) without the complexity of +# resolving a full MR commit range across both push and merge_request +# pipeline triggers. +lint:commit: + extends: .node-lint + stage: lint + interruptible: true + script: + - npx commitlint --from HEAD~1 --to HEAD --verbose + +lint:json: + stage: lint + interruptible: true + image: python:3-slim + script: + - | + rc=0; count=0 + while IFS= read -r -d '' f; do + count=$((count + 1)) + if python3 -m json.tool "$f" > /dev/null 2>&1; then + echo " [PASS] $f" + else + echo " [FAIL] $f" + rc=1 + fi + done < <(find . -name '*.json' -not -path '*/.git/*' -not -path '*/node_modules/*' \ + -not -path '*/nuxt/*' -not -path '*/drupal/*' -not -path '*/.vscode/*' -not -path '*/.devcontainer/*' -print0) + echo "--- $count JSON file(s) checked ---" + exit "$rc" + +lint:shell: + stage: lint + interruptible: true + image: bash:5 + script: + - | + rc=0; count=0 + while IFS= read -r -d '' f; do + count=$((count + 1)) + if bash -n "$f" 2>/dev/null; then + echo " [PASS] $f" + else + echo " [FAIL] $f" + rc=1 + fi + done < <(find . -name '*.sh' -not -path '*/.git/*' -not -path '*/node_modules/*' \ + -not -path '*/nuxt/*' -not -path '*/drupal/*' -print0) + echo "--- $count shell file(s) checked ---" + exit "$rc" + +lint:yaml: + stage: lint + interruptible: true + image: python:3-slim + script: + - pip install yamllint -q + - 'yamllint -d "{extends: default, rules: {line-length: {max: 200, level: warning}}}" .gitlab-ci.yml .github/workflows/ci.yml' + +# Prose-quality lint for README.md: flags AI-generated-text tells (em-dash overuse, +# hedging, anthropomorphic verbs, overused vocabulary, etc.) via the +# vale-ai-tells style package, scoped by .vale.ini to hand-authored prose +# only. Blocking: all real findings against current content are fixed, +# and ai-tells.ColonUsage is disabled (see the comment in .vale.ini) since +# it can't tell this repo's "**term**: Description" convention apart from +# a genuine AI tell. +lint:vale: + stage: lint + interruptible: true + image: python:3-slim + script: + - | + vale_version="3.17.1" + arch="$(uname -m)" + case "$arch" in + aarch64|arm64) + vale_arch=arm64 + vale_sha256=92d91ebf9ee69ec077379be95cd09e6710ab33d3d5bab66bb482e66ebc80dc23 + ;; + x86_64) + vale_arch=64-bit + vale_sha256=db947f89f2292e6a0381a61de155f6a5f5cb4cb460ca178ea412ef605559cefd + ;; + *) echo "Unsupported architecture: $arch" >&2; exit 1 ;; + esac + apt-get update -yqq && apt-get install -yqq curl >/dev/null + curl -sL --max-time 60 "https://github.com/vale-cli/vale/releases/download/v${vale_version}/vale_${vale_version}_Linux_${vale_arch}.tar.gz" -o /tmp/vale.tar.gz + echo "${vale_sha256} /tmp/vale.tar.gz" | sha256sum -c - || { echo "vale checksum mismatch" >&2; exit 1; } + tar -xzf /tmp/vale.tar.gz -C /usr/local/bin vale + chmod +x /usr/local/bin/vale + - | + ai_tells_version="1.31.0" + ai_tells_sha256="bc1267248f13e65928475c439ad7ae1bf806a20d09254c08d7d8c4a9c8b811f0" + curl -sL --max-time 60 "https://github.com/tbhb/vale-ai-tells/releases/download/v${ai_tells_version}/ai-tells.zip" -o /tmp/ai-tells.zip + echo "${ai_tells_sha256} /tmp/ai-tells.zip" | sha256sum -c - || { echo "ai-tells checksum mismatch" >&2; exit 1; } + python3 -c " + import zipfile, os + with zipfile.ZipFile('/tmp/ai-tells.zip') as z: + for name in z.namelist(): + if name.startswith('ai-tells/styles/') and not name.endswith('/'): + target = os.path.join('styles', name[len('ai-tells/styles/'):]) + os.makedirs(os.path.dirname(target), exist_ok=True) + with open(target, 'wb') as f: + f.write(z.read(name)) + " + - vale README.md + +.php-node-setup: + image: php:8.4 + before_script: + - apt-get update -yqq + - apt-get install -yqq $PHP_APT_PACKAGES python3 python3-setuptools build-essential + - docker-php-ext-install $PHP_EXTENSIONS + - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer --version=2.10.2 + - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash + - export NVM_DIR="$HOME/.nvm" && . "$NVM_DIR/nvm.sh" --no-use + - nvm install && nvm use + +build: + extends: .php-node-setup + stage: build + script: + # Validate and install Composer dependencies. + - cd drupal && composer validate --strict && composer install --no-interaction --no-progress + + # Install Nuxt dependencies. Not `npm run build`/`generate` - Druxt + # fetches the JSON:API index at build time, so building needs a live + # Drupal backend. That happens in the test_generate job, via .devtools/. + - cd ../nuxt && npm install + artifacts: + paths: + - drupal/vendor + - nuxt/node_modules + +# This repo's whole point is full static output with no live backend at +# runtime - the meaningful test is that `nuxt generate` actually succeeds +# against a real backend and produces real static files, not a Cypress +# suite exercising a server that (by design) nothing will be running +# against in production. + +# Coverage reporting needs Node 20+, and these tests use only node:test +# and node:assert, so they run on a modern Node with nothing installed. +test_scripts: + stage: test + image: node:22 + needs: [] + script: + - npm run test:scripts:coverage + artifacts: + paths: + - coverage/scripts-lcov.info + +test_generate: + extends: .php-node-setup + stage: test + needs: + - build + script: + - cd drupal + - .devtools/assemble + - .devtools/provision + - .devtools/start + - cd ../nuxt + - npm install + - npm run generate + - test -s dist/index.html + - ls dist/_nuxt/*.js > /dev/null + artifacts: + when: on_failure + paths: + - nuxt/dist + +# The documented consumer flow (`npx giget@1 gh:druxt/... my-site +# --install`) consumes a tarball: no .git, no preinstalled dependencies, +# and the root npm install stands everything up. git archive produces +# that same artifact from this exact commit - giget itself can only +# fetch refs already pushed to GitHub. +test_consumer_install: + extends: .php-node-setup + # The documented minimum, so a pass means the lock and the provisioning + # actually work there - not just on a newer PHP. + image: php:8.3 + stage: test + needs: [] + script: + - git archive --prefix=site/ "$CI_COMMIT_SHA" | tar -x -C /tmp + # `env -u CI`: postinstall deliberately steps aside on CI machines - + # this job's whole point is to behave like a consumer machine. + - cd /tmp/site && env -u CI npm install + - . /tmp/site/.env && curl -sf "${BASE_URL}/jsonapi" | grep -q '"jsonapi"' + - cd /tmp/site/nuxt && npm run generate + +# What the root install promises on machines without a working PHP: a +# consumer's `npm install` must never fail, `npm run setup` must +# fail loudly, and a too-old PHP must be rejected by the preflight. +test_install_guardrails: + stage: test + image: node:22 + needs: [] + script: + # npm install without PHP: exits 0 and points at `npm run setup`. + - env -u CI npm install > /tmp/install.log 2>&1 + - grep -q 'The backend needs PHP' /tmp/install.log + # npm run setup without PHP: fails, and says what is missing. + - if npm run setup > /tmp/setup.log 2>&1; then echo "setup should have failed"; exit 1; fi + - grep -q 'Missing required tools' /tmp/setup.log + # Too-old PHP: the version preflight rejects it. + - | + SHIM=$(mktemp -d) + printf '#!/bin/sh\necho 8.2.29\n' > "$SHIM/php" + printf '#!/bin/sh\nexit 0\n' > "$SHIM/composer" + chmod +x "$SHIM/php" "$SHIM/composer" + if PATH="$SHIM:$PATH" npm run setup > /tmp/old-php.log 2>&1; then echo "setup should have failed"; exit 1; fi + grep -q 'too old' /tmp/old-php.log + # Too-old PHP through npm install: the preflight exits the process, so + # without postinstall screening first this would fail the install. + - | + SHIM=$(mktemp -d) + printf '#!/bin/sh\necho 8.2.29\n' > "$SHIM/php" + printf '#!/bin/sh\nexit 0\n' > "$SHIM/composer" + chmod +x "$SHIM/php" "$SHIM/composer" + PATH="$SHIM:$PATH" env -u CI npm install > /tmp/old-php-install.log 2>&1 + grep -q 'The backend needs PHP' /tmp/old-php-install.log diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index a56c559..0000000 --- a/.gitpod.yml +++ /dev/null @@ -1,72 +0,0 @@ -image: - file: .gitpod/Dockerfile - -tasks: - - init: | - # Installing DDev and Drupal backend. - .gitpod/scripts/ddev-download-images.sh - .gitpod/scripts/ddev-setup.sh - .gitpod/scripts/drupal-setup.sh - # Installing Nuxt/Druxt frontend. - .gitpod/scripts/nuxt-setup.sh - command: | - .gitpod/scripts/ddev-setup.sh - .gitpod/scripts/nuxt-setup.sh - cd $GITPOD_REPO_ROOT/nuxt && NUXT_TELEMETRY_DISABLED=1 npm run dev - -# VScode xdebug extension -vscode: - extensions: - - dbaeumer.vscode-eslint - - editorconfig.editorconfig - - felixfbecker.php-debug - - octref.vetur - -ports: - # Drupal - - port: 8080 - onOpen: ignore - visibility: public - - # Nuxt/Druxt - - port: 3000 - onOpen: ignore - visibility: public - - # Nuxt/Druxt - - port: 3003 - onOpen: ignore - visibility: public - - # Currently un-notified and unsupported mailhog http port - - port: 8025 - onOpen: ignore - # Currently un-notified and unsupported mailhog https port - - port: 8026 - onOpen: ignore - # Currently un-notified and unsupported phpmyadmin http port - - port: 8036 - onOpen: ignore - # Currently un-notified and unsupported phpmyadmin https port - - port: 8037 - onOpen: ignore - # router http port that we're ignoring. - - port: 8888 - onOpen: ignore - # router https port that we're ignoring. - - port: 8889 - onOpen: ignore - # xdebug port - - port: 9000 - onOpen: ignore - -github: - prebuilds: - master: true - branches: true - pullRequests: true - pullRequestsFromForks: true - addCheck: true - addComment: false - addBadge: true - addLabel: true diff --git a/.gitpod/Dockerfile b/.gitpod/Dockerfile deleted file mode 100644 index 2d261fb..0000000 --- a/.gitpod/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM gitpod/workspace-full -SHELL ["/bin/bash", "-c"] - -RUN sudo apt-get -qq update - -# Install ddev -RUN brew update && brew install drud/ddev/ddev - -# Install latest composer -RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" -RUN php -r "if (hash_file('sha384', 'composer-setup.php') === '55ce33d7678c5a611085589f1f3ddf8b3c52d662cd01d4ba75c0ee0459970c2200a51f492d557530c71c15d8dba01eae') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" -RUN sudo php composer-setup.php --install-dir /usr/bin --filename composer -RUN php -r "unlink('composer-setup.php');" - -# Install latest npm -RUN npm install -g npm diff --git a/.gitpod/scripts/ddev-download-images.sh b/.gitpod/scripts/ddev-download-images.sh deleted file mode 100755 index 52f005b..0000000 --- a/.gitpod/scripts/ddev-download-images.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -ddev version | awk '/(drud|phpmyadmin)/ {print $2;}' >/tmp/images.txt -while IFS= read -r item -do - docker pull "$item" -done < <(cat /tmp/images.txt) diff --git a/.gitpod/scripts/ddev-setup.sh b/.gitpod/scripts/ddev-setup.sh deleted file mode 100755 index 7f86637..0000000 --- a/.gitpod/scripts/ddev-setup.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash - -DRUPAL_DIR="${GITPOD_REPO_ROOT}/drupal" - -# Misc housekeeping before start -ddev config global --instrumentation-opt-in=true - -# Start ddev -cd $DRUPAL_DIR && ddev start diff --git a/.gitpod/scripts/drupal-setup.sh b/.gitpod/scripts/drupal-setup.sh deleted file mode 100755 index f45e6d0..0000000 --- a/.gitpod/scripts/drupal-setup.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -set -eu -o pipefail - -DRUPAL_DIR="${GITPOD_REPO_ROOT}/drupal" - -# Set up Drupal website -cd "$DRUPAL_DIR" && ddev drupal-install diff --git a/.gitpod/scripts/nuxt-setup.sh b/.gitpod/scripts/nuxt-setup.sh deleted file mode 100755 index 20d6435..0000000 --- a/.gitpod/scripts/nuxt-setup.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -# Set up Druxt for use on gitpod - -set -eu -o pipefail - -DRUXT_DIR="${GITPOD_REPO_ROOT}/nuxt" - -# Set up Nuxt -cd "$DRUXT_DIR" && echo "BASE_URL=$(gp url 8080)" > .env -cd "$DRUXT_DIR" && npm i diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 0000000..6191a33 --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,15 @@ +{ + "customRules": ["./.markdownlint-rules/no-fence-in-fence.cjs"], + "config": { + "local/no-fence-in-fence": true, + "default": true, + "MD013": false, + "MD024": { "siblings_only": true }, + "MD025": { "front_matter_title": "" }, + "MD033": false, + "MD034": false, + "MD041": false, + "MD036": false, + }, + "ignores": ["node_modules/**", "**/node_modules/**", "nuxt/**", "drupal/**"], +} diff --git a/.markdownlint-rules/no-fence-in-fence.cjs b/.markdownlint-rules/no-fence-in-fence.cjs new file mode 100644 index 0000000..243b0b1 --- /dev/null +++ b/.markdownlint-rules/no-fence-in-fence.cjs @@ -0,0 +1,67 @@ +// Flags fence-marker-with-language lines (```bash, ````js, ...) appearing +// as CONTENT inside an already-open fenced code block. That is legal +// CommonMark - a 4-backtick fence can contain 3-backtick markers as +// literal text, and inside a 3-backtick block a ```lang line doesn't +// close it (closing fences may not carry an info string) - which is +// exactly why no core markdownlint rule catches it. In practice it is +// almost always a mangled edit: an opener where the closer belongs, +// silently swallowing the prose after it into the rendered code block. +// +// Deliberately showing markdown fences inside a wider fence? Use a +// `markdown` info string on the outer fence - that combination is +// skipped here. +'use strict' + +// CommonMark allows whitespace between the fence marker and the info +// string ("``` bash"), and the info string itself may be multi-word - +// `.*` + trim covers both, so a space-separated opener can't slip past +// the rule's fence-state tracking unmatched. +const FENCE = /^(\s*)(`{3,}|~{3,})(.*)$/ + +module.exports = { + names: ['local/no-fence-in-fence'], + description: + 'Fence marker with a language inside an open code fence (mangled fences swallow prose)', + tags: ['code', 'local'], + parser: 'none', + function: (params, onError) => { + let open = null + + params.lines.forEach((line, index) => { + const match = line.match(FENCE) + if (!match) { + return + } + const marker = match[2] + const info = match[3].trim() + // CommonMark: a backtick fence's info string may not contain + // backticks - such a line is inline code, not a fence. + if (marker[0] === '`' && info.includes('`')) { + return + } + // First word of the info string is the language. + const language = (info.split(/\s+/)[0] || '').toLowerCase() + + if (!open) { + open = { char: marker[0], length: marker.length, language } + return + } + + // A closing fence: same character, at least as long, no info string. + if (marker[0] === open.char && marker.length >= open.length && info === '') { + open = null + return + } + + // Anything fence-like WITH a language while a fence is open is + // content - and almost certainly a mistake, unless the outer + // fence says it contains markdown. + if (info !== '' && !['markdown', 'md'].includes(open.language)) { + onError({ + lineNumber: index + 1, + detail: `"${marker}${info}" is inside an open ${open.char.repeat(open.length)}${open.language} fence - a mangled opener/closer pair renders the prose after it as code`, + }) + } + }) + }, +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..37e391f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v16.17.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..d7a6d11 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +nuxt/ +drupal/ +package-lock.json +styles/ diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..bfea7f2 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,16 @@ +{ + "printWidth": 100, + "semi": false, + "singleQuote": true, + "trailingComma": "es5", + "tabWidth": 2, + "overrides": [ + { + "files": ["*.md"], + "options": { + "printWidth": 100, + "proseWrap": "preserve" + } + } + ] +} diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 0000000..68adf9e --- /dev/null +++ b/.vale.ini @@ -0,0 +1,28 @@ +StylesPath = styles +MinAlertLevel = suggestion + +# For local dev: `vale sync` reads this to fetch the style package (fine for +# a local run - same trust model as `npm install` without a lockfile hash). +# CI does NOT rely on this: it downloads the same release asset separately, +# verifies its SHA-256, and extracts it into StylesPath itself, since `vale +# sync` doesn't check the archive's contents against anything. If you bump +# this version, update the pinned URL and checksum in both CI files too +# (.gitlab-ci.yml and .github/workflows/ci.yml) - all three need to move +# together. +Packages = https://github.com/tbhb/vale-ai-tells/releases/download/v1.31.0/ai-tells.zip + +# Scoped to hand-authored prose only: the README. Nothing else in this repo is prose someone wrote +# for a reader - nuxt/ and drupal/ are vendor-managed once installed, and +# neither is even reachable here (both gitignored, never committed). +[README.md] +BasedOnStyles = ai-tells + +# ai-tells.ColonUsage flags any capitalized word after a colon (its own docs +# call this a "known limitation": a run-in bold label like "**Note:** Like +# this." flags too, since Vale strips markdown before matching, and says to +# disable the rule where that convention is established). This repo's own +# README leans on that exact convention: "**Service**: URL" +# tables, "**One-time login link**: ..." labels, and proper nouns like +# "Druxt"/"Drush" after a colon. Set per-section (not global) since Vale's +# INI directives are scoped to the glob section they appear under. +ai-tells.ColonUsage = NO diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..77bda14 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,16 @@ +{ + "recommendations": [ + "hverlin.mise-vscode", + "davidanson.vscode-markdownlint", + "streetsidesoftware.code-spell-checker", + "EditorConfig.EditorConfig", + "redhat.vscode-yaml", + "bmewburn.vscode-intelephense-client", + "ValeryanM.vscode-phpsab", + "SanderRonde.phpstan-vscode", + "xdebug.php-debug", + "Stanislav.vscode-drupal", + "Vue.volar", + "dbaeumer.vscode-eslint" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 2826814..7082bb6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,18 +1,54 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Listen for XDebug", - "type": "php", - "request": "launch", - "hostname": "0.0.0.0", - "port": 9000, - "pathMappings": { - "/var/www/html": "${workspaceRoot}" - } - } - ] + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + // Enable first: `npm run xdebug` (or `make xdebug` / `mise run xdebug`) + // restarts the local backend with step-debugging on. `npm run + // devtools -- start` (no XDEBUG) turns it back off. + "name": "Listen for XDebug (local backend)", + "type": "php", + "request": "launch", + "hostname": "0.0.0.0", + "port": 9003, + "pathMappings": { + "${workspaceFolder}/drupal": "${workspaceFolder}/drupal" + } + }, + { + // DDEV's own docroot is inside its web container - map it back to + // this workspace. Run `ddev xdebug on` first (DDEV's own toggle; + // this repo's `npm run xdebug` only knows about the local backend). + "name": "Listen for XDebug (DDEV)", + "type": "php", + "request": "launch", + "hostname": "0.0.0.0", + "port": 9003, + "pathMappings": { + "/var/www/html": "${workspaceFolder}/drupal" + } + }, + { + "name": "Debug Nuxt (dev)", + "type": "node", + "request": "launch", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "cwd": "${workspaceFolder}", + // Not NODE_OPTIONS=--inspect: every child (npm, dev.mjs, nuxt) + // would inherit it and fight over inspector port 9229. The + // debugger's own bootloader attaches to each child instead. + "autoAttachChildProcesses": true, + "console": "integratedTerminal", + "skipFiles": ["/**"] + } + ], + "compounds": [ + { + "name": "Debug backend + frontend (local)", + "configurations": ["Listen for XDebug (local backend)", "Debug Nuxt (dev)"] + } + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..fc86f3c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,144 @@ +# Changelog + +All notable changes to this starterkit. + +**The version tracks Druxt, which is still pre-1.0** (`druxt` 0.24.0, +`druxt-site` 0.14.3). A starterkit numbered above the framework it +builds on would claim a stability neither has, so this stays on 0.x +until Druxt reaches 1.0. + +While it does, the usual 0.x reading applies: + +- **Minor** - anything that changes the setup you would follow, up to + and including a Drupal major. Breaking changes are called out under + their own heading; on 0.x the minor is where they live. +- **Patch** - fixes and dependency updates that leave the documented + setup alone. + +## 0.3.0 (2026-08-21) + +A full modernization: Drupal 11, a local backend that needs no Docker, +and a one-command install. + +Breaking for anyone following the old setup, which on 0.x is what a +minor is for. + +### Breaking changes + +- Drupal 9 to **Drupal 11.4.5**, with druxt on 1.2 and tome on 1.16, + both D11-compatible releases. +- The Gitpod configuration is gone, replaced by a dev container and CI. + +### Features + +- **One-command setup.** `npm install` on a fresh checkout provisions + everything, which is what makes + `npx giget@1 gh:druxt/quickstart-druxt-serverless my-site --install` + deliver a running backend and a generated frontend rather than an + empty package. The same pipeline is available as `npm run setup`. +- **Static generation against the local backend**: `npm run generate` + builds the site from a backend this repo stands up itself, so the + documented output no longer needs a hosted Drupal. +- **Test coverage**: a test of the documented `giget` install path, + guard-rail tests for machines without PHP, container environment tests + for DDEV, Lando and the dev container, and a generate test that + provisions a real backend. +- **A Docker-free local backend** in `drupal/.devtools/`: Composer + install, a site install, the OAuth consumer, and a PHP built-in + server, driven by `assemble`, `provision`, `start`, `stop` and `info`. + PHP and Composer are the only requirements. +- **Dev container** support for VS Code, Codespaces and DevPod, which + sets the site up on create. +- **[Lando](https://lando.dev) as a backend option** alongside DDEV, with + `lando drupal-install` and `lando druxt-add-consumer` running the same + scripts the DDEV commands do. +- **Lifecycle commands** through npm, `make` and `mise`, including + `npm run drush -- ` proxied to whichever backend is + configured, and `npm run xdebug` to restart the backend with step + debugging. +- **Windows guidance**: the local backend cannot run there, so setup says + so immediately and names the routes that do work, instead of failing + part way through key generation. +- **A lint suite** - ESLint, Prettier, cspell, markdownlint, knip, + commitlint and Vale - so the starterkit holds itself to the practices + it demonstrates. + +### Bug fixes + +- Druxt modules moved from `buildModules` to `modules`. `buildModules` + are not loaded by `nuxt start`, so the proxy and authentication + registrations vanished in production while the dev server looked fine. +- The dev server moves to the next free port between 3000 and 3009 when + 3000 is taken, and prints which one it took. Nuxt's own fallback picks + a random port, so everything that advertised the frontend URL pointed + at the wrong place. A `PORT` you name is still yours - a busy one + fails, rather than moving somewhere you did not ask for. +- Setup runs one at a time. A dev container attaches while its + post-create setup is still installing, and a second setup started from + that terminal corrupted `vendor/` and `node_modules/`. +- `composer install` retries: a transient registry error no longer ends + a first run. +- The dev container no longer leaves Xdebug active, which made every + `php` and `composer` call wait for a debugger. +- The patch descriptions no longer link to a merge request that resolves + only on a private network. `npm run lint:private` fails the build on + any tracked file that references one. +- The install command is pinned to `giget@1`. giget 2 and newer call + `fetch`, which needs Node 18, while the site pins Node 16, so the + headline command failed on the exact version the README tells you to + use, with only `fetch is not defined` to explain itself. It looked + fine on any machine that had run giget before, because giget serves + repeat fetches from its cache, so the failure hit new users rather + than maintainers. giget 1 bundles a fetch polyfill, so one Node + version now covers both the download and the site. +- The committed lock installs on PHP 8.3, the version the setup + preflight accepts. `drupal/core-dev` pulled in `doctrine/instantiator` + 2.1.0, which requires PHP 8.4, so an 8.3 machine passed the preflight + and then failed in Composer. core-dev is gone - nothing here runs + phpunit, and it was 86 of the 187 locked packages - and + `config.platform.php` now pins resolution to 8.3, so a later update + cannot reintroduce the mismatch. The consumer-install CI job runs on + 8.3 so a pass means the documented minimum genuinely works. +- `npm install` stays green when the PHP on `PATH` is too old. The setup + preflight rejects it with `process.exit`, which skips the catch that + keeps installs passing, so a machine with PHP 8.2 failed `npm install` + outright instead of getting the frontend-only fallback the missing-PHP + case gets. postinstall now screens the version itself and steps aside + with the version it found; `npm run setup` still fails loudly. + +### Dependencies + +- `drupal/decoupled_router` is held below 2.0.7. That release gave + `RouterPathTranslatorSubscriber::onPathTranslation()` a `: void` return + type, and druxt 1.2.1 declares its three subscribers without one, so + the container fails to rebuild: `drush cr` aborts, Drupal keeps serving + from the old container, and druxt's subscribers are silently absent. + Nothing here required decoupled_router directly, so only the lock stood + between an update and a broken site. The constraint comes off when + druxt releases a version carrying the fix + ([#3618675](https://www.drupal.org/i/3618675)). +- Dependabot no longer files version updates. Renovate covers the same + ecosystems and carries the auto-merge policy, so every bump was + arriving twice. Dependabot security alerts are unaffected. + +### Known limitations + +- Nuxt 2 and Node 16 are both end of life. This starterkit is pinned to + them because Druxt targets Nuxt 2; the Nuxt 3 story is separate work. + +## 0.2.0 (2022-09-16) + +### Features + +- Updated Drupal to 9.4. + +### Bug fixes + +- Fixed the DDEV MTU problem on some networks. +- Pinned dependencies so builds stopped drifting. + +## 0.1.0 (2022-02-19) + +The first version of the starterkit: a Drupal 9 and Nuxt 2 +mono-repo generating a fully static site, with DDEV and Gitpod for +local development. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..df28b34 --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +SHELL=/bin/bash + +.PHONY: help setup dev build start stop assemble provision generate login xdebug info reset drush + +help: + @echo "COMMANDS" + @echo "========" + @echo "setup - Install frontend deps, provision Drupal (SQLite), start the backend." + @echo "dev - Ensure the backend is up, then run the Nuxt dev server (port 3000)." + @echo "build - Build the Nuxt frontend for production." + @echo "start - Ensure the backend is up, then serve the built frontend (port 3000)." + @echo "stop - Stop the local (.devtools) backend. No-op for DDEV/external backends." + @echo "assemble - Install Composer dependencies (backend)." + @echo "provision - Install the site from committed config and Tome content (SQLite)." + @echo "generate - Ensure the backend is up, then build full static output (nuxt/dist)." + @echo "login - Print a Drupal one-time login link." + @echo "xdebug - Enable XDebug step-debugging on the local backend." + @echo "info - Print a summary of the current environment." + @echo "reset - Stop the backend and wipe the throwaway SQLite database." + @echo "drush - Run a Drush command, e.g. make drush cr all." + @echo "" + @echo "Backend-only targets: make -C drupal help" + +setup: + npm run setup + +dev: + npm run dev + +build: + npm run build + +start: + npm run start + +stop: + npm run stop + +assemble: + npm run assemble + +provision: + npm run provision + +generate: + npm run generate + +login: + npm run login + +xdebug: + npm run xdebug + +info: + npm run info + +reset: + npm run reset + +# Pass arguments through to npm run drush: make drush cr all +ifeq (drush,$(firstword $(MAKECMDGOALS))) + DRUSH_RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) + $(eval $(DRUSH_RUN_ARGS):;@:) +endif + +drush: + npm run drush -- $(DRUSH_RUN_ARGS) + +.DEFAULT_GOAL := help diff --git a/README.md b/README.md index 298e9ca..6f16f38 100644 --- a/README.md +++ b/README.md @@ -4,59 +4,224 @@ DruxtSite connects Drupal to Nuxt via JSON:API to provide a framework for building a Fully Decoupled site. -This template is configured to have full static content deployed to a CDN, without the need for a live Drupal backend. +This template builds to full static output, deployable to any CDN - Drupal and Tome only matter at build time. There's no live backend to run in production. -Develop, craft and create locally or in the cloud, deploy anywhere. +This repository includes: -This repostory provides a quickstart installation of: -- Drupal 9 with Tome sync +- Drupal 11 with Tome sync - Nuxt 2 - DruxtSite - ## Quickstart -Try it before you fork it: +```bash +npx giget@1 gh:druxt/quickstart-druxt-serverless#develop my-druxt-site --install +cd my-druxt-site +npm run generate +``` + +`--install` runs the full setup automatically: frontend, Composer, and a +local Drupal 11 + SQLite backend, installed straight from the committed +Tome config and content. It needs PHP 8.3 or newer and Composer on +`PATH`. Without them it installs the frontend only, prints the next steps +and still exits cleanly, so the install never fails on a machine that +cannot run the backend. -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/druxt/quickstart-druxt-site-tome) +The `@1` is deliberate. giget 2 and newer call `fetch`, which needs Node +18, and this site runs on [Node 16](.nvmrc) - an unpinned `giget@latest` +fails there with `fetch is not defined`. giget 1 bundles a fetch +polyfill, so one Node version covers both the download and the site. +Prefer to start from your own repository? Use the GitHub +[Use this template](https://github.com/druxt/quickstart-druxt-serverless/generate) +button, then clone the repository it creates. + +`npm run generate` builds the full static site to `nuxt/dist/` - deploy +that directory anywhere that serves static files. ## Getting started -1. Click the **Use this template** button in GitHub and follow the on-screen instructions to **Create a new repository**. +Requires [Node 16](.nvmrc) and one of: -2. Once the repository has been generated, open it in Gitpod by appending `https://gitpod.io#` to the GitHub url. +- PHP 8.3 or newer (with the pdo_sqlite extension) + Composer on your + machine + (Drush comes with the backend - no global install needed), or +- [DDEV](https://ddev.readthedocs.io) or [Lando](https://lando.dev) (Docker) - Example: `https://gitpod.io#github.com/druxt/quickstart-druxt-site-tome` +On Windows, use the dev container, WSL2, or a container backend - see +[Windows](#windows). - _Note:_ If this is your first time using Gitpod, you can signup for a free plan with your Github account. +[nvm](https://github.com/nvm-sh/nvm) or [mise](https://mise.jdx.dev/) users: +`nvm use` / `mise install` provides the pinned versions. -3. Wait for your codebase to build. +### One-command setup (local PHP + SQLite, no Docker) - _Note:_ To speed up this step, enable Prebuilds by follow the instructions @ https://www.gitpod.io/docs/prebuilds#enable-prebuilt-workspaces +1. Create your repository from this template (or clone it), then from + the repository root: + ```bash + npm run setup + ``` -## How to use it + This installs the frontend dependencies, provisions Drupal from the + committed config and Tome content (SQLite, throwaway), starts the + backend, and writes `BASE_URL` to `.env`. -Your environment contains a pre-install, pre-configured and running instance of Drupal with Tome, and Nuxt with the DruxtSite module enabled. + `make setup` works too, as do `make dev`, `make generate`, `make +info`, `make reset`, etc. -You can access the services in your browser, via the **Remote Explorer** extension, or via the URL pattern: `https://[PORT]-[GITPOD_ID].[GITPOD_SERVER].gitpod.io` +2. Develop against the live backend: + ```bash + npm run dev + ``` -## Services + - Drupal backend: http://127.0.0.1:8888 + - Nuxt frontend: http://localhost:3000 (or the next free port up to + 3009, which it prints) + - One-time Drupal login: `npm run login` + +3. Build the full static site: + + ```bash + npm run generate + ``` + + Ensures the backend is up, then writes the deployable output to + `nuxt/dist/`. + +`npm run dev`, `npm run start` and `npm run generate` all automatically +start the local backend if it is not already running, and leave +external backends alone. + +Other commands: `npm run stop`, `npm run reset` (fresh site), `npm run +info`, `npm run login`, `npm run devtools --