surfingkeys-conf/gulpfile.js

362 lines
11 KiB
JavaScript
Raw Normal View History

2017-12-02 02:48:58 +00:00
const gulp = require("gulp")
const { task, src, dest } = gulp
const { parallel, series } = gulp
const parcel = require("gulp-parcel")
2017-12-02 02:48:58 +00:00
const replace = require("gulp-replace")
const rename = require("gulp-rename")
const eslint = require("gulp-eslint")
2019-03-10 03:28:48 +00:00
const file = require("gulp-file")
2017-12-02 02:48:58 +00:00
const path = require("path")
const del = require("del")
const platforms = require("platform-folders")
2019-03-10 03:28:48 +00:00
const fs = require("fs").promises
const fetch = require("node-fetch")
const express = require("express")
const gulpIf = require("gulp-if")
const { COPYFILE_EXCL } = require("fs").constants
2017-12-02 02:48:58 +00:00
const { URL } = require("url")
2017-08-28 03:24:24 +00:00
const pkg = require("./package.json")
const copyrightYearOne = 2017
const paths = {
scripts: ["conf.priv.js", "completions.js", "conf.js", "actions.js", "help.js", "keys.js", "util.js"],
entry: "conf.js",
gulpfile: "gulpfile.js",
readme: "README.tmpl.md",
assets: "assets",
screenshots: "assets/screenshots",
favicons: "assets/favicons",
faviconsManifest: "favicons.json",
readmeOut: "README.md",
scriptOut: "surfingkeys.js",
installDir: platforms.getConfigHome(),
}
let srcFilesLoaded = false
let compl
let conf
let keys
let util
const requireSrcFiles = () => {
if (srcFilesLoaded) {
return
}
/* eslint-disable global-require, import/no-dynamic-require */
compl = require("./completions")
conf = require("./conf")
keys = require("./keys")
util = require("./util")
/* eslint-enable global-require, import/no-dynamic-require */
srcFilesLoaded = true
}
let faviconsManifest
const loadFaviconsManifest = async () => {
if (typeof faviconsManifest === "object" && Object.keys(faviconsManifest).length > 0) {
return
}
try {
// eslint-disable-next-line global-require, import/no-dynamic-require
faviconsManifest = require(`./${path.join(paths.favicons, paths.faviconsManifest)}`)
} catch (e) {
console.log(`Warning: couldn't load favicons manifest: ${e}`) // eslint-disable-line no-console
faviconsManifest = {}
}
2017-12-02 02:48:58 +00:00
}
2017-08-28 03:24:24 +00:00
const servePort = 9919
// This notice will be injected into the generated README.md file
const disclaimer = `\
<!--
NOTICE:
This is an automatically generated file - Do not edit it directly.
The source file is ${paths.readme}
2017-12-02 02:48:58 +00:00
-->`
task("clean", () => del(["build", ".cache", ".tmp-gulp-compile-*"]))
task("clean-favicons", () => del([paths.favicons]))
2017-08-28 03:24:24 +00:00
const lint = (globs, opts = {}) => gulp.src(globs)
.pipe(eslint(opts))
.pipe(eslint.format())
task("lint", () => lint([...paths.scripts, paths.gulpfile]))
task("lint-fix", () => lint([...paths.scripts, paths.gulpfile], { fix: true })
.pipe(gulpIf(
(f) => f.eslint !== undefined && f.eslint.fixed === true,
gulp.dest((f) => f.base),
)))
2019-03-07 22:13:49 +00:00
task("check-priv", async () => {
try {
await fs.stat("./conf.priv.js")
} catch (e) {
// eslint-disable-next-line no-console
console.log("Notice: Initializing ./conf.priv.js - configure your API keys here.")
return fs.copyFile("./conf.priv.example.js", "./conf.priv.js", COPYFILE_EXCL)
}
return Promise.resolve()
})
2019-03-07 22:13:49 +00:00
const parseContributor = (contributor) => {
let c = contributor
if (typeof contributor === "string") {
const m = contributor.match(/^(?<name>.*?)\s*(<(?<email>.*?)>)?\s*(\((?<url>.*?)\))?$/)
if (!m) {
throw new Error(`couldn't parse contributor '${contributor}'`)
}
c = m.groups
} else if (typeof contributor !== "object") {
throw new Error(`expected contributor to be of type 'string' or 'object', got '${typeof contributor}'`)
}
if (!c.name) {
return null
}
return `${c.url ? `<a href="${c.url}">` : ""}${c.name}${c.url ? "</a>" : ""}`
}
task("docs", parallel(async () => {
requireSrcFiles()
await loadFaviconsManifest()
2017-12-02 02:48:58 +00:00
const screens = {}
let screenshotList = ""
2019-03-10 03:28:48 +00:00
const screenshots = await fs.readdir(path.join(__dirname, paths.screenshots))
screenshots.forEach((s) => {
const name = path.basename(s, ".png").split("-")
const alias = name[0]
2017-12-02 02:48:58 +00:00
if (!screens[alias]) {
screens[alias] = []
}
screens[alias].push(path.join(paths.screenshots, path.basename(s)))
})
2019-03-10 03:28:48 +00:00
let complTable = Object.keys(compl).sort((a, b) => {
if (a < b) return -1
if (a > b) return 1
return 0
})
let keysTable = Object.keys(keys.maps).sort((a, b) => {
if (a === "global") return -1
if (b === "global") return 1
2017-12-02 02:48:58 +00:00
if (a < b) return -1
if (a > b) return 1
return 0
2019-03-10 03:28:48 +00:00
})
complTable = await complTable.reduce(async (acc1p, k) => {
const acc1 = await acc1p
2017-12-02 02:48:58 +00:00
const c = compl[k]
2019-03-10 03:28:48 +00:00
const u = new URL(c.domain ? `https://${c.domain}` : c.search)
const domain = u.hostname
2017-12-02 02:48:58 +00:00
let s = ""
if (screens[c.alias]) {
screens[c.alias].forEach((url, i) => {
const num = (i > 0) ? ` ${i + 1}` : ""
2019-03-10 03:28:48 +00:00
s += `<a href="#${c.name}${num.replace(" ", "-")}">:framed_picture:</a>`
2017-12-02 02:48:58 +00:00
screenshotList += `##### ${c.name}${num}\n`
screenshotList += `![${c.name} screenshot](./${url})\n\n`
})
}
const favicon = faviconsManifest[domain] ? `<img src="./assets/favicons/${faviconsManifest[domain]}" width="16px"> ` : ""
2019-03-10 03:28:48 +00:00
return `${acc1}
<tr>
<td><a href="${u.protocol}//${domain}">${favicon}</a></td>
<td><code>${c.alias}</code></td>
<td>${c.name}</td>
<td><a href="${u.protocol}//${domain}">${domain}</a></td>
<td>${s}</td>
</tr>`
}, Promise.resolve(""))
keysTable = await keysTable.reduce(async (acc1p, domain) => {
const acc1 = await acc1p
const header = "<tr><td><strong>Mapping</strong></td><td><strong>Description</strong></td></tr>"
const c = keys.maps[domain]
const maps = c.reduce((acc2, map) => {
2018-11-16 09:04:51 +00:00
let leader = ""
if (typeof map.leader !== "undefined") {
leader = map.leader // eslint-disable-line prefer-destructuring
} else if (domain === "global") {
leader = ""
} else {
leader = conf.siteleader
}
const mapStr = util.escape(`${leader}${map.alias}`.replace(" ", "<space>"))
return `${acc2}<tr><td><code>${mapStr}</code></td><td>${map.description}</td></tr>\n`
}, "")
2019-03-10 03:28:48 +00:00
let domainStr = "<strong>global</strong>"
let favicon = ""
2019-03-10 03:28:48 +00:00
if (domain !== "global") {
favicon = faviconsManifest[domain] ? `<img src="./assets/favicons/${faviconsManifest[domain]}" width="16px"> ` : ""
2019-03-10 03:28:48 +00:00
domainStr = `<a href="//${domain}">${favicon}${domain}</a>`
}
return `${acc1}<tr><th colspan="2">${domainStr}</th></tr>${header}\n${maps}`
2019-03-10 03:28:48 +00:00
}, Promise.resolve(""))
const year = (new Date()).getFullYear()
const copyrightYears = `${copyrightYearOne !== year ? `${copyrightYearOne}-${year}` : copyrightYearOne}`
let copyright = `<p><h4>Author</h4>&copy; ${copyrightYears} ${parseContributor(pkg.author)}</p>`
if (Array.isArray(pkg.contributors) && pkg.contributors.length > 0) {
copyright += "<p><h4>Contributors</h4><ul>"
copyright += pkg.contributors.reduce((acc, c) => `${acc}<li>${parseContributor(c)}</li>`, "")
copyright += "</ul></p>"
}
copyright += `<p><h4>License</h4>Released under the <a href="./LICENSE">${pkg.license} License</a></p>`
return src([paths.readme])
.pipe(replace("<!--{{DISCLAIMER}}-->", disclaimer))
.pipe(replace("<!--{{COMPL_COUNT}}-->", Object.keys(compl).length))
.pipe(replace("<!--{{COMPL_TABLE}}-->", complTable))
.pipe(replace("<!--{{KEYS_MAPS_COUNT}}-->", Object.values(keys.maps).reduce((acc, m) => acc + m.length, 0)))
.pipe(replace("<!--{{KEYS_SITES_COUNT}}-->", Object.keys(keys.maps).length))
.pipe(replace("<!--{{KEYS_TABLE}}-->", keysTable))
.pipe(replace("<!--{{SCREENSHOTS}}-->", screenshotList))
.pipe(replace("<!--{{COPYRIGHT}}-->", copyright))
.pipe(rename(paths.readmeOut))
.pipe(dest("."))
2019-03-10 03:28:48 +00:00
}))
const getFavicon = async ({ domain, favicon }, timeout = 5000) => {
const url = favicon
let data
try {
const res = await fetch(url, { timeout })
if (!res.ok) {
throw new Error(`request to ${url} failed with code ${res.status}`)
}
data = await res.buffer()
} catch (e) {
process.stdout.write(`no favicon found for ${url}: ${e}\n`)
2019-03-10 03:28:48 +00:00
// transparent pixel
data = Buffer.from(
"AAABAAEAAQEAAAEAIAAwAAAAFgAAACgAAAABAAAAAgAAAAEAIAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAA==",
"base64",
2019-03-10 03:28:48 +00:00
)
}
return {
domain,
name: `${domain}.ico`,
2019-03-10 03:28:48 +00:00
source: data,
}
}
task("favicons", series("clean-favicons", async () => {
requireSrcFiles()
2019-03-10 03:28:48 +00:00
const sites = [].concat(
// search engine completions
Object.entries(compl)
.map(([, v]) => ({
domain: new URL(v.domain ? `https://${v.domain}` : v.search).hostname,
favicon: `https://icons.duckduckgo.com/ip3/${new URL(v.domain ? `https://${v.domain}` : v.search).hostname}.ico`,
2019-03-10 03:28:48 +00:00
})),
// site-specific keybindings
Object.keys(keys.maps)
2019-09-11 11:51:27 +00:00
.filter((k) => k !== "global")
.map((k) => ({
2019-03-10 03:28:48 +00:00
domain: k,
favicon: `https://icons.duckduckgo.com/ip3/${new URL(`https://${k}`).hostname}.ico`,
2019-03-10 03:28:48 +00:00
})),
).filter((e, i, arr) => i === arr.indexOf(e)) // Keep only first occurrence of each element
2019-03-10 03:28:48 +00:00
2019-09-11 11:51:27 +00:00
const favicons = (await Promise.all(sites.map(async (site) => getFavicon(site))))
.filter((e) => e !== undefined)
faviconsManifest = favicons.reduce((acc, e) => {
acc[e.domain] = e.name
return acc
}, {})
const files = [{
name: paths.faviconsManifest,
source: JSON.stringify(faviconsManifest),
}, ...favicons]
return file(files)
.pipe(dest(paths.favicons))
}))
task("docs-full", series("favicons", "docs"))
2019-03-10 03:28:48 +00:00
task("build",
2019-03-07 22:13:49 +00:00
series(
parallel(
"check-priv",
"clean",
),
parallel(
"lint",
() => src(paths.entry, { read: false })
.pipe(parcel())
.pipe(rename(paths.scriptOut))
.pipe(dest("build")),
),
2019-03-07 22:13:49 +00:00
))
task("dist", parallel("docs-full", "build"))
2019-03-10 17:31:30 +00:00
task("install", series("build", () => src(path.join("build", paths.scriptOut))
.pipe(dest(paths.installDir))))
2019-03-07 22:13:49 +00:00
const watch = (g, t) => () =>
gulp.watch(g, { ignoreInitial: false, usePolling: true }, t)
task("watch-build", watch(paths.scripts, series("build")))
task("watch-install", watch(paths.scripts, series("install")))
task("watch-lint", watch([...paths.scripts, paths.gulpfile], series("lint")))
task("watch-docs", watch([...paths.scripts, paths.readme], series("docs")))
task("watch-docs-full", watch([...paths.scripts, paths.readme], series("docs-full")))
const serve = (done) => {
const app = express()
const handler = (allowedOrigin) => async (req, res) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`) // eslint-disable-line no-console
res.writeHead(200, {
"Content-Type": "text/javascript; charset=UTF-8",
"Access-Control-Allow-Origin": allowedOrigin,
})
res.end(await fs.readFile(path.join("build", paths.scriptOut)))
}
app.get("/", handler("chrome-extension://mffcegbjcdejldmihkogmcnkgbbhioid"))
app.get("/chrome", handler("chrome-extension://mffcegbjcdejldmihkogmcnkgbbhioid"))
app.get("/firefox", handler("moz-extension://a7b04efeb-0b36-47f6-9f57-70293e5ee7b2"))
app.listen(servePort)
console.log(`web server is listening on port ${servePort}`) // eslint-disable-line no-console
app.on("close", () => {
console.log("web server is closing...") // eslint-disable-line no-console
done()
})
}
task("serve-simple", serve)
2019-03-07 22:13:49 +00:00
task("serve-build", parallel("watch-build", "serve-simple"))
2019-03-07 22:13:49 +00:00
task("serve", series("serve-build"))
task("watch", series("watch-install"))
task("default", series("build"))