Skip to content

Commit dde11f8

Browse files
Foam as Web Extension (#1395)
See #1290 for context. Major thanks to @pderaaij that did all the hard work here. * using js-sha1 instead of node's crypto to compute sha1 * Using esbuild to bundle native and web extension (WIP) * Added message regarding unsupported embeds in web extension * support for graph webview in web extension
1 parent cd9ee4d commit dde11f8

23 files changed

+207
-58
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
node_modules
22
.DS_Store
33
.vscode-test/
4+
.vscode-test-web/
45
*.tsbuildinfo
56
*.vsix
67
*.log

packages/foam-vscode/.vscodeignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ out/**/*.spec.*
66
test-data/**
77
src/**
88
jest.config.js
9+
esbuild.js
910
.test-workspace
1011
.gitignore
1112
vsc-extension-quickstart.md

packages/foam-vscode/esbuild.js

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// also see https://code.visualstudio.com/api/working-with-extensions/bundling-extension
2+
const assert = require('assert');
3+
const esbuild = require('esbuild');
4+
5+
// pass the platform to esbuild as an argument
6+
7+
function getPlatform() {
8+
const args = process.argv.slice(2);
9+
const pArg = args.find(arg => arg.startsWith('--platform='));
10+
if (pArg) {
11+
return pArg.split('=')[1];
12+
}
13+
throw new Error('No platform specified. Pass --platform <web|node>.');
14+
}
15+
16+
const platform = getPlatform();
17+
assert(['web', 'node'].includes(platform), 'Platform must be "web" or "node".');
18+
19+
const production = process.argv.includes('--production');
20+
const watch = process.argv.includes('--watch');
21+
22+
const config = {
23+
web: {
24+
platform: 'browser',
25+
format: 'cjs',
26+
outfile: `out/bundles/extension-web.js`,
27+
plugins: [
28+
{
29+
name: 'path-browserify',
30+
setup(build) {
31+
build.onResolve({ filter: /^path$/ }, args => {
32+
return { path: require.resolve('path-browserify') };
33+
});
34+
},
35+
},
36+
{
37+
name: 'wikilink-embed',
38+
setup(build) {
39+
build.onResolve({ filter: /wikilink-embed/ }, args => {
40+
return {
41+
path: require.resolve(
42+
args.resolveDir + '/wikilink-embed-web-extension.ts'
43+
),
44+
};
45+
});
46+
},
47+
},
48+
],
49+
},
50+
node: {
51+
platform: 'node',
52+
format: 'cjs',
53+
outfile: `out/bundles/extension-node.js`,
54+
plugins: [],
55+
},
56+
};
57+
58+
async function main() {
59+
const ctx = await esbuild.context({
60+
...config[platform],
61+
entryPoints: ['src/extension.ts'],
62+
bundle: true,
63+
minify: production,
64+
sourcemap: !production,
65+
sourcesContent: false,
66+
external: ['vscode'],
67+
logLevel: 'silent',
68+
plugins: [
69+
...config[platform].plugins,
70+
/* add to the end of plugins array */
71+
esbuildProblemMatcherPlugin,
72+
],
73+
});
74+
if (watch) {
75+
await ctx.watch();
76+
} else {
77+
await ctx.rebuild();
78+
await ctx.dispose();
79+
}
80+
}
81+
82+
/**
83+
* @type {import('esbuild').Plugin}
84+
*/
85+
const esbuildProblemMatcherPlugin = {
86+
name: 'esbuild-problem-matcher',
87+
88+
setup(build) {
89+
build.onStart(() => {
90+
console.log('[watch] build started');
91+
});
92+
build.onEnd(result => {
93+
result.errors.forEach(({ text, location }) => {
94+
console.error(`✘ [ERROR] ${text}`);
95+
console.error(
96+
` ${location.file}:${location.line}:${location.column}:`
97+
);
98+
});
99+
console.log('[watch] build finished');
100+
});
101+
},
102+
};
103+
104+
main().catch(e => {
105+
console.error(e);
106+
process.exit(1);
107+
});

packages/foam-vscode/package.json

