@objectstack/cli 5.0.0 → 5.2.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/commands/package/publish.d.ts +7 -0
- package/dist/commands/package/publish.d.ts.map +1 -1
- package/dist/commands/package/publish.js +103 -0
- package/dist/commands/package/publish.js.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +170 -13
- package/dist/commands/serve.js.map +1 -1
- package/dist/utils/console.d.ts +10 -2
- package/dist/utils/console.d.ts.map +1 -1
- package/dist/utils/console.js +88 -34
- package/dist/utils/console.js.map +1 -1
- package/dist/utils/studio-field-patch.d.ts +43 -0
- package/dist/utils/studio-field-patch.d.ts.map +1 -0
- package/dist/utils/studio-field-patch.js +336 -0
- package/dist/utils/studio-field-patch.js.map +1 -0
- package/dist/utils/studio.d.ts +28 -0
- package/dist/utils/studio.d.ts.map +1 -1
- package/dist/utils/studio.js +225 -0
- package/dist/utils/studio.js.map +1 -1
- package/package.json +36 -32
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* Studio field-patch — ts-morph powered surgery on a single field
|
|
4
|
+
* inside an `ObjectSchema.create({...})` or `defineObject({...})` call.
|
|
5
|
+
*
|
|
6
|
+
* Supported field shapes:
|
|
7
|
+
* account_number: Field.autonumber({ ...props }) // CallExpression
|
|
8
|
+
* owner: Field.lookup('user', { ...props }) // CallExpression with second-arg object
|
|
9
|
+
* custom: { ...props } // bare ObjectLiteral
|
|
10
|
+
*
|
|
11
|
+
* Properties we know how to update: `label`, `description`, `required`.
|
|
12
|
+
* Falsy values (`null`, `''`, `false`) are interpreted as "remove this
|
|
13
|
+
* property" so the source stays minimal.
|
|
14
|
+
*/
|
|
15
|
+
import { Project, SyntaxKind, } from 'ts-morph';
|
|
16
|
+
/**
|
|
17
|
+
* Locate the field's inner object-literal and apply the patch.
|
|
18
|
+
* Writes the file in place when successful.
|
|
19
|
+
*/
|
|
20
|
+
export async function patchObjectFieldFile(absPath, fieldKey, patch) {
|
|
21
|
+
return withFieldsObj(absPath, async (fieldsObj) => {
|
|
22
|
+
const fieldProp = fieldsObj.getProperty(fieldKey);
|
|
23
|
+
if (!fieldProp || !fieldProp.isKind(SyntaxKind.PropertyAssignment)) {
|
|
24
|
+
return { ok: false, error: `field \`${fieldKey}\` not found in fields` };
|
|
25
|
+
}
|
|
26
|
+
const innerObj = resolveInnerObjectLiteral(fieldProp.getInitializer());
|
|
27
|
+
if (!innerObj) {
|
|
28
|
+
return { ok: false, error: `field \`${fieldKey}\` initializer has no editable object literal` };
|
|
29
|
+
}
|
|
30
|
+
if ('label' in patch)
|
|
31
|
+
applyStringProp(innerObj, 'label', patch.label);
|
|
32
|
+
if ('description' in patch)
|
|
33
|
+
applyStringProp(innerObj, 'description', patch.description);
|
|
34
|
+
if ('required' in patch)
|
|
35
|
+
applyBooleanProp(innerObj, 'required', patch.required);
|
|
36
|
+
return { ok: true };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Append a new field to the `fields: { ... }` object literal. The
|
|
41
|
+
* `initializer` is the raw TS source that goes on the right-hand side
|
|
42
|
+
* of `<fieldName>: …` — typically an object literal but may be any
|
|
43
|
+
* expression (e.g. `Field.text({ ... })`).
|
|
44
|
+
*
|
|
45
|
+
* Refuses to overwrite an existing field — callers must surface a
|
|
46
|
+
* conflict to the user.
|
|
47
|
+
*/
|
|
48
|
+
export async function addObjectField(absPath, fieldName, initializer) {
|
|
49
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(fieldName)) {
|
|
50
|
+
return { ok: false, error: `invalid field name \`${fieldName}\` (must be snake_case)` };
|
|
51
|
+
}
|
|
52
|
+
return withFieldsObj(absPath, async (fieldsObj) => {
|
|
53
|
+
if (fieldsObj.getProperty(fieldName)) {
|
|
54
|
+
return { ok: false, error: `field \`${fieldName}\` already exists` };
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
// Append by raw-text insertion so we match the file's existing
|
|
58
|
+
// indentation / line-break conventions instead of forcing ts-morph
|
|
59
|
+
// defaults (which produced 6-space indent and a flush-left closing
|
|
60
|
+
// brace inside a 4-space-indented body).
|
|
61
|
+
const openBrace = fieldsObj.getFirstChildByKind(SyntaxKind.OpenBraceToken);
|
|
62
|
+
const closeBrace = fieldsObj.getLastChildByKind(SyntaxKind.CloseBraceToken);
|
|
63
|
+
if (!openBrace || !closeBrace) {
|
|
64
|
+
return { ok: false, error: 'malformed object literal (missing braces)' };
|
|
65
|
+
}
|
|
66
|
+
const sf = fieldsObj.getSourceFile();
|
|
67
|
+
const fullText = sf.getFullText();
|
|
68
|
+
const bodyStart = openBrace.getEnd();
|
|
69
|
+
const bodyEnd = closeBrace.getStart();
|
|
70
|
+
// Detect existing indentation by sniffing the original source:
|
|
71
|
+
// propIndent — leading whitespace before the first existing prop
|
|
72
|
+
// closeIndent — leading whitespace before the existing close brace
|
|
73
|
+
// Both are read verbatim so the new prop and the close brace land at
|
|
74
|
+
// the exact columns the file already uses (handles 2-space, 4-space,
|
|
75
|
+
// tabs, or anything else without guessing the indent step).
|
|
76
|
+
const sniffIndent = (pos) => {
|
|
77
|
+
let i = pos - 1;
|
|
78
|
+
while (i >= 0 && (fullText[i] === ' ' || fullText[i] === '\t'))
|
|
79
|
+
i--;
|
|
80
|
+
return fullText.slice(i + 1, pos);
|
|
81
|
+
};
|
|
82
|
+
const firstProp = fieldsObj.getProperties()[0];
|
|
83
|
+
const closeIndent = sniffIndent(closeBrace.getStart());
|
|
84
|
+
let propIndent;
|
|
85
|
+
if (firstProp && firstProp.isKind(SyntaxKind.PropertyAssignment)) {
|
|
86
|
+
propIndent = sniffIndent(firstProp.getStart());
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Empty `{}` — give the new prop one step beyond the close brace.
|
|
90
|
+
// Default to two spaces; if the close-brace indent looks tab-based,
|
|
91
|
+
// use a tab.
|
|
92
|
+
propIndent = closeIndent + (closeIndent.includes('\t') ? '\t' : ' ');
|
|
93
|
+
}
|
|
94
|
+
// Body region between braces; rebuild with the new prop appended,
|
|
95
|
+
// ensuring trailing comma + newline + indent before the new prop and
|
|
96
|
+
// a newline + closing indent before the close brace.
|
|
97
|
+
const body = fullText.slice(bodyStart, bodyEnd);
|
|
98
|
+
let trimmed = body.replace(/\s+$/, '');
|
|
99
|
+
if (trimmed && !trimmed.endsWith(','))
|
|
100
|
+
trimmed += ',';
|
|
101
|
+
const isEmpty = trimmed.length === 0;
|
|
102
|
+
const newBody = isEmpty
|
|
103
|
+
? `\n${propIndent}${fieldName}: ${initializer},\n${closeIndent}`
|
|
104
|
+
: `${trimmed}\n${propIndent}${fieldName}: ${initializer},\n${closeIndent}`;
|
|
105
|
+
// Use raw SourceFile.replaceText to avoid ts-morph auto-reindenting
|
|
106
|
+
// every line inside the literal (replaceWithText on an ObjectLiteral
|
|
107
|
+
// re-indents by manipulation settings, shifting unchanged lines).
|
|
108
|
+
sf.replaceText([bodyStart, bodyEnd], newBody);
|
|
109
|
+
return { ok: true };
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
return { ok: false, error: `add failed: ${err?.message ?? String(err)}` };
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Re-order the properties inside the `fields: { ... }` object literal
|
|
118
|
+
* to match `order[]`. Field names absent from `order` are appended at
|
|
119
|
+
* the end in their existing relative order — keeps the operation safe
|
|
120
|
+
* if the UI has a stale snapshot.
|
|
121
|
+
*
|
|
122
|
+
* Implementation: slice each property's source range out of the file
|
|
123
|
+
* (including any leading whitespace / blank-line / comment trivia and
|
|
124
|
+
* the trailing `,`), then concatenate the slices in the new order. By
|
|
125
|
+
* working on the raw source, indentation, trailing commas, blank-line
|
|
126
|
+
* separators and inline comments are all preserved verbatim. The only
|
|
127
|
+
* cost: a hanging blank line that previously sat *between* two props
|
|
128
|
+
* follows whichever property it precedes after the reorder, which is
|
|
129
|
+
* the natural and least-surprising behaviour.
|
|
130
|
+
*/
|
|
131
|
+
export async function reorderObjectFields(absPath, order) {
|
|
132
|
+
if (!Array.isArray(order)) {
|
|
133
|
+
return { ok: false, error: 'order must be an array of field names' };
|
|
134
|
+
}
|
|
135
|
+
return withFieldsObj(absPath, async (fieldsObj) => {
|
|
136
|
+
const props = fieldsObj.getProperties();
|
|
137
|
+
const openBrace = fieldsObj.getFirstChildByKind(SyntaxKind.OpenBraceToken);
|
|
138
|
+
const closeBrace = fieldsObj.getLastChildByKind(SyntaxKind.CloseBraceToken);
|
|
139
|
+
if (!openBrace || !closeBrace) {
|
|
140
|
+
return { ok: false, error: 'malformed object literal (missing braces)' };
|
|
141
|
+
}
|
|
142
|
+
const sf = fieldsObj.getSourceFile();
|
|
143
|
+
const fullText = sf.getFullText();
|
|
144
|
+
const bodyStart = openBrace.getEnd();
|
|
145
|
+
const bodyEnd = closeBrace.getStart();
|
|
146
|
+
// For each property: capture the slice from "just after previous prop's
|
|
147
|
+
// trailing comma" (or bodyStart for the first prop) up to and including
|
|
148
|
+
// this prop's own trailing comma (skipping inline whitespace up to it).
|
|
149
|
+
// Leading whitespace / blank lines / comments that precede a prop stay
|
|
150
|
+
// with that prop, so they travel together when reordered.
|
|
151
|
+
const slices = new Map();
|
|
152
|
+
let cursor = bodyStart;
|
|
153
|
+
for (let i = 0; i < props.length; i++) {
|
|
154
|
+
const p = props[i];
|
|
155
|
+
if (!p.isKind(SyntaxKind.PropertyAssignment) && !p.isKind(SyntaxKind.ShorthandPropertyAssignment)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const name = p.getName();
|
|
159
|
+
// Find end-of-slice: skip whitespace after the prop, swallow one comma if present.
|
|
160
|
+
let end = p.getEnd();
|
|
161
|
+
while (end < bodyEnd && /[ \t]/.test(fullText[end]))
|
|
162
|
+
end++;
|
|
163
|
+
if (fullText[end] === ',')
|
|
164
|
+
end++;
|
|
165
|
+
slices.set(name, fullText.slice(cursor, end));
|
|
166
|
+
cursor = end;
|
|
167
|
+
}
|
|
168
|
+
// Anything between the last prop's comma and the close brace (typically
|
|
169
|
+
// a trailing newline + indentation) becomes the new "tail".
|
|
170
|
+
const tail = fullText.slice(cursor, bodyEnd);
|
|
171
|
+
if (slices.size === 0) {
|
|
172
|
+
return { ok: false, error: '`fields` is empty — nothing to reorder' };
|
|
173
|
+
}
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
const ordered = [];
|
|
176
|
+
for (const name of order) {
|
|
177
|
+
const slice = slices.get(name);
|
|
178
|
+
if (slice !== undefined) {
|
|
179
|
+
ordered.push(slice);
|
|
180
|
+
seen.add(name);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Append anything the UI didn't know about so we never drop fields.
|
|
184
|
+
for (const [name, slice] of slices) {
|
|
185
|
+
if (!seen.has(name))
|
|
186
|
+
ordered.push(slice);
|
|
187
|
+
}
|
|
188
|
+
const newBody = ordered.join('') + tail;
|
|
189
|
+
try {
|
|
190
|
+
// sf.replaceText avoids re-indenting unchanged lines, which is what
|
|
191
|
+
// makes fieldsObj.replaceWithText destroy formatting.
|
|
192
|
+
sf.replaceText([bodyStart, bodyEnd], newBody);
|
|
193
|
+
return { ok: true };
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
return { ok: false, error: `reorder failed: ${err?.message ?? String(err)}` };
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Shared entry: open the file, drill into the `fields` literal, run
|
|
202
|
+
* `mutate(...)`, persist on success. Centralizes the schema-call /
|
|
203
|
+
* fields-literal lookup that every operation needs.
|
|
204
|
+
*/
|
|
205
|
+
async function withFieldsObj(absPath, mutate) {
|
|
206
|
+
const project = new Project({ useInMemoryFileSystem: false, skipAddingFilesFromTsConfig: true });
|
|
207
|
+
let sf;
|
|
208
|
+
try {
|
|
209
|
+
sf = project.addSourceFileAtPath(absPath);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
return { ok: false, error: `parse failed: ${err?.message ?? String(err)}` };
|
|
213
|
+
}
|
|
214
|
+
const schemaCall = findSchemaCall(sf);
|
|
215
|
+
if (!schemaCall) {
|
|
216
|
+
return { ok: false, error: 'no ObjectSchema.create / defineObject call found in file' };
|
|
217
|
+
}
|
|
218
|
+
const schemaArg = schemaCall.getArguments()[0];
|
|
219
|
+
if (!schemaArg || !schemaArg.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
220
|
+
return { ok: false, error: 'schema call argument is not an object literal' };
|
|
221
|
+
}
|
|
222
|
+
const fieldsProp = schemaArg.getProperty('fields');
|
|
223
|
+
if (!fieldsProp || !fieldsProp.isKind(SyntaxKind.PropertyAssignment)) {
|
|
224
|
+
return { ok: false, error: 'schema object has no `fields` property' };
|
|
225
|
+
}
|
|
226
|
+
const fieldsInit = fieldsProp.getInitializer();
|
|
227
|
+
if (!fieldsInit || !fieldsInit.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
228
|
+
return { ok: false, error: '`fields` initializer is not an object literal' };
|
|
229
|
+
}
|
|
230
|
+
const result = await mutate(fieldsInit);
|
|
231
|
+
if (!result.ok)
|
|
232
|
+
return result;
|
|
233
|
+
try {
|
|
234
|
+
await sf.save();
|
|
235
|
+
return { ok: true };
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
return { ok: false, error: `write failed: ${err?.message ?? String(err)}` };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// ─── helpers ────────────────────────────────────────────────────────
|
|
242
|
+
function findSchemaCall(sf) {
|
|
243
|
+
const calls = sf.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
244
|
+
for (const call of calls) {
|
|
245
|
+
const expr = call.getExpression().getText();
|
|
246
|
+
if (expr === 'ObjectSchema.create' || expr === 'defineObject') {
|
|
247
|
+
return call;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Given a property initializer, return the inner ObjectLiteralExpression
|
|
254
|
+
* we should patch.
|
|
255
|
+
* - `{ ... }` → that literal
|
|
256
|
+
* - `Field.X({ ... })` → first arg if object literal
|
|
257
|
+
* - `Field.X('rel', { ... })` → second arg
|
|
258
|
+
* - `Field.X({ ... }, { ... })` → first arg (defensive)
|
|
259
|
+
*/
|
|
260
|
+
function resolveInnerObjectLiteral(init) {
|
|
261
|
+
if (!init)
|
|
262
|
+
return null;
|
|
263
|
+
if (init.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
264
|
+
return init;
|
|
265
|
+
}
|
|
266
|
+
if (init.isKind(SyntaxKind.CallExpression)) {
|
|
267
|
+
const args = init.getArguments();
|
|
268
|
+
for (const arg of args) {
|
|
269
|
+
if (arg.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
270
|
+
return arg;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
function applyStringProp(obj, key, value) {
|
|
277
|
+
const existing = obj.getProperty(key);
|
|
278
|
+
if (value == null || value === '') {
|
|
279
|
+
if (existing)
|
|
280
|
+
existing.remove();
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const literal = renderStringLiteral(obj, value);
|
|
284
|
+
if (existing && existing.isKind(SyntaxKind.PropertyAssignment)) {
|
|
285
|
+
existing.setInitializer(literal);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
obj.addPropertyAssignment({ name: key, initializer: literal });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Render `value` as a TS string literal, preferring whichever quote
|
|
293
|
+
* style (single vs double) the surrounding object already uses for
|
|
294
|
+
* existing string props. Falls back to single quotes — the dominant
|
|
295
|
+
* convention across the codebase — when there's no signal.
|
|
296
|
+
*/
|
|
297
|
+
function renderStringLiteral(obj, value) {
|
|
298
|
+
let single = 0;
|
|
299
|
+
let double = 0;
|
|
300
|
+
for (const p of obj.getProperties()) {
|
|
301
|
+
if (!p.isKind(SyntaxKind.PropertyAssignment))
|
|
302
|
+
continue;
|
|
303
|
+
const init = p.getInitializer();
|
|
304
|
+
if (!init || !init.isKind(SyntaxKind.StringLiteral))
|
|
305
|
+
continue;
|
|
306
|
+
const raw = init.getText();
|
|
307
|
+
if (raw.startsWith("'"))
|
|
308
|
+
single++;
|
|
309
|
+
else if (raw.startsWith('"'))
|
|
310
|
+
double++;
|
|
311
|
+
}
|
|
312
|
+
const useDouble = double > single;
|
|
313
|
+
if (useDouble || value.includes("'")) {
|
|
314
|
+
// JSON.stringify gives us proper escaping for `"` and control chars.
|
|
315
|
+
return JSON.stringify(value);
|
|
316
|
+
}
|
|
317
|
+
// Single-quoted: escape only single quotes and backslashes.
|
|
318
|
+
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
319
|
+
}
|
|
320
|
+
function applyBooleanProp(obj, key, value) {
|
|
321
|
+
const existing = obj.getProperty(key);
|
|
322
|
+
// Default for `required` is false, so we omit the property when false
|
|
323
|
+
// to keep the source minimal — matches what authors hand-write.
|
|
324
|
+
if (value !== true) {
|
|
325
|
+
if (existing)
|
|
326
|
+
existing.remove();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (existing && existing.isKind(SyntaxKind.PropertyAssignment)) {
|
|
330
|
+
existing.setInitializer('true');
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
obj.addPropertyAssignment({ name: key, initializer: 'true' });
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
//# sourceMappingURL=studio-field-patch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"studio-field-patch.js","sourceRoot":"","sources":["../../src/utils/studio-field-patch.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/studio.d.ts
CHANGED
|
@@ -56,4 +56,32 @@ export declare function createStudioStaticPlugin(distPath: string, options?: {
|
|
|
56
56
|
init: () => Promise<void>;
|
|
57
57
|
start: (ctx: any) => Promise<void>;
|
|
58
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Dev-only plugin that exposes a tiny write API at
|
|
61
|
+
* `/_studio/api/metadata/*` so Studio's "Create" dialogs can scaffold
|
|
62
|
+
* real `.ts` files instead of asking the user to paste a snippet.
|
|
63
|
+
*
|
|
64
|
+
* Security posture: enabled ONLY when `isDev === true`. All file paths
|
|
65
|
+
* must live under `<cwd>`, contain a `/src/` segment, and carry an
|
|
66
|
+
* approved extension. Path traversal (`..`) and absolute paths are
|
|
67
|
+
* rejected outright. Existing files are NEVER overwritten unless the
|
|
68
|
+
* caller passes `mode: 'overwrite'`.
|
|
69
|
+
*
|
|
70
|
+
* Endpoints:
|
|
71
|
+
* GET /_studio/api/metadata/layout?package=<id>
|
|
72
|
+
* 200: { srcRoot: string } relative to cwd, e.g. "src" or "packages/<id>/src"
|
|
73
|
+
*
|
|
74
|
+
* POST /_studio/api/metadata/file
|
|
75
|
+
* body: { path: string, content: string, mode?: 'create' | 'overwrite' }
|
|
76
|
+
* 200: { ok: true, path: string }
|
|
77
|
+
* 409: { ok: false, error: 'exists' }
|
|
78
|
+
* 400: { ok: false, error: ... }
|
|
79
|
+
*/
|
|
80
|
+
export declare function createStudioWriteApiPlugin(cwd: string, options?: {
|
|
81
|
+
isDev: boolean;
|
|
82
|
+
}): {
|
|
83
|
+
name: string;
|
|
84
|
+
init: () => Promise<void>;
|
|
85
|
+
start: (ctx: any) => Promise<void>;
|
|
86
|
+
};
|
|
59
87
|
//# sourceMappingURL=studio.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"studio.d.ts","sourceRoot":"","sources":["../../src/utils/studio.ts"],"names":[],"mappings":"AAaA,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAKzD,sEAAsE;AACtE,eAAO,MAAM,WAAW,aAAa,CAAC;AAOtC;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CA+CjD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEzD;AAID;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,GAAE,MAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAYlF;AAID,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,OAAO,EAAE,YAAY,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GACpC,OAAO,CAAC,aAAa,CAAC,CAwExB;AAID;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM;;;iBAMjC,GAAG;EAwCzB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE;;;iBAMzF,GAAG;EAsEzB"}
|
|
1
|
+
{"version":3,"file":"studio.d.ts","sourceRoot":"","sources":["../../src/utils/studio.ts"],"names":[],"mappings":"AAaA,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,eAAe,CAAC;AAKzD,sEAAsE;AACtE,eAAO,MAAM,WAAW,aAAa,CAAC;AAOtC;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,GAAG,IAAI,CA+CjD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAEzD;AAID;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,GAAE,MAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAYlF;AAID,MAAM,WAAW,aAAa;IAC5B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,OAAO,EAAE,YAAY,CAAC;CACvB;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GACpC,OAAO,CAAC,aAAa,CAAC,CAwExB;AAID;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM;;;iBAMjC,GAAG;EAwCzB;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAE;;;iBAMzF,GAAG;EAsEzB;AAID;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAqB;;;iBAM/E,GAAG;EAmMzB"}
|
package/dist/utils/studio.js
CHANGED
|
@@ -278,6 +278,231 @@ export function createStudioStaticPlugin(distPath, options) {
|
|
|
278
278
|
},
|
|
279
279
|
};
|
|
280
280
|
}
|
|
281
|
+
// ─── Dev-only Write API ─────────────────────────────────────────────
|
|
282
|
+
/**
|
|
283
|
+
* Dev-only plugin that exposes a tiny write API at
|
|
284
|
+
* `/_studio/api/metadata/*` so Studio's "Create" dialogs can scaffold
|
|
285
|
+
* real `.ts` files instead of asking the user to paste a snippet.
|
|
286
|
+
*
|
|
287
|
+
* Security posture: enabled ONLY when `isDev === true`. All file paths
|
|
288
|
+
* must live under `<cwd>`, contain a `/src/` segment, and carry an
|
|
289
|
+
* approved extension. Path traversal (`..`) and absolute paths are
|
|
290
|
+
* rejected outright. Existing files are NEVER overwritten unless the
|
|
291
|
+
* caller passes `mode: 'overwrite'`.
|
|
292
|
+
*
|
|
293
|
+
* Endpoints:
|
|
294
|
+
* GET /_studio/api/metadata/layout?package=<id>
|
|
295
|
+
* 200: { srcRoot: string } relative to cwd, e.g. "src" or "packages/<id>/src"
|
|
296
|
+
*
|
|
297
|
+
* POST /_studio/api/metadata/file
|
|
298
|
+
* body: { path: string, content: string, mode?: 'create' | 'overwrite' }
|
|
299
|
+
* 200: { ok: true, path: string }
|
|
300
|
+
* 409: { ok: false, error: 'exists' }
|
|
301
|
+
* 400: { ok: false, error: ... }
|
|
302
|
+
*/
|
|
303
|
+
export function createStudioWriteApiPlugin(cwd, options = { isDev: false }) {
|
|
304
|
+
return {
|
|
305
|
+
name: 'com.objectstack.studio-write-api',
|
|
306
|
+
init: async () => { },
|
|
307
|
+
start: async (ctx) => {
|
|
308
|
+
if (!options.isDev)
|
|
309
|
+
return;
|
|
310
|
+
const httpServer = ctx.getService?.('http.server');
|
|
311
|
+
if (!httpServer?.getRawApp) {
|
|
312
|
+
ctx.logger?.warn?.('Studio write API: http.server not found — skipping');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const app = httpServer.getRawApp();
|
|
316
|
+
const projectRoot = path.resolve(cwd);
|
|
317
|
+
const ALLOWED_EXT = new Set(['.ts', '.tsx', '.json']);
|
|
318
|
+
const respond = (_c, status, body) => new Response(JSON.stringify(body), {
|
|
319
|
+
status,
|
|
320
|
+
headers: { 'content-type': 'application/json; charset=utf-8' },
|
|
321
|
+
});
|
|
322
|
+
// Resolve the most likely source-code root for a given package id.
|
|
323
|
+
const resolveSrcRoot = (pkgId) => {
|
|
324
|
+
const candidates = [
|
|
325
|
+
pkgId ? path.join('packages', pkgId, 'src') : null,
|
|
326
|
+
pkgId ? path.join('examples', pkgId, 'src') : null,
|
|
327
|
+
'src', // single-app project layout
|
|
328
|
+
].filter(Boolean);
|
|
329
|
+
for (const c of candidates) {
|
|
330
|
+
if (fs.existsSync(path.join(projectRoot, c)))
|
|
331
|
+
return c;
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
};
|
|
335
|
+
app.get(`${STUDIO_PATH}/api/metadata/layout`, (c) => {
|
|
336
|
+
const pkgId = c.req.query?.('package') ?? null;
|
|
337
|
+
const srcRoot = resolveSrcRoot(pkgId);
|
|
338
|
+
return respond(c, 200, { srcRoot });
|
|
339
|
+
});
|
|
340
|
+
app.post(`${STUDIO_PATH}/api/metadata/file`, async (c) => {
|
|
341
|
+
let body;
|
|
342
|
+
try {
|
|
343
|
+
body = await c.req.json();
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return respond(c, 400, { ok: false, error: 'invalid json body' });
|
|
347
|
+
}
|
|
348
|
+
const rel = typeof body?.path === 'string' ? body.path : '';
|
|
349
|
+
const content = typeof body?.content === 'string' ? body.content : '';
|
|
350
|
+
const mode = body?.mode === 'overwrite' ? 'overwrite' : 'create';
|
|
351
|
+
if (!rel)
|
|
352
|
+
return respond(c, 400, { ok: false, error: 'path is required' });
|
|
353
|
+
if (path.isAbsolute(rel) || rel.split(/[\\/]/).includes('..')) {
|
|
354
|
+
return respond(c, 400, { ok: false, error: 'path must be a project-relative path without `..`' });
|
|
355
|
+
}
|
|
356
|
+
const ext = path.extname(rel).toLowerCase();
|
|
357
|
+
if (!ALLOWED_EXT.has(ext)) {
|
|
358
|
+
return respond(c, 400, { ok: false, error: `unsupported extension ${ext}` });
|
|
359
|
+
}
|
|
360
|
+
const abs = path.resolve(projectRoot, rel);
|
|
361
|
+
if (!abs.startsWith(projectRoot + path.sep)) {
|
|
362
|
+
return respond(c, 400, { ok: false, error: 'path escapes project root' });
|
|
363
|
+
}
|
|
364
|
+
// Must contain a `/src/` segment — keeps writes scoped to source
|
|
365
|
+
// code, not random config files at the repo root.
|
|
366
|
+
const segments = path.relative(projectRoot, abs).split(path.sep);
|
|
367
|
+
if (!segments.includes('src')) {
|
|
368
|
+
return respond(c, 400, { ok: false, error: 'path must live under a src/ directory' });
|
|
369
|
+
}
|
|
370
|
+
if (fs.existsSync(abs) && mode === 'create') {
|
|
371
|
+
return respond(c, 409, { ok: false, error: 'exists' });
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
await fs.promises.mkdir(path.dirname(abs), { recursive: true });
|
|
375
|
+
await fs.promises.writeFile(abs, content, 'utf-8');
|
|
376
|
+
ctx.logger?.info?.(`Studio write API: ${mode} ${rel}`);
|
|
377
|
+
return respond(c, 200, { ok: true, path: rel });
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
ctx.logger?.error?.(`Studio write API failed: ${err?.message}`);
|
|
381
|
+
return respond(c, 500, { ok: false, error: err?.message ?? String(err) });
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
ctx.logger?.info?.(`Studio write API mounted at ${STUDIO_PATH}/api/metadata/* (dev mode)`);
|
|
385
|
+
/**
|
|
386
|
+
* Shared validation for endpoints that mutate an existing `.ts`
|
|
387
|
+
* source file by relative path. Returns `{ abs }` on success or
|
|
388
|
+
* a respond() Response when validation fails.
|
|
389
|
+
*/
|
|
390
|
+
const validateTsPath = (c, rel) => {
|
|
391
|
+
if (!rel)
|
|
392
|
+
return respond(c, 400, { ok: false, error: 'path is required' });
|
|
393
|
+
if (path.isAbsolute(rel) || rel.split(/[\\/]/).includes('..')) {
|
|
394
|
+
return respond(c, 400, { ok: false, error: 'path must be a project-relative path without `..`' });
|
|
395
|
+
}
|
|
396
|
+
if (path.extname(rel).toLowerCase() !== '.ts') {
|
|
397
|
+
return respond(c, 400, { ok: false, error: 'only .ts files are supported' });
|
|
398
|
+
}
|
|
399
|
+
const abs = path.resolve(projectRoot, rel);
|
|
400
|
+
if (!abs.startsWith(projectRoot + path.sep)) {
|
|
401
|
+
return respond(c, 400, { ok: false, error: 'path escapes project root' });
|
|
402
|
+
}
|
|
403
|
+
if (!path.relative(projectRoot, abs).split(path.sep).includes('src')) {
|
|
404
|
+
return respond(c, 400, { ok: false, error: 'path must live under a src/ directory' });
|
|
405
|
+
}
|
|
406
|
+
if (!fs.existsSync(abs)) {
|
|
407
|
+
return respond(c, 404, { ok: false, error: 'file not found' });
|
|
408
|
+
}
|
|
409
|
+
return { abs };
|
|
410
|
+
};
|
|
411
|
+
app.post(`${STUDIO_PATH}/api/metadata/field-patch`, async (c) => {
|
|
412
|
+
let body;
|
|
413
|
+
try {
|
|
414
|
+
body = await c.req.json();
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
return respond(c, 400, { ok: false, error: 'invalid json body' });
|
|
418
|
+
}
|
|
419
|
+
const rel = typeof body?.path === 'string' ? body.path : '';
|
|
420
|
+
const fieldKey = typeof body?.field === 'string' ? body.field : '';
|
|
421
|
+
const patch = body?.patch && typeof body.patch === 'object' ? body.patch : null;
|
|
422
|
+
if (!fieldKey || !patch) {
|
|
423
|
+
return respond(c, 400, { ok: false, error: 'field and patch are required' });
|
|
424
|
+
}
|
|
425
|
+
const v = validateTsPath(c, rel);
|
|
426
|
+
if (!v.abs)
|
|
427
|
+
return v;
|
|
428
|
+
try {
|
|
429
|
+
const { patchObjectFieldFile } = await import('./studio-field-patch.js');
|
|
430
|
+
const result = await patchObjectFieldFile(v.abs, fieldKey, patch);
|
|
431
|
+
if (!result.ok)
|
|
432
|
+
return respond(c, 400, result);
|
|
433
|
+
ctx.logger?.info?.(`Studio field-patch: ${rel} field=${fieldKey} keys=${Object.keys(patch).join(',')}`);
|
|
434
|
+
return respond(c, 200, { ok: true, path: rel, field: fieldKey });
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
ctx.logger?.error?.(`Studio field-patch failed: ${err?.message}`);
|
|
438
|
+
return respond(c, 500, { ok: false, error: err?.message ?? String(err) });
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
app.post(`${STUDIO_PATH}/api/metadata/field-add`, async (c) => {
|
|
442
|
+
let body;
|
|
443
|
+
try {
|
|
444
|
+
body = await c.req.json();
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
return respond(c, 400, { ok: false, error: 'invalid json body' });
|
|
448
|
+
}
|
|
449
|
+
const rel = typeof body?.path === 'string' ? body.path : '';
|
|
450
|
+
const fieldName = typeof body?.fieldName === 'string' ? body.fieldName : '';
|
|
451
|
+
const initializer = typeof body?.initializer === 'string' ? body.initializer : '';
|
|
452
|
+
if (!fieldName || !initializer) {
|
|
453
|
+
return respond(c, 400, { ok: false, error: 'fieldName and initializer are required' });
|
|
454
|
+
}
|
|
455
|
+
const v = validateTsPath(c, rel);
|
|
456
|
+
if (!v.abs)
|
|
457
|
+
return v;
|
|
458
|
+
try {
|
|
459
|
+
const { addObjectField } = await import('./studio-field-patch.js');
|
|
460
|
+
const result = await addObjectField(v.abs, fieldName, initializer);
|
|
461
|
+
if (!result.ok) {
|
|
462
|
+
// Existing field is a 409 conflict; other errors stay 400.
|
|
463
|
+
const status = result.error.includes('already exists') ? 409 : 400;
|
|
464
|
+
return respond(c, status, result);
|
|
465
|
+
}
|
|
466
|
+
ctx.logger?.info?.(`Studio field-add: ${rel} field=${fieldName}`);
|
|
467
|
+
return respond(c, 200, { ok: true, path: rel, field: fieldName });
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
ctx.logger?.error?.(`Studio field-add failed: ${err?.message}`);
|
|
471
|
+
return respond(c, 500, { ok: false, error: err?.message ?? String(err) });
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
app.post(`${STUDIO_PATH}/api/metadata/field-reorder`, async (c) => {
|
|
475
|
+
let body;
|
|
476
|
+
try {
|
|
477
|
+
body = await c.req.json();
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
return respond(c, 400, { ok: false, error: 'invalid json body' });
|
|
481
|
+
}
|
|
482
|
+
const rel = typeof body?.path === 'string' ? body.path : '';
|
|
483
|
+
const order = Array.isArray(body?.order) ? body.order.filter((s) => typeof s === 'string') : null;
|
|
484
|
+
if (!order || order.length === 0) {
|
|
485
|
+
return respond(c, 400, { ok: false, error: 'order is required (non-empty string[])' });
|
|
486
|
+
}
|
|
487
|
+
const v = validateTsPath(c, rel);
|
|
488
|
+
if (!v.abs)
|
|
489
|
+
return v;
|
|
490
|
+
try {
|
|
491
|
+
const { reorderObjectFields } = await import('./studio-field-patch.js');
|
|
492
|
+
const result = await reorderObjectFields(v.abs, order);
|
|
493
|
+
if (!result.ok)
|
|
494
|
+
return respond(c, 400, result);
|
|
495
|
+
ctx.logger?.info?.(`Studio field-reorder: ${rel} (${order.length} fields)`);
|
|
496
|
+
return respond(c, 200, { ok: true, path: rel, count: order.length });
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
ctx.logger?.error?.(`Studio field-reorder failed: ${err?.message}`);
|
|
500
|
+
return respond(c, 500, { ok: false, error: err?.message ?? String(err) });
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
281
506
|
// ─── Helpers ────────────────────────────────────────────────────────
|
|
282
507
|
const MIME_TYPES = {
|
|
283
508
|
'.html': 'text/html; charset=utf-8',
|