use Webpack code-splitting to dynamically import Highlight.js languages as they are detected by Showdown Markdown extension
This commit is contained in:
parent
8dc2a99926
commit
41c12c853c
9 changed files with 70 additions and 33 deletions
|
@ -20,12 +20,11 @@ RUN virtualenv --distribute venv \
|
||||||
|
|
||||||
# Install front-end dependencies
|
# Install front-end dependencies
|
||||||
# JS depedencies
|
# JS depedencies
|
||||||
COPY yarn.lock ./
|
COPY yarn.lock package.json tsconfig.json webpack.config.js tslint.json ./
|
||||||
RUN yarn install --ignore-engines
|
RUN yarn install --ignore-engines
|
||||||
|
|
||||||
# JS compile
|
# JS compile
|
||||||
COPY static static
|
COPY static static
|
||||||
COPY package.json tsconfig.json webpack.config.js tslint.json ./
|
|
||||||
RUN yarn build \
|
RUN yarn build \
|
||||||
&& jpegoptim static/img/**/*.jpg \
|
&& jpegoptim static/img/**/*.jpg \
|
||||||
&& optipng -clobber -quiet static/img/**/*.png
|
&& optipng -clobber -quiet static/img/**/*.png
|
||||||
|
|
|
@ -56,10 +56,10 @@ def common_login(user_uuid, permanent_session=True):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _list_files(path, extension):
|
def _list_files(path, extension, contains=""):
|
||||||
""" Returns a list of all the files with the given extension found under the given path. """
|
""" Returns a list of all the files with the given extension found under the given path. """
|
||||||
def matches(f):
|
def matches(f):
|
||||||
return os.path.splitext(f)[1] == '.' + extension and f.split(os.path.extsep)[1] != 'spec'
|
return os.path.splitext(f)[1] == '.' + extension and contains in os.path.splitext(f)[0]
|
||||||
|
|
||||||
def join_path(dp, f):
|
def join_path(dp, f):
|
||||||
# Remove the static/ prefix. It is added in the template.
|
# Remove the static/ prefix. It is added in the template.
|
||||||
|
@ -74,7 +74,7 @@ def render_page_template(name, route_data=None, **kwargs):
|
||||||
library_styles = []
|
library_styles = []
|
||||||
main_styles = []
|
main_styles = []
|
||||||
library_scripts = []
|
library_scripts = []
|
||||||
main_scripts = _list_files('build', 'js')
|
main_scripts = _list_files('build', 'js', "bundle")
|
||||||
|
|
||||||
use_cdn = app.config.get('USE_CDN', True)
|
use_cdn = app.config.get('USE_CDN', True)
|
||||||
if request.args.get('use_cdn') is not None:
|
if request.args.get('use_cdn') is not None:
|
||||||
|
|
|
@ -53,8 +53,6 @@
|
||||||
"@types/core-js": "^0.9.39",
|
"@types/core-js": "^0.9.39",
|
||||||
"@types/jasmine": "^2.5.41",
|
"@types/jasmine": "^2.5.41",
|
||||||
"@types/jquery": "^2.0.40",
|
"@types/jquery": "^2.0.40",
|
||||||
"@types/react": "0.14.39",
|
|
||||||
"@types/react-dom": "0.14.17",
|
|
||||||
"@types/showdown": "^1.4.32",
|
"@types/showdown": "^1.4.32",
|
||||||
"angular-mocks": "1.6.2",
|
"angular-mocks": "1.6.2",
|
||||||
"css-loader": "0.25.0",
|
"css-loader": "0.25.0",
|
||||||
|
|
|
@ -29,6 +29,16 @@ describe("MarkdownEditorComponent", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("ngOnDestroy", () => {
|
||||||
|
|
||||||
|
it("removes 'beforeunload' event listener", () => {
|
||||||
|
$windowMock.setup(mock => mock.onbeforeunload).is(() => 1);
|
||||||
|
component.ngOnDestroy();
|
||||||
|
|
||||||
|
expect($windowMock.Object.onbeforeunload.call(this)).toEqual(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("changeEditMode", () => {
|
describe("changeEditMode", () => {
|
||||||
|
|
||||||
it("sets component's edit mode to given mode", () => {
|
it("sets component's edit mode to given mode", () => {
|
||||||
|
@ -147,7 +157,15 @@ describe("MarkdownEditorComponent", () => {
|
||||||
|
|
||||||
describe("discardChanges", () => {
|
describe("discardChanges", () => {
|
||||||
|
|
||||||
it("emits output event with no content", (done) => {
|
it("prompts user to confirm discarding changes", () => {
|
||||||
|
const confirmSpy: Spy = $windowMock.setup(mock => mock.confirm).is((message) => false).Spy;
|
||||||
|
component.discardChanges();
|
||||||
|
|
||||||
|
expect(confirmSpy.calls.argsFor(0)[0]).toEqual(`Are you sure you want to discard your changes?`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits output event with no content if user confirms discarding changes", (done) => {
|
||||||
|
$windowMock.setup(mock => mock.confirm).is((message) => true);
|
||||||
component.discard.subscribe((event: {}) => {
|
component.discard.subscribe((event: {}) => {
|
||||||
expect(event).toEqual({});
|
expect(event).toEqual({});
|
||||||
done();
|
done();
|
||||||
|
@ -155,5 +173,16 @@ describe("MarkdownEditorComponent", () => {
|
||||||
|
|
||||||
component.discardChanges();
|
component.discardChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not emit output event if user declines confirmation of discarding changes", (done) => {
|
||||||
|
$windowMock.setup(mock => mock.confirm).is((message) => false);
|
||||||
|
component.discard.subscribe((event: {}) => {
|
||||||
|
fail(`Should not emit output event`);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
component.discardChanges();
|
||||||
|
done();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, Inject, Input, Output, EventEmitter, ViewChild, HostListener } from 'ng-metadata/core';
|
import { Component, Inject, Input, Output, EventEmitter, ViewChild, HostListener, OnDestroy } from 'ng-metadata/core';
|
||||||
import { MarkdownSymbol } from '../../../types/common.types';
|
import { MarkdownSymbol } from '../../../types/common.types';
|
||||||
import { BrowserPlatform } from '../../../constants/platform.constant';
|
import { BrowserPlatform } from '../../../constants/platform.constant';
|
||||||
import './markdown-editor.component.css';
|
import './markdown-editor.component.css';
|
||||||
|
@ -11,7 +11,7 @@ import './markdown-editor.component.css';
|
||||||
selector: 'markdown-editor',
|
selector: 'markdown-editor',
|
||||||
templateUrl: '/static/js/directives/ui/markdown/markdown-editor.component.html'
|
templateUrl: '/static/js/directives/ui/markdown/markdown-editor.component.html'
|
||||||
})
|
})
|
||||||
export class MarkdownEditorComponent {
|
export class MarkdownEditorComponent implements OnDestroy {
|
||||||
|
|
||||||
@Input('<') public content: string;
|
@Input('<') public content: string;
|
||||||
|
|
||||||
|
@ -34,6 +34,10 @@ export class MarkdownEditorComponent {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
this.$window.onbeforeunload = () => null;
|
||||||
|
}
|
||||||
|
|
||||||
public changeEditMode(newMode: EditMode): void {
|
public changeEditMode(newMode: EditMode): void {
|
||||||
this.editMode = newMode;
|
this.editMode = newMode;
|
||||||
}
|
}
|
||||||
|
@ -110,8 +114,10 @@ export class MarkdownEditorComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
public discardChanges(): void {
|
public discardChanges(): void {
|
||||||
|
if (this.$window.confirm(`Are you sure you want to discard your changes?`)) {
|
||||||
this.discard.emit({});
|
this.discard.emit({});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public get currentEditMode(): EditMode {
|
public get currentEditMode(): EditMode {
|
||||||
return this.editMode;
|
return this.editMode;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { NgModule } from 'ng-metadata/core';
|
import { NgModule } from 'ng-metadata/core';
|
||||||
import { Converter, ConverterOptions } from 'showdown';
|
import { Converter } from 'showdown';
|
||||||
import * as showdown from 'showdown';
|
import * as showdown from 'showdown';
|
||||||
import { registerLanguage, highlightAuto } from 'highlight.js/lib/highlight';
|
import { registerLanguage, highlightAuto } from 'highlight.js/lib/highlight';
|
||||||
import 'highlight.js/styles/vs.css';
|
import 'highlight.js/styles/vs.css';
|
||||||
|
@ -10,14 +10,15 @@ const highlightedLanguages: string[] = require('../../../constants/highlighted-l
|
||||||
* Dynamically fetch and register a new language with Highlight.js
|
* Dynamically fetch and register a new language with Highlight.js
|
||||||
*/
|
*/
|
||||||
export const addHighlightedLanguage = (language: string): Promise<{}> => {
|
export const addHighlightedLanguage = (language: string): Promise<{}> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(async(resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
// TODO(alecmerdler): Use `import()` here instead of `require()`
|
// TODO(alecmerdler): Use `import()` here instead of `System.import()` after upgrading to TypeScript 2.4
|
||||||
const langModule = require(`highlight.js/lib/languages/${language}`);
|
const langModule = await System.import(`highlight.js/lib/languages/${language}`);
|
||||||
registerLanguage(language, langModule);
|
registerLanguage(language, langModule);
|
||||||
|
console.debug(`Language ${language} registered for syntax highlighting`);
|
||||||
resolve();
|
resolve();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Language ${language} not supported for syntax highlighting`);
|
console.debug(`Language ${language} not supported for syntax highlighting`);
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -25,7 +26,7 @@ export const addHighlightedLanguage = (language: string): Promise<{}> => {
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Showdown JS extension for syntax highlighting using Highlight.js
|
* Showdown JS extension for syntax highlighting using Highlight.js. Will attempt to register detected languages.
|
||||||
*/
|
*/
|
||||||
export const showdownHighlight = (): showdown.FilterExtension => {
|
export const showdownHighlight = (): showdown.FilterExtension => {
|
||||||
const htmlunencode = (text: string) => {
|
const htmlunencode = (text: string) => {
|
||||||
|
@ -39,7 +40,10 @@ export const showdownHighlight = (): showdown.FilterExtension => {
|
||||||
const right = '</code></pre>';
|
const right = '</code></pre>';
|
||||||
const flags = 'g';
|
const flags = 'g';
|
||||||
const replacement = (wholeMatch: string, match: string, leftSide: string, rightSide: string) => {
|
const replacement = (wholeMatch: string, match: string, leftSide: string, rightSide: string) => {
|
||||||
// TODO(alecmerdler): Call `addHighlightedLanguage` to load new languages that are detected using code-splitting
|
const language: string = leftSide.slice(leftSide.indexOf('language-') + ('language-').length,
|
||||||
|
leftSide.indexOf('"', leftSide.indexOf('language-')));
|
||||||
|
addHighlightedLanguage(language).catch(error => null);
|
||||||
|
|
||||||
match = htmlunencode(match);
|
match = htmlunencode(match);
|
||||||
return leftSide + highlightAuto(match).value + rightSide;
|
return leftSide + highlightAuto(match).value + rightSide;
|
||||||
};
|
};
|
||||||
|
@ -53,7 +57,7 @@ export const showdownHighlight = (): showdown.FilterExtension => {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Dynamically import syntax-highlighting supported languages
|
// Import default syntax-highlighting supported languages
|
||||||
highlightedLanguages.forEach((langName) => addHighlightedLanguage(langName));
|
highlightedLanguages.forEach((langName) => addHighlightedLanguage(langName));
|
||||||
|
|
||||||
|
|
||||||
|
|
3
static/js/types/custom.d.ts
vendored
Normal file
3
static/js/types/custom.d.ts
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
declare var System: {
|
||||||
|
import: (module: string) => Promise<any>;
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
declare var require: any;
|
declare var require: NodeRequire;
|
||||||
|
|
||||||
// Require all modules ending in ".spec.ts" from the js directory and all subdirectories
|
// Require all modules ending in ".spec.ts" from the js directory and all subdirectories
|
||||||
var testsContext = (<any>require).context("../js", true, /\.spec\.ts$/);
|
var testsContext = (<any>require).context("../js", true, /\.spec\.ts$/);
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const highlightedLanguages = require('./static/js/constants/highlighted-languages.constant.json');
|
|
||||||
|
|
||||||
|
|
||||||
let config = {
|
let config = {
|
||||||
entry: "./static/js/main.ts",
|
entry: "./static/js/main.ts",
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, "static/build"),
|
path: path.resolve(__dirname, "static/build"),
|
||||||
filename: 'quay-frontend.js'
|
publicPath: "/static/build/",
|
||||||
|
filename: '[name]-quay-frontend.bundle.js',
|
||||||
|
chunkFilename: '[name]-quay-frontend.chunk.js'
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: [".ts", ".js"],
|
extensions: [".ts", ".js"],
|
||||||
|
@ -47,10 +48,6 @@ let config = {
|
||||||
angular: "angular",
|
angular: "angular",
|
||||||
$: "jquery",
|
$: "jquery",
|
||||||
}),
|
}),
|
||||||
// Whitelist highlight-supported languages (based on https://bjacobel.com/2016/12/04/highlight-bundle-size/)
|
|
||||||
new webpack.ContextReplacementPlugin(
|
|
||||||
/highlight\.js\/lib\/languages$/,
|
|
||||||
new RegExp(`^./(${highlightedLanguages.join('|')})$`)),
|
|
||||||
],
|
],
|
||||||
devtool: "cheap-module-source-map",
|
devtool: "cheap-module-source-map",
|
||||||
};
|
};
|
||||||
|
@ -60,14 +57,15 @@ let config = {
|
||||||
* Production settings
|
* Production settings
|
||||||
*/
|
*/
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === 'production') {
|
||||||
config.plugins.push(
|
config.plugins.concat([
|
||||||
new webpack.optimize.UglifyJsPlugin({
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
sourceMap: true,
|
sourceMap: true,
|
||||||
// Disable mangle to prevent AngularJS errors
|
// Disable mangle to prevent AngularJS errors
|
||||||
mangle: false
|
mangle: false
|
||||||
})
|
}),
|
||||||
);
|
new webpack.optimize.CommonsChunkPlugin({name: 'common'}),
|
||||||
config.output.filename = 'quay-frontend-[hash].js';
|
]);
|
||||||
|
config.output.filename = '[name]-quay-frontend-[hash].bundle.js';
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|
Reference in a new issue