Skip to content

Commit e31fccc

Browse files
authored
Merge pull request #19 from api-components/feat/source-maps-option
@W-21680934 feat: add sourceMaps option to ApiDefinition
2 parents 07d11a8 + a5b5723 commit e31fccc

6 files changed

Lines changed: 491 additions & 338 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ If the value is an array then first element must be API format and second is API
2929
`ApiDefinition.type``String`. API type to process. Can be `RAML 0.8`, `RAML 1.0`, `OAS 2.0`, `OAS 3.0`, `ASYNC 2.0`, or `GRPC`.
3030
`ApiDefinition.mime``String`. API media type. Default to `application/yaml`. For GRPC use `application/x-protobuf`.
3131
`ApiDefinition.resolution``String`. AMF resolution pipeline. Default to `editing` which is the original resolution pipeline for API Console. Future releases of AMF can support different options.
32+
`ApiDefinition.flattened``Boolean`. When `true`, generates a compact JSON-LD model using `@graph` instead of an expanded array. Recommended for large APIs. Default to `false`.
33+
`ApiDefinition.sourceMaps``Boolean`. Controls source maps in the **full** model (`<name>.json`) only. When `false`, source map nodes are omitted — useful when no editing tooling will consume this API. The compact model (`<name>-compact.json`) never includes source maps. Default to `true`.
34+
35+
### Output files
36+
37+
Each API produces two files:
38+
39+
| File | URIs | Source maps | Intended consumer |
40+
|------|------|-------------|-------------------|
41+
| `<name>.json` | Expanded (`http://a.ml/...`) | Yes (controlled by `sourceMaps`) | Editing tools, validators |
42+
| `<name>-compact.json` | Compact (`apiContract:WebAPI`) | Never | API Console, display/browsing |
3243

3344

