Skip to content

Releases: Synerdy/modsx

v1.0.0-beta.5

v1.0.0-beta.5 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Sep 08:46

Fixed

  • The documented install command did not work on Windows. PowerShell strips
    a ^ from an argument whether it is quoted or not, so
    composer require --dev synerdy/modsx:^1.0@beta reached Composer as
    1.0@beta and was refused as an exact version that does not exist. Reported
    from a real terminal, having been written into the README by someone who had
    watched the same thing happen while testing and documented the broken form
    anyway.

    Every shell command on the page now uses a tilde. ~1.0@beta is the same
    range as ^1.0@beta, and ~0.7.0 the same as ^0.7.

    The quiet half was worse than the loud one. composer require --dev synerdy/modsx:^0.7 succeeds on PowerShell and writes 0.7 into
    composer.json - an exact pin where a range was meant, with nothing to say so.
    Measured by reading composer.json back, not inferred from the resolver's
    output, which looked correct.

v1.0.0-beta.4

v1.0.0-beta.4 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 06 Sep 23:54

Added

  • modsx:scaffold creates files, not only directories. An entry whose last
    segment has a dot in it names a file:

    'scaffold' => [
        'app/Http/Controllers/{Studly}',
        'resources/views/{kebab}',
        'docs/{kebab}.md',
        'config/{kebab}.php',
    ],

    It closes the last place where the prefix still had to be typed by hand.
    Directories were covered - modsx:scaffold Blog docs works out
    docs/modsx-blog for you - but a single file at docs/modsx-blog.md,
    config/modsx-blog.php or routes/modsx-blog.php had to be made in an
    editor, spelling the prefixed name yourself. That is the one mistake this
    package exists to prevent, and it was the one it left you to make.

    The file is created empty, and an existing one is never written over. Nothing
    is put inside on purpose: the same entry shape makes a Markdown page and a
    PHP config file, and a heading that helps in the first is a syntax error in
    the second. An empty config/modsx-blog.php reads back as 1 rather than an
    array until you fill it in - measured rather than assumed, and it does not
    stop the application booting.

    A file is told from a directory by the last segment having a dot, rather than
    by a list of blessed extensions, because what a module keeps is yours to
    decide. A directory deliberately named modsx-blog.v2 would be read as a
    file; rare enough to accept, and the answer is to write the entry without a
    dot.

  • A module can keep its documentation, in docs/. docs/modsx-blog.md for
    one page, docs/modsx-blog/ for a directory of them - the same two shapes
    configuration already had, and for the same reason.

    Almost nothing was needed to support it. The name has always identified the
    module in a file as much as in a directory, and the rule cuts at the first
    dot, so .md behaved like .php and .css already did. What was missing
    was that docs was not in scan_paths, which made the whole directory
    invisible. It is there by default now.

    Everything else follows without a line of special handling: documentation is
    copied by modsx:backup, brought back by modsx:restore, counted as a
    change by modsx:diff, removed by modsx:delete, and listed by
    modsx:path. That is the point of keeping it here rather than beside the
    project's own - restoring a module from three weeks ago gives you the
    documentation it had three weeks ago, not today's description of code that
    no longer exists.

    modsx:info names it, since that is where somebody goes to find out what a
    module is and "there is a page about this" answers it better than one row in
    a list of twenty. Recognised by sitting under the scanned docs directory
    rather than by a setting of its own: a second setting could disagree with
    scan_paths, and documentation would then stop being part of the module
    while still being labelled as its documentation.

    A directory of pages makes a module, a lone page does not - the rule
    everywhere else, applied here too. So docs/modsx-shop/ brings Shop into
    being before any of its code exists, while docs/modsx-shop.md alone is
    reported by modsx:doctor as a page naming a module that is not there.

    Adding a scan path is a breaking change for anyone who already keeps a
    docs/modsx-*.md: it becomes module content, which means modsx:delete
    removes it. Cheap while this is a prerelease.

Fixed

  • The check that the README names the current prerelease was asking the wrong
    question.
    It demanded the newest tag by name, which is false during exactly
    the moment it matters: preparing a release writes the new version, commits
    it, and only then tags it, so the README is briefly ahead of every tag. The
    first release after adding the check would have been blocked by it.

    It now asks only that the README has not fallen behind - that it names no
    prerelease an existing tag has superseded. That is the failure it was written
    for, and being ahead is no longer mistaken for it.

