@anby/platform-sdk 0.7.2 → 0.8.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/dist/cjs/apps/publish.d.ts.map +1 -1
- package/dist/cjs/apps/publish.js +36 -8
- package/dist/cjs/apps/publish.js.map +1 -1
- package/dist/cjs/vite/index.d.ts +5 -10
- package/dist/cjs/vite/index.d.ts.map +1 -1
- package/dist/cjs/vite/index.js +217 -39
- package/dist/cjs/vite/index.js.map +1 -1
- package/dist/esm/apps/publish.js +36 -8
- package/dist/esm/apps/publish.js.map +1 -1
- package/dist/esm/vite/index.js +219 -41
- package/dist/esm/vite/index.js.map +1 -1
- package/package.json +1 -1
- package/src/apps/publish.ts +40 -12
- package/src/vite/index.ts +245 -53
package/src/vite/index.ts
CHANGED
|
@@ -1,33 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @anby/platform-sdk/vite — Vite plugin for
|
|
2
|
+
* @anby/platform-sdk/vite — Vite plugin for Anby app integration.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Two responsibilities:
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* 1. **Auto-scan** source code to discover entity/event declarations and
|
|
7
|
+
* keep `anby-app.manifest.json` in sync — developer never edits
|
|
8
|
+
* provides/requires by hand.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* 2. **Codegen** typed module augmentations (`.anby/types.d.ts`) so
|
|
11
|
+
* `publishEvent` gets compile-time type checking.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
* plugins: [
|
|
13
|
-
* anbyVitePlugin(),
|
|
14
|
-
* remix({...}),
|
|
15
|
-
* ],
|
|
16
|
-
* });
|
|
17
|
-
* ```
|
|
18
|
-
*
|
|
19
|
-
* On every dev start (and on every change to `anby-app.manifest.json`),
|
|
20
|
-
* the plugin reads the manifest's `provides.events` and `requires.events`
|
|
21
|
-
* arrays and writes typed module augmentations to `.anby/types.d.ts`. The
|
|
22
|
-
* SDK's `publishEvent` overload picks up the merged `AppProvidedEvents`
|
|
23
|
-
* interface and gives the dev compile-time errors on unknown event names.
|
|
24
|
-
*
|
|
25
|
-
* The plugin imports `vite` only as a TYPE — `import type { Plugin }` is
|
|
26
|
-
* erased at compile time, so the SDK package does NOT need to ship vite
|
|
27
|
-
* as a runtime dependency. Apps that use this plugin already have vite.
|
|
13
|
+
* Runs on every dev start and on source/manifest file changes.
|
|
28
14
|
*/
|
|
29
|
-
import {
|
|
30
|
-
|
|
15
|
+
import {
|
|
16
|
+
readFileSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
statSync,
|
|
22
|
+
} from 'node:fs';
|
|
23
|
+
import { dirname, resolve, isAbsolute, join, basename, relative } from 'node:path';
|
|
31
24
|
import type { Plugin } from 'vite';
|
|
32
25
|
import { getInlinedManifest } from '../apps/publish.js';
|
|
33
26
|
|
|
@@ -36,24 +29,207 @@ export interface AnbyVitePluginOptions {
|
|
|
36
29
|
manifestPath?: string;
|
|
37
30
|
/** Output path for generated types relative to vite root. Default: `./.anby/types.d.ts`. */
|
|
38
31
|
outFile?: string;
|
|
39
|
-
/**
|
|
40
|
-
* Output path for the wire-format manifest static asset, relative to
|
|
41
|
-
* vite root. Default: `./public/_anby/manifest.json`.
|
|
42
|
-
*
|
|
43
|
-
* The Marketplace Submit-app form fetches this file from
|
|
44
|
-
* `${publicUrl}/_anby/manifest.json` to publish a running app without
|
|
45
|
-
* the operator pasting any JSON. Vite (dev) and Remix (prod) both
|
|
46
|
-
* serve files in `public/` as static assets, so the same artifact
|
|
47
|
-
* works in every runtime — no middleware, no Remix route file.
|
|
48
|
-
*/
|
|
32
|
+
/** Output path for the wire-format manifest static asset. Default: `./public/_anby/manifest.json`. */
|
|
49
33
|
manifestArtifactPath?: string;
|
|
34
|
+
/** Source directories to scan for entity/event usage. Default: `['./app']`. */
|
|
35
|
+
scanDirs?: string[];
|
|
36
|
+
/** Disable auto-scan (only codegen from manifest). Default: false. */
|
|
37
|
+
disableAutoScan?: boolean;
|
|
50
38
|
}
|
|
51
39
|
|
|
52
40
|
interface ManifestEventsShape {
|
|
53
|
-
provides?: { events?: string[] };
|
|
54
|
-
requires?: { events?: string[] };
|
|
41
|
+
provides?: { events?: string[]; entities?: Array<string | { name: string; version: string; schema?: unknown }> };
|
|
42
|
+
requires?: { events?: string[]; entities?: string[] };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Auto-scan: discover entities/events from source code ──────────────
|
|
46
|
+
|
|
47
|
+
interface ScanResult {
|
|
48
|
+
providesEntities: Array<{ name: string; version: string; schema: string }>;
|
|
49
|
+
providesEvents: string[];
|
|
50
|
+
requiresEntities: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Scan source files for SDK API usage patterns:
|
|
55
|
+
* - getEntityClient('org.period@v1') → requires entity
|
|
56
|
+
* - publishEvent({ type: 'org.node.created' ... }) → provides event
|
|
57
|
+
* - app/schemas/*.v*.json files → provides entities
|
|
58
|
+
*/
|
|
59
|
+
function scanSourceCode(root: string, scanDirs: string[]): ScanResult {
|
|
60
|
+
const providesEvents = new Set<string>();
|
|
61
|
+
const requiresEntities = new Set<string>();
|
|
62
|
+
const providesEntities: Array<{ name: string; version: string; schema: string }> = [];
|
|
63
|
+
|
|
64
|
+
// 1. Scan for entity schema files: app/schemas/{name}.{version}.json
|
|
65
|
+
const schemasDir = resolve(root, 'app/schemas');
|
|
66
|
+
if (existsSync(schemasDir)) {
|
|
67
|
+
try {
|
|
68
|
+
const files = readdirSync(schemasDir);
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
// Match: org-period.v1.json → name=org.period, version=v1
|
|
71
|
+
const match = file.match(/^(.+)\.(v\d+)\.json$/);
|
|
72
|
+
if (match) {
|
|
73
|
+
const rawName = match[1]; // "org-period"
|
|
74
|
+
const version = match[2]; // "v1"
|
|
75
|
+
// Convert filename to entity name: "org-period" → "org.period"
|
|
76
|
+
const entityName = rawName.replace(/-/g, '.');
|
|
77
|
+
const schemaPath = `./app/schemas/${file}`;
|
|
78
|
+
providesEntities.push({ name: entityName, version, schema: schemaPath });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// schemas dir unreadable — skip
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2. Scan source files for API usage patterns
|
|
87
|
+
for (const dir of scanDirs) {
|
|
88
|
+
const absDir = isAbsolute(dir) ? dir : resolve(root, dir);
|
|
89
|
+
if (!existsSync(absDir)) continue;
|
|
90
|
+
walkFiles(absDir, (filePath) => {
|
|
91
|
+
if (!filePath.match(/\.(ts|tsx|js|jsx)$/)) return;
|
|
92
|
+
// Skip node_modules and generated files
|
|
93
|
+
if (filePath.includes('node_modules') || filePath.includes('.anby/')) return;
|
|
94
|
+
|
|
95
|
+
let content: string;
|
|
96
|
+
try {
|
|
97
|
+
content = readFileSync(filePath, 'utf-8');
|
|
98
|
+
} catch {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Pattern 1: getEntityClient('org.period@v1')
|
|
103
|
+
const entityClientRegex1 = /getEntityClient\s*\(\s*['"]([^'"]+@[^'"]+)['"]/g;
|
|
104
|
+
for (const match of content.matchAll(entityClientRegex1)) {
|
|
105
|
+
requiresEntities.add(match[1]); // e.g. "org.period@v1"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Pattern 2: getEntityClient(tenantId, 'org.period', 'v1') or getEntityClient<T>(tenantId, 'org.period', 'v1')
|
|
109
|
+
const entityClientRegex2 = /getEntityClient\s*(?:<[^>]+>)?\s*\([^,]+,\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/g;
|
|
110
|
+
for (const match of content.matchAll(entityClientRegex2)) {
|
|
111
|
+
requiresEntities.add(`${match[1]}@${match[2]}`); // e.g. "org.period@v1"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// publishEvent({ type: 'org.node.created' ... })
|
|
115
|
+
const publishEventRegex = /publishEvent\s*\(\s*\{[^}]*?type\s*:\s*['"]([^'"]+)['"]/g;
|
|
116
|
+
for (const match of content.matchAll(publishEventRegex)) {
|
|
117
|
+
providesEvents.add(match[1]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// createEvent({ type: 'org.node.created' ... })
|
|
121
|
+
const createEventRegex = /createEvent\s*\(\s*\{[^}]*?type\s*:\s*['"]([^'"]+)['"]/g;
|
|
122
|
+
for (const match of content.matchAll(createEventRegex)) {
|
|
123
|
+
providesEvents.add(match[1]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Type union declarations near publishEvent context:
|
|
127
|
+
// type: 'org.node.created' | 'org.node.updated' | 'org.node.deleted'
|
|
128
|
+
// Matches dotted string literals in union type annotations
|
|
129
|
+
const typeUnionRegex = /type\s*:\s*((?:['"][a-z][a-z0-9.]*[a-z0-9]['"](?:\s*\|\s*)?)+)/g;
|
|
130
|
+
for (const unionMatch of content.matchAll(typeUnionRegex)) {
|
|
131
|
+
const unionStr = unionMatch[1];
|
|
132
|
+
const literalRegex = /['"]([a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*)+)['"]/g;
|
|
133
|
+
for (const litMatch of unionStr.matchAll(literalRegex)) {
|
|
134
|
+
providesEvents.add(litMatch[1]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
providesEntities,
|
|
142
|
+
providesEvents: [...providesEvents].sort(),
|
|
143
|
+
requiresEntities: [...requiresEntities].sort(),
|
|
144
|
+
};
|
|
55
145
|
}
|
|
56
146
|
|
|
147
|
+
function walkFiles(dir: string, callback: (path: string) => void): void {
|
|
148
|
+
let entries: string[];
|
|
149
|
+
try {
|
|
150
|
+
entries = readdirSync(dir);
|
|
151
|
+
} catch {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
const full = join(dir, entry);
|
|
156
|
+
try {
|
|
157
|
+
const stat = statSync(full);
|
|
158
|
+
if (stat.isDirectory()) {
|
|
159
|
+
if (entry === 'node_modules' || entry === '.anby' || entry === 'build') continue;
|
|
160
|
+
walkFiles(full, callback);
|
|
161
|
+
} else if (stat.isFile()) {
|
|
162
|
+
callback(full);
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// skip unreadable
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Manifest sync: merge scan results into manifest ───────────────────
|
|
171
|
+
|
|
172
|
+
function syncManifest(manifestPath: string, scan: ScanResult): boolean {
|
|
173
|
+
if (!existsSync(manifestPath)) return false;
|
|
174
|
+
|
|
175
|
+
let raw: string;
|
|
176
|
+
try {
|
|
177
|
+
raw = readFileSync(manifestPath, 'utf-8');
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const manifest = JSON.parse(raw);
|
|
183
|
+
let changed = false;
|
|
184
|
+
|
|
185
|
+
// Sync provides.entities
|
|
186
|
+
if (scan.providesEntities.length > 0) {
|
|
187
|
+
const current = JSON.stringify(manifest.provides?.entities ?? []);
|
|
188
|
+
const scanned = JSON.stringify(scan.providesEntities);
|
|
189
|
+
if (current !== scanned) {
|
|
190
|
+
manifest.provides = manifest.provides || {};
|
|
191
|
+
manifest.provides.entities = scan.providesEntities;
|
|
192
|
+
changed = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Sync provides.events
|
|
197
|
+
if (scan.providesEvents.length > 0) {
|
|
198
|
+
const current = JSON.stringify((manifest.provides?.events ?? []).slice().sort());
|
|
199
|
+
const scanned = JSON.stringify(scan.providesEvents);
|
|
200
|
+
if (current !== scanned) {
|
|
201
|
+
manifest.provides = manifest.provides || {};
|
|
202
|
+
manifest.provides.events = scan.providesEvents;
|
|
203
|
+
changed = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Sync requires.entities
|
|
208
|
+
if (scan.requiresEntities.length > 0) {
|
|
209
|
+
const current = JSON.stringify((manifest.requires?.entities ?? []).slice().sort());
|
|
210
|
+
const scanned = JSON.stringify(scan.requiresEntities);
|
|
211
|
+
if (current !== scanned) {
|
|
212
|
+
manifest.requires = manifest.requires || {};
|
|
213
|
+
manifest.requires.entities = scan.requiresEntities;
|
|
214
|
+
changed = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (changed) {
|
|
219
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
220
|
+
console.log('[anby-vite] ✔ Auto-updated manifest from source code scan');
|
|
221
|
+
const parts: string[] = [];
|
|
222
|
+
if (scan.providesEntities.length) parts.push(`provides ${scan.providesEntities.length} entities`);
|
|
223
|
+
if (scan.providesEvents.length) parts.push(`provides ${scan.providesEvents.length} events`);
|
|
224
|
+
if (scan.requiresEntities.length) parts.push(`requires ${scan.requiresEntities.length} entities`);
|
|
225
|
+
if (parts.length) console.log(`[anby-vite] ${parts.join(', ')}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return changed;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Codegen: typed module augmentations ───────────────────────────────
|
|
232
|
+
|
|
57
233
|
function renderTypes(provides: string[], requires: string[]): string {
|
|
58
234
|
const sections: string[] = [];
|
|
59
235
|
|
|
@@ -84,7 +260,6 @@ function renderTypes(provides: string[], requires: string[]): string {
|
|
|
84
260
|
|
|
85
261
|
sections.push('}');
|
|
86
262
|
sections.push('');
|
|
87
|
-
// Pure declaration file marker
|
|
88
263
|
sections.push('export {};');
|
|
89
264
|
sections.push('');
|
|
90
265
|
|
|
@@ -102,16 +277,32 @@ function readManifestEvents(manifestAbsPath: string): {
|
|
|
102
277
|
return { provides, requires };
|
|
103
278
|
}
|
|
104
279
|
|
|
280
|
+
// ── Plugin ────────────────────────────────────────────────────────────
|
|
281
|
+
|
|
105
282
|
export function anbyVitePlugin(opts: AnbyVitePluginOptions = {}): Plugin {
|
|
106
283
|
const manifestRelative = opts.manifestPath ?? './anby-app.manifest.json';
|
|
107
284
|
const outRelative = opts.outFile ?? './.anby/types.d.ts';
|
|
108
285
|
const artifactRelative =
|
|
109
286
|
opts.manifestArtifactPath ?? './public/_anby/manifest.json';
|
|
287
|
+
const scanDirsRelative = opts.scanDirs ?? ['./app'];
|
|
288
|
+
const autoScan = opts.disableAutoScan !== true;
|
|
110
289
|
|
|
290
|
+
let root = '';
|
|
111
291
|
let resolvedManifest = '';
|
|
112
292
|
let resolvedOut = '';
|
|
113
293
|
let resolvedArtifact = '';
|
|
114
294
|
|
|
295
|
+
function runAutoScan() {
|
|
296
|
+
if (!autoScan) return;
|
|
297
|
+
if (!existsSync(resolvedManifest)) return;
|
|
298
|
+
try {
|
|
299
|
+
const scan = scanSourceCode(root, scanDirsRelative);
|
|
300
|
+
syncManifest(resolvedManifest, scan);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.warn(`[anby-vite] auto-scan failed: ${(err as Error).message}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
115
306
|
function generateTypes() {
|
|
116
307
|
if (!existsSync(resolvedManifest)) {
|
|
117
308
|
console.warn(
|
|
@@ -131,13 +322,6 @@ export function anbyVitePlugin(opts: AnbyVitePluginOptions = {}): Plugin {
|
|
|
131
322
|
}
|
|
132
323
|
}
|
|
133
324
|
|
|
134
|
-
/**
|
|
135
|
-
* Generate `public/_anby/manifest.json` — the wire-format manifest
|
|
136
|
-
* with all `provides.entities[].schema` paths resolved to inline JSON
|
|
137
|
-
* content. Vite/Remix serve `public/` as static assets in both dev
|
|
138
|
-
* and production, so this single artifact powers the Marketplace
|
|
139
|
-
* Submit-app form everywhere.
|
|
140
|
-
*/
|
|
141
325
|
async function generateManifestArtifact() {
|
|
142
326
|
if (!existsSync(resolvedManifest)) return;
|
|
143
327
|
try {
|
|
@@ -154,17 +338,16 @@ export function anbyVitePlugin(opts: AnbyVitePluginOptions = {}): Plugin {
|
|
|
154
338
|
}
|
|
155
339
|
|
|
156
340
|
function regenerateAll() {
|
|
157
|
-
|
|
158
|
-
|
|
341
|
+
runAutoScan(); // 1. Scan code → update manifest
|
|
342
|
+
generateTypes(); // 2. Read manifest → generate types
|
|
343
|
+
void generateManifestArtifact(); // 3. Read manifest → generate wire artifact
|
|
159
344
|
}
|
|
160
345
|
|
|
161
346
|
return {
|
|
162
347
|
name: '@anby/platform-sdk/vite',
|
|
163
348
|
|
|
164
349
|
configResolved(config) {
|
|
165
|
-
|
|
166
|
-
// This is robust against pnpm workspaces / symlinks / monorepos.
|
|
167
|
-
const root = config.root;
|
|
350
|
+
root = config.root;
|
|
168
351
|
resolvedManifest = isAbsolute(manifestRelative)
|
|
169
352
|
? manifestRelative
|
|
170
353
|
: resolve(root, manifestRelative);
|
|
@@ -181,12 +364,21 @@ export function anbyVitePlugin(opts: AnbyVitePluginOptions = {}): Plugin {
|
|
|
181
364
|
},
|
|
182
365
|
|
|
183
366
|
configureServer(server) {
|
|
184
|
-
// Watch
|
|
185
|
-
// against the resolved path so we don't false-trigger on other
|
|
186
|
-
// manifest.json files in the workspace (e.g., node_modules).
|
|
367
|
+
// Watch manifest + source dirs
|
|
187
368
|
server.watcher.add(resolvedManifest);
|
|
369
|
+
for (const dir of scanDirsRelative) {
|
|
370
|
+
const absDir = isAbsolute(dir) ? dir : resolve(root, dir);
|
|
371
|
+
if (existsSync(absDir)) server.watcher.add(absDir);
|
|
372
|
+
}
|
|
373
|
+
|
|
188
374
|
server.watcher.on('change', (changedPath) => {
|
|
189
|
-
|
|
375
|
+
const abs = resolve(changedPath);
|
|
376
|
+
if (abs === resolvedManifest) {
|
|
377
|
+
// Manifest changed directly — regenerate types + artifact (no re-scan)
|
|
378
|
+
generateTypes();
|
|
379
|
+
void generateManifestArtifact();
|
|
380
|
+
} else if (changedPath.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
381
|
+
// Source file changed — re-scan → update manifest → regenerate
|
|
190
382
|
regenerateAll();
|
|
191
383
|
}
|
|
192
384
|
});
|