3445
### Example apis.json
@@ -44,6 +55,12 @@ If the value is an array then first element must be API format and second is API
4455
"type": "RAML 1.0",
4556
"mime": "application/raml",
4657
"resolution": "editing"
58+
},
59+
"grpc/service.proto": {
60+
"type": "GRPC",
61+
"mime": "application/protobuf",
62+
"flattened": true,
63+
"sourceMaps": false
4764
}
4865
}
4966
```

index.js

Lines changed: 95 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,50 @@ const {
1212
PipelineId,
1313
} = amf;
1414

15+
/**
16+
* Strips AMF source-map nodes and inline source-map properties from a
17+
* serialised JSON-LD @graph string.
18+
*
19+
* AMF v5 only includes declared types (doc:declares) in the @graph when
20+
* withSourceMaps() is used. For the compact model we want declares without
21+
* the source-map noise, so we generate with source maps and then strip them.
22+
*
23+
* @param {string} data Raw JSON-LD string
24+
* @returns {string} Stripped JSON-LD string
25+
*/
26+
function stripSourceMaps(data) {
27+
const obj = JSON.parse(data);
28+
const graph = obj['@graph'];
29+
if (!Array.isArray(graph)) return data;
30+
31+
const filtered = graph.filter((node) => {
32+
const rawTypes = node['@type'];
33+
const joined = Array.isArray(rawTypes) ? rawTypes.join(',') : String(rawTypes || '');
34+
if (joined.includes('SourceMap') || joined.includes('source-maps')) return false;
35+
const id = node['@id'];
36+
if (typeof id === 'string' && id.includes('source-map')) return false;
37+
return true;
38+
});
39+
40+
const sourceMapProps = [
41+
'http://a.ml/vocabularies/document-source-maps#sources',
42+
'http://a.ml/vocabularies/document-source-maps#lexical',
43+
'sourcemaps:sources',
44+
'sourcemaps:lexical',
45+
'sourcemaps:synthesized-field',
46+
'sourcemaps:grpc-raw-proto',
47+
'sourcemaps:element',
48+
'sourcemaps:value',
49+
];
50+
for (const node of filtered) {
51+
for (const prop of sourceMapProps) {
52+
delete node[prop];
53+
}
54+
}
55+
56+
return JSON.stringify({ ...obj, '@graph': filtered });
57+
}
58+
1559
/** @typedef {import('./types').ApiConfiguration} ApiConfiguration */
1660
/** @typedef {import('./types').FilePrepareResult} FilePrepareResult */
1761
/** @typedef {import('./types').ApiGenerationOptions} ApiGenerationOptions */
@@ -40,56 +84,85 @@ function getConfiguration(type) {
4084
/**
4185
* Generates json/ld file from parsed document using AMF v5 API.
4286
*
87+
* Produces two output files:
88+
* - `<name>.json` — full model, expanded URIs, source maps controlled by `sourceMaps`
89+
* - `<name>-compact.json` — compact URIs, never includes source maps (optimized for display)
90+
*
4391
* @param {string} sourceFile
4492
* @param {string} file
4593
* @param {string} type
4694
* @param {string} destPath
4795
* @param {string} resolution
4896
* @param {boolean} flattened
97+
* @param {boolean} sourceMaps
4998
* @return {Promise<void>}
5099
*/
51-
async function processFile(sourceFile, file, type, destPath, resolution, flattened) {
100+
async function processFile(sourceFile, file, type, destPath, resolution, flattened, sourceMaps) {
52101
let dest = `${file.substr(0, file.lastIndexOf('.')) }.json`;
53102
if (dest.indexOf('/') !== -1) {
54103
dest = dest.substr(dest.lastIndexOf('/'));
55104
}
56105

57-
// Setup render options
58-
let renderOpts = new RenderOptions().withSourceMaps().withCompactUris();
59-
if (flattened) {
60-
renderOpts = renderOpts.withCompactedEmission();
61-
}
62-
63-
// Get configuration for API type
64-
const apiConfiguration = getConfiguration(type).withRenderOptions(renderOpts);
65-
const client = apiConfiguration.baseUnitClient();
66-
67-
// Parse the file
68-
const parseResult = await client.parse(sourceFile);
106+
// Parse using a base client (render options don't affect parsing).
107+
// AMF v5 mutates the baseUnit during transform/render so we must parse twice —
108+
// once for the full model and once for the compact model — to get independent
109+
// base units.
110+
const parseClient = getConfiguration(type).baseUnitClient();
111+
const parseResult = await parseClient.parse(sourceFile);
69112

70113
if (!parseResult.conforms) {
71114
/* eslint-disable-next-line no-console */
72115
console.log('Validation warnings/errors:');
73116
console.log(parseResult.toString());
74117
}
75118

76-
// Transform using resolution pipeline
77119
const pipelineId = resolution === 'editing' ? PipelineId.Editing : PipelineId.Default;
78-
const transformed = client.transform(parseResult.baseUnit, pipelineId);
120+
const transformed = parseClient.transform(parseResult.baseUnit, pipelineId);
121+
122+
// Fresh parse for compact model (avoids base-unit mutation by the full render above)
123+
const parseClientForCompact = getConfiguration(type).baseUnitClient();
124+
const parseResultForCompact = await parseClientForCompact.parse(sourceFile);
125+
const transformedForCompact = parseClientForCompact.transform(parseResultForCompact.baseUnit, pipelineId);
79126

80-
// Render to JSON-LD
81127
const fullFile = path.join(destPath, dest);
82128
const compactDest = dest.replace('.json', '-compact.json');
83129
const compactFile = path.join(destPath, compactDest);
84130

85-
// Generate full model (same as compact in v5 with withCompactUris)
86-
const modelData = await client.render(transformed.baseUnit, 'application/ld+json');
131+
// Full model: expanded URIs, source maps controlled by option (for editing tooling)
132+
// withoutCompactedEmission() is required: AMF v5 defaults to @graph (compacted emission),
133+
// but consumers like amf-loader.ts expect the old array format [{"@id": "amf://id", ...}].
134+
// Only flattened models intentionally use @graph.
135+
let fullRenderOpts = new RenderOptions().withoutCompactedEmission();
136+
if (sourceMaps) {
137+
fullRenderOpts = fullRenderOpts.withSourceMaps();
138+
}
139+
if (flattened) {
140+
fullRenderOpts = fullRenderOpts.withCompactedEmission();
141+
}
142+
const fullData = await getConfiguration(type)
143+
.withRenderOptions(fullRenderOpts)
144+
.baseUnitClient()
145+
.render(transformed.baseUnit, 'application/ld+json');
87146

88147
await fs.ensureFile(fullFile);
89-
await fs.writeFile(fullFile, modelData, 'utf8');
148+
await fs.writeFile(fullFile, fullData, 'utf8');
149+
150+
// Compact model: same as full model but with source maps stripped after rendering.
151+
// AMF v5 only includes doc:declares (type definitions) when withSourceMaps() is
152+
// active, so we must generate with source maps and remove them in post-processing.
153+
// withCompactUris() is omitted: it also drops declared types from the @graph.
154+
let compactRenderOpts = new RenderOptions().withSourceMaps().withoutCompactedEmission();
155+
if (flattened) {
156+
compactRenderOpts = compactRenderOpts.withCompactedEmission();
157+
}
158+
const compactRaw = await getConfiguration(type)
159+
.withRenderOptions(compactRenderOpts)
160+
.baseUnitClient()
161+
.render(transformedForCompact.baseUnit, 'application/ld+json');
162+
const compactData = flattened ? compactRaw : stripSourceMaps(compactRaw);
90163

91164
await fs.ensureFile(compactFile);
92-
await fs.writeFile(compactFile, modelData, 'utf8');
165+
await fs.writeFile(compactFile, compactData, 'utf8');
93166
}
94167

95168
/**
@@ -127,9 +200,9 @@ async function parseFile(file, cnf, opts) {
127200
if (!dest.endsWith('/')) {
128201
dest += '/';
129202
}
130-
const { type, mime='application/yaml', resolution='editing', flattened = false } = normalizeOptions(cnf);
203+
const { type, mime='application/yaml', resolution='editing', flattened = false, sourceMaps = true } = normalizeOptions(cnf);
131204
const sourceFile = `file://${src}${file}`;
132-
return processFile(sourceFile, file, type, dest, resolution, flattened);
205+
return processFile(sourceFile, file, type, dest, resolution, flattened, sourceMaps);
133206
}
134207

135208
/**

0 commit comments

Comments
 (0)