Files
core/CONTRIBUTE.md
T

10 KiB

Contributing to boboko-core

This is a Composer library (and an npm package of the same name — see JS/CSS), not a runnable app — you can't php artisan serve it directly. To develop and verify changes, you need a consumer app wired to a local checkout, plus a real database, since a large part of this package (Lunar models, migrations, Filament panel resources) can only be meaningfully verified against a live Lunar install.

Local dev setup

Consumer apps (3dealer, boboko-test, …) are checked out next to this repo:

RadicalElements/
├── boboko-core/    (this repo)
└── 3dealer/        (or boboko-test, ... — consumer app, Docker-based)

Two modes: local and repo

A consumer app can consume core in one of two modes, and carries the wiring for both. The inactive one is parked under an underscore-prefixed key:

local — your ../boboko-core checkout repo — tagged releases from the forge
composer.json repositories: path repo ../boboko-core ("symlink": true) repositories: VCS repo https://code.radical-elements.com/boboko/core.git
package.json @boboko/core: file:../boboko-core @boboko/core: git+https://code.radical-elements.com/boboko/core.git#semver:0.x
Docker Compose bin/dc-core.sh (dev + docker-compose.core-dev.yml overlay) bin/dc (dev only)
  • The committed state is always repo mode. Local mode rewrites both lockfiles to point at ../boboko-core, which doesn't exist on the server — never commit it.
  • Both sides use an open 0.x range: "boboko/core": "0.*" in Composer, #semver:0.x in npm. Don't use a caret: below 1.0.0, ^0.27.0 means >=0.27.0 <0.28.0 in both tools, so it would silently refuse the next minor.
  • docker-compose.core-dev.yml bind-mounts ../boboko-core into the containers — at /var/www/boboko-core for app/queue/scheduler (where the path repo resolves from /var/www/html) and at /boboko-core for vite (where file:../boboko-core resolves from /app). Inside the app container, vendor/boboko/core is a symlink into that mount.

Switching modes: bin/core-mode

Don't swap the keys by hand — each consumer app ships a bin/core-mode script:

bin/core-mode          # print the current mode
bin/core-mode local    # work against ../boboko-core
bin/core-mode repo     # back to tagged releases

It swaps the composer.json / package.json wiring, runs down with the old mode's Compose wrapper and up with the new one, then waits until the entrypoints have re-resolved core. Running it for the mode you're already in skips the edits and just restarts the stack — in repo mode, that's how you pick up a newly pushed tag.

