Stencil.js Internal Developer Platform คืออะไร
Stencil.js เป็น compiler สำหรับสร้าง Web Components ที่ทำงานได้ทุก framework (React, Angular, Vue, vanilla JS) พัฒนาโดยทีม Ionic Framework Internal Developer Platform (IDP) คือแพลตฟอร์มภายในองค์กรที่ช่วยให้นักพัฒนาสร้าง deploy และจัดการ applications ได้ด้วยตนเอง การรวม Stencil.js กับ IDP ช่วยสร้าง shared component library ที่ทุกทีมใช้ร่วมกัน ลด duplication เพิ่ม consistency และเร่งความเร็วการพัฒนา
Stencil.js สำหรับ Design System
# stencil_design.py — Stencil.js for design systems
import json
class StencilDesignSystem:
BENEFITS = {
"framework_agnostic": {
"name": "Framework Agnostic",
"description": "Web Components ทำงานกับทุก framework — React, Angular, Vue, Svelte",
"impact": "ทีมต่างๆ ใช้ framework ต่างกันได้ แต่ใช้ components เดียวกัน",
},
"compiler": {
"name": "Build-time Compiler",
"description": "Stencil compile เป็น native Web Components — ไม่มี runtime overhead",
"impact": "Bundle size เล็ก, performance ดี, lazy loading built-in",
},
"typescript": {
"name": "TypeScript + JSX",
"description": "เขียน components ด้วย TypeScript + JSX — type-safe, familiar syntax",
"impact": "Developer experience ดี, IDE support, auto-complete",
},
"testing": {
"name": "Built-in Testing",
"description": "Unit tests + E2E tests built-in — Jest + Puppeteer",
"impact": "ทดสอบ components ง่าย ไม่ต้อง setup เพิ่ม",
},
}
COMPONENT_EXAMPLE = """
// my-button.tsx — Stencil component example
import { Component, Prop, Event, EventEmitter, h } from '@stencil/core';
@Component({
tag: 'my-button',
styleUrl: 'my-button.css',
shadow: true,
})
export class MyButton {
@Prop() variant: 'primary' | 'secondary' | 'danger' = 'primary';
@Prop() size: 'sm' | 'md' | 'lg' = 'md';
@Prop() disabled: boolean = false;
@Prop() loading: boolean = false;
@Event() buttonClick: EventEmitter;
private handleClick = () => {
if (!this.disabled && !this.loading) {
this.buttonClick.emit();
}
};
render() {
return (
);
}
}
"""
def show_benefits(self):
print("=== Stencil.js Benefits ===\n")
for key, b in self.BENEFITS.items():
print(f"[{b['name']}]")
print(f" {b['description']}")
print()
def show_component(self):
print("=== Component Example ===")
print(self.COMPONENT_EXAMPLE[:500])
design = StencilDesignSystem()
design.show_benefits()
design.show_component()
IDP Architecture
# idp_arch.py — Internal Developer Platform architecture
import json
class IDPArchitecture:
LAYERS = {
"developer_portal": {
"name": "Developer Portal (Backstage)",
"description": "UI สำหรับ developers — service catalog, docs, templates",
"tools": "Backstage, Port, Cortex",
},
"component_library": {
"name": "Shared Component Library (Stencil.js)",
"description": "Web Components ที่ทุกทีมใช้ร่วมกัน — design system",
"tools": "Stencil.js, Storybook, Chromatic",
},
"ci_cd": {
"name": "CI/CD Pipeline",
"description": "Build, test, publish components อัตโนมัติ",
"tools": "GitHub Actions, npm registry, Changesets",
},
"infrastructure": {
"name": "Infrastructure Platform",
"description": "Deploy applications ด้วย golden paths",
"tools": "Kubernetes, Terraform, ArgoCD",
},
}
GOLDEN_PATHS = {
"new_app": {
"name": "Create New App",
"steps": "Template → Scaffold → CI/CD → Deploy → Monitor",
"includes": "Design system components pre-configured",
},
"new_component": {
"name": "Create New Component",
"steps": "Template → Develop → Test → Review → Publish to npm",
"includes": "Storybook, unit tests, accessibility checks",
},
"deploy": {
"name": "Deploy to Production",
"steps": "PR → Review → Staging → Canary → Production",
"includes": "Automated testing, component visual regression",
},
}
def show_layers(self):
print("=== IDP Architecture ===\n")
for key, layer in self.LAYERS.items():
print(f"[{layer['name']}]")
print(f" {layer['description']}")
print(f" Tools: {layer['tools']}")
print()
def show_golden_paths(self):
print("=== Golden Paths ===")
for key, path in self.GOLDEN_PATHS.items():
print(f"\n[{path['name']}]")
print(f" Steps: {path['steps']}")
arch = IDPArchitecture()
arch.show_layers()
arch.show_golden_paths()
Component Library Pipeline
# pipeline.py — Component library CI/CD pipeline
import json
class ComponentPipeline:
GITHUB_ACTIONS = """
# .github/workflows/component-library.yml
name: Component Library
on:
push:
branches: [main]
pull_request:
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Lint
run: npm run lint
- name: Unit Tests
run: npm run test -- --ci
- name: Build
run: npm run build
- name: E2E Tests
run: npm run test.e2e
- name: Visual Regression
run: npx chromatic --project-token=}
- name: Accessibility Check
run: npm run test.a11y
publish:
needs: build-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm run build
- name: Publish to npm
run: npx changeset publish
env:
NODE_AUTH_TOKEN: }
- name: Deploy Storybook
run: npm run deploy-storybook
"""
STORYBOOK = """
// Button.stories.tsx — Storybook story
export default {
title: 'Components/Button',
component: 'my-button',
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'danger'] },
size: { control: 'select', options: ['sm', 'md', 'lg'] },
disabled: { control: 'boolean' },
loading: { control: 'boolean' },
},
};
const Template = (args) => `
Click Me
`;
export const Primary = Template.bind({});
Primary.args = { variant: 'primary', size: 'md' };
export const Loading = Template.bind({});
Loading.args = { variant: 'primary', loading: true };
"""
def show_pipeline(self):
print("=== CI/CD Pipeline ===")
print(self.GITHUB_ACTIONS[:500])
def show_storybook(self):
print("\n=== Storybook Stories ===")
print(self.STORYBOOK[:400])
pipeline = ComponentPipeline()
pipeline.show_pipeline()
pipeline.show_storybook()
Python Automation Tools
# automation.py — Python tools for component library management
import json
class ComponentAutomation:
CODE = """
# component_manager.py — Manage Stencil component library
import json
import subprocess
from pathlib import Path
from datetime import datetime
class ComponentLibraryManager:
def __init__(self, lib_path="./component-library"):
self.lib_path = Path(lib_path)
def list_components(self):
'''List all components'''
components_dir = self.lib_path / "src" / "components"
components = []
for comp_dir in components_dir.iterdir():
if comp_dir.is_dir():
tsx_files = list(comp_dir.glob("*.tsx"))
test_files = list(comp_dir.glob("*.spec.ts"))
css_files = list(comp_dir.glob("*.css"))
components.append({
'name': comp_dir.name,
'has_tests': len(test_files) > 0,
'has_styles': len(css_files) > 0,
'files': len(list(comp_dir.iterdir())),
})
return components
def component_health(self):
'''Check component library health'''
components = self.list_components()
tested = sum(1 for c in components if c['has_tests'])
styled = sum(1 for c in components if c['has_styles'])
return {
'total_components': len(components),
'with_tests': tested,
'test_coverage_pct': round(tested / max(len(components), 1) * 100, 1),
'with_styles': styled,
'components': components,
}
def scaffold_component(self, name, props=None):
'''Scaffold a new component'''
comp_dir = self.lib_path / "src" / "components" / name
comp_dir.mkdir(parents=True, exist_ok=True)
# Generate TSX
tsx_content = self._generate_tsx(name, props or [])
(comp_dir / f"{name}.tsx").write_text(tsx_content)
# Generate CSS
css_content = f":host {{ display: block; }}"
(comp_dir / f"{name}.css").write_text(css_content)
# Generate test
test_content = self._generate_test(name)
(comp_dir / f"{name}.spec.ts").write_text(test_content)
return {'name': name, 'path': str(comp_dir)}
def _generate_tsx(self, name, props):
class_name = ''.join(w.capitalize() for w in name.split('-'))
prop_decorators = '\\n'.join(
f" @Prop() {p['name']}: {p['type']} = {p['default']};"
for p in props
)
return f'''
import {{ Component, Prop, h }} from '@stencil/core';
@Component({{
tag: '{name}',
styleUrl: '{name}.css',
shadow: true,
}})
export class {class_name} {{
{prop_decorators}
render() {{
return (
);
}}
}}
'''
def _generate_test(self, name):
return f'''
import {{ newSpecPage }} from '@stencil/core/testing';
import {{ {name.replace('-', '')} }} from './{name}';
describe('{name}', () => {{
it('renders', async () => {{
const page = await newSpecPage({{
components: [{name.replace('-', '')}],
html: '<{name}>{name}>',
}});
expect(page.root).toBeTruthy();
}});
}});
'''
# mgr = ComponentLibraryManager()
# health = mgr.component_health()
# mgr.scaffold_component("my-card", [
# {"name": "title", "type": "string", "default": "''"},
# {"name": "elevated", "type": "boolean", "default": "false"},
# ])
"""
def show_code(self):
print("=== Component Automation ===")
print(self.CODE[:600])
auto = ComponentAutomation()
auto.show_code()
Backstage Integration
# backstage.py — Backstage developer portal integration
import json
class BackstageIntegration:
CATALOG = """
# catalog-info.yaml — Backstage component registration
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: design-system
description: Shared Stencil.js Component Library
annotations:
github.com/project-slug: org/design-system
backstage.io/techdocs-ref: dir:.
tags:
- stencil
- web-components
- design-system
links:
- url: https://storybook.example.com
title: Storybook
- url: https://www.npmjs.com/package/@org/design-system
title: npm Package
spec:
type: library
lifecycle: production
owner: platform-team
"""
TEMPLATE = """
# scaffolder-template.yaml — New App with Design System
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: react-app-with-design-system
title: React App with Design System
spec:
owner: platform-team
type: website
parameters:
- title: App Details
properties:
name:
title: App Name
type: string
description:
title: Description
type: string
steps:
- id: scaffold
name: Scaffold
action: fetch:template
input:
url: ./skeleton
values:
name: }
- id: publish
name: Publish
action: publish:github
input:
repoUrl: github.com?owner=org&repo=}
"""
def show_catalog(self):
print("=== Backstage Catalog ===")
print(self.CATALOG[:400])
def show_template(self):
print("\n=== Scaffolder Template ===")
print(self.TEMPLATE[:400])
backstage = BackstageIntegration()
backstage.show_catalog()
backstage.show_template()
FAQ - คำถามที่พบบ่อย
Q: Stencil.js กับ Lit อันไหนดีกว่าสำหรับ design system?
A: Stencil: compiler-based, lazy loading, framework wrappers (React/Angular/Vue), testing built-in Lit: runtime library, เล็กกว่า (~5KB), simple API, Google-backed เลือก Stencil: ถ้าต้องการ framework wrappers + testing + lazy loading เลือก Lit: ถ้าต้องการ simplicity + smaller bundle + Google ecosystem
Q: IDP จำเป็นต้องมีไหม?
A: ขึ้นกับขนาดองค์กร: < 10 developers: ไม่จำเป็น — ใช้ README + templates พอ 10-50 developers: แนะนำ — ช่วย standardize + ลด onboarding time > 50 developers: จำเป็น — ไม่มี IDP = chaos, duplicated work, inconsistency ROI: ลดเวลา onboarding 50%, ลด duplicated components 70%, เพิ่ม developer satisfaction
Q: Component library ต้องมีกี่ components ถึงจะคุ้ม?
A: เริ่มจาก 5-10 components พื้นฐาน: Button, Input, Card, Modal, Toast, Table, Tabs คุ้มเมื่อ: ใช้ซ้ำ > 3 ทีม หรือ > 5 projects เพิ่มเติม: Form components, Navigation, Data display, Feedback components สำคัญ: คุณภาพ > จำนวน — 10 components ที่ดีดีกว่า 50 components ที่แย่
Q: Shadow DOM ดีหรือไม่ดี?
A: ดี: Encapsulation — styles ไม่รั่วเข้าออก, ใช้ได้ทุก framework ไม่ดี: Style customization ยากกว่า, ต้องใช้ CSS custom properties (--var) ทางเลือก: Stencil รองรับ shadow: false — ใช้ scoped CSS แทน แนะนำ: ใช้ Shadow DOM + CSS custom properties สำหรับ theming
