├── .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 |
--------------------------------------------------------------------------------