Upgrade from 1.7 to 1.8
1.8 is a minor release. <MeekoPopover>, <MeekoSelect> and <MeekoThemeToggle> are deprecated: they keep working in 1.x and are removed in 2.0. UI 1.8.0 provides replacements with the same contract. The new stable public API useThemeToggle() lets you build your own selector. Modules are registered by name (modules: ['@meekojs/i18n']) with options under their key; importing them and [módulo, opciones] are deprecated (MEEK126). As a security change, every API and RPC mutation checks Origin by default, also without a session (see Origin on every mutation). This guide describes planned changes, not yet published; also consult the CHANGELOG and stability policy.
Recommended order#
- Update core to
1.8and@meekojs/uito1.8.0when published. UI 1.8 requires Meeko>=1.8 <2; do not use the newUIThemeTogglewith core 1.7. - Enable the UI module in
meeko.config.ts. - Rename the three tags and update explicit imports of their types.
- Run
meeko checkand check the controls, their styles and forms in your application. - Review webhooks, public APIs and nonbrowser clients calling mutations without a session, and decide for each whether to send
Originor declareorigin: "none". - Register modules by name, with options under their key (see Modules by name).
Required UI module#
pnpm add @meekojs/meeko@^1.8.0 @meekojs/ui@^1.8.0import { defineMeekoConfig } from '@meekojs/meeko/framework'
export default defineMeekoConfig({
modules: ['@meekojs/ui'],
theme: true,
})Keep the rest of your configuration. theme is only needed for the theme selector. The module registers components and adds its stylesheet before application styles: review any existing visual customizations.
Rename tags#
| Until 1.7 | Since core 1.8 with UI 1.8 |
|---|---|
<MeekoPopover> |
<UIPopover> |
<MeekoSelect> |
<UICombobox> |
<MeekoThemeToggle> |
<UIThemeToggle> |
<UIPopover role="menu">
<snippet name="trigger">Actions</snippet>
<button role="menuitem">Edit</button>
</UIPopover>
<UICombobox
model="color"
bind:options="colors"
name="color"
/>
<UIThemeToggle mode="select" />UICombobox is the same custom list as <MeekoSelect>. A select without multiple, searchable, filter or max can also move to UISelect, a native <select> with much less JavaScript and less markup of its own: see its documentation.
Rename closing tags too. Props, models, snippets, events, keyboard behavior, accessibility and data-meeko-* markup keep their contract: existing CSS selectors still find the same attributes. UICombobox<T, Multiple> infers its types from options and multiple as before.
Type imports#
The three prop types change package and prefix:
// Before
import type { MeekoPopoverProps, MeekoSelectProps, MeekoThemeToggleProps, PopoverPlacement, SelectModel } from '@meekojs/meeko/framework'
// After
import type { UIPopoverProps, UIComboboxProps, UIThemeToggleProps, PopoverPlacement, ComboboxModel } from '@meekojs/ui/runtime'Popover/select helpers (PopoverMode, PopoverRole, PopoverTrigger, PopoverToggleEvent, PopoverSnippets, PopoverExposed, SelectOptionState, ComboboxFilter, ComboboxFilterContext, SelectSnippets and SelectField) keep their names and are imported from @meekojs/ui/runtime. Theme system types (ThemeChoice, ThemeToggleMessages and ThemeToggleSnippets, plus new ThemeToggleState and ThemeToggleSystem) remain in @meekojs/meeko/framework. Old core exports remain available throughout 1.x.
MEEK121 warning#
The compiler emits MEEK121 when using the three deprecated built-in tags; meeko check, the build and language server show the warning. The editor strikes through the tag and explains its replacement on hover. The warning does not change SSR or hydration in 1.x. Your own component replacing a built-in tag keeps its normal resolution.
Custom selector with useThemeToggle()#
If you prefer your own selector, you do not need UI. useThemeToggle() and types ThemeToggleState and ThemeToggleSystem are stable public core API since 1.8. The composable returns theme, choices, system, known and interactive. Keep a POST /_meeko/theme form with theme and return to change without JavaScript, and render icons with data-meeko-theme-icon so the <head> rule shows the correct one from the first frame. The complete example explains hydration and accessible names when the choice is not yet known.
The theme configuration, useTheme(), cookie, <head> script, icon rule and POST /_meeko/theme stay in core. Deprecation affects control tags.
Origin in all mutations#
Since 1.8, POST, PUT, PATCH and DELETE from defineApiRoute and all defineRpc calls return 403 INVALID_ORIGIN if their Origin is missing, is null or is neither the application's nor one of SessionManager({ allowedOrigins }), also without a session cookie. Until 1.7, anonymous mutations were only checked with origin: "required", and another website could execute any anonymous route accepting a form or JSON sent as text/plain. This is a breaking change approved as a security fix (stability policy).
What stays the same. Browsers send Origin on every mutation, including same-origin ones, and api.* and rpc.* clients send the current request's origin during SSR, so a web application needs no changes. Mutations with a session already required Origin and the CSRF token, and native forms required Origin; these stay the same. GET, HEAD and OPTIONS remain unchecked.
What to review:
-
Webhooks (payments, Git, messaging). They arrive from another server without
Originand now receive403. Declareorigin: "none"on the route and verify the provider's signature or secret before acting. This also applies to those sendingapplication/x-www-form-urlencoded(Twilio, Slack): since1.8,"none"exempts a native form (form) without a session fromOrigin, which until1.7was always required. WithcontentType: "response"and nobody, the handler can read the body usingcontext.request.formData():ts// server/api/webhooks/payments.post.ts export default defineApiRoute({ origin: 'none', contentType: 'response', handler: async ({ context }) => { const payload = await context.request.text() if (!(await verifySignature(payload, context.request.headers.get('x-signature')))) throw new ApiError('INVALID_SIGNATURE', 401) return new Response(null, { status: 204 }) }, }) -
Public APIs for scripts, native apps or other services, authenticated with an
authorizationtoken instead of a cookie. Declareorigin: "none"on those routes, or have the client send an allowedOrigin. If other websites call them from the browser, also add origins toallowedOrigins(to pass without"none") or configure CORS in middleware or the proxy; Meeko does not send CORS headers. -
Clients from another known origin, such as a dashboard at
https://admin.example.com: add it toSessionManager({ allowedOrigins }), which now also applies to mutations without a session. -
Tests and scripts making
POSTrequests with Nodefetch,curlorexecuteApiRoute/executeRpcwithoutOrigin: addoriginwith the application's origin (for example,headers: { origin: "http://localhost:4173" }).createApiClientandcreateRpcClientacceptheadersto do this once. -
Behind a TLS-terminating proxy, the server sees
http://and the browser sends anhttps://Origin: setMEEKO_PUBLIC_ORIGIN(orpublicOrigin).MEEKO_TRUST_PROXYdoes not change the application's own origin; it only determines the client address. Such an application already failed in1.7on mutations with a session. -
createLocalCallsnow sends the application's origin asOrigin, like SSR'sapi.*andrpc.*clients, instead of forwarding the original request's. A local mutation from a page requested withGET(withoutOrigin) works, with or without a session; with a session it still needs the original request's CSRF token. If a test expected a403due to the forwardedOrigin, it no longer receives one.
origin: "none" is unsafe: it exposes the route to CSRF and login CSRF on requests without a session. A request with a session still requires Origin and the CSRF token even if the route declares it, also in a native form (Origin and _csrf). origin: "required" remains valid and equals the default. Details in framework.md.
Modules by name#
Since 1.8, a module is registered only by name in modules and its options go under its own key in meeko.config.ts, like in Nuxt. Importing the module (modules: [ui]) and [módulo, opciones] are deprecated: they keep working in 1.x, with warning MEEK126, and are removed in 2.0.
// Before (1.7)
import i18n from '@meekojs/i18n/module'
import { defineMeekoConfig } from '@meekojs/meeko/framework'
import ui from '@meekojs/ui'
export default defineMeekoConfig({
modules: [ui, [i18n, { defaultLocale: 'es', catalogs: { es: 'app/locales/es.json', en: 'app/locales/en.json' } }]],
ui: { defaults: { button: { shape: 'pill' } } },
})// After (1.8)
import { defineMeekoConfig } from '@meekojs/meeko/framework'
export default defineMeekoConfig({
modules: ['@meekojs/ui', '@meekojs/i18n'],
ui: { defaults: { button: { shape: 'pill' } } },
i18n: { defaultLocale: 'es', catalogs: { es: 'app/locales/es.json', en: 'app/locales/en.json' } },
})- Replace each imported module with its name (package name or path) and delete its
import.'@meekojs/i18n'resolves its./modulesubpath: do not write'@meekojs/i18n/module'. The earlier built-in module is'@meekojs/meeko/i18n', also using thei18nkey. - Move each
[módulo, opciones]'s options to the key declared by itsmeta.configKey:i18nfor@meekojs/i18n,uifor@meekojs/ui. WarningMEEK126names it. If a module does not declareconfigKey, the warning says so: keep its tuple until its author adds one. - Add
{ "path": "./.meeko/tsconfig.config.json" }toreferencesin the project'stsconfig.json(meeko initalready includes it), so the editor types and completes module keys. - Run
meeko check. Since1.8, it always checksmeeko.config.tstypes with those of its modules (previously only when loading failed): a mistyped option isTS2322with its line, and a previously unnoticed error, such asprocess.envwithout@types/node, also appears.
A key for an installed module absent from modules (i18n: { … } without '@meekojs/i18n') produces warning MEEK127, naming the missing entry; Meeko ignores it as before. Two modules with the same meta.configKey are now an error.
Module authors. Declare meta.configKey and augment MeekoConfig with its type (declare module '@meekojs/meeko/framework' { interface MeekoConfig { readonly seo?: SeoOptions } }) in the default export's file; Meeko loads that augmentation automatically in .meeko/types/config.d.ts. If the package root is its runtime, export the module at the ./module subpath. Add "keywords": ["meeko-module"] to package.json so the editor completes its name. See Modules.
Destructure defineProps (optional)#
1.8 supports destructuring defineProps without losing reactivity (see Destructure props). No changes are needed: const props = defineProps<Props>() works as before. To adopt it in a component:
- Replace
const props = defineProps<Props>({ size: 'md' })withconst { label, size = 'md', ...rest } = defineProps<Props>(): defaults move to the pattern (using both isMEEK124). - Replace each
props.labelwithlabelin the script and template. A name already used by a local variable or parameter still refers to that variable: rename the prop (label: title) if needed. - If you separated props with
const { a, b, ...rest } = propsinside acomputedto forward the rest throughbind, use the destructuring's...rest, which already follows props. - Run
meeko check:MEEK101identifies copies read only once,MEEK125identifies assignments andTS2344a name that is not a prop.
Defaults can now read top-level literal constants and imports; a setup variable previously producing TS2304 is now MEEK123.
Component types#
<Name>Props no longer includes the callbacks of defineEmits. If your code reads or writes them on that type, change it to ComponentProps<"Name">:
- const props: BaseButtonProps = { href: '/x', onClick }
+ const props: ComponentProps<'BaseButton'> = { href: '/x', onClick }Only types change, and meeko check flags each case with TS2353 or TS2339. BaseButtonEmits, new, is the defineEmits map. This makes a wrapper possible; see Wrapping another component.
A component package published before 1.8 keeps working without republishing it and without you doing anything. Meeko reads what its .d.meeko.ts exports: its <Name>Props is its ComponentProps, with the callbacks, as before; <Name>Emits does not exist for it, because its declaration has no event map, and using it gives TS2304. Its Snippets, Models and Exposed do not change. Once republished with 1.8 it has both types separated, like the .meeko files of your project.
defineModel also as a prop and an event (optional)#
There is nothing to change: model="signal" stays the same and defineModel<T>() still returns Signal<T>. Now a parent can also pass the value with bind:modelValue, inside bind="object", and listen to on:update:modelValue, as in Vue. Named models use bind:touched and on:update:touched. See defineModel.
<Name>PropsincludesmodelValue?andComponentPropsalsoonUpdate:modelValue?;<Name>Emitsincludesupdate:modelValue, optional.- A
.meekothat declares in its script a prop or an event with the name of its model (defineProps<{ modelValue: string }>()next todefineModel<string>()) givesMEEK063: remove it fromdefineProps. Before, the child received it as a separate prop, unrelated to the model. modelnext toon:update:modelValueisMEEK066.- An
asynchandler works: what was written stays while it saves, and a rejection reaches the error handler. - Reading a prop in the
defaultoption ofdefineModelno longer givesMEEK101.
Other visible changes#
- A component's
css. With Meeko's Vite plugin, thecssexport is PostCSS-processed CSS in the SSR graph andmeeko dev, and remains the compiler string in the production client bundle (see stability.md). A Vitest test readingComponente.cssor SSR-rendering a component still receives nonempty CSS: withouttest.css: true, Vitest does not process CSS andcssis the compiler string; withcss: true, processed CSS. on:toggleon a fast popover. A<MeekoPopover>orUIPopoverhintopened and closed very quickly now emitstoggletruefollowed byfalse; previously the browser merged both events and nothing was emitted. A handler counting openings now sees these too.- Component
<style>order. SSR and prerendering write them at the end of<head>, afterapp/styles/main.css, module stylesheets anddocument'shead, as the client does on hydration. Previously they came first, and a global rule with the same specificity as a scoped rule won until JavaScript loaded. If amain.cssrule was meant to override a component's, increase its specificity: after hydration, the component's already won. <input>or another<select>inside a<select>. It is nowMEEK008. It used to compile to an empty<select>followed by its loose options, which is what the browser does with that HTML. Move that tag out of the<select>.
ESLint: new package#
@meekojs/eslint-plugin is published for the first time with 1.8: 1.7.1 did not include it, so there are no options to migrate. It is optional, and you can keep your ESLint configuration if you already have one. When you adopt it, defineEslint() sorts only imports with perfectionist/sort-imports and perfectionist/sort-named-imports, in ascending natural order (item2 before item10). In Meeko 1.x they are warn, and they become error in 2.0. eslint --fix only moves those imports. If your CI uses --max-warnings 0, these warnings also fail its execution.
To also sort exports, objects, interfaces, types, enums, switch, Set, Map and more, use defineEslint({ perfectionist: true }). It enables the recommended-natural set except sort-classes and sort-modules, whose --fix breaks valid code; if you want either, declare it in rules, for example perfectionist/sort-classes: 'warn'. With true, --fix also reorders where the order is observable, such as Object.keys, a Map or the order in which an each:of renders the keys of an object: review the diff before accepting it. To turn off one rule, use rules: { 'perfectionist/sort-objects': 'off' }, and to sort nothing, perfectionist: false. To require imports as errors, use defineEslint({ rules: { 'perfectionist/sort-imports': 'error', 'perfectionist/sort-named-imports': 'error' } }); the severity override preserves natural order. See Sort with Perfectionist.
Install the new packages#
Release preparation, unpublished: wait until npm confirms all six versions. All public packages align at 1.8.0.
pnpm add @meekojs/meeko@^1.8.0 @meekojs/ui@^1.8.0 @meekojs/i18n@^1.8.0 @meekojs/icon@^1.8.0
pnpm add -D @meekojs/eslint-plugin@^1.8.0 @meekojs/prettier-plugin@^1.8.0 eslint prettier viteRegister only the modules you use in modules. @meekojs/ui/editor belongs to UI and is registered separately. To use it, also install its optional peers:
pnpm add @milkdown/crepe@^7.23.0 @milkdown/kit@^7.23.0 @codemirror/lang-css@^6.3.1 @codemirror/lang-html@^6.4.12 @codemirror/lang-javascript@^6.2.5 @codemirror/lang-json@^6.0.2 @codemirror/language@^6.13.1 @codemirror/view@^6.43.15 @lezer/highlight@^1.2.5Install @iconify-json/* collections explicitly in the consumer; review each collection's license before distributing its icons. See Publish the Meeko packages.