@@ -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