PoC Electron app (#264)
Initial version of Electron wrapper + build pipeline
This commit is contained in:
parent
558aee71fe
commit
fdc08d893a
79
.github/workflows/desktop.yml
vendored
Normal file
79
.github/workflows/desktop.yml
vendored
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
name: Build & Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build (${{ matrix.os }} - ${{ matrix.arch }})
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: macOS-latest
|
||||||
|
arch: arm64
|
||||||
|
- os: macOS-latest
|
||||||
|
arch: x64
|
||||||
|
- os: windows-latest
|
||||||
|
arch: x64
|
||||||
|
- os: ubuntu-latest
|
||||||
|
arch: x64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v3.5.1
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
cache: npm
|
||||||
|
cache-dependency-path: desktop/package-lock.json
|
||||||
|
- name: Setup Deno
|
||||||
|
# uses: denoland/setup-deno@v1
|
||||||
|
uses: denoland/setup-deno@d4873ceeec10de6275fecd1f94b6985369d40231
|
||||||
|
with:
|
||||||
|
deno-version: v1.29.1
|
||||||
|
- name: Build Silver Bullet
|
||||||
|
run: deno task build
|
||||||
|
- name: Create Silver Bullet bundle
|
||||||
|
run: deno task bundle
|
||||||
|
- name: Set MacOS signing certs
|
||||||
|
if: matrix.os == 'macOS-latest'
|
||||||
|
run: chmod +x scripts/add-macos-cert.sh && ./scripts/add-macos-cert.sh
|
||||||
|
env:
|
||||||
|
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
|
||||||
|
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
|
||||||
|
# - name: Set Windows signing certificate
|
||||||
|
# if: matrix.os == 'windows-latest'
|
||||||
|
# continue-on-error: true
|
||||||
|
# id: write_file
|
||||||
|
# uses: timheuer/base64-to-file@v1
|
||||||
|
# with:
|
||||||
|
# fileName: 'win-certificate.pfx'
|
||||||
|
# encodedString: ${{ secrets.WINDOWS_CODESIGN_P12 }}
|
||||||
|
- name: Install npm dependencies
|
||||||
|
run: npm install
|
||||||
|
working-directory: desktop
|
||||||
|
- name: Build application
|
||||||
|
run: npm run make -- --arch=${{ matrix.arch }}
|
||||||
|
working-directory: desktop
|
||||||
|
env:
|
||||||
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
|
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
#WINDOWS_CODESIGN_FILE: ${{ steps.write_file.outputs.filePath }}
|
||||||
|
#WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
draft: true
|
||||||
|
files: |
|
||||||
|
desktop/out/**/*.deb
|
||||||
|
desktop/out/**/*.dmg
|
||||||
|
desktop/out/**/*Setup.exe
|
||||||
|
desktop/out/**/*.nupkg
|
||||||
|
desktop/out/**/*.rpm
|
||||||
|
desktop/out/**/*.zip
|
||||||
|
desktop/out/**/RELEASES
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
# uses: denoland/setup-deno@v1
|
# uses: denoland/setup-deno@v1
|
||||||
uses: denoland/setup-deno@d4873ceeec10de6275fecd1f94b6985369d40231
|
uses: denoland/setup-deno@d4873ceeec10de6275fecd1f94b6985369d40231
|
||||||
with:
|
with:
|
||||||
deno-version: v1.28.1
|
deno-version: v1.29.1
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: deno task build
|
run: deno task build
|
||||||
|
16
desktop/.eslintrc.json
Normal file
16
desktop/.eslintrc.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"plugin:import/electron",
|
||||||
|
"plugin:import/typescript"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser"
|
||||||
|
}
|
92
desktop/.gitignore
vendored
Normal file
92
desktop/.gitignore
vendored
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
resources
|
||||||
|
deno-download*
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# nuxt.js build output
|
||||||
|
.nuxt
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# Webpack
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Electron-Forge
|
||||||
|
out/
|
130
desktop/forge.config.ts
Normal file
130
desktop/forge.config.ts
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import type { ForgeConfig } from "@electron-forge/shared-types";
|
||||||
|
import type { TargetArch } from "electron-packager";
|
||||||
|
import { MakerSquirrel } from "@electron-forge/maker-squirrel";
|
||||||
|
import { MakerZIP } from "@electron-forge/maker-zip";
|
||||||
|
import { MakerDeb } from "@electron-forge/maker-deb";
|
||||||
|
import { MakerRpm } from "@electron-forge/maker-rpm";
|
||||||
|
import { WebpackPlugin } from "@electron-forge/plugin-webpack";
|
||||||
|
|
||||||
|
import { mainConfig } from "./webpack.main.config";
|
||||||
|
import { rendererConfig } from "./webpack.renderer.config";
|
||||||
|
import { platform } from "node:os";
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import decompress from "decompress";
|
||||||
|
import { downloadFile } from "./http_util";
|
||||||
|
|
||||||
|
const denoVersion = "v1.29.1";
|
||||||
|
|
||||||
|
const denoZip: Record<string, string> = {
|
||||||
|
"win32-x64": "deno-x86_64-pc-windows-msvc.zip",
|
||||||
|
"darwin-x64": "deno-x86_64-apple-darwin.zip",
|
||||||
|
"darwin-arm64": "deno-aarch64-apple-darwin.zip",
|
||||||
|
"linux-x64": "deno-x86_64-unknown-linux-gnu.zip",
|
||||||
|
};
|
||||||
|
|
||||||
|
const denoExecutableResource = platform() === "win32"
|
||||||
|
? "resources/deno.exe"
|
||||||
|
: "resources/deno";
|
||||||
|
|
||||||
|
async function downloadDeno(platform: string, arch: string): Promise<void> {
|
||||||
|
const folder = fs.mkdtempSync("deno-download");
|
||||||
|
const destFile = path.join(folder, "deno.zip");
|
||||||
|
const zipFile = denoZip[`${platform}-${arch}`];
|
||||||
|
if (!zipFile) {
|
||||||
|
throw new Error(`No deno binary for ${platform}-${arch}`);
|
||||||
|
}
|
||||||
|
await downloadFile(
|
||||||
|
`https://github.com/denoland/deno/releases/download/${denoVersion}/${zipFile}`,
|
||||||
|
destFile,
|
||||||
|
);
|
||||||
|
await decompress(destFile, "resources");
|
||||||
|
fs.rmSync(folder, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: ForgeConfig = {
|
||||||
|
packagerConfig: {
|
||||||
|
name: process.platform === "linux" ? "silverbullet" : "SilverBullet",
|
||||||
|
executableName: process.platform === "linux"
|
||||||
|
? "silverbullet"
|
||||||
|
: "SilverBullet",
|
||||||
|
icon: "../web/images/logo",
|
||||||
|
appBundleId: "md.silverbullet",
|
||||||
|
extraResource: [denoExecutableResource, "resources/silverbullet.js"],
|
||||||
|
beforeCopyExtraResources: [(
|
||||||
|
_buildPath: string,
|
||||||
|
_electronVersion: string,
|
||||||
|
platform: TargetArch,
|
||||||
|
arch: TargetArch,
|
||||||
|
callback: (err?: Error | null) => void,
|
||||||
|
) => {
|
||||||
|
if (fs.existsSync(denoExecutableResource)) {
|
||||||
|
fs.rmSync(denoExecutableResource, { force: true });
|
||||||
|
}
|
||||||
|
Promise.resolve().then(async () => {
|
||||||
|
// Download deno
|
||||||
|
await downloadDeno(platform, arch);
|
||||||
|
// Copy silverbullet.js
|
||||||
|
fs.copyFileSync("../dist/silverbullet.js", "resources/silverbullet.js");
|
||||||
|
}).then((r) => callback()).catch(callback);
|
||||||
|
}],
|
||||||
|
osxSign: true,
|
||||||
|
},
|
||||||
|
rebuildConfig: {},
|
||||||
|
makers: [
|
||||||
|
new MakerSquirrel({}),
|
||||||
|
new MakerZIP({}, ["darwin", "linux"]),
|
||||||
|
new MakerRpm({}),
|
||||||
|
new MakerDeb({}),
|
||||||
|
],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
new WebpackPlugin({
|
||||||
|
port: 3001,
|
||||||
|
mainConfig,
|
||||||
|
renderer: {
|
||||||
|
config: rendererConfig,
|
||||||
|
|
||||||
|
entryPoints: [
|
||||||
|
{
|
||||||
|
// html: "./src/index.html",
|
||||||
|
// js: "./src/renderer.ts",
|
||||||
|
name: "main_window",
|
||||||
|
preload: {
|
||||||
|
js: "./src/preload.ts",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function notarizeMaybe() {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.CI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.APPLE_ID || !process.env.APPLE_ID_PASSWORD) {
|
||||||
|
console.warn(
|
||||||
|
"Should be notarizing, but environment variables APPLE_ID or APPLE_ID_PASSWORD are missing!",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
config.packagerConfig!.osxNotarize = {
|
||||||
|
appleId: process.env.APPLE_ID!,
|
||||||
|
appleIdPassword: process.env.APPLE_ID_PASSWORD!,
|
||||||
|
teamId: process.env.APPLE_TEAM_ID!,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
notarizeMaybe();
|
||||||
|
|
||||||
|
export default config;
|
29
desktop/http_util.ts
Normal file
29
desktop/http_util.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
export async function downloadFile(
|
||||||
|
url: string,
|
||||||
|
destFile: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const file = fs.createWriteStream(destFile);
|
||||||
|
let response = await axios.request({
|
||||||
|
url: url,
|
||||||
|
method: "GET",
|
||||||
|
responseType: "stream",
|
||||||
|
});
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
response.data.pipe(file);
|
||||||
|
let error: Error | null = null;
|
||||||
|
file.on("error", (e) => {
|
||||||
|
error = e;
|
||||||
|
reject(e);
|
||||||
|
});
|
||||||
|
file.on("close", () => {
|
||||||
|
if (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file.close();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
19081
desktop/package-lock.json
generated
Normal file
19081
desktop/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
desktop/package.json
Normal file
53
desktop/package.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"name": "silverbullet",
|
||||||
|
"version": "0.0.2",
|
||||||
|
"description": "Markdown as a platform",
|
||||||
|
"main": ".webpack/main",
|
||||||
|
"scripts": {
|
||||||
|
"start": "electron-forge start",
|
||||||
|
"package": "electron-forge package",
|
||||||
|
"make": "electron-forge make",
|
||||||
|
"publish": "electron-forge publish",
|
||||||
|
"lint": "eslint --ext .ts,.tsx .",
|
||||||
|
"clean": "rm -rf out"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"repository": "github:silverbulletmd/silverbullet",
|
||||||
|
"author": {
|
||||||
|
"name": "Zef Hemel",
|
||||||
|
"email": "zef@zef.me"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"@electron-forge/cli": "^6.0.4",
|
||||||
|
"@electron-forge/maker-deb": "^6.0.4",
|
||||||
|
"@electron-forge/maker-rpm": "^6.0.4",
|
||||||
|
"@electron-forge/maker-squirrel": "^6.0.4",
|
||||||
|
"@electron-forge/maker-zip": "^6.0.4",
|
||||||
|
"@electron-forge/plugin-webpack": "^6.0.4",
|
||||||
|
"@types/decompress": "^4.2.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
||||||
|
"@typescript-eslint/parser": "^5.47.1",
|
||||||
|
"@vercel/webpack-asset-relocator-loader": "^1.7.3",
|
||||||
|
"css-loader": "^6.7.3",
|
||||||
|
"electron": "22.0.0",
|
||||||
|
"eslint": "^8.31.0",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
|
"fork-ts-checker-webpack-plugin": "^7.2.14",
|
||||||
|
"node-loader": "^2.0.0",
|
||||||
|
"style-loader": "^3.3.1",
|
||||||
|
"ts-loader": "^9.4.2",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"typescript": "~4.5.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@electron-forge/publisher-github": "^6.0.4",
|
||||||
|
"axios": "^1.2.2",
|
||||||
|
"decompress": "^4.2.1",
|
||||||
|
"electron-squirrel-startup": "^1.0.0",
|
||||||
|
"electron-store": "^8.1.0",
|
||||||
|
"node-fetch": "^3.3.0",
|
||||||
|
"portfinder": "^1.0.32",
|
||||||
|
"update-electron-app": "^2.0.1"
|
||||||
|
}
|
||||||
|
}
|
56
desktop/src/index.ts
Normal file
56
desktop/src/index.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { app, BrowserWindow, Menu } from "electron";
|
||||||
|
import { openFolder, openFolderPicker } from "./instance";
|
||||||
|
import { menu } from "./menu";
|
||||||
|
import { getOpenWindows } from "./store";
|
||||||
|
|
||||||
|
// This allows TypeScript to pick up the magic constants that's auto-generated by Forge's Webpack
|
||||||
|
// plugin that tells the Electron app where to look for the Webpack-bundled app code (depending on
|
||||||
|
// whether you're running in development or production).
|
||||||
|
declare const MAIN_WINDOW_WEBPACK_ENTRY: string;
|
||||||
|
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
|
||||||
|
|
||||||
|
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||||
|
if (require("electron-squirrel-startup")) {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto updater
|
||||||
|
require("update-electron-app")();
|
||||||
|
|
||||||
|
async function boot() {
|
||||||
|
const openWindows = getOpenWindows();
|
||||||
|
if (openWindows.length === 0) {
|
||||||
|
await openFolderPicker();
|
||||||
|
} else {
|
||||||
|
for (const window of openWindows) {
|
||||||
|
// Doing this sequentially to avoid race conditions in starting servers
|
||||||
|
await openFolder(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method will be called when Electron has finished
|
||||||
|
// initialization and is ready to create browser windows.
|
||||||
|
// Some APIs can only be used after this event occurs.
|
||||||
|
app.on("ready", () => {
|
||||||
|
Menu.setApplicationMenu(menu);
|
||||||
|
console.log("App data path", app.getPath("userData"));
|
||||||
|
boot().catch(console.error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
|
// for applications and their menu bar to stay active until the user quits
|
||||||
|
// explicitly with Cmd + Q.
|
||||||
|
app.on("window-all-closed", () => {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on("activate", () => {
|
||||||
|
// On OS X it's common to re-create a window in the app when the
|
||||||
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
boot();
|
||||||
|
}
|
||||||
|
});
|
247
desktop/src/instance.ts
Normal file
247
desktop/src/instance.ts
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
import { ChildProcessWithoutNullStreams, spawn } from "node:child_process";
|
||||||
|
import { app, BrowserWindow, dialog, Menu, MenuItem, shell } from "electron";
|
||||||
|
import portfinder from "portfinder";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import { existsSync } from "node:fs";
|
||||||
|
import { platform } from "node:os";
|
||||||
|
import {
|
||||||
|
newWindowState,
|
||||||
|
persistWindowState,
|
||||||
|
removeWindow,
|
||||||
|
WindowState,
|
||||||
|
} from "./store";
|
||||||
|
|
||||||
|
declare const MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY: string;
|
||||||
|
|
||||||
|
type Instance = {
|
||||||
|
folder: string;
|
||||||
|
port: number;
|
||||||
|
// Increased with "browser-window-created" event, decreased wtih "close" event
|
||||||
|
refcount: number;
|
||||||
|
proc: ChildProcessWithoutNullStreams;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runningServers = new Map<string, Instance>();
|
||||||
|
|
||||||
|
// Should work for Liux and Mac
|
||||||
|
let denoPath = `${process.resourcesPath}/deno`;
|
||||||
|
|
||||||
|
// If not...
|
||||||
|
if (!existsSync(denoPath)) {
|
||||||
|
// Windows
|
||||||
|
if (platform() === "win32") {
|
||||||
|
if (existsSync(`${process.resourcesPath}/deno.exe`)) {
|
||||||
|
denoPath = `${process.resourcesPath}/deno.exe`;
|
||||||
|
} else {
|
||||||
|
denoPath = "deno.exe";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Everything else
|
||||||
|
denoPath = "deno";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function folderPicker(): Promise<string> {
|
||||||
|
const dialogReturn = await dialog.showOpenDialog({
|
||||||
|
title: "Pick a page folder",
|
||||||
|
properties: ["openDirectory", "createDirectory"],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (dialogReturn.filePaths.length === 1) {
|
||||||
|
return dialogReturn.filePaths[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openFolderPicker() {
|
||||||
|
const folderPath = await folderPicker();
|
||||||
|
if (folderPath) {
|
||||||
|
openFolder(newWindowState(folderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openFolder(windowState: WindowState): Promise<void> {
|
||||||
|
const instance = await spawnInstance(windowState.folderPath);
|
||||||
|
newWindow(instance, windowState);
|
||||||
|
}
|
||||||
|
|
||||||
|
function determineSilverBulletScriptPath(): string {
|
||||||
|
let scriptPath = `${process.resourcesPath}/silverbullet.js`;
|
||||||
|
if (!existsSync(scriptPath)) {
|
||||||
|
console.log("Dev mode");
|
||||||
|
// Assumption: we're running in dev mode (npm start)
|
||||||
|
return "../silverbullet.ts";
|
||||||
|
}
|
||||||
|
const userData = app.getPath("userData");
|
||||||
|
if (existsSync(`${userData}/silverbullet.js`)) {
|
||||||
|
// Custom downloaded (upgraded) version
|
||||||
|
scriptPath = `${userData}/silverbullet.js`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return scriptPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function spawnInstance(pagePath: string): Promise<Instance> {
|
||||||
|
let instance = runningServers.get(pagePath);
|
||||||
|
if (instance) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick random port
|
||||||
|
portfinder.setBasePort(3010);
|
||||||
|
portfinder.setHighestPort(3999);
|
||||||
|
const port = await portfinder.getPortPromise();
|
||||||
|
|
||||||
|
const proc = spawn(denoPath, [
|
||||||
|
"run",
|
||||||
|
"-A",
|
||||||
|
"--unstable",
|
||||||
|
determineSilverBulletScriptPath(),
|
||||||
|
"--port",
|
||||||
|
"" + port,
|
||||||
|
pagePath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
proc.stdout.on("data", (data) => {
|
||||||
|
process.stdout.write(`[SB Out] ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.stderr.on("data", (data) => {
|
||||||
|
process.stderr.write(`[SB Err] ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("close", (code) => {
|
||||||
|
if (code) {
|
||||||
|
console.log(`child process exited with code ${code}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try for 15s to see if SB is live
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
try {
|
||||||
|
const result = await fetch(`http://localhost:${port}`);
|
||||||
|
if (result.ok) {
|
||||||
|
console.log("Live!");
|
||||||
|
instance = {
|
||||||
|
folder: pagePath,
|
||||||
|
port: port,
|
||||||
|
refcount: 0,
|
||||||
|
proc: proc,
|
||||||
|
};
|
||||||
|
runningServers.set(pagePath, instance);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
console.log("Still booting...");
|
||||||
|
} catch {
|
||||||
|
console.log("Still booting...");
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make more specific
|
||||||
|
export function findInstanceByUrl(url: URL) {
|
||||||
|
for (const instance of runningServers.values()) {
|
||||||
|
if (instance.port === +url.port) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let quitting = false;
|
||||||
|
|
||||||
|
export function newWindow(instance: Instance, windowState: WindowState) {
|
||||||
|
// Create the browser window.
|
||||||
|
const window = new BrowserWindow({
|
||||||
|
height: windowState.height,
|
||||||
|
width: windowState.width,
|
||||||
|
x: windowState.x,
|
||||||
|
y: windowState.y,
|
||||||
|
webPreferences: {
|
||||||
|
preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
instance.refcount++;
|
||||||
|
|
||||||
|
persistWindowState(windowState, window);
|
||||||
|
|
||||||
|
window.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
const instance = findInstanceByUrl(new URL(url));
|
||||||
|
if (instance) {
|
||||||
|
newWindow(instance, newWindowState(instance.folder));
|
||||||
|
} else {
|
||||||
|
shell.openExternal(url);
|
||||||
|
}
|
||||||
|
return { action: "deny" };
|
||||||
|
});
|
||||||
|
|
||||||
|
window.webContents.on("context-menu", (event, params) => {
|
||||||
|
const menu = new Menu();
|
||||||
|
|
||||||
|
// Allow users to add the misspelled word to the dictionary
|
||||||
|
if (params.misspelledWord) {
|
||||||
|
// Add each spelling suggestion
|
||||||
|
for (const suggestion of params.dictionarySuggestions) {
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: suggestion,
|
||||||
|
click: () => window.webContents.replaceMisspelling(suggestion),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (params.dictionarySuggestions.length > 0) {
|
||||||
|
menu.append(new MenuItem({ type: "separator" }));
|
||||||
|
}
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: "Add to dictionary",
|
||||||
|
click: () =>
|
||||||
|
window.webContents.session.addWordToSpellCheckerDictionary(
|
||||||
|
params.misspelledWord,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
menu.append(new MenuItem({ type: "separator" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.append(new MenuItem({ label: "Cut", role: "cut" }));
|
||||||
|
menu.append(new MenuItem({ label: "Copy", role: "copy" }));
|
||||||
|
menu.append(new MenuItem({ label: "Paste", role: "paste" }));
|
||||||
|
menu.popup();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.on("resized", () => {
|
||||||
|
console.log("Reized window");
|
||||||
|
persistWindowState(windowState, window);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.on("moved", () => {
|
||||||
|
persistWindowState(windowState, window);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.webContents.on("did-navigate-in-page", () => {
|
||||||
|
persistWindowState(windowState, window);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.once("close", () => {
|
||||||
|
console.log("Closed window");
|
||||||
|
instance.refcount--;
|
||||||
|
console.log("Refcount", instance.refcount);
|
||||||
|
if (!quitting) {
|
||||||
|
removeWindow(windowState);
|
||||||
|
}
|
||||||
|
if (instance.refcount === 0) {
|
||||||
|
console.log("Stopping server");
|
||||||
|
instance.proc.kill();
|
||||||
|
runningServers.delete(instance.folder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.loadURL(`http://localhost:${instance.port}${windowState.urlPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
console.log("Quitting");
|
||||||
|
quitting = true;
|
||||||
|
});
|
161
desktop/src/menu.ts
Normal file
161
desktop/src/menu.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import { app, Menu, MenuItemConstructorOptions, shell } from "electron";
|
||||||
|
import { findInstanceByUrl, newWindow, openFolderPicker } from "./instance";
|
||||||
|
import { newWindowState } from "./store";
|
||||||
|
|
||||||
|
const template: MenuItemConstructorOptions[] = [
|
||||||
|
{
|
||||||
|
label: "File",
|
||||||
|
role: "fileMenu",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "New Window",
|
||||||
|
accelerator: "CommandOrControl+N",
|
||||||
|
click: (_item, win) => {
|
||||||
|
const url = new URL(win.webContents.getURL());
|
||||||
|
const instance = findInstanceByUrl(url);
|
||||||
|
if (instance) {
|
||||||
|
newWindow(instance, newWindowState(instance.folder));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Open Space",
|
||||||
|
accelerator: "CommandOrControl+Shift+O",
|
||||||
|
click: () => {
|
||||||
|
openFolderPicker();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "Quit",
|
||||||
|
accelerator: "CommandOrControl+Q",
|
||||||
|
role: "quit",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Edit",
|
||||||
|
role: "editMenu",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Undo",
|
||||||
|
accelerator: "CommandOrControl+Z",
|
||||||
|
role: "undo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Redo",
|
||||||
|
accelerator: "Shift+CommandOrControl+Z",
|
||||||
|
role: "redo",
|
||||||
|
},
|
||||||
|
{ type: "separator" },
|
||||||
|
{
|
||||||
|
label: "Cut",
|
||||||
|
accelerator: "CommandOrControl+X",
|
||||||
|
role: "cut",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Copy",
|
||||||
|
accelerator: "CommandOrControl+C",
|
||||||
|
role: "copy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Paste",
|
||||||
|
accelerator: "CommandOrControl+V",
|
||||||
|
role: "paste",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Paste and match style",
|
||||||
|
accelerator: "CommandOrControl+Shift+V",
|
||||||
|
role: "pasteAndMatchStyle",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Select All",
|
||||||
|
accelerator: "CommandOrControl+A",
|
||||||
|
role: "selectAll",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Navigate",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Home",
|
||||||
|
accelerator: "Alt+h",
|
||||||
|
click: (_item, win) => {
|
||||||
|
win.loadURL(new URL(win.webContents.getURL()).origin);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Reload",
|
||||||
|
accelerator: "CommandOrControl+r",
|
||||||
|
role: "forceReload",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Back",
|
||||||
|
accelerator: "CommandOrControl+[",
|
||||||
|
click: (_item, win) => {
|
||||||
|
win.webContents.goBack();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Forward",
|
||||||
|
accelerator: "CommandOrControl+]",
|
||||||
|
click: (_item, win) => {
|
||||||
|
win.webContents.goForward();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Develop",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Open in Browser",
|
||||||
|
click: (_item, win) => {
|
||||||
|
shell.openExternal(win.webContents.getURL());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Open Space Folder",
|
||||||
|
click: (_item, win) => {
|
||||||
|
let url = win.webContents.getURL();
|
||||||
|
shell.openPath(findInstanceByUrl(new URL(url)).folder);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Toggle Dev Tools",
|
||||||
|
accelerator: "CommandOrControl+Alt+J",
|
||||||
|
role: "toggleDevTools",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Window",
|
||||||
|
role: "windowMenu",
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: "Minimize",
|
||||||
|
accelerator: "CommandOrControl+M",
|
||||||
|
role: "minimize",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Maximize",
|
||||||
|
click: (_item, win) => {
|
||||||
|
win.maximize();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Close",
|
||||||
|
accelerator: "CommandOrControl+W",
|
||||||
|
role: "close",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
const name = app.getName();
|
||||||
|
template.unshift({ label: name, submenu: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
export const menu = Menu.buildFromTemplate(template);
|
3
desktop/src/preload.ts
Normal file
3
desktop/src/preload.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// See the Electron documentation for details on how to use preload scripts:
|
||||||
|
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
|
||||||
|
console.log("Yo, I'm preload.ts!");
|
31
desktop/src/renderer.ts
Normal file
31
desktop/src/renderer.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* This file will automatically be loaded by webpack and run in the "renderer" context.
|
||||||
|
* To learn more about the differences between the "main" and the "renderer" context in
|
||||||
|
* Electron, visit:
|
||||||
|
*
|
||||||
|
* https://electronjs.org/docs/latest/tutorial/process-model
|
||||||
|
*
|
||||||
|
* By default, Node.js integration in this file is disabled. When enabling Node.js integration
|
||||||
|
* in a renderer process, please be aware of potential security implications. You can read
|
||||||
|
* more about security risks here:
|
||||||
|
*
|
||||||
|
* https://electronjs.org/docs/tutorial/security
|
||||||
|
*
|
||||||
|
* To enable Node.js integration in this file, open up `main.js` and enable the `nodeIntegration`
|
||||||
|
* flag:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* // Create the browser window.
|
||||||
|
* mainWindow = new BrowserWindow({
|
||||||
|
* width: 800,
|
||||||
|
* height: 600,
|
||||||
|
* webPreferences: {
|
||||||
|
* nodeIntegration: true
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
console.log('👋 This message is being logged by "renderer.js", included via webpack');
|
79
desktop/src/store.ts
Normal file
79
desktop/src/store.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { BrowserWindow } from "electron";
|
||||||
|
import Store from "electron-store";
|
||||||
|
|
||||||
|
export type WindowState = {
|
||||||
|
id: string; // random GUID
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
x?: number;
|
||||||
|
y?: number;
|
||||||
|
folderPath: string;
|
||||||
|
urlPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = new Store({
|
||||||
|
defaults: {
|
||||||
|
openWindows: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getOpenWindows(): WindowState[] {
|
||||||
|
return store.get("openWindows");
|
||||||
|
}
|
||||||
|
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
|
||||||
|
export function newWindowState(folderPath: string): WindowState {
|
||||||
|
return {
|
||||||
|
id: crypto.randomBytes(16).toString("hex"),
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
x: undefined,
|
||||||
|
y: undefined,
|
||||||
|
folderPath,
|
||||||
|
urlPath: "/",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function persistWindowState(
|
||||||
|
windowState: WindowState,
|
||||||
|
window: BrowserWindow,
|
||||||
|
) {
|
||||||
|
const [width, height] = window.getSize();
|
||||||
|
const [x, y] = window.getPosition();
|
||||||
|
windowState.height = height;
|
||||||
|
windowState.width = width;
|
||||||
|
windowState.x = x;
|
||||||
|
windowState.y = y;
|
||||||
|
const urlString = window.webContents.getURL();
|
||||||
|
if (urlString) {
|
||||||
|
windowState.urlPath = new URL(urlString).pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
let found = false;
|
||||||
|
const newWindows = getOpenWindows().map((win) => {
|
||||||
|
if (win.id === windowState.id) {
|
||||||
|
found = true;
|
||||||
|
return windowState;
|
||||||
|
} else {
|
||||||
|
return win;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!found) {
|
||||||
|
newWindows.push(windowState);
|
||||||
|
}
|
||||||
|
store.set(
|
||||||
|
"openWindows",
|
||||||
|
newWindows,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeWindow(windowState: WindowState) {
|
||||||
|
const newWindows = getOpenWindows().filter((win) =>
|
||||||
|
win.id !== windowState.id
|
||||||
|
);
|
||||||
|
store.set(
|
||||||
|
"openWindows",
|
||||||
|
newWindows,
|
||||||
|
);
|
||||||
|
}
|
19
desktop/tsconfig.json
Normal file
19
desktop/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES6",
|
||||||
|
"allowJs": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"outDir": "dist",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"paths": {
|
||||||
|
"*": ["node_modules/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
|
}
|
18
desktop/webpack.main.config.ts
Normal file
18
desktop/webpack.main.config.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { Configuration } from 'webpack';
|
||||||
|
|
||||||
|
import { rules } from './webpack.rules';
|
||||||
|
|
||||||
|
export const mainConfig: Configuration = {
|
||||||
|
/**
|
||||||
|
* This is the main entry point for your application, it's the first file
|
||||||
|
* that runs in the main process.
|
||||||
|
*/
|
||||||
|
entry: './src/index.ts',
|
||||||
|
// Put your normal webpack config below here
|
||||||
|
module: {
|
||||||
|
rules,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'],
|
||||||
|
},
|
||||||
|
};
|
12
desktop/webpack.plugins.ts
Normal file
12
desktop/webpack.plugins.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import type IForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const ForkTsCheckerWebpackPlugin: typeof IForkTsCheckerWebpackPlugin = require(
|
||||||
|
"fork-ts-checker-webpack-plugin",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const plugins = [
|
||||||
|
new ForkTsCheckerWebpackPlugin({
|
||||||
|
logger: "webpack-infrastructure",
|
||||||
|
}),
|
||||||
|
];
|
19
desktop/webpack.renderer.config.ts
Normal file
19
desktop/webpack.renderer.config.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { Configuration } from "webpack";
|
||||||
|
|
||||||
|
import { rules } from "./webpack.rules";
|
||||||
|
import { plugins } from "./webpack.plugins";
|
||||||
|
|
||||||
|
rules.push({
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [{ loader: "style-loader" }, { loader: "css-loader" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rendererConfig: Configuration = {
|
||||||
|
module: {
|
||||||
|
rules,
|
||||||
|
},
|
||||||
|
plugins,
|
||||||
|
resolve: {
|
||||||
|
extensions: [".js", ".ts", ".jsx", ".tsx", ".css"],
|
||||||
|
},
|
||||||
|
};
|
31
desktop/webpack.rules.ts
Normal file
31
desktop/webpack.rules.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import type { ModuleOptions } from 'webpack';
|
||||||
|
|
||||||
|
export const rules: Required<ModuleOptions>['rules'] = [
|
||||||
|
// Add support for native node modules
|
||||||
|
{
|
||||||
|
// We're specifying native_modules in the test because the asset relocator loader generates a
|
||||||
|
// "fake" .node file which is really a cjs file.
|
||||||
|
test: /native_modules[/\\].+\.node$/,
|
||||||
|
use: 'node-loader',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /[/\\]node_modules[/\\].+\.(m?js|node)$/,
|
||||||
|
parser: { amd: false },
|
||||||
|
use: {
|
||||||
|
loader: '@vercel/webpack-asset-relocator-loader',
|
||||||
|
options: {
|
||||||
|
outputAssetBase: 'native_modules',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
exclude: /(node_modules|\.webpack)/,
|
||||||
|
use: {
|
||||||
|
loader: 'ts-loader',
|
||||||
|
options: {
|
||||||
|
transpileOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
23
scripts/add-macos-cert.sh
Executable file
23
scripts/add-macos-cert.sh
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
KEY_CHAIN=build.keychain
|
||||||
|
MACOS_CERT_P12_FILE=certificate.p12
|
||||||
|
|
||||||
|
# Recreate the certificate from the secure environment variable
|
||||||
|
echo $MACOS_CERT_P12 | base64 --decode > $MACOS_CERT_P12_FILE
|
||||||
|
|
||||||
|
#create a keychain
|
||||||
|
security create-keychain -p actions $KEY_CHAIN
|
||||||
|
|
||||||
|
# Make the keychain the default so identities are found
|
||||||
|
security default-keychain -s $KEY_CHAIN
|
||||||
|
|
||||||
|
# Unlock the keychain
|
||||||
|
security unlock-keychain -p actions $KEY_CHAIN
|
||||||
|
|
||||||
|
security import $MACOS_CERT_P12_FILE -k $KEY_CHAIN -P $MACOS_CERT_PASSWORD -T /usr/bin/codesign;
|
||||||
|
|
||||||
|
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
|
||||||
|
|
||||||
|
# remove certs
|
||||||
|
rm -fr *.p12
|
BIN
web/images/logo.icns
Normal file
BIN
web/images/logo.icns
Normal file
Binary file not shown.
@ -155,7 +155,7 @@ For the sake of simplicity, we will use the `page` data source and limit the res
|
|||||||
|--|--|--|--|--|--|--|--|--|
|
|--|--|--|--|--|--|--|--|--|
|
||||||
|Markdown |1669534332564|text/markdown|1022|rw| | | | |
|
|Markdown |1669534332564|text/markdown|1022|rw| | | | |
|
||||||
|🔌 Graph View|1669388320673|text/markdown|1042|rw|plug|github:bbroeksema/silverbullet-graphview/graphview.plug.json|https://github.com/bbroeksema/silverbullet-graphview|Bertjan Broeksema|
|
|🔌 Graph View|1669388320673|text/markdown|1042|rw|plug|github:bbroeksema/silverbullet-graphview/graphview.plug.json|https://github.com/bbroeksema/silverbullet-graphview|Bertjan Broeksema|
|
||||||
|SETTINGS |1667053645895|text/markdown|169 |rw| | | | |
|
|SETTINGS |1671107145991|text/markdown|169 |rw| | | | |
|
||||||
<!-- /query -->
|
<!-- /query -->
|
||||||
|
|
||||||
|
|
||||||
@ -166,13 +166,13 @@ For the sake of simplicity, we will use the `page` data source and limit the res
|
|||||||
**Result:** Okay, this is what we wanted but there is also information such as `perm`, `type` and `lastModified` that we don't need.
|
**Result:** Okay, this is what we wanted but there is also information such as `perm`, `type` and `lastModified` that we don't need.
|
||||||
|
|
||||||
<!-- #query page where type = "plug" order by lastModified desc limit 5 -->
|
<!-- #query page where type = "plug" order by lastModified desc limit 5 -->
|
||||||
|name |lastModified |contentType |size|perm|type|repo |uri |author |share-support|
|
|name |lastModified |contentType |size|perm|type|uri |repo |author |share-support|
|
||||||
|--|--|--|--|--|--|--|--|--|--|
|
|--|--|--|--|--|--|--|--|--|--|
|
||||||
|🔌 Directive|1671044429696|text/markdown|2605|rw|plug|https://github.com/silverbulletmd/silverbullet | | | |
|
|🔌 KaTeX |1671723760117|text/markdown|1346|rw|plug|github:silverbulletmd/silverbullet-katex/katex.plug.json |https://github.com/silverbulletmd/silverbullet-katex |Zef Hemel| |
|
||||||
|🔌 Backlinks|1670833065065|text/markdown|960 |rw|plug|https://github.com/Willyfrog/silverbullet-backlinks|ghr:Willyfrog/silverbullet-backlinks|Guillermo Vayá| |
|
|🔌 Mermaid |1671723720005|text/markdown|1501|rw|plug|github:silverbulletmd/silverbullet-mermaid/mermaid.plug.json |https://github.com/silverbulletmd/silverbullet-mermaid |Zef Hemel| |
|
||||||
|🔌 Collab |1670435068917|text/markdown|2923|rw|plug|https://github.com/silverbulletmd/silverbullet | | |true|
|
|🔌 Mattermost|1671205865185|text/markdown|3535|rw|plug|github:silverbulletmd/silverbullet-mattermost/mattermost.plug.json|https://github.com/silverbulletmd/silverbullet-mattermost|Zef Hemel|true|
|
||||||
|🔌 Tasks |1669536555227|text/markdown|1231|rw|plug|https://github.com/silverbulletmd/silverbullet | | | |
|
|🔌 Share |1671205498955|text/markdown|694 |rw|plug| |https://github.com/silverbulletmd/silverbullet | | |
|
||||||
|🔌 Share |1669536545411|text/markdown|672 |rw|plug|https://github.com/silverbulletmd/silverbullet | | | |
|
|🔌 Directive |1671044959953|text/markdown|2605|rw|plug| |https://github.com/silverbulletmd/silverbullet | | |
|
||||||
<!-- /query -->
|
<!-- /query -->
|
||||||
|
|
||||||
#### 6.3 Query to select only certain fields
|
#### 6.3 Query to select only certain fields
|
||||||
@ -183,14 +183,14 @@ and `repo` columns and then sort by last modified time.
|
|||||||
**Result:** Okay, this is much better. However, I believe this needs a touch
|
**Result:** Okay, this is much better. However, I believe this needs a touch
|
||||||
from a visual perspective.
|
from a visual perspective.
|
||||||
|
|
||||||
<!-- #query page select name author repo uririri where type = "plug" order by lastModified desc limit 5 -->
|
<!-- #query page select name author repo uririrririrririrririrririri where type = "plug" order by lastModified desc limit 5 -->
|
||||||
|name |author |repo |ri|
|
|name |author |repo |ririrririrririri|
|
||||||
|--|--|--|--|
|
|--|--|--|--|
|
||||||
|🔌 Directive| |https://github.com/silverbulletmd/silverbullet ||
|
|🔌 KaTeX |Zef Hemel|https://github.com/silverbulletmd/silverbullet-katex ||
|
||||||
|🔌 Backlinks|Guillermo Vayá|https://github.com/Willyfrog/silverbullet-backlinks||
|
|🔌 Mermaid |Zef Hemel|https://github.com/silverbulletmd/silverbullet-mermaid ||
|
||||||
|🔌 Collab | |https://github.com/silverbulletmd/silverbullet ||
|
|🔌 Mattermost|Zef Hemel|https://github.com/silverbulletmd/silverbullet-mattermost||
|
||||||
|🔌 Tasks | |https://github.com/silverbulletmd/silverbullet ||
|
|🔌 Share | |https://github.com/silverbulletmd/silverbullet ||
|
||||||
|🔌 Share | |https://github.com/silverbulletmd/silverbullet ||
|
|🔌 Directive | |https://github.com/silverbulletmd/silverbullet ||
|
||||||
<!-- /query -->
|
<!-- /query -->
|
||||||
|
|
||||||
#### 6.4 Display the data in a format defined by a template
|
#### 6.4 Display the data in a format defined by a template
|
||||||
@ -199,12 +199,12 @@ from a visual perspective.
|
|||||||
|
|
||||||
**Result:** Here you go. This is the result we would like to achieve 🎉. Did you see how I used `render` and `template/plug` in a query? 🚀
|
**Result:** Here you go. This is the result we would like to achieve 🎉. Did you see how I used `render` and `template/plug` in a query? 🚀
|
||||||
|
|
||||||
<!-- #query page select name author repo uririri where type = "plug" order by lastModified desc limit 5 render [[template/plug]] -->
|
<!-- #query page select name author repo uririrririrririrririrririri where type = "plug" order by lastModified desc limit 5 render [[template/plug]] -->
|
||||||
* [[🔌 Directive]]
|
* [[🔌 KaTeX]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-katex))
|
||||||
* [[🔌 Backlinks]] by **Guillermo Vayá** ([repo](https://github.com/Willyfrog/silverbullet-backlinks))
|
* [[🔌 Mermaid]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mermaid))
|
||||||
* [[🔌 Collab]]
|
* [[🔌 Mattermost]] by **Zef Hemel** ([repo](https://github.com/silverbulletmd/silverbullet-mattermost))
|
||||||
* [[🔌 Tasks]]
|
|
||||||
* [[🔌 Share]]
|
* [[🔌 Share]]
|
||||||
|
* [[🔌 Directive]]
|
||||||
<!-- /query -->
|
<!-- /query -->
|
||||||
|
|
||||||
PS: You don't need to select only certain fields to use templates. Templates are
|
PS: You don't need to select only certain fields to use templates. Templates are
|
||||||
|
Loading…
Reference in New Issue
Block a user