Skip to content

Commit

Permalink
Adds SCSS support for plugins
Browse files Browse the repository at this point in the history
This adds two additional options for plugins

sass: path to the stylesheet within the publicDir. When provided, this will be used to when the plugin is active. Providing false diables loading of any stylesheets for the plugin.
styleSheet: path to an SCSS file within the plugins root directory. When in development, this will generate a CSS file to be specified in conjunction with the styleSheet option.

Signed-off-by: Tyler Smalley <tyler.smalley@elastic.co>
  • Loading branch information
Tyler Smalley committed Jun 4, 2018
1 parent 92c9ad7 commit 6c5780d
Show file tree
Hide file tree
Showing 18 changed files with 598 additions and 104 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@
"@types/react": "^16.3.14",
"@types/react-dom": "^16.0.5",
"angular-mocks": "1.4.7",
"anymatch": "^2.0.0",
"babel-eslint": "8.1.2",
"babel-jest": "^22.4.3",
"backport": "2.2.0",
Expand Down Expand Up @@ -298,6 +299,7 @@
"murmurhash3js": "3.0.1",
"ncp": "2.0.0",
"nock": "8.0.0",
"node-sass": "^4.9.0",
"pixelmatch": "4.0.2",
"prettier": "^1.12.1",
"proxyquire": "1.7.11",
Expand Down
112 changes: 77 additions & 35 deletions src/cli/cluster/cluster_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
* under the License.
*/

import { resolve } from 'path';
import { resolve, join } from 'path';
import { debounce, invoke, bindAll, once, uniq } from 'lodash';

import Log from '../log';
import Worker from './worker';
import BasePathProxy from './base_path_proxy';
import { SassBuilder } from './sass_builder';
import { Config } from '../../server/config/config';
import { transformDeprecations } from '../../server/config/transform_deprecations';
import { findPluginSpecs } from '../../plugin_discovery/find_plugin_specs';

process.env.kbnWorkerType = 'managr';

Expand Down Expand Up @@ -52,36 +54,36 @@ export default class ClusterManager {

optimizerArgv.push(
`--server.basePath=${this.basePathProxy.basePath}`,
'--server.rewriteBasePath=true',
'--server.rewriteBasePath=true'
);

serverArgv.push(
`--server.port=${this.basePathProxy.targetPort}`,
`--server.basePath=${this.basePathProxy.basePath}`,
'--server.rewriteBasePath=true',
'--server.rewriteBasePath=true'
);
}

this.workers = [
this.optimizer = new Worker({
(this.optimizer = new Worker({
type: 'optmzr',
title: 'optimizer',
log: this.log,
argv: optimizerArgv,
watch: false
}),
watch: false,
})),

this.server = new Worker({
(this.server = new Worker({
type: 'server',
log: this.log,
argv: serverArgv
})
argv: serverArgv,
})),
];