+15-10
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"activationEvents": [
2222
"workspaceContains:.vscode/foam.json"
2323
],
24-
"main": "./out/extension.js",
24+
"main": "./out/bundles/extension-node.js",
25+
"browser": "./out/bundles/extension-web.js",
2526
"capabilities": {
2627
"untrustedWorkspaces": {
2728
"supported": "limited",
@@ -657,21 +658,23 @@
657658
]
658659
},
659660
"scripts": {
660-
"build": "tsc -p ./",
661-
"pretest": "yarn build",
662-
"test": "node ./out/test/run-tests.js",
663-
"pretest:unit": "yarn build",
664-
"test:unit": "node ./out/test/run-tests.js --unit",
665-
"pretest:e2e": "yarn build",
666-
"test:e2e": "node ./out/test/run-tests.js --e2e",
661+
"build:node": "node esbuild.js --platform=node",
662+
"build:web": "node esbuild.js --platform=web",
663+
"build": "yarn build:node && yarn build:web",
664+
"vscode:prepublish": "yarn clean && yarn build:node --production && yarn build:web --production",
665+
"compile": "tsc -p ./",
666+
"test-reset-workspace": "rm -rf .test-workspace && mkdir .test-workspace && touch .test-workspace/.keep",
667+
"test-setup": "yarn compile && yarn build && yarn test-reset-workspace",
668+
"test": "yarn test-setup && node ./out/test/run-tests.js",
669+
"test:unit": "yarn test-setup && node ./out/test/run-tests.js --unit",
670+
"test:e2e": "yarn test-setup && node ./out/test/run-tests.js --e2e",
667671
"lint": "dts lint src",
668672
"clean": "rimraf out",
669673
"watch": "tsc --build ./tsconfig.json --watch",
670674
"vscode:start-debugging": "yarn clean && yarn watch",
671-
"esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node",
672-
"vscode:prepublish": "yarn run esbuild-base -- --minify",
673675
"package-extension": "npx vsce package --yarn",
674676
"install-extension": "code --install-extension ./foam-vscode-$npm_package_version.vsix",
677+
"open-in-browser": "vscode-test-web --quality=stable --browser=chromium --extensionDevelopmentPath=. ",
675678
"publish-extension-openvsx": "npx ovsx publish foam-vscode-$npm_package_version.vsix -p $OPENVSX_TOKEN",
676679
"publish-extension-vscode": "npx vsce publish --packagePath foam-vscode-$npm_package_version.vsix",
677680
"publish-extension": "yarn publish-extension-vscode && yarn publish-extension-openvsx"
@@ -713,7 +716,9 @@
713716
"gray-matter": "^4.0.2",
714717
"lodash": "^4.17.21",
715718
"lru-cache": "^7.14.1",
719+
"js-sha1": "^0.7.0",
716720
"markdown-it-regex": "^0.2.0",
721+
"path-browserify": "^1.0.1",
717722
"remark-frontmatter": "^2.0.0",
718723
"remark-parse": "^8.0.2",
719724
"remark-wiki-link": "^0.0.4",

packages/foam-vscode/src/core/janitor/convert-links-format.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe('generateStdMdLink', () => {
5252
'[first-document](first-document.md)',
5353
];
5454
expect(actual.length).toEqual(expected.length);
55-
const _ = actual.map((LinkReplace, index) => {
55+
actual.forEach((LinkReplace, index) => {
5656
expect(LinkReplace.newText).toEqual(expected[index]);
5757
});
5858
});
@@ -64,7 +64,7 @@ describe('generateStdMdLink', () => {
6464
.map(link => convertLinkFormat(link, 'wikilink', _workspace, note));
6565
const expected: string[] = ['[[first-document|file]]'];
6666
expect(actual.length).toEqual(expected.length);
67-
const _ = actual.map((LinkReplace, index) => {
67+
actual.forEach((LinkReplace, index) => {
6868
expect(LinkReplace.newText).toEqual(expected[index]);
6969
});
7070
});

packages/foam-vscode/src/core/model/graph.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ResourceLink } from './note';
33
import { URI } from './uri';
44
import { FoamWorkspace } from './workspace';
55
import { IDisposable } from '../common/lifecycle';
6-
import { Logger, withTiming } from '../utils/log';
6+
import { Logger } from '../utils/log';
77
import { Emitter } from '../common/event';
88

99
export type Connection = {

packages/foam-vscode/src/core/model/location.ts

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Range } from './range';
22
import { URI } from './uri';
3-
import { ResourceLink } from './note';
43

54
/**
65
* Represents a location inside a resource, such as a line

packages/foam-vscode/src/core/model/uri.ts

-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,6 @@ function encode(uri: URI, skipEncoding: boolean): string {
195195
: encodeURIComponentMinimal;
196196

197197
let res = '';
198-
// eslint-disable-next-line prefer-const
199198
let { scheme, authority, path, query, fragment } = uri;
200199
if (scheme) {
201200
res += scheme;

packages/foam-vscode/src/core/services/markdown-parser.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -459,9 +459,9 @@ export const getBlockFor = (
459459
}
460460
});
461461

462-
let nLines = startLine == -1 ? 1 : endLine - startLine;
462+
let nLines = startLine === -1 ? 1 : endLine - startLine;
463463
let block =
464-
startLine == -1
464+
startLine === -1
465465
? lines[searchLine] ?? ''
466466
: lines.slice(startLine, endLine).join('\n');
467467

packages/foam-vscode/src/core/utils/core.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import crypto from 'crypto';
1+
import sha1 from 'js-sha1';
22

33
export function isNotNull<T>(value: T | null): value is T {
44
return value != null;
@@ -20,5 +20,4 @@ export function isNumeric(value: string): boolean {
2020
return /-?\d+$/.test(value);
2121
}
2222

23-
export const hash = (text: string) =>
24-
crypto.createHash('sha1').update(text).digest('hex');
23+
export const hash = (text: string) => sha1.sha1(text);

packages/foam-vscode/src/extension.ts

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { VsCodeWatcher } from './services/watcher';
1717
import { createMarkdownParser } from './core/services/markdown-parser';
1818
import VsCodeBasedParserCache from './services/cache';
1919
import { createMatcherAndDataStore } from './services/editor';
20-
import { getFoamVsCodeConfig } from './services/config';
2120

2221
export async function activate(context: ExtensionContext) {
2322
const logger = new VsCodeOutputLogger();

packages/foam-vscode/src/features/commands/create-note.spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { CREATE_NOTE_COMMAND, createNote } from './create-note';
1414
import { Location } from '../../core/model/location';
1515
import { Range } from '../../core/model/range';
1616
import { ResourceLink } from '../../core/model/note';
17-
import { MarkdownResourceProvider } from '../../core/services/markdown-provider';
1817
import { createMarkdownParser } from '../../core/services/markdown-parser';
1918

2019
describe('create-note command', () => {

packages/foam-vscode/src/features/commands/create-note.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export async function createNote(args: CreateNoteArgs, foam: Foam) {
129129
const edit = MarkdownLink.createUpdateLinkEdit(args.sourceLink.data, {
130130
target: identifier,
131131
});
132-
if (edit.newText != args.sourceLink.data.rawText) {
132+
if (edit.newText !== args.sourceLink.data.rawText) {
133133
const updateLink = new vscode.WorkspaceEdit();
134134
const uri = toVsCodeUri(args.sourceLink.uri);
135135
updateLink.replace(

packages/foam-vscode/src/features/navigation-provider.spec.ts

-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ import { FoamGraph } from '../core/model/graph';
1313
import { commandAsURI } from '../utils/commands';
1414
import { CREATE_NOTE_COMMAND } from './commands/create-note';
1515
import { Location } from '../core/model/location';
16-
import { URI } from '../core/model/uri';
17-
import { Range } from '../core/model/range';
18-
import { ResourceLink } from '../core/model/note';
1916

2017
describe('Document navigation', () => {
2118
const parser = createMarkdownParser([]);

packages/foam-vscode/src/features/panels/connections.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export default async function activate(
3030
treeDataProvider: provider,
3131
showCollapseAll: true,
3232
});
33-
const baseTitle = treeView.title;
3433

3534
const updateTreeView = async () => {
3635
provider.target = vscode.window.activeTextEditor
@@ -53,12 +52,7 @@ export default async function activate(
5352
}
5453

5554
export class ConnectionsTreeDataProvider extends BaseTreeProvider<vscode.TreeItem> {
56-
public show = new ContextMemento<'all links' | 'backlinks' | 'forward links'>(
57-
this.state,
58-
`foam-vscode.views.connections.show`,
59-
'all links',
60-
true
61-
);
55+
public show: ContextMemento<'all links' | 'backlinks' | 'forward links'>;
6256
public target?: URI = undefined;
6357
public nValues = 0;
6458
private connectionItems: ResourceRangeTreeItem[] = [];
@@ -70,6 +64,12 @@ export class ConnectionsTreeDataProvider extends BaseTreeProvider<vscode.TreeIte
7064
registerCommands = true // for testing. don't love it, but will do for now
7165
) {
7266
super();
67+
this.show = new ContextMemento<'all links' | 'backlinks' | 'forward links'>(
68+
this.state,
69+
`foam-vscode.views.connections.show`,
70+
'all links',
71+
true
72+
);
7373
if (!registerCommands) {
7474
return;
7575
}

packages/foam-vscode/src/features/panels/dataviz.ts

+16-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as vscode from 'vscode';
2-
import { TextDecoder } from 'util';
32
import { Foam } from '../../core/model/foam';
43
import { Logger } from '../../core/utils/log';
54
import { fromVsCodeUri } from '../../utils/vsc-utils';
@@ -167,28 +166,33 @@ async function getWebviewContent(
167166
context: vscode.ExtensionContext,
168167
panel: vscode.WebviewPanel
169168
) {
170-
const datavizPath = vscode.Uri.joinPath(
171-
vscode.Uri.file(context.extensionPath),
169+
const datavizUri = vscode.Uri.joinPath(
170+
context.extensionUri,
172171
'static',
173172
'dataviz'
174173
);
175-
176174
const getWebviewUri = (fileName: string) =>
177-
panel.webview.asWebviewUri(vscode.Uri.joinPath(datavizPath, fileName));
175+
panel.webview.asWebviewUri(vscode.Uri.joinPath(datavizUri, fileName));
178176

179-
const indexHtml = await vscode.workspace.fs.readFile(
180-
vscode.Uri.joinPath(datavizPath, 'index.html')
181-
);
177+
const indexHtml =
178+
vscode.env.uiKind === vscode.UIKind.Desktop
179+
? new TextDecoder('utf-8').decode(
180+
await vscode.workspace.fs.readFile(
181+
vscode.Uri.joinPath(datavizUri, 'index.html')
182+
)
183+
)
184+
: await fetch(getWebviewUri('index.html').toString()).then(r => r.text());
182185

183186
// Replace the script paths with the appropriate webview URI.
184-
const filled = new TextDecoder('utf-8')
185-
.decode(indexHtml)
186-
.replace(/data-replace (src|href)="[^"]+"/g, match => {
187+
const filled = indexHtml.replace(
188+
/data-replace (src|href)="[^"]+"/g,
189+
match => {
187190
const i = match.indexOf(' ');
188191
const j = match.indexOf('=');
189192
const uri = getWebviewUri(match.slice(j + 2, -1).trim());
190193
return match.slice(i + 1, j) + '="' + uri.toString() + '"';
191-
});
194+
}
195+
);
192196

193197
return filled;
194198
}

packages/foam-vscode/src/features/panels/notes-explorer.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -91,18 +91,20 @@ export class NotesProvider extends FolderTreeProvider<
9191
NotesTreeItems,
9292
Resource
9393
> {
94-
public show = new ContextMemento<'all' | 'notes-only'>(
95-
this.state,
96-
`foam-vscode.views.notes-explorer.show`,
97-
'all'
98-
);
94+
public show: ContextMemento<'all' | 'notes-only'>;
9995

10096
constructor(
10197
private workspace: FoamWorkspace,
10298
private graph: FoamGraph,
10399
private state: vscode.Memento
104100
) {
105101
super();
102+
this.show = new ContextMemento<'all' | 'notes-only'>(
103+
this.state,
104+
`foam-vscode.views.notes-explorer.show`,
105+
'all'
106+
);
107+
106108
this.disposables.push(
107109
vscode.commands.registerCommand(
108110
`foam-vscode.views.notes-explorer.show:all`,

0 commit comments

Comments
 (0)