v1.0.0-beta.3

v1.0.0-beta.3 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 06 Sep 19:14

Added

  • modsx:prune --duplicates removes versions that record nothing, meaning
    a version holding exactly what the version after it holds. It ignores
    --keep: the two ask different questions of a version, one about how much
    history to hold on to and one about history that says nothing, and the
    answers do not combine.

    Only versions next to each other count, and the distinction is the whole
    design. Two identical versions with a different one between them are not a
    repeat but a return - the module was changed and changed back - and removing
    the later one would leave latest() naming a state the application is not
    in, which modsx:status, modsx:backuplist and a bare modsx:restore all
    read. So 0001 A, 0002 A, 0003 A collapses to 0003, while
    0001 A, 0002 B, 0003 A is left exactly as it is.

    The newest of each run is what stays, for the same reason.

    A version carrying a comment is always asked about, never assumed, and the
    comment is shown while asking. The content survives in the version it
    duplicates, but a note about a moment is not content and only its author can
    say whether it still matters. Where there is nobody to ask - --force,
    --json, no terminal - those versions are kept and the answer says so;
    --with-comments is how a script says it has already decided.

    Archived migrations count towards being identical here, unlike in the check
    that skips an unchanged backup. That check asks whether a restore would do
    anything; this one is about deleting, and a version holding the only copy of
    a migration must not look disposable.

    Versions a snapshot or the state pointer names are never removed, as with
    --keep.

Changed

  • modsx:backup no longer copies a module that has not changed. It
    compares against the newest version first and says what it found:

     INFO  Nothing to back up: [Blog] is identical to version 0004.
    

    A second copy of a module that has not moved records no fact and costs a
    full directory, so it was never what anyone wanted - but it was the default,
    and --skip-unchanged had to be remembered to avoid it. The flag is now the
    other way round: --even-if-unchanged writes a version anyway, for the one
    case where a second copy of the same thing is the point, marking a moment
    that matters even though the code has not.

    The safety copies modsx:restore and modsx:delete take before they change
    anything follow the same rule, and that is where most duplicate versions were
    coming from: restoring twice in a row wrote the same state twice. When a
    version already holds the current state, that is what those copies exist for.

    A comment does not force a version on its own. If nothing changed, the
    version it was meant for is never written, and the command says the comment
    went nowhere rather than losing it quietly.

    Breaking, and deliberately so while this is a prerelease: --skip-unchanged
    is gone, and BackupManager::backup() takes $evenIfUnchanged where it took
    $skipUnchanged. Scripts passing the old flag will be told it does not exist
    rather than quietly doing something else.

v1.0.0-beta.2

v1.0.0-beta.2 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 06 Sep 00:55

Fixed

  • A backup no longer fails while a file watcher is running. On Windows
    rename() is refused for as long as another process holds a handle inside
    the directory, and every write here ends in a rename(): the result is
    assembled in a staging directory and moved into place only once complete. A
    running npm run dev was enough - Vite's watcher notices the new files,
    opens the directory to watch it, and the move that would have finished the
    backup failed with nothing but Failed to copy [...] to explain it.

    Reported against 1.0.0-beta.1 with the diagnosis already done, including the
    detail that decided the fix: a rename immediately after the copy succeeds,
    one 250ms later does not. The watcher's handle is not transient, it holds for
    as long as the watch does, so retrying alone would only have made the failure
    slower. What rescues it is copying, which needs no exclusive handle.

    Moves now go through PathMover, which separates two guarantees that a single
    "move" was quietly conflating:

    • place cares that the destination ends up whole. Its source is a staging
      directory nobody else refers to, so renaming, and copying when that is
      refused, are equally good. Used by backup, export, import and restore.
    • move cares that the source is gone, because something else is about to
      stand where it did. Copying cannot promise that - deleting the original can
      fail halfway, which on Windows is exactly what a held file does - so it
      renames or it fails. Used only where the application's own files are moved
      aside.

    The second is why restoring over a file something has open now stops before
    touching anything and says so, rather than emptying the module of everything
    except the file nobody could overwrite. That case cannot be engineered away:
    on Windows an open file cannot be replaced. It can be refused honestly.

  • modsx:doctor finds staging directories a run left behind, and
    --fix removes them. A move that fell back to copying cannot always delete
    its source, so an interrupted run can leave one. They were invisible to every
    other check, since their names begin with a dot and File::directories()
    does not return those.

