@bsb/base 9.1.5 → 9.1.8
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/lib/base/BSBService.d.ts +1 -1
- package/lib/base/BSBService.js +2 -7
- package/lib/base/BSBService.js.map +1 -1
- package/lib/base/PluginConfig.d.ts +3 -24
- package/lib/base/PluginConfig.js.map +1 -1
- package/lib/dev.js +11 -0
- package/lib/dev.js.map +1 -1
- package/lib/interfaces/schema-events.d.ts +3 -5
- package/lib/interfaces/schema-events.js +1 -4
- package/lib/interfaces/schema-events.js.map +1 -1
- package/lib/plugins/service-default3/index.js +0 -1
- package/lib/plugins/service-default3/index.js.map +1 -1
- package/lib/schemas/config-default.json +1 -1
- package/lib/schemas/config-default.plugin.json +7 -1
- package/lib/schemas/events-default.json +1 -1
- package/lib/schemas/events-default.plugin.json +7 -1
- package/lib/schemas/observable-default.json +1 -1
- package/lib/schemas/observable-default.plugin.json +7 -1
- package/lib/schemas/service-benchmarkify.json +1 -1
- package/lib/schemas/service-default0.json +1 -1
- package/lib/schemas/service-default1.json +1 -1
- package/lib/schemas/service-default2.json +1 -1
- package/lib/schemas/service-default3.json +1 -1
- package/lib/schemas/service-default4.json +1 -1
- package/lib/scripts/bsb-client-cli.js +6 -6
- package/lib/scripts/bsb-client-cli.js.map +1 -1
- package/lib/scripts/bsb-plugin-cli.js +561 -39
- package/lib/scripts/bsb-plugin-cli.js.map +1 -1
- package/lib/scripts/extract-schemas-from-source.js +55 -12
- package/lib/scripts/extract-schemas-from-source.js.map +1 -1
- package/lib/scripts/generate-plugin-json.js +5 -5
- package/lib/scripts/generate-plugin-json.js.map +1 -1
- package/package.json +106 -106
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
* bsb-plugin-cli clean - Remove build artifacts
|
|
14
14
|
*/
|
|
15
15
|
import { execSync, execFileSync, spawn } from 'node:child_process';
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
16
17
|
import * as fs from 'node:fs';
|
|
17
18
|
import * as path from 'node:path';
|
|
18
19
|
import { createRequire } from 'node:module';
|
|
20
|
+
import chokidar from 'chokidar';
|
|
19
21
|
import { getModuleDir, toImportUrl } from '../base/module-runtime.js';
|
|
20
22
|
// Colors for terminal output
|
|
21
23
|
const colors = {
|
|
@@ -44,6 +46,236 @@ function info(message) {
|
|
|
44
46
|
const CWD = process.cwd();
|
|
45
47
|
const COMMAND = process.argv[2];
|
|
46
48
|
const MODULE_DIR = getModuleDir(import.meta.url);
|
|
49
|
+
const BSB_DIR = path.join(CWD, 'src', '.bsb');
|
|
50
|
+
const CACHE_DIR = path.join(BSB_DIR, 'cache');
|
|
51
|
+
const BUILD_CACHE_PATH = path.join(CACHE_DIR, 'build-state.json');
|
|
52
|
+
const TSC_BUILD_INFO_PATH = path.join(CACHE_DIR, 'tsc.tsbuildinfo');
|
|
53
|
+
const DEV_WATCH_PATHS = [
|
|
54
|
+
path.join(CWD, 'package.json'),
|
|
55
|
+
path.join(CWD, 'sec-config.yaml'),
|
|
56
|
+
path.join(CWD, 'src'),
|
|
57
|
+
];
|
|
58
|
+
const DEV_IGNORE_PATHS = [
|
|
59
|
+
path.join(CWD, '.git'),
|
|
60
|
+
path.join(CWD, 'lib'),
|
|
61
|
+
path.join(CWD, 'node_modules'),
|
|
62
|
+
path.join(CWD, 'src', '.bsb'),
|
|
63
|
+
];
|
|
64
|
+
const SCHEMA_FILE_BASENAMES = new Set([
|
|
65
|
+
'index.ts',
|
|
66
|
+
'index.tsx',
|
|
67
|
+
'config.ts',
|
|
68
|
+
'config.tsx',
|
|
69
|
+
'events.ts',
|
|
70
|
+
'events.tsx',
|
|
71
|
+
'schema.ts',
|
|
72
|
+
'schema.tsx',
|
|
73
|
+
'schemas.ts',
|
|
74
|
+
'schemas.tsx',
|
|
75
|
+
'types.ts',
|
|
76
|
+
'types.tsx',
|
|
77
|
+
]);
|
|
78
|
+
function readPackageJson() {
|
|
79
|
+
const packageJsonPath = path.join(CWD, 'package.json');
|
|
80
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
81
|
+
}
|
|
82
|
+
function ensureDir(dirPath) {
|
|
83
|
+
if (!fs.existsSync(dirPath)) {
|
|
84
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function normalizePath(inputPath) {
|
|
88
|
+
return inputPath.replace(/\\/g, '/');
|
|
89
|
+
}
|
|
90
|
+
function normalizeChangedPath(filePath) {
|
|
91
|
+
const resolvedPath = path.isAbsolute(filePath) ? path.relative(CWD, filePath) : filePath;
|
|
92
|
+
return normalizePath(resolvedPath);
|
|
93
|
+
}
|
|
94
|
+
function hashStrings(values) {
|
|
95
|
+
const hash = createHash('sha256');
|
|
96
|
+
for (const value of values.sort()) {
|
|
97
|
+
hash.update(value);
|
|
98
|
+
hash.update('\n');
|
|
99
|
+
}
|
|
100
|
+
return hash.digest('hex');
|
|
101
|
+
}
|
|
102
|
+
function hashFile(filePath) {
|
|
103
|
+
const hash = createHash('sha256');
|
|
104
|
+
hash.update(fs.readFileSync(filePath));
|
|
105
|
+
return hash.digest('hex');
|
|
106
|
+
}
|
|
107
|
+
function collectFilesRecursive(dirPath, predicate) {
|
|
108
|
+
if (!fs.existsSync(dirPath)) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
const files = [];
|
|
112
|
+
const stack = [dirPath];
|
|
113
|
+
while (stack.length > 0) {
|
|
114
|
+
const current = stack.pop();
|
|
115
|
+
const entries = fs.readdirSync(current, { withFileTypes: true });
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const fullPath = path.join(current, entry.name);
|
|
118
|
+
if (entry.isDirectory()) {
|
|
119
|
+
stack.push(fullPath);
|
|
120
|
+
}
|
|
121
|
+
else if (entry.isFile() && predicate(fullPath)) {
|
|
122
|
+
files.push(fullPath);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return files;
|
|
127
|
+
}
|
|
128
|
+
function readBuildCache() {
|
|
129
|
+
try {
|
|
130
|
+
if (!fs.existsSync(BUILD_CACHE_PATH)) {
|
|
131
|
+
return { version: 1 };
|
|
132
|
+
}
|
|
133
|
+
const parsed = JSON.parse(fs.readFileSync(BUILD_CACHE_PATH, 'utf-8'));
|
|
134
|
+
if (parsed.version === 1) {
|
|
135
|
+
return parsed;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Ignore malformed cache files.
|
|
140
|
+
}
|
|
141
|
+
return { version: 1 };
|
|
142
|
+
}
|
|
143
|
+
function writeBuildCache(cache) {
|
|
144
|
+
ensureDir(CACHE_DIR);
|
|
145
|
+
fs.writeFileSync(BUILD_CACHE_PATH, JSON.stringify(cache, null, 2), 'utf-8');
|
|
146
|
+
}
|
|
147
|
+
function getCoreSchemasHash() {
|
|
148
|
+
const bsbBasePath = resolveBsbBasePath();
|
|
149
|
+
if (!bsbBasePath) {
|
|
150
|
+
return '';
|
|
151
|
+
}
|
|
152
|
+
const packageJsonPath = path.join(bsbBasePath, 'package.json');
|
|
153
|
+
const inputs = [fs.existsSync(packageJsonPath) ? `pkg:${hashFile(packageJsonPath)}` : 'pkg:missing'];
|
|
154
|
+
const sourcePluginsDir = path.join(bsbBasePath, 'src', 'plugins');
|
|
155
|
+
if (fs.existsSync(sourcePluginsDir)) {
|
|
156
|
+
inputs.push(...collectFilesRecursive(sourcePluginsDir, (filePath) => {
|
|
157
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
158
|
+
if (ext !== '.ts' && ext !== '.tsx') {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
return SCHEMA_FILE_BASENAMES.has(path.basename(filePath));
|
|
162
|
+
}).map((filePath) => `${normalizePath(path.relative(bsbBasePath, filePath))}:${hashFile(filePath)}`));
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
const schemaDir = path.join(bsbBasePath, 'lib', 'schemas');
|
|
166
|
+
inputs.push(...collectFilesRecursive(schemaDir, (filePath) => filePath.endsWith('.json'))
|
|
167
|
+
.map((filePath) => `${normalizePath(path.relative(bsbBasePath, filePath))}:${hashFile(filePath)}`));
|
|
168
|
+
}
|
|
169
|
+
return hashStrings(inputs);
|
|
170
|
+
}
|
|
171
|
+
function getSchemaInputsHash(plugins, coreSchemasHash) {
|
|
172
|
+
const inputs = [`core:${coreSchemasHash}`];
|
|
173
|
+
const packageJsonPath = path.join(CWD, 'package.json');
|
|
174
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
175
|
+
inputs.push(`pkg:${hashFile(packageJsonPath)}`);
|
|
176
|
+
}
|
|
177
|
+
for (const plugin of plugins) {
|
|
178
|
+
const pluginFiles = collectFilesRecursive(plugin.srcDir, (filePath) => {
|
|
179
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
180
|
+
if (ext !== '.ts' && ext !== '.tsx') {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
return SCHEMA_FILE_BASENAMES.has(path.basename(filePath));
|
|
184
|
+
});
|
|
185
|
+
for (const filePath of pluginFiles) {
|
|
186
|
+
inputs.push(`${normalizePath(path.relative(CWD, filePath))}:${hashFile(filePath)}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return hashStrings(inputs);
|
|
190
|
+
}
|
|
191
|
+
function getGeneratedSchemasHash() {
|
|
192
|
+
const schemaDir = path.join(CWD, 'src', '.bsb', 'schemas');
|
|
193
|
+
const schemaFiles = collectFilesRecursive(schemaDir, (filePath) => filePath.endsWith('.json'));
|
|
194
|
+
if (schemaFiles.length === 0) {
|
|
195
|
+
return '';
|
|
196
|
+
}
|
|
197
|
+
return hashStrings(schemaFiles.map((filePath) => `${normalizePath(path.relative(CWD, filePath))}:${hashFile(filePath)}`));
|
|
198
|
+
}
|
|
199
|
+
function hasGeneratedSchemas() {
|
|
200
|
+
const schemaDir = path.join(CWD, 'src', '.bsb', 'schemas');
|
|
201
|
+
return fs.existsSync(schemaDir) && fs.readdirSync(schemaDir).some((file) => file.endsWith('.json'));
|
|
202
|
+
}
|
|
203
|
+
function hasGeneratedClients() {
|
|
204
|
+
const clientsDir = path.join(CWD, 'src', '.bsb', 'clients');
|
|
205
|
+
return fs.existsSync(clientsDir) && fs.readdirSync(clientsDir).some((file) => file.endsWith('.ts'));
|
|
206
|
+
}
|
|
207
|
+
function isTsSourcePath(filePath) {
|
|
208
|
+
const normalized = normalizeChangedPath(filePath);
|
|
209
|
+
return normalized.startsWith('src/') && (normalized.endsWith('.ts') || normalized.endsWith('.tsx'));
|
|
210
|
+
}
|
|
211
|
+
function isSchemaRelevantPath(filePath) {
|
|
212
|
+
const normalized = normalizeChangedPath(filePath);
|
|
213
|
+
if (normalized === 'package.json') {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
if (!isTsSourcePath(normalized)) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return SCHEMA_FILE_BASENAMES.has(path.basename(normalized));
|
|
220
|
+
}
|
|
221
|
+
function isAssetPath(filePath) {
|
|
222
|
+
const normalized = normalizeChangedPath(filePath);
|
|
223
|
+
if (!normalized.startsWith('src/plugins/')) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
const ext = path.extname(normalized).toLowerCase();
|
|
227
|
+
return ext !== '.ts' && ext !== '.tsx';
|
|
228
|
+
}
|
|
229
|
+
function isStaticAssetPath(filePath) {
|
|
230
|
+
return normalizeChangedPath(filePath).includes('/static/');
|
|
231
|
+
}
|
|
232
|
+
function isConfigPath(filePath) {
|
|
233
|
+
const normalized = normalizeChangedPath(filePath);
|
|
234
|
+
return normalized === 'sec-config.yaml' || normalized === 'package.json';
|
|
235
|
+
}
|
|
236
|
+
function copySingleAssetFile(changedPath) {
|
|
237
|
+
const normalized = normalizeChangedPath(changedPath);
|
|
238
|
+
if (!isAssetPath(normalized)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
const absoluteSource = path.join(CWD, normalized);
|
|
242
|
+
const relativeToSrc = path.relative(path.join(CWD, 'src'), absoluteSource);
|
|
243
|
+
const destination = path.join(CWD, 'lib', relativeToSrc);
|
|
244
|
+
if (fs.existsSync(absoluteSource)) {
|
|
245
|
+
ensureDir(path.dirname(destination));
|
|
246
|
+
fs.copyFileSync(absoluteSource, destination);
|
|
247
|
+
}
|
|
248
|
+
else if (fs.existsSync(destination)) {
|
|
249
|
+
fs.rmSync(destination, { force: true });
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
function hasLibSchemas() {
|
|
254
|
+
const libSchemas = path.join(CWD, 'lib', 'schemas');
|
|
255
|
+
return fs.existsSync(libSchemas) && fs.readdirSync(libSchemas).some((file) => file.endsWith('.json'));
|
|
256
|
+
}
|
|
257
|
+
function hasPluginMetadataOutputs(plugins) {
|
|
258
|
+
const schemasDir = path.join(CWD, 'lib', 'schemas');
|
|
259
|
+
return plugins.every((plugin) => fs.existsSync(path.join(schemasDir, `${plugin.name}.plugin.json`)));
|
|
260
|
+
}
|
|
261
|
+
function getMetadataInputsHash(plugins) {
|
|
262
|
+
const packageJsonPath = path.join(CWD, 'package.json');
|
|
263
|
+
const inputs = [];
|
|
264
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
265
|
+
inputs.push(`pkg:${hashFile(packageJsonPath)}`);
|
|
266
|
+
}
|
|
267
|
+
for (const plugin of plugins) {
|
|
268
|
+
const indexPath = path.join(plugin.srcDir, 'index.ts');
|
|
269
|
+
if (fs.existsSync(indexPath)) {
|
|
270
|
+
inputs.push(`${normalizePath(path.relative(CWD, indexPath))}:${hashFile(indexPath)}`);
|
|
271
|
+
}
|
|
272
|
+
const schemaPath = path.join(CWD, 'src', '.bsb', 'schemas', `${plugin.name}.json`);
|
|
273
|
+
if (fs.existsSync(schemaPath)) {
|
|
274
|
+
inputs.push(`${normalizePath(path.relative(CWD, schemaPath))}:${hashFile(schemaPath)}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return hashStrings(inputs);
|
|
278
|
+
}
|
|
47
279
|
// Resolve @bsb/base package root using Node module resolution.
|
|
48
280
|
// Handles npm workspaces (hoisted node_modules), symlinks, and pnpm.
|
|
49
281
|
function resolveBsbBasePath() {
|
|
@@ -222,6 +454,7 @@ async function generatePluginJson(plugin) {
|
|
|
222
454
|
}
|
|
223
455
|
info(`Generating plugin metadata for ${plugin.name}`);
|
|
224
456
|
const metadata = pluginModule.Config.metadata;
|
|
457
|
+
const packageJson = readPackageJson();
|
|
225
458
|
// Auto-detect category from plugin directory name
|
|
226
459
|
const category = plugin.name.startsWith('service-') ? 'service' :
|
|
227
460
|
plugin.name.startsWith('observable-') ? 'observable' :
|
|
@@ -231,7 +464,7 @@ async function generatePluginJson(plugin) {
|
|
|
231
464
|
const pluginMetadata = {
|
|
232
465
|
id: plugin.name,
|
|
233
466
|
name: metadata.name,
|
|
234
|
-
version:
|
|
467
|
+
version: packageJson.version || '1.0.0',
|
|
235
468
|
description: metadata.description || '',
|
|
236
469
|
category,
|
|
237
470
|
tags: metadata.tags || [],
|
|
@@ -239,10 +472,10 @@ async function generatePluginJson(plugin) {
|
|
|
239
472
|
dependencies: [],
|
|
240
473
|
};
|
|
241
474
|
// Only include optional fields if they have real values
|
|
242
|
-
if (
|
|
243
|
-
pluginMetadata.author =
|
|
244
|
-
if (
|
|
245
|
-
pluginMetadata.license =
|
|
475
|
+
if (packageJson.author)
|
|
476
|
+
pluginMetadata.author = packageJson.author;
|
|
477
|
+
if (packageJson.license)
|
|
478
|
+
pluginMetadata.license = packageJson.license;
|
|
246
479
|
if (metadata.homepage)
|
|
247
480
|
pluginMetadata.homepage = metadata.homepage;
|
|
248
481
|
if (metadata.repository)
|
|
@@ -443,51 +676,183 @@ function generateRootTestsJson(plugins) {
|
|
|
443
676
|
fs.writeFileSync(testsPath, JSON.stringify(updated, null, 2), 'utf-8');
|
|
444
677
|
success(`Generated bsb-tests.json with ${plugins.length} plugin(s)`);
|
|
445
678
|
}
|
|
679
|
+
function shouldRunSchemaPipeline(changedPaths) {
|
|
680
|
+
if (!changedPaths || changedPaths.length === 0) {
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
return changedPaths.some(isSchemaRelevantPath);
|
|
684
|
+
}
|
|
685
|
+
function getBsbCliPath() {
|
|
686
|
+
const bsbBase = resolveBsbBasePath();
|
|
687
|
+
if (!bsbBase) {
|
|
688
|
+
error('BSB CLI not found. Make sure @bsb/base is installed.');
|
|
689
|
+
}
|
|
690
|
+
const bsbCliPath = path.join(bsbBase, 'lib', 'cli.js');
|
|
691
|
+
if (!fs.existsSync(bsbCliPath)) {
|
|
692
|
+
error(`BSB CLI entry not found at ${bsbCliPath}. @bsb/base may need rebuilding.`);
|
|
693
|
+
}
|
|
694
|
+
return bsbCliPath;
|
|
695
|
+
}
|
|
696
|
+
function getBsbDevPath() {
|
|
697
|
+
const bsbBase = resolveBsbBasePath();
|
|
698
|
+
if (!bsbBase) {
|
|
699
|
+
error('BSB dev entry not found. Make sure @bsb/base is installed.');
|
|
700
|
+
}
|
|
701
|
+
const sourceDevPath = path.join(bsbBase, 'src', 'dev.ts');
|
|
702
|
+
if (fs.existsSync(sourceDevPath)) {
|
|
703
|
+
return sourceDevPath;
|
|
704
|
+
}
|
|
705
|
+
const compiledDevPath = path.join(bsbBase, 'lib', 'dev.js');
|
|
706
|
+
if (fs.existsSync(compiledDevPath)) {
|
|
707
|
+
return compiledDevPath;
|
|
708
|
+
}
|
|
709
|
+
error(`BSB dev entry not found in ${bsbBase}`);
|
|
710
|
+
}
|
|
711
|
+
function typecheckDev() {
|
|
712
|
+
try {
|
|
713
|
+
info('Type checking TypeScript');
|
|
714
|
+
execSync(`npx tsc --noEmit --incremental --tsBuildInfoFile "${normalizePath(TSC_BUILD_INFO_PATH)}"`, { cwd: CWD, stdio: 'inherit' });
|
|
715
|
+
success('Type checking TypeScript');
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async function prepareGeneratedArtifacts(options = {}) {
|
|
723
|
+
ensureDir(CACHE_DIR);
|
|
724
|
+
const plugins = detectPluginStructure();
|
|
725
|
+
const cache = readBuildCache();
|
|
726
|
+
const changedPaths = options.changedPaths ?? [];
|
|
727
|
+
let coreSchemasHash = getCoreSchemasHash();
|
|
728
|
+
let schemaInputsHash = getSchemaInputsHash(plugins, coreSchemasHash);
|
|
729
|
+
const runSchemaPipeline = shouldRunSchemaPipeline(options.changedPaths);
|
|
730
|
+
const metadataInputsHash = getMetadataInputsHash(plugins);
|
|
731
|
+
const coreSchemasChanged = cache.coreSchemasHash !== coreSchemasHash;
|
|
732
|
+
const shouldSyncCoreSchemas = coreSchemasChanged || !hasGeneratedClients() || !hasGeneratedSchemas();
|
|
733
|
+
if (shouldSyncCoreSchemas) {
|
|
734
|
+
syncParentSchemas();
|
|
735
|
+
coreSchemasHash = getCoreSchemasHash();
|
|
736
|
+
schemaInputsHash = getSchemaInputsHash(plugins, coreSchemasHash);
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
info('Skipping core schema sync (unchanged)');
|
|
740
|
+
}
|
|
741
|
+
const shouldExtractSchemas = runSchemaPipeline && (shouldSyncCoreSchemas ||
|
|
742
|
+
cache.schemaInputsHash !== schemaInputsHash ||
|
|
743
|
+
!hasGeneratedSchemas());
|
|
744
|
+
if (shouldExtractSchemas) {
|
|
745
|
+
await extractSchemasFromSource();
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
info('Skipping schema extraction (unchanged)');
|
|
749
|
+
}
|
|
750
|
+
const generatedSchemasHash = getGeneratedSchemasHash();
|
|
751
|
+
const shouldGenerateClients = runSchemaPipeline && (shouldExtractSchemas ||
|
|
752
|
+
cache.clientInputsHash !== generatedSchemasHash ||
|
|
753
|
+
!hasGeneratedClients());
|
|
754
|
+
if (shouldGenerateClients) {
|
|
755
|
+
generateVirtualClients();
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
info('Skipping virtual client generation (unchanged)');
|
|
759
|
+
}
|
|
760
|
+
writeBuildCache({
|
|
761
|
+
version: 1,
|
|
762
|
+
coreSchemasHash,
|
|
763
|
+
schemaInputsHash,
|
|
764
|
+
clientInputsHash: getGeneratedSchemasHash(),
|
|
765
|
+
metadataInputsHash: changedPaths.length > 0 ? cache.metadataInputsHash : metadataInputsHash,
|
|
766
|
+
});
|
|
767
|
+
return {
|
|
768
|
+
plugins,
|
|
769
|
+
coreSchemasHash,
|
|
770
|
+
schemaInputsHash,
|
|
771
|
+
generatedSchemasHash: getGeneratedSchemasHash(),
|
|
772
|
+
};
|
|
773
|
+
}
|
|
446
774
|
// Build the plugin
|
|
447
|
-
async function build() {
|
|
775
|
+
async function build(options = {}) {
|
|
448
776
|
log('\n=== Building BSB Plugin ===\n', 'bright');
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
// These will be compiled alongside the project in step 5
|
|
457
|
-
generateVirtualClients();
|
|
777
|
+
const cache = readBuildCache();
|
|
778
|
+
const changedPaths = options.changedPaths ?? [];
|
|
779
|
+
const { plugins, coreSchemasHash, schemaInputsHash } = await prepareGeneratedArtifacts(options);
|
|
780
|
+
const runSchemaPipeline = shouldRunSchemaPipeline(options.changedPaths);
|
|
781
|
+
const metadataInputsHash = getMetadataInputsHash(plugins);
|
|
782
|
+
const packageChanged = changedPaths.some((filePath) => normalizeChangedPath(filePath) === 'package.json');
|
|
783
|
+
const assetChanged = changedPaths.some(isAssetPath);
|
|
458
784
|
// Step 4: Clean
|
|
459
|
-
clean
|
|
785
|
+
if (options.clean !== false) {
|
|
786
|
+
clean();
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
info('Skipping clean for incremental rebuild');
|
|
790
|
+
}
|
|
460
791
|
// Step 5: Compile TypeScript (virtual clients in src/.bsb/clients/ compile with the project)
|
|
461
|
-
|
|
792
|
+
if (options.incremental) {
|
|
793
|
+
exec(`npx tsc --incremental --tsBuildInfoFile "${normalizePath(TSC_BUILD_INFO_PATH)}"`, 'Compiling TypeScript incrementally');
|
|
794
|
+
}
|
|
795
|
+
else {
|
|
796
|
+
exec('npx tsc', 'Compiling TypeScript');
|
|
797
|
+
}
|
|
462
798
|
// Step 6: Copy non-TypeScript assets for each plugin
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
799
|
+
const shouldCopyAllAssets = !options.changedPaths || changedPaths.length === 0 || assetChanged || !fs.existsSync(path.join(CWD, 'lib'));
|
|
800
|
+
if (shouldCopyAllAssets) {
|
|
801
|
+
for (const plugin of plugins) {
|
|
802
|
+
copyPluginAssets(plugin);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
else {
|
|
806
|
+
info('Skipping asset copy (unchanged)');
|
|
466
807
|
}
|
|
467
808
|
// Step 7: Copy extracted schemas to lib/schemas/
|
|
468
|
-
|
|
809
|
+
const shouldCopySchemas = runSchemaPipeline || !hasLibSchemas();
|
|
810
|
+
if (shouldCopySchemas) {
|
|
811
|
+
copySchemasToLib();
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
info('Skipping schema copy (unchanged)');
|
|
815
|
+
}
|
|
469
816
|
// Step 8: Generate per-plugin metadata JSON (needs compiled JS for Config.metadata)
|
|
470
|
-
|
|
471
|
-
|
|
817
|
+
const shouldGenerateMetadata = runSchemaPipeline ||
|
|
818
|
+
packageChanged ||
|
|
819
|
+
cache.metadataInputsHash !== metadataInputsHash ||
|
|
820
|
+
!hasPluginMetadataOutputs(plugins);
|
|
821
|
+
if (shouldGenerateMetadata) {
|
|
822
|
+
for (const plugin of plugins) {
|
|
823
|
+
await generatePluginJson(plugin);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
info('Skipping plugin metadata generation (unchanged)');
|
|
472
828
|
}
|
|
473
829
|
// Step 9: Generate root bsb-plugin.json (aggregates all per-plugin metadata)
|
|
474
|
-
|
|
830
|
+
if (shouldGenerateMetadata || !fs.existsSync(path.join(CWD, 'bsb-plugin.json'))) {
|
|
831
|
+
generateRootPluginJson();
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
info('Skipping bsb-plugin.json generation (unchanged)');
|
|
835
|
+
}
|
|
475
836
|
// Step 10: Generate root bsb-tests.json (default ignore entries)
|
|
476
|
-
|
|
837
|
+
if (shouldGenerateMetadata || !fs.existsSync(path.join(CWD, 'bsb-tests.json'))) {
|
|
838
|
+
generateRootTestsJson(plugins.map(p => ({ id: p.name })));
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
info('Skipping bsb-tests.json generation (unchanged)');
|
|
842
|
+
}
|
|
843
|
+
writeBuildCache({
|
|
844
|
+
version: 1,
|
|
845
|
+
coreSchemasHash,
|
|
846
|
+
schemaInputsHash,
|
|
847
|
+
clientInputsHash: getGeneratedSchemasHash(),
|
|
848
|
+
metadataInputsHash: getMetadataInputsHash(plugins),
|
|
849
|
+
});
|
|
477
850
|
log('\n' + colors.green + colors.bright + '[BUILD COMPLETE]' + colors.reset + '\n');
|
|
478
851
|
}
|
|
479
852
|
// Start the BSB service
|
|
480
853
|
function start() {
|
|
481
854
|
log('\n=== Starting BSB Service ===\n', 'bright');
|
|
482
|
-
|
|
483
|
-
const bsbBase = resolveBsbBasePath();
|
|
484
|
-
if (!bsbBase) {
|
|
485
|
-
error('BSB CLI not found. Make sure @bsb/base is installed.');
|
|
486
|
-
}
|
|
487
|
-
const bsbCliPath = path.join(bsbBase, 'lib', 'cli.js');
|
|
488
|
-
if (!fs.existsSync(bsbCliPath)) {
|
|
489
|
-
error(`BSB CLI entry not found at ${bsbCliPath}. @bsb/base may need rebuilding.`);
|
|
490
|
-
}
|
|
855
|
+
const bsbCliPath = getBsbCliPath();
|
|
491
856
|
info('Starting service');
|
|
492
857
|
// Spawn the process and inherit stdio for real-time output
|
|
493
858
|
const child = spawn('node', [bsbCliPath], {
|
|
@@ -507,13 +872,170 @@ function start() {
|
|
|
507
872
|
child.kill('SIGINT');
|
|
508
873
|
});
|
|
509
874
|
}
|
|
510
|
-
|
|
875
|
+
function startServiceProcess() {
|
|
876
|
+
const bsbDevPath = getBsbDevPath();
|
|
877
|
+
info('Starting service');
|
|
878
|
+
return spawn('node', ['--import', 'tsx', bsbDevPath], {
|
|
879
|
+
cwd: CWD,
|
|
880
|
+
stdio: 'inherit',
|
|
881
|
+
env: {
|
|
882
|
+
...process.env,
|
|
883
|
+
APP_DIR: CWD,
|
|
884
|
+
BSB_DEV_EXTERNAL_WATCH: '1',
|
|
885
|
+
BSB_DEV_LOADER: 'tsx',
|
|
886
|
+
},
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
async function stopServiceProcess(child) {
|
|
890
|
+
if (!child || child.killed || child.exitCode !== null) {
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
await new Promise((resolve) => {
|
|
894
|
+
child.once('exit', () => resolve());
|
|
895
|
+
child.kill('SIGINT');
|
|
896
|
+
setTimeout(() => {
|
|
897
|
+
if (child.exitCode === null) {
|
|
898
|
+
child.kill('SIGTERM');
|
|
899
|
+
}
|
|
900
|
+
}, 2000);
|
|
901
|
+
setTimeout(() => {
|
|
902
|
+
if (child.exitCode === null) {
|
|
903
|
+
child.kill('SIGKILL');
|
|
904
|
+
}
|
|
905
|
+
}, 4000);
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
// Development mode with incremental rebuilds and restarts
|
|
511
909
|
async function dev() {
|
|
512
910
|
log('\n=== Development Mode ===\n', 'bright');
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
911
|
+
let child = null;
|
|
912
|
+
let watcher = null;
|
|
913
|
+
let isRebuilding = false;
|
|
914
|
+
let restartPending = false;
|
|
915
|
+
const pendingChanges = new Set();
|
|
916
|
+
let debounceTimer = null;
|
|
917
|
+
const rebuildAndRestart = async () => {
|
|
918
|
+
if (isRebuilding) {
|
|
919
|
+
restartPending = true;
|
|
920
|
+
info('Rebuild already in progress, queueing another pass');
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
isRebuilding = true;
|
|
924
|
+
const changedPaths = Array.from(pendingChanges);
|
|
925
|
+
pendingChanges.clear();
|
|
926
|
+
try {
|
|
927
|
+
if (changedPaths.length > 0) {
|
|
928
|
+
info(`Detected changes: ${changedPaths.join(', ')}`);
|
|
929
|
+
}
|
|
930
|
+
const configOnly = changedPaths.length > 0 &&
|
|
931
|
+
changedPaths.every((filePath) => isConfigPath(filePath));
|
|
932
|
+
const assetOnly = changedPaths.length > 0 &&
|
|
933
|
+
changedPaths.every((filePath) => isAssetPath(filePath) || isConfigPath(filePath));
|
|
934
|
+
const staticAssetOnly = changedPaths.length > 0 &&
|
|
935
|
+
changedPaths.every((filePath) => isStaticAssetPath(filePath) || isConfigPath(filePath));
|
|
936
|
+
const sourceCodeChanged = changedPaths.some(isTsSourcePath);
|
|
937
|
+
const copiedAnyAsset = assetOnly
|
|
938
|
+
? changedPaths.map(copySingleAssetFile).some(Boolean)
|
|
939
|
+
: false;
|
|
940
|
+
if (configOnly) {
|
|
941
|
+
info('Skipping rebuild (config-only change)');
|
|
942
|
+
}
|
|
943
|
+
else if (!assetOnly || !copiedAnyAsset) {
|
|
944
|
+
info('Preparing generated artifacts for dev');
|
|
945
|
+
await prepareGeneratedArtifacts({ changedPaths });
|
|
946
|
+
if (sourceCodeChanged) {
|
|
947
|
+
const typecheckOk = typecheckDev();
|
|
948
|
+
if (!typecheckOk) {
|
|
949
|
+
info('Type check failed, keeping current service instance');
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
info('Skipping TypeScript rebuild (asset/config-only change)');
|
|
956
|
+
}
|
|
957
|
+
if (staticAssetOnly && copiedAnyAsset && !configOnly) {
|
|
958
|
+
info('Skipping restart (static asset-only change)');
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
info('Restarting service');
|
|
962
|
+
await stopServiceProcess(child);
|
|
963
|
+
child = startServiceProcess();
|
|
964
|
+
}
|
|
965
|
+
catch (err) {
|
|
966
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
967
|
+
log(`Dev rebuild failed: ${message}`, 'red');
|
|
968
|
+
}
|
|
969
|
+
finally {
|
|
970
|
+
isRebuilding = false;
|
|
971
|
+
if (restartPending) {
|
|
972
|
+
restartPending = false;
|
|
973
|
+
void rebuildAndRestart();
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
const queueChange = (filePath) => {
|
|
978
|
+
const normalizedPath = normalizeChangedPath(filePath);
|
|
979
|
+
info(`Change detected: ${normalizedPath}`);
|
|
980
|
+
pendingChanges.add(normalizedPath);
|
|
981
|
+
if (debounceTimer) {
|
|
982
|
+
clearTimeout(debounceTimer);
|
|
983
|
+
}
|
|
984
|
+
debounceTimer = setTimeout(() => {
|
|
985
|
+
void rebuildAndRestart();
|
|
986
|
+
}, 200);
|
|
987
|
+
};
|
|
988
|
+
try {
|
|
989
|
+
info(`Watching paths: ${DEV_WATCH_PATHS.map(normalizePath).join(', ')}`);
|
|
990
|
+
watcher = chokidar.watch(DEV_WATCH_PATHS, {
|
|
991
|
+
ignored: (watchPath) => {
|
|
992
|
+
const normalized = normalizeChangedPath(String(watchPath));
|
|
993
|
+
return DEV_IGNORE_PATHS.some((ignorePath) => {
|
|
994
|
+
const normalizedIgnorePath = normalizeChangedPath(ignorePath);
|
|
995
|
+
return normalized === normalizedIgnorePath || normalized.startsWith(`${normalizedIgnorePath}/`);
|
|
996
|
+
});
|
|
997
|
+
},
|
|
998
|
+
ignoreInitial: true,
|
|
999
|
+
persistent: true,
|
|
1000
|
+
});
|
|
1001
|
+
watcher.on('add', queueChange);
|
|
1002
|
+
watcher.on('change', queueChange);
|
|
1003
|
+
watcher.on('unlink', queueChange);
|
|
1004
|
+
await new Promise((resolve) => {
|
|
1005
|
+
watcher.once('ready', () => {
|
|
1006
|
+
info('Watching for changes');
|
|
1007
|
+
resolve();
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
info('Preparing generated artifacts for dev');
|
|
1011
|
+
await prepareGeneratedArtifacts();
|
|
1012
|
+
if (typecheckDev()) {
|
|
1013
|
+
child = startServiceProcess();
|
|
1014
|
+
}
|
|
1015
|
+
else {
|
|
1016
|
+
info('Initial type check failed, waiting for changes');
|
|
1017
|
+
}
|
|
1018
|
+
process.on('SIGINT', async () => {
|
|
1019
|
+
if (debounceTimer) {
|
|
1020
|
+
clearTimeout(debounceTimer);
|
|
1021
|
+
}
|
|
1022
|
+
if (watcher) {
|
|
1023
|
+
await watcher.close();
|
|
1024
|
+
}
|
|
1025
|
+
await stopServiceProcess(child);
|
|
1026
|
+
process.exit(0);
|
|
1027
|
+
});
|
|
1028
|
+
await new Promise(() => { });
|
|
1029
|
+
}
|
|
1030
|
+
finally {
|
|
1031
|
+
if (debounceTimer) {
|
|
1032
|
+
clearTimeout(debounceTimer);
|
|
1033
|
+
}
|
|
1034
|
+
if (watcher) {
|
|
1035
|
+
await watcher.close();
|
|
1036
|
+
}
|
|
1037
|
+
await stopServiceProcess(child);
|
|
1038
|
+
}
|
|
517
1039
|
}
|
|
518
1040
|
// Run tests
|
|
519
1041
|
function test() {
|