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" }
}

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:

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';

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:

Ç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';

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

Yayın öncesi denetim

  1. npm run build → dist/index.js, dist/index.cjs, dist/types/index.d.ts, dist/style.css oluştu mu?
  2. dist/index.js içinde jsx(/createElement( yok, from "@motifx/core" var mı? (Derleyici çalıştı ve çekirdek dışarıda kaldı.)
  3. npx publint ve npx @arethetypeswrong/cli --pack: exports/types tutarlı mı, CJS tüketici tipleri görüyor mu?
  4. Boş bir Vite uygulamasında npm pack çıktısını kurup bir bileşeni kullanın; app.useDevelopment(true) ile MJX121 ya da MJX002 uyarısı çıkmadığını görün.
  5. Monorepo'da file:/link: ile bağlı test ederken tüketicide resolve: { dedupe: ['@motifx/core'] } verin; yayımlanan pakette bu gerekmez.
  6. README'de peer bağımlılıkları, style.css iç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.