@shapeshift-labs/frontier-lang-cli 0.3.7 → 0.3.9
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 +4 -1
- package/dist/index.js +252 -10
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,6 +6,9 @@ Command line interface for parsing, checking, hashing, and emitting Frontier Lan
|
|
|
6
6
|
frontier-lang check app.frontier --strict-effects
|
|
7
7
|
frontier-lang emit app.frontier --target rust --out generated.rs
|
|
8
8
|
frontier-lang capabilities app.frontier --target typescript --platform node
|
|
9
|
+
frontier-lang import src/todo.ts --sidecar --out native-import.json
|
|
10
|
+
frontier-lang project-native native-import.json --source-only --out restored.ts
|
|
11
|
+
frontier-lang native-coverage src/todo.ts
|
|
9
12
|
```
|
|
10
13
|
|
|
11
14
|
## Related Packages
|
|
@@ -190,7 +193,7 @@ npm install -g @shapeshift-labs/frontier-lang-cli
|
|
|
190
193
|
frontier-lang check examples/todo.frontier
|
|
191
194
|
```
|
|
192
195
|
|
|
193
|
-
Commands: `parse`, `check`, `hash`, `emit`, `emit-ts`, `emit-js`, `emit-rust`, `emit-python`, and `emit-c`.
|
|
196
|
+
Commands: `parse`, `check`, `hash`, `ast`, `capabilities`, `to-json`, `from-json`, `import`, `project-native`, `native-coverage`, `roundtrip`, `corpus-roundtrip`, `emit`, `emit-ts`, `emit-js`, `emit-rust`, `emit-python`, and `emit-c`.
|
|
194
197
|
|
|
195
198
|
```sh
|
|
196
199
|
frontier-lang emit app.frontier --target rust --out app.rs
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import { parseFrontierFile, parseFrontierSource } from '@shapeshift-labs/frontier-lang-parser';
|
|
5
6
|
import { checkDocument } from '@shapeshift-labs/frontier-lang-checker';
|
|
6
7
|
import { hashDocumentBase } from '@shapeshift-labs/frontier-lang-kernel';
|
|
7
8
|
import {
|
|
8
9
|
compileFrontierDocument,
|
|
10
|
+
createNativeSourcePreservation,
|
|
11
|
+
createNativeImportCoverageMatrix,
|
|
12
|
+
createSemanticImportSidecar,
|
|
9
13
|
createUniversalAstFromDocument,
|
|
10
14
|
importNativeSource,
|
|
15
|
+
projectNativeImportToSource,
|
|
11
16
|
projectFrontierAst,
|
|
12
17
|
readUniversalAstJson,
|
|
13
18
|
resolveCapabilityAdapters,
|
|
@@ -18,6 +23,9 @@ export async function runCli(argv = process.argv.slice(2), io = console) {
|
|
|
18
23
|
const [command, file, ...rest] = argv;
|
|
19
24
|
if (!command || command === 'help' || command === '--help') return help(io);
|
|
20
25
|
if (!file && command !== 'version') throw new Error(`Missing input file for ${command}`);
|
|
26
|
+
if (command === 'corpus-roundtrip') {
|
|
27
|
+
return outputMaybeFile(io, rest, runCorpusRoundtrip(file, rest));
|
|
28
|
+
}
|
|
21
29
|
const source = file ? readFileSync(file, 'utf8') : '';
|
|
22
30
|
if (command === 'from-json') {
|
|
23
31
|
const envelope = readUniversalAstJson(source);
|
|
@@ -27,15 +35,28 @@ export async function runCli(argv = process.argv.slice(2), io = console) {
|
|
|
27
35
|
return io.log(result.output);
|
|
28
36
|
}
|
|
29
37
|
if (command === 'import') {
|
|
38
|
+
const imported = importNativeFile(file, source, rest);
|
|
39
|
+
if (rest.includes('--sidecar-only')) return outputMaybeFile(io, rest, createSemanticImportSidecar(imported));
|
|
40
|
+
if (rest.includes('--sidecar')) {
|
|
41
|
+
return outputMaybeFile(io, rest, { import: imported, sidecar: createSemanticImportSidecar(imported) });
|
|
42
|
+
}
|
|
43
|
+
return outputMaybeFile(io, rest, imported);
|
|
44
|
+
}
|
|
45
|
+
if (command === 'project-native') {
|
|
46
|
+
const imported = readNativeImportForProjection(file, source, rest);
|
|
47
|
+
const projection = projectNativeImportToSource(imported, { preferPreservedSource: !rest.includes('--stubs') });
|
|
48
|
+
if (rest.includes('--source-only')) {
|
|
49
|
+
const outIndex = rest.indexOf('--out');
|
|
50
|
+
if (outIndex >= 0 && rest[outIndex + 1]) writeFileSync(rest[outIndex + 1], projection.sourceText);
|
|
51
|
+
else io.log(projection.sourceText);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
return outputMaybeFile(io, rest, projection);
|
|
55
|
+
}
|
|
56
|
+
if (command === 'native-coverage') {
|
|
30
57
|
const language = readOption(rest, '--language') ?? inferLanguage(file);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
parser: readOption(rest, '--parser'),
|
|
34
|
-
sourcePath: file,
|
|
35
|
-
sourceHash: readOption(rest, '--source-hash'),
|
|
36
|
-
sourceText: source,
|
|
37
|
-
nativeAstMetadata: { sourceBytes: source.length }
|
|
38
|
-
}));
|
|
58
|
+
const imported = importNativeFile(file, source, rest, { language });
|
|
59
|
+
return outputMaybeFile(io, rest, createNativeImportCoverageMatrix({ imports: [imported] }));
|
|
39
60
|
}
|
|
40
61
|
const document = file ? parseFrontierFile(file, source) : parseFrontierSource(source);
|
|
41
62
|
if (command === 'to-json') {
|
|
@@ -92,8 +113,229 @@ function outputMaybeFile(io, args, value) {
|
|
|
92
113
|
const outIndex = args.indexOf('--out');
|
|
93
114
|
if (outIndex >= 0 && args[outIndex + 1]) writeFileSync(args[outIndex + 1], json + '\n'); else io.log(json);
|
|
94
115
|
}
|
|
95
|
-
function
|
|
116
|
+
function runCorpusRoundtrip(inputPath, args) {
|
|
117
|
+
const target = readOption(args, '--target') ?? 'typescript';
|
|
118
|
+
const entries = collectCorpusEntries(inputPath);
|
|
119
|
+
const files = entries.map((entry) => corpusRoundtripFile(entry, { target, parser: readOption(args, '--parser') }));
|
|
120
|
+
const failed = files.filter((fileResult) => !fileResult.ok);
|
|
121
|
+
return {
|
|
122
|
+
kind: 'frontier.lang.corpusRoundtrip',
|
|
123
|
+
version: 1,
|
|
124
|
+
inputPath,
|
|
125
|
+
target,
|
|
126
|
+
total: files.length,
|
|
127
|
+
passed: files.length - failed.length,
|
|
128
|
+
failed: failed.length,
|
|
129
|
+
sourceMapCount: files.reduce((sum, fileResult) => sum + (fileResult.sourceMapCount ?? 0), 0),
|
|
130
|
+
lossCount: files.reduce((sum, fileResult) => sum + (fileResult.lossCount ?? 0), 0),
|
|
131
|
+
sourcePreservationCount: files.filter((fileResult) => fileResult.sourcePreservationId).length,
|
|
132
|
+
projectionModes: files.reduce((counts, fileResult) => {
|
|
133
|
+
if (fileResult.projectionMode) counts[fileResult.projectionMode] = (counts[fileResult.projectionMode] ?? 0) + 1;
|
|
134
|
+
return counts;
|
|
135
|
+
}, {}),
|
|
136
|
+
readiness: files.reduce((counts, fileResult) => {
|
|
137
|
+
for (const readiness of fileResult.mergeReadiness ?? []) counts[readiness] = (counts[readiness] ?? 0) + 1;
|
|
138
|
+
return counts;
|
|
139
|
+
}, {}),
|
|
140
|
+
files
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function corpusRoundtripFile(entry, options) {
|
|
145
|
+
const path = entry.path;
|
|
146
|
+
try {
|
|
147
|
+
const source = readFileSync(path, 'utf8');
|
|
148
|
+
const language = entry.language ?? inferLanguage(path);
|
|
149
|
+
if (/\.frontier$/i.test(path)) {
|
|
150
|
+
const document = parseFrontierFile(path, source);
|
|
151
|
+
const envelope = createUniversalAstFromDocument(document, {
|
|
152
|
+
evidence: [{ id: `frontier_lang_cli_corpus_${idFragment(path)}`, kind: 'test', status: 'passed', path, summary: 'Corpus Frontier source parsed into universal AST.' }]
|
|
153
|
+
});
|
|
154
|
+
const encoded = writeUniversalAstJson(envelope);
|
|
155
|
+
const decoded = readUniversalAstJson(encoded);
|
|
156
|
+
const result = compileFrontierDocument(decoded.document, { target: options.target });
|
|
157
|
+
return {
|
|
158
|
+
path,
|
|
159
|
+
language: 'frontier',
|
|
160
|
+
kind: 'frontierSource',
|
|
161
|
+
ok: result.ok && decoded.document.id === document.id,
|
|
162
|
+
hash: result.hash,
|
|
163
|
+
sourceMapCount: envelope.sourceMaps?.length ?? 0,
|
|
164
|
+
lossCount: envelope.losses?.length ?? 0,
|
|
165
|
+
evidenceCount: envelope.evidence?.length ?? 0,
|
|
166
|
+
diagnostics: result.diagnostics,
|
|
167
|
+
outputBytes: result.output.length,
|
|
168
|
+
jsonBytes: encoded.length
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const imported = importNativeSource({
|
|
172
|
+
language,
|
|
173
|
+
parser: entry.parser ?? options.parser,
|
|
174
|
+
sourcePath: path,
|
|
175
|
+
sourceText: source
|
|
176
|
+
});
|
|
177
|
+
const projection = projectNativeImportToSource(imported);
|
|
178
|
+
const encoded = writeUniversalAstJson(imported.universalAst);
|
|
179
|
+
const decoded = readUniversalAstJson(encoded);
|
|
180
|
+
return {
|
|
181
|
+
path,
|
|
182
|
+
language,
|
|
183
|
+
kind: 'nativeSource',
|
|
184
|
+
ok: decoded.document.id === imported.document.id,
|
|
185
|
+
sourceMapCount: imported.sourceMaps?.length ?? 0,
|
|
186
|
+
sourceMapMappingCount: (imported.sourceMaps ?? []).reduce((sum, sourceMap) => sum + (sourceMap.mappings?.length ?? 0), 0),
|
|
187
|
+
lossCount: imported.losses?.length ?? 0,
|
|
188
|
+
evidenceCount: imported.evidence?.length ?? 0,
|
|
189
|
+
symbolCount: imported.semanticIndex?.symbols?.length ?? 0,
|
|
190
|
+
occurrenceCount: imported.semanticIndex?.occurrences?.length ?? 0,
|
|
191
|
+
sourcePreservationId: imported.metadata?.sourcePreservationId,
|
|
192
|
+
sourcePreservationExact: imported.metadata?.sourcePreservation?.summary?.exactSourceAvailable === true,
|
|
193
|
+
projectionMode: projection.mode,
|
|
194
|
+
projectionReadiness: projection.readiness?.readiness,
|
|
195
|
+
projectionLossCount: projection.losses?.length ?? 0,
|
|
196
|
+
mergeReadiness: (imported.mergeCandidates ?? []).map((candidate) => candidate.readiness),
|
|
197
|
+
jsonBytes: encoded.length
|
|
198
|
+
};
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return {
|
|
201
|
+
path,
|
|
202
|
+
language: entry.language ?? inferLanguage(path),
|
|
203
|
+
kind: 'error',
|
|
204
|
+
ok: false,
|
|
205
|
+
error: error instanceof Error ? error.message : String(error)
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function collectCorpusEntries(inputPath) {
|
|
211
|
+
const absolute = resolve(inputPath);
|
|
212
|
+
const stat = statSync(absolute);
|
|
213
|
+
if (stat.isDirectory()) {
|
|
214
|
+
return collectCorpusDirectory(absolute).map((path) => ({ path }));
|
|
215
|
+
}
|
|
216
|
+
if (/\.json$/i.test(absolute)) {
|
|
217
|
+
return readCorpusManifest(absolute);
|
|
218
|
+
}
|
|
219
|
+
return [{ path: absolute }];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function collectCorpusDirectory(root) {
|
|
223
|
+
const files = [];
|
|
224
|
+
for (const item of readdirSync(root, { withFileTypes: true })) {
|
|
225
|
+
const path = join(root, item.name);
|
|
226
|
+
if (item.isDirectory()) {
|
|
227
|
+
if (!isIgnoredCorpusDirectory(item.name)) files.push(...collectCorpusDirectory(path));
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (item.isFile() && isCorpusSourceFile(path)) files.push(path);
|
|
231
|
+
}
|
|
232
|
+
return files.sort();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function readCorpusManifest(path) {
|
|
236
|
+
const manifest = JSON.parse(readFileSync(path, 'utf8'));
|
|
237
|
+
const base = dirname(path);
|
|
238
|
+
const rawEntries = Array.isArray(manifest) ? manifest : manifest.files ?? manifest.entries ?? [];
|
|
239
|
+
return rawEntries.map((entry) => {
|
|
240
|
+
if (typeof entry === 'string') return { path: resolve(base, entry) };
|
|
241
|
+
return {
|
|
242
|
+
...entry,
|
|
243
|
+
path: resolve(base, entry.path ?? entry.file)
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function importNativeFile(file, source, args, defaults = {}) {
|
|
249
|
+
const language = defaults.language ?? readOption(args, '--language') ?? inferLanguage(file);
|
|
250
|
+
const sourceHash = readOption(args, '--source-hash');
|
|
251
|
+
const sourcePreservation = sourcePreservationOptionsRequested(args)
|
|
252
|
+
? createNativeSourcePreservation({
|
|
253
|
+
language,
|
|
254
|
+
sourcePath: file,
|
|
255
|
+
sourceHash,
|
|
256
|
+
sourceText: source,
|
|
257
|
+
includeSourceText: !args.includes('--omit-source-text'),
|
|
258
|
+
includeTokens: !args.includes('--no-tokens'),
|
|
259
|
+
includeTrivia: !args.includes('--no-trivia'),
|
|
260
|
+
includeDirectives: !args.includes('--no-directives'),
|
|
261
|
+
maxTokens: readIntegerOption(args, '--max-tokens'),
|
|
262
|
+
maxTrivia: readIntegerOption(args, '--max-trivia'),
|
|
263
|
+
maxDirectives: readIntegerOption(args, '--max-directives'),
|
|
264
|
+
metadata: { cli: true }
|
|
265
|
+
})
|
|
266
|
+
: undefined;
|
|
267
|
+
return importNativeSource({
|
|
268
|
+
language,
|
|
269
|
+
parser: readOption(args, '--parser'),
|
|
270
|
+
sourcePath: file,
|
|
271
|
+
sourceHash,
|
|
272
|
+
sourceText: source,
|
|
273
|
+
sourcePreservation,
|
|
274
|
+
nativeAstMetadata: { sourceBytes: source.length }
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function readNativeImportForProjection(file, source, args) {
|
|
279
|
+
const parsed = tryParseJson(source);
|
|
280
|
+
if (!parsed) return importNativeFile(file, source, args);
|
|
281
|
+
if (parsed.kind === 'frontier.lang.universalAst') {
|
|
282
|
+
const nativeSource = parsed.nativeSources?.[0];
|
|
283
|
+
return {
|
|
284
|
+
id: parsed.metadata?.nativeImportId ?? `import_${idFragment(parsed.id)}`,
|
|
285
|
+
language: parsed.metadata?.sourceLanguage ?? nativeSource?.language ?? readOption(args, '--language') ?? inferLanguage(file),
|
|
286
|
+
sourcePath: parsed.metadata?.sourcePath ?? nativeSource?.sourcePath,
|
|
287
|
+
universalAst: parsed,
|
|
288
|
+
nativeSource,
|
|
289
|
+
nativeAst: nativeSource?.ast,
|
|
290
|
+
semanticIndex: parsed.semanticIndex,
|
|
291
|
+
sourceMaps: parsed.sourceMaps,
|
|
292
|
+
losses: parsed.losses,
|
|
293
|
+
evidence: parsed.evidence,
|
|
294
|
+
metadata: parsed.metadata ?? {}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return parsed;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function tryParseJson(source) {
|
|
301
|
+
const trimmed = source.trim();
|
|
302
|
+
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return undefined;
|
|
303
|
+
try {
|
|
304
|
+
return JSON.parse(trimmed);
|
|
305
|
+
} catch {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function isIgnoredCorpusDirectory(name) {
|
|
311
|
+
return name === 'node_modules' || name === '.git' || name === 'dist' || name === 'coverage' || name === '.next';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function isCorpusSourceFile(path) {
|
|
315
|
+
return /\.(frontier|[cm]?tsx?|m?jsx?|rs|py|c|h|hpp|cpp|cc|cxx|go|java|kt|cs|swift|php|rb|rake)$/i.test(path);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function idFragment(value) {
|
|
319
|
+
return String(value ?? 'unknown').replace(/[^A-Za-z0-9]+/g, '_').replace(/^_+|_+$/g, '').toLowerCase() || 'unknown';
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function help(io) { io.log('frontier-lang <parse|check|hash|ast|capabilities|to-json|from-json|import|project-native|native-coverage|roundtrip|corpus-roundtrip|emit|emit-ts|emit-js|emit-rust|emit-python|emit-c> <file> [--target target] [--language language] [--parser parser] [--platform platform] [--ast] [--sidecar] [--sidecar-only] [--source-only] [--stubs] [--out file] [--strict-effects]'); }
|
|
96
323
|
function readOption(args, flag) { const index = args.indexOf(flag); return index >= 0 ? args[index + 1] : undefined; }
|
|
324
|
+
function readIntegerOption(args, flag) {
|
|
325
|
+
const value = readOption(args, flag);
|
|
326
|
+
if (value === undefined) return undefined;
|
|
327
|
+
const parsed = Number.parseInt(value, 10);
|
|
328
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
329
|
+
}
|
|
330
|
+
function sourcePreservationOptionsRequested(args) {
|
|
331
|
+
return args.includes('--omit-source-text')
|
|
332
|
+
|| args.includes('--no-tokens')
|
|
333
|
+
|| args.includes('--no-trivia')
|
|
334
|
+
|| args.includes('--no-directives')
|
|
335
|
+
|| args.includes('--max-tokens')
|
|
336
|
+
|| args.includes('--max-trivia')
|
|
337
|
+
|| args.includes('--max-directives');
|
|
338
|
+
}
|
|
97
339
|
function inferLanguage(file) {
|
|
98
340
|
if (!file) return 'unknown';
|
|
99
341
|
if (/\.[cm]?tsx?$/.test(file)) return 'typescript';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shapeshift-labs/frontier-lang-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Command line interface for parsing, checking, hashing, and emitting Frontier Lang projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@shapeshift-labs/frontier-lang-checker": "0.3.3",
|
|
57
|
-
"@shapeshift-labs/frontier-lang-compiler": "0.2.
|
|
57
|
+
"@shapeshift-labs/frontier-lang-compiler": "0.2.10",
|
|
58
58
|
"@shapeshift-labs/frontier-lang-kernel": "0.3.3",
|
|
59
59
|
"@shapeshift-labs/frontier-lang-parser": "0.3.3"
|
|
60
60
|
},
|