UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

113 lines (100 loc) 5.26 kB
module.exports = exports = (m,o) => m.meta?.minified ? m : (new Minifier) .minify (m,o) class Minifier { /** * Minifies a model by removing all definitions not reachable from given roots. * @param {object} [options] - Options for the minification as follows... * @param {kinds} [options.keep] Controls which children of services to keep. * @param {string[]} [options.cleanse] Names of properties or annotations to be removed. * @typedef {{ entity, aspect, type, event, action, function }} kinds * @returns {CSN.Model} the minified model with kept definitions only. */ minify (csn, options, { skip_unused } = global.cds.env.features) { const o = this.options = options || {} if (skip_unused === false) return csn if (skip_unused === 'services') o.services = 'all' const all = new Map (Object.entries( this.defs ??= csn.definitions || {} )) const children = (n,fn,pre=n+'.') => { for (let [n,d] of all) if (n.startsWith(pre)) fn (n,d) } const keep = (d,kinds) => d.kind in kinds || _can_minify(d) const _own = o.keep ??= { entity:1, type:1, event:1, action:1, function:1 } const _ext = { event:1, action:1, function:1 } const kept = this.kept = csn.definitions = {} const skipped = this.skipped = {} if (o.services) { // If o.services is specified, only keep matching services and their children const rx = o.services == 'all' || o.services == '/all/i' ? {test:()=>true} : o.services for (let [s,d] of all) if (d.kind === 'service' && rx.test(s)) { this.keep(s,d); children (s, (c,d) => this.keep(c,d)) } } else { // Otherwise first mark all external services and their children as initially skipped for (let [s,d] of all) if (d.kind === 'service' && _skip_service(s,d)) { skipped[s] = 0; children (s, (c,d) => keep(d,_ext) ? this.keep(c,d,s) : skipped[c] = s) // used later on in this.keep() } // Then keep all own services and their children for (let [s,d] of all) if (d.kind === 'service' && !(s in skipped)) { this.keep(s,d); children (s, (c,d) => keep(d,_own) ? this.keep(c,d) : skipped[c] = 0) } // Also keep remaining non-service entities for (let [e,d] of all) if (d.kind === 'entity' || !_can_minify(d)) { e in kept || e in skipped || _skip_entity(e,d) || this.keep(e,d) } } ;(csn.meta ??= {}) .minified = true return csn } cleanse (d, o = this.options, keep = this._keep ??= Object.keys (o.keep)) { for (let p in o.cleanse) { if (p[0] !== '@') delete d[p] // a single property for (let a in d) if (a.startsWith(p) && !keep.some(k => a.startsWith(k))) delete d[a] } } walk (d) { this.cleanse(d) if (d.target) this.keep (d.target) // has to go first w/o return for redirected targets if (d.type in this.defs) return this.keep (d.type) // return to avoid endless recursion if (d.type?.ref) return this.keep (d.type.ref[0]) // return to avoid endless recursion if (d.projection) this.view (d.projection,d) if (d.query) this.view (d.query,d) if (d.items) this.walk (d.items) if (d.returns) this.walk (d.returns) for (let e in d.elements) this.walk (d.elements[e]) for (let a in d.actions) this.walk (d.actions[a]) for (let p in d.params) this.walk (d.params[p]) for (let i in d.includes) this.keep (d.includes[i]) // Note: this ^^^^^^^^^^^^ is required for cdsc.recompile; with delete d.includes, redirects in AFC broke } view (q,v) { if (q.SELECT) q = q.SELECT // i.e. entity as select from ... if (q.mixin) for (let e in q.mixin) this.walk (q.mixin[e]) if (q.from?.ref) { let r = q.from.ref[0]; if (r.id) r = r.id; if (r in this.kept) return if (v?.['@cds.minify'] === 'unused-elements') return this.keep_columns (q) else return this.keep (r) // keep sources of views } if (q.from?.join) return q.from.args.forEach (from => this.view ({from})) if (q.SET) return q.SET.args.forEach (q => this.view (q.SELECT||q)) } keep_columns (q) { let r = q.from.ref[0]; if (r.id) r = r.id; if (r in this.kept) return let e = this.defs[r]; if (!e) return let all = e.elements, kept = {} if (q.columns) _keep (q.columns) if (q.where) _keep (q.where) e.elements = kept return this.keep (r,e) function _keep (xx) { for (let x of xx) { if (x.func) { _keep (x.args); continue } if (x.xpr) { _keep (x.xpr); continue } if (x.ref) { let [r] = x.ref; if (r in all) kept[r] = all[r] } }} } keep (n,d,parent) { if (n in this.kept) return; else d ??= this.defs[n] if (d) this.walk (this.kept[n] = d, n); else return let texts = this.defs[n+'.texts']; if (texts) this.keep (n+'.texts', texts) let p = parent ?? this.skipped[n]; if (p) this.keep(p) // keep initially skipped services } } const _skip_service = (s,d) => (d['@cds.external'] >= 2) && _can_minify(d) const _skip_entity = (e,d) => (d['@cds.external'] >= 2 || d['@cds.persistence.skip'] === 'if-unused') && _can_minify(d) || e.endsWith('.texts') const _can_minify = d => d['@cds.minify'] !== false exports.Minifier = Minifier