Skip to content

Commit 31d68f9

Browse files
author
Orta
authored
Merge pull request #277 from microsoft/plugins
Plugins
2 parents ea0c4b7 + 240c58f commit 31d68f9

File tree

10 files changed

+230
-93
lines changed

10 files changed

+230
-93
lines changed

packages/create-typescript-playground-plugin/template/scripts/getDTS.js

Lines changed: 28 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ const go = async () => {
2121
mkdirSync(vendor)
2222
}
2323

24-
await getFileAndStoreLocally(
25-
'https://www.typescriptlang.org/v2/js/sandbox/tsWorker.d.ts',
26-
join(vendor, 'tsWorker.d.ts')
27-
)
24+
const host = 'https://www.typescriptlang.org/v2'
25+
// const host = "http://localhost:8000";
26+
27+
await getFileAndStoreLocally(host + '/js/sandbox/tsWorker.d.ts', join(vendor, 'tsWorker.d.ts'))
28+
29+
await getFileAndStoreLocally(host + '/js/playground/pluginUtils.d.ts', join(vendor, 'pluginUtils.d.ts'))
2830

2931
await getFileAndStoreLocally(
30-
'https://www.typescriptlang.org/v2/js/sandbox/vendor/typescript-vfs.d.ts',
32+
host + '/js/sandbox/vendor/typescript-vfs.d.ts',
3133
join(vendor, 'typescript-vfs.d.ts'),
3234
text => {
3335
const removeImports = text.replace('/// <reference types="lz-string" />', '')
@@ -36,36 +38,28 @@ const go = async () => {
3638
}
3739
)
3840

39-
await getFileAndStoreLocally(
40-
'https://www.typescriptlang.org/v2/js/sandbox/index.d.ts',
41-
join(vendor, 'sandbox.d.ts'),
42-
text => {
43-
const removeImports = text.replace(/^import/g, '// import').replace(/\nimport/g, '// import')
44-
const replaceTSVFS = removeImports.replace(
45-
"// import * as tsvfs from './vendor/typescript-vfs'",
46-
"import * as tsvfs from './typescript-vfs'"
47-
)
48-
const removedLZ = replaceTSVFS.replace('lzstring: typeof lzstring', '// lzstring: typeof lzstring')
49-
const addedTsWorkerImport = 'import { TypeScriptWorker } from "./tsWorker";' + removedLZ
50-
return addedTsWorkerImport
51-
}
52-
)
41+
await getFileAndStoreLocally(host + '/js/sandbox/index.d.ts', join(vendor, 'sandbox.d.ts'), text => {
42+
const removeImports = text.replace(/^import/g, '// import').replace(/\nimport/g, ']\n// import')
43+
const replaceTSVFS = removeImports.replace(
44+
"// import * as tsvfs from './vendor/typescript-vfs'",
45+
"\nimport * as tsvfs from './typescript-vfs'"
46+
)
47+
const removedLZ = replaceTSVFS.replace('lzstring: typeof lzstring', '// lzstring: typeof lzstring')
48+
const addedTsWorkerImport = 'import { TypeScriptWorker } from "./tsWorker";' + removedLZ
49+
return addedTsWorkerImport
50+
})
5351

54-
await getFileAndStoreLocally(
55-
'https://www.typescriptlang.org/v2/js/playground/index.d.ts',
56-
join(vendor, '/playground.d.ts'),
57-
text => {
58-
const replaceSandbox = text.replace(/typescript-sandbox/g, './sandbox')
59-
const replaceTSVFS = replaceSandbox.replace(
60-
/typescriptlang-org\/static\/js\/sandbox\/vendor\/typescript-vfs/g,
61-
'./typescript-vfs'
62-
)
63-
const removedLZ = replaceTSVFS.replace('lzstring: typeof', '// lzstring: typeof')
64-
const removedWorker = removedLZ.replace('getWorkerProcess', '// getWorkerProcess')
65-
const removedUI = removedWorker.replace('ui:', '// ui:')
66-
return removedUI
67-
}
68-
)
52+
await getFileAndStoreLocally(host + '/js/playground/index.d.ts', join(vendor, '/playground.d.ts'), text => {
53+
const replaceSandbox = text.replace(/typescript-sandbox/g, './sandbox')
54+
const replaceTSVFS = replaceSandbox.replace(
55+
/typescriptlang-org\/static\/js\/sandbox\/vendor\/typescript-vfs/g,
56+
'./typescript-vfs'
57+
)
58+
const removedLZ = replaceTSVFS.replace('lzstring: typeof', '// lzstring: typeof')
59+
const removedWorker = removedLZ.replace('getWorkerProcess', '// getWorkerProcess')
60+
const removedUI = removedWorker.replace('ui:', '// ui:')
61+
return removedUI
62+
})
6963
}
7064