Documentation

  • A "Windows and file watchers" section, saying what to exclude from a
    watcher and why it is worth doing even now that backups survive without it:
    a watcher that sees a backup being written reloads the page over files that
    are not part of the application.

    Deliberately advice rather than automation. Editing somebody's
    vite.config.js on install would mean rewriting arbitrary JavaScript from
    PHP with no parser, in a file that may not exist, may be TypeScript, and may
    configure watch.ignored as a function - and it would fix one watcher out of
    a class that also includes editors, antivirus and folder sync.

v1.0.0-beta.1

v1.0.0-beta.1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 03 Sep 21:25

Added

  • modsx:snapshot, modsx:rollback, modsx:snapshotlist, modsx:snapshotprune
    — the whole project at one moment.
    A snapshot records which version of each
    module was current together, which answers the question a per-module backup
    cannot: I cannot restore Blog from three weeks ago, because back then it
    depended on a different User.

    A snapshot copies nothing. The versions it names are already in the backup
    tree, so what is written is a few hundred bytes of version numbers in
    modsx-backups/_snapshots/0002.json; copying them would double the disk cost
    and give one version two places to live. A module unchanged since its last
    backup gets no new version either, only another reference to the one it had,
    so a snapshot of an untouched project writes nothing but the snapshot. That
    is deliberate: a snapshot nobody minds taking is one that will be there when
    it is needed.

    What modsx:rollback guarantees, stated exactly, because the difference
    matters. Every version the snapshot names is confirmed to exist before
    anything is touched - that is the failure that actually happens, and it is
    caught while the application is whole. A snapshot of the current state is
    taken first and its number printed at the end. Each module is then staged and
    swapped on its own, and a failure part-way puts the modules already restored
    back where the safety snapshot found them. What it is not is one filesystem
    transaction; there is no such thing across a dozen directory trees, so the
    last step is compensation, and the safety snapshot is named rather than left
    to be worked out.

    It does not touch the database. Rolling code back does not roll a schema
    back, and no migrations are run in either direction.

  • modsx:deps — which modules a module needs, worked out by reading it.
    The graph is derived, not declared. A module called Media appears in other
    modules' code as ModsxMedia, modsx-media or modsx_media and in no other
    form, so a reference to it is something that can be found rather than
    something someone has to remember to write down.

    That is the whole argument against a requires: list: it is a register
    nothing checks, and a snapshot built from a stale one is quietly incomplete -
    worse than no snapshot, because it is trusted. A mention in a comment or a
    string counts here too, since the mistake that causes is a snapshot holding
    one module too many, while the opposite mistake breaks a rollback.

    Name boundaries are respected, so ModsxBlogPost refers to BlogPost and not
    also to Blog - the same rule that decides which module owns a file. The snake
    form is the exception, where a suffix is ordinary: modsx_media_assets is
    Media's table.

    Configuration under modsx.dependencies adds edges for what reading cannot
    see - a class name assembled from a string, a listener wired up elsewhere -
    and never replaces the ones found in the code. An edge found in both is
    reported as found, because that is the claim that can be pointed at.

  • modsx:status — every module in one table: what state it is in, which
    version its working tree came from, what the newest backup is, and how many
    files have changed since.

     Module    State       Current   Latest backup   Changes
     Blog      modified    0001      0002            1
     Shop      clean       0001      0001            0
     Billing   untracked   -         -               -
     Admin     missing     -         0001            -
    

    The frame is the one version control already taught everyone: Current is
    where the tree came from, so Changes counts what has happened since rather
    than the distance to the newest backup, which is a different question and
    gets its own column. That makes a module able to be clean and behind at the
    same time, which is the combination worth being told about - backing up from
    an older version builds the next one on top of it, and the listing now says
    so before that happens.

  • A module's working tree records the version it came from, in
    modsx-backups/<Module>/modsx-state.json, written by modsx:backup and
    modsx:restore. modsx:import deliberately records nothing: it adds a
    version to the backup tree without touching the application, so the working
    tree did not come from it, and saying otherwise is the one thing this record
    must never do.

    It never decides whether a module exists. Discovery stays exactly what it
    was, a directory named by the convention, and ModuleLocator knows nothing
    about this file: a module made with mkdir has no record and is reported
    untracked, which is the truthful answer rather than a gap. Delete every one
    of these files and the package behaves as it did before they existed - a
    property with a test on it, because it is what keeps the convention the only
    source of truth about what a module is.

    It lives beside the versions it points at because it means nothing without
    them: when they go, it goes. It cannot live inside the module either, or a
    backup would copy it and version 0004 would contain a file claiming the
    module is at 0003.

  • modsx:doctor reports a record naming a version that has been pruned.
    Informational, not a problem: the record still says truthfully where the tree
    came from, and modsx:status carries on by measuring against the newest
    version instead. Deleting the file is a complete fix.

Changed

  • modsx:prune no longer deletes a version that a snapshot names, the way
    a tag keeps a commit from being collected. Without it a rollback would
    discover the loss at the moment it needed the version, which is too late to
    be useful. It lists what it held back and why, since a command that silently
    removes less than the age rule offered looks like a bug rather than a
    safeguard.

    Deliberately with no override. Everywhere else in this package --force
    means do not ask me, and letting it also mean ignore a safeguard would
    let a scripted prune quietly strand every snapshot naming those versions.
    Releasing them is modsx:snapshotprune's job: let the snapshot go, and the
    versions follow.

  • modsx:doctor reports a snapshot naming a version that is no longer
    there.
    Informational, and only reachable by editing the backup tree by
    hand, since prune holds those versions back: the snapshot is still listed and
    still looks usable while the one thing it exists for has become impossible.

Fixed

  • The release workflow no longer asks GitHub to mark a prerelease as the
    latest release.
    v1.0.0-beta.1 is the highest tag in the repository, so
    the newest-tag check claimed the Latest badge for it while the suffix check
    marked it a prerelease - two contradictory instructions in one call. Only
    stable tags are candidates now, which also stops a beta denying the badge to
    the stable release that follows it, since git's version sort puts
    v1.0.0-beta.1 above v1.0.0.

Documentation

  • A .gitignore line for anyone who commits their backup tree:
    modsx-backups/*/modsx-state.json. Which version your working copy came
    from is a local fact, not a shared one, and two people restoring different
    versions would otherwise conflict over it.

v0.7.0 - comparing two versions, and a manifest that can't escape

Choose a tag to compare

@Synerdy Synerdy released this 01 Sep 23:18

Comparing two backup versions

modsx:diff has always compared your application against a version. Give it a second version and the application drops out of the comparison entirely:

php artisan modsx:diff Blog 0002 0004

The version named first is the baseline in both forms, so modsx:diff Blog 0002 and modsx:diff Blog 0002 0004 ask the same question from the same starting point — only the other side moves. Swapping the two arguments gives the same comparison seen from the other end: what was added becomes what is gone.

The wording follows. Against the application, the useful frame is what a restore would do next:

Added (restore would delete)
Modified (restore would overwrite)
Removed (restore would bring back)

Between two versions no restore is in sight, so describing those numbers by an action nobody asked for would simply be untrue:

Added (only in 0004)
Modified (differs between the two)
Removed (only in 0002)

Your working tree is not read at all in this mode, so the answer is the same whatever state the application happens to be in. --summary and --json work here too; the JSON carries from and to in place of version, which is how a script tells the two modes apart by shape alone.

A manifest can no longer place files outside the project

modsx:import takes a version number and a list of paths out of a manifest that somebody else wrote, and checked neither.

A version of ../../.. and a path of ../../escaped each wrote outside the project root. Both were reproduced before being fixed, rather than assumed — and ZipArchive::extractTo() was verified not to be the way in, since it sanitises entry names itself.

The version is now validated in the one place it becomes a path, and a manifest path is refused if it escapes with .., is absolute, or carries a drive letter.

If you only ever import archives you exported yourself, none of this was reachable. It starts to matter the moment an archive arrives from anywhere else.

A generator's options no longer need --

php artisan modsx:make component blog.alert --view      # now
php artisan modsx:make component blog.alert -- --view   # before

Anything else used to be refused with Symfony's The "--view" option does not exist — a message that says nothing about the separator you forgot. Which is easy to forget: --resource, -m, --view and --api come up constantly.

modsx:make answers to one option of its own, --dry-run. It now sorts the raw tokens itself and passes everything else to the generator, which is also why a generator from any package needs nothing declared. Writing -- still works, and remains the way to reach a generator option that collides with ours.

The cost, stated plainly: a misspelling of --dry-run is no longer caught by Symfony. It is checked for by name instead, and answered with Did you mean --dry-run? rather than forwarded to a generator that would blame itself.

An exported archive is named after its module

Blog-0002.zip, not 0002.zip — which said nothing at all once the file had been moved or mailed anywhere.

modsx:prune removes both names, so an archive written under the old one is still swept along with the version it belongs to, rather than left behind for good.

modsx:backuplist counts files and archived migrations

A version has held both since 0.3.0, so listing its directories alone described less than half of what was in it.

Version   Created               Directories   Files   Archived   Comment
0002      2026-09-01T21:14:07             3       2          1   before the rewrite

Also

  • modsx:backup names the files it did find when a module has files but no directories, instead of reporting the module as missing. It still refuses: a module is a set of directories, and the unclaimed-file check rests on that.
  • public/build joins the default exclude list, so a compiled asset bundle is not walked while looking for a module's directories.
  • CI rebuilds docs/ and fails if it differs from what is committed, and checks that the two READMEs keep the same headings, the same number of code blocks and the same number of table rows.

Upgrading

While Modsx is on 0.x, this release will not arrive with a plain composer update. Ask for it by name:

composer require --dev synerdy/modsx:^0.7

Composer treats anything below 1.0.0 with pre-release caution: there, ^0.6.1 means >=0.6.1 <0.7.0, putting the minor where the major normally sits. So composer update stays on the minor you installed — by design, not by accident — and composer why-not synerdy/modsx 0.7.0 will tell you as much.

The one thing to check before upgrading: if anything of yours refers to an exported archive by filename, it is Blog-0002.zip now.

v0.6.0 - reaching the directories a module doesn't own

Choose a tag to compare

@Synerdy Synerdy released this 31 Aug 22:41

Reaching the directories a module doesn't own

A module's own views are resources/views/modsx-blog/, but its layout is one slice of the application's layouts/ — the framework's directory first, the module second, exactly as in resources/css/modsx-blog/. Neither command could express that: both write the module at the front of a name.

The workaround was typing modsx-blog by hand — into make:view layouts.modsx-blog.app, or into mkdir — which is the mistake these commands exist to prevent.

modsx:make layout, page, partial

php artisan modsx:make layout blog.app     # -> views/layouts/modsx-blog/app.blade.php
php artisan modsx:make page blog.index     # -> views/pages/modsx-blog/index.blade.php
php artisan modsx:make partial blog.head   # -> views/partials/modsx-blog/head.blade.php

There is no make:layout in Laravel. These are entries in the same modsx.generators table, and what makes them different is that the entry names the generator to run as well as the form:

'layout'  => ['view', 'layouts/{kebab}/'],
'page'    => ['view', 'pages/{kebab}/'],
'partial' => ['view', 'partials/{kebab}/'],

So the names are yours. 'service' => ['class', 'Services/{Studly}/'] gives you modsx:make service Blog/PostPublisher, and it appears in the interactive picker alongside Laravel's own generators.

Deliberately no component: make:component is Laravel's own and already lands correctly, writing the class and letting Laravel derive views/components/modsx-blog/ from where that class went.

modsx:scaffold takes the directories to create

Naming them makes those instead of the configured list — for the one you want now, without changing what every future module gets:

php artisan modsx:scaffold Blog resources/css
# resources/css/modsx-blog/

php artisan modsx:scaffold Blog resources/js app/Services
# resources/js/modsx-blog/
# app/Services/ModsxBlog/

Note the last one: ModsxBlog, not modsx-blog. You write the path as it looks in the project, and the form of the module's own directory is read off where that path leads:

You type It creates
resources/css, public/vendor, lang/en …/modsx-blog
app/Services, database/factories, tests/Feature …/ModsxBlog

app/, database/ and tests/ are the PSR-4 roots of a stock Laravel application, where a hyphen is not a legal PHP identifier. Everywhere else the name is only ever a path. Where that guess is wrong — a PSR-4 root of your own — a placeholder settles it: modsx:scaffold Blog "modules/Shared/{Studly}".

An existing directory is skipped and reported, as with the configured list, and a path may not contain ...

Also

  • A path typed on the command line was refused with a message about a modsx.scaffold entry the reader never touched. It has its own message now.
  • The convention table listed app/ and database/ as the namespace roots while omitting tests/.

Full detail in the CHANGELOG.

v0.5.2 - dot notation for the module

Choose a tag to compare

@Synerdy Synerdy released this 31 Aug 10:02

Separate the module with a dot, the way Laravel writes a view

A view is blog.create in Laravel's own documentation — all lower case, dots throughout. modsx:make wanted the module separated with a slash, and typing the natural form got an error about backslashes, which is unhelpful advice to someone who typed a dot.

The module now ends at the first /, \ or ., whichever comes first:

php artisan modsx:make view blog.create        # -> make:view modsx-blog/create
php artisan modsx:make view blog.admin.index   # -> make:view modsx-blog/admin.index
php artisan modsx:make config blog.services    # -> make:config modsx-blog-services
php artisan modsx:make controller Blog/PostController

Only the first separator divides — a module name can contain none — so the rest of the name keeps its own dots. Strictly additive: every existing test passed unchanged.

The output was never wrong. make:view does str_replace(['\\', '.'], '/', $name) itself, which makes modsx-blog/create and modsx-blog.create the same view. It was the input that was too narrow.

Every generator, documented

The modsx:make section now lists every generator Laravel ships, written as the call you would make through Modsx, with the name each one receives — read off real runs rather than reasoned about:

  • 28 generators take PascalCase and fall under *ModsxBlog/UserController
  • three have a form of their own: view (view path), config (kebab-case), migration (snake_case)
  • six *-table generators take no name at all — they are framework scaffolds, not anything belonging to a module

Either separator works at every one of them.

The one departure from plain Laravel

A config name is snake_case to make:config. Modsx gives it kebab-case, and has to: config/modsx_blog_services.php would not be recognised as the module's file — the rule looks for the modsx- kebab prefix — so the config would be orphaned, backed up with nothing and removed with nothing.

Tests

Assertions moved from what the command prints to where the file actually lands, across every documented form. Two of them had only been passing by accident: Testbench resolves view.paths before the base path moves, so make:view had been writing into vendor/, and make:model only uses app/Models when that directory exists.


Full detail in the CHANGELOG.

v0.5.1 - modsx:make converts the whole name

Choose a tag to compare

@Synerdy Synerdy released this 30 Aug 23:55

modsx:make converted only half the name

The module was written in the generator's form and the rest of the name passed through untouched:

modsx:make config Blog/MailSettings   ->  modsx-blog-MailSettings
modsx:make view   Blog/PostList       ->  modsx-blog/PostList

Kebab-case and StudlyCase inside one identifier — neither a config key you would type nor one Laravel would generate. Migrations were converted; nothing else was.

The entry in modsx.generators now settles the whole name:

modsx:make view      Blog/PostList          ->  modsx-blog/post-list
modsx:make view      Blog/Admin/PostList    ->  modsx-blog/admin/post-list
modsx:make config    Blog/MailSettings      ->  modsx-blog-mail-settings
modsx:make migration Blog/CreatePostsTable  ->  modsx_blog_create_posts_table
modsx:make controller Blog/PostController   ->  ModsxBlog/PostController   (untouched)

A {Studly} entry leaves the rest of the name alone, a class name already being written the way its generator wants it. Migrations stop being a special case in the code — they are one of three patterns rather than the exception. Conversion runs segment by segment, because Str::kebab('Admin/PostList') is admin/-post-list: the separator reads as a word boundary.

A longer scaffold list, commented out

The published config now offers Livewire, services, form requests, middleware, factories, seeders, tests, resources/css/, resources/js/, and view directories — all commented. The defaults are unchanged and stay short on purpose: a directory nobody fills in is invisible to git and reported by modsx:doctor, so a generous default would only make work for --fix.

Views take the same shape as everything else, the module going inside the framework's directory:

resources/views/
├── components/modsx-blog/   ->  <x-modsx-blog.card>
├── layouts/modsx-blog/
├── partials/modsx-blog/
└── modsx-blog/

layouts/modsx-blog/, not modsx-blog/layouts/ — the module comes second everywhere else in this convention, exactly as in resources/css/modsx-blog/. A starter kit's own layouts/app.blade.php carries no prefix, so no module claims it, and the two sit side by side. Modules are found at any depth.

Documentation caught up with the code

  • Limitations still said Blog and BlogPost could not coexist and that modsx:doctor reported their prefix as a conflict. Both stopped being true in 0.5.0. What remains is that a migration matching two modules goes to the longer name.
  • Installation now reads composer require --dev synerdy/modsx. Outside an artisan command the package does nothing at all — the service provider returns immediately unless the application is running in the console — and nothing in an application ever calls into it. The one case for require is named too: running modsx:* where dev dependencies are absent.
  • The naming tables no longer show a single file where the convention wants a directory, and a new paragraph says why the two name forms exist at all: directories under app/ are PSR-4 namespace segments, and a PHP identifier cannot contain a hyphen.

Full detail in the CHANGELOG.

v0.5.0 - a name identifies a module

Choose a tag to compare

@Synerdy Synerdy released this 30 Aug 22:17

Deleting a module could remove a neighbouring module's files

With both Blog and BlogPost present, Blog claimed every file whose name merely started with modsx-blog. So modsx:delete Blog removed config/modsx-blog-post.php — BlogPost's — and backing Blog up archived BlogPost's migrations into Blog's version. modsx:doctor reported the pair as an error, but nothing stopped the command.

The cause was that one name meant two different things:

resources/views/modsx-blog-admin/   ->  module BlogAdmin   (a module of its own)
config/modsx-blog-admin.php         ->  Blog's file, and BlogAdmin's too

A name identifies a module — in a file as much as in a directory

A file's name is now read exactly as a directory's is, cut at the first dot:

Path Module
config/modsx-blog.php Blog
resources/views/modsx-blog.blade.php Blog
lang/en/modsx-blog.php, lang/pl/modsx-blog.php Blog
config/modsx-blog-post.php BlogPost, and only BlogPost
config/modsx-blog-admin.php BlogAdmin

Module names are unique, so at most one module can match a file — two can never both claim one. Where a module genuinely needs several files in one place, the form is a directory: config/modsx-blog/settings.php, which Laravel reads as config('modsx-blog.settings').

Breaking: a file like config/modsx-blog-admin.php was Blog's and now is not. modsx:doctor lists every file naming a module that doesn't exist, under a new unclaimed_files key — without that, such a file would drop out of its backup silently. Existing backups are unaffected: restore reads the manifest, so a version gives back exactly what it recorded.

Migrations go to the longest module name that claims them

A migration is the one thing that cannot be named for its module and nothing else, since every migration needs its own name:

modsx_blog_create_posts_table          ->  Blog
modsx_blog_post_create_comments_table  ->  BlogPost, or Blog if BlogPost doesn't exist

Decided by the module list, with no vocabulary of migration verbs — make:migration accepts any name at all (backfill_, cleanup_), and module names are themselves sometimes verbs (Import, Update).

Blog alongside BlogPost is therefore a supported layout. modsx:doctor no longer counts it as a problem; it is listed informationally, stating how their migrations divide.

Also

  • confirmDestructive() moved into a trait used only by the three commands that declare --force. It had guarded itself with hasOption('force') because the other eleven did not have it — eleven static-analysis errors that a stale PHPStan cache had been hiding.
  • composer smoke runs a command by hand in a Testbench app, rebuilding package discovery first. Development only.

Full detail in the CHANGELOG.