@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.
Files changed (3) hide show
  1. package/README.md +4 -1
  2. package/dist/index.js +252 -10
  3. 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
- return outputMaybeFile(io, rest, importNativeSource({
32
- language,
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 help(io) { io.log('frontier-lang <parse|check|hash|ast|capabilities|to-json|from-json|import|roundtrip|emit|emit-ts|emit-js|emit-rust|emit-python|emit-c> <file> [--target target] [--language language] [--parser parser] [--platform platform] [--ast] [--out file] [--strict-effects]'); }
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.7",
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.7",
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
  },