Agent notes: ostrio:flow-router-extra
July 22, 2026 · View on GitHub
Use when editing this repo, shipping Atmosphere releases, or implementing / debugging routing in modern Meteor (including TS-first apps importing meteor/ostrio:flow-router-extra). Canonical long-form API: repo docs/ (not bundled — see .meteorignore). This file is the single agent-oriented surface: patterns, gotchas, and where logic lives in source.
Package identity
- Atmosphere name:
ostrio:flow-router-extra(not legacykadira:flow-router; tests sometimes aliasPackage['kadira:flow-router']for compatibility). - Version:
package.js→ keep README “Compatibility” and siblings (ostrio:flow-router-meta,ostrio:flow-router-title) aligned on release.
package.js surface
| Item | Detail |
|---|---|
| Meteor | api.versionsFrom(['1.4', '2.8.0', '3.0.1', '3.4']) |
| Core deps | modules, ecmascript, promise, tracker, reactive-dict, reactive-var, ejson, check both archs |
| Weak TS | zodern:types@1.0.13, typescript (weak) |
| Weak Blaze | templating, blaze@2.0.0 || 3.0.0 client only |
| Entry | api.mainModule('client/_init.js', 'client'), api.mainModule('server/_init.js', 'server') |
| Types | api.addAssets('index.d.ts', ['client', 'server']) + package-types.json (typesEntry) |
Public exports
Client (client/_init.js):
import {
FlowRouter,
Router,
Route,
Group,
Triggers,
BlazeRenderer,
RouterHelpers,
} from 'meteor/ostrio:flow-router-extra';
Server (server/_init.js): same minus RouterHelpers. Triggers and BlazeRenderer are empty stubs — do not use them on the server.
Singleton: FlowRouter is a Router instance with FlowRouter.Router / FlowRouter.Route attached (companion packages, e.g. ostrio:flow-router-meta).
TypeScript
- Types:
index.d.ts+package-types.json. Apps:meteor add zodern:types, Meteor TS guide, generate types someteor/ostrio:flow-router-extraresolves. - Isomorphic imports: gate
RouterHelpers(and client-only APIs) withMeteor.isClientor split modules — server bundle does not exportRouterHelpers. index.test-d.ts: keep in sync wheneverindex.d.ts,package.js, or public exports (client/_init.js,server/_init.js) change — extend assertions sotsdstays green.- Run type tests from package root:
meteor npm run test:tsd/meteor npm exec tsd(sameindex.test-d.tsvsindex.d.ts).
Routes registration
- API:
FlowRouter.route(pathDef, options?)→Routeinstance. Paths must start with/, except the catch-all'*'(see below). - Named routes: set
options.name. Use name or path fragment inFlowRouter.path(nameOrPathDef, params, queryParams)/FlowRouter.url(...). - Isomorphic: register the same table on client (navigation) and server (SSR /
matchPath, meta packages).import { FlowRouter } from 'meteor/ostrio:flow-router-extra'in both; server has nogo/ DOM.
Minimal:
FlowRouter.route('/', {
name: 'home',
action() {
// Blaze: this.render(...); React/other: mount here
},
});
With param:
FlowRouter.route('/post/:id', {
name: 'post',
action(params) {
// params.id
},
});
Implementation refs: client/router.js (route, _updateCallbacks), client/route.js (hooks, Blaze this.render).
Route groups registration
- API:
FlowRouter.group({ name, prefix, ...options })→Group. Nested groups:group.group({ ... })(lib/group-base.js). prefix: must start with/; nested prefixes concatenate.- Merge rules: child routes get
triggersEnter/triggersExitmerged (group first, then route).waitOnfrom group becomeswaitForchain on the route. - Omitted from group→route merge (stay on route / group for addons): among others
meta,link,script,title,titlePrefix— seelib/group-base.jsomitlist.
const app = FlowRouter.group({
name: 'app',
prefix: '/app',
triggersEnter: [/* shared enter */],
});
app.route('/dashboard', {
name: 'dashboard',
action() { /* matches /app/dashboard */ },
});
const admin = app.group({ name: 'admin', prefix: '/admin' });
admin.route('/users', { name: 'adminUsers' }); // /app/admin/users
Tests / examples: test/client/group.spec.js, test/common/group.spec.js.
Wildcard (404 / not-found) route
- Preferred:
FlowRouter.route('*', { name: '__notFound', action() { ... } })(or any name). Registered last internally so it does not shadow concrete routes (client/router.js_updateCallbacks). - Deprecated:
FlowRouter.notFound = { ... }— logs deprecation, rewrites toroute('*', ...)with default name__notFound(client/router.js).
FlowRouter.route('*', {
name: 'notFound',
action() {
// 404 UI
},
});
Companion packages (ostrio:flow-router-title, ostrio:flow-router-meta) document both styles.
Global options (FlowRouter instance)
| Surface | Role |
|---|---|
FlowRouter.globals | Array: push({ waitOn, waitOnResources, ... }) — merged into every route’s wait pipeline (see client/route.js / docs/hooks/waitOnResources.md). |
FlowRouter.subscriptions | Function run as global subscription hook on the internal _globalRoute (client/router.js _buildTracker). |
FlowRouter.decodeQueryParamsOnce | boolean — set true for new apps (fixes double-decode; default false for legacy). See docs/api/decodeQueryParamsOnce.md. |
FlowRouter.triggers.enter / .exit | Register global triggers with optional { only: ['routeName'] } or { except: [...] } (client/router.js _initTriggersAPI, client/triggers.js). |
FlowRouter.env | replaceState, reload, trailingSlash Meteor.EnvironmentVariables — withReplaceState, reload, withTrailingSlash helpers on client. |
FlowRouter.wait() | Defers default Meteor.startup initialize() until you call FlowRouter.initialize(options) (custom boot order). |
FlowRouter.initialize(options) | Once. Calls MicroRouter.start: click (default true), popstate (default true). Note: some markdown in docs/api/initialize.md mentions page.click; implementation uses top-level options.click / options.popstate (client/router.js). |
FlowRouter.onRouteRegister(cb) | Fires when a route is registered; payload strips heavy hooks (onRouteRegister / _triggerRouteRegister in client/router.js / lib/router-base.js). |
FlowRouter.decodeQueryParamsOnce = true;
FlowRouter.globals.push({
waitOnResources() {
return { images: ['/logo.png'] };
},
});
FlowRouter.subscriptions = function() {
// this.register(name, handle) on global route
};
FlowRouter.triggers.enter([(context, redirect) => {
if (!Meteor.userId()) redirect('/login');
}]);
Hooks (execution order)
Order matches docs/hooks/README.md:
guard(parent group → child group → route; may be async)whileWaitingwaitOnwaitOnResourcesendWaitingdataonNoDatatriggersEnter(after globalFlowRouter.triggers.enterconcatenation)actiontriggersExit
Per-file docs: docs/hooks/*.md. Implementation: client/route.js (waitOn, callAction, etc.).
Add-on keys on route/group options (title, meta, link, script, …) are for ostrio:flow-router-title / ostrio:flow-router-meta, not core router logic.
Tracker rule: do not use reactive globals (Session, etc.) inside .subscriptions in a way that trips safeToRun — error from _buildTracker in client/router.js.
Global triggers API (Triggers + FlowRouter.triggers)
FlowRouter.triggers.enter(triggers, filter?)/exit(...)—filter:{ only: ['routeName', ...] }OR{ except: [...] }, not both (client/triggers.jsapplyFilters).- Route-level:
triggersEnter,triggersExitonFlowRouter.route/ grouproute. - Signature (conceptually):
(context, redirect, stop, data)—redirect(url, params?, query?)must be synchronous;stop()aborts chain (seeclient/triggers.jsrunTriggers). Triggersexport: helpers likeapplyFilters,createRouteBoundTriggers,runTriggers— used internally; serverTriggersis{}.
RouterHelpers (client)
Source: client/active.route.js (initialized in client/_init.js with RouterHelpers = helpersInit(FlowRouter)).
Programmatic (no Blaze): use RouterHelpers methods directly:
| Method | Purpose |
|---|---|
RouterHelpers.name(pattern) | Current route name matches string / RegExp (optional params for building path to compare). |
RouterHelpers.path(pattern) | Current path matches string / RegExp. |
RouterHelpers.pathFor(pathDef, params) | Build path string (like Blaze pathFor). |
RouterHelpers.configure({ activeClass, caseSensitive, disabledClass, regex }) | Active-route styling defaults. |
With Blaze (templating present): global helpers registered — pathFor, urlFor, param, queryParam, currentRouteName, subsReady, isSubReady, currentRouteOption, plus active-route style: isActiveRoute, isActivePath, isNotActiveRoute, isNotActivePath.
Server: only pathFor-ish subset per active.route.js (pathFor, urlFor on server object) — not full client helper set.
Conflicts: built-in replaces zimme:active-route and arillo:flow-router-helpers (client/_init.js warns if those packages exist).
Repo layout (implementation map)
| Path | Role |
|---|---|
client/_init.js | Singletons, exports, deprecated-package warnings |
client/router.js | Client Router: MicroRouter, go, triggers, initialize/wait, _updateCallbacks ('*' last) |
client/route.js | waitOn, data, action, Blaze, subscriptions |
client/group.js | Group extends lib/group-base.js |
client/triggers.js | Triggers.runTriggers, filters |
client/active.route.js | RouterHelpers |
lib/router-base.js | RouterBase: path/url, globals, group, onRouteRegister |
lib/micro-router.js | History, pathToRegExp / matchPath (shared with server) |
lib/group-base.js | Nested groups, prefix merge, route() option merge |
server/router.js | matchPath, no navigation |
server/plugins/fast-render.js | Fast render — see docs/fast-render-integration.md |
Architecture (short)
RouterBase— shared route table,path/url,globals,group().- Client —
MicroRouter→_actionHandle→waitOn→Triggers.runTriggers(global + route) → Tracker →subscriptions+action. - Server — same registration for matching;
matchPathuseslib/micro-router.js.
Tips and tricks
- Base path: app served under
ROOT_URL_PATH_PREFIX— router strips/adds base when talking toMicroRouter(client/router.js_stripBase). - Idempotent navigation:
gono-ops if path unchanged unlessreloadenv forces redo (client/router.jsgo). - Named subs:
FlowRouter.subsReady('name')resolves handles registered withthis.register('name', sub)insidesubscriptions(route + global). - External redirect: triggers cannot redirect off-origin HTTP(S); use
window.location(client/router.js_redirectFn). - Avoid duplicate community packages listed in
client/_init.js(deprecatedmeteorhacks:*, etc.).
Debugging
- Console: many paths log with prefix
[ostrio:flow-router-extra](Meteor._debug) — e.g.lib/_helpers.js,client/route.js(promise/wait errors). - Initialization:
FlowRouter.initialize()throws if called twice;wait()throws if called after init (client/router.js). - Triggers:
already redirected/redirect needs to be done in syncfromclient/triggers.js— async redirect misuse. - Tests:
meteor test-packages ./from repo root; helpers setdecodeQueryParamsOnce = trueintest/client/_helpers.js/test/server/_helpers.js. - Bundle:
docs/,test/,AGENTS.mdexcluded from app bundle via.meteorignore— edits do not affect Meteor client weight.
Conventions (do not regress)
- 404: prefer
route('*', ...);notFoundsetter deprecated. - Query strings:
FlowRouter.decodeQueryParamsOnce = truefor new apps. underscore: not a runtime dependency (tests only if needed).
Testing
meteor test-packages ./,package.jsonTestliststest/client/*.spec.js,test/common/*.spec.js.- Meteor 3:
package.jsnotes on fast-render test compatibility — verify before re-enabling.
Ecosystem
Often released together: ostrio:flow-router-extra, ostrio:flow-router-title, ostrio:flow-router-meta, Flow-Router-Demos. After route table exists: new FlowRouterMeta(FlowRouter), new FlowRouterTitle(FlowRouter) (client).
Dev workflow
- Clean env: after
meteor reset/ removingnode_modules,meteor npm installbeforemeteor run.
Learned User Preferences
- Prefer
import/exportover globals. - Prefer
async/awaitinMeteor.startupwhen wiring initialization. - Changelog / release notes: preserve commit emojis, highlight new features, split into
⚠️ major changes,Changes,✨ New,📦 Dependencies(prod vs dev). - Prefer Meteor-wrapped npm commands in this workspace (e.g.,
meteor npm run ...,meteor npm exec ...) to keep Meteor-managed Node/tooling environment consistency.
Learned Workspace Facts
- Blaze
client/renderer.js/client/modules.jsuserequestAnimationFrameto chunk queued route renders and defer attaching in-memory layout to the live DOM; still appropriate (not deprecated); trimming legacywebkit/mozrAF prefixes is optional cleanup. ostrio:flow-router-metaandostrio:flow-router-titlehook privaterouter._notfoundRoute/router._currentandnotFound/notfoundoption shape; changes to 404 or not-found internals inclient/router.jsmust stay compatible with those integrations.tsd/npm run test:types: devDependency@types/meteor,package.json→tsd.compilerOptions.paths(meteor/*→node_modules/@types/meteor/*), mirror intsconfig.jsonwith"files": ["index.d.ts", "index.test-d.ts"]and"types": []so a parent@types/meteorinstall does not double-load. Companion packages stubmeteor/ostrio:flow-router-extraviatsd-stubs/and may need/// <reference path="./index.d.ts" />inindex.test-d.ts.- Stale async
action()→this.render()after navigation is ignored when that route is no longer current (Renderer.renderForRoute/isActiveRouteinclient/route.jsandclient/renderer.js;test/client/async-render.stale.spec.js). maxWaitFor: when the time limit is hit duringwaitOn, the route still proceeds toaction; navigating away abortswaitOnand skipsactionfor the route being left.ostrio:flow-router-extranow uses internal query modulelib/qs.js(no npmqsruntime dependency); query APIs/docs/types standardize onqueryParamsnaming, and nested query merge goes throughqs.merge(...)in router path/url building.