@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
113 lines (100 loc) • 5.26 kB
JavaScript
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