// broker messages between workers
this.workers.forEach((worker) => {
worker.on('broadcast', (msg) => {
this.workers.forEach((to) => {
this.workers.forEach(worker => {
worker.on('broadcast', msg => {
this.workers.forEach(to => {
if (to !== worker && to.online) {
to.fork.send(msg);
}
Expand All @@ -94,26 +96,62 @@ export default class ClusterManager {
if (opts.watch) {
const pluginPaths = config.get('plugins.paths');
const scanDirs = config.get('plugins.scanDirs');
const extraPaths = [
...pluginPaths,
...scanDirs,
];
const extraPaths = [...pluginPaths, ...scanDirs];

const extraIgnores = scanDirs
.map(scanDir => resolve(scanDir, '*'))
.concat(pluginPaths)
.reduce((acc, path) => acc.concat(
resolve(path, 'test'),
resolve(path, 'build'),
resolve(path, 'target'),
resolve(path, 'scripts'),
resolve(path, 'docs'),
), []);
.reduce(
(acc, path) =>
acc.concat(
resolve(path, 'test'),
resolve(path, 'build'),
resolve(path, 'target'),
resolve(path, 'scripts'),
resolve(path, 'docs')
),
[]
);

this.setupWatching(extraPaths, extraIgnores);
this.setupScssWatching(pluginPaths, scanDirs);
} else {
this.startCluster();
}
}

setupScssWatching(pluginPaths, scanDirs) {
const { FSWatcher } = require('chokidar');
const watcher = new FSWatcher({ ignoreInitial: true });
const { spec$ } = findPluginSpecs({ plugins: { paths: pluginPaths, scanDirs } });

spec$.toArray().toPromise().then(enabledPlugins => {
const scssBundles = enabledPlugins.reduce((acc, plugin) => {
if (plugin.getScss() && plugin.getStyleSheet()) {
const sassPath = join(plugin.getPath(), plugin.getScss());
const styleSheetPath = join(plugin.getPublicDir(), plugin.getStyleSheet());

const builder = new SassBuilder(sassPath, styleSheetPath, { watcher, log: this.log });
builder.build();
builder.addToWatcher();

return [ ...acc, builder ];
} else {
return acc;
}

else this.startCluster();
return acc;
}, []);


watcher.on('all', async (event, path) => {
for (let i = 0; i < scssBundles.length; i++) {
if (await scssBundles[i].buildIfInPath(path)) {
return;
}
}
});
});
}

startCluster() {
Expand All @@ -138,29 +176,33 @@ export default class ClusterManager {
fromRoot('x-pack/server'),
fromRoot('x-pack/webpackShims'),
fromRoot('config'),
...extraPaths
...extraPaths,
].map(path => resolve(path));

this.watcher = chokidar.watch(uniq(watchPaths), {
cwd: fromRoot('.'),
ignored: [
/[\\\/](\..*|node_modules|bower_components|public|__[a-z0-9_]+__|coverage)[\\\/]/,
/\.test\.js$/,
...extraIgnores
]
/.*\.s(c|a)ss/,
...extraIgnores,
],
});

this.watcher.on('add', this.onWatcherAdd);
this.watcher.on('error', this.onWatcherError);

this.watcher.on('ready', once(() => {
// start sending changes to workers
this.watcher.removeListener('add', this.onWatcherAdd);
this.watcher.on('all', this.onWatcherChange);
this.watcher.on(
'ready',
once(() => {
// start sending changes to workers
this.watcher.removeListener('add', this.onWatcherAdd);
this.watcher.on('all', this.onWatcherChange);

this.log.good('watching for changes', `(${this.addedCount} files)`);
this.startCluster();
}));
this.log.good('watching for changes', `(${this.addedCount} files)`);
this.startCluster();
})
);
}

setupManualRestart() {
Expand All @@ -174,7 +216,7 @@ export default class ClusterManager {
const rl = readline.createInterface(process.stdin, process.stdout);

let nls = 0;
const clear = () => nls = 0;
const clear = () => (nls = 0);
const clearSoon = debounce(clear, 2000);

rl.setPrompt('');
Expand Down
92 changes: 92 additions & 0 deletions src/cli/cluster/sass_builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import path from 'path';
import fs from 'fs';
import sass from 'node-sass';
import { FSWatcher } from 'chokidar';
import anymatch from 'anymatch';

export class SassBuilder {
/**
* @param {String} input - Full path to SASS file
* @param {String} output - Full path to CSS to write to
* @param {Object|null} options
* @property {FSWatcher|null} options.watcher - Instance of Chokidar to use
* @property {Log|null} options.log - Instance of logger
*/

constructor(input, output, options = {}) {
this.input = input;
this.output = output;
this.watcher = options.watcher || new FSWatcher({});
this.logger = options.log;
}

/**
* Adds glob to instance of Watcher
*/

addToWatcher() {
this.watcher.add(this.getGlob());
}

/**
* Glob based on input path
*/

getGlob() {
return path.join(path.dirname(this.input), '**', '*.s{a,c}ss');
}

log(level, ...message) {
if (!this.logger) {
return;
}

this.logger[level](...message);
}

async buildIfInPath(path) {
if (anymatch(this.getGlob(), path)) {
await this.build();
return true;
}

return false;
}

/**
* Transpiles SASS and writes CSS to output
*/

async build() {
try {
const rendered = await sass.renderSync({
file: this.input,
outfile: this.output
});

fs.writeFileSync(this.output, rendered.css);
this.log('good', 'Compiled SASS into CSS ', this.output);
} catch(e) {
this.log('bad', 'Compiling SCSS failed', e);
}
}
}
60 changes: 60 additions & 0 deletions src/cli/cluster/sass_builder.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import sinon from 'sinon';
import sass from 'node-sass';
import { SassBuilder } from './sass_builder';

describe('SASS builder', () => {
const sandbox = sinon.createSandbox();

it('generates a glob', () => {
const builder = new SassBuilder('/foo/style.sass', 'foo/public/style.css');
expect(builder.getGlob()).toEqual('/foo/**/*.s{a,c}ss');
});

it('adds a watch for SASS files on the basePath', () => {
const watcher = {
add: sinon.stub(),
};
const builder = new SassBuilder('/foo/style.sass', 'foo/public/style.css', {
watcher,
});

builder.addToWatcher();

sinon.assert.calledOnce(watcher.add);
sinon.assert.calledWithExactly(watcher.add, builder.getGlob());
});

it('builds SASS', () => {
sandbox.stub(sass, 'renderSync').callsFake(() => {
return { css: 'test' };
});

const builder = new SassBuilder('/foo/style.sass', '/foo/public/style.css');
builder.build();

sinon.assert.calledOnce(sass.renderSync);
sinon.assert.calledWithExactly(sass.renderSync, {
file: '/foo/style.sass',
outfile: '/foo/public/style.css',
});
});
});
1 change: 1 addition & 0 deletions src/core_plugins/state_session_storage_redirect/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export default function (kibana) {
id: 'stateSessionStorageRedirect',
main: 'plugins/state_session_storage_redirect',
hidden: true,
styleSheet: false,
}
}
});
Expand Down
1 change: 1 addition & 0 deletions src/plugin_discovery/find_plugin_specs.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export function findPluginSpecs(settings, configToMutate) {
return await defaultConfig(settings);
}).shareReplay();


// find plugin packs in configured paths/dirs
const packageJson$ = config$.mergeMap(config => {
return Observable.merge(
Expand Down
7 changes: 7 additions & 0 deletions src/plugin_discovery/plugin_spec/__tests__/plugin_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,13 @@ describe('plugin discovery/plugin spec', () => {
});
});

describe('#getPublicDir()', () => {
it('defaults if scss is specified', () => {
const spec = new PluginSpec(fooPack, { scss: 'foo.scss' });
expect(spec.getStyleSheet()).to.eql('foo.css');
});
});

describe('#getPublicDir()', () => {
describe('spec.publicDir === false', () => {
it('returns null', () => {
Expand Down
Loading

0 comments on commit 6c5780d

Please sign in to comment.