From d2ddc7b6c6a4b238a79f29329ede24108569601b Mon Sep 17 00:00:00 2001 From: Elizabeth Craig Date: Mon, 28 Oct 2019 14:01:35 -0700 Subject: [PATCH] Editor: fix bugs, enable in demo app, update Monaco (#10973) * Update Monaco and enable editor in OUFR demo app * Change files * specify raw-loader dep * Fix invalid hook error --- apps/fabric-website-resources/package.json | 1 + .../src/index.bundle.ts | 8 --- apps/fabric-website-resources/src/index.tsx | 4 ++ .../webpack.config.js | 49 ++++++++++--------- .../webpack.serve.config.js | 33 +++++++------ apps/fabric-website/package.json | 3 +- ...c-website-2019-10-26-12-51-04-tsx-fix.json | 8 +++ ...resources-2019-10-26-12-51-04-tsx-fix.json | 8 +++ ...co-editor-2019-10-26-12-51-04-tsx-fix.json | 8 +++ ...sx-editor-2019-10-26-12-51-04-tsx-fix.json | 8 +++ packages/monaco-editor/README.md | 2 + packages/monaco-editor/monaco-typescript.d.ts | 26 ++++++++-- packages/monaco-editor/package.json | 2 +- .../scripts/addMonacoWebpackConfig.js | 17 ++++++- .../monaco-editor/src/configureEnvironment.ts | 24 ++++++--- packages/tsx-editor/package.json | 1 + .../tsx-editor/src/components/TsxEditor.tsx | 8 +-- .../exampleTransform.test.ts.snap | 8 +-- .../src/transpiler/exampleTransform.test.ts | 10 ++-- .../src/transpiler/exampleTransform.ts | 31 +++++++----- .../tsx-editor/src/transpiler/transpile.ts | 9 +++- .../src/utilities/defaultSupportedPackages.ts | 5 +- .../src/utilities/isEditorSupported.ts | 4 +- yarn.lock | 8 +-- 24 files changed, 191 insertions(+), 94 deletions(-) delete mode 100644 apps/fabric-website-resources/src/index.bundle.ts create mode 100644 change/@uifabric-fabric-website-2019-10-26-12-51-04-tsx-fix.json create mode 100644 change/@uifabric-fabric-website-resources-2019-10-26-12-51-04-tsx-fix.json create mode 100644 change/@uifabric-monaco-editor-2019-10-26-12-51-04-tsx-fix.json create mode 100644 change/@uifabric-tsx-editor-2019-10-26-12-51-04-tsx-fix.json diff --git a/apps/fabric-website-resources/package.json b/apps/fabric-website-resources/package.json index 8ea44079ab747..f9ffcc68d6f9f 100644 --- a/apps/fabric-website-resources/package.json +++ b/apps/fabric-website-resources/package.json @@ -71,6 +71,7 @@ "@uifabric/set-version": "^7.0.2", "@uifabric/styling": "^7.7.2", "@uifabric/theme-samples": "^7.0.5", + "@uifabric/tsx-editor": "^0.10.2", "@uifabric/utilities": "^7.5.0", "@uifabric/variants": "^7.0.5", "expect-puppeteer": "4.1.0", diff --git a/apps/fabric-website-resources/src/index.bundle.ts b/apps/fabric-website-resources/src/index.bundle.ts deleted file mode 100644 index c0aefb4f26837..0000000000000 --- a/apps/fabric-website-resources/src/index.bundle.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './index'; - -// Using the default import, include all icon definitions. Products that care -// about bundle size should not be using the main entry, until tree shaking -// is perfected. (Use the top level imports instead.) -import { initializeIcons } from 'office-ui-fabric-react/lib/Icons'; - -initializeIcons(); diff --git a/apps/fabric-website-resources/src/index.tsx b/apps/fabric-website-resources/src/index.tsx index 108fc4fe23d59..69bfc5d55256e 100644 --- a/apps/fabric-website-resources/src/index.tsx +++ b/apps/fabric-website-resources/src/index.tsx @@ -1,5 +1,9 @@ import { createDemoApp } from '@uifabric/example-app-base'; +import { configureEnvironment } from '@uifabric/tsx-editor'; import { AppDefinition } from './AppDefinition'; import { GettingStartedPage } from './GettingStartedPage'; +// Configure example editor +configureEnvironment({ baseUrl: '.', useMinified: false }); + createDemoApp(AppDefinition, GettingStartedPage); diff --git a/apps/fabric-website-resources/webpack.config.js b/apps/fabric-website-resources/webpack.config.js index 4fb17531df3e9..2dbccb785d324 100644 --- a/apps/fabric-website-resources/webpack.config.js +++ b/apps/fabric-website-resources/webpack.config.js @@ -1,37 +1,38 @@ let path = require('path'); -const resources = require('../../scripts/webpack/webpack-resources'); +const resources = require('@uifabric/build/webpack/webpack-resources'); +const { addMonacoWebpackConfig } = require('@uifabric/tsx-editor/scripts/addMonacoWebpackConfig'); const BUNDLE_NAME = 'office-ui-fabric-react'; const IS_PRODUCTION = process.argv.indexOf('--production') > -1; module.exports = [ - ...resources.createConfig(BUNDLE_NAME, IS_PRODUCTION, { - entry: { [BUNDLE_NAME]: './lib/index.bundle.js' }, + ...resources.createConfig( + BUNDLE_NAME, + IS_PRODUCTION, + addMonacoWebpackConfig({ + entry: { [BUNDLE_NAME]: './lib/index.js' }, - output: { - libraryTarget: 'var', - library: 'Fabric' - }, - - externals: [ - { - react: 'React' + output: { + libraryTarget: 'var', + library: 'Fabric' }, - { + + externals: { + react: 'React', 'react-dom': 'ReactDOM' - } - ], + }, - resolve: { - alias: { - 'office-ui-fabric-react$': path.resolve(__dirname, '../../packages/office-ui-fabric-react/lib'), - 'office-ui-fabric-react/src': path.resolve(__dirname, '../../packages/office-ui-fabric-react/src'), - 'office-ui-fabric-react/lib': path.resolve(__dirname, '../../packages/office-ui-fabric-react/lib'), - '@uifabric/api-docs/lib': path.resolve(__dirname, '../../packages/api-docs/lib'), - 'Props.ts.js': 'Props', - 'Example.tsx.js': 'Example' + resolve: { + alias: { + 'office-ui-fabric-react$': path.resolve(__dirname, '../../packages/office-ui-fabric-react/lib'), + 'office-ui-fabric-react/src': path.resolve(__dirname, '../../packages/office-ui-fabric-react/src'), + 'office-ui-fabric-react/lib': path.resolve(__dirname, '../../packages/office-ui-fabric-react/lib'), + '@uifabric/api-docs/lib': path.resolve(__dirname, '../../packages/api-docs/lib'), + 'Props.ts.js': 'Props', + 'Example.tsx.js': 'Example' + } } - } - }), + }) + ), require('./webpack.serve.config') ]; diff --git a/apps/fabric-website-resources/webpack.serve.config.js b/apps/fabric-website-resources/webpack.serve.config.js index f01918c7dcc1b..17b27e7aaea15 100644 --- a/apps/fabric-website-resources/webpack.serve.config.js +++ b/apps/fabric-website-resources/webpack.serve.config.js @@ -1,19 +1,22 @@ -const getResolveAlias = require('../../scripts/webpack/getResolveAlias'); -const resources = require('../../scripts/webpack/webpack-resources'); +const getResolveAlias = require('@uifabric/build/webpack/getResolveAlias'); +const resources = require('@uifabric/build/webpack/webpack-resources'); +const { addMonacoWebpackConfig } = require('@uifabric/tsx-editor/scripts/addMonacoWebpackConfig'); -module.exports = resources.createServeConfig({ - entry: './src/index.tsx', +const BUNDLE_NAME = 'demo-app'; - output: { - filename: 'demo-app.js' - }, +module.exports = resources.createServeConfig( + addMonacoWebpackConfig({ + entry: { + [BUNDLE_NAME]: './src/index.tsx' + }, - externals: { - react: 'React', - 'react-dom': 'ReactDOM' - }, + externals: { + react: 'React', + 'react-dom': 'ReactDOM' + }, - resolve: { - alias: getResolveAlias() - } -}); + resolve: { + alias: getResolveAlias() + } + }) +); diff --git a/apps/fabric-website/package.json b/apps/fabric-website/package.json index b0286ddbdef73..8f870925384b3 100644 --- a/apps/fabric-website/package.json +++ b/apps/fabric-website/package.json @@ -41,7 +41,8 @@ "react-dom": "16.8.6", "react-highlight": "0.10.0", "write-file-webpack-plugin": "^4.1.0", - "@uifabric/build": "^7.0.0" + "@uifabric/build": "^7.0.0", + "@uifabric/tsx-editor": "^0.10.2" }, "dependencies": { "@microsoft/load-themed-styles": "^1.7.13", diff --git a/change/@uifabric-fabric-website-2019-10-26-12-51-04-tsx-fix.json b/change/@uifabric-fabric-website-2019-10-26-12-51-04-tsx-fix.json new file mode 100644 index 0000000000000..76c727ca3f912 --- /dev/null +++ b/change/@uifabric-fabric-website-2019-10-26-12-51-04-tsx-fix.json @@ -0,0 +1,8 @@ +{ + "type": "none", + "comment": "Add missing tsx-editor dev dep", + "packageName": "@uifabric/fabric-website", + "email": "elcraig@microsoft.com", + "commit": "da81d25a70517a242b31f2b25ee8b07975b6e539", + "date": "2019-10-26T19:41:23.322Z" +} diff --git a/change/@uifabric-fabric-website-resources-2019-10-26-12-51-04-tsx-fix.json b/change/@uifabric-fabric-website-resources-2019-10-26-12-51-04-tsx-fix.json new file mode 100644 index 0000000000000..35af23a4b1edd --- /dev/null +++ b/change/@uifabric-fabric-website-resources-2019-10-26-12-51-04-tsx-fix.json @@ -0,0 +1,8 @@ +{ + "type": "minor", + "comment": "Enable live editor and update bundling", + "packageName": "@uifabric/fabric-website-resources", + "email": "elcraig@microsoft.com", + "commit": "da81d25a70517a242b31f2b25ee8b07975b6e539", + "date": "2019-10-26T19:41:10.088Z" +} diff --git a/change/@uifabric-monaco-editor-2019-10-26-12-51-04-tsx-fix.json b/change/@uifabric-monaco-editor-2019-10-26-12-51-04-tsx-fix.json new file mode 100644 index 0000000000000..07ed59514f5c9 --- /dev/null +++ b/change/@uifabric-monaco-editor-2019-10-26-12-51-04-tsx-fix.json @@ -0,0 +1,8 @@ +{ + "type": "minor", + "comment": "Pick up new Monaco version and try a different cross-domain worker loading method", + "packageName": "@uifabric/monaco-editor", + "email": "elcraig@microsoft.com", + "commit": "da81d25a70517a242b31f2b25ee8b07975b6e539", + "date": "2019-10-26T19:41:34.548Z" +} diff --git a/change/@uifabric-tsx-editor-2019-10-26-12-51-04-tsx-fix.json b/change/@uifabric-tsx-editor-2019-10-26-12-51-04-tsx-fix.json new file mode 100644 index 0000000000000..47abb4416f5f1 --- /dev/null +++ b/change/@uifabric-tsx-editor-2019-10-26-12-51-04-tsx-fix.json @@ -0,0 +1,8 @@ +{ + "type": "patch", + "comment": "Fix some minor bugs", + "packageName": "@uifabric/tsx-editor", + "email": "elcraig@microsoft.com", + "commit": "da81d25a70517a242b31f2b25ee8b07975b6e539", + "date": "2019-10-26T19:51:04.006Z" +} diff --git a/packages/monaco-editor/README.md b/packages/monaco-editor/README.md index 8691c47c79906..f2a916933b6f6 100644 --- a/packages/monaco-editor/README.md +++ b/packages/monaco-editor/README.md @@ -79,3 +79,5 @@ If you need types from `monaco-typescript`, import as follows: ```js import { TypeScriptWorker } from '@uifabric/monaco-editor/monaco-typescript.d'; ``` + +Note that you may run into conflicts with these types if you're on a different TypeScript version than the one the typings were generated against. diff --git a/packages/monaco-editor/monaco-typescript.d.ts b/packages/monaco-editor/monaco-typescript.d.ts index 10dd6de3383d6..af60cd42aa430 100644 --- a/packages/monaco-editor/monaco-typescript.d.ts +++ b/packages/monaco-editor/monaco-typescript.d.ts @@ -1,5 +1,10 @@ -// This file was generated by checking out and building monaco-typescript (https://github.com/Microsoft/monaco-typescript) -// and merging release/esm/*.d.ts together, with minor edits. +// This file may need to be re-generated when updating the monaco-editor version. Steps: +// 1. Clone https://github.com/Microsoft/monaco-typescript +// 2. npm install +// 3. Edit src/tsconfig.json and src/tsconfig.esm.json to include "declaration": true +// 4. npm run compile +// 5. Merge .d.ts files from release/esm into this file (unfortunately a manual process right now) +// 6. Resolve any type mismatch issues (likely caused by mismatches between our TS version and Monaco's) // merged imports from all files import * as ts from 'typescript'; @@ -19,6 +24,7 @@ import { Omit } from '@uifabric/utilities'; export type EmitOutput = ts.EmitOutput; // languageFeatures.d.ts +export declare function flattenDiagnosticMessageText(diag: string | ts.DiagnosticMessageChain | undefined, newLine: string, indent?: number): string; export declare abstract class Adapter { protected _worker: (first: Uri, ...more: Uri[]) => Promise; constructor(_worker: (first: Uri, ...more: Uri[]) => Promise); @@ -26,7 +32,7 @@ export declare abstract class Adapter { protected _offsetToPosition(uri: Uri, offset: number): monaco.IPosition; protected _textSpanToRange(uri: Uri, span: ts.TextSpan): monaco.IRange; } -export declare class DiagnostcsAdapter extends Adapter { +export declare class DiagnosticsAdapter extends Adapter { private _defaults; private _selector; private _disposables; @@ -35,6 +41,7 @@ export declare class DiagnostcsAdapter extends Adapter { dispose(): void; private _doValidate; private _convertDiagnostics; + private _tsDiagnosticCategoryToMarkerSeverity; } export declare class SuggestAdapter extends Adapter implements monaco.languages.CompletionItemProvider { readonly triggerCharacters: string[]; @@ -44,7 +51,7 @@ export declare class SuggestAdapter extends Adapter implements monaco.languages. } export declare class SignatureHelpAdapter extends Adapter implements monaco.languages.SignatureHelpProvider { signatureHelpTriggerCharacters: string[]; - provideSignatureHelp(model: monaco.editor.IReadOnlyModel, position: Position, token: CancellationToken): Thenable; + provideSignatureHelp(model: monaco.editor.IReadOnlyModel, position: Position, token: CancellationToken): Thenable; } export declare class QuickInfoAdapter extends Adapter implements monaco.languages.HoverProvider { provideHover(model: monaco.editor.IReadOnlyModel, position: Position, token: CancellationToken): Thenable; @@ -105,6 +112,13 @@ export declare class FormatOnTypeAdapter extends FormatHelper implements monaco. readonly autoFormatTriggerCharacters: string[]; provideOnTypeFormattingEdits(model: monaco.editor.IReadOnlyModel, position: Position, ch: string, options: monaco.languages.FormattingOptions, token: CancellationToken): Thenable; } +export declare class CodeActionAdaptor extends FormatHelper implements monaco.languages.CodeActionProvider { + provideCodeActions(model: monaco.editor.ITextModel, range: Range, context: monaco.languages.CodeActionContext, token: CancellationToken): Promise; + private _tsCodeFixActionToMonacoCodeAction; +} +export declare class RenameAdapter extends Adapter implements monaco.languages.RenameProvider { + provideRenameEdits(model: monaco.editor.ITextModel, position: Position, newName: string, token: CancellationToken): Promise; +} // monaco.contribution.d.ts export interface IExtraLib { @@ -164,6 +178,7 @@ export declare class TypeScriptWorker implements ts.LanguageServiceHost { private static clearFiles; getSyntacticDiagnostics(fileName: string): Promise; getSemanticDiagnostics(fileName: string): Promise; + getSuggestionDiagnostics(fileName: string): Promise; getCompilerOptionsDiagnostics(fileName: string): Promise; getCompletionsAtPosition(fileName: string, position: number): Promise; getCompletionEntryDetails(fileName: string, position: number, entry: string): Promise; @@ -176,7 +191,10 @@ export declare class TypeScriptWorker implements ts.LanguageServiceHost { getFormattingEditsForDocument(fileName: string, options: ts.FormatCodeOptions): Promise; getFormattingEditsForRange(fileName: string, start: number, end: number, options: ts.FormatCodeOptions): Promise; getFormattingEditsAfterKeystroke(fileName: string, postion: number, ch: string, options: ts.FormatCodeOptions): Promise; + findRenameLocations(fileName: string, positon: number, findInStrings: boolean, findInComments: boolean, providePrefixAndSuffixTextForRename: boolean): Promise; + getRenameInfo(fileName: string, positon: number, options: ts.RenameInfoOptions): Promise; getEmitOutput(fileName: string): Promise; + getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: number[], formatOptions: ts.FormatCodeOptions): Promise>; updateExtraLibs(extraLibs: IExtraLibs): void; } export interface ICreateData { diff --git a/packages/monaco-editor/package.json b/packages/monaco-editor/package.json index fafa0c20e0daf..1dea826cb9175 100644 --- a/packages/monaco-editor/package.json +++ b/packages/monaco-editor/package.json @@ -15,11 +15,11 @@ "just": "just-scripts" }, "devDependencies": { + "monaco-editor": "0.18.1", "@uifabric/build": "^7.0.0" }, "dependencies": { "@microsoft/load-themed-styles": "^1.7.13", - "monaco-editor": "0.17.1", "tslib": "^1.7.1" }, "optionalPeerDependencies": { diff --git a/packages/monaco-editor/scripts/addMonacoWebpackConfig.js b/packages/monaco-editor/scripts/addMonacoWebpackConfig.js index cd86854a35611..65ba2ea716b78 100644 --- a/packages/monaco-editor/scripts/addMonacoWebpackConfig.js +++ b/packages/monaco-editor/scripts/addMonacoWebpackConfig.js @@ -20,11 +20,25 @@ function addMonacoWebpackConfig(config, includeAllLanguages) { throw new Error('config passed to addMonacoConfig must be an object, not an array or function.'); } - const { entry, output, resolve } = config; + const { entry, output, externals, resolve } = config; if (!entry || typeof entry !== 'object') { throw new Error(`config.entry passed to addMonacoWebpackConfig must be an object. Got: ${JSON.stringify(entry)}`); } + // As of monaco-editor@0.18.1, typescriptServices.js includes a direct require for this package, + // which breaks webpack. Use an external to get rid of it (this works since the require is + // wrapped in a try/catch). Will be fixed once this merges and is published. + // https://github.com/microsoft/monaco-typescript/pull/49 + /** @type {webpack.ExternalsElement[]} */ + const newExternals = [{ '@microsoft/typescript-etw': 'FakeModule' }]; + if (externals) { + if (Array.isArray(externals)) { + newExternals.push(...externals); + } else { + newExternals.push(externals); + } + } + // Somewhat based on https://github.com/microsoft/monaco-editor/blob/master/docs/integrate-esm.md return { ...config, @@ -40,6 +54,7 @@ function addMonacoWebpackConfig(config, includeAllLanguages) { } : {}) }, + externals: newExternals, output: { ...output, globalObject: 'self' // required for monaco--see https://github.com/webpack/webpack/issues/6642 diff --git a/packages/monaco-editor/src/configureEnvironment.ts b/packages/monaco-editor/src/configureEnvironment.ts index 202b456a6ec9e..14e9b8504f9a1 100644 --- a/packages/monaco-editor/src/configureEnvironment.ts +++ b/packages/monaco-editor/src/configureEnvironment.ts @@ -4,8 +4,8 @@ export interface IMonacoConfig { /** Whether to use minified versions of the files (`.min.js`) */ useMinified?: boolean; /** - * Whether to use a configuration variant which works when this script lives - * on a different domain than the core Monaco scripts + * Whether to use a configuration variant which works when the worker script lives on a + * different domain than the website host (like a CDN). */ crossDomain?: boolean; } @@ -28,7 +28,7 @@ const labelMap: { [key: string]: string } = { json: 'json' }; -export function getMonacoConfig(): IMonacoConfig | undefined { +function getMonacoConfig(): IMonacoConfig | undefined { return ( globalObj.MonacoConfig || // TODO: remove once fabric-website homepage.htm is updated @@ -69,13 +69,23 @@ export function configureEnvironment(config?: IMonacoConfig): void { if (crossDomain) { // This is needed for cases where the JS files will be on a different domain (the CDN) - // instead of the domain the HTML is running on. Web workers (used by Monaco) can't be - // loaded by script residing on a different domain, so we use this proxy script on the - // main domain to load the worker script. (Also do this with localhost/devhost for testing.) + // instead of the domain the HTML is running on. Web worker scripts can't be directly + // loaded from a different domain, so we use this proxy. More info: // https://github.com/microsoft/monaco-editor/blob/master/docs/integrate-amd-cross.md - return 'data:text/javascript;charset=utf-8,' + encodeURIComponent(`importScripts("${path}");`); + // Plot twist! The approach of manually building a data URI suggested by those docs + // didn't work in Edge, but this blob approach seems to work everywhere. + // https://benohead.com/cross-domain-cross-browser-web-workers/ + const blob = new Blob([`importScripts("${path}")`], { type: 'application/javascript' }); + return URL.createObjectURL(blob); } return path; } }; } + +/** + * Returns true if either MonacoEnvironment or MonacoConfig is set. + */ +export function isConfigAvailable(): boolean { + return !!(globalObj.MonacoConfig || globalObj.MonacoEnvironment); +} diff --git a/packages/tsx-editor/package.json b/packages/tsx-editor/package.json index ee26b8339df56..2f5faa1df38a1 100644 --- a/packages/tsx-editor/package.json +++ b/packages/tsx-editor/package.json @@ -48,6 +48,7 @@ "@uifabric/monaco-editor": "^0.3.0", "@uifabric/react-hooks": "^7.0.1", "office-ui-fabric-react": "^7.55.2", + "raw-loader": "^0.5.1", "react-syntax-highlighter": "^10.1.3", "tslib": "^1.7.1", "typescript": "3.5.1" diff --git a/packages/tsx-editor/src/components/TsxEditor.tsx b/packages/tsx-editor/src/components/TsxEditor.tsx index 4db95287da84a..1665bc48db642 100644 --- a/packages/tsx-editor/src/components/TsxEditor.tsx +++ b/packages/tsx-editor/src/components/TsxEditor.tsx @@ -141,9 +141,11 @@ function _loadTypes(supportedPackages: IPackageGroup[]): Promise { // React must be loaded first promises.push( // @ts-ignore: this import is handled by webpack - import('!raw-loader!@types/react/index.d.ts') // prettier-ignore - .then((result: { default: string }) => { - typescriptDefaults.addExtraLib(result.default, `${typesPrefix}/react/index.d.ts`); + import('!raw-loader!@types/react/index.d.ts') + // raw-loader 0.x exports a single string, and later versions export a default. + // The package.json specifies 0.x, but handle either just in case. + .then((result: string | { default: string }) => { + typescriptDefaults.addExtraLib(typeof result === 'string' ? result : result.default, `${typesPrefix}/react/index.d.ts`); }) ); diff --git a/packages/tsx-editor/src/transpiler/__snapshots__/exampleTransform.test.ts.snap b/packages/tsx-editor/src/transpiler/__snapshots__/exampleTransform.test.ts.snap index 260b3bc782deb..ebf1c8e45ede7 100644 --- a/packages/tsx-editor/src/transpiler/__snapshots__/exampleTransform.test.ts.snap +++ b/packages/tsx-editor/src/transpiler/__snapshots__/exampleTransform.test.ts.snap @@ -2,7 +2,7 @@ exports[`example transform can return component 1`] = ` Object { - "output": "(function() { + "output": "(function(React) { const { Label, Fabric, initializeIcons } = window.Fabric; // Initialize icons in case this example uses them @@ -18,13 +18,13 @@ const LabelBasicExample = () => { const LabelBasicExampleWrapper = () => React.createElement(Fabric, null, React.createElement(LabelBasicExample, null)); return LabelBasicExampleWrapper; -})()", +})", } `; exports[`example transform can return component with transpiled example 1`] = ` Object { - "output": "(function() { + "output": "(function(React) { const { Label, Fabric, initializeIcons } = window.Fabric; // Initialize icons in case this example uses them @@ -37,7 +37,7 @@ var LabelBasicExample = function () { const LabelBasicExampleWrapper = () => React.createElement(Fabric, null, React.createElement(LabelBasicExample, null)); return LabelBasicExampleWrapper; -})()", +})", } `; diff --git a/packages/tsx-editor/src/transpiler/exampleTransform.test.ts b/packages/tsx-editor/src/transpiler/exampleTransform.test.ts index 4da53931ccf3e..abb5f01a824e6 100644 --- a/packages/tsx-editor/src/transpiler/exampleTransform.test.ts +++ b/packages/tsx-editor/src/transpiler/exampleTransform.test.ts @@ -65,12 +65,12 @@ describe('example transform', () => { }); it('can return component', () => { - const result = transformFile('function.txt', { returnComponent: true }); + const result = transformFile('function.txt', { returnFunction: true }); expect(result.output).toBeTruthy(); // no rendering expect(result.output).not.toContain('ReactDOM.render'); - // starts with IIFE - expect(result.output!.startsWith('(function() {')).toBe(true); + // starts with function + expect(result.output!.startsWith('(function(React) {')).toBe(true); // retuns component expect(result.output).toContain('return LabelBasicExampleWrapper'); // generates wrapper component @@ -81,10 +81,10 @@ describe('example transform', () => { }); it('can return component with transpiled example', () => { - const result = transformFile('function.txt', { useJs: true, returnComponent: true }); + const result = transformFile('function.txt', { useJs: true, returnFunction: true }); expect(result.output).toBeTruthy(); expect(result.output).not.toContain('ReactDOM.render'); - expect(result.output!.startsWith('(function() {')).toBe(true); + expect(result.output!.startsWith('(function(React) {')).toBe(true); expect(result.output).toContain('return LabelBasicExampleWrapper'); expect(result.output).toContain('LabelBasicExampleWrapper'); expect(result.output).not.toContain(' React.ComponentType; + export interface ITransformExampleParams { /** * TS for the example. Will be used to find imports/exports. Will also be used in the final @@ -19,13 +23,15 @@ export interface ITransformExampleParams { jsCode?: string; /** - * If true, the code will be transformed into an immediately invoked function expression which - * returns the component (rather than including a `ReactDOM.render(...)` line). If false, the code - * will not be wrapped in a function and will actually render the component. + * If false, the returned code will end with a `ReactDOM.render(...)` line and won't be wrapped + * in a function. + * If true, the returned code will be wrapped in a function of type `ExampleWrapperFunction`, + * which should be called with the correct local version of React (to avoid hook errors due to + * React mismatches in case there's a global React) and returns the component. */ - returnComponent?: boolean; + returnFunction?: boolean; - /** ID for the component to be rendered into (required unless `returnComponent` is true) */ + /** ID for the component to be rendered into (required unless `returnFunction` is true) */ id?: string; /** Supported package groups (React is implicitly supported) */ @@ -42,7 +48,7 @@ const win = getWindow() as * Transform an example for rendering in a browser context (example page or codepen). */ export function transformExample(params: ITransformExampleParams): ITransformedCode { - const { tsCode, jsCode, id = 'content', supportedPackages, returnComponent } = params; + const { tsCode, jsCode, id = 'content', supportedPackages, returnFunction } = params; // Imports or exports will be removed since they are not supported. const code = (jsCode || tsCode) @@ -85,8 +91,8 @@ export function transformExample(params: ITransformExampleParams): ITransformedC // and initialize icons in case the example uses them. finalComponent = component + 'Wrapper'; - // If immediately running the code, the component can't use JSX format - const wrapperCode = returnComponent + // If eval-ing the code, the component can't use JSX format + const wrapperCode = returnFunction ? `React.createElement(Fabric, null, React.createElement(${component}, null))` : `<${component} />`; lines.push('', `const ${finalComponent} = () => ${wrapperCode};`); @@ -107,11 +113,12 @@ export function transformExample(params: ITransformExampleParams): ITransformedC .map(globalName => `const { ${identifiersByGlobal[globalName].join(', ')} } = window.${globalName};`) .concat(lines); - if (returnComponent) { - // Wrap in IIFE - lines.unshift('(function() {'); + if (returnFunction) { + // Wrap in function, with the right React instance as a parameter. + // Parentheses allow the function to remain unnamed. + lines.unshift('(function(React) {'); lines.push(`return ${finalComponent};`); - lines.push('})()'); + lines.push('})'); } else { // Add render line lines.push(`ReactDOM.render(<${finalComponent} />, document.getElementById('${id}'))`); diff --git a/packages/tsx-editor/src/transpiler/transpile.ts b/packages/tsx-editor/src/transpiler/transpile.ts index 9d149bbe7e03e..45c8f0969fc8c 100644 --- a/packages/tsx-editor/src/transpiler/transpile.ts +++ b/packages/tsx-editor/src/transpiler/transpile.ts @@ -1,3 +1,4 @@ +import * as React from 'react'; import * as monaco from '@uifabric/monaco-editor'; import { TypeScriptWorker, EmitOutput } from '@uifabric/monaco-editor/monaco-typescript.d'; import { getWindow } from 'office-ui-fabric-react/lib/Utilities'; @@ -76,11 +77,15 @@ export function transpileAndEval(model: IMonacoTextModel, supportedPackages: IBa const transformedExample = transformExample({ tsCode: exampleTs, jsCode: transpileOutput.output, - returnComponent: true, + returnFunction: true, supportedPackages }); if (transformedExample.output) { - return { ...transformedExample, component: eval(transformedExample.output) }; + return { + ...transformedExample, + // Pass in the right React in case there's a different global one on the page... + component: eval(transformedExample.output)(React) + }; } else { return { error: transformedExample.error || 'Unknown error transforming example' }; } diff --git a/packages/tsx-editor/src/utilities/defaultSupportedPackages.ts b/packages/tsx-editor/src/utilities/defaultSupportedPackages.ts index 57b5a4d2b448b..abe81da5be4cf 100644 --- a/packages/tsx-editor/src/utilities/defaultSupportedPackages.ts +++ b/packages/tsx-editor/src/utilities/defaultSupportedPackages.ts @@ -35,7 +35,10 @@ if (typesContext) { packageName === '@uifabric/example-data' ? exampleDataGroup : packageName === '@uifabric/react-hooks' ? hooksGroup : fabricGroup; packageGroup.packages.push({ packageName, - loadTypes: () => typesContext!(dtsPath) + loadTypes: () => + // raw-loader 0.x exports a single string, and later versions export a default. + // The package.json specifies 0.x, but handle either just in case. + typesContext!(dtsPath).then((result: string | { default: string }) => (typeof result === 'string' ? result : result.default)) }); }); } else { diff --git a/packages/tsx-editor/src/utilities/isEditorSupported.ts b/packages/tsx-editor/src/utilities/isEditorSupported.ts index 27637d406790a..512f62aa9a321 100644 --- a/packages/tsx-editor/src/utilities/isEditorSupported.ts +++ b/packages/tsx-editor/src/utilities/isEditorSupported.ts @@ -1,5 +1,5 @@ import { getWindow, isIE11 } from 'office-ui-fabric-react/lib/Utilities'; -import { getMonacoConfig } from '@uifabric/monaco-editor/lib/configureEnvironment'; +import { isConfigAvailable } from '@uifabric/monaco-editor/lib/configureEnvironment'; import { isExampleValid } from '../transpiler/exampleParser'; import { getSetting } from './settings'; import { IBasicPackageGroup } from '../interfaces/packageGroup'; @@ -10,7 +10,7 @@ export function isEditorSupported(code: string, supportedPackages: IBasicPackage // Not server-side rendering !!win && // Required environment config available - !!getMonacoConfig() && + !!isConfigAvailable() && // Opt-out query param or session storage is not set getSetting('useEditor') !== '0' && // Not IE 11 diff --git a/yarn.lock b/yarn.lock index 8f736003ffd08..aaea616fc73ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10214,10 +10214,10 @@ moment@2.x.x, moment@^2.22.1: resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== -monaco-editor@0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.17.1.tgz#8fbe96ca54bfa75262706e044f8f780e904aa45c" - integrity sha512-JAc0mtW7NeO+0SwPRcdkfDbWLgkqL9WfP1NbpP9wNASsW6oWqgZqNIWt4teymGjZIXTElx3dnQmUYHmVrJ7HxA== +monaco-editor@0.18.1: + version "0.18.1" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.18.1.tgz#ced7c305a23109875feeaf395a504b91f6358cfc" + integrity sha512-fmL+RFZ2Hrezy+X/5ZczQW51LUmvzfcqOurnkCIRFTyjdVjzR7JvENzI6+VKBJzJdPh6EYL4RoWl92b2Hrk9fw== moo@^0.4.3: version "0.4.3"