Rehberler
MotifJS için npm Kütüphanesi Yayımlama
MotifJS bileşenleri, servisleri ya da sarmalayıcıları içeren bir npm paketi hazırlama; paket biçimi, derleme, tipler, CSS, prop sözleşmesi, test ve yayın denetimi.
Bir UI kütüphanesi, bir sarmalayıcı ya da bir servis seti yazıp başkalarının npm install ile kullanmasını istiyorsanız bu bölüm size göredir. Paket, uygulamanın kullandığı aynı @motifx/core örneğine bağlanmalı, JSX'i önceden derlenmiş olarak taşımalı ve tüketicinin build ayarına bir şey dayatmamalıdır. Aşağıdaki kurulum bunu sağlar.
Paketin sınırı
İki tür paket vardır; ayar farkı küçüktür:
| Paket | İçerik | Derleyici gerekir mi? |
|---|---|---|
| JSX'siz paket | JSX kullanmayan .ts bileşenleri (super('canvas'), controls.add, bindings.*) |
Kök eleman static elementTag ya da super(...) ile bildiriliyor ve dekoratör kullanılmıyorsa hayır; tsc ya da herhangi bir paketleyici yeter. Kök eleman generic'ten üretilecekse derleyici eklentisi gerekir. |
| JSX kütüphanesi (UI kit) | .tsx bileşenleri |
Evet; paket yayımlanmadan önce @motifx/compiler ile derlenir. |
Her iki durumda da @motifx/core bir peer bağımlılıktır: paket onu kendi içine almaz, tüketicinin kurduğu kopyayı kullanır. İki kopya iki reaktivite motoru demektir ve bu hata sessizdir (bkz. Tek kopya kuralı).
package.json
{
"name": "@sirket/motif-ui",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./style.css": "./dist/style.css",
"./package.json": "./package.json"
},
"files": ["dist", "README.md"],
"scripts": {
"dev": "vite",
"build": "vite build && tsc -p tsconfig.build.json",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"prepublishOnly": "npm run build"
},
"peerDependencies": {
"@motifx/core": "^1.0.0"
},
"devDependencies": {
"@motifx/core": "^1.0.0",
"@motifx/compiler": "^1.0.0",
"jsdom": "^26.0.0",
"typescript": "^5.4.0",
"vite": "^5.4.0",
"vitest": "^3.0.0"
},
"keywords": ["motifjs", "motifx"],
"publishConfig": { "access": "public" }
}
peerDependenciesiledevDependencies'te aynı@motifx/coremajor'ı: biri tüketiciye koşul, diğeri sizin derleme ve testiniz için.@motifx/compileryalnızdevDependencies'tedir; tüketici onu kurmak zorunda kalmaz çünkü paket derlenmiş kod taşır.sideEffects: falsetree-shaking'i açar; o zaman içe aktarıldığında iş yapan modül (genel CSS enjeksiyonu, genel kayıt) bulundurmayın. Böyle bir şey gerekiyorsa açık bir fonksiyon (registerX(app)) olarak dışa aktarın.- Sarmaladığınız üçüncü parti kütüphane (
chart.jsgibi) de peer bağımlılık olur;externallistesine onu da ekleyin.
Derleme
/// <reference types="vitest/config" />
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';
import compiler from '@motifx/compiler';
export default defineConfig({
plugins: [compiler()],
build: {
lib: {
entry: fileURLToPath(new URL('./src/index.ts', import.meta.url)),
formats: ['es', 'cjs'],
fileName: (format) => (format === 'es' ? 'index.js' : 'index.cjs'),
},
rollupOptions: {
external: ['@motifx/core'],
},
sourcemap: true,
minify: false,
target: 'es2020',
},
test: {
environment: 'jsdom',
include: ['src/**/*.test.{ts,tsx}'],
},
});
/// <reference types="vitest/config" />
import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';
import compiler from '@motifx/compiler';
export default defineConfig({
plugins: [compiler()],
build: {
lib: {
entry: fileURLToPath(new URL('./src/index.ts', import.meta.url)),
formats: ['es', 'cjs'],
fileName: (format) => (format === 'es' ? 'index.js' : 'index.cjs'),
},
rollupOptions: {
external: ['@motifx/core'],
},
sourcemap: true,
minify: false,
target: 'es2020',
},
test: {
environment: 'jsdom',
include: ['src/**/*.test.{ts,tsx}'],
},
});
{
"compilerOptions": {
"target": "ES2021",
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"jsx": "preserve",
"useDefineForClassFields": true,
"noImplicitOverride": true,
"declaration": true,
"declarationMap": true,
"noEmit": true,
"skipLibCheck": true,
"lib": ["ESNext", "DOM", "DOM.Iterable"]
},
"include": ["./src/**/*", "./vite.config.ts"]
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"emitDeclarationOnly": true,
"outDir": "./dist/types",
"rootDir": "./src"
},
"include": ["./src/**/*"],
"exclude": ["./src/**/*.test.ts", "./src/**/*.test.tsx"]
}
Ne olur:
vite buildkütüphane kipinde çalışır;@motifx/compilerher.tsxdosyasındaki JSX'ibindings.*çağrılarına indirger. Çıktı düz JavaScript'tir, JSX kalmaz;@motifx/coreiçe aktarımları (motifComponent,motifCompiled…)externalolduğu için paketin dışında kalır ve tüketicinin kopyasına bağlanır.tsc -p tsconfig.build.jsonyalnız.d.tsüretir; derleyici tiplere dokunmaz.Component<HTMLDivElement>generic'inden üretilenstatic elementTagderlenmiş JS'te vardır; tip tarafında görünmez, görünmesi de gerekmez.- Derlenmiş kod bir derleyici sözleşmesine bağlıdır. Çekirdek desteklediği sözleşmeleri tanır; desteklenmeyen bir sözleşmeyle derlenmiş kodda tüketici geliştirme modunda
MJX121uyarısı görür.peerDependenciesaralığını paketi derlediğiniz çekirdeğin major sürümüne göre (^1.0.0) verin; çekirdeğin major sürümü yükselince paketi yeniden derleyip yayımlayın. noImplicitOverride: bileşen sınıflarınızComponentBase'in bir üyesini (dispose,controls,props,element…) kazara gölgelerse derleme hatası alırsınız.
JSX kullanmayan bir sarmalayıcıda jsx satırları gereksizdir. plugins: [compiler()] yine gerekir: .ts dosyalarında eleman türünü generic'ten üreten ve dekoratörleri dönüştüren odur. Elemanı static elementTag ile kendiniz yazıyor ve dekoratör kullanmıyorsanız onu da çıkarabilirsiniz; gerisi aynıdır.
Giriş noktası ve dışa aktarımlar
export { Button } from './Button';
export { Dialog } from './Dialog';
export type { ButtonProps } from './Button';
export { registerUiServices } from './services';
export { Button } from './Button';
export { Dialog } from './Dialog';
export { registerUiServices } from './services';
- Bileşenleri ve prop tiplerini adlı dışa aktarın;
default exportyalnızLazy/rota tembel yüklemesine uygun tekil bileşenler için anlamlıdır. - Ağır bileşenleri (editör, grafik, harita) ayrı alt yollardan sunabilirsiniz:
exports["./editor"]+build.lib.entrynesnesi. TüketiciLazy({ caller: () => import('@sirket/motif-ui/editor') })ile yalnız gerekeni yükler (bkz. Lazy).
Prop sözleşmesi
Paketin bileşenleri başkalarının kodunda kullanılacak; derleyicinin kurallarına uyan bir prop yüzeyi tanımlayın:
import { Component, read, type Bind } from "@motifx/core";
export interface ButtonProps {
label: Bind<string>;
tone?: Bind<"default" | "accent" | "danger">;
disabled?: Bind<boolean>;
onPress?: (e: MouseEvent) => void;
}
export class Button extends Component<HTMLButtonElement, ButtonProps> {
override onConfigured() {
this.class.add("mf-button", () => `mf-button--${read(this.props.tone) ?? "default"}`);
this.attr.add({ type: "button", disabled: () => read(this.props.disabled) ?? false });
this.motif.on("click", (_button, e: MouseEvent) => this.props.onPress?.(e));
}
override view() {
return <>{() => read(this.props.label)}</>;
}
}
import { Component, ComponentBase, motifComponent, read, type Bind } from "@motifx/core";
export interface ButtonProps {
label: Bind<string>;
tone?: Bind<"default" | "accent" | "danger">;
disabled?: Bind<boolean>;
onPress?: (e: MouseEvent) => void;
}
export class Button extends Component<HTMLButtonElement, ButtonProps> {
static override elementTag = "button";
override onConfigured() {
this.class.add("mf-button", () => `mf-button--${read(this.props.tone) ?? "default"}`);
this.attr.add({ type: "button", disabled: () => read(this.props.disabled) ?? false });
this.motif.on("click", (_button, e: MouseEvent) => this.props.onPress?.(e));
}
override view() {
return motifComponent("text", (node: ComponentBase) => node.bindings.text(() => read(this.props.label)));
}
}
import { read, type Bind } from "@motifx/core";
export interface ButtonProps {
label: Bind<string>;
tone?: Bind<"default" | "accent" | "danger">;
disabled?: Bind<boolean>;
onPress?: (e: MouseEvent) => void;
}
export function Button(props: ButtonProps) {
return (
<button
type="button"
class={() => `mf-button mf-button--${read(props.tone) ?? "default"}`}
disabled={() => read(props.disabled) ?? false}
onclick={(e) => props.onPress?.(e)}
>
{() => read(props.label)}
</button>
);
}
import { ComponentBase, motifComponent, read, type Bind } from "@motifx/core";
export interface ButtonProps {
label: Bind<string>;
tone?: Bind<"default" | "accent" | "danger">;
disabled?: Bind<boolean>;
onPress?: (e: MouseEvent) => void;
}
export function Button(props: ButtonProps) {
return motifComponent("button", (button: ComponentBase) => {
button.class.add("mf-button", () => `mf-button--${read(props.tone) ?? "default"}`);
button.attr.add({ type: "button", disabled: () => read(props.disabled) ?? false });
button.motif.on("click", (_button: ComponentBase, e: MouseEvent) => props.onPress?.(e));
button.bindings.text(() => read(props.label));
});
}
import { motifOptions, read, type Bind } from "@motifx/core";
export interface ButtonProps {
label: Bind<string>;
tone?: Bind<"default" | "accent" | "danger">;
disabled?: Bind<boolean>;
onPress?: (e: MouseEvent) => void;
}
export const Button = (props: ButtonProps) => motifOptions({
el: "button",
onConfigured() {
this.class.add("mf-button", () => `mf-button--${read(props.tone) ?? "default"}`);
this.attr.add({ type: "button", disabled: () => read(props.disabled) ?? false });
this.motif.on("click", (_button, e: MouseEvent) => props.onPress?.(e));
},
view() {
return <>{() => read(props.label)}</>;
},
});
import { ComponentBase, motifComponent, motifOptions, read, type Bind } from "@motifx/core";
export interface ButtonProps {
label: Bind<string>;
tone?: Bind<"default" | "accent" | "danger">;
disabled?: Bind<boolean>;
onPress?: (e: MouseEvent) => void;
}
export const Button = (props: ButtonProps) => motifOptions({
el: "button",
onConfigured() {
this.class.add("mf-button", () => `mf-button--${read(props.tone) ?? "default"}`);
this.attr.add({ type: "button", disabled: () => read(props.disabled) ?? false });
this.motif.on("click", (_button, e: MouseEvent) => props.onPress?.(e));
},
view() {
return motifComponent("text", (node: ComponentBase) => node.bindings.text(() => read(props.label)));
},
});
import { Component, read } from "@motifx/core";
export class Button extends Component {
static elementTag = "button";
onConfigured() {
this.class.add("mf-button", () => `mf-button--${read(this.props.tone) ?? "default"}`);
this.attr.add({ type: "button", disabled: () => read(this.props.disabled) ?? false });
this.motif.on("click", (_button, e) => this.props.onPress?.(e));
}
view() {
return <>{() => read(this.props.label)}</>;
}
}
import { Component, motifComponent, read } from "@motifx/core";
export class Button extends Component {
static elementTag = "button";
onConfigured() {
this.class.add("mf-button", () => `mf-button--${read(this.props.tone) ?? "default"}`);
this.attr.add({ type: "button", disabled: () => read(this.props.disabled) ?? false });
this.motif.on("click", (_button, e) => this.props.onPress?.(e));
}
view() {
return motifComponent("text", (node) => node.bindings.text(() => read(this.props.label)));
}
}
import { read } from "@motifx/core";
export function Button(props) {
return (
<button
type="button"
class={() => `mf-button mf-button--${read(props.tone) ?? "default"}`}
disabled={() => read(props.disabled) ?? false}
onclick={(e) => props.onPress?.(e)}
>
{() => read(props.label)}
</button>
);
}
import { motifComponent, read } from "@motifx/core";
export function Button(props) {
return motifComponent("button", (button) => {
button.class.add("mf-button", () => `mf-button--${read(props.tone) ?? "default"}`);
button.attr.add({ type: "button", disabled: () => read(props.disabled) ?? false });
button.motif.on("click", (_button, e) => props.onPress?.(e));
button.bindings.text(() => read(props.label));
});
}
import { motifOptions, read } from "@motifx/core";
export const Button = (props) => motifOptions({
el: "button",
onConfigured() {
this.class.add("mf-button", () => `mf-button--${read(props.tone) ?? "default"}`);
this.attr.add({ type: "button", disabled: () => read(props.disabled) ?? false });
this.motif.on("click", (_button, e) => props.onPress?.(e));
},
view() {
return <>{() => read(props.label)}</>;
},
});
import { motifComponent, motifOptions, read } from "@motifx/core";
export const Button = (props) => motifOptions({
el: "button",
onConfigured() {
this.class.add("mf-button", () => `mf-button--${read(props.tone) ?? "default"}`);
this.attr.add({ type: "button", disabled: () => read(props.disabled) ?? false });
this.motif.on("click", (_button, e) => props.onPress?.(e));
},
view() {
return motifComponent("text", (node) => node.bindings.text(() => read(props.label)));
},
});
Tüketici bileşeni kendi yazımıyla kullanır:
<Button label="Yeni not" tone="accent" onPress={() => this.create()} />
<Button label="Yeni not" tone="accent" onPress={() => create()} />
const newNote = new Button({ label: "Yeni not", tone: "accent", onPress: () => this.create() });
const newNote = motifComponent(Button, { label: "Yeni not", tone: "accent", onPress: () => this.create() });
const newNote = motifComponent(Button, { label: "Yeni not", tone: "accent", onPress: () => create() });
<Button label="Yeni not" tone="accent" onPress={() => this.create()} />
<Button label="Yeni not" tone="accent" onPress={() => create()} />
const newNote = new Button({ label: "Yeni not", tone: "accent", onPress: () => this.create() });
const newNote = motifComponent(Button, { label: "Yeni not", tone: "accent", onPress: () => this.create() });
const newNote = motifComponent(Button, { label: "Yeni not", tone: "accent", onPress: () => create() });
Fonksiyon bileşeninde create, düzen fonksiyonunun içindeki yerel fonksiyondur.
Sınıfta view() kökü değil içeriği döndürür. Kök eleman generic'ten (Component<HTMLButtonElement>), static elementTag'den ya da super("button") çağrısından gelir; kökün sınıfı, öznitelikleri ve olayları onConfigured'da bağlanır. view() içinde yeniden <button> yazmak iç içe iki eleman üretir.
Options'ta view() kökü değil içeriği döndürür. Kök eleman el alanıdır; kökün sınıfı, öznitelikleri ve olayları onConfigured'da bağlanır. view() içinde yeniden <button> yazmak iç içe iki eleman üretir.
Fonksiyon bileşeninde döndürülen eleman köktür; sınıf, öznitelik ve olaylar doğrudan onun üzerine yazılır.
Kurallar:
- Değişebilen her prop
Bind<T>'dir veread()ile okunur. Tüketicilabel="Kaydet"de yazabilir,label={() => state.text}de; derleyici öznitelikteki üçlü operatörü zaten getter'a sarar (bkz. Özniteliklerde ternary). Propread()olmadan okunursa getter biçimi çalışmaz. - Ortak öznitelikler köke düşer.
class,style,id,tabindex,role,aria-*,data-*bileşen etiketinden kök elemana otomatik uygulanır; bunları prop olarak tanımlayıp elle kopyalamayın (bkz. Attribute fallthrough). Kökün kendiclass'ı tüketicininkiyle birleşir. - Ayrılmış adları kullanmayın:
options,ref,key,transition,childs,initializeComponentve yaşam döngüsü kancalarının adları (onBuilt,onMounted…) çerçeveye gider (bkz. Prop adlarında dikkat). - DOM olay adıyla çakışan geri çağrı adı vermeyin.
onChange,onSelect,onResize,onReset,onCopy,onContextMenugibi bir prop bileşen etiketinde kök elemana DOM dinleyicisi olarak bağlanır ve prop'lara hiç ulaşmaz; derleyiciMJX002ile uyarır.onPress,onValueChange,onCommitgibi adlar güvenlidir. - Tüketicinin bileşen örneğine
refile ulaşacağını varsayın; dışa açmak istediğiniz davranışı bir metot olarak verin (dialog.open()), iç alanları kapalı tutun.
Çocuk içerik childs ile gelir; kök elemanı generic ya da static elementTag ile bildirilmiş ve view() yazmamış bir sınıfta <Card>…</Card> biçimi doğrudan çalışır. Kökü super("div", props) ile kuran sınıfta çocukları siz yerleştirirsiniz (bkz. Çocuk bileşenler).
Çocuk içerik props.childs ile gelir ve döndürülen kökün içine yerleştirilir (bkz. Çocuk bileşenler).
Çocuk içerik childs ile gelir; view() yazmayan bir Options bileşeninde <Card>…</Card> biçimi doğrudan çalışır, çocuklar el elemanına eklenir. view() yazan Options onları this.childs ile kendisi yerleştirir (bkz. Çocuk bileşenler).
Stil ve CSS
Paket CSS'ini tek bir dosya olarak sunun ve tüketici onu bilinçli olarak içe aktarsın:
// tüketici
import '@sirket/motif-ui/style.css';
// tüketici
import '@sirket/motif-ui/style.css';
- Kütüphane kipinde
src/index.tsiçinden içe aktarılan CSS Vite 5'tedist/style.cssolarak yazılır (Vite 6'da adıbuild.lib.cssFileName: 'style'ile verin);exports["./style.css"]onu adreslenebilir yapar. Girişten içe aktarmaksideEffects: falseile çelişmez: CSS dosyaları paketleyici tarafından yan etkili sayılır. - Kuralları bir
@layeriçine koyun (@layer mf.base, mf.components;). Katmansız kurallar bütün katmanları özgüllükten bağımsız ezdiği için tüketicinin tek satırlık bir ezmesi daima kazanır; tema değerlerini--mf-*gibi önekli CSS değişkenleriyle verin. - Bileşen içinde
<style>etiketi (bkz. Satır içi style) küçük, kendine yeten bileşenlerde işe yarar ama her örnekte bir<style>düğümü üretir ve kapsamlı değildir; kütüphane ölçeğinde dosya tercih edin. - Geçişler için sınıf adlarını belgeleyin:
transition="mf-fade"yazan tüketicimf-fade-enter-from/active/tokurallarını sizden bekler (bkz. Geçiş prop'u).
Servisler
Paket bir servis (tema, bildirim kuyruğu, dil) taşıyorsa @Injectable ile işaretleyin; sınıf modülü yüklendiği anda işaretlenir ve uygulama build() edilirken kaydedilir. Modülü build()'den sonra yüklenen sınıf ilk çözümlemede kaydedilir (bkz. Dekoratörlerle otomatik kayıt). Derleyici eklentisi dekoratörleri .ts dosyalarında da derler; eklentisiz bir sarmalayıcıda TypeScript standart dekoratörü kendisi derler.
Tüketicinin yaşam süresini seçmesi gerekiyorsa açık bir kayıt fonksiyonu daha nettir:
import { ServiceCollection } from '@motifx/core';
import { ThemeService } from './ThemeService';
export function registerUiServices(services: ServiceCollection) {
services.addSingleton(ThemeService, ThemeService);
}
import { ThemeService } from './ThemeService';
export function registerUiServices(services) {
services.addSingleton(ThemeService, ThemeService);
}
// tüketici
const builder = Application.CreateBuilder();
registerUiServices(builder.services);
const app = builder.build();
// tüketici
const builder = Application.CreateBuilder();
registerUiServices(builder.services);
const app = builder.build();
Açık kayıt dekoratörü ezer; ikisini birlikte vermek güvenlidir.
Test ve geliştirme
vitest+jsdom,vite.config.ts'teki aynı eklentiyle çalışır; testlerde JSX için ek ayar gerekmez.document'a ekleyiponMounteddavranışını da sınayın.- Her bileşen için bir tur testi yazın: kur, DOM'a ekle,
dispose(), canlı bileşen sayısının başa döndüğünü doğrulayın (bkz. Temizliği kanıtlayın). Sızıntı ancak böyle görülür. - Geliştirme için paketin köküne bir
index.htmlvesrc/showcase/koyun;viteile bileşenleri gerçek tarayıcıda görün. Vitrin,exportsdışında kaldığı için pakete girmez. motif-lint(@motifx/compilerile gelir) prop tiplerine karşı JSX'i denetler;package.json'a"lint:jsx": "motif-lint"ekleyin (bkz. motif-lint).
Yayın öncesi denetim
npm run build→dist/index.js,dist/index.cjs,dist/types/index.d.ts,dist/style.cssoluştu mu?dist/index.jsiçindejsx(/createElement(yok,from "@motifx/core"var mı? (Derleyici çalıştı ve çekirdek dışarıda kaldı.)npx publintvenpx @arethetypeswrong/cli --pack:exports/typestutarlı mı, CJS tüketici tipleri görüyor mu?- Boş bir Vite uygulamasında
npm packçıktısını kurup bir bileşeni kullanın;app.useDevelopment(true)ileMJX121ya daMJX002uyarısı çıkmadığını görün. - Monorepo'da
file:/link:ile bağlı test ederken tüketicideresolve: { dedupe: ['@motifx/core'] }verin; yayımlanan pakette bu gerekmez. README'de peer bağımlılıkları,style.cssiçe aktarımını, prop tablosunu ve geçiş sınıf adlarını yazın.
Sonraki adım
Paket geliştirirken bileşen ağacını, reaktiviteyi ve sayaçları incelemek için DevTools sözleşmesi bölümüne geçin.