@bespokeagentics/create-microdots-host 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/package.json +26 -0
- package/src/cli.ts +50 -0
- package/src/index.ts +215 -0
- package/templates/base/README.md +49 -0
- package/templates/base/_gitignore +7 -0
- package/templates/base/host/index.html +33 -0
- package/templates/base/host/package.json +10 -0
- package/templates/base/host/public/themes/neutral-dark.css +109 -0
- package/templates/base/host/public/themes/neutral.css +109 -0
- package/templates/base/host/src/main.ts +202 -0
- package/templates/base/host/src/topology.test.ts +42 -0
- package/templates/base/host/topology.json +84 -0
- package/templates/base/host/vite.base.ts +41 -0
- package/templates/base/microdots/pulse/package.json +20 -0
- package/templates/base/microdots/pulse/service/handlers.ts +36 -0
- package/templates/base/microdots/pulse/service/main.ts +30 -0
- package/templates/base/microdots/pulse/src/app.ts +275 -0
- package/templates/base/microdots/pulse/src/client.ts +14 -0
- package/templates/base/microdots/pulse/src/contract.ts +32 -0
- package/templates/base/microdots/pulse/src/element.ts +24 -0
- package/templates/base/microdots/pulse/src/entry.ts +1 -0
- package/templates/base/microdots/pulse/src/surface.ts +30 -0
- package/templates/base/microdots/pulse/src/tokens.css +30 -0
- package/templates/base/microdots/scope-picker/package.json +20 -0
- package/templates/base/microdots/scope-picker/service/handlers.ts +15 -0
- package/templates/base/microdots/scope-picker/service/main.ts +30 -0
- package/templates/base/microdots/scope-picker/src/app.ts +256 -0
- package/templates/base/microdots/scope-picker/src/client.ts +18 -0
- package/templates/base/microdots/scope-picker/src/contract.ts +27 -0
- package/templates/base/microdots/scope-picker/src/element.ts +21 -0
- package/templates/base/microdots/scope-picker/src/entry.ts +1 -0
- package/templates/base/microdots/scope-picker/src/surface.ts +23 -0
- package/templates/base/microdots/scope-picker/src/tokens.css +30 -0
- package/templates/base/scripts/build-output.test.ts +71 -0
- package/templates/base/scripts/build-output.ts +28 -0
- package/templates/base/scripts/build.ts +37 -0
- package/templates/base/scripts/check-tokens.ts +63 -0
- package/templates/base/scripts/dev.ts +78 -0
- package/templates/base/scripts/discover.test.ts +133 -0
- package/templates/base/scripts/discover.ts +137 -0
- package/templates/base/scripts/dot-vite.ts +28 -0
- package/templates/base/scripts/emit-manifests.ts +154 -0
- package/templates/base/tsconfig.json +18 -0
- package/templates/base/types.d.ts +9 -0
- package/templates/base/vitest.config.ts +8 -0
- package/templates/plain/host/src/styles.css +46 -0
- package/templates/plain/host/vite.config.template.ts +4 -0
- package/templates/plain/microdots/pulse/src/styles.css +17 -0
- package/templates/plain/microdots/pulse/vite.config.template.ts +4 -0
- package/templates/plain/microdots/scope-picker/src/styles.css +19 -0
- package/templates/plain/microdots/scope-picker/vite.config.template.ts +4 -0
- package/templates/plain/package.json +39 -0
- package/templates/tailwind/host/src/styles.css +28 -0
- package/templates/tailwind/host/vite.config.template.ts +5 -0
- package/templates/tailwind/microdots/pulse/src/styles.css +24 -0
- package/templates/tailwind/microdots/pulse/vite.config.template.ts +5 -0
- package/templates/tailwind/microdots/scope-picker/src/styles.css +25 -0
- package/templates/tailwind/microdots/scope-picker/vite.config.template.ts +5 -0
- package/templates/tailwind/package.json +41 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, test } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
assertSurfaceTagsMatch,
|
|
8
|
+
listMicroDots,
|
|
9
|
+
registryFor,
|
|
10
|
+
} from './discover.ts'
|
|
11
|
+
|
|
12
|
+
const roots: string[] = []
|
|
13
|
+
|
|
14
|
+
const root = (): string => {
|
|
15
|
+
const directory = mkdtempSync(resolve(tmpdir(), 'microdots-discovery-'))
|
|
16
|
+
roots.push(directory)
|
|
17
|
+
return directory
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const writeDot = (
|
|
21
|
+
directory: string,
|
|
22
|
+
name: string,
|
|
23
|
+
metadata: Readonly<Record<string, unknown>>,
|
|
24
|
+
): void => {
|
|
25
|
+
const dot = resolve(directory, name)
|
|
26
|
+
mkdirSync(dot, { recursive: true })
|
|
27
|
+
writeFileSync(
|
|
28
|
+
resolve(dot, 'package.json'),
|
|
29
|
+
JSON.stringify({ name: `@starter/${name}`, microdot: metadata }),
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
roots.splice(0).forEach(directory => {
|
|
35
|
+
rmSync(directory, { recursive: true, force: true })
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('MicroDot discovery', () => {
|
|
40
|
+
test('one metadata record drives one registry row per tag', () => {
|
|
41
|
+
const directory = root()
|
|
42
|
+
writeDot(directory, 'one', {
|
|
43
|
+
tags: ['one-view', 'one-admin'],
|
|
44
|
+
port: 3991,
|
|
45
|
+
bundle: 'one.js',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
expect(registryFor(listMicroDots(directory))).toEqual([
|
|
49
|
+
{
|
|
50
|
+
tag: 'one-view',
|
|
51
|
+
bundle: '/microdots/one.js',
|
|
52
|
+
localPort: 3991,
|
|
53
|
+
attributes: {},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
tag: 'one-admin',
|
|
57
|
+
bundle: '/microdots/one.js',
|
|
58
|
+
localPort: 3991,
|
|
59
|
+
attributes: {},
|
|
60
|
+
},
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('duplicate tags fail before orchestration', () => {
|
|
65
|
+
const directory = root()
|
|
66
|
+
writeDot(directory, 'one', {
|
|
67
|
+
tags: ['one-view'],
|
|
68
|
+
port: 3991,
|
|
69
|
+
bundle: 'same.js',
|
|
70
|
+
})
|
|
71
|
+
writeDot(directory, 'two', {
|
|
72
|
+
tags: ['one-view'],
|
|
73
|
+
port: 3992,
|
|
74
|
+
bundle: 'two.js',
|
|
75
|
+
})
|
|
76
|
+
expect(() => listMicroDots(directory)).toThrow('duplicate tag')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('duplicate ports fail before orchestration', () => {
|
|
80
|
+
const directory = root()
|
|
81
|
+
writeDot(directory, 'one', {
|
|
82
|
+
tags: ['one-view'],
|
|
83
|
+
port: 3991,
|
|
84
|
+
bundle: 'one.js',
|
|
85
|
+
})
|
|
86
|
+
writeDot(directory, 'two', {
|
|
87
|
+
tags: ['two-view'],
|
|
88
|
+
port: 3991,
|
|
89
|
+
bundle: 'two.js',
|
|
90
|
+
})
|
|
91
|
+
expect(() => listMicroDots(directory)).toThrow('duplicate port')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('duplicate bundles fail before orchestration', () => {
|
|
95
|
+
const directory = root()
|
|
96
|
+
writeDot(directory, 'one', {
|
|
97
|
+
tags: ['one-view'],
|
|
98
|
+
port: 3991,
|
|
99
|
+
bundle: 'same.js',
|
|
100
|
+
})
|
|
101
|
+
writeDot(directory, 'two', {
|
|
102
|
+
tags: ['two-view'],
|
|
103
|
+
port: 3992,
|
|
104
|
+
bundle: 'same.js',
|
|
105
|
+
})
|
|
106
|
+
expect(() => listMicroDots(directory)).toThrow('duplicate bundle')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('surface tags must match package metadata exactly', () => {
|
|
110
|
+
const directory = root()
|
|
111
|
+
writeDot(directory, 'one', {
|
|
112
|
+
tags: ['one-view', 'one-admin'],
|
|
113
|
+
port: 3991,
|
|
114
|
+
bundle: 'one.js',
|
|
115
|
+
})
|
|
116
|
+
const dot = listMicroDots(directory)[0]
|
|
117
|
+
if (dot === undefined) throw new Error('fixture dot was not discovered')
|
|
118
|
+
|
|
119
|
+
expect(() =>
|
|
120
|
+
assertSurfaceTagsMatch(dot, [{ tag: 'one-admin' }, { tag: 'one-view' }]),
|
|
121
|
+
).not.toThrow()
|
|
122
|
+
expect(() => assertSurfaceTagsMatch(dot, [{ tag: 'one-view' }])).toThrow(
|
|
123
|
+
'do not match package.json microdot.tags',
|
|
124
|
+
)
|
|
125
|
+
expect(() =>
|
|
126
|
+
assertSurfaceTagsMatch(dot, [
|
|
127
|
+
{ tag: 'one-view' },
|
|
128
|
+
{ tag: 'one-admin' },
|
|
129
|
+
{ tag: 'one-extra' },
|
|
130
|
+
]),
|
|
131
|
+
).toThrow('do not match package.json microdot.tags')
|
|
132
|
+
})
|
|
133
|
+
})
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { basename, resolve } from 'node:path'
|
|
3
|
+
import type { Plugin } from 'vite'
|
|
4
|
+
|
|
5
|
+
export const repoRoot = resolve(import.meta.dirname, '..')
|
|
6
|
+
export const microdotsRoot = resolve(repoRoot, 'microdots')
|
|
7
|
+
|
|
8
|
+
export type MicroDotPackage = {
|
|
9
|
+
readonly name: string
|
|
10
|
+
readonly directory: string
|
|
11
|
+
readonly tags: ReadonlyArray<string>
|
|
12
|
+
readonly port: number
|
|
13
|
+
readonly bundle: string
|
|
14
|
+
readonly deployedApiUrl?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type PackageDocument = {
|
|
18
|
+
readonly name?: unknown
|
|
19
|
+
readonly microdot?: unknown
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const readJson = (path: string): PackageDocument =>
|
|
23
|
+
JSON.parse(readFileSync(path, 'utf8'))
|
|
24
|
+
|
|
25
|
+
const fail = (directory: string, detail: string): never => {
|
|
26
|
+
throw new Error(`[microdots] ${directory}/package.json ${detail}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const readMicroDotDirectory = (directory: string): MicroDotPackage => {
|
|
30
|
+
const document = readJson(resolve(directory, 'package.json'))
|
|
31
|
+
const metadata = document.microdot
|
|
32
|
+
if (typeof metadata !== 'object' || metadata === null) {
|
|
33
|
+
return fail(directory, 'must declare a microdot object')
|
|
34
|
+
}
|
|
35
|
+
const tags = 'tags' in metadata ? metadata.tags : undefined
|
|
36
|
+
const port = 'port' in metadata ? metadata.port : undefined
|
|
37
|
+
const bundle = 'bundle' in metadata ? metadata.bundle : undefined
|
|
38
|
+
const deployedApiUrl =
|
|
39
|
+
'deployedApiUrl' in metadata ? metadata.deployedApiUrl : undefined
|
|
40
|
+
if (
|
|
41
|
+
!Array.isArray(tags) ||
|
|
42
|
+
tags.length === 0 ||
|
|
43
|
+
!tags.every(tag => typeof tag === 'string' && /^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(tag))
|
|
44
|
+
) {
|
|
45
|
+
return fail(directory, 'microdot.tags must be non-empty custom-element names')
|
|
46
|
+
}
|
|
47
|
+
if (typeof port !== 'number' || !Number.isInteger(port) || port < 1) {
|
|
48
|
+
return fail(directory, 'microdot.port must be a positive integer')
|
|
49
|
+
}
|
|
50
|
+
if (typeof bundle !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*\.js$/.test(bundle)) {
|
|
51
|
+
return fail(directory, 'microdot.bundle must be a kebab-case .js filename')
|
|
52
|
+
}
|
|
53
|
+
if (deployedApiUrl !== undefined && typeof deployedApiUrl !== 'string') {
|
|
54
|
+
return fail(directory, 'microdot.deployedApiUrl must be a string when present')
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
name: basename(directory),
|
|
58
|
+
directory,
|
|
59
|
+
tags,
|
|
60
|
+
port,
|
|
61
|
+
bundle,
|
|
62
|
+
...(deployedApiUrl === undefined ? {} : { deployedApiUrl }),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const assertUnique = (
|
|
67
|
+
dots: ReadonlyArray<MicroDotPackage>,
|
|
68
|
+
label: string,
|
|
69
|
+
values: (dot: MicroDotPackage) => ReadonlyArray<string>,
|
|
70
|
+
): void => {
|
|
71
|
+
const owners = new Map<string, string>()
|
|
72
|
+
dots.forEach(dot => {
|
|
73
|
+
values(dot).forEach(value => {
|
|
74
|
+
const previous = owners.get(value)
|
|
75
|
+
if (previous !== undefined) {
|
|
76
|
+
throw new Error(`[microdots] duplicate ${label} "${value}" in ${previous} and ${dot.name}`)
|
|
77
|
+
}
|
|
78
|
+
owners.set(value, dot.name)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const listMicroDots = (
|
|
84
|
+
root: string = microdotsRoot,
|
|
85
|
+
): ReadonlyArray<MicroDotPackage> => {
|
|
86
|
+
if (!existsSync(root)) return []
|
|
87
|
+
const dots = readdirSync(root, { withFileTypes: true })
|
|
88
|
+
.filter(entry => entry.isDirectory())
|
|
89
|
+
.map(entry => resolve(root, entry.name))
|
|
90
|
+
.filter(directory => existsSync(resolve(directory, 'package.json')))
|
|
91
|
+
.map(readMicroDotDirectory)
|
|
92
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
93
|
+
assertUnique(dots, 'tag', dot => dot.tags)
|
|
94
|
+
assertUnique(dots, 'port', dot => [String(dot.port)])
|
|
95
|
+
assertUnique(dots, 'bundle', dot => [dot.bundle])
|
|
96
|
+
return dots
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const registryFor = (
|
|
100
|
+
dots: ReadonlyArray<MicroDotPackage> = listMicroDots(),
|
|
101
|
+
) =>
|
|
102
|
+
dots.flatMap(dot =>
|
|
103
|
+
dot.tags.map(tag => ({
|
|
104
|
+
tag,
|
|
105
|
+
bundle: `/microdots/${dot.bundle}`,
|
|
106
|
+
localPort: dot.port,
|
|
107
|
+
...(dot.deployedApiUrl === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { deployedApiUrl: dot.deployedApiUrl }),
|
|
110
|
+
attributes: {},
|
|
111
|
+
})),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
export const assertSurfaceTagsMatch = (
|
|
115
|
+
dot: MicroDotPackage,
|
|
116
|
+
surfaces: ReadonlyArray<{ readonly tag: string }>,
|
|
117
|
+
): void => {
|
|
118
|
+
const declared = [...dot.tags].sort()
|
|
119
|
+
const emitted = surfaces.map(surface => surface.tag).sort()
|
|
120
|
+
if (JSON.stringify(declared) !== JSON.stringify(emitted)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`[manifest] ${dot.name} surface tags [${emitted.join(', ')}] do not match package.json microdot.tags [${declared.join(', ')}]`,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const VIRTUAL_REGISTRY = 'virtual:microdots-registry'
|
|
128
|
+
const RESOLVED_REGISTRY = `\0${VIRTUAL_REGISTRY}`
|
|
129
|
+
|
|
130
|
+
export const microDotRegistryPlugin = (): Plugin => ({
|
|
131
|
+
name: 'starter:microdot-registry',
|
|
132
|
+
resolveId: id => (id === VIRTUAL_REGISTRY ? RESOLVED_REGISTRY : undefined),
|
|
133
|
+
load: id =>
|
|
134
|
+
id === RESOLVED_REGISTRY
|
|
135
|
+
? `export const registry = ${JSON.stringify(registryFor())}`
|
|
136
|
+
: undefined,
|
|
137
|
+
})
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { resolve } from 'node:path'
|
|
2
|
+
import type { Plugin, UserConfig } from 'vite'
|
|
3
|
+
|
|
4
|
+
import { emitMicroDotManifest } from '@bespokeagentics/microdots-vite-plugin'
|
|
5
|
+
|
|
6
|
+
import { readMicroDotDirectory } from './discover.ts'
|
|
7
|
+
|
|
8
|
+
export const dotConfig = (
|
|
9
|
+
dotDirectory: string,
|
|
10
|
+
plugins: ReadonlyArray<Plugin> = [],
|
|
11
|
+
): UserConfig => {
|
|
12
|
+
const dot = readMicroDotDirectory(dotDirectory)
|
|
13
|
+
return {
|
|
14
|
+
plugins: [...plugins, emitMicroDotManifest(dotDirectory)],
|
|
15
|
+
build: {
|
|
16
|
+
outDir: resolve(dotDirectory, '../../host/public/microdots'),
|
|
17
|
+
emptyOutDir: false,
|
|
18
|
+
cssCodeSplit: false,
|
|
19
|
+
target: 'esnext',
|
|
20
|
+
lib: {
|
|
21
|
+
entry: resolve(dotDirectory, 'src/entry.ts'),
|
|
22
|
+
formats: ['es'],
|
|
23
|
+
fileName: () => dot.bundle,
|
|
24
|
+
},
|
|
25
|
+
rolldownOptions: { output: { codeSplitting: false } },
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import {
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from 'node:fs'
|
|
9
|
+
import { resolve } from 'node:path'
|
|
10
|
+
import { pathToFileURL } from 'node:url'
|
|
11
|
+
import { gzipSync } from 'node:zlib'
|
|
12
|
+
import type {
|
|
13
|
+
AttributeSpec,
|
|
14
|
+
MicroDotSurface,
|
|
15
|
+
} from '@bespokeagentics/microdots-element'
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
assertSurfaceTagsMatch,
|
|
19
|
+
listMicroDots,
|
|
20
|
+
repoRoot,
|
|
21
|
+
} from './discover.ts'
|
|
22
|
+
|
|
23
|
+
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
|
24
|
+
typeof value === 'object' && value !== null
|
|
25
|
+
|
|
26
|
+
const isSurface = (value: unknown): value is MicroDotSurface => {
|
|
27
|
+
if (!isRecord(value)) return false
|
|
28
|
+
return (
|
|
29
|
+
typeof value['tag'] === 'string' &&
|
|
30
|
+
isRecord(value['attributes']) &&
|
|
31
|
+
isRecord(value['events'])
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const buildId = (bytes: Uint8Array): string =>
|
|
36
|
+
createHash('sha256').update(bytes).digest('hex').slice(0, 8)
|
|
37
|
+
|
|
38
|
+
const manifestAttribute = (name: string, spec: AttributeSpec) => ({
|
|
39
|
+
name,
|
|
40
|
+
type: spec.type ?? 'string',
|
|
41
|
+
required: spec.required ?? false,
|
|
42
|
+
...(spec.default === undefined ? {} : { default: spec.default }),
|
|
43
|
+
live: spec.live ?? false,
|
|
44
|
+
ownership: spec.ownership ?? 'dot',
|
|
45
|
+
...(spec.values === undefined ? {} : { values: spec.values }),
|
|
46
|
+
...(spec.description === undefined ? {} : { description: spec.description }),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const eventRef = (
|
|
50
|
+
dotName: string,
|
|
51
|
+
eventName: string,
|
|
52
|
+
payload: unknown,
|
|
53
|
+
contract: Readonly<Record<string, unknown>>,
|
|
54
|
+
): string => {
|
|
55
|
+
const exportName = Object.keys(contract).find(name =>
|
|
56
|
+
Object.is(contract[name], payload),
|
|
57
|
+
)
|
|
58
|
+
if (exportName === undefined) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`[manifest] ${dotName}.${eventName} payload is not a named contract export`,
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
return `@starter/${dotName}/contract#${exportName}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const assertNoDocumentReset = (name: string, source: string): void => {
|
|
67
|
+
const fingerprints = [
|
|
68
|
+
/\*,\s*::?after,\s*::?before/,
|
|
69
|
+
/(^|[;}]|\\)h1,h2,h3,h4,h5,h6\{/,
|
|
70
|
+
/-webkit-tap-highlight-color/,
|
|
71
|
+
]
|
|
72
|
+
if (fingerprints.some(pattern => pattern.test(source))) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`[manifest] ${name} bundle carries a document-wide reset; use the scoped MicroDot Tailwind prologue`,
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const onlyIndex = process.argv.indexOf('--only')
|
|
80
|
+
const only = onlyIndex === -1 ? undefined : process.argv[onlyIndex + 1]
|
|
81
|
+
const dots = listMicroDots().filter(dot => only === undefined || dot.name === only)
|
|
82
|
+
|
|
83
|
+
if (only !== undefined && dots.length !== 1) {
|
|
84
|
+
throw new Error(`[manifest] no MicroDot named ${only}`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const dot of dots) {
|
|
88
|
+
const bundlePath = resolve(repoRoot, 'host/public/microdots', dot.bundle)
|
|
89
|
+
if (!existsSync(bundlePath)) {
|
|
90
|
+
throw new Error(`[manifest] ${dot.name} bundle is missing: ${bundlePath}`)
|
|
91
|
+
}
|
|
92
|
+
const source = readFileSync(bundlePath)
|
|
93
|
+
assertNoDocumentReset(dot.name, source.toString('utf8'))
|
|
94
|
+
const build = buildId(source)
|
|
95
|
+
const stem = dot.bundle.slice(0, -3)
|
|
96
|
+
const hashedFile = `${stem}.${build}.js`
|
|
97
|
+
const dist = resolve(dot.directory, 'dist')
|
|
98
|
+
mkdirSync(dist, { recursive: true })
|
|
99
|
+
copyFileSync(bundlePath, resolve(dist, hashedFile))
|
|
100
|
+
|
|
101
|
+
const surfaceModule: unknown = await import(
|
|
102
|
+
pathToFileURL(resolve(dot.directory, 'src/surface.ts')).href
|
|
103
|
+
)
|
|
104
|
+
const contractModule: unknown = await import(
|
|
105
|
+
pathToFileURL(resolve(dot.directory, 'src/contract.ts')).href
|
|
106
|
+
)
|
|
107
|
+
if (!isRecord(surfaceModule) || !Array.isArray(surfaceModule['surfaces'])) {
|
|
108
|
+
throw new Error(`[manifest] ${dot.name} surface.ts must export surfaces[]`)
|
|
109
|
+
}
|
|
110
|
+
if (!isRecord(contractModule)) {
|
|
111
|
+
throw new Error(`[manifest] ${dot.name} contract exports are unreadable`)
|
|
112
|
+
}
|
|
113
|
+
const surfaces = surfaceModule['surfaces']
|
|
114
|
+
if (!surfaces.every(isSurface)) {
|
|
115
|
+
throw new Error(`[manifest] ${dot.name} surfaces[] contains an invalid surface`)
|
|
116
|
+
}
|
|
117
|
+
assertSurfaceTagsMatch(dot, surfaces)
|
|
118
|
+
const document = {
|
|
119
|
+
name: dot.name,
|
|
120
|
+
build,
|
|
121
|
+
bundle: {
|
|
122
|
+
file: dot.bundle,
|
|
123
|
+
hashedFile,
|
|
124
|
+
bytes: source.byteLength,
|
|
125
|
+
gzipBytes: gzipSync(source).byteLength,
|
|
126
|
+
},
|
|
127
|
+
service: {
|
|
128
|
+
localPort: dot.port,
|
|
129
|
+
...(dot.deployedApiUrl === undefined
|
|
130
|
+
? {}
|
|
131
|
+
: { deployedApiUrl: dot.deployedApiUrl }),
|
|
132
|
+
},
|
|
133
|
+
tags: surfaces.map(surface => ({
|
|
134
|
+
tag: surface.tag,
|
|
135
|
+
attributes: Object.entries(surface.attributes).map(([name, spec]) =>
|
|
136
|
+
manifestAttribute(name, spec),
|
|
137
|
+
),
|
|
138
|
+
events: Object.entries(surface.events).map(([name, event]) => ({
|
|
139
|
+
name,
|
|
140
|
+
payload: {
|
|
141
|
+
ref: eventRef(dot.name, name, event.payload, contractModule),
|
|
142
|
+
},
|
|
143
|
+
...(event.description === undefined
|
|
144
|
+
? {}
|
|
145
|
+
: { description: event.description }),
|
|
146
|
+
})),
|
|
147
|
+
})),
|
|
148
|
+
}
|
|
149
|
+
writeFileSync(
|
|
150
|
+
resolve(dist, `${dot.name}.manifest.json`),
|
|
151
|
+
`${JSON.stringify(document, null, 2)}\n`,
|
|
152
|
+
)
|
|
153
|
+
console.log(`[manifest] ${dot.name}@${build} → dist/${dot.name}.manifest.json`)
|
|
154
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ESNext", "DOM"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
11
|
+
"exactOptionalPropertyTypes": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"types": ["bun", "node", "vite/client"]
|
|
16
|
+
},
|
|
17
|
+
"include": ["types.d.ts", "host/**/*", "microdots/**/*", "scripts/**/*", "tests/**/*"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
* { box-sizing: border-box; }
|
|
2
|
+
[hidden] { display: none !important; }
|
|
3
|
+
|
|
4
|
+
html { background: var(--surface-page); color: var(--text-primary); }
|
|
5
|
+
body {
|
|
6
|
+
margin: 0;
|
|
7
|
+
min-width: 20rem;
|
|
8
|
+
min-height: 100vh;
|
|
9
|
+
font-family: var(--font-body);
|
|
10
|
+
background: var(--surface-page);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.shell-header,
|
|
14
|
+
.shell-nav,
|
|
15
|
+
main,
|
|
16
|
+
.shell-footer { width: min(72rem, calc(100% - 2rem)); margin-inline: auto; }
|
|
17
|
+
|
|
18
|
+
.shell-header {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-wrap: wrap;
|
|
21
|
+
align-items: end;
|
|
22
|
+
justify-content: space-between;
|
|
23
|
+
gap: 1rem;
|
|
24
|
+
padding-block: 2.5rem 1.5rem;
|
|
25
|
+
}
|
|
26
|
+
.shell-header h1 { margin: 0; font-size: clamp(1.8rem, 5vw, 3.4rem); line-height: 1.05; }
|
|
27
|
+
.shell-kicker { margin: 0 0 0.5rem; color: var(--text-accent); font-weight: var(--fw-semibold); text-transform: uppercase; letter-spacing: var(--ls-wider); font-size: var(--fs-caption); }
|
|
28
|
+
.theme-control { display: grid; gap: 0.35rem; color: var(--text-secondary); font-size: var(--fs-label); }
|
|
29
|
+
.theme-control select { color: var(--text-primary); background: var(--surface-default); border: 1px solid var(--border-default); border-radius: var(--radius-sm); padding: 0.6rem 2rem 0.6rem 0.75rem; }
|
|
30
|
+
|
|
31
|
+
.shell-nav { display: flex; flex-wrap: wrap; gap: 0.4rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--border-subtle); }
|
|
32
|
+
.shell-nav__link { color: var(--text-secondary); text-decoration: none; border-radius: var(--radius-sm); padding: 0.55rem 0.8rem; }
|
|
33
|
+
.shell-nav__link:hover { color: var(--text-primary); background: var(--int-hover); }
|
|
34
|
+
.shell-nav__link[aria-current='page'] { color: var(--text-accent); background: var(--int-active); font-weight: var(--fw-semibold); }
|
|
35
|
+
|
|
36
|
+
main { padding-block: 1.5rem 3rem; }
|
|
37
|
+
.route-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); gap: 1rem; align-items: start; }
|
|
38
|
+
.route-section { display: contents; }
|
|
39
|
+
.microdot-slot { min-width: 0; }
|
|
40
|
+
.microdot-slot > * { display: block; }
|
|
41
|
+
|
|
42
|
+
.placeholder { min-height: 14rem; display: grid; place-content: center; gap: 0.5rem; padding: 2rem; text-align: center; color: var(--text-secondary); background: var(--surface-default); border: 1px dashed var(--border-default); border-radius: var(--radius-lg); }
|
|
43
|
+
.placeholder h2, .placeholder p { margin: 0; }
|
|
44
|
+
.placeholder--error { color: var(--status-error-fg); border-color: var(--status-error-border); background: var(--status-error-bg); }
|
|
45
|
+
|
|
46
|
+
.shell-footer { padding-block: 1rem 2rem; color: var(--text-muted); border-top: 1px solid var(--border-subtle); font-family: var(--font-mono); font-size: var(--fs-mono-sm); }
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
@import './tokens.css';
|
|
2
|
+
|
|
3
|
+
.starter-card { font-family: var(--font-body); color: var(--text-primary); background: var(--surface-default); border: 1px solid var(--border-default); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); padding: 1.25rem; display: grid; gap: 1rem; }
|
|
4
|
+
.starter-card h2 { margin: 0; color: var(--text-heading); font-family: var(--font-display); font-size: var(--fs-display-m); }
|
|
5
|
+
.starter-card header, .starter-card footer { display: grid; gap: 0.5rem; }
|
|
6
|
+
.starter-card footer { grid-template-columns: 1fr auto; align-items: center; border-top: 1px solid var(--border-subtle); padding-top: 1rem; color: var(--text-muted); font-size: var(--fs-caption); }
|
|
7
|
+
.starter-card__body { min-height: 8rem; display: grid; align-content: center; gap: 0.75rem; padding: 1rem; background: var(--surface-raised); border-radius: var(--radius-md); }
|
|
8
|
+
.microdot-overline, .microdot-note, .microdot-state { margin: 0; }
|
|
9
|
+
.microdot-overline { color: var(--text-muted); font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: 0.08em; }
|
|
10
|
+
.microdot-note { color: var(--text-secondary); font-size: var(--fs-body-s); line-height: 1.5; }
|
|
11
|
+
.microdot-state { color: var(--text-primary); font-weight: var(--fw-medium); }
|
|
12
|
+
.microdot-state--error { color: var(--status-error-fg); }
|
|
13
|
+
.microdot-action { font: inherit; cursor: pointer; color: var(--text-on-brand); background: var(--accent-fill); border: 0; border-radius: var(--radius-sm); padding: 0.55rem 0.8rem; }
|
|
14
|
+
.microdot-action:hover { background: var(--accent-fill-hover); }
|
|
15
|
+
.pulse__field { display: flex; justify-content: space-between; gap: 1rem; }
|
|
16
|
+
.pulse__label { color: var(--text-muted); font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
17
|
+
.pulse__value { color: var(--text-primary); font-family: var(--font-mono); font-size: var(--fs-caption); text-align: right; word-break: break-all; }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
@import './tokens.css';
|
|
2
|
+
|
|
3
|
+
.starter-card { font-family: var(--font-body); color: var(--text-primary); background: var(--surface-default); border: 1px solid var(--border-default); border-radius: var(--radius-lg); box-shadow: var(--shadow-card); padding: 1.25rem; display: grid; gap: 1rem; }
|
|
4
|
+
.starter-card h2 { margin: 0; color: var(--text-heading); font-family: var(--font-display); font-size: var(--fs-display-m); }
|
|
5
|
+
.starter-card header, .starter-card footer { display: grid; gap: 0.5rem; }
|
|
6
|
+
.starter-card footer { grid-template-columns: 1fr auto; align-items: center; border-top: 1px solid var(--border-subtle); padding-top: 1rem; color: var(--text-muted); font-size: var(--fs-caption); }
|
|
7
|
+
.starter-card__body { min-height: 8rem; display: grid; align-content: center; gap: 0.75rem; padding: 1rem; background: var(--surface-raised); border-radius: var(--radius-md); }
|
|
8
|
+
.microdot-overline, .microdot-note, .microdot-state { margin: 0; }
|
|
9
|
+
.microdot-overline { color: var(--text-muted); font-size: var(--fs-caption); text-transform: uppercase; letter-spacing: 0.08em; }
|
|
10
|
+
.microdot-note { color: var(--text-secondary); font-size: var(--fs-body-s); line-height: 1.5; }
|
|
11
|
+
.microdot-state { color: var(--text-primary); font-weight: var(--fw-medium); }
|
|
12
|
+
.microdot-state--error { color: var(--status-error-fg); }
|
|
13
|
+
.microdot-action, .scope-picker__choice { font: inherit; cursor: pointer; border-radius: var(--radius-sm); padding: 0.55rem 0.8rem; }
|
|
14
|
+
.microdot-action { color: var(--text-on-brand); background: var(--accent-fill); border: 0; }
|
|
15
|
+
.microdot-action:hover { background: var(--accent-fill-hover); }
|
|
16
|
+
.scope-picker__choices { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
|
17
|
+
.scope-picker__choice { color: var(--text-primary); background: var(--surface-default); border: 1px solid var(--border-default); }
|
|
18
|
+
.scope-picker__choice:hover { background: var(--int-hover); }
|
|
19
|
+
.scope-picker__choice--selected { background: var(--int-active); border-color: var(--accent-fill); }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"workspaces": ["host", "microdots/*"],
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "bun scripts/dev.ts",
|
|
9
|
+
"build": "bun scripts/build.ts",
|
|
10
|
+
"typecheck": "tsc --noEmit",
|
|
11
|
+
"lint": "oxlint host microdots scripts",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"tokens:check": "bun scripts/check-tokens.ts",
|
|
14
|
+
"check": "bun run typecheck && bun run lint && bun run test && bun run tokens:check"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@bespokeagentics/microdots-authoring": "0.1.1",
|
|
18
|
+
"@bespokeagentics/microdots-element": "0.1.1",
|
|
19
|
+
"@bespokeagentics/microdots-host": "0.1.1",
|
|
20
|
+
"@bespokeagentics/microdots-runtime": "0.1.1",
|
|
21
|
+
"@bespokeagentics/microdots-theme": "0.1.1",
|
|
22
|
+
"@effect/platform-node": "4.0.0-rc.108",
|
|
23
|
+
"effect": "4.0.0-rc.108",
|
|
24
|
+
"foldkit": "^0.144.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@bespokeagentics/microdots-vite-plugin": "0.1.1",
|
|
28
|
+
"@types/bun": "^1.3.14",
|
|
29
|
+
"@types/node": "^26.2.0",
|
|
30
|
+
"happy-dom": "^20.10.4",
|
|
31
|
+
"oxlint": "^1.78.0",
|
|
32
|
+
"typescript": "^7.0.2",
|
|
33
|
+
"vite": "^8.0.16",
|
|
34
|
+
"vitest": "^4.1.10"
|
|
35
|
+
},
|
|
36
|
+
"overrides": {
|
|
37
|
+
"@effect/platform-node-shared": "4.0.0-rc.108"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
@import '@bespokeagentics/microdots-theme/theme.css';
|
|
3
|
+
@source './';
|
|
4
|
+
|
|
5
|
+
[hidden] { display: none !important; }
|
|
6
|
+
|
|
7
|
+
@layer components {
|
|
8
|
+
html { @apply bg-surface-page text-fg; }
|
|
9
|
+
body { @apply m-0 min-h-screen min-w-80 bg-surface-page font-body; }
|
|
10
|
+
.shell-header, .shell-nav, main, .shell-footer { @apply mx-auto w-[min(72rem,calc(100%-2rem))]; }
|
|
11
|
+
.shell-header { @apply flex flex-wrap items-end justify-between gap-4 py-10 pb-6; }
|
|
12
|
+
.shell-header h1 { @apply m-0 text-[clamp(1.8rem,5vw,3.4rem)] leading-tight; }
|
|
13
|
+
.shell-kicker { @apply mb-2 mt-0 text-caption font-semibold uppercase tracking-wider text-fg-accent; }
|
|
14
|
+
.theme-control { @apply grid gap-1.5 text-label text-fg-muted; }
|
|
15
|
+
.theme-control select { @apply rounded-sm border border-border bg-surface px-3 py-2 text-fg; }
|
|
16
|
+
.shell-nav { @apply flex flex-wrap gap-1.5 border-b border-border-subtle pb-6; }
|
|
17
|
+
.shell-nav__link { @apply rounded-sm px-3 py-2 text-fg-muted no-underline hover:bg-int-hover hover:text-fg; }
|
|
18
|
+
.shell-nav__link[aria-current='page'] { @apply bg-int-active font-semibold text-fg-accent; }
|
|
19
|
+
main { @apply py-6 pb-12; }
|
|
20
|
+
.route-grid { @apply grid items-start gap-4; grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); }
|
|
21
|
+
.route-section { display: contents; }
|
|
22
|
+
.microdot-slot { @apply min-w-0; }
|
|
23
|
+
.microdot-slot > * { @apply block; }
|
|
24
|
+
.placeholder { @apply grid min-h-56 place-content-center gap-2 rounded-lg border border-dashed border-border bg-surface p-8 text-center text-fg-muted; }
|
|
25
|
+
.placeholder h2, .placeholder p { @apply m-0; }
|
|
26
|
+
.placeholder--error { @apply border-status-error-border bg-status-error-bg text-status-error-fg; }
|
|
27
|
+
.shell-footer { @apply border-t border-border-subtle py-4 pb-8 font-mono text-mono-sm text-fg-subtle; }
|
|
28
|
+
}
|