(3dealer has bin/core-mode and bin/deploy; they're app-agnostic, so other consumer apps can copy them as-is.)

Day to day in local mode

Use bin/dc-core.sh for every Compose command (bin/dc-core.sh exec app …, bin/dc-core.sh logs -f, …) — plain docker compose or bin/dc leaves the core mount out.

The dev entrypoints re-resolve core on every boot: docker/entrypoint.sh runs composer update "boboko/*" and docker/entrypoint-vite.sh runs npm update @boboko/core. So:

  • Whatever branch is checked out in ../boboko-core is what the app runs. Switching core branches switches the app's code — check which branch you're on before debugging "missing" features.

  • PHP edits to existing files show up immediately (it's a symlink). New classes, new migrations, or composer.json changes need a re-resolve: restart the stack, or run

    bin/dc-core.sh exec app composer update boboko/core --with-all-dependencies
    

    Skipping this is the most common cause of "my change isn't showing up."

  • In 3dealer, the app container's vendor/ is a named Docker volume, so the host's vendor/ directory is stale — inspect packages inside the container, not on the host.

JS/CSS: a real npm package

Core's Stimulus controllers and CSS ship as the @boboko/core npm package, installed into the consumer's node_modules — as a symlink to ../boboko-core in local mode, as a real copy of the tagged release in repo mode. It's a real package (rather than files read out of vendor/) so npm installs core's own dependencies (leaflet, @hotwired/stimulus) transitively, the same way Composer does for PHP.

Public entry points (package.json exports):

Import File
@boboko/core resources/js/index.js — the stable barrel (registerCheckout, registerWishlist, …)
@boboko/core/vite-plugin vite-plugin.js — boboko()
@boboko/core/css/* resources/css/*
@boboko/core/checkout, @boboko/core/checkout/* resources/js/checkout/…

A consumer imports from the @boboko/core barrel only — not from a module's internal files — so the internal layout here can change without breaking every consumer:

// consumer app's resources/js/app.js
import { registerCheckout, registerWishlist } from "@boboko/core";
registerCheckout(application);
registerWishlist(application);
// consumer app's vite.config.js
import { boboko } from "@boboko/core/vite-plugin";

export default defineConfig({
    plugins: [
        laravel({
            input: [
                // Core's structural checkout styles load first, so the app's own theming wins.
                "node_modules/@boboko/core/resources/css/checkout.css",
                "resources/css/app.css",
                "resources/js/app.js",
            ],
        }),
        boboko(),
    ],
});
{{-- consumer app's layout --}}
@vite(['node_modules/@boboko/core/resources/css/checkout.css', 'resources/css/app.css', 'resources/js/app.js'])

boboko() owns the Vite settings the local-mode symlink needs, so consumers don't hand-copy them: it excludes @boboko/core from dependency pre-bundling (otherwise Vite serves a stale cached copy after you edit core), pre-bundles leaflet/@hotwired/stimulus explicitly, and turns on resolve.preserveSymlinks and server.watch.followSymlinks so bare imports resolve from the consumer's node_modules and core edits trigger HMR. All of it is a harmless no-op against a real installed copy in repo mode.

Releasing a version

  1. Bump "version" in both composer.json and package.json — they must match.

  2. Add a CHANGELOG.md entry under the new version. While pre-1.0, a new capability for consuming apps is a minor bump (0.27.x → 0.28.0); a fix, redesign or internal swap with no new capability is a patch bump.

  3. Commit, tag vX.Y.Z, and push the commit and the tag:

    git tag v0.27.5
    git push origin master v0.27.5
    

A tag alone changes nothing in production — each consumer app has to pick it up and deploy (below).

Deploying a consumer app

Production only ever runs what the app's committed lockfiles pin. Each consumer app ships bin/deploy, which:

  1. Refuses to run on the wrong branch, with uncommitted changes (other than the core wiring files), or behind origin.
  2. Runs bin/core-mode repo — switching from local mode if needed, restarting either way — so both lockfiles resolve the newest 0.x tag. It warns if ../boboko-core has a newer tag than what resolved (usually an unpushed tag).
  3. Commits the lockfile bump (Chore: Bumping boboko/core to X.Y.Z) if there is one, shows what will be pushed, and asks for confirmation.
  4. Pushes, then runs vendor/bin/envoy run deploy against the host in .env.envoy.

Envoy (Envoy.blade.php) then, on the server: git reset --hard + git pull, docker compose build (the production image target runs composer install --no-dev and npm ci from the committed lockfiles — this is where the core tag actually lands), up -d, caches config/routes/events, restarts queue and scheduler, and regenerates Stoic thumbnails. The production entrypoint skips Composer entirely and runs migrations (including core's), seeders, the Meilisearch sync, and artisan optimize.

After deploying you're left in repo mode — bin/core-mode local to go back.

When a change spans core and the app (e.g. a core migration plus an app model cast that depends on it), ship them together: tag core first, then commit the app change and deploy — bin/deploy bumps the lock to the new tag in the same deploy.

Verifying changes against a real database

There is no automated test suite for this package — too much of Lunar's behavior (table prefixing, nested sets, translatable attributes, Filament panel filters) only breaks in combination, against real Postgres, in a way that's impractical to fake in isolation. Instead, verify changes directly against a consumer app's live database. The practical workflow used throughout this package's MigrateImport feature:

One-off checks, via php artisan tinker --execute="..." in the consumer app's container:

./bin/dc-core.sh exec app php artisan tinker --execute="
use Modules\Core\MigrateImport\Shopify\Resolvers\ProductTypeResolver;
\$type = (new ProductTypeResolver())->resolve('Test Type');
echo \$type->name . PHP_EOL;
"

Multi-step scripts (creating related records, checking round-trips), via a raw PHP script bootstrapped like an Artisan command — this avoids tinker's persistent-session quirks and more closely matches how code actually runs in a queued job:

./bin/dc-core.sh exec app php -r "
require 'vendor/autoload.php';
\$app = require 'bootstrap/app.php';
\$kernel = \$app->make(Illuminate\Contracts\Console\Kernel::class);
\$kernel->bootstrap();

// ... your verification code ...
"

Always clean up fixtures created this way — either via DB::statement('DELETE FROM ...') in the same script (respecting foreign key order — Lunar has cascading relations like lunar_customer_group_product that block naive deletes), or leave them if they're harmless and the DB is a disposable dev/test instance.

Non-obvious Lunar bugs/traps hit while building against it are documented in docs/lunar.md, not here — check there before debugging something that looks like a Lunar quirk.

Code organization

Feature work lives under src/<Feature>/, with models in a Models/ subdirectory (see src/Auth/Models, src/Customer/Models, src/MigrateImport/Models). Source-specific importer logic is grouped by source name (e.g. src/MigrateImport/Shopify/), with resolver classes (one responsibility each — resolve-or-create a single Lunar entity) grouped further under Resolvers/.