UNPKG

35.2 kBJavaScriptView Raw
1const { Replaceable, replace } = require('../stdlib');
2const { ensurePath, write, exists, read } = require('../stdlib');
3const { clone } = require('../stdlib');
4const { ensurePathSync } = require('../stdlib');
5const { copyFileSync } = require('fs');
6const { resolve, join, dirname, relative, basename, isAbsolute } = require('path');
7const { c } = require('../stdlib');
8const { writeFileSync, lstatSync, readFileSync, existsSync } = require('fs');
9const { differently } = require('../stdlib');
10const { deepStrictEqual } = require('assert');
11const { controlStyle } = require('../stdlib');
12const { compileStylesheetsSync } = require('../stdlib');
13const { resolveInternal, stat, resolveFile, CLOSURE_STYLESHEETS } = require('./lib');
14const { log, error } = require('./lib/logging');
15const { render } = require('../stdlib');
16const { aqt } = require('../stdlib');
17const { sipsResolution } = require('./elements/img/lib');
18const makeClassGetter = require('./lib/make-class-getter');
19const { wrapSlash } = require('./lib/resource-stream');
20
21let writingCache = Promise.resolve()
22
23/**
24 * The Splendid object unique for each page, that contains its metadata, including assets, used components, _etc_.
25 */
26class Splendid {
27 /**
28 * @param {Object} options
29 * @param {string} [options.env] The environment for which Splendid is run.
30 * @param {import('..').Page} [options.page] The page being processed.
31 * @param {Array<import('..').Page>} [options.pages] The array of all pages.
32 * @param {import('..').Config} [options.config] The configuration object.
33 * @param {import('./App').default} [options.app] The app.
34 */
35 constructor({
36 env, page, pages, config, cache = {}, app,
37 }) {
38 if (!app) throw new Error('The app is required.')
39 if (!config) throw new Error('Config is required.')
40 if (!page) throw new Error('Page is required.')
41 if (!page.key) throw new Error('Page must have a key.')
42 /**
43 * This is used to wrap Ajax content in a div. Only use when there's additional markup around {{ content }} in a `div id="Content"` element.
44 */
45 this._ajaxWrapper = null
46
47 /**
48 * When called from components, this will return the name of the current file being processed.
49 * @return {string}
50 */
51 this.currentFile = null
52 /**
53 * The environment for which Splendid is run.
54 */
55 this.env = env
56 /**
57 * The current page being processed.
58 */
59 this.page = page
60 /**
61 * The array of all pages in the app.
62 */
63 this.pages = pages
64 /**
65 * The configuration object.
66 */
67 this._config = config
68 /**
69 * The reference to the content stream.
70 * @type {import('./lib/streams/ContentStream').default}
71 */
72 this.contentStream = null
73 this.cache = cache
74
75 /**
76 * The styles added by the content and layout.
77 */
78 this._styles = []
79 /**
80 * The styles added by the content and layout for NOJS version.
81 * @type {{ href: string }[]}
82 */
83 this._noJSstyles = []
84 /**
85 * The styles added by components to be inlined.
86 * @type {!Array<string>}
87 */
88 this._inlineStyles = []
89 /**
90 * The scripts and modules that are to be added before the closing body tag.
91 * @type {Array<{ src: string, type: string, 'data-onload': boolean, 'nocompile': boolean }>}
92 */
93 this._scripts = []
94 /**
95 * The links that should be added to the page as `<link href="..." props>`
96 * @type {{ href: string }[]}
97 */
98 this._links = []
99 this._externs = []
100 /**
101 * The inline scripts.
102 * @type {Array<{ js: string, type: string }>}
103 */
104 this._js = []
105 this._components = []
106 /** All rename maps of CSS added by the components for compilation. */
107 this._css = {}
108 /** Rename maps that are exported for components. */
109 this._renameMaps = {}
110
111 /**
112 * Strings that should be appended before the body end.
113 */
114 this.preBodyCode = []
115
116 const splendidDir = dirname(require.resolve('splendid/package.json'))
117 /**
118 * This is required to construct the components invocation in layout stream by
119 * calling competent.makeComps().
120 */
121 this.componentsMap = Object.entries(app.map).reduce((acc, [key, val]) => {
122 let newKey
123 if (key.startsWith(splendidDir)) {
124 const inSplendid = key.replace(splendidDir, '') // e.g., /src or /build
125 if (inSplendid.startsWith('/build')) {
126 newKey = join('splendid', inSplendid) // already prepared transpiled JSX 👍
127 .replace(/\.js$/, '')
128 } else if (inSplendid.startsWith('/src') && !process.env.SPLENDID_SRC) {
129 newKey = join('splendid', inSplendid.replace('/src', '/build'))
130 .replace(/\.jsx?$/, '')
131 } else { // local linking Splendid case
132 // have to use paths because
133 // JSX in node_modules is not transpiled
134 const rel = relative(join(config.appDir, 'comps', dirname(this.page.key)), 'node_modules/splendid')
135 newKey = key.replace(splendidDir, rel)
136 }
137 } else newKey = relative(resolve(
138 config.appDir, 'comps', dirname(this.page.key)
139 ), key)
140 acc[newKey] = val
141 return acc
142 }, {})
143 /**
144 * The unprocessed map with real locations.
145 */
146 this._realComponentsMap = app.map
147
148 this._writingCache = Promise.resolve()
149 this._seed = 1
150
151 this.polyfills = {}
152
153 /**
154 * The app holding replacements.
155 */
156 this.app = app
157
158 /**
159 * Whether the styles should be combined across the whole app.
160 * Set by the `combine-css` components.
161 */
162 this.combineStyles = false
163
164 /**
165 * The code to be executed after ajax requests.
166 */
167 this._postAjax = []
168
169 this.usedComponents = {} // the map
170 // @todo: map components, elements, blocks
171 /**
172 * @type {Array<{re, replacement}>}
173 */
174 this.replacements = [...app.areaReplacements, ...app.replacements]
175
176 this.headings = []
177 /**
178 * What selectors to hide with `no-js` class on html.
179 */
180 this.hiddenNoJs = []
181
182 /**
183 * Scripts that should be combined into a bundle (imported in comps file).
184 */
185 this.combinedScripts = []
186 /**
187 * Replacements that will be put back at the very end so that they don't participate in rules.
188 * @type {string[]}
189 */
190 this.deferredReplacements = []
191 /**
192 * Rules added by components for post processing.
193 */
194 this.postProcessRules = {}
195 /**
196 * Init scripts that need to be invoked.
197 */
198 this._initComponents = []
199 /**
200 * Which parts of the big chunk of incoming data correspond
201 * to what input file.
202 */
203 this._areas = []
204 this._ids = {}
205
206 /**
207 * Whether Bootstrap selectors should be renamed.
208 */
209 this._renameBootstrap = false
210 /**
211 * Whether selectors from Bootstrap should be dropped.
212 */
213 this._dropBootstrap = false
214 /**
215 * @type {string}
216 * Whether to embed combined CSS on the page. The `combine-css` component will set this to a marker
217 * that will be replaced after after all pages are compiled.
218 */
219 this._embedCombined = null
220
221 /**
222 * Scripts in the head element.
223 */
224 this.headInlineScripts = []
225
226 /**
227 * The size of the root content column.
228 */
229 this.colSize = null
230 }
231 /**
232 * Manually replace (render) a string block.
233 * @param {string} data The block to replace.
234 * @param {Array<Replacement>} [replacements] The rules.
235 */
236 async replace(data, replacements = this.replacements) {
237 const rs = new Replaceable(replacements)
238 rs.splendid = this
239 const r = await replace(rs, data)
240 return r
241 }
242 /**
243 * @param {string} data
244 * @param {Splendid} splendid
245 */
246 static async replace(data, splendid) {
247 const rs = new Replaceable(splendid.replacements)
248 const s = new Splendid({
249 env: splendid.env,
250 config: splendid.config,
251 page: splendid.page,
252 pages: splendid.pages,
253 app: splendid.app,
254 })
255 rs.splendid = s
256 const r = await replace(rs, data)
257 return r
258 }
259 /**
260 * Returns the actual deferred value based on a marker.
261 * @param {string} value
262 */
263 getDeferred(value) {
264 const r = value.startsWith('SPLENDID-DEFER-')
265 if (!r) return value
266 const [i] = /\d+/.exec(value)
267 const v = this.deferredReplacements[i]
268 return v
269 }
270 get controlStyle() {
271 return controlStyle
272 }
273 get resolveInternal() {
274 return resolveInternal
275 }
276 /**
277 * Adds a link to the head tag.
278 */
279 addLink(link) {
280 const h = JSON.stringify(link)
281 if (this._links.some((a) => JSON.stringify(a) == h)) return
282 this._links.push(link)
283 }
284 addPostProcessRule(name, rule) {
285 this.postProcessRules[name] = rule
286 }
287 addHeading({ level, title }) {
288 const id = Splendid.getId(title)
289 this.headings.push({ level, title, id })
290 return id
291 }
292 /**
293 * Creates a string that is safe to put into the `id` attribute.
294 * @param {string} title The string to convert into ID.
295 */
296 static getId(title) {
297 const id = title.toLocaleLowerCase()
298 .replace(/<\/?\w+\s+[\s\S]+?>/g, '')
299 .replace(/\s+/g, '-')
300 .replace(/([^-\w\d]|_)/g, '')
301 return id
302 }
303 /**
304 * Hide this selector when no-js is present. This will add
305 * `<script>document.documentElement.classList.remove("no-js")</script>`
306 * in the head.
307 * @param {string} selector The selector to hide.
308 */
309 hideNoJs(selector) {
310 if (!this.hiddenNoJs.includes(selector)) this.hiddenNoJs.push(selector)
311 }
312 get config() {
313 return this._config
314 // return {
315 // bundle: {
316 // css: 'combine-all',
317 // ...this._config.bundle || {},
318 // },
319 // ...this._config,
320 // }
321 }
322 random(n = 5) {
323 const nn = Math.pow(10, n)
324 let x = Math.sin(this._seed++) * nn
325 return Math.floor((x - Math.floor(x)) * nn)
326 }
327 /**
328 * Returns html-save id with stripped tags.
329 * @param {string} title Some string to convert into ID.
330 */
331 getId(title) {
332 return Splendid.getId(title)
333 }
334 /**
335 * @param {string} path The path to the file to add.
336 * @param {boolean} [nocache] Bypass cache.
337 */
338 async addFile(path, nocache = this.config.nocache) {
339 if (path in this.cache) return
340 const { source, file } = resolveFile(path, this.config.appDir, this.page.dir)
341 this.cache[path] = file
342 const exi = await exists(source)
343 const output = this.getDocPath(file)
344 if (!exi) {
345 const docExi = await exists(output)
346 if (!docExi) {
347 this.logError2('addFile', 'File %s was not added as it does not exist',
348 c(relative('', source), 'red'))
349 } else {
350 // const e = await compareFiles(source, j) // could this be cached better
351 // if (e) return
352 }
353 return
354 }
355
356 const mtime = await this.getLocaleMtime(source)
357 const m = this.getCache('add-file', source)
358 if (!nocache && m == mtime) return
359
360 await clone(source, dirname(output))
361 this.log2('addFile', relative('', source))
362 if (!nocache) await this.appendCache('add-file', { [source]: mtime })
363 return ''
364 }
365 /**
366 * Add multiple files at once.
367 * @param {string[]} paths Paths to files.
368 * @param {boolean} nocache Don't use `add-files.json` cache.
369 */
370 async addFiles(paths, nocache = this.config.nocache) {
371 const p = paths.filter((path) => {
372 return !(path in this.cache)
373 })
374 const sources = (await Promise.all(p.map(async (path) => {
375 const { source, file } = resolveFile(path, this.config.appDir, this.page.dir)
376 this.cache[path] = file
377 const exi = await exists(source)
378 const output = this.getDocPath(file)
379 if (!exi) {
380 this.logError2('addFile', 'File %s was not added as it does not exist',
381 c(relative('', source), 'red'))
382 return
383 }
384 const s = relative('', source)
385 return { source: s, output, date: new Date(exi.mtime).toLocaleString() }
386 }))).filter(Boolean).reduce((acc, { source, output, date }) => {
387 acc[source] = { output, date }
388 return acc
389 }, {})
390 const mtimes = Object.entries(sources).reduce((acc, [key, { date }]) => {
391 acc[key] = date
392 return acc
393 }, {})
394 const cache = this.getCache('add-files')
395 const pp = nocache ? mtimes : Object.entries(mtimes).reduce((acc, [key, val]) => {
396 const current = cache[key]
397 if (current == val) return acc
398 acc[key] = val
399 return acc
400 }, {})
401 await Promise.all(Object.keys(pp).map(async source => {
402 const { output } = sources[source]
403 await clone(source, dirname(output))
404 this.log2('addFile', source)
405 }))
406 if (!nocache) await this.appendCache('add-files', pp)
407 }
408 /**
409 * Escapes the >, < and & in the string wit HTML entities.
410 * @param {string} string
411 */
412 escapeHTML(string = '') {
413 return string
414 .replace(/&/g, '&amp;')
415 .replace(/</g, '&lt;')
416 .replace(/>/g, '&gt;')
417 }
418 /**
419 * @param {string} path The path to the file to add.
420 * @param {boolean} [nocache] Bypass cache.
421 */
422 addFileSync(path, nocache = this.config.nocache) {
423 if (path in this.cache) return
424 const { source, file } = resolveFile(path, this.config.appDir)
425 this.cache[path] = file
426 const exi = existsSync(source)
427 const output = this.getDocPath(file)
428 if (!exi) {
429 const docExi = existsSync(output)
430 if (!docExi) {
431 this.logError2('addFile', 'File %s was not added as it does not exist',
432 c(relative('', source), 'red'))
433 } else {
434 // const e = await compareFiles(source, j) // could this be cached better
435 // if (e) return
436 }
437 return
438 }
439
440 const mtime = this.getLocaleMtimeSync(source)
441 const m = this.getCache('add-file', source)
442 if (!nocache && m == mtime) return
443
444 ensurePathSync(output)
445 copyFileSync(source, output)
446 this.log2('addFile', relative('', source))
447 if (!nocache) this.appendCacheSync('add-file', { [source]: mtime })
448 return ''
449 }
450 // /**
451 // * @param {string} path
452 // */
453 // normalise(path) {
454 // if (path.startsWith(process.cwd())) {
455 // return relative(resolve(this.getPath('')), path)
456 // }
457 // return path
458 // }
459 /**
460 * Returns the change mtime in local date format.
461 * @param {string} path
462 */
463 async getLocaleMtime(path) {
464 const s = await stat(path)
465 const mtime = s.mtime.toLocaleString()
466 return mtime
467 }
468 /**
469 * Returns the change mtime in local date format.
470 * @param {string} path
471 */
472 getLocaleMtimeSync(path) {
473 const s = lstatSync(path)
474 const mtime = s.mtime.toLocaleString()
475 return mtime
476 }
477 /**
478 * If the path starts with `.`, the return will be path relative to current file being processed in the splendid dir.
479 * @param {string} src
480 * @return {string}
481 */
482 resolveRelative(src) {
483 // implemented by SplendidProxy in components/index.js
484
485 }
486 _resolveRelative(src, position) {
487 if (!src) return src
488 if (src.startsWith('~/')) {
489 src = relative(this.config.appDir, src.replace('~/', ''))
490 } else if (src.startsWith('.')) {
491 if (!position) {
492 this.logError2('resolve-relative', 'no position to resolve %s', src)
493 return src
494 }
495 const currentFile = this._getCurrentFile(position)
496 const thePath = join(dirname(currentFile), src)
497 src = relative(this.config.appDir, thePath)
498 }
499 return src
500 }
501 /**
502 * Marks the component for export and compilation as a JS Preact component on the page.
503 */
504 addComponent(key, id, props, children) {
505 this._components.push({ key, id, props, children })
506 }
507 /**
508 * Use `style` instead.
509 * @depreacted
510 */
511 addStyle(...args) {
512 this.logError('.addStyle is deprecated. Use .style instead.')
513 return this.style(...args)
514 }
515 /**
516 * Adds a style that should be combined together with other styles.
517 * @param {string} path The location of the stylesheet.
518 */
519 style(path) {
520 if (typeof path != 'string') throw new Error('A string only is expected.')
521 // const style = typeof href == 'string' ? { href } : href
522 // Object.assign(style, props)
523
524 // if (style.preload) return this.preload(style)
525
526 // const h = JSON.stringify(style)
527 // if (this._styles.some((a) => JSON.stringify(a) == h)) return
528 if (this._styles.includes(path)) return
529 this._styles.push(path)
530 }
531 /**
532 * Add a `<link href="{href}" rel="preload" as="{as}" ...props>` element to the head.
533 * @param {Object|string} link The link as string or hash with href and other param.
534 * @param {string} [as] The as attribute. Can be omitted for css and js files.
535 */
536 preload(link, as) {
537 if (typeof link == 'string') link = { href: link }
538 if (!as && !link.as) {
539 if (link.href.endsWith('.css')) as = 'style'
540 else if (link.href.endsWith('.js')) as = 'script'
541 else throw new Error('The "as" attribute must be specified for a preload link.')
542 }
543 link.rel = 'preload'
544 if (!link.as) link.as = as
545 return this.addLink(link)
546 }
547 /**
548 * @param {Object|string} href The stylesheet as string or hash with href and other param.
549 * @param {Object} props Additional properties.
550 */
551 stylesheet(href, props = {}) {
552 const link = typeof href == 'string' ? { href } : href
553 Object.assign(link, props)
554 link.rel = 'stylesheet'
555 return this.addLink(link)
556 }
557 /**
558 * Add the external style via the link element wrapped around <noscript>
559 * @param {string} href
560 */
561 addNoJSStyle(href, props = {}) {
562 const link = typeof href == 'string' ? { href } : href
563 Object.assign(link, props)
564 link.rel = 'stylesheet'
565
566 const h = JSON.stringify(link)
567 if (this._noJSstyles.some((a) => JSON.stringify(a) == h)) return
568 this._noJSstyles.push(link)
569 }
570 /**
571 * Add the style by inlining it inside of the head element.
572 * @param {string} style
573 */
574 addInlineStyle(style) {
575 if (!this._inlineStyles.includes(style))
576 this._inlineStyles.push(style)
577 }
578 /**
579 * Read the style, compile it and inline in the head element.
580 * @param {string} path The path to the css inside the splendid app dir.
581 * @param {Object} props Properties.
582 * @param {string} [props.rootSelector] The root selector for each ruleset.
583 */
584 inlineCSS(path, props = {}) {
585 const { rootSelector } = props
586 const { source } = isAbsolute(path) ? { source: path } : resolveFile(path, this.config.appDir)
587
588 const mtime = this.getLocaleMtimeSync(source)
589 const { mtime: m, data } = this.getCache('inline-stylesheet', path)
590 if (m == mtime) return this.addInlineStyle(data)
591
592 this.log2('inlineCSS', 'Compiling %s', c(source, 'magenta'))
593
594 const { status, stderr, stylesheet, block } = compileStylesheetsSync(source, {
595 path: CLOSURE_STYLESHEETS,
596 allowUnrecognizedProperties: true,
597 prettyPrint: this.config.prettyCombineCSS,
598 expandBrowserPrefix: true,
599 rootSelector,
600 rename: null,
601 })
602 if (status) {
603 throw new Error(
604 `Could not process inline stylesheet:\n${block || stderr}.`)
605 } else if (stylesheet) {
606 this.addInlineStyle(stylesheet)
607 this.appendCacheSync('inline-stylesheet', { [path]: { mtime, data: stylesheet } })
608 }
609 }
610
611 /**
612 * @param {string} key
613 * @param {Object} props
614 */
615 _addInitComponent(key, props) {
616 this._initComponents.push({ key, props })
617 }
618 addCSS(...args) {
619 this.logError('.addCSS is deprecated. Use .css instead.')
620 return this.css(...args)
621 }
622 /**
623 * This method is used by components to build the css.
624 * @param {string} stylesheetPath The path to the stylesheet.
625 * @param {string} [rootSelector] The root selector to start each ruleset with.
626 * @param {Object} [opts]
627 * @param {Array<string>|string} opts.whitelist Class names to whitelist.
628 * @param {boolean} opts.exported Whether the component is exported and requires the rename map.
629 * @param {boolean} [opts.dynamic=false] Whether the component will load the stylesheet manually (implementation must be provided).
630 * @param {boolean} [opts.link=false] If the stylesheet will be included as a link in the head tag.
631 * @param {boolean} [opts.inline=false] Compile and inline CSS in the head of the document.
632 * @param {boolean} [opts.combined=true] Whether the style should be included on the page (default `true`).
633 */
634 css(stylesheetPath, rootSelector = null, opts = {}) {
635 // 0. page-level cached already processed rename maps
636 if (stylesheetPath in this._css) return this._css[stylesheetPath]
637
638 let { whitelist, exported = true, inline, dynamic,
639 link, combined = !(dynamic || link || inline), preload = false,
640 allowUnrecognizedProperties,
641 } = opts
642 if (inline) return this.inlineCSS(stylesheetPath, { rootSelector })
643
644 const { source: path, file } = resolveFile(stylesheetPath, this.config.appDir)
645
646 if (whitelist && !Array.isArray(whitelist)) whitelist = [whitelist]
647
648 const name = basename(file)
649 const css = join('css', name) // create a name for file in splendid app dir
650
651 if (dynamic) this.addNoJSStyle(css) // components will load it dynamically.
652 else if (link) this.stylesheet(css) // add link to page
653 else if (combined) this.style(css) // adds style to the combined.css
654 if (preload) this.preload(css, 'style')
655
656 // 1. check cache
657 const cache = /** @type {{renameMap: Object, mtime: string}} */
658 (this. getCache('css', stylesheetPath))
659 const mtime = this.getLocaleMtimeSync(path)
660 if (cache.mtime == mtime && JSON.stringify(cache.whitelist) == JSON.stringify(whitelist)) {
661 const d = makeClassGetter(cache.renameMap)
662 this._css[stylesheetPath] = d
663 if (exported) this._renameMaps[stylesheetPath] = cache.renameMap
664 return d
665 }
666
667 // 2. compile
668
669 this.log2('addCss', 'Compiling %s', c(path, 'magenta'))
670
671 const { renameMap, stylesheet, status, stderr } = compileStylesheetsSync(path, {
672 path: CLOSURE_STYLESHEETS,
673 ...this.config.stylesheets,
674 whitelist,
675 rootSelector,
676 expandBrowserPrefix: dynamic, // otherwise will be expanded during combine stage
677 allowUnrecognizedProperties,
678 // rename: 'SIMPLE',
679 // whitelist: whitelist inthis.config.stylesheets.
680 })
681
682 if (status) {
683 throw this.newError(`Could not compile CSS ${stylesheetPath}\n${stderr}`)
684 }
685
686 this.writeAppSync(css, stylesheet)
687
688 // if (dynamic) writeFileSync(join('docs/css', name), stylesheet) ResourceStream will pick this up from nojs styles block
689
690 const d = makeClassGetter(renameMap)
691 this._css[stylesheetPath] = d
692
693 if (exported) {
694 writeRenameMap(this, stylesheetPath, renameMap)
695 this._renameMaps[stylesheetPath] = renameMap
696 }
697
698 this.appendCacheSync('css', {
699 [stylesheetPath]: {
700 mtime,
701 renameMap,
702 ...(whitelist ? { whitelist } : {}),
703 },
704 })
705
706 return d
707 }
708 deferRender(v) {
709 const res = render(v)
710 return this.deferReplacement(res)
711 }
712 newError(text) {
713 const err = new Error(c(text, 'red'))
714 err.stack = err.message
715 return err
716 }
717 deferReplacement(value) {
718 this.deferredReplacements.push(value)
719 return `SPLENDID-DEFER-${this.deferredReplacements.length - 1}`
720 }
721 /**
722 * Adds the polyfill to the build.
723 * @param {string} name Available polyfills are: 'intersection-observer'.
724 * @param {boolean} [combine] Whether to include into the PageComp rather that script tag.
725 */
726 polyfill(name, combine = false) {
727 if (this.polyfills[name]) return
728 if (name == 'intersection-observer') {
729 this.script('splendid://js/polyfill/intersection-observer.js', false, {}, combine)
730 this.addFile('splendid://js/polyfill/intersection-observer.js.map')
731 // this.addJs(`if (!('IntersectionObserver' in window)) {
732 // var el = document.createElement('script')
733 // el.src = 'js/polyfill/intersection-observer.js'
734 // }`, false, { id: 'io-polyfill' })
735 this.polyfills[name] = true
736 } else if (name == 'closest') {
737 this.script('splendid://js/polyfill/closest.js', false, {}, combine)
738 this.polyfills[name] = true
739 } else if (name == 'replace-with') {
740 this.script('splendid://js/polyfill/replace-with.js', false, {}, combine)
741 this.polyfills[name] = true
742 } else if (name == 'object-assign') {
743 this.script('splendid://js/polyfill/object.assign.js', false, {}, combine)
744 this.polyfills[name] = true
745 }
746 // if (this.polyfills[name])
747 // console.log('Successfully added %s polyfill', c(name, 'green'))
748 // else
749 // console.log('Did not add %s polyfill', c(name, 'red'))
750 }
751 /**
752 * Returns the path to the file inside of the app dir.
753 * @param {string} path
754 * @param {...string} args
755 */
756 getPath(path, ...args) {
757 return join(this.config.appDir, path, ...args)
758 }
759 /**
760 * Returns the path to the file inside of the output dir.
761 * @param {string} path
762 * @param {string[]} args
763 */
764 getDocPath(path, ...args) {
765 const pp = join('/', path, ...args)
766 return join(this.config.output, pp)
767 }
768 /**
769 * Adds a script to the array.
770 * @deprecated Use `script` instead.
771 */
772 addScript(...args) {
773 this.logError('.addScript is deprecated. Use .script instead.')
774 return this.script(...args)
775 }
776 /**
777 * Adds a script to the array.
778 * @param {string} script Path to the script relative to splendid app dir (e.g., js/script.js)
779 * @param {boolean} [mod=false] Whether this is a module
780 * @param {Object} [props] The properties.
781 * @param {boolean|string} [combine=false] Whether to import the script in comps rather than embed using the `<script>` tag. If string is given, it is imported with name (todo).
782 * @returns {boolean} Whether the script was added.
783 */
784 script(script, mod = false, props = {}, combine = false) {
785 if (combine) {
786 if (this.combinedScripts.includes(script)) return
787 return this.combinedScripts.push(script)
788 }
789
790 const item = {
791 src: script,
792 ...(mod ? { type: 'module' } : {}),
793 ...props,
794 }
795
796 const h = JSON.stringify(item)
797 if (this._scripts.some((a) => JSON.stringify(a) == h)) return
798
799 return this._scripts.push(item)
800 }
801 addJs(...args) {
802 this.logError('.addJs is deprecated. Use .js instead.')
803 return this.js(...args)
804 }
805 /**
806 * Adds a script body to the document.
807 * @param {string} js The JS source code to add.
808 * @param {boolean} [mod=false] Whether to include this script as a module. Default `false`.
809 * @param {Object} [props] The properties.
810 * @param {boolean} [combine] Should this be part of PageComp (todo).
811 */
812 js(js, mod, props = {}, combine) {
813 const item = {
814 js,
815 ...(mod ? { type: 'module' } : {}),
816 ...props,
817 }
818 const h = JSON.stringify(item)
819 if (this._js.some((a) => JSON.stringify(a) == h)) return
820 return this._js.push(item)
821 }
822 /**
823 * Adds an extern file for Closure.
824 */
825 addExtern(extern) {
826 this.logError2('addExtern', 'deprecated, use `splendid.extern`.')
827 this._externs.push(extern)
828 }
829 /**
830 * Adds an extern file for Closure.
831 */
832 extern(extern) {
833 if (!this._externs.includes(extern))
834 this._externs.push(extern)
835 }
836 /**
837 * Returns the cache for the given type as an object.
838 * @param {string} type The type of the cache.
839 * @param {string} [name] The name of the cache entry within the cache type.
840 * @return {Object<string, *>}
841 */
842 getCache(type, name) {
843 const p = resolve(this.config.appDir, `.cache/${type}.json`)
844 delete require.cache[p]
845 try {
846 const r = require(p)
847 if (name) {
848 if (name in r) return r[name]
849 return {}
850 }
851 return r
852 } catch (err) {
853 return {}
854 }
855 }
856 /**
857 * Returns a path according to the page's dir and mount.
858 * @param {string} href The path to wrap.
859 */
860 wrapSlash(href) {
861 return wrapSlash(href,
862 this.config.mount, this.page.url, this.page.dir)
863 }
864 /**
865 * Writes the file inside the app directory (`splendid`), after comparing to the existing version.
866 * @param {string} path inside the app dir
867 * @param {string|Buffer} data what to write
868 */
869 async writeApp(path, data) {
870 const j = this.getPath(path)
871 try {
872 const current = await read(j)
873 if (current == data) return j
874 } catch (err) {
875 debugger
876 // ok
877 }
878 await ensurePath(j)
879 await write(j, data)
880 return j
881 }
882 /**
883 * Returns the current file based on the position.
884 * Called by competent implementation.
885 * @param {number} pos
886 * @param {Array<{file:string, from: number, length:number}>} areas
887 */
888 static getCurrentFile(pos, areas) {
889 const area = areas.find(({ from, length }) => {
890 const to = from + length
891 return pos >= from && pos < to
892 })
893 if (!area) throw new Error(`Area for ${pos} not found`)
894 return area.file
895 }
896 /**
897 * Writes the file inside the app directory (`splendid`).
898 * @param {string} path inside the app dir
899 * @param {string|Buffer} data what to write
900 */
901 writeAppSync(path, data) {
902 const j = this.getPath(path)
903
904 try {
905 const r = readFileSync(j, 'utf8')
906 if (r == data) return
907 } catch (err) {
908 //
909 }
910
911 ensurePathSync(j)
912 writeFileSync(j, data)
913 return j
914 }
915 /**
916 * Create a file in the docs directory.
917 * @alias writeFile
918 */
919 writeDoc(path, data) {
920 return this.writeFile(path, data)
921 }
922 /**
923 * Create a file in the output directory.
924 * @param {string} path The path in the docs dir.
925 * @param {Buffer|string} data What to write.
926 * @return {string} The path where the file was saved.
927 */
928 async writeFile(path, data) {
929 const j = this.getDocPath(path)
930 await ensurePath(j)
931 await write(j, data)
932 return j
933 }
934 appendCacheSync(type, value) {
935 const r = this.getPath('.cache', `${type}.json`)
936 ensurePathSync(r)
937 const current = this.getCache(type)
938 const v = { ...current, ...value }
939 try {
940 deepStrictEqual(v, current)
941 } catch (err) {
942 console.log('Updating cache for %s', r)
943 console.log(differently(current, v).replace(': [object Object]', ''))
944 const j = JSON.stringify(v, null, 2)
945 writeFileSync(r, j)
946 }
947 }
948 /**
949 * The concurrency job for the component.
950 */
951 get concurrency() {
952 return {}
953 }
954 async appendCache(type, value) {
955 writingCache = writingCache.then(() => {
956 // can't just spawn async fn otherwise errors won't be caught
957 return new Promise(async (rr, jj) => {
958 try {
959 const r = this.getPath('.cache', `${type}.json`)
960 await ensurePath(r)
961 const current = this.getCache(type)
962 const v = { ...current, ...value }
963 try {
964 deepStrictEqual(v, current)
965 } catch (err) {
966 console.log('Updating cache for %s', r)
967 console.log(differently(current, v).replace(': [object Object]', ''))
968 const j = JSON.stringify(v, null, 2)
969 await this._writingCache
970 const p = write(r, j)
971 this._writingCache = p
972 await p
973 }
974 rr()
975 } catch (e) {
976 jj(e)
977 }
978 })
979 })
980 return writingCache
981 }
982 /**
983 * Call this method to make the component be initialised on pages.
984 * @param {Object} props If passed, the HTML properties will be overriden with these new ones.
985 */
986 export(props) {
987 // implementation in src/lib/components/index.js
988 }
989 /**
990 * @param {boolean} enable Whether to enable pretty printing (default true).
991 */
992 setPretty(enable = true) {}
993 /**
994 * @param {boolean} enable Whether to enable pretty printing (default true).
995 */
996 pretty(enable = true) {}
997 /**
998 * By default, all elements will be rendered in two passes, to be able to render children.
999 * This method controls this behaviour.
1000 * @param {boolean} [mightHaveRecursion] Whether to exclude the element key itself from second render.
1001 * Default `false`.
1002 * @param {boolean} shouldRender Should the second render happen. Default `true`.
1003 */
1004 renderAgain(mightHaveRecursion, shouldRender) {}
1005 /**
1006 * Logs data with the name of the component that is being replaced.
1007 */
1008 log(...args) {
1009 log('splendid', ...args)
1010 }
1011 /**
1012 * Logs error in red with the name of the component that is being replaced.
1013 */
1014 logError(...args) {
1015 error('splendid', ...args)
1016 }
1017 /**
1018 * Makes sure that the blank line won't appear in the output.
1019 * @returns {null}
1020 */
1021 removeLine() {
1022 return null
1023 }
1024 get log2() {
1025 return log
1026 }
1027 logError2(name, ...args) {
1028 error(name, ...args)
1029 }
1030 get COMPETENT_UPDATED() {
1031 if (_COMPETENT_UPDATED !== undefined) return false // just once per process
1032 const version = this.getCache('competent', 'version')
1033 _COMPETENT_UPDATED = version != COMPETENT_VERSION
1034 return _COMPETENT_UPDATED
1035 }
1036 get COMPETENT_VERSION() {
1037 return COMPETENT_VERSION
1038 }
1039 /**
1040 * Request a web page and return information including headers, statusCode, statusMessage along with the body (which is also parsed if JSON received).
1041 * @param {string} address The URL such as http://example.com/api.
1042 * @param {import('..').AqtOptions} [options] Configuration for requests.
1043 * @returns {Promise<import('..').AqtReturn>}
1044 */
1045 aqt(address, options) {
1046 return aqt(address, options)
1047 }
1048 /**
1049 * Gets image dimensions.
1050 * @param {string} input Path to the file.
1051 * @return {Promise<{width: number, height: number}>}
1052 */
1053 async imageDimensions(input) {
1054 const mtime = await this.getLocaleMtime(input)
1055 let { mtime: m, width, height } = this.getCache('image-dimensions', input)
1056 if (mtime == m) {
1057 return { width, height }
1058 }
1059 if (process.platform == 'darwin') {
1060 ({ pixelWidth: width, pixelHeight: height } = await sipsResolution(input))
1061 } else {
1062 throw new Error(`Platform ${
1063 process.platform} is not supported for image dimensions`)
1064 }
1065 await this.appendCache('image-dimensions', { [input]: {
1066 mtime, width, height } })
1067 return { width, height }
1068 }
1069 /**
1070 * Returns an array of suitable resizes.
1071 * @param {string} input The path to the image file.
1072 * @param {number[]} sizes The desired sizes
1073 * @param {number} max The maximum size.
1074 */
1075 async getResizes(input, sizes, max) {
1076 const { width, height } = await this.imageDimensions(input)
1077 const resizes = sizes
1078 .filter(s => s <= width && s <= max)
1079 return { resizes, width, height }
1080 }
1081}
1082let _COMPETENT_UPDATED
1083const COMPETENT_VERSION = '3.7.2'
1084
1085
1086/**
1087 * @param {Splendid} splendid
1088 * @param {string} stylesheetPath
1089 * @param {Object<string, string>} renameMap
1090 */
1091const writeRenameMap = (splendid, stylesheetPath, renameMap) => {
1092 // await ensurePath(splendid.getPath('comps', '__rename-maps', 't.js'))
1093 // await Object.entries(css).reduce(async (acc, [key, value]) => {
1094 // await acc
1095 const k = stylesheetPath.replace('splendid://', 'splendid/').replace(/\.css$/, '.js')
1096 const path = splendid.getPath('comps', '__rename-maps', k)
1097 ensurePathSync(path)
1098 const v = JSON.stringify(renameMap)
1099 writeFileSync(path, `export default ${v}`)
1100}
1101
1102
1103module.exports = Splendid
\No newline at end of file