@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
93 lines (81 loc) • 4.45 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 events = { event:1, action:1, function:1 }
const keep = o.keep ??= { entity:1, type:1, ...events }
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) => d.kind in events ? this.keep (c,d) : 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) => d.kind in keep ? this.keep (c,d) : skipped[c] = 0)
}
// Also keep remaining non-service entities
for (let [e,d] of all) if (d.kind === 'entity') {
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)
if (d.query) this.view (d.query)
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) {
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) return this.keep (_source(q.from.ref[0])) // 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))
function _source (r) { return r.id || r }
}
keep (n,d) {
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 parent = this.skipped[n]; if (parent) this.keep(parent) // keep initially skipped services
}
}
const _skip_service = (s,d) => d['@cds.external'] >= 2
const _skip_entity = (e,d) => d['@cds.external'] >= 2 || d['@cds.persistence.skip'] === 'if-unused' || e.endsWith('.texts')
exports.Minifier = Minifier