7165
go()
Lines changed: 43 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,45 @@
1-
const customPlugin: import('./vendor/playground').PlaygroundPlugin = {
2-
id: 'my-plugin',
3-
displayName: 'New Plugin',
4-
5-
// This is called synchronously when a your plugin is about to be
6-
// presented in a tab. Container is a blank div in the sidebar
7-
// for you to present data in.
8-
didMount: (sandbox, container) => {
9-
console.log('Showing new plugin')
10-
11-
const p = document.createElement('p')
12-
p.textContent = 'Playground plugin defaults'
13-
container.appendChild(p)
14-
15-
const startButton = document.createElement('input')
16-
startButton.type = 'button'
17-
startButton.value = 'Change the code in the editor'
18-
container.appendChild(startButton)
19-
20-
startButton.onclick = () => {
21-
sandbox.setText('You clicked the button!')
22-
}
23-
},
24-
25-
// This is called occasionally as text changes in monaco,
26-
// it does not directly map 1 keyup to once run of the function
27-
// because it is intentionally called at most once every 0.3 seconds
28-
// and then will always run at the end.
29-
modelChangedDebounce: async (sandbox, _model) => {
30-
//
31-
},
32-
33-
// Gives you a chance to remove anything set up,
34-
// the container itself if wiped of children after this.
35-
didUnmount: () => {
36-
console.log('Removing plugin')
37-
},
1+
import type { PlaygroundPlugin, PluginUtils } from "./vendor/playground"
2+
3+
const makePlugin = (utils: PluginUtils) => {
4+
const customPlugin: PlaygroundPlugin = {
5+
id: 'example',
6+
displayName: 'Dev Example',
7+
didMount: (sandbox, container) => {
8+
console.log('Showing new plugin')
9+
10+
const p = (str: string) => utils.el(str, "p", container);
11+
const h4 = (str: string) => utils.el(str, "h4", container);
12+
13+
h4("Example Plugin")
14+
15+
p("This plugin has a button which changes the text in the editor, click below to test it.")
16+
17+
const startButton = document.createElement('input')
18+
startButton.type = 'button'
19+
startButton.value = 'Change the code in the editor'
20+
container.appendChild(startButton)
21+
22+
startButton.onclick = () => {
23+
sandbox.setText('// You clicked the button!')
24+
}
25+
},
26+
27+
// This is called occasionally as text changes in monaco,
28+
// it does not directly map 1 keyup to once run of the function
29+
// because it is intentionally called at most once every 0.3 seconds
30+
// and then will always run at the end.
31+
modelChangedDebounce: async (_sandbox, _model) => {
32+
// Do some work with the new text
33+
},
34+
35+
// Gives you a chance to remove anything set up,
36+
// the container itself if wiped of children after this.
37+
didUnmount: () => {
38+
console.log('Removing plugin')
39+
},
40+
}
41+
42+
return customPlugin
3843
}
3944

40-
export default customPlugin
45+
export default makePlugin

packages/playground/src/createConfigDropdown.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { Playground } from '.'
2-
3-
type Sandbox = ReturnType<typeof import('typescript-sandbox').createTypeScriptSandbox>
1+
type Sandbox = import('typescript-sandbox').Sandbox
42
type Monaco = typeof import('monaco-editor')
53

64
type OptionsSummary = {

packages/playground/src/createElements.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { PlaygroundPlugin } from '.'
22

3-
type Sandbox = ReturnType<typeof import('typescript-sandbox').createTypeScriptSandbox>
3+
type Sandbox = import('typescript-sandbox').Sandbox
44

55
export const createDragBar = () => {
66
const sidebar = document.createElement('div')

packages/playground/src/exporter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { UI } from './createUI'
22

3-
type Sandbox = ReturnType<typeof import('typescript-sandbox').createTypeScriptSandbox>
3+
type Sandbox = import('typescript-sandbox').Sandbox
44
type CompilerOptions = import('monaco-editor').languages.typescript.CompilerOptions
55

66
export const createExporter = (sandbox: Sandbox, monaco: typeof import('monaco-editor'), ui: UI) => {

packages/playground/src/index.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
type Sandbox = ReturnType<typeof import('typescript-sandbox').createTypeScriptSandbox>
1+
type Sandbox = import('typescript-sandbox').Sandbox
22
type Monaco = typeof import('monaco-editor')
33

44
declare const window: any
@@ -22,6 +22,8 @@ import { ExampleHighlighter } from './monaco/ExampleHighlight'
2222
import { createConfigDropdown, updateConfigDropdownForCompilerOptions } from './createConfigDropdown'
2323
import { showErrors } from './sidebar/showErrors'
2424
import { optionsPlugin, allowConnectingToLocalhost, activePlugins } from './sidebar/options'
25+
import { createUtils, PluginUtils } from './pluginUtils'
26+
export { PluginUtils } from './pluginUtils'
2527

2628
export type PluginFactory = {
2729
(i: (key: string, components?: any) => string): PlaygroundPlugin
@@ -345,6 +347,35 @@ export const setupPlayground = (
345347
console.log('\twindow.sandbox', window.sandbox)
346348
console.log('\twindow.playground', window.playground)
347349

350+
/** A plugin */
351+
const activateExternalPlugin = (
352+
plugin: PlaygroundPlugin | ((utils: PluginUtils) => PlaygroundPlugin),
353+
autoActivate: boolean
354+
) => {
355+
let readyPlugin: PlaygroundPlugin
356+
// Can either be a factory, or object
357+
if (typeof plugin === 'function') {
358+
const utils = createUtils(sandbox)
359+
readyPlugin = plugin(utils)
360+
} else {
361+
readyPlugin = plugin
362+
}
363+
364+
if (autoActivate) {
365+
console.log(readyPlugin)
366+
}
367+
368+
playground.registerPlugin(readyPlugin)
369+
370+
// Auto-select the dev plugin
371+
const pluginWantsFront = readyPlugin.shouldBeSelected && readyPlugin.shouldBeSelected()
372+
373+
if (pluginWantsFront || autoActivate) {
374+
// Auto-select the dev plugin
375+
activatePlugin(readyPlugin, currentPlugin(), sandbox, tabBar, container)
376+
}
377+
}
378+
348379
// Dev mode plugin
349380
if (allowConnectingToLocalhost()) {
350381
window.exports = {}
@@ -354,11 +385,14 @@ export const setupPlayground = (
354385
const re = window.require
355386
re(['local/index'], (devPlugin: any) => {
356387
console.log('Set up dev plugin from localhost:5000')
357-
console.log(devPlugin)
358-
playground.registerPlugin(devPlugin)
359-
360-
// Auto-select the dev plugin
361-
activatePlugin(devPlugin, currentPlugin(), sandbox, tabBar, container)
388+
try {
389+
activateExternalPlugin(devPlugin, true)
390+
} catch (error) {
391+
console.error(error)
392+
setTimeout(() => {
393+
ui.flashInfo('Error: Could not load dev plugin from localhost:5000')
394+
}, 700)
395+
}
362396
})
363397
} catch (error) {
364398
console.error('Problem loading up the dev plugin')
@@ -371,12 +405,7 @@ export const setupPlayground = (
371405
// @ts-ignore
372406
const re = window.require
373407
re([`unpkg/${plugin.module}@latest/dist/index`], (devPlugin: PlaygroundPlugin) => {
374-
playground.registerPlugin(devPlugin)
375-
376-
// Auto-select the dev plugin
377-
if (devPlugin.shouldBeSelected && devPlugin.shouldBeSelected()) {
378-
activatePlugin(devPlugin, currentPlugin(), sandbox, tabBar, container)
379-
}
408+
activateExternalPlugin(devPlugin, true)
380409
})
381410
} catch (error) {
382411
console.error('Problem loading up the plugin:', plugin)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type { Sandbox } from 'typescript-sandbox'
2+
import type { Node } from "typescript"
3+
4+
/** Creates a set of util functions which is exposed to Plugins to make it easier to build consistent UIs */
5+
export const createUtils = (sb: any) => {
6+
const sandbox: Sandbox = sb
7+
const ts = sandbox.ts
8+
9+
const requireURL = (path: string) => {
10+
// https://unpkg.com/browse/typescript-playground-presentation-mode@0.0.1/dist/x.js => unpkg/browse/typescript-playground-presentation-mode@0.0.1/dist/x
11+
const isDev = document.location.host.includes('localhost')
12+
const prefix = isDev ? 'local/' : 'unpkg/typescript-playground-presentation-mode/dist/'
13+
return prefix + path
14+
}
15+
16+
const el = (str: string, el: string, container: Element) => {
17+
const para = document.createElement(el)
18+
para.innerHTML = str
19+
container.appendChild(para)
20+
}
21+
22+
const createASTTree = (node: Node) => {
23+
const div = document.createElement('div')
24+
div.className = "ast"
25+
26+
const infoForNode = (node: Node) => {
27+
const name = ts.SyntaxKind[node.kind]
28+
return {
29+
name,
30+
}
31+
}
32+
33+
const renderLiteralField = (key: string, value: string) => {
34+
const li = document.createElement('li')
35+
li.innerHTML = `${key}: ${value}`
36+
return li
37+
}
38+
39+
const renderSingleChild = (key: string, value: Node) => {
40+
const li = document.createElement('li')
41+
li.innerHTML = `${key}: <strong>${ts.SyntaxKind[value.kind]}</strong>`
42+
return li
43+
}
44+
45+
const renderManyChildren = (key: string, value: Node[]) => {
46+
const li = document.createElement('li')
47+
const nodes = value.map(n => "<strong>&nbsp;&nbsp;" + ts.SyntaxKind[n.kind] + "<strong>").join("<br/>")
48+
li.innerHTML = `${key}: [<br/>${nodes}</br>]`
49+
return li
50+
}
51+
52+
const renderItem = (parentElement: Element, node: Node) => {
53+
const ul = document.createElement('ul')
54+
parentElement.appendChild(ul)
55+
ul.className = 'ast-tree'
56+
57+
const info = infoForNode(node)
58+
59+
const li = document.createElement('li')
60+
ul.appendChild(li)
61+
62+
const a = document.createElement('a')
63+
a.textContent = info.name
64+
li.appendChild(a)
65+
66+
const properties = document.createElement('ul')
67+
properties.className = 'ast-tree'
68+
li.appendChild(properties)
69+
70+
Object.keys(node).forEach(field => {
71+
if (typeof field === "function") return
72+
if (field === "parent" || field === "flowNode") return
73+
74+
const value = (node as any)[field]
75+
if (typeof value === "object" && Array.isArray(value) && "pos" in value[0] && "end" in value[0]) {
76+
// Is an array of Nodes
77+
properties.appendChild(renderManyChildren(field, value))
78+
} else if (typeof value === "object" && "pos" in value && "end" in value) {
79+
// Is a single child property
80+
properties.appendChild(renderSingleChild(field, value))
81+
} else {
82+
properties.appendChild(renderLiteralField(field, value))
83+
}
84+
})
85+
}
86+
87+
renderItem(div, node)
88+
return div
89+
}
90+
91+
92+
return {
93+
/** Use this to make a few dumb element generation funcs */
94+
el,
95+
/** Get a relative URL for something in your dist folder depending on if you're in dev mode or not */
96+
requireURL,
97+
/** Returns a div which has an interactive AST a TypeScript AST by passing in the root node */
98+
createASTTree
99+
}
100+
}
101+
102+
export type PluginUtils = ReturnType<typeof createUtils>

0 commit comments

Comments
 (0)