@notis_ai/cli 0.2.0-beta.191.1 → 0.2.0-beta.196.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.
- package/README.md +479 -80
- package/config/portal-route-roots.json +48 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +29072 -2784
- package/dist/base-skills/notis-apps/SKILL.md +88 -67
- package/dist/base-skills/notis-apps/references/architecture.md +114 -167
- package/dist/base-skills/notis-apps/references/design.md +26 -107
- package/dist/base-skills/notis-apps/references/memory.md +58 -0
- package/dist/base-skills/notis-apps/references/reading.md +52 -74
- package/dist/base-skills/notis-apps/references/release.md +104 -142
- package/dist/base-skills/notis-apps/references/sdk.md +113 -60
- package/dist/base-skills/notis-apps/references/troubleshooting.md +22 -21
- package/dist/base-skills/notis-apps/references/views.md +142 -0
- package/dist/base-skills/notis-cli/SKILL.md +4 -4
- package/dist/base-skills/notis-cli/references/app-delivery.md +12 -11
- package/dist/base-skills/notis-cli/references/native-databases.md +6 -6
- package/dist/base-skills/notis-cli/references/tool-examples.md +4 -4
- package/dist/base-skills/notis-query/SKILL.md +34 -60
- package/dist/base-skills/notis-query/references/database-discovery.md +59 -69
- package/dist/base-skills/notis-query/references/documents.md +63 -50
- package/dist/base-skills/notis-query/references/query.md +57 -524
- package/dist/sdk/src/bundleModule.ts +79 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +6 -3
- package/dist/sdk/src/components/DocumentPage.tsx +141 -0
- package/dist/sdk/src/components/RecordComponents.tsx +33 -0
- package/dist/sdk/src/components/RenderChartData.tsx +19 -0
- package/dist/sdk/src/config.ts +4 -26
- package/dist/sdk/src/documents.ts +2 -0
- package/dist/sdk/src/hooks/useAction.ts +28 -0
- package/dist/sdk/src/hooks/useCloudComputer.ts +35 -4
- package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
- package/dist/sdk/src/hooks/useHandover.ts +5 -4
- package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
- package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
- package/dist/sdk/src/hooks/useNotis.ts +3 -0
- package/dist/sdk/src/hooks/useNotisNavigation.ts +26 -11
- package/dist/sdk/src/hooks/useQuery.ts +7 -4
- package/dist/sdk/src/hooks/useViewParams.ts +121 -0
- package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
- package/dist/sdk/src/index.ts +36 -0
- package/dist/sdk/src/interactions/shortcuts.tsx +17 -3
- package/dist/sdk/src/nativeData.ts +39 -0
- package/dist/sdk/src/presentation.tsx +82 -0
- package/dist/sdk/src/provider.tsx +9 -4
- package/dist/sdk/src/queryCache.ts +5 -3
- package/dist/sdk/src/queryErrors.ts +51 -0
- package/dist/sdk/src/runtime.ts +146 -8
- package/dist/sdk/src/space.ts +425 -0
- package/dist/sdk/src/styles.css +96 -72
- package/dist/sdk/src/vite.ts +11 -1
- package/dist/skill-sync/index.js +458 -68
- package/dist/skill-sync/index.js.map +4 -4
- package/dist/skill-sync-worker.mjs +451 -77
- package/dist/space-harness/host.js +66 -0
- package/dist/view-renderer/dist/host.css +1 -0
- package/dist/view-renderer/dist/host.js +1042 -0
- package/dist/view-renderer/dist/theme.css +73 -0
- package/dist/view-renderer/src/dom.js +180 -0
- package/dist/view-renderer/src/errors.js +8 -0
- package/dist/view-renderer/src/host.tsx +173 -0
- package/dist/view-renderer/src/index.js +4 -0
- package/dist/view-renderer/src/pool.js +76 -0
- package/dist/view-renderer/src/remote-broker.js +59 -0
- package/dist/view-renderer/src/renderer.js +201 -0
- package/package.json +11 -5
- package/skills/notis-apps/cli.md +270 -163
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +2 -2
- package/skills/notis-onboarding/BRIEF.md +7 -3
- package/src/command-specs/apps.js +1 -8
- package/src/command-specs/files.js +29 -0
- package/src/command-specs/helpers.js +1 -0
- package/src/command-specs/index.js +10 -4
- package/src/command-specs/skills.js +229 -0
- package/src/command-specs/spaces.js +572 -0
- package/src/command-specs/views.js +69 -0
- package/src/runtime/agent-browser.js +30 -7
- package/src/runtime/app-boundary-validator.js +7 -4
- package/src/runtime/app-data-literals.js +82 -0
- package/src/runtime/app-platform.js +321 -253
- package/src/runtime/confined-space-build.js +74 -0
- package/src/runtime/confined-space-verify.js +49 -0
- package/src/runtime/native-record-files.js +47 -0
- package/src/runtime/retired-sdk-files.js +20 -0
- package/src/runtime/skill-sync/cloud-client.ts +26 -4
- package/src/runtime/skill-sync/index.ts +154 -35
- package/src/runtime/skill-sync/local-scanner.ts +125 -17
- package/src/runtime/skill-sync/native-identity.ts +102 -0
- package/src/runtime/skill-sync/native-plan.ts +59 -0
- package/src/runtime/skill-sync/sync-plan.ts +16 -9
- package/src/runtime/skill-sync/types.ts +32 -6
- package/src/runtime/source-root.js +32 -0
- package/src/runtime/space-collection-scaffold.js +66 -0
- package/src/runtime/space-database-structures.js +229 -0
- package/src/runtime/space-fixture-broker.js +50 -0
- package/src/runtime/space-lock.js +337 -0
- package/src/runtime/space-platform.js +105 -0
- package/src/runtime/space-source.js +281 -0
- package/src/runtime/space-test-server.js +327 -0
- package/src/runtime/space-verification.js +89 -0
- package/src/runtime/space-verify-browser-worker.js +51 -0
- package/src/runtime/space-view-manifest.js +254 -0
- package/src/runtime/sync-skills.js +1 -1
- package/src/runtime/transport.js +8 -1
- package/src/runtime/view-render.js +49 -0
- package/src/space-harness-host.ts +103 -0
- package/template/app/globals.css +1 -0
- package/src/command-specs/reports.js +0 -97
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { httpRequest } from '../runtime/transport.js';
|
|
2
|
+
import { usageError } from '../runtime/errors.js';
|
|
3
|
+
import { renderViewWithCliRuntime } from '../runtime/view-render.js';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
export function viewRenderCommandSpec({ screenshotOnly = false } = {}) {
|
|
8
|
+
const command_path = screenshotOnly ? ['spaces', 'screenshot'] : ['views', 'render'];
|
|
9
|
+
return {
|
|
10
|
+
command_path, summary: screenshotOnly ? 'Capture a full-height PNG of a live view with your current access.' : 'Render a live view as Markdown and a full-height PNG with your current access.',
|
|
11
|
+
when_to_use: 'Check the actual deployed view; writes are blocked and current access is checked again before any output is saved.',
|
|
12
|
+
args_schema: { arguments: [{ token: '<url>', key: 'url', description: 'Current view-qualified or record link.' }], options: [
|
|
13
|
+
...(!screenshotOnly ? [{ flags: '--outputs <kinds>', description: 'Comma-separated markdown,screenshot (default both).' }] : []),
|
|
14
|
+
{ flags: '--width <pixels>', description: '1440 (default) or 390.' },
|
|
15
|
+
{ flags: '--output-dir <path>', description: 'New output directory; defaults to a unique .notis/renders directory. Existing files are never overwritten.' },
|
|
16
|
+
] }, examples: [screenshotOnly ? 'notis spaces screenshot <view-link> --width 390' : 'notis views render <view-link> --outputs markdown,screenshot'],
|
|
17
|
+
mutates: false, idempotent: true, require_auth: true,
|
|
18
|
+
backend_call: { type: 'http', path: '/portal_view_render/prepare' },
|
|
19
|
+
async handler(ctx) {
|
|
20
|
+
const outputs = screenshotOnly ? ['screenshot'] : (ctx.options.outputs || 'markdown,screenshot').split(',');
|
|
21
|
+
if (!outputs.length || new Set(outputs).size !== outputs.length || outputs.some(value => !['markdown', 'screenshot'].includes(value))) {
|
|
22
|
+
throw usageError('Choose markdown, screenshot, or markdown,screenshot.');
|
|
23
|
+
}
|
|
24
|
+
const width = ctx.options.width === undefined ? 1440 : Number(ctx.options.width);
|
|
25
|
+
const directory = resolve(ctx.options.outputDir || `.notis/renders/${randomUUID()}`);
|
|
26
|
+
const controller = new AbortController(), cancel = () => controller.abort();
|
|
27
|
+
process.once('SIGINT', cancel); process.once('SIGTERM', cancel);
|
|
28
|
+
try {
|
|
29
|
+
const { result, artifacts } = await renderViewWithCliRuntime({ runtime: ctx.runtime,
|
|
30
|
+
url: ctx.args.url, outputs, width, directory, signal: controller.signal });
|
|
31
|
+
const data = { ...result, artifacts };
|
|
32
|
+
if (data.screenshot) {
|
|
33
|
+
const { data_base64: _bytes, ...metadata } = data.screenshot;
|
|
34
|
+
data.screenshot = { ...metadata, path: artifacts.screenshot };
|
|
35
|
+
}
|
|
36
|
+
return ctx.output.emitSuccess({ command: command_path.join(' '), data,
|
|
37
|
+
humanSummary: `Rendered ${result.url}. Saved ${Object.values(artifacts).join(' and ')}.`, meta: { mutating: false } });
|
|
38
|
+
} finally { process.removeListener('SIGINT', cancel); process.removeListener('SIGTERM', cancel); }
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const viewsCommandSpecs = [{
|
|
44
|
+
command_path: ['views', 'find'],
|
|
45
|
+
summary: 'Find the views you can open for a record, database, view link, Space or search.',
|
|
46
|
+
when_to_use: 'Get fresh view-qualified links and declared URL parameters before citing or opening a view.',
|
|
47
|
+
args_schema: { options: [
|
|
48
|
+
{ flags: '--record-key <id>', description: 'Find views that show this exact native record.' },
|
|
49
|
+
{ flags: '--database-id <id>', description: 'Find views over this database.' },
|
|
50
|
+
{ flags: '--url <url>', description: 'Resolve this view link and its current parameters.' },
|
|
51
|
+
{ flags: '--space-id <id>', description: 'Describe this Space view.' },
|
|
52
|
+
{ flags: '--query <text>', description: 'Search accessible view names, descriptions and readable context.' },
|
|
53
|
+
] },
|
|
54
|
+
examples: ['notis views find --record-key <record-key>', 'notis views find --url <view-link>', 'notis views find --query inbox'],
|
|
55
|
+
mutates: false, idempotent: true, require_auth: true,
|
|
56
|
+
backend_call: { type: 'http', path: '/portal_spaces/find-views' },
|
|
57
|
+
async handler(ctx) {
|
|
58
|
+
const options = { record_key: ctx.options.recordKey, database_id: ctx.options.databaseId,
|
|
59
|
+
url: ctx.options.url, space_id: ctx.options.spaceId, query: ctx.options.query };
|
|
60
|
+
const selected = Object.entries(options).filter(([, value]) => value !== undefined);
|
|
61
|
+
if (selected.length !== 1 || typeof selected[0][1] !== 'string' || !selected[0][1].trim()) {
|
|
62
|
+
throw usageError('Choose exactly one of --record-key, --database-id, --url, --space-id or --query.');
|
|
63
|
+
}
|
|
64
|
+
const result = await httpRequest({ runtime: ctx.runtime, method: 'POST', path: '/portal_spaces/find-views',
|
|
65
|
+
body: Object.fromEntries(selected), spacesProtocol: 1 });
|
|
66
|
+
return ctx.output.emitSuccess({ command: 'views find', data: result.payload,
|
|
67
|
+
humanSummary: JSON.stringify(result.payload, null, 2), meta: { mutating: false, request_id: result.requestId } });
|
|
68
|
+
},
|
|
69
|
+
}, viewRenderCommandSpec()];
|
|
@@ -131,9 +131,11 @@ function runAgentBrowser(args, { timeoutMs = 30_000 } = {}) {
|
|
|
131
131
|
let stdout = '';
|
|
132
132
|
let stderr = '';
|
|
133
133
|
let timedOut = false;
|
|
134
|
+
let hardStop;
|
|
134
135
|
const timer = setTimeout(() => {
|
|
135
136
|
timedOut = true;
|
|
136
137
|
child.kill('SIGTERM');
|
|
138
|
+
hardStop = setTimeout(() => child.kill('SIGKILL'), 2000);
|
|
137
139
|
}, timeoutMs);
|
|
138
140
|
|
|
139
141
|
child.stdout.on('data', (chunk) => {
|
|
@@ -143,7 +145,7 @@ function runAgentBrowser(args, { timeoutMs = 30_000 } = {}) {
|
|
|
143
145
|
stderr += chunk.toString();
|
|
144
146
|
});
|
|
145
147
|
child.on('error', (error) => {
|
|
146
|
-
clearTimeout(timer);
|
|
148
|
+
clearTimeout(timer); clearTimeout(hardStop);
|
|
147
149
|
resolvePromise({
|
|
148
150
|
exitCode: 1,
|
|
149
151
|
stdout,
|
|
@@ -152,7 +154,7 @@ function runAgentBrowser(args, { timeoutMs = 30_000 } = {}) {
|
|
|
152
154
|
});
|
|
153
155
|
});
|
|
154
156
|
child.on('close', (code) => {
|
|
155
|
-
clearTimeout(timer);
|
|
157
|
+
clearTimeout(timer); clearTimeout(hardStop);
|
|
156
158
|
resolvePromise({
|
|
157
159
|
exitCode: timedOut ? 124 : (code ?? 1),
|
|
158
160
|
stdout,
|
|
@@ -225,10 +227,17 @@ export async function runHarnessRoute({
|
|
|
225
227
|
snapshotPath = null,
|
|
226
228
|
designViewports = DEFAULT_DESIGN_VIEWPORTS,
|
|
227
229
|
waitForRuntime = false,
|
|
230
|
+
signal,
|
|
228
231
|
}) {
|
|
232
|
+
const cancelled = () => ({ mounted: false, renderStarted: false, runtimeCalls: [],
|
|
233
|
+
errors: [{ phase: 'browser', message: 'Verification cancelled.' }], snapshotPath: null, tool_error: null });
|
|
234
|
+
if (signal?.aborted) return cancelled();
|
|
229
235
|
const opened = await runAgentBrowser(['--session', sessionName, 'open', url], {
|
|
230
236
|
timeoutMs: Math.min(Math.max(timeoutMs, 5000), 30_000),
|
|
231
237
|
});
|
|
238
|
+
// Let an in-flight open settle before the owner closes its browser. Closing
|
|
239
|
+
// concurrently with open can leave a newly launched browser behind.
|
|
240
|
+
if (signal?.aborted) return cancelled();
|
|
232
241
|
if (opened.exitCode !== 0) {
|
|
233
242
|
return {
|
|
234
243
|
mounted: false,
|
|
@@ -242,7 +251,7 @@ export async function runHarnessRoute({
|
|
|
242
251
|
|
|
243
252
|
const deadline = Date.now() + timeoutMs;
|
|
244
253
|
let readySince = null;
|
|
245
|
-
while (Date.now() < deadline) {
|
|
254
|
+
while (Date.now() < deadline && !signal?.aborted) {
|
|
246
255
|
const lastRead = await readHarness(sessionName, 5000);
|
|
247
256
|
if (!lastRead.ok) {
|
|
248
257
|
readySince = null;
|
|
@@ -259,6 +268,8 @@ export async function runHarnessRoute({
|
|
|
259
268
|
await delay(250);
|
|
260
269
|
}
|
|
261
270
|
|
|
271
|
+
if (signal?.aborted) return cancelled();
|
|
272
|
+
|
|
262
273
|
const finalRead = await readHarness(sessionName, 5000);
|
|
263
274
|
if (!finalRead.ok) {
|
|
264
275
|
return {
|
|
@@ -525,10 +536,22 @@ export async function captureHarnessScreenshot({
|
|
|
525
536
|
}
|
|
526
537
|
|
|
527
538
|
export async function closeAgentBrowserSession(sessionName) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
539
|
+
return (await closeAgentBrowserSessionResult(sessionName)).ok;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export async function closeAgentBrowserSessionResult(sessionName) {
|
|
543
|
+
const started = Date.now();
|
|
544
|
+
// agent-browser waits up to 30s for CDP Browser.close and then up to 5s
|
|
545
|
+
// for its owned Chrome process. Do not kill the client at the start of the
|
|
546
|
+
// provider's shutdown grace period and lose its actual close acknowledgement.
|
|
547
|
+
const result = await runAgentBrowser(['--session', sessionName, '--json', 'close'], { timeoutMs: 40_000 });
|
|
548
|
+
let acknowledged = false;
|
|
549
|
+
try {
|
|
550
|
+
const reply = parseAgentBrowserJson(result.stdout);
|
|
551
|
+
acknowledged = reply?.success === true && reply?.data?.closed === true;
|
|
552
|
+
} catch { /* Missing acknowledgement is a failed cleanup, never a pass. */ }
|
|
553
|
+
return { ok: result.exitCode === 0 && acknowledged, exit_code: result.exitCode,
|
|
554
|
+
timed_out: Boolean(result.timedOut), duration_ms: Date.now() - started };
|
|
532
555
|
}
|
|
533
556
|
|
|
534
557
|
// ---------------------------------------------------------------------------
|
|
@@ -3,6 +3,7 @@ import { dirname, extname, join, resolve } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
5
|
import { usageError } from './errors.js';
|
|
6
|
+
import { maskSerializedJsonData } from './app-data-literals.js';
|
|
6
7
|
|
|
7
8
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
|
|
@@ -136,7 +137,7 @@ function validateTextFile(relPath, content) {
|
|
|
136
137
|
return applyRules(content, rules.css, relPath);
|
|
137
138
|
}
|
|
138
139
|
if (extension === '.js' || extension === '.jsx' || extension === '.ts' || extension === '.tsx') {
|
|
139
|
-
return applyRules(content, rules.javascript, relPath);
|
|
140
|
+
return applyRules(maskSerializedJsonData(content, extension), rules.javascript, relPath);
|
|
140
141
|
}
|
|
141
142
|
return [];
|
|
142
143
|
}
|
|
@@ -311,7 +312,9 @@ export function collectDesignViolationsForFile(relPath, content, config = getCom
|
|
|
311
312
|
if (!matchesAny(normalized, config.include)) return [];
|
|
312
313
|
if (matchesAny(normalized, config.exclude)) return [];
|
|
313
314
|
|
|
314
|
-
const
|
|
315
|
+
const checked = ['.js', '.jsx', '.ts', '.tsx'].includes(extname(normalized))
|
|
316
|
+
? maskSerializedJsonData(content, extname(normalized)) : content;
|
|
317
|
+
const lines = checked.split('\n');
|
|
315
318
|
const violations = [];
|
|
316
319
|
for (const rule of config.rules) {
|
|
317
320
|
if (rule.include && !matchesAny(normalized, rule.include)) continue;
|
|
@@ -319,12 +322,12 @@ export function collectDesignViolationsForFile(relPath, content, config = getCom
|
|
|
319
322
|
|
|
320
323
|
const matches = [];
|
|
321
324
|
if (rule.scope === 'file') {
|
|
322
|
-
const match = rule.regex.exec(
|
|
325
|
+
const match = rule.regex.exec(checked);
|
|
323
326
|
if (match) matches.push(match.index);
|
|
324
327
|
} else {
|
|
325
328
|
rule.regex.lastIndex = 0;
|
|
326
329
|
let match;
|
|
327
|
-
while ((match = rule.regex.exec(
|
|
330
|
+
while ((match = rule.regex.exec(checked)) !== null) {
|
|
328
331
|
matches.push(match.index);
|
|
329
332
|
if (match[0].length === 0) rule.regex.lastIndex += 1;
|
|
330
333
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
let babelParse;
|
|
5
|
+
|
|
6
|
+
function isJsonParse(node) {
|
|
7
|
+
const callee = node.callee;
|
|
8
|
+
return node.type === 'CallExpression' && callee?.type === 'MemberExpression' && !callee.computed
|
|
9
|
+
&& callee.object?.type === 'Identifier' && callee.object.name === 'JSON'
|
|
10
|
+
&& callee.property?.type === 'Identifier' && callee.property.name === 'parse';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function dataContext(ancestors) {
|
|
14
|
+
if (ancestors.some(node => node.type === 'JSXAttribute')) return false;
|
|
15
|
+
for (let index = ancestors.length - 1; index >= 0; index -= 1) {
|
|
16
|
+
const node = ancestors[index];
|
|
17
|
+
if (node.type === 'JSXAttribute' || node.type === 'ImportDeclaration'
|
|
18
|
+
|| node.type === 'ImportExpression' || node.type === 'TSImportType'
|
|
19
|
+
|| node.type === 'ExportAllDeclaration' || node.type === 'ExportNamedDeclaration'
|
|
20
|
+
|| node.type === 'NewExpression' || node.type === 'TaggedTemplateExpression'
|
|
21
|
+
|| node.type === 'MemberExpression' || node.type === 'OptionalMemberExpression') return false;
|
|
22
|
+
if (node.type === 'CallExpression' || node.type === 'OptionalCallExpression') {
|
|
23
|
+
if (!isJsonParse(node)) return false;
|
|
24
|
+
}
|
|
25
|
+
if (['VariableDeclarator', 'ExportDefaultDeclaration', 'ReturnStatement'].includes(node.type)) return true;
|
|
26
|
+
if (node.type === 'ExpressionStatement') return false;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Serialized JSON is data, not source: a review may quote forbidden host code
|
|
33
|
+
* or historical styles. Mask only complete object/array JSON literal values in
|
|
34
|
+
* data-expression positions. Keep ordinary strings, import/call/selector and
|
|
35
|
+
* JSX attribute contexts, and every template expression under the existing
|
|
36
|
+
* rules. Parsing errors (or a missing parser) retain the original checks.
|
|
37
|
+
* Source/archive bytes are never rewritten; offsets and line breaks survive.
|
|
38
|
+
*/
|
|
39
|
+
export function maskSerializedJsonData(content, extension = '.js') {
|
|
40
|
+
let ast;
|
|
41
|
+
try {
|
|
42
|
+
babelParse ||= require('@babel/parser').parse;
|
|
43
|
+
ast = babelParse(content, {
|
|
44
|
+
sourceType: 'unambiguous',
|
|
45
|
+
allowReturnOutsideFunction: true,
|
|
46
|
+
plugins: [...(['.ts', '.tsx'].includes(extension) ? ['typescript'] : []),
|
|
47
|
+
...(extension !== '.ts' ? ['jsx'] : [])],
|
|
48
|
+
});
|
|
49
|
+
} catch { return content; }
|
|
50
|
+
const ranges = [];
|
|
51
|
+
const pending = [[ast, []]];
|
|
52
|
+
while (pending.length) {
|
|
53
|
+
const [node, ancestors] = pending.pop();
|
|
54
|
+
if (!node || typeof node !== 'object') continue;
|
|
55
|
+
const literal = node.type === 'StringLiteral' ? node.value
|
|
56
|
+
: node.type === 'TemplateLiteral' && node.expressions.length === 0 ? node.quasis[0]?.value.cooked : null;
|
|
57
|
+
if (typeof literal === 'string' && dataContext(ancestors)) {
|
|
58
|
+
try {
|
|
59
|
+
const value = JSON.parse(literal);
|
|
60
|
+
if (value !== null && typeof value === 'object') {
|
|
61
|
+
ranges.push([node.start, node.end]);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
} catch { /* Ordinary/non-JSON strings keep their original checks. */ }
|
|
65
|
+
}
|
|
66
|
+
const parents = [...ancestors, node];
|
|
67
|
+
for (const [key, value] of Object.entries(node)) {
|
|
68
|
+
if (['loc', 'extra', 'tokens', 'comments', 'errors'].includes(key)) continue;
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
for (const child of value) if (child?.type) pending.push([child, parents]);
|
|
71
|
+
} else if (value?.type) pending.push([value, parents]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
let cursor = 0;
|
|
75
|
+
const result = [];
|
|
76
|
+
for (const [start, end] of ranges.sort((a, b) => a[0] - b[0])) {
|
|
77
|
+
result.push(content.slice(cursor, start), content.slice(start, end).replace(/[^\r\n]/g, ' '));
|
|
78
|
+
cursor = end;
|
|
79
|
+
}
|
|
80
|
+
result.push(content.slice(cursor));
|
|
81
|
+
return result.join('');
|
|
82
|
+
}
|