@mica-js/sdk 0.0.0-stage → 0.2.1

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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +311 -2
  3. package/dist/build/cli.d.ts +22 -0
  4. package/dist/build/cli.js +211 -0
  5. package/dist/build/config.d.ts +16 -0
  6. package/dist/build/config.js +92 -0
  7. package/dist/build/content.d.ts +14 -0
  8. package/dist/build/content.js +159 -0
  9. package/dist/build/prepare.d.ts +51 -0
  10. package/dist/build/prepare.js +306 -0
  11. package/dist/build/project.d.ts +4 -0
  12. package/dist/build/project.js +30 -0
  13. package/dist/build/vite.d.ts +7 -0
  14. package/dist/build/vite.js +60 -0
  15. package/dist/ddl.d.ts +2 -0
  16. package/dist/ddl.js +1 -0
  17. package/dist/device.d.ts +1 -0
  18. package/dist/device.js +1 -0
  19. package/dist/index.d.ts +2 -0
  20. package/dist/index.js +1 -0
  21. package/dist/metadata/index.d.ts +5 -0
  22. package/dist/metadata/index.js +2 -0
  23. package/dist/metadata/schema.d.ts +10 -0
  24. package/dist/metadata/schema.js +392 -0
  25. package/dist/metadata/types.d.ts +192 -0
  26. package/dist/metadata/types.js +7 -0
  27. package/dist/metadata/validate.d.ts +25 -0
  28. package/dist/metadata/validate.js +283 -0
  29. package/dist/model.d.ts +1 -0
  30. package/dist/model.js +1 -0
  31. package/dist/widget.d.ts +1 -0
  32. package/dist/widget.js +1 -0
  33. package/package.json +74 -3
  34. package/templates/basic/mica.config.ts +3 -0
  35. package/templates/basic/package.json +14 -0
  36. package/templates/basic/src/devices/resistor/device.json +30 -0
  37. package/templates/basic/src/devices/resistor/index.ts +8 -0
  38. package/templates/basic/src/devices/resistor/model.ddl +6 -0
  39. package/templates/basic/src/widgets/lamp/index.ts +22 -0
  40. package/templates/basic/src/widgets/lamp/widget.json +9 -0
  41. package/templates/basic/tsconfig.json +14 -0
@@ -0,0 +1,159 @@
1
+ import { readFile, readdir, stat } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { isBuiltin } from 'node:module';
4
+ import { compileDDL } from '@mica-js/ddl';
5
+ import { build, normalizePath } from 'vite';
6
+ import { parseAst } from 'rolldown/parseAst';
7
+ import { validateMetadata } from '../metadata/index.js';
8
+ function assertSelfContained(code, label) {
9
+ const visit = (node) => {
10
+ if (node === null || typeof node !== 'object')
11
+ return;
12
+ if (Array.isArray(node)) {
13
+ for (const child of node)
14
+ visit(child);
15
+ return;
16
+ }
17
+ const record = node;
18
+ if (record.type === 'ImportDeclaration' ||
19
+ record.type === 'ImportExpression' ||
20
+ ((record.type === 'ExportNamedDeclaration' || record.type === 'ExportAllDeclaration') &&
21
+ record.source) ||
22
+ (record.type === 'MetaProperty' && record.meta.name === 'import'))
23
+ throw new Error(`${label}: implementation contains a runtime module or asset reference`);
24
+ for (const child of Object.values(record))
25
+ visit(child);
26
+ };
27
+ visit(parseAst(code));
28
+ }
29
+ async function exists(path) {
30
+ try {
31
+ return (await stat(path)).isFile();
32
+ }
33
+ catch (error) {
34
+ if (error.code === 'ENOENT')
35
+ return false;
36
+ throw error;
37
+ }
38
+ }
39
+ // JSON alone loses the IR's non-finite numbers and negative zero.
40
+ export function modelExpression(value) {
41
+ if (typeof value === 'number') {
42
+ if (Number.isNaN(value))
43
+ return 'NaN';
44
+ if (value === Infinity)
45
+ return 'Infinity';
46
+ if (value === -Infinity)
47
+ return '-Infinity';
48
+ return Object.is(value, -0) ? '-0' : String(value);
49
+ }
50
+ if (value === null || typeof value === 'string' || typeof value === 'boolean')
51
+ return JSON.stringify(value);
52
+ if (value instanceof Uint8Array)
53
+ return `new Uint8Array(${modelExpression(Array.from(value))})`;
54
+ if (Array.isArray(value))
55
+ return `[${value.map(modelExpression).join(',')}]`;
56
+ if (typeof value === 'object')
57
+ return `{${Object.entries(value)
58
+ .map(([key, item]) => `${key === '__proto__' ? '["__proto__"]' : JSON.stringify(key)}:${modelExpression(item)}`)
59
+ .join(',')}}`;
60
+ throw new TypeError(`Cannot serialize model value: ${typeof value}`);
61
+ }
62
+ /** Builds each registration separately so no shared chunk or runtime import escapes. */
63
+ export async function buildContent(options) {
64
+ const files = [];
65
+ for (const kind of ['device', 'widget']) {
66
+ const category = `${kind}s`;
67
+ const directory = resolve(options.root, options.sourceDir ?? 'src', category);
68
+ const entries = await readdir(directory, { withFileTypes: true }).catch((error) => {
69
+ if (error.code === 'ENOENT')
70
+ return [];
71
+ throw error;
72
+ });
73
+ for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
74
+ if (!entry.isDirectory())
75
+ continue;
76
+ const id = entry.name;
77
+ const base = resolve(directory, id);
78
+ const jsonPath = resolve(base, `${kind}.json`);
79
+ const metadata = JSON.parse(await readFile(jsonPath, 'utf8'));
80
+ validateMetadata(metadata, kind, id);
81
+ let source;
82
+ const entryId = 'virtual:mica-content-entry';
83
+ if (kind === 'device') {
84
+ const ddlPath = resolve(base, 'model.ddl');
85
+ const model = await compileDDL(await readFile(ddlPath, 'utf8'), {
86
+ uri: `mica:devices/${id}/model.ddl`,
87
+ });
88
+ const adapter = resolve(base, 'index.ts');
89
+ source = `export const model = ${modelExpression(model)};\n`;
90
+ source += (await exists(adapter))
91
+ ? `export { default as create } from ${JSON.stringify(normalizePath(adapter))};\n`
92
+ : 'export function create() { return { sync() {}, dispose() {} }; }\n';
93
+ }
94
+ else {
95
+ source = `export { default } from ${JSON.stringify(normalizePath(resolve(base, 'index.ts')))};\n`;
96
+ }
97
+ const bundled = await build({
98
+ configFile: false,
99
+ root: options.root,
100
+ publicDir: false,
101
+ logLevel: 'silent',
102
+ resolve: {
103
+ ...(options.aliases === undefined ? {} : { alias: options.aliases }),
104
+ conditions: ['source', 'module', 'browser', 'production'],
105
+ },
106
+ plugins: [
107
+ {
108
+ name: 'mica:content-entry',
109
+ enforce: 'pre',
110
+ resolveId(id) {
111
+ if (isBuiltin(id))
112
+ throw new Error(`Node built-ins are unavailable in content modules: ${id}`);
113
+ if (id === entryId)
114
+ return '\0' + entryId;
115
+ },
116
+ load(id) {
117
+ if (id === '\0' + entryId)
118
+ return source;
119
+ },
120
+ },
121
+ ],
122
+ build: {
123
+ write: false,
124
+ target: 'es2022',
125
+ sourcemap: false,
126
+ minify: options.minify ?? false,
127
+ lib: { entry: entryId, formats: ['es'], fileName: () => `${kind}.js`, cssFileName: kind },
128
+ rolldownOptions: {
129
+ input: entryId,
130
+ output: { codeSplitting: false },
131
+ onwarn(warning, warn) {
132
+ if (warning.code === 'UNRESOLVED_IMPORT')
133
+ throw new Error(warning.message);
134
+ warn(warning);
135
+ },
136
+ },
137
+ },
138
+ });
139
+ if ('close' in bundled)
140
+ throw new Error('Watch builds are not supported inside a content build');
141
+ const outputs = (Array.isArray(bundled) ? bundled : [bundled]).flatMap((result) => result.output);
142
+ const chunk = outputs[0];
143
+ if (outputs.length !== 1 ||
144
+ chunk?.type !== 'chunk' ||
145
+ chunk.imports.length ||
146
+ chunk.dynamicImports.some((name) => name !== chunk.fileName))
147
+ throw new Error(`${category}/${id}: implementation must produce exactly one self-contained JavaScript module (${outputs.map((item) => item.fileName).join(', ')}; imports: ${chunk?.type === 'chunk' ? [...chunk.imports, ...chunk.dynamicImports].join(', ') : 'n/a'})`);
148
+ assertSelfContained(chunk.code, `${category}/${id}`);
149
+ const expectedExports = kind === 'device' ? ['create', 'model'] : ['default'];
150
+ if (expectedExports.some((name) => !chunk.exports.includes(name)))
151
+ throw new Error(`${category}/${id}: missing implementation export`);
152
+ files.push({
153
+ fileName: `${category}/${id}/${kind}.json`,
154
+ source: `${JSON.stringify(metadata, null, 2)}\n`,
155
+ }, { fileName: `${category}/${id}/${kind}.js`, source: chunk.code });
156
+ }
157
+ }
158
+ return files;
159
+ }
@@ -0,0 +1,51 @@
1
+ /** Device definition file expected inside every device directory. */
2
+ export declare const DEVICE_SOURCE_FILE = "model.ddl";
3
+ /** TypeScript declaration generated next to {@link DEVICE_SOURCE_FILE}. */
4
+ export declare const DEVICE_DECLARATION_FILE = "model.d.ddl.ts";
5
+ export interface PrepareOptions {
6
+ /** Package root containing the source directory; defaults to `process.cwd()`. */
7
+ readonly root?: string;
8
+ /** Source directory inside the root that holds `devices/`; defaults to `src`. */
9
+ readonly sourceDir?: string;
10
+ }
11
+ export interface WatchOptions extends PrepareOptions {
12
+ /** Coalescing window between a file event and the regeneration; defaults to 50 ms. */
13
+ readonly debounceMs?: number;
14
+ /** Receives the declaration paths after every successful regeneration. */
15
+ readonly onPrepare?: (files: readonly string[]) => void;
16
+ /** Receives every failed regeneration; watching continues afterwards. */
17
+ readonly onError?: (error: unknown) => void;
18
+ }
19
+ export interface PrepareWatcher {
20
+ /** Declaration paths from the initial scan; resolves empty when that scan failed. */
21
+ readonly ready: Promise<readonly string[]>;
22
+ /** Stops watching and resolves once the regeneration in flight has finished. */
23
+ dispose(): Promise<void>;
24
+ }
25
+ /**
26
+ * Compiles `<root>/<sourceDir>/devices/<name>/model.ddl` into the source-adjacent
27
+ * `model.d.ddl.ts` declaration module of every device directory.
28
+ *
29
+ * Every device compiles before the first write, so a failing definition never leaves the package
30
+ * with a partly updated set of declarations. A declaration whose content is unchanged keeps its
31
+ * previous file and timestamp. The previous declaration of a device is removed when its
32
+ * definition fails to compile or no longer exists; only files carrying the generated marker are
33
+ * ever removed.
34
+ *
35
+ * @returns Absolute paths of the declaration files that the current definitions describe, in
36
+ * device directory order.
37
+ * @throws The original `BuildError` of a failed compilation, with the device file path prefixed
38
+ * to its message.
39
+ */
40
+ export declare function prepare(options?: PrepareOptions): Promise<readonly string[]>;
41
+ /**
42
+ * Regenerates declarations whenever a device definition below `<root>/<sourceDir>` appears,
43
+ * changes or disappears; output writes never feed back into the watch loop.
44
+ *
45
+ * Events are coalesced within {@link WatchOptions.debounceMs} and runs are serialized, so a
46
+ * save burst produces a single regeneration. Failures reach {@link WatchOptions.onError} and
47
+ * never stop the watcher.
48
+ *
49
+ * @throws When the source directory cannot be watched; the message names that directory.
50
+ */
51
+ export declare function watchDevices(options?: WatchOptions): PrepareWatcher;
@@ -0,0 +1,306 @@
1
+ import { watch } from 'node:fs';
2
+ import { readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises';
3
+ import { basename, join, resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { compileDDL, generateDeviceTypeScript } from '@mica-js/ddl';
6
+ /** Device definition file expected inside every device directory. */
7
+ export const DEVICE_SOURCE_FILE = 'model.ddl';
8
+ /** TypeScript declaration generated next to {@link DEVICE_SOURCE_FILE}. */
9
+ export const DEVICE_DECLARATION_FILE = 'model.d.ddl.ts';
10
+ /** Runtime surface every generated declaration imports its boundary types from. */
11
+ const DEVICE_MODULE = '@mica-js/sdk/device';
12
+ /** Text marking a file as a `mica prepare` output, so only generated files are removed. */
13
+ const GENERATED_MARKER = '@mica-js/sdk prepare';
14
+ const DEVICE_DIRECTORY = 'devices';
15
+ const DEFAULT_SOURCE_DIR = 'src';
16
+ const DEFAULT_DEBOUNCE_MS = 50;
17
+ const DEVICE_ALIASES = [
18
+ 'Props',
19
+ 'PropertyInput',
20
+ 'Inputs',
21
+ 'Memory',
22
+ 'Outputs',
23
+ 'Context',
24
+ 'Batch',
25
+ 'Adapter',
26
+ 'Factory',
27
+ ];
28
+ /**
29
+ * Compiles `<root>/<sourceDir>/devices/<name>/model.ddl` into the source-adjacent
30
+ * `model.d.ddl.ts` declaration module of every device directory.
31
+ *
32
+ * Every device compiles before the first write, so a failing definition never leaves the package
33
+ * with a partly updated set of declarations. A declaration whose content is unchanged keeps its
34
+ * previous file and timestamp. The previous declaration of a device is removed when its
35
+ * definition fails to compile or no longer exists; only files carrying the generated marker are
36
+ * ever removed.
37
+ *
38
+ * @returns Absolute paths of the declaration files that the current definitions describe, in
39
+ * device directory order.
40
+ * @throws The original `BuildError` of a failed compilation, with the device file path prefixed
41
+ * to its message.
42
+ */
43
+ export async function prepare(options = {}) {
44
+ const root = resolve(options.root ?? process.cwd());
45
+ const sourceDir = options.sourceDir ?? DEFAULT_SOURCE_DIR;
46
+ const devicesDir = resolve(root, sourceDir, DEVICE_DIRECTORY);
47
+ const directories = await listDeviceDirectories(devicesDir);
48
+ const devices = [];
49
+ const orphaned = [];
50
+ for (const directory of directories) {
51
+ const deviceDir = join(devicesDir, directory);
52
+ const sourceFile = join(deviceDir, DEVICE_SOURCE_FILE);
53
+ const declarationFile = join(deviceDir, DEVICE_DECLARATION_FILE);
54
+ if (await isFile(sourceFile))
55
+ devices.push({ sourceFile, declarationFile });
56
+ else
57
+ orphaned.push(declarationFile);
58
+ }
59
+ const outputs = [];
60
+ for (const device of devices)
61
+ outputs.push({
62
+ file: device.declarationFile,
63
+ content: await compileDeclaration(device.sourceFile, device.declarationFile),
64
+ });
65
+ for (const file of orphaned)
66
+ await removeGenerated(file);
67
+ for (const output of outputs)
68
+ await writeIfChanged(output.file, output.content);
69
+ return outputs.map((output) => output.file);
70
+ }
71
+ /**
72
+ * Regenerates declarations whenever a device definition below `<root>/<sourceDir>` appears,
73
+ * changes or disappears; output writes never feed back into the watch loop.
74
+ *
75
+ * Events are coalesced within {@link WatchOptions.debounceMs} and runs are serialized, so a
76
+ * save burst produces a single regeneration. Failures reach {@link WatchOptions.onError} and
77
+ * never stop the watcher.
78
+ *
79
+ * @throws When the source directory cannot be watched; the message names that directory.
80
+ */
81
+ export function watchDevices(options = {}) {
82
+ const root = resolve(options.root ?? process.cwd());
83
+ const sourceDir = options.sourceDir ?? DEFAULT_SOURCE_DIR;
84
+ const sourceRoot = resolve(root, sourceDir);
85
+ const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
86
+ let disposed = false;
87
+ let queued = false;
88
+ let timer;
89
+ let current;
90
+ const report = (error) => {
91
+ try {
92
+ options.onError?.(error);
93
+ }
94
+ catch {
95
+ // A failing reporter must not end the watch loop.
96
+ }
97
+ };
98
+ const scan = async () => {
99
+ try {
100
+ const files = await prepare({ root, sourceDir });
101
+ options.onPrepare?.(files);
102
+ return files;
103
+ }
104
+ catch (error) {
105
+ if (!disposed)
106
+ report(error);
107
+ return [];
108
+ }
109
+ };
110
+ const drain = async () => {
111
+ for (;;) {
112
+ queued = false;
113
+ const files = await scan();
114
+ if (!queued || disposed)
115
+ return files;
116
+ }
117
+ };
118
+ const schedule = () => {
119
+ if (disposed)
120
+ return;
121
+ if (timer !== undefined)
122
+ clearTimeout(timer);
123
+ timer = setTimeout(() => {
124
+ timer = undefined;
125
+ run();
126
+ }, debounceMs);
127
+ };
128
+ const start = () => {
129
+ const task = drain();
130
+ current = task;
131
+ void task.finally(() => {
132
+ if (current === task)
133
+ current = undefined;
134
+ if (queued && !disposed)
135
+ schedule();
136
+ });
137
+ return task;
138
+ };
139
+ const run = () => {
140
+ if (disposed)
141
+ return;
142
+ if (current)
143
+ queued = true;
144
+ else
145
+ start();
146
+ };
147
+ let watcher;
148
+ try {
149
+ watcher = watch(sourceRoot, { recursive: true }, (_event, filename) => {
150
+ if (disposed)
151
+ return;
152
+ void isRelevantEvent(sourceRoot, filename).then((relevant) => {
153
+ if (relevant && !disposed)
154
+ schedule();
155
+ }, () => {
156
+ // An unreadable path may still be the device directory that changed.
157
+ if (!disposed)
158
+ schedule();
159
+ });
160
+ });
161
+ }
162
+ catch (error) {
163
+ throw new Error(`Cannot watch ${sourceRoot} for ${DEVICE_SOURCE_FILE} changes: ${messageOf(error)}`, { cause: error });
164
+ }
165
+ watcher.on('error', (error) => {
166
+ if (!disposed)
167
+ report(error);
168
+ });
169
+ const ready = start();
170
+ return {
171
+ ready,
172
+ async dispose() {
173
+ if (disposed)
174
+ return;
175
+ disposed = true;
176
+ if (timer !== undefined) {
177
+ clearTimeout(timer);
178
+ timer = undefined;
179
+ }
180
+ watcher.close();
181
+ await current;
182
+ },
183
+ };
184
+ }
185
+ async function compileDeclaration(sourceFile, declarationFile) {
186
+ try {
187
+ const source = await readFile(sourceFile, 'utf8');
188
+ const model = await compileDDL(source, { uri: pathToFileURL(sourceFile).href });
189
+ return renderDeclaration(model);
190
+ }
191
+ catch (error) {
192
+ // The editor must never keep declarations that a failed definition contradicts.
193
+ await removeGenerated(declarationFile).catch(() => undefined);
194
+ throw annotate(error, sourceFile);
195
+ }
196
+ }
197
+ function renderDeclaration(model) {
198
+ const prefix = model.name;
199
+ const generated = generateDeviceTypeScript(model, {
200
+ deviceModule: DEVICE_MODULE,
201
+ name: prefix,
202
+ });
203
+ const aliases = DEVICE_ALIASES.map((alias) => `export type ${alias} = ${prefix}${alias};`);
204
+ return [
205
+ `// Generated by ${GENERATED_MARKER} from ${DEVICE_SOURCE_FILE}; do not edit.`,
206
+ '',
207
+ generated.trimEnd(),
208
+ '',
209
+ ...aliases,
210
+ '',
211
+ ].join('\n');
212
+ }
213
+ /** A `model.ddl` edit or a device directory appearing, disappearing or being renamed. */
214
+ async function isRelevantEvent(sourceRoot, filename) {
215
+ if (filename === null)
216
+ return true;
217
+ const relativePath = filename.toString();
218
+ if (basename(relativePath) === DEVICE_SOURCE_FILE)
219
+ return true;
220
+ return isDirectory(join(sourceRoot, relativePath));
221
+ }
222
+ async function listDeviceDirectories(devicesDir) {
223
+ let entries;
224
+ try {
225
+ entries = await readdir(devicesDir, { withFileTypes: true });
226
+ }
227
+ catch (error) {
228
+ if (errorCode(error) === 'ENOENT')
229
+ return [];
230
+ throw error;
231
+ }
232
+ const directories = [];
233
+ for (const entry of entries) {
234
+ if (entry.isDirectory())
235
+ directories.push(entry.name);
236
+ else if (entry.isSymbolicLink() && (await isDirectory(join(devicesDir, entry.name))))
237
+ directories.push(entry.name);
238
+ }
239
+ return directories.sort();
240
+ }
241
+ async function removeGenerated(file) {
242
+ let content;
243
+ try {
244
+ content = await readFile(file, 'utf8');
245
+ }
246
+ catch (error) {
247
+ if (errorCode(error) === 'ENOENT')
248
+ return;
249
+ throw error;
250
+ }
251
+ if (!content.includes(GENERATED_MARKER))
252
+ return;
253
+ await unlink(file).catch((error) => {
254
+ if (errorCode(error) !== 'ENOENT')
255
+ throw error;
256
+ });
257
+ }
258
+ async function writeIfChanged(file, content) {
259
+ try {
260
+ if ((await readFile(file, 'utf8')) === content)
261
+ return;
262
+ }
263
+ catch (error) {
264
+ if (errorCode(error) !== 'ENOENT')
265
+ throw error;
266
+ }
267
+ await writeFile(file, content, 'utf8');
268
+ }
269
+ async function isFile(path) {
270
+ try {
271
+ return (await stat(path)).isFile();
272
+ }
273
+ catch (error) {
274
+ const code = errorCode(error);
275
+ if (code === 'ENOENT' || code === 'ENOTDIR')
276
+ return false;
277
+ throw error;
278
+ }
279
+ }
280
+ async function isDirectory(path) {
281
+ try {
282
+ return (await stat(path)).isDirectory();
283
+ }
284
+ catch (error) {
285
+ const code = errorCode(error);
286
+ if (code === 'ENOENT' || code === 'ENOTDIR')
287
+ return false;
288
+ throw error;
289
+ }
290
+ }
291
+ function annotate(error, sourceFile) {
292
+ if (error instanceof Error) {
293
+ error.message = `${sourceFile}: ${error.message}`;
294
+ return error;
295
+ }
296
+ return new Error(`${sourceFile}: ${String(error)}`);
297
+ }
298
+ function messageOf(error) {
299
+ return error instanceof Error ? error.message : String(error);
300
+ }
301
+ function errorCode(error) {
302
+ if (typeof error !== 'object' || error === null || !('code' in error))
303
+ return undefined;
304
+ const code = error.code;
305
+ return typeof code === 'string' ? code : undefined;
306
+ }
@@ -0,0 +1,4 @@
1
+ import type { loadProjectConfig } from './config.js';
2
+ type Project = Awaited<ReturnType<typeof loadProjectConfig>>;
3
+ export declare function buildProject(project: Project): Promise<void>;
4
+ export {};
@@ -0,0 +1,30 @@
1
+ import { isAbsolute, relative, resolve } from 'node:path';
2
+ import { build } from 'vite';
3
+ import { micaSdk } from './vite.js';
4
+ function viteConfig(project) {
5
+ const { root, config } = project;
6
+ const source = resolve(root, config.sourceDir ?? 'src');
7
+ const output = resolve(root, config.outDir ?? 'dist');
8
+ const contains = (parent, child) => {
9
+ const path = relative(parent, child);
10
+ return (path === '' ||
11
+ (!isAbsolute(path) && path !== '..' && !path.startsWith('..\\') && !path.startsWith('../')));
12
+ };
13
+ if (contains(output, root) || contains(output, source) || contains(source, output))
14
+ throw new Error('outDir must be separate from the project root and sourceDir.');
15
+ return {
16
+ configFile: false,
17
+ root,
18
+ base: config.base ?? '/',
19
+ publicDir: false,
20
+ appType: 'custom',
21
+ resolve: {
22
+ alias: Object.fromEntries(Object.entries(config.alias ?? {}).map(([key, path]) => [key, resolve(root, path)])),
23
+ },
24
+ plugins: [micaSdk({ sourceDir: config.sourceDir ?? 'src' })],
25
+ build: { outDir: config.outDir ?? 'dist' },
26
+ };
27
+ }
28
+ export async function buildProject(project) {
29
+ await build(viteConfig(project));
30
+ }
@@ -0,0 +1,7 @@
1
+ import type { Plugin } from 'vite';
2
+ export interface MicaSdkOptions {
3
+ /** Source directory relative to the Vite root; defaults to src. */
4
+ readonly sourceDir?: string;
5
+ }
6
+ /** Builds independent two-file devices/widgets for the SDK CLI. */
7
+ export declare function micaSdk(options?: MicaSdkOptions): Plugin;
@@ -0,0 +1,60 @@
1
+ import { buildContent } from './content.js';
2
+ import { prepare } from './prepare.js';
3
+ const entryId = 'virtual:mica-sdk-empty';
4
+ /** Builds independent two-file devices/widgets for the SDK CLI. */
5
+ export function micaSdk(options = {}) {
6
+ let config;
7
+ const compile = async () => {
8
+ await prepare({ root: config.root, ...options });
9
+ return buildContent({
10
+ root: config.root,
11
+ ...options,
12
+ aliases: config.resolve.alias,
13
+ minify: config.build.minify !== false,
14
+ });
15
+ };
16
+ return {
17
+ name: 'mica:sdk',
18
+ config(_config, environment) {
19
+ if (environment.command !== 'build')
20
+ return;
21
+ return {
22
+ publicDir: false,
23
+ build: {
24
+ sourcemap: false,
25
+ copyPublicDir: false,
26
+ lib: { entry: entryId, formats: ['es'], fileName: () => '__mica_sdk__.js' },
27
+ rolldownOptions: { input: entryId },
28
+ },
29
+ };
30
+ },
31
+ configResolved(resolved) {
32
+ config = resolved;
33
+ },
34
+ resolveId(id) {
35
+ if (id === entryId)
36
+ return '\0' + entryId;
37
+ },
38
+ load(id) {
39
+ if (id === '\0' + entryId)
40
+ return 'export {};';
41
+ },
42
+ async buildStart() {
43
+ if (config.command !== 'build')
44
+ return;
45
+ for (const file of await compile())
46
+ this.emitFile({ type: 'asset', ...file });
47
+ },
48
+ generateBundle(_options, bundle) {
49
+ if (config.command !== 'build')
50
+ return;
51
+ for (const [name, output] of Object.entries(bundle)) {
52
+ if (output.type === 'chunk' && output.facadeModuleId === '\0' + entryId)
53
+ delete bundle[name];
54
+ else if (output.type === 'chunk' ||
55
+ !/^(devices\/[A-Za-z0-9-]+\/device|widgets\/[A-Za-z0-9-]+\/widget)\.(js|json)$/.test(name))
56
+ this.error(`Unexpected content artifact: ${name}`);
57
+ }
58
+ },
59
+ };
60
+ }
package/dist/ddl.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { compileDDL, generateDeviceTypes, generateDeviceTypeScript } from '@mica-js/ddl';
2
+ export type { CompileDDLOptions, DeviceTypeScriptOptions, DeviceIR } from '@mica-js/ddl';
package/dist/ddl.js ADDED
@@ -0,0 +1 @@
1
+ export { compileDDL, generateDeviceTypes, generateDeviceTypeScript } from '@mica-js/ddl';
@@ -0,0 +1 @@
1
+ export type { BorrowedBytes, ReadonlyBytes, ChannelMessage, Notification, DeviceAdapter, DeviceBatch, DeviceContext, DeviceFactory, InputChannel, InputNotification, TopicBridge, TopicMessage, JitHost, JitOptions, JitTranslate, JitTranslateInput, InstructionHook, Disposable, Event, } from '@mica-js/simulator';
package/dist/device.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { createDeviceLibrary, createBoard, SimulationError } from '@mica-js/simulator';
2
+ export type { Board, BoardDescription, BoardOptions, DeviceLibrary, DeviceLibraryEntry, } from '@mica-js/simulator';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createDeviceLibrary, createBoard, SimulationError } from '@mica-js/simulator';
@@ -0,0 +1,5 @@
1
+ export { deviceMetadataSchema, widgetMetadataSchema } from './schema.js';
2
+ export type { MetadataJsonSchema } from './schema.js';
3
+ export { MetadataValidationError, validateMetadata } from './validate.js';
4
+ export type { MetadataIssue } from './validate.js';
5
+ export type { ArcGraphic, BooleanProperty, BytesProperty, CheckboxEditor, CircleGraphic, DesignatorAnnotationMetadata, DeviceMetadata, EnumProperty, FileEditor, Graphic, GraphicFill, LineGraphic, Metadata, MetadataKind, NumberEditor, NumberProperty, PinDirection, PinLabelMetadata, PinMetadata, Point, PolylineGraphic, PortDelivery, PortDirection, PortMetadata, PortPayloadType, PropertyEditor, PropertyMetadata, PropertyType, RectGraphic, SelectEditor, StringProperty, SymbolAnnotationsMetadata, SymbolBounds, SymbolMetadata, SymbolPinMetadata, TextAlign, TextareaEditor, TextEditor, TextGraphic, WidgetMetadata, } from './types.js';
@@ -0,0 +1,2 @@
1
+ export { deviceMetadataSchema, widgetMetadataSchema } from './schema.js';
2
+ export { MetadataValidationError, validateMetadata } from './validate.js';
@@ -0,0 +1,10 @@
1
+ import type { MetadataKind } from './types.js';
2
+ export type { MetadataKind };
3
+ /** A JSON Schema node. The exported schemas are plain data and can be serialized as-is. */
4
+ export type MetadataJsonSchema = {
5
+ readonly [keyword: string]: unknown;
6
+ };
7
+ /** Metadata of a single device, independent of any package manifest or DDL source. */
8
+ export declare const deviceMetadataSchema: MetadataJsonSchema;
9
+ /** Metadata of a single widget, independent of any package manifest or DDL source. */
10
+ export declare const widgetMetadataSchema: MetadataJsonSchema;