├── .env.example ├── .github ├── dependabot.yml └── workflows │ ├── push.yml │ └── schedule.yml ├── .gitignore ├── .replrc.js ├── Procfile ├── demo.css ├── demo.js ├── dist ├── apis.json ├── apps.json ├── glossary.json ├── packages.json └── tutorials.json ├── electron-api.json ├── index.js ├── indices ├── apis.js ├── apps.js ├── glossary.js ├── index.js ├── packages.js └── tutorials.js ├── lib └── algolia-index.js ├── package-lock.json ├── package.json ├── readme.md ├── script ├── build.js ├── update-data-sources.sh ├── update-electron-apis.js └── upload.js └── test.js /.env.example: -------------------------------------------------------------------------------- 1 | ALGOLIA_APPLICATION_ID= 2 | ALGOLIA_API_KEY= -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: npm 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | open-pull-requests-limit: 99 9 | ignore: 10 | - dependency-name: electron-* 11 | versions: 12 | - ">= 0" 13 | - package-ecosystem: github-actions 14 | directory: "/" 15 | schedule: 16 | interval: daily 17 | open-pull-requests-limit: 99 18 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - master 5 | push: 6 | branches: 7 | - master 8 | 9 | name: Continuous Integration 10 | 11 | jobs: 12 | installDependencies: 13 | name: NodeJS 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@master 17 | - uses: actions/setup-node@v2.1.5 18 | with: 19 | node-version: "14.x" 20 | - name: Install dependencies 21 | run: npm ci 22 | - name: Run tests 23 | run: npm test 24 | - name: Upload Algolia Indices 25 | if: github['ref'] == 'refs/heads/master' 26 | env: 27 | ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} 28 | ALGOLIA_APPLICATION_ID: ${{ secrets.ALGOLIA_APPLICATION_ID }} 29 | run: npm run upload 30 | - name: Publish via semantic-release 31 | if: github['ref'] == 'refs/heads/master' 32 | env: 33 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 34 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 35 | run: npm run semantic-release 36 | -------------------------------------------------------------------------------- /.github/workflows/schedule.yml: -------------------------------------------------------------------------------- 1 | on: 2 | schedule: 3 | - cron: 15 09 * * * 4 | workflow_dispatch: {} 5 | 6 | name: Update Algolia Data Sources 7 | 8 | jobs: 9 | fetchLatestDataSources: 10 | name: Fetch latest data sources 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@master 14 | - uses: actions/setup-node@v2.1.5 15 | with: 16 | node-version: "14.x" 17 | - name: Install dependencies 18 | run: npm ci 19 | - name: Fetch latest data sources 20 | run: npm run update-data-sources 21 | env: 22 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .npmrc 3 | node_modules 4 | *.log 5 | -------------------------------------------------------------------------------- /.replrc.js: -------------------------------------------------------------------------------- 1 | const index = require('.') 2 | 3 | module.exports = { 4 | context: [ 5 | {name: 'index', value: index}, 6 | {name: 'titles', value: index.map(index => index.title)} 7 | ] 8 | } -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: budo demo.js --css demo.css --port $PORT -------------------------------------------------------------------------------- /demo.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | padding: 50px; 7 | font-family: "helvetica neue", helvetica, arial; 8 | } 9 | 10 | #search-box input { 11 | padding: 10px; 12 | width: 100%; 13 | } 14 | 15 | .ais-Hits-item { 16 | padding: 10px; 17 | border: 1px solid #eee; 18 | border-radius: 3px; 19 | margin: 5px 0; 20 | } 21 | 22 | .ais-Hits-item em { 23 | background-color: yellow; 24 | } 25 | 26 | .ais-SearchBox-magnifier { 27 | display: none; 28 | } 29 | 30 | .ais-SearchBox-submit { 31 | top: 12px; 32 | width: 30px; 33 | height: 30px; 34 | border: none; 35 | position: absolute; 36 | right: 50px; 37 | opacity: 0.2; 38 | } 39 | 40 | .ais-SearchBox-reset { 41 | top: 12px; 42 | width: 30px; 43 | height: 30px; 44 | border: none; 45 | position: absolute; 46 | right: 10px; 47 | opacity: 0.2; 48 | } 49 | -------------------------------------------------------------------------------- /demo.js: -------------------------------------------------------------------------------- 1 | const html = require('nanohtml') 2 | const algoliasearch = require('algoliasearch') 3 | const instantsearch = require('instantsearch.js').default 4 | document.title = 'Electron Search' 5 | 6 | const $main = html` 7 |
8 | 9 |
10 |
11 |
12 | ` 13 | 14 | const hitTemplate = ` 15 | {{#_highlightResult.icon64}} 16 | 17 | {{#helpers.highlight}}{ "attribute": "name" }{{/helpers.highlight}} - 18 | {{#helpers.highlight}}{ "attribute": "description" }{{/helpers.highlight}} 19 | {{/_highlightResult.icon64}} 20 | 21 | {{^_highlightResult.icon64}} 22 | {{{type.value}}} 23 | {{#helpers.highlight}}{ "attribute": "title" }{{/helpers.highlight}} - 24 | {{#helpers.highlight}}{ "attribute": "tldr" }{{/helpers.highlight}} 25 | {{/_highlightResult.icon64}} 26 | ` 27 | 28 | document.body.appendChild($main) 29 | 30 | const search = instantsearch({ 31 | searchClient: algoliasearch('L9LD9GHGQJ', '24e7e99910a15eb5d9d93531e5682370'), 32 | indexName: 'electron-apis', 33 | routing: true 34 | }) 35 | 36 | search.addWidget( 37 | instantsearch.widgets.hits({ 38 | container: '#hits', 39 | templates: { 40 | empty: 'No results', 41 | item: hitTemplate 42 | }, 43 | transformItems: items => 44 | // eslint-disable-next-line 45 | items.map(item => (console.log(item), { 46 | ...item 47 | })) 48 | }) 49 | ) 50 | 51 | search.addWidget( 52 | instantsearch.widgets.searchBox({ 53 | container: '#search-box', 54 | placeholder: 'Search Electron APIs' 55 | }) 56 | ) 57 | 58 | search.addWidget( 59 | instantsearch.widgets.refinementList({ 60 | container: '#refinement-list', 61 | attribute: 'type', 62 | limit: 10, 63 | templates: { 64 | header: 'Types' 65 | } 66 | }) 67 | ) 68 | 69 | search.start() 70 | 71 | search.on('render', (...args) => { 72 | // console.log('algolia render', args) 73 | }) 74 | 75 | search.on('error', (...args) => { 76 | console.log('algolia error', args) 77 | }) 78 | -------------------------------------------------------------------------------- /dist/glossary.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glossary", 3 | "records": [ 4 | { 5 | "objectID": "glossary-ASAR", 6 | "term": "ASAR", 7 | "description": "ASAR stands for Atom Shell Archive Format. An asar archive is a simple tar-like format that concatenates files into a single file. Electron can read arbitrary files from it without unpacking the whole file.", 8 | "url": "https://electronjs.org/docs/glossary#asar", 9 | "keyValuePairs": [ 10 | "is:doc", 11 | "is:glossary", 12 | "glossary:ASAR" 13 | ] 14 | }, 15 | { 16 | "objectID": "glossary-CRT", 17 | "term": "CRT", 18 | "description": "The C Run-time Library (CRT) is the part of the C++ Standard Library that incorporates the ISO C99 standard library. The Visual C++ libraries that implement the CRT support native code development, and both mixed native and managed code, and pure managed code for .NET development.", 19 | "url": "https://electronjs.org/docs/glossary#crt", 20 | "keyValuePairs": [ 21 | "is:doc", 22 | "is:glossary", 23 | "glossary:CRT" 24 | ] 25 | }, 26 | { 27 | "objectID": "glossary-DMG", 28 | "term": "DMG", 29 | "description": "An Apple Disk Image is a packaging format used by macOS. DMG files are commonly used for distributing application \"installers\". electron-builder supports dmg as a build target.", 30 | "url": "https://electronjs.org/docs/glossary#dmg", 31 | "keyValuePairs": [ 32 | "is:doc", 33 | "is:glossary", 34 | "glossary:DMG" 35 | ] 36 | }, 37 | { 38 | "objectID": "glossary-IME", 39 | "term": "IME", 40 | "description": "Input Method Editor. A program that allows users to enter characters and symbols not found on their keyboard. For example, this allows users of Latin keyboards to input Chinese, Japanese, Korean and Indic characters.", 41 | "url": "https://electronjs.org/docs/glossary#ime", 42 | "keyValuePairs": [ 43 | "is:doc", 44 | "is:glossary", 45 | "glossary:IME" 46 | ] 47 | }, 48 | { 49 | "objectID": "glossary-IDL", 50 | "term": "IDL", 51 | "description": "Interface description language. Write function signatures and data types in a format that can be used to generate interfaces in Java, C++, JavaScript, etc.", 52 | "url": "https://electronjs.org/docs/glossary#idl", 53 | "keyValuePairs": [ 54 | "is:doc", 55 | "is:glossary", 56 | "glossary:IDL" 57 | ] 58 | }, 59 | { 60 | "objectID": "glossary-IPC", 61 | "term": "IPC", 62 | "description": "IPC stands for Inter-Process Communication. Electron uses IPC to send serialized JSON messages between the main and renderer processes.", 63 | "url": "https://electronjs.org/docs/glossary#ipc", 64 | "keyValuePairs": [ 65 | "is:doc", 66 | "is:glossary", 67 | "glossary:IPC" 68 | ] 69 | }, 70 | { 71 | "objectID": "glossary-libchromiumcontent", 72 | "term": "libchromiumcontent", 73 | "description": "A shared library that includes the Chromium Content module and all its dependencies (e.g., Blink, V8, etc.). Also referred to as \"libcc\".", 74 | "url": "https://electronjs.org/docs/glossary#libchromiumcontent", 75 | "keyValuePairs": [ 76 | "is:doc", 77 | "is:glossary", 78 | "glossary:libchromiumcontent" 79 | ] 80 | }, 81 | { 82 | "objectID": "glossary-main process", 83 | "term": "main process", 84 | "description": "The main process, commonly a file named main.js, is the entry point to every Electron app. It controls the life of the app, from open to close. It also manages native elements such as the Menu, Menu Bar, Dock, Tray, etc. The main process is responsible for creating each new renderer process in the app. The full Node API is built in.", 85 | "url": "https://electronjs.org/docs/glossary#main-process", 86 | "keyValuePairs": [ 87 | "is:doc", 88 | "is:glossary", 89 | "glossary:main process" 90 | ] 91 | }, 92 | { 93 | "objectID": "glossary-MAS", 94 | "term": "MAS", 95 | "description": "Acronym for Apple's Mac App Store. For details on submitting your app to the MAS, see the Mac App Store Submission Guide.", 96 | "url": "https://electronjs.org/docs/glossary#mas", 97 | "keyValuePairs": [ 98 | "is:doc", 99 | "is:glossary", 100 | "glossary:MAS" 101 | ] 102 | }, 103 | { 104 | "objectID": "glossary-Mojo", 105 | "term": "Mojo", 106 | "description": "An IPC system for communicating intra- or inter-process, and that's important because Chrome is keen on being able to split its work into separate processes or not, depending on memory pressures etc.", 107 | "url": "https://electronjs.org/docs/glossary#mojo", 108 | "keyValuePairs": [ 109 | "is:doc", 110 | "is:glossary", 111 | "glossary:Mojo" 112 | ] 113 | }, 114 | { 115 | "objectID": "glossary-native modules", 116 | "term": "native modules", 117 | "description": "Native modules (also called addons in Node.js) are modules written in C or C++ that can be loaded into Node.js or Electron using the require() function, and used as if they were an ordinary Node.js module. They are used primarily to provide an interface between JavaScript running in Node.js and C/C++ libraries.", 118 | "url": "https://electronjs.org/docs/glossary#native-modules", 119 | "keyValuePairs": [ 120 | "is:doc", 121 | "is:glossary", 122 | "glossary:native modules" 123 | ] 124 | }, 125 | { 126 | "objectID": "glossary-NSIS", 127 | "term": "NSIS", 128 | "description": "Nullsoft Scriptable Install System is a script-driven Installer authoring tool for Microsoft Windows. It is released under a combination of free software licenses, and is a widely-used alternative to commercial proprietary products like InstallShield. electron-builder supports NSIS as a build target.", 129 | "url": "https://electronjs.org/docs/glossary#nsis", 130 | "keyValuePairs": [ 131 | "is:doc", 132 | "is:glossary", 133 | "glossary:NSIS" 134 | ] 135 | }, 136 | { 137 | "objectID": "glossary-OSR", 138 | "term": "OSR", 139 | "description": "OSR (Off-screen rendering) can be used for loading heavy page in background and then displaying it after (it will be much faster). It allows you to render page without showing it on screen.", 140 | "url": "https://electronjs.org/docs/glossary#osr", 141 | "keyValuePairs": [ 142 | "is:doc", 143 | "is:glossary", 144 | "glossary:OSR" 145 | ] 146 | }, 147 | { 148 | "objectID": "glossary-process", 149 | "term": "process", 150 | "description": "A process is an instance of a computer program that is being executed. Electron apps that make use of the main and one or many renderer process are actually running several programs simultaneously.", 151 | "url": "https://electronjs.org/docs/glossary#process", 152 | "keyValuePairs": [ 153 | "is:doc", 154 | "is:glossary", 155 | "glossary:process" 156 | ] 157 | }, 158 | { 159 | "objectID": "glossary-renderer process", 160 | "term": "renderer process", 161 | "description": "The renderer process is a browser window in your app. Unlike the main process, there can be multiple of these and each is run in a separate process. They can also be hidden.", 162 | "url": "https://electronjs.org/docs/glossary#renderer-process", 163 | "keyValuePairs": [ 164 | "is:doc", 165 | "is:glossary", 166 | "glossary:renderer process" 167 | ] 168 | }, 169 | { 170 | "objectID": "glossary-Squirrel", 171 | "term": "Squirrel", 172 | "description": "Squirrel is an open-source framework that enables Electron apps to update automatically as new versions are released. See the autoUpdater API for info about getting started with Squirrel.", 173 | "url": "https://electronjs.org/docs/glossary#squirrel", 174 | "keyValuePairs": [ 175 | "is:doc", 176 | "is:glossary", 177 | "glossary:Squirrel" 178 | ] 179 | }, 180 | { 181 | "objectID": "glossary-userland", 182 | "term": "userland", 183 | "description": "This term originated in the Unix community, where \"userland\" or \"userspace\" referred to programs that run outside of the operating system kernel. More recently, the term has been popularized in the Node and npm community to distinguish between the features available in \"Node core\" versus packages published to the npm registry by the much larger \"user\" community.", 184 | "url": "https://electronjs.org/docs/glossary#userland", 185 | "keyValuePairs": [ 186 | "is:doc", 187 | "is:glossary", 188 | "glossary:userland" 189 | ] 190 | }, 191 | { 192 | "objectID": "glossary-V8", 193 | "term": "V8", 194 | "description": "V8 is Google's open source JavaScript engine. It is written in C++ and is used in Google Chrome. V8 can run standalone, or can be embedded into any C++ application.", 195 | "url": "https://electronjs.org/docs/glossary#v8", 196 | "keyValuePairs": [ 197 | "is:doc", 198 | "is:glossary", 199 | "glossary:V8" 200 | ] 201 | }, 202 | { 203 | "objectID": "glossary-webview", 204 | "term": "webview", 205 | "description": "webview tags are used to embed 'guest' content (such as external web pages) in your Electron app. They are similar to iframes, but differ in that each webview runs in a separate process. It doesn't have the same permissions as your web page and all interactions between your app and embedded content will be asynchronous. This keeps your app safe from the embedded content.", 206 | "url": "https://electronjs.org/docs/glossary#webview", 207 | "keyValuePairs": [ 208 | "is:doc", 209 | "is:glossary", 210 | "glossary:webview" 211 | ] 212 | } 213 | ] 214 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('require-directory')(module, './dist') 2 | -------------------------------------------------------------------------------- /indices/apis.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const AlgoliaIndex = require('../lib/algolia-index') 4 | // @ts-ignore 5 | const apis = require('../electron-api.json') 6 | const slugger = new (require('github-slugger'))() 7 | 8 | module.exports = new AlgoliaIndex('apis', getRecords()) 9 | 10 | function getUrlFragment (urlFragment) { 11 | return urlFragment !== undefined ? urlFragment : '' 12 | } 13 | 14 | function signatureNeeded (signature) { 15 | if (signature !== undefined) { 16 | return signature 17 | } 18 | 19 | return '' 20 | } 21 | 22 | function getRecords () { 23 | const records = [] 24 | 25 | apis.forEach(api => { 26 | // TODO constructorMethod 27 | const properties = api.properties || [] 28 | properties.forEach(property => { 29 | property.apiType = 'properties' 30 | property.fullSignature = `${api.name}.${property.name}` 31 | property.tldr = getTLDR(property) 32 | property.slug = slugger.slug(property.fullSignature, false) 33 | property.url = `${api.websiteUrl}${getUrlFragment(property.urlFragment)}` 34 | records.push(property) 35 | }) 36 | 37 | const instanceProperties = api.instanceProperties || [] 38 | instanceProperties.forEach(property => { 39 | property.apiType = 'instanceProperties' 40 | property.fullSignature = `${api.name}.${property.name}` 41 | property.tldr = getTLDR(property) 42 | property.slug = slugger.slug(property.fullSignature, false) 43 | property.url = `${api.websiteUrl}${getUrlFragment(property.urlFragment)}` 44 | records.push(property) 45 | }) 46 | 47 | const methods = api.methods || [] 48 | methods.forEach(method => { 49 | method.apiType = 'methods' 50 | method.fullSignature = `${api.name}.${method.name}${signatureNeeded(method.signature)}` 51 | method.tldr = getTLDR(method) 52 | method.slug = slugger.slug(method.fullSignature, false) 53 | method.url = `${api.websiteUrl}${getUrlFragment(method.urlFragment)}` 54 | records.push(method) 55 | }) 56 | 57 | const staticMethods = api.staticMethods || [] 58 | staticMethods.forEach(method => { 59 | method.apiType = 'staticMethod' 60 | method.fullSignature = `${api.name}.${method.name}${signatureNeeded(method.signature)}` 61 | method.tldr = getTLDR(method) 62 | method.slug = slugger.slug(method.fullSignature, false) 63 | method.url = `${api.websiteUrl}${getUrlFragment(method.urlFragment)}` 64 | records.push(method) 65 | }) 66 | 67 | const instanceMethods = api.instanceMethods || [] 68 | instanceMethods.forEach(method => { 69 | method.apiType = 'instanceMethod' 70 | method.fullSignature = `${api.instanceName}.${method.name}${signatureNeeded(method.signature)}` 71 | method.tldr = getTLDR(method) 72 | method.slug = slugger.slug(method.fullSignature, false) 73 | method.url = `${api.websiteUrl}${getUrlFragment(method.urlFragment)}` 74 | records.push(method) 75 | }) 76 | 77 | const events = api.events || [] 78 | events.forEach(event => { 79 | event.apiType = 'event' 80 | event.fullSignature = `${api.name}.on('${event.name}')` 81 | event.url = `${api.websiteUrl}${getUrlFragment(event.urlFragment)}` 82 | event.slug = slugger.slug(event.fullSignature, false) 83 | event.tldr = getTLDR(event) 84 | records.push(event) 85 | }) 86 | 87 | const instanceEvents = api.instanceEvents || [] 88 | instanceEvents.forEach(event => { 89 | event.apiType = 'event' 90 | event.fullSignature = `${api.instanceName}.on('${event.name}')` 91 | event.url = `${api.websiteUrl}${getUrlFragment(event.urlFragment)}` 92 | event.slug = slugger.slug(event.fullSignature, false) 93 | event.tldr = getTLDR(event) 94 | records.push(event) 95 | }) 96 | }) 97 | 98 | return records.map(record => { 99 | record.keyValuePairs = [ 100 | 'is:doc', 101 | 'is:api', 102 | `api:${record.name}`, 103 | `api:${record.slug}`, 104 | `api:${record.fullSignature}`, 105 | `doc:${record.name}`, 106 | `doc:${record.slug}`, 107 | `doc:${record.fullSignature}` 108 | ] 109 | 110 | return Object.assign( 111 | { objectID: `${record.url.replace('https://electronjs.org/docs/api/', 'api-')}-${record.slug}` }, 112 | record 113 | ) 114 | }) 115 | } 116 | 117 | function getTLDR (api) { 118 | const { description, returns } = api 119 | let tldr = null 120 | 121 | if (!description && returns && returns.description) { 122 | tldr = 123 | 'Returns ' + 124 | returns.description.charAt(0).toLowerCase() + 125 | returns.description.slice(1) 126 | } else if (typeof description !== 'string' || !description.length) { 127 | return null 128 | } else { 129 | tldr = description.split('. ')[0] 130 | } 131 | 132 | if (!tldr.endsWith('.')) tldr += '.' 133 | 134 | return tldr 135 | } 136 | -------------------------------------------------------------------------------- /indices/apps.js: -------------------------------------------------------------------------------- 1 | const apps = require('electron-apps') 2 | const AlgoliaIndex = require('../lib/algolia-index') 3 | 4 | module.exports = new AlgoliaIndex('apps', getRecords()) 5 | 6 | function getRecords () { 7 | return apps.map(app => { 8 | // remove large fields to avoid going over algolia plan limits 9 | delete app.latestRelease 10 | delete app.readmeCleaned 11 | delete app.readmeOriginal 12 | 13 | app.keyValuePairs = [ 14 | 'is:app', 15 | `app:${app.name}`, 16 | `app:${app.slug}` 17 | ] 18 | 19 | return Object.assign( 20 | { objectID: `app-${app.slug}` }, 21 | app 22 | ) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /indices/glossary.js: -------------------------------------------------------------------------------- 1 | const { chain } = require('lodash') 2 | const AlgoliaIndex = require('../lib/algolia-index') 3 | 4 | module.exports = new AlgoliaIndex('glossary', getRecords()) 5 | 6 | function getRecords () { 7 | return chain(Object.values(require('electron-i18n').glossary['en-US'])) 8 | .map(glossary => { 9 | const { term, description } = glossary 10 | const objectID = `glossary-${term}` 11 | 12 | const keyValuePairs = [ 13 | 'is:doc', 14 | 'is:glossary', 15 | `glossary:${term}` 16 | ] 17 | 18 | const url = `https://electronjs.org/docs/glossary#${term.toLowerCase()}`.replace(/ /g, '-') 19 | return { 20 | objectID, 21 | term, 22 | description, 23 | url, 24 | keyValuePairs 25 | } 26 | }) 27 | .compact() // remove nulls from early returns above 28 | .value() 29 | } 30 | -------------------------------------------------------------------------------- /indices/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('require-directory')(module) 2 | -------------------------------------------------------------------------------- /indices/packages.js: -------------------------------------------------------------------------------- 1 | const { pick } = require('lodash') 2 | const packages = require('electron-npm-packages') 3 | const AlgoliaIndex = require('../lib/algolia-index') 4 | const props = 'name description sourcerank repository keywords license homepage owners created modified dependencies devDependencies scripts'.split(' ') 5 | 6 | module.exports = new AlgoliaIndex('packages', getRecords()) 7 | 8 | function getRecords () { 9 | return packages.map(pkg => { 10 | pkg = Object.assign( 11 | { objectID: `package-${pkg.name}` }, 12 | pick(pkg, props) 13 | ) 14 | 15 | if (pkg.repository && pkg.repository.https_url) { 16 | pkg.repository = pkg.repository.https_url 17 | } 18 | 19 | pkg.keyValuePairs = [ 20 | 'is:package', 21 | 'is:pkg', 22 | `pkg:${pkg.name}`, 23 | `package:${pkg.name}` 24 | ] 25 | 26 | if (Array.isArray(pkg.owners)) { 27 | pkg.owners.forEach(({ name }) => { 28 | if (!name) return 29 | pkg.keyValuePairs.push(`owner:${name}`) 30 | pkg.keyValuePairs.push(`author:${name}`) 31 | pkg.keyValuePairs.push(`maintainer:${name}`) 32 | }) 33 | } 34 | 35 | // algolia doesn't search on keys, so save all dep names in a searchable array 36 | if (pkg.dependencies) { 37 | pkg.depNames = Object.keys(pkg.dependencies) 38 | pkg.depNames.forEach(dep => { 39 | pkg.keyValuePairs.push(`dep:${dep}`) 40 | }) 41 | } 42 | 43 | if (pkg.devDependencies) { 44 | pkg.devDepNames = Object.keys(pkg.devDependencies) 45 | pkg.devDepNames.forEach(dep => { 46 | pkg.keyValuePairs.push(`dep:${dep}`) 47 | }) 48 | } 49 | 50 | return pkg 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /indices/tutorials.js: -------------------------------------------------------------------------------- 1 | const { chain } = require('lodash') 2 | const cheerio = require('cheerio') 3 | const AlgoliaIndex = require('../lib/algolia-index') 4 | 5 | module.exports = new AlgoliaIndex('tutorials', getRecords()) 6 | 7 | function getRecords () { 8 | return chain(Object.values(require('electron-i18n').docs['en-US'])) 9 | .filter(tutorial => { 10 | const { isApiDoc, isApiStructureDoc, slug } = tutorial 11 | return !isApiDoc && !isApiStructureDoc && slug !== 'README' 12 | }) 13 | .map(tutorial => { 14 | const { title, githubUrl, slug, sections, href } = tutorial 15 | const objectID = `tutorial-${slug}` 16 | const html = sections.map(section => section.html).join('\n\n') 17 | const body = cheerio.load(html).text() 18 | 19 | const keyValuePairs = [ 20 | 'is:doc', 21 | 'is:tutorial', 22 | `doc:${title}`, 23 | `doc:${slug}`, 24 | `tutorial:${title}`, 25 | `tutorial:${slug}` 26 | ] 27 | 28 | const url = `https://electronjs.org${href}` // href includes leading slash 29 | return { 30 | objectID, 31 | title, 32 | githubUrl, 33 | url, 34 | slug, 35 | body, 36 | keyValuePairs 37 | } 38 | }) 39 | .value() 40 | } 41 | -------------------------------------------------------------------------------- /lib/algolia-index.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { chunk } = require('lodash') 3 | const algoliasearch = require('algoliasearch') 4 | const countArrayValues = require('count-array-values') 5 | 6 | module.exports = class AlgoliaIndex { 7 | constructor (name, records) { 8 | this.name = name 9 | this.records = records 10 | this.validate() 11 | return this 12 | } 13 | 14 | validate () { 15 | assert(typeof this.name === 'string' && this.name.length, '`name` is required') 16 | assert(Array.isArray(this.records) && this.records.length, '`records` must be a non-empty array') 17 | 18 | // each ID is unique 19 | const objectIDs = this.records.map(record => record.objectID) 20 | const dupes = countArrayValues(objectIDs) 21 | .filter(({ value, count }) => count > 1) 22 | .map(({ value }) => value) 23 | assert(!dupes.length, `every objectID must be unique. dupes: ${dupes.join('; ')}`) 24 | 25 | this.records.forEach(record => { 26 | assert( 27 | typeof record.objectID === 'string' && record.objectID.length, 28 | `objectID must be a string. received: ${record.objectID}, ${JSON.stringify(record)}` 29 | ) 30 | 31 | assert( 32 | Array.isArray(record.keyValuePairs) && record.keyValuePairs.length, 33 | `keyValuePairs must be a non-empty array, , ${JSON.stringify(record)}` 34 | ) 35 | }) 36 | 37 | return true 38 | } 39 | 40 | async upload () { 41 | this.validate() 42 | 43 | const { ALGOLIA_APPLICATION_ID, ALGOLIA_API_KEY } = process.env 44 | const client = algoliasearch(ALGOLIA_APPLICATION_ID, ALGOLIA_API_KEY) 45 | await client.deleteIndex(this.name) 46 | const index = client.initIndex(this.name) 47 | const chunks = chunk(this.records, 1000) 48 | 49 | chunks.map(function (batch) { 50 | return index.addObjects(batch) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-algolia-indices", 3 | "version": "0.0.0-development", 4 | "description": "Searchable data about Electron APIs, Tutorials, Packages, Repos, and Apps", 5 | "repository": "https://github.com/electron/algolia-indices", 6 | "author": "zeke", 7 | "license": "MIT", 8 | "main": "index.js", 9 | "scripts": { 10 | "build": "node script/build.js", 11 | "preupload": "npm run test", 12 | "upload": "node script/upload.js", 13 | "update-data-sources": "script/update-data-sources.sh", 14 | "pretest": "npm run build", 15 | "test": "tape test.js | tap-summary --no-progress && standard --fix", 16 | "lint": "standard --fix", 17 | "start": "budo demo.js --live --no-debug --open --css demo.css", 18 | "repl": "local-repl", 19 | "semantic-release": "semantic-release" 20 | }, 21 | "dependencies": { 22 | "require-directory": "^2.1.1" 23 | }, 24 | "devDependencies": { 25 | "algoliasearch": "^4.9.0", 26 | "budo": "^11.2.0", 27 | "cheerio": "^1.0.0-rc.2", 28 | "count-array-values": "^1.2.1", 29 | "dotenv-safe": "^8.2.0", 30 | "electron-apps": "^1.8661.0", 31 | "electron-i18n": "^1.2963.0", 32 | "electron-npm-packages": "^4.1.2", 33 | "electron-releases": "^3.654.0", 34 | "github-slugger": "^1.2.0", 35 | "instantsearch.js": "^4.0.0", 36 | "is-url": "^1.2.4", 37 | "local-repl": "^4.0.0", 38 | "lodash": "^4.17.10", 39 | "nanohtml": "^1.9.0", 40 | "node-fetch": "^2.6.0", 41 | "semantic-release": "^17.2.1", 42 | "standard": "^16.0.3", 43 | "tap-summary": "^4.0.0", 44 | "tape": "^4.9.0" 45 | }, 46 | "files": [ 47 | "index.js", 48 | "dist/**/*" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Electron Algolia Indices 2 | 3 | > Searchable data about Electron APIs, Tutorials, Packages, Repos, and Apps 4 | 5 | ## Data Sources 6 | 7 | Type | Source 8 | ------------ | ----------- 9 | APIs | [electron-api.json](https://electronjs.org/blog/api-docs-json-schema) 10 | Tutorials | [electron/i18n](https://github.com/electron/i18n#usage) 11 | Packages | [electron/packages](https://ghub.io/electron-npm-packages) 12 | Repos | [electron/repos](https://github.com/electron/dependent-repos) 13 | Apps | [electron/apps](https://github.com/electron/apps) 14 | 15 | ## Demo 16 | 17 | See [electron-algolia.herokuapp.com](https://electron-algolia.herokuapp.com/) 18 | 19 | ## Development 20 | 21 | Try it out locally: 22 | 23 | ```sh 24 | git clone https://github.com/electron/algolia-indices 25 | cd algolia-indices 26 | npm install 27 | npm test 28 | npm start 29 | ``` 30 | 31 | ## License 32 | 33 | MIT 34 | -------------------------------------------------------------------------------- /script/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs') 4 | const path = require('path') 5 | const indices = require('../indices') 6 | 7 | Object.keys(indices).forEach(key => { 8 | fs.writeFileSync( 9 | path.join(__dirname, `../dist/${key}.json`), 10 | JSON.stringify(indices[key], null, 2) 11 | ) 12 | }) 13 | -------------------------------------------------------------------------------- /script/update-data-sources.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -v # print commands before execution, but don't expand env vars in output 4 | set -o errexit # always exit on error 5 | set -o pipefail # honor exit codes when piping 6 | set -o nounset # fail on unset variables 7 | 8 | # bootstrap 9 | git clone "https://electron-bot:$GH_TOKEN@github.com/electron/algolia-indices" module 10 | cd module 11 | npm ci 12 | 13 | # update stuff 14 | npm update electron-apps 15 | npm update electron-i18n 16 | npm update electron-npm-packages 17 | npm update electron-releases 18 | 19 | # Update electron-api.json 20 | node ./script/update-electron-apis.js 21 | # Buildes the indices 22 | npm run build 23 | npm test 24 | 25 | # bail if nothing changed 26 | if [ "$(git status --porcelain)" = "" ]; then 27 | echo "no new content found; goodbye!" 28 | exit 29 | fi 30 | 31 | # save changes in git 32 | git config user.email electron@github.com 33 | git config user.name electron-bot 34 | git add . 35 | git commit -am "feat: update data sources (auto-roll 🤖)" 36 | git pull --rebase && git push origin master --follow-tags 37 | -------------------------------------------------------------------------------- /script/update-electron-apis.js: -------------------------------------------------------------------------------- 1 | const i18n = require('electron-i18n') 2 | const electronReleases = require('electron-releases') 3 | const { deps: { version } } = electronReleases.find(release => release.version === i18n.electronLatestStableVersion) 4 | const fetch = require('node-fetch') 5 | const fs = require('fs') 6 | const path = require('path') 7 | 8 | fetch(`https://github.com/electron/electron/releases/download/v${version}/electron-api.json`) 9 | .then(res => { 10 | const dest = fs.createWriteStream(path.resolve(__dirname, '../electron-api.json')) 11 | res.body.pipe(dest) 12 | }) 13 | -------------------------------------------------------------------------------- /script/upload.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('dotenv-safe').config() 4 | 5 | const indices = require('../indices') 6 | 7 | for (const key in indices) { 8 | const index = indices[key] 9 | console.log(`Uploading index: ${index.name}`) 10 | index.upload() 11 | } 12 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const isURL = require('is-url') 3 | const indices = require('./indices') 4 | const exportedIndices = require('.') 5 | 6 | test('electron-search', t => { 7 | // ensure the object exported by `require('electron-algolia-indices')` 8 | // is the same as what we are testing in this file 9 | t.deepEqual(indices, exportedIndices, 'exports and indices object') 10 | 11 | // All Indices 12 | // ---------------------------------------------------------------------- 13 | const indexNames = ['apis', 'apps', 'glossary', 'packages', 'tutorials'] 14 | t.deepEqual(Object.keys(indices), indexNames, 'defines expected indexNames as keys') 15 | 16 | t.deepEqual(Object.values(indices).map(index => index.name), indexNames, 'has expected index names') 17 | 18 | Object.values(indices).forEach(index => { 19 | t.ok(index.validate(), `index is valid: ${index.name}`) 20 | }) 21 | 22 | // APIs 23 | // ---------------------------------------------------------------------- 24 | const apis = indices.apis.records 25 | 26 | t.ok(apis.length > 450, 'lots of APIs') 27 | 28 | const staticMethod = apis.find(api => api.fullSignature === 'Menu.getApplicationMenu()') 29 | t.equal(staticMethod.url, 'https://electronjs.org/docs/api/menu#menugetapplicationmenu', 'sets proper URL on static methods') 30 | 31 | const event = apis.find(api => api.fullSignature === "browserWindow.on('page-title-updated')") 32 | t.equal(event.url, 'https://electronjs.org/docs/api/browser-window#event-page-title-updated', 'sets expected URL on events') 33 | 34 | apis.forEach(api => { 35 | t.equal(typeof api.fullSignature, 'string', `${api.fullSignature} has a fullSignature`) 36 | t.equal(typeof api.name, 'string', `${api.fullSignature} has a name`) 37 | t.ok(!api.tldr || (api.tldr.endsWith('.') && !api.tldr.endsWith('..')), `${api.fullSignature} has a valid tldr, or no tldr`) 38 | t.ok(api.keyValuePairs.includes('is:api'), `${api.fullSignature} has is:api key-value pair`) 39 | t.ok(api.keyValuePairs.includes('is:doc'), `${api.fullSignature} has is:api key-value pair`) 40 | }) 41 | 42 | const apisWithTldrs = apis.filter(api => api.tldr && api.tldr.length > 10) 43 | const tldrThreshold = 80 44 | t.ok(apisWithTldrs.length / apis.length * 100 > tldrThreshold, `At least ${tldrThreshold}% of APIs have a tldr`) 45 | 46 | // method URLs should match the slugs generated by github.com (and hubdown) 47 | const apiUrls = apis.map(api => api.url) 48 | const expectedUrl = 'https://electronjs.org/docs/api/web-request#webrequestonheadersreceivedfilter-listener' 49 | t.ok(apiUrls.includes(expectedUrl), `API with URL exists: ${expectedUrl}`) 50 | 51 | // Tutorials 52 | // ---------------------------------------------------------------------- 53 | const tutorials = indices.tutorials.records 54 | 55 | t.ok(tutorials.length > 25, 'lots of tutorials') 56 | 57 | tutorials.forEach(tutorial => { 58 | if (!tutorial.title) console.log(tutorial) 59 | t.equal(typeof tutorial.title, 'string', `${tutorial.title} has a title`) 60 | t.equal(typeof tutorial.body, 'string', `${tutorial.title} has a body`) 61 | t.ok(isURL(tutorial.githubUrl), `${tutorial.title} has a valid GitHub URL`) 62 | t.ok(isURL(tutorial.url), `${tutorial.title} has a valid website URL`) 63 | t.ok(tutorial.keyValuePairs.includes('is:doc'), `${tutorial.title} has is:doc key-value pair`) 64 | t.ok(tutorial.keyValuePairs.includes('is:tutorial'), `${tutorial.title} has is:tutorial key-value pair`) 65 | }) 66 | 67 | // Glossary 68 | // ---------------------------------------------------------------------- 69 | const glossarys = indices.glossary.records 70 | 71 | t.ok(glossarys.length > 15, 'lots of glossary') 72 | 73 | glossarys.forEach(glossary => { 74 | if (!glossary.term) console.log(glossary) 75 | t.equal(typeof glossary.term, 'string', `${glossary.title} has a title`) 76 | t.equal(typeof glossary.description, 'string', `${glossary.title} has a body`) 77 | t.ok(isURL(glossary.url), `${glossary.title} has a valid website URL`) 78 | t.ok(glossary.keyValuePairs.includes('is:doc'), `${glossary.title} has is:doc key-value pair`) 79 | t.ok(glossary.keyValuePairs.includes('is:glossary'), `${glossary.title} has is:glossary key-value pair`) 80 | }) 81 | 82 | // Packages 83 | // ---------------------------------------------------------------------- 84 | const packages = indices.packages.records 85 | 86 | t.ok(packages.length > 25, 'lots of packages') 87 | 88 | packages.forEach(pkg => { 89 | if (!pkg.name) console.log(pkg) 90 | t.equal(typeof pkg.name, 'string', `${pkg.name} has a name`) 91 | t.ok(pkg.keyValuePairs.includes('is:pkg'), `${pkg.name} has is:pkg key-value pair`) 92 | t.ok(pkg.keyValuePairs.includes('is:pkg'), `${pkg.name} has is:package key-value pair`) 93 | // t.ok(isURL(pkg.githubUrl), `${pkg.title} has a valid GitHub URL`) 94 | // t.ok(isURL(pkg.repository), `${pkg.title} has a valid repository`) 95 | }) 96 | 97 | // Apps 98 | // ---------------------------------------------------------------------- 99 | const apps = indices.apps.records 100 | 101 | t.ok(apps.length > 500, 'lots of APPS') 102 | 103 | apps.forEach(app => { 104 | if (!app.name) console.log(app) 105 | t.equal(typeof app.name, 'string', `${app.name} has a name`) 106 | t.ok(app.keyValuePairs.includes('is:app'), `${app.name} has is:pkg key-value pair`) 107 | t.ok(app.keyValuePairs.includes('is:app'), `${app.name} has is:package key-value pair`) 108 | t.equal(typeof app.category, 'string', `${app.name} has category`) 109 | t.equal(typeof app.icon, 'string', `${app.name} has icon`) 110 | // Skipped, not all apps have a website or repository. 111 | // t.ok(isURL(app.website), `${app.title} has a valid website URL`) 112 | // t.ok(isURL(app.repository), `${app.title} has a valid repository`) 113 | }) 114 | 115 | // TODO: Repos 116 | // ---------------------------------------------------------------------- 117 | 118 | t.end() 119 | }) 120 | --------------------------------------------------------------------------------