Modular Publishing
Every TypeScript file this package generates is written into a namespace-derived directory tree that mirrors your PHP namespace structure. This is not an opt-in feature — there is no flat-output mode, and no configuration toggle to disable it. Models, enums, resources, form requests, broadcast events, and routes are all placed using the same namespace-derived path.
NOTE
In V1 of this package, modular output was an opt-in setting alongside a default flat-directory mode. Supporting both styles required nearly duplicate code paths for every feature and was error-prone — especially as the package grew from 3 feature groups to 7+. V2 removed the flat mode entirely; output is always namespace-derived now. See the V2 upgrade guide if you're upgrading from V1.
Output Structure
The output structure reflects your PHP namespaces, and every feature this package publishes participates in it — not just models and enums. A typical App\*-namespaced application, alongside a secondary Accounting\* module, produces something like:
resources/js/types/data/
├── app/
│ ├── enums/
│ │ ├── role.ts
│ │ └── index.ts
│ ├── models/
│ │ ├── user.ts
│ │ ├── admin/
│ │ │ ├── store.ts
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── events/
│ │ ├── OrderShipped.ts
│ │ ├── UserRegisteredEvent.ts
│ │ └── index.ts
│ └── http/
│ ├── controllers/
│ │ ├── post-controller.ts
│ │ └── index.ts
│ ├── requests/
│ │ ├── store-post-request.ts
│ │ └── index.ts
│ └── resources/
│ ├── user-resource.ts
│ └── index.ts
├── accounting/
│ ├── enums/
│ │ ├── invoice-status.ts
│ │ └── index.ts
│ ├── models/
│ │ ├── invoice.ts
│ │ └── index.ts
│ └── http/
│ └── resources/
│ ├── invoice-resource.ts
│ └── index.ts
├── broadcast-channels.ts
├── broadcast-events.ts
├── echo-broadcast-events.d.ts
├── inertia-config.d.ts
├── vite-env.d.ts
├── laravel-ts-collected-files.json
└── laravel-ts-global.d.tsEach namespace directory gets its own barrel index.ts file that exports every type within that directory — see Barrel Files below.
NOTE
Broadcast event files are the one exception to kebab-casing: they keep their original PHP class name (OrderShipped.ts, not order-shipped.ts), since that name also has to match the class re-exported from the file. Every other feature (models, enums, resources, form requests, and route/controller files) is kebab-cased.
The root-level files above are combined, non-namespaced output — they aren't tied to any one class, so they don't get a namespace directory. broadcast-channels.ts, broadcast-events.ts, laravel-ts-collected-files.json, vite-env.d.ts, and inertia-config.d.ts are all generated by default; echo-broadcast-events.d.ts requires an Echo package to be installed; laravel-ts-global.d.ts requires globals.enabled (off by default); and a JSON definitions file (laravel-ts-definitions.json by default) requires json.enabled (also off by default).
How It Works
Every namespace path is computed by LaravelTsPublish::namespaceToPath(), which:
- Strips the class name, keeping only the namespace.
- Applies the configured
namespace_strip_prefix, if the namespace starts with it (see Stripping a Namespace Prefix). - Kebab-cases each namespace segment individually, then joins them with
/.
| PHP Class | Output File |
|---|---|
App\Models\User | app/models/user.ts |
App\Enums\Role | app/enums/role.ts |
Accounting\Models\Invoice | accounting/models/invoice.ts |
Shipping\Enums\ShipmentStatus | shipping/enums/shipment-status.ts |
App\Domain\Billing\Models\Invoice | app/domain/billing/models/invoice.ts |
Nested Namespaces
Namespaces of any depth are preserved as nested directories — there's no limit to how deep a namespace can go. For example, App\Models\Admin\Store produces a nested admin/ directory inside app/models/:
app/models/
├── admin/
│ ├── store.ts
│ └── index.ts
├── user.ts
└── index.tsAutomatic Relative Imports
Import paths between generated files are computed automatically by LaravelTsPublish::relativeImportPath(), based purely on the two namespace paths involved — never hand-written, and never dependent on a path alias being configured:
| From | To | Result |
|---|---|---|
blog/models | blog/models | . |
blog/models | blog/enums | ../enums |
app/models | blog/enums | ../../blog/enums |
models | models/videos | ./videos |
Same-or-descendant paths are prefixed with ./ (a bare specifier like videos would otherwise be treated as a package import by TypeScript, not a relative path); ancestor paths walk up with one ../ per directory level before descending back down to the target.
// accounting/models/invoice.ts
import { Payment } from "."; // Same namespace (accounting/models)
import { User } from "../../app/models"; // Cross-module import
import { InvoiceStatusType } from "../enums"; // Sibling namespace (accounting/enums)
export interface Invoice {
id: number;
user_id: number;
number: string;
status: InvoiceStatusType;
subtotal: number;
tax: number;
total: number;
// ...
}
export interface InvoiceRelations {
user: User;
payments: Payment[];
// ...
}
export interface InvoiceAll extends Invoice, InvoiceRelations {}Stripping a Namespace Prefix
If your modules live under a common namespace prefix (e.g. Modules\), strip it from the output path with the namespace_strip_prefix config option:
// config/ts-publish.php
'namespace_strip_prefix' => 'Modules\\',| PHP Class | Without Strip Prefix | With 'Modules\\' Strip Prefix |
|---|---|---|
Modules\Blog\Models\Article | modules/blog/models/article.ts | blog/models/article.ts |
Modules\Shipping\Enums\Carrier | modules/shipping/enums/carrier.ts | shipping/enums/carrier.ts |
This keeps the output directory structure clean by removing the redundant prefix. The default is an empty string, so no prefix is stripped unless you configure one.
Barrel Files
Each namespace directory receives its own barrel index.ts file, alphabetically sorted and deduplicated. For example, accounting/models/index.ts:
export * from "./invoice";And app/models/index.ts:
export * from "./address";
export * from "./order";
export * from "./product";
export * from "./user";
// ... all models in this namespaceThis lets you import from a namespace root instead of a specific file:
import { User, Order } from "@js/types/data/app/models";
import { Invoice } from "@js/types/data/accounting/models";
import { InvoiceStatusType } from "@js/types/data/accounting/enums";TIP
Barrel files are generated per-feature — models, enums, resources, form requests, and broadcast events each get their own barrel per namespace directory. Running ts:publish --preview (or -v) prints each one separately, labeled e.g. Model Barrel Files: or Enum Barrel Files:, alongside the individual per-class file contents.
Applies Across Every Feature
Namespace-derived output isn't unique to models — it's the same mechanism used everywhere in this package:
- Models —
{Model},{Model}Mutators,{Model}Relations, and enum-resolved{Model}Resourceinterfaces. - Enums — the enum object and its generated type aliases.
- API Resources — resource interfaces.
- Form Requests — request payload interfaces.
- Broadcast Events — event interfaces.
- Routing — route helper files, one per controller, placed at
{namespacePath}/{controller-name}.ts.
Broadcast channels, the global declaration file, JSON output, the watcher file, the Inertia augmentation, and the Vite env augmentation are unaffected — they're single combined files by nature and aren't tied to any one class's namespace.