Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,13 @@ import { SignupButton } from 'solid-ui/components/signup-button'

Web components use a two-stage build to produce a clean public runtime layout while keeping internal TypeScript artifacts separate:

1. **webpack** (`npm run build-dist`) bundles each component entrypoint and emits the runtime files to `dist/components/<name>/index.js` and `dist/components/<name>/index.esm.js`.
2. **tsc** (`npm run build-js`) emits internal declaration and JS artifacts mirroring the source tree under `dist/v2/components/<name>/`.
3. **`scripts/build-component-dts.mjs`** (runs automatically after tsc as part of `postbuild-js`) writes thin public declaration wrappers at `dist/components/<name>/index.d.ts`, re-exporting from the internal `dist/v2/components/<name>/` output.
1. **`scripts/component-manifest.mjs`** is the source of truth for v2 web components. It defines the component entrypoints used by webpack and the public subpath names exposed from the package.
2. **webpack** (`npm run build-dist`) bundles each component entrypoint from the manifest and emits the runtime files to `dist/components/<name>/index.js` and `dist/components/<name>/index.esm.js`.
3. **tsc** (`npm run build-js`) emits internal declaration and JS artifacts mirroring the source tree under `dist/v2/components/<name>/`.
4. **`scripts/build-component-dts.mjs`** (runs automatically after tsc as part of `postbuild-js`) writes thin public declaration wrappers at `dist/components/<name>/index.d.ts`, re-exporting from the internal `dist/v2/components/<name>/` output.
5. **`scripts/sync-component-exports.mjs`** keeps the `package.json` `exports` map aligned with the manifest. It runs automatically as part of `npm run build` and `npm version` workflows.

The legacy main bundle remains a special case. In [webpack.config.mjs](webpack.config.mjs) only the `main` entry keeps the UMD `UI` global export; component entries are generated from the manifest and built as standalone scripts so they do not clobber one another when loaded directly.

This keeps the `package.json` subpath export fully aligned while exposing only the public `dist/components/...` layout:

Expand All @@ -338,6 +342,16 @@ This keeps the `package.json` subpath export fully aligned while exposing only t

Consumers never import from `dist/v2/components/...`; that path is an internal build artifact only.

### Adding a new web component

When adding a new v2 component:

1. Create the component folder under `src/v2/components/<ComponentName>/` with its `index.ts` entrypoint.
2. Add one record to `scripts/component-manifest.mjs`.
3. Run `npm run sync-component-exports` if you want to update `package.json` immediately, or just run `npm run build` and let the build do it automatically.

You should not need to hand-edit the webpack component entry list or the `package.json` component export map anymore.

## Development

When developing a component in solid-ui you can test it in isolation using storybook
Expand Down Expand Up @@ -411,4 +425,8 @@ You are logged in as nameOfLoggedIn user.

* Raptor mini: add a readme to the Footer component with example.

* Claude Sonnet 4.6: Make the dop down as a list under the input field and entlarge the pop up, make it higher, adjustable to fit the drop down. And make the drop down arrow area larger
* Claude Sonnet 4.6: Make the dop down as a list under the input field and entlarge the pop up, make it higher, adjustable to fit the drop down. And make the drop down arrow area larger

* GPT-5.4 Model: can you wire up the keyboard interactions and aria attributes for Select.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* GPT-5.4 Model: can you wire up the keyboard interactions and aria attributes for Select.
* GPT-5.4 Model: can you wire up the keyboard interactions and aria attributes for Select?


* GPT-5.4 Model: Take the code from /Users/sharon/2025Dev/solid-ui/src/media/media-capture.ts and make it a web component. Make it work in forms as well as not. Make it configurable and follow LoginButton.
70 changes: 66 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,80 @@
"import": "./dist/components/header/index.esm.js",
"require": "./dist/components/header/index.js"
},
"./components/layout/header": {
"types": "./dist/components/header/index.d.ts",
"import": "./dist/components/header/index.esm.js",
"require": "./dist/components/header/index.js"
},
"./components/login-button": {
"types": "./dist/components/loginButton/index.d.ts",
"import": "./dist/components/loginButton/index.esm.js",
"require": "./dist/components/loginButton/index.js"
},
"./components/auth/login-button": {
"types": "./dist/components/loginButton/index.d.ts",
"import": "./dist/components/loginButton/index.esm.js",
"require": "./dist/components/loginButton/index.js"
},
"./components/auth/signup-button": {
"types": "./dist/components/signupButton/index.d.ts",
"import": "./dist/components/signupButton/index.esm.js",
"require": "./dist/components/signupButton/index.js"
},
"./components/signup-button": {
"types": "./dist/components/signupButton/index.d.ts",
"import": "./dist/components/signupButton/index.esm.js",
"require": "./dist/components/signupButton/index.js"
},
"./components/media/photo-capture": {
"types": "./dist/components/photoCapture/index.d.ts",
"import": "./dist/components/photoCapture/index.esm.js",
"require": "./dist/components/photoCapture/index.js"
},
"./components/photo-capture": {
"types": "./dist/components/photoCapture/index.d.ts",
"import": "./dist/components/photoCapture/index.esm.js",
"require": "./dist/components/photoCapture/index.js"
},
"./components/actions/button": {
"types": "./dist/components/button/index.d.ts",
"import": "./dist/components/button/index.esm.js",
"require": "./dist/components/button/index.js"
},
"./components/button": {
"types": "./dist/components/button/index.d.ts",
"import": "./dist/components/button/index.esm.js",
"require": "./dist/components/button/index.js"
},
"./components/footer": {
"types": "./dist/components/footer/index.d.ts",
"import": "./dist/components/footer/index.esm.js",
"require": "./dist/components/footer/index.js"
},
"./components/layout/footer": {
"types": "./dist/components/footer/index.d.ts",
"import": "./dist/components/footer/index.esm.js",
"require": "./dist/components/footer/index.js"
},
"./components/forms/select": {
"types": "./dist/components/select/index.d.ts",
"import": "./dist/components/select/index.esm.js",
"require": "./dist/components/select/index.js"
},
"./components/select": {
"types": "./dist/components/select/index.d.ts",
"import": "./dist/components/select/index.esm.js",
"require": "./dist/components/select/index.js"
},
"./components/forms/combobox": {
"types": "./dist/components/combobox/index.d.ts",
"import": "./dist/components/combobox/index.esm.js",
"require": "./dist/components/combobox/index.js"
},
"./components/combobox": {
"types": "./dist/components/combobox/index.d.ts",
"import": "./dist/components/combobox/index.esm.js",
"require": "./dist/components/combobox/index.js"
}
},
"files": [
Expand All @@ -38,10 +98,11 @@
],
"scripts": {
"clean": "rm -rf ./dist ./src/versionInfo.ts ./docs/api .tsbuildinfo",
"build": "npm run clean && npm run typecheck && npm run build-version && npm run build-dist && npm run build-js && npm run postbuild-js && npm run build-storybook",
"build": "npm run clean && npm run sync-component-exports && npm run typecheck && npm run build-version && npm run build-dist && npm run build-js && npm run postbuild-js && npm run build-storybook",
"build-version": "sh ./timestamp.sh > src/versionInfo.ts && eslint 'src/versionInfo.ts' --fix",
"prebuild-js": "rm -f .tsbuildinfo",
"build-js": "tsc",
"sync-component-exports": "node scripts/sync-component-exports.mjs",
"postbuild-js": "rm -f dist/versionInfo.d.ts dist/versionInfo.d.ts.map && node scripts/build-component-dts.mjs",
"build-dist": "webpack --progress",
"build-form-examples": "npm run build-js && npm run build-version && npm run build-dist && cp ./dist/solid-ui.js ./docs/form-examples/",
Expand All @@ -54,12 +115,13 @@
"test-debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand --watch",
"watch:js": "tsc --watch --preserveWatchOutput",
"watch:component-dts": "node scripts/watch-component-dts.mjs",
"watch:component-exports": "node scripts/watch-component-exports.mjs",
"watch:dist": "webpack --watch --mode development",
"watch": "npm run build-version && npm run build-js && npm run postbuild-js && sh -c 'npm run watch:js & npm run watch:component-dts & npm run watch:dist & wait'",
"dev": "npm run build-version && sh -c 'npm run watch:js & npm run watch:component-dts & npm run watch:dist & wait'",
"watch": "npm run sync-component-exports && npm run build-version && npm run build-js && npm run postbuild-js && sh -c 'npm run watch:js & npm run watch:component-dts & npm run watch:component-exports & npm run watch:dist & wait'",
"dev": "npm run sync-component-exports && npm run build-version && sh -c 'npm run watch:js & npm run watch:component-dts & npm run watch:component-exports & npm run watch:dist & wait'",
"doc": "typedoc --out ./docs/api/ ./src/ --excludeInternal",
"prepublishOnly": "npm run build && npm run lint && npm test && npm run doc",
"preversion": "npm run lint && npm run typecheck && npm test",
"preversion": "npm run sync-component-exports && npm run lint && npm run typecheck && npm test",
"postpublish": "git push origin main --follow-tags",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build --output-dir ./examples/storybook"
Expand Down
23 changes: 17 additions & 6 deletions scripts/build-component-dts.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from 'fs'
import path from 'path'
import { v2Components } from './component-manifest.mjs'

const distDir = path.resolve(process.cwd(), 'dist')
const v2ComponentsDir = path.join(distDir, 'v2', 'components')
Expand All @@ -9,21 +10,31 @@ if (!existsSync(v2ComponentsDir)) {
throw new Error(`Missing expected directory: ${v2ComponentsDir}`)
}

const componentDirs = readdirSync(v2ComponentsDir).filter(name => {
const manifestComponents = v2Components.map(({ sourceDir, sourcePath = sourceDir }) => ({
publicDir: sourceDir,
sourcePath
}))

const fallbackComponentDirs = readdirSync(v2ComponentsDir).filter(name => {
const fullPath = path.join(v2ComponentsDir, name)
return statSync(fullPath).isDirectory()
})
}).map(name => ({
publicDir: name,
sourcePath: name
}))

const componentDirs = manifestComponents.length > 0 ? manifestComponents : fallbackComponentDirs

for (const componentDir of componentDirs) {
const sourceIndex = path.join(v2ComponentsDir, componentDir, 'index.d.ts')
for (const { publicDir, sourcePath } of componentDirs) {
const sourceIndex = path.join(v2ComponentsDir, sourcePath, 'index.d.ts')
if (!existsSync(sourceIndex)) {
continue
}

const outputDir = path.join(publicComponentsDir, componentDir)
const outputDir = path.join(publicComponentsDir, publicDir)
mkdirSync(outputDir, { recursive: true })

const relativePath = path.relative(outputDir, path.join(v2ComponentsDir, componentDir, 'index.d.ts'))
const relativePath = path.relative(outputDir, sourceIndex)
.replace(/\\/g, '/')
.replace(/\.d\.ts$/, '')

Expand Down
64 changes: 64 additions & 0 deletions scripts/component-manifest.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export const v2Components = [
{
sourceDir: 'header',
sourcePath: 'layout/header',
exportNames: ['header', 'layout/header']
},
{
sourceDir: 'loginButton',
sourcePath: 'auth/loginButton',
exportNames: ['login-button', 'auth/login-button']
},
{
sourceDir: 'signupButton',
sourcePath: 'auth/signupButton',
exportNames: ['auth/signup-button', 'signup-button']
},
{
sourceDir: 'photoCapture',
sourcePath: 'media/photoCapture',
exportNames: ['media/photo-capture', 'photo-capture']
},
{
sourceDir: 'button',
sourcePath: 'actions/button',
exportNames: ['actions/button', 'button']
},
{
sourceDir: 'footer',
sourcePath: 'layout/footer',
exportNames: ['footer', 'layout/footer']
},
{
sourceDir: 'select',
sourcePath: 'forms/select',
exportNames: ['forms/select', 'select']
},
{
sourceDir: 'combobox',
sourcePath: 'forms/combobox',
exportNames: ['forms/combobox', 'combobox']
}
]

export const componentEntries = Object.fromEntries(
v2Components.map(({ sourceDir, sourcePath = sourceDir }) => [
sourceDir,
{
import: `./src/v2/components/${sourcePath}/index.ts`
}
])
)

export const componentExports = Object.fromEntries(
v2Components.flatMap(({ sourceDir, exportNames }) =>
exportNames.map(exportName => [
`./components/${exportName}`,
{
types: `./dist/components/${sourceDir}/index.d.ts`,
import: `./dist/components/${sourceDir}/index.esm.js`,
require: `./dist/components/${sourceDir}/index.js`
}
])
)
)
17 changes: 17 additions & 0 deletions scripts/sync-component-exports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { readFileSync, writeFileSync } from 'fs'
import path from 'path'
import { componentExports } from './component-manifest.mjs'

const packageJsonPath = path.resolve(process.cwd(), 'package.json')
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))

const preservedExports = Object.fromEntries(
Object.entries(packageJson.exports || {}).filter(([subpath]) => !subpath.startsWith('./components/'))
)

packageJson.exports = {
...preservedExports,
...componentExports
}

writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`)
58 changes: 58 additions & 0 deletions scripts/watch-component-exports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { spawn } from 'child_process'
import { existsSync, watch } from 'fs'
import path from 'path'

const scriptsDir = path.resolve(process.cwd(), 'scripts')
const manifestFile = 'component-manifest.mjs'
const syncScript = path.resolve(scriptsDir, 'sync-component-exports.mjs')
let syncTimer = null
let running = false
let rerunRequested = false

const runSync = () => {
if (running) {
rerunRequested = true
return
}

running = true
rerunRequested = false

const child = spawn(process.execPath, [syncScript], {
stdio: 'inherit'
})

child.on('exit', code => {
running = false

if (code !== 0) {
console.error(`sync-component-exports exited with code ${code}`)
}

if (rerunRequested) {
runSync()
}
})
}

const scheduleSync = () => {
clearTimeout(syncTimer)
syncTimer = setTimeout(() => {
runSync()
}, 150)
}

if (!existsSync(scriptsDir)) {
throw new Error(`Missing expected directory: ${scriptsDir}`)
}

console.log(`Watching ${path.join(scriptsDir, manifestFile)} for export manifest changes...`)

runSync()

watch(scriptsDir, (eventType, filename) => {
if (!filename) return
if (filename === manifestFile) {
scheduleSync()
}
})
Loading
Loading