[kbn/optimizer] only build specified themes (#70389)

Co-authored-by: spalger <spalger@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: cchaos <caroline.horn@elastic.co>
This commit is contained in:
Spencer 2020-07-02 15:06:32 -07:00 committed by GitHub
parent 5fcf803d3d
commit f5b280007f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 545 additions and 143 deletions

View file

@ -436,7 +436,7 @@ We are still to develop a proper process to accept any contributed translations.
When writing a new component, create a sibling SASS file of the same name and import directly into the JS/TS component file. Doing so ensures the styles are never separated or lost on import and allows for better modularization (smaller individual plugin asset footprint).
Any JavaScript (or TypeScript) file that imports SASS (.scss) files will automatically build with the [EUI](https://elastic.github.io/eui/#/guidelines/sass) & Kibana invisibles (SASS variables, mixins, functions) from the [`styling_constants.scss` file](https://github.com/elastic/kibana/blob/master/src/legacy/ui/public/styles/_styling_constants.scss). However, any Legacy (file path includes `/legacy`) files will not.
All SASS (.scss) files will automatically build with the [EUI](https://elastic.github.io/eui/#/guidelines/sass) & Kibana invisibles (SASS variables, mixins, functions) from the [`globals_[theme].scss` file](src/legacy/ui/public/styles/_globals_v7light.scss).
**Example:**
@ -679,15 +679,15 @@ Part of this process only applies to maintainers, since it requires access to Gi
Kibana publishes [Release Notes](https://www.elastic.co/guide/en/kibana/current/release-notes.html) for major and minor releases. The Release Notes summarize what the PRs accomplish in language that is meaningful to users. To generate the Release Notes, the team runs a script against this repo to collect the merged PRs against the release.
#### Create the Release Notes text
The text that appears in the Release Notes is pulled directly from your PR title, or a single paragraph of text that you specify in the PR description.
The text that appears in the Release Notes is pulled directly from your PR title, or a single paragraph of text that you specify in the PR description.
To use a single paragraph of text, enter `Release note:` or a `## Release note` header in the PR description, followed by your text. For example, refer to this [PR](https://github.com/elastic/kibana/pull/65796) that uses the `## Release note` header.
When you create the Release Notes text, use the following best practices:
* Use present tense.
* Use present tense.
* Use sentence case.
* When you create a feature PR, start with `Adds`.
* When you create an enhancement PR, start with `Improves`.
* When you create an enhancement PR, start with `Improves`.
* When you create a bug fix PR, start with `Fixes`.
* When you create a deprecation PR, start with `Deprecates`.

View file

@ -42,6 +42,26 @@ When a directory is listed in the "extraPublicDirs" it will always be included i
Any import in a bundle which resolves into another bundles "context" directory, ie `src/plugins/*`, must map explicitly to a "public dir" exported by that plugin. If the resolved import is not in the list of public dirs an error will be thrown and the optimizer will fail to build that bundle until the error is fixed.
## Themes
SASS imports in bundles are automatically converted to CSS for one or more themes. In development we build the `v7light` and `v7dark` themes by default to improve build performance. When producing distributable bundles the default shifts to `*` so that the distributable bundles will include all themes, preventing the bundles from needing to be rebuilt when users change the active theme in Kibana's advanced settings.
To customize the themes that are built for development you can specify the `KBN_OPTIMIZER_THEMES` environment variable to one or more theme tags, or use `*` to build styles for all themes. Unfortunately building more than one theme significantly impacts build performance, so try to be strategic about which themes you build.
Currently supported theme tags: `v7light`, `v7dark`, `v8light`, `v8dark`
Examples:
```sh
# start Kibana with only a single theme
KBN_OPTIMIZER_THEMES=v7light yarn start
# start Kibana with dark themes for version 7 and 8
KBN_OPTIMIZER_THEMES=v7dark,v8dark yarn start
# start Kibana with all the themes
KBN_OPTIMIZER_THEMES=* yarn start
```
## API
To run the optimizer from code, you can import the [`OptimizerConfig`][OptimizerConfig] class and [`runOptimizer`][Optimizer] function. Create an [`OptimizerConfig`][OptimizerConfig] instance by calling it's static `create()` method with some options, then pass it to the [`runOptimizer`][Optimizer] function. `runOptimizer()` returns an observable of update objects, which are summaries of the optimizer state plus an optional `event` property which describes the internal events occuring and may be of use. You can use the [`logOptimizerState()`][LogOptimizerState] helper to write the relevant bits of state to a tooling log or checkout it's implementation to see how the internal events like [`WorkerStdio`][ObserveWorker] and [`WorkerStarted`][ObserveWorker] are used.

View file

@ -0,0 +1 @@
$globalStyleConstant: 11;

View file

@ -0,0 +1 @@
$globalStyleConstant: 12;

View file

@ -0,0 +1 @@
$globalStyleConstant: 13;

View file

@ -29,3 +29,4 @@ export * from './array_helpers';
export * from './event_stream_helpers';
export * from './disallowed_syntax_plugin';
export * from './parse_path';
export * from './theme_tags';

View file

@ -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 { parseThemeTags } from './theme_tags';
it('returns default tags when passed undefined', () => {
expect(parseThemeTags()).toMatchInlineSnapshot(`
Array [
"v7dark",
"v7light",
]
`);
});
it('returns all tags when passed *', () => {
expect(parseThemeTags('*')).toMatchInlineSnapshot(`
Array [
"v7dark",
"v7light",
"v8dark",
"v8light",
]
`);
});
it('returns specific tag when passed a single value', () => {
expect(parseThemeTags('v8light')).toMatchInlineSnapshot(`
Array [
"v8light",
]
`);
});
it('returns specific tags when passed a comma separated list', () => {
expect(parseThemeTags('v8light, v7dark,v7light')).toMatchInlineSnapshot(`
Array [
"v7dark",
"v7light",
"v8light",
]
`);
});
it('returns specific tags when passed an array', () => {
expect(parseThemeTags(['v8light', 'v7light'])).toMatchInlineSnapshot(`
Array [
"v7light",
"v8light",
]
`);
});
it('throws when an invalid tag is in the array', () => {
expect(() => parseThemeTags(['v8light', 'v7light', 'bar'])).toThrowErrorMatchingInlineSnapshot(
`"Invalid theme tags [bar], options: [v7dark, v7light, v8dark, v8light]"`
);
});
it('throws when an invalid tags in comma separated list', () => {
expect(() => parseThemeTags('v8light ,v7light,bar,box ')).toThrowErrorMatchingInlineSnapshot(
`"Invalid theme tags [bar, box], options: [v7dark, v7light, v8dark, v8light]"`
);
});
it('returns tags in alphabetical order', () => {
const tags = parseThemeTags(['v7light', 'v8light']);
expect(tags).toEqual(tags.slice().sort((a, b) => a.localeCompare(b)));
});
it('returns an immutable array', () => {
expect(() => {
const tags = parseThemeTags('v8light');
// @ts-expect-error
tags.push('foo');
}).toThrowErrorMatchingInlineSnapshot(`"Cannot add property 1, object is not extensible"`);
});

View file

@ -0,0 +1,65 @@
/*
* 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 { ascending } from './array_helpers';
const tags = (...themeTags: string[]) =>
Object.freeze(themeTags.sort(ascending((tag) => tag)) as ThemeTag[]);
const validTag = (tag: any): tag is ThemeTag => ALL_THEMES.includes(tag);
const isArrayOfStrings = (input: unknown): input is string[] =>
Array.isArray(input) && input.every((v) => typeof v === 'string');
export type ThemeTags = readonly ThemeTag[];
export type ThemeTag = 'v7light' | 'v7dark' | 'v8light' | 'v8dark';
export const DEFAULT_THEMES = tags('v7light', 'v7dark');
export const ALL_THEMES = tags('v7light', 'v7dark', 'v8light', 'v8dark');
export function parseThemeTags(input?: any): ThemeTags {
if (!input) {
return DEFAULT_THEMES;
}
if (input === '*') {
return ALL_THEMES;
}
if (typeof input === 'string') {
input = input.split(',').map((tag) => tag.trim());
}
if (!isArrayOfStrings(input)) {
throw new Error(`Invalid theme tags, must be an array of strings`);
}
if (!input.length) {
throw new Error(
`Invalid theme tags, you must specify at least one of [${ALL_THEMES.join(', ')}]`
);
}
const invalidTags = input.filter((t) => !validTag(t));
if (invalidTags.length) {
throw new Error(
`Invalid theme tags [${invalidTags.join(', ')}], options: [${ALL_THEMES.join(', ')}]`
);
}
return tags(...input);
}

View file

@ -20,11 +20,13 @@
import Path from 'path';
import { UnknownVals } from './ts_helpers';
import { ThemeTags, parseThemeTags } from './theme_tags';
export interface WorkerConfig {
readonly repoRoot: string;
readonly watch: boolean;
readonly dist: boolean;
readonly themeTags: ThemeTags;
readonly cache: boolean;
readonly profileWebpack: boolean;
readonly browserslistEnv: string;
@ -80,6 +82,8 @@ export function parseWorkerConfig(json: string): WorkerConfig {
throw new Error('`browserslistEnv` must be a string');
}
const themes = parseThemeTags(parsed.themeTags);
return {
repoRoot,
cache,
@ -88,6 +92,7 @@ export function parseWorkerConfig(json: string): WorkerConfig {
profileWebpack,
optimizerCacheKey,
browserslistEnv,
themeTags: themes,
};
} catch (error) {
throw new Error(`unable to parse worker config: ${error.message}`);

File diff suppressed because one or more lines are too long

View file

@ -180,7 +180,7 @@ it('uses cache on second run and exist cleanly', async () => {
tap((state) => {
if (state.event?.type === 'worker stdio') {
// eslint-disable-next-line no-console
console.log('worker', state.event.stream, state.event.chunk.toString('utf8'));
console.log('worker', state.event.stream, state.event.line);
}
}),
toArray()
@ -226,7 +226,7 @@ const expectFileMatchesSnapshotWithCompression = (filePath: string, snapshotLabe
// Verify the brotli variant matches
expect(
// @ts-ignore @types/node is missing the brotli functions
// @ts-expect-error @types/node is missing the brotli functions
Zlib.brotliDecompressSync(
Fs.readFileSync(Path.resolve(MOCK_REPO_DIR, `${filePath}.br`))
).toString()

View file

@ -24,7 +24,7 @@ import { tap } from 'rxjs/operators';
import { OptimizerConfig } from './optimizer';
import { OptimizerUpdate$ } from './run_optimizer';
import { CompilerMsg, pipeClosure } from './common';
import { CompilerMsg, pipeClosure, ALL_THEMES } from './common';
export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) {
return pipeClosure((update$: OptimizerUpdate$) => {
@ -37,12 +37,7 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) {
const { event, state } = update;
if (event?.type === 'worker stdio') {
const chunk = event.chunk.toString('utf8');
log.warning(
`worker`,
event.stream,
chunk.slice(0, chunk.length - (chunk.endsWith('\n') ? 1 : 0))
);
log.warning(`worker`, event.stream, event.line);
}
if (event?.type === 'bundle not cached') {
@ -76,6 +71,11 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) {
if (!loggedInit) {
loggedInit = true;
log.info(`initialized, ${state.offlineBundles.length} bundles cached`);
if (config.themeTags.length !== ALL_THEMES.length) {
log.warning(
`only building [${config.themeTags}] themes, customize with the KBN_OPTIMIZER_THEMES environment variable`
);
}
}
return;
}

View file

@ -103,6 +103,10 @@ describe('getOptimizerCacheKey()', () => {
"dist": false,
"optimizerCacheKey": "♻",
"repoRoot": <absolute path>,
"themeTags": Array [
"v7dark",
"v7light",
],
},
}
`);

View file

@ -0,0 +1,49 @@
/*
* 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 { Readable } from 'stream';
import { toArray } from 'rxjs/operators';
import { observeStdio$ } from './observe_stdio';
it('notifies on every line, uncluding partial content at the end without a newline', async () => {
const chunks = [`foo\nba`, `r\nb`, `az`];
await expect(
observeStdio$(
new Readable({
read() {
this.push(chunks.shift()!);
if (!chunks.length) {
this.push(null);
}
},
})
)
.pipe(toArray())
.toPromise()
).resolves.toMatchInlineSnapshot(`
Array [
"foo",
"bar",
"baz",
]
`);
});

View file

@ -0,0 +1,76 @@
/*
* 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 { Readable } from 'stream';
import * as Rx from 'rxjs';
// match newline characters followed either by a non-space character or another newline
const NEWLINE = /\r?\n/;
/**
* Observe a readable stdio stream and emit the entire lines
* of text produced, completing once the stdio stream emits "end"
* and erroring if it emits "error".
*/
export function observeStdio$(stream: Readable) {
return new Rx.Observable<string>((subscriber) => {
let buffer = '';
subscriber.add(
Rx.fromEvent<Buffer>(stream, 'data').subscribe({
next(chunk) {
buffer += chunk.toString('utf8');
while (true) {
const match = NEWLINE.exec(buffer);
if (!match) {
break;
}
const multilineChunk = buffer.slice(0, match.index);
buffer = buffer.slice(match.index + match[0].length);
subscriber.next(multilineChunk);
}
},
})
);
const flush = () => {
while (buffer.length && !subscriber.closed) {
const line = buffer;
buffer = '';
subscriber.next(line);
}
};
subscriber.add(
Rx.fromEvent<void>(stream, 'end').subscribe(() => {
flush();
subscriber.complete();
})
);
subscriber.add(
Rx.fromEvent<Error>(stream, 'error').subscribe((error) => {
flush();
subscriber.error(error);
})
);
});
}

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { Readable } from 'stream';
import { inspect } from 'util';
import execa from 'execa';
@ -26,12 +25,13 @@ import { map, takeUntil, first, ignoreElements } from 'rxjs/operators';
import { isWorkerMsg, WorkerConfig, WorkerMsg, Bundle, BundleRefs } from '../common';
import { observeStdio$ } from './observe_stdio';
import { OptimizerConfig } from './optimizer_config';
export interface WorkerStdio {
type: 'worker stdio';
stream: 'stdout' | 'stderr';
chunk: Buffer;
line: string;
}
export interface WorkerStarted {
@ -99,28 +99,6 @@ function usingWorkerProc<T>(
);
}
function observeStdio$(stream: Readable, name: WorkerStdio['stream']) {
return Rx.fromEvent<Buffer>(stream, 'data').pipe(
takeUntil(
Rx.race(
Rx.fromEvent<void>(stream, 'end'),
Rx.fromEvent<Error>(stream, 'error').pipe(
map((error) => {
throw error;
})
)
)
),
map(
(chunk): WorkerStdio => ({
type: 'worker stdio',
chunk,
stream: name,
})
)
);
}
/**
* We used to pass configuration to the worker as JSON encoded arguments, but they
* grew too large for argv, especially on Windows, so we had to move to an async init
@ -186,8 +164,24 @@ export function observeWorker(
type: 'worker started',
bundles,
}),
observeStdio$(proc.stdout, 'stdout'),
observeStdio$(proc.stderr, 'stderr'),
observeStdio$(proc.stdout).pipe(
map(
(line): WorkerStdio => ({
type: 'worker stdio',
line,
stream: 'stdout',
})
)
),
observeStdio$(proc.stderr).pipe(
map(
(line): WorkerStdio => ({
type: 'worker stdio',
line,
stream: 'stderr',
})
)
),
Rx.fromEvent<[unknown]>(proc, 'message')
.pipe(
// validate the messages from the process

View file

@ -20,6 +20,7 @@
jest.mock('./assign_bundles_to_workers.ts');
jest.mock('./kibana_platform_plugins.ts');
jest.mock('./get_plugin_bundles.ts');
jest.mock('../common/theme_tags.ts');
import Path from 'path';
import Os from 'os';
@ -27,6 +28,7 @@ import Os from 'os';
import { REPO_ROOT, createAbsolutePathSerializer } from '@kbn/dev-utils';
import { OptimizerConfig } from './optimizer_config';
import { parseThemeTags } from '../common';
jest.spyOn(Os, 'cpus').mockReturnValue(['foo'] as any);
@ -35,6 +37,7 @@ expect.addSnapshotSerializer(createAbsolutePathSerializer());
beforeEach(() => {
delete process.env.KBN_OPTIMIZER_MAX_WORKERS;
delete process.env.KBN_OPTIMIZER_NO_CACHE;
delete process.env.KBN_OPTIMIZER_THEMES;
jest.clearAllMocks();
});
@ -81,6 +84,26 @@ describe('OptimizerConfig::parseOptions()', () => {
}).toThrowErrorMatchingInlineSnapshot(`"worker count must be a number"`);
});
it('defaults to * theme when dist = true', () => {
OptimizerConfig.parseOptions({
repoRoot: REPO_ROOT,
dist: true,
});
expect(parseThemeTags).toBeCalledWith('*');
});
it('defaults to KBN_OPTIMIZER_THEMES when dist = false', () => {
process.env.KBN_OPTIMIZER_THEMES = 'foo';
OptimizerConfig.parseOptions({
repoRoot: REPO_ROOT,
dist: false,
});
expect(parseThemeTags).toBeCalledWith('foo');
});
it('applies defaults', () => {
expect(
OptimizerConfig.parseOptions({
@ -102,6 +125,7 @@ describe('OptimizerConfig::parseOptions()', () => {
],
"profileWebpack": false,
"repoRoot": <absolute path>,
"themeTags": undefined,
"watch": false,
}
`);
@ -127,6 +151,7 @@ describe('OptimizerConfig::parseOptions()', () => {
],
"profileWebpack": false,
"repoRoot": <absolute path>,
"themeTags": undefined,
"watch": false,
}
`);
@ -154,6 +179,7 @@ describe('OptimizerConfig::parseOptions()', () => {
],
"profileWebpack": false,
"repoRoot": <absolute path>,
"themeTags": undefined,
"watch": false,
}
`);
@ -178,6 +204,7 @@ describe('OptimizerConfig::parseOptions()', () => {
],
"profileWebpack": false,
"repoRoot": <absolute path>,
"themeTags": undefined,
"watch": false,
}
`);
@ -201,6 +228,7 @@ describe('OptimizerConfig::parseOptions()', () => {
],
"profileWebpack": false,
"repoRoot": <absolute path>,
"themeTags": undefined,
"watch": false,
}
`);
@ -222,6 +250,7 @@ describe('OptimizerConfig::parseOptions()', () => {
"pluginScanDirs": Array [],
"profileWebpack": false,
"repoRoot": <absolute path>,
"themeTags": undefined,
"watch": false,
}
`);
@ -243,6 +272,7 @@ describe('OptimizerConfig::parseOptions()', () => {
"pluginScanDirs": Array [],
"profileWebpack": false,
"repoRoot": <absolute path>,
"themeTags": undefined,
"watch": false,
}
`);
@ -264,6 +294,7 @@ describe('OptimizerConfig::parseOptions()', () => {
"pluginScanDirs": Array [],
"profileWebpack": false,
"repoRoot": <absolute path>,
"themeTags": undefined,
"watch": false,
}
`);
@ -286,6 +317,7 @@ describe('OptimizerConfig::parseOptions()', () => {
"pluginScanDirs": Array [],
"profileWebpack": false,
"repoRoot": <absolute path>,
"themeTags": undefined,
"watch": false,
}
`);
@ -308,6 +340,7 @@ describe('OptimizerConfig::parseOptions()', () => {
"pluginScanDirs": Array [],
"profileWebpack": false,
"repoRoot": <absolute path>,
"themeTags": undefined,
"watch": false,
}
`);
@ -346,6 +379,7 @@ describe('OptimizerConfig::create()', () => {
pluginScanDirs: Symbol('parsed plugin scan dirs'),
repoRoot: Symbol('parsed repo root'),
watch: Symbol('parsed watch'),
themeTags: Symbol('theme tags'),
inspectWorkers: Symbol('parsed inspect workers'),
profileWebpack: Symbol('parsed profile webpack'),
}));
@ -369,6 +403,7 @@ describe('OptimizerConfig::create()', () => {
"plugins": Symbol(new platform plugins),
"profileWebpack": Symbol(parsed profile webpack),
"repoRoot": Symbol(parsed repo root),
"themeTags": Symbol(theme tags),
"watch": Symbol(parsed watch),
}
`);
@ -385,7 +420,7 @@ describe('OptimizerConfig::create()', () => {
[Window],
],
"invocationCallOrder": Array [
7,
21,
],
"results": Array [
Object {
@ -408,7 +443,7 @@ describe('OptimizerConfig::create()', () => {
[Window],
],
"invocationCallOrder": Array [
8,
22,
],
"results": Array [
Object {

View file

@ -20,7 +20,14 @@
import Path from 'path';
import Os from 'os';
import { Bundle, WorkerConfig, CacheableWorkerConfig } from '../common';
import {
Bundle,
WorkerConfig,
CacheableWorkerConfig,
ThemeTag,
ThemeTags,
parseThemeTags,
} from '../common';
import { findKibanaPlatformPlugins, KibanaPlatformPlugin } from './kibana_platform_plugins';
import { getPluginBundles } from './get_plugin_bundles';
@ -73,6 +80,18 @@ interface Options {
/** flag that causes the core bundle to be built along with plugins */
includeCoreBundle?: boolean;
/**
* style themes that sass files will be converted to, the correct style will be
* loaded in the browser automatically by checking the global `__kbnThemeTag__`.
* Specifying additional styles increases build time.
*
* Defaults:
* - "*" when building the dist
* - comma separated list of themes in the `KBN_OPTIMIZER_THEMES` env var
* - "k7light"
*/
themes?: ThemeTag | '*' | ThemeTag[];
}
interface ParsedOptions {
@ -86,6 +105,7 @@ interface ParsedOptions {
pluginScanDirs: string[];
inspectWorkers: boolean;
includeCoreBundle: boolean;
themeTags: ThemeTags;
}
export class OptimizerConfig {
@ -139,6 +159,10 @@ export class OptimizerConfig {
throw new TypeError('worker count must be a number');
}
const themeTags = parseThemeTags(
options.themes || (dist ? '*' : process.env.KBN_OPTIMIZER_THEMES)
);
return {
watch,
dist,
@ -150,6 +174,7 @@ export class OptimizerConfig {
pluginPaths,
inspectWorkers,
includeCoreBundle,
themeTags,
};
}
@ -181,7 +206,8 @@ export class OptimizerConfig {
options.repoRoot,
options.maxWorkerCount,
options.dist,
options.profileWebpack
options.profileWebpack,
options.themeTags
);
}
@ -194,7 +220,8 @@ export class OptimizerConfig {
public readonly repoRoot: string,
public readonly maxWorkerCount: number,
public readonly dist: boolean,
public readonly profileWebpack: boolean
public readonly profileWebpack: boolean,
public readonly themeTags: ThemeTags
) {}
getWorkerConfig(optimizerCacheKey: unknown): WorkerConfig {
@ -205,6 +232,7 @@ export class OptimizerConfig {
repoRoot: this.repoRoot,
watch: this.watch,
optimizerCacheKey,
themeTags: this.themeTags,
browserslistEnv: this.dist ? 'production' : process.env.BROWSERSLIST_ENV || 'dev',
};
}

View file

@ -127,7 +127,7 @@ export function createOptimizerStateSummarizer(
}
if (event.type === 'worker stdio' || event.type === 'worker started') {
// same state, but updated to the event is shared externally
// same state, but updated so the event is shared externally
return createOptimizerState(state);
}

View file

@ -77,7 +77,7 @@ const observeCompiler = (
*/
const complete$ = Rx.fromEventPattern<Stats>((cb) => done.tap(PLUGIN_NAME, cb)).pipe(
maybeMap((stats) => {
// @ts-ignore not included in types, but it is real https://github.com/webpack/webpack/blob/ab4fa8ddb3f433d286653cd6af7e3aad51168649/lib/Watching.js#L58
// @ts-expect-error not included in types, but it is real https://github.com/webpack/webpack/blob/ab4fa8ddb3f433d286653cd6af7e3aad51168649/lib/Watching.js#L58
if (stats.compilation.needAdditionalPass) {
return undefined;
}

View file

@ -17,16 +17,43 @@
* under the License.
*/
import { stringifyRequest, getOptions } from 'loader-utils';
import webpack from 'webpack';
import { stringifyRequest } from 'loader-utils';
import { parseThemeTags, ALL_THEMES, ThemeTag } from '../common';
const getVersion = (tag: ThemeTag) => (tag.includes('v7') ? 7 : 8);
const getIsDark = (tag: ThemeTag) => tag.includes('dark');
const compare = (a: ThemeTag, b: ThemeTag) =>
(getVersion(a) === getVersion(b) ? 1 : 0) + (getIsDark(a) === getIsDark(b) ? 1 : 0);
// eslint-disable-next-line import/no-default-export
export default function (this: webpack.loader.LoaderContext) {
this.cacheable(true);
const options = getOptions(this);
const bundleId: string = options.bundleId!;
const themeTags = parseThemeTags(options.themeTags);
const cases = ALL_THEMES.map((tag) => {
if (themeTags.includes(tag)) {
return `
case '${tag}':
return require(${stringifyRequest(this, `${this.resourcePath}?${tag}`)});`;
}
const fallback = themeTags
.slice()
.sort((a, b) => compare(b, tag) - compare(a, tag))
.shift()!;
const message = `SASS files in [${bundleId}] were not built for theme [${tag}]. Styles were compiled using the [${fallback}] theme instead to keep Kibana somewhat usable. Please adjust the advanced settings to make use of [${themeTags}] or make sure the KBN_OPTIMIZER_THEMES environment variable includes [${tag}] in a comma separated list of themes you want to compile. You can also set it to "*" to build all themes.`;
return `
case '${tag}':
console.error(new Error(${JSON.stringify(message)}));
return require(${stringifyRequest(this, `${this.resourcePath}?${fallback}`)})`;
}).join('\n');
return `
if (window.__kbnDarkMode__) {
require(${stringifyRequest(this, `${this.resourcePath}?dark`)})
} else {
require(${stringifyRequest(this, `${this.resourcePath}?light`)});
}
`;
switch (window.__kbnThemeTag__) {${cases}
}`;
}

View file

@ -21,11 +21,10 @@ import Path from 'path';
import { stringifyRequest } from 'loader-utils';
import webpack from 'webpack';
// @ts-ignore
// @ts-expect-error
import TerserPlugin from 'terser-webpack-plugin';
// @ts-ignore
// @ts-expect-error
import webpackMerge from 'webpack-merge';
// @ts-ignore
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
import CompressionPlugin from 'compression-webpack-plugin';
import * as UiSharedDeps from '@kbn/ui-shared-deps';
@ -134,8 +133,8 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker:
test: /\.scss$/,
exclude: /node_modules/,
oneOf: [
{
resourceQuery: /dark|light/,
...worker.themeTags.map((theme) => ({
resourceQuery: `?${theme}`,
use: [
{
loader: 'style-loader',
@ -196,34 +195,27 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker:
loaderContext,
Path.resolve(
worker.repoRoot,
'src/legacy/ui/public/styles/_styling_constants.scss'
`src/legacy/ui/public/styles/_globals_${theme}.scss`
)
)};\n`;
},
webpackImporter: false,
implementation: require('node-sass'),
sassOptions(loaderContext: webpack.loader.LoaderContext) {
const darkMode = loaderContext.resourceQuery === '?dark';
return {
outputStyle: 'nested',
includePaths: [Path.resolve(worker.repoRoot, 'node_modules')],
sourceMapRoot: `/${bundle.type}:${bundle.id}`,
importer: (url: string) => {
if (darkMode && url.includes('eui_colors_light')) {
return { file: url.replace('eui_colors_light', 'eui_colors_dark') };
}
return { file: url };
},
};
sassOptions: {
outputStyle: 'nested',
includePaths: [Path.resolve(worker.repoRoot, 'node_modules')],
sourceMapRoot: `/${bundle.type}:${bundle.id}`,
},
},
},
],
},
})),
{
loader: require.resolve('./theme_loader'),
options: {
bundleId: bundle.id,
themeTags: worker.themeTags,
},
},
],
},

View file

@ -122,7 +122,7 @@ module.exports = async ({ config }) => {
prependData(loaderContext) {
return `@import ${stringifyRequest(
loaderContext,
resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss')
resolve(REPO_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss')
)};\n`;
},
sassOptions: {

View file

@ -51,15 +51,6 @@ export const ElasticEui = require('@elastic/eui');
export const ElasticEuiLibServices = require('@elastic/eui/lib/services');
export const ElasticEuiLibServicesFormat = require('@elastic/eui/lib/services/format');
export const ElasticEuiChartsTheme = require('@elastic/eui/dist/eui_charts_theme');
export let ElasticEuiLightTheme;
export let ElasticEuiDarkTheme;
if (window.__kbnThemeVersion__ === 'v7') {
ElasticEuiLightTheme = require('@elastic/eui/dist/eui_theme_light.json');
ElasticEuiDarkTheme = require('@elastic/eui/dist/eui_theme_dark.json');
} else {
ElasticEuiLightTheme = require('@elastic/eui/dist/eui_theme_amsterdam_light.json');
ElasticEuiDarkTheme = require('@elastic/eui/dist/eui_theme_amsterdam_dark.json');
}
import * as Theme from './theme.ts';
export { Theme };

View file

@ -23,9 +23,15 @@ const globals: any = typeof window === 'undefined' ? {} : window;
export type Theme = typeof LightTheme;
// in the Kibana app we can rely on this global being defined, but in
// some cases (like jest, or karma tests) the global is undefined
export const tag: string = globals.__kbnThemeTag__ || 'v7light';
export const version = tag.startsWith('v7') ? 7 : 8;
export const darkMode = tag.endsWith('dark');
export let euiLightVars: Theme;
export let euiDarkVars: Theme;
if (globals.__kbnThemeVersion__ === 'v7') {
if (version === 7) {
euiLightVars = require('@elastic/eui/dist/eui_theme_light.json');
euiDarkVars = require('@elastic/eui/dist/eui_theme_dark.json');
} else {
@ -37,7 +43,7 @@ if (globals.__kbnThemeVersion__ === 'v7') {
* EUI Theme vars that automatically adjust to light/dark theme
*/
export let euiThemeVars: Theme;
if (globals.__kbnDarkTheme__) {
if (darkMode) {
euiThemeVars = euiDarkVars;
} else {
euiThemeVars = euiLightVars;

View file

@ -1,7 +1,3 @@
// This file is built by both the legacy and KP build systems so we need to
// import this explicitly
@import '../../legacy/ui/public/styles/_styling_constants';
@import './core';
@import './chrome/index';
@import './overlays/index';

View file

@ -1,5 +1,3 @@
@import 'src/legacy/ui/public/styles/styling_constants';
// Elastic charts
@import '@elastic/charts/dist/theme';
@import '@elastic/eui/src/themes/charts/theme';

View file

@ -1,5 +1,3 @@
@import 'src/legacy/ui/public/styles/styling_constants';
// This file pulls some styles of NP plugins into the legacy test stylesheet
// so they are available for karma browser tests.
@import '../../../../plugins/vis_type_vislib/public/index';

View file

@ -1,6 +1,3 @@
// Should import both the EUI constants and any Kibana ones that are considered global
@import 'src/legacy/ui/public/styles/styling_constants';
/* Timelion plugin styles */
// Prefix all styles with "tim" to avoid conflicts.

View file

@ -1,5 +1,3 @@
@import 'src/legacy/ui/public/styles/styling_constants';
foo {
bar {
display: flex;

View file

@ -29,19 +29,15 @@ import isPathInside from 'is-path-inside';
import { PUBLIC_PATH_PLACEHOLDER } from '../../../optimize/public_path_placeholder';
const renderSass = promisify(sass.render);
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
const access = promisify(fs.access);
const copyFile = promisify(fs.copyFile);
const mkdirAsync = promisify(fs.mkdir);
const UI_ASSETS_DIR = resolve(__dirname, '../../../core/server/core_app/assets');
const DARK_THEME_IMPORTER = (url) => {
if (url.includes('eui_colors_light')) {
return { file: url.replace('eui_colors_light', 'eui_colors_dark') };
}
return { file: url };
};
const LIGHT_GLOBALS_PATH = resolve(__dirname, '../../../legacy/ui/public/styles/_globals_v7light');
const DARK_GLOBALS_PATH = resolve(__dirname, '../../../legacy/ui/public/styles/_globals_v7dark');
const makeAsset = (request, { path, root, boundry, copyRoot, urlRoot }) => {
const relativePath = relative(root, path);
@ -84,10 +80,16 @@ export class Build {
*/
async build() {
const scss = await readFile(this.sourcePath);
const relativeGlobalsPath =
this.theme === 'dark'
? relative(this.sourceDir, DARK_GLOBALS_PATH)
: relative(this.sourceDir, LIGHT_GLOBALS_PATH);
const rendered = await renderSass({
file: this.sourcePath,
data: `@import '${relativeGlobalsPath}';\n${scss}`,
outFile: this.targetPath,
importer: this.theme === 'dark' ? DARK_THEME_IMPORTER : undefined,
sourceMap: true,
outputStyle: 'nested',
sourceMapEmbed: true,

View file

@ -0,0 +1,12 @@
// v7dark global scope
//
// prepended to all .scss imports (from JS, when v7dark theme selected) and
// legacy uiExports.styleSheetPaths when any dark theme is selected
@import '@elastic/eui/src/themes/eui/eui_colors_dark';
@import '@elastic/eui/src/global_styling/functions/index';
@import '@elastic/eui/src/global_styling/variables/index';
@import '@elastic/eui/src/global_styling/mixins/index';
@import './mixins';

View file

@ -1,9 +1,10 @@
// EUI global scope
// v7light global scope
//
// prepended to all .scss imports (from JS, when v7light theme selected) and
// legacy uiExports.styleSheetPaths when any dark theme is selected
@import '@elastic/eui/src/themes/eui/eui_colors_light';
// Note that fonts are loaded directly by src/legacy/ui/ui_render/views/chrome.pug
@import '@elastic/eui/src/global_styling/functions/index';
@import '@elastic/eui/src/global_styling/variables/index';
@import '@elastic/eui/src/global_styling/mixins/index';

View file

@ -0,0 +1,16 @@
// v8dark global scope
//
// prepended to all .scss imports (from JS, when v8dark theme selected)
@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_colors_dark';
@import '@elastic/eui/src/global_styling/functions/index';
@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/functions/index';
@import '@elastic/eui/src/global_styling/variables/index';
@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/variables/index';
@import '@elastic/eui/src/global_styling/mixins/index';
@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/mixins/index';
@import './mixins';

View file

@ -0,0 +1,16 @@
// v8light global scope
//
// prepended to all .scss imports (from JS, when v8light theme selected)
@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_colors_light';
@import '@elastic/eui/src/global_styling/functions/index';
@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/functions/index';
@import '@elastic/eui/src/global_styling/variables/index';
@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/variables/index';
@import '@elastic/eui/src/global_styling/mixins/index';
@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/mixins/index';
@import './mixins';

View file

@ -1,7 +1,6 @@
var kbnCsp = JSON.parse(document.querySelector('kbn-csp').getAttribute('data'));
window.__kbnStrictCsp__ = kbnCsp.strictCsp;
window.__kbnDarkMode__ = {{darkMode}};
window.__kbnThemeVersion__ = "{{themeVersion}}";
window.__kbnThemeTag__ = "{{themeTag}}";
window.__kbnPublicPath__ = {{publicPathMap}};
window.__kbnBundles__ = {{kbnBundlesLoaderSource}}

View file

@ -89,6 +89,7 @@ export function uiRenderMixin(kbnServer, server, config) {
const isCore = !app;
const uiSettings = request.getUiSettingsService();
const darkMode =
!authEnabled || request.auth.isAuthenticated
? await uiSettings.get('theme:darkMode')
@ -99,6 +100,8 @@ export function uiRenderMixin(kbnServer, server, config) {
? await uiSettings.get('theme:version')
: 'v7';
const themeTag = `${themeVersion === 'v7' ? 'v7' : 'v8'}${darkMode ? 'dark' : 'light'}`;
const buildHash = server.newPlatform.env.packageInfo.buildNum;
const basePath = config.get('server.basePath');
@ -178,8 +181,7 @@ export function uiRenderMixin(kbnServer, server, config) {
const bootstrap = new AppBootstrap({
templateData: {
darkMode,
themeVersion,
themeTag,
jsDependencyPaths,
styleSheetPaths,
publicPathMap,

View file

@ -1,5 +1,3 @@
@import 'src/legacy/ui/public/styles/styling_constants';
// Prefix all styles with "tlm" to avoid conflicts.
// Examples
// tlmChart

View file

@ -80,7 +80,7 @@ module.exports = async ({ config }) => {
prependData(loaderContext) {
return `@import ${stringifyRequest(
loaderContext,
path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss')
path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss')
)};\n`;
},
sassOptions: {
@ -199,7 +199,6 @@ module.exports = async ({ config }) => {
config.resolve.alias['ui/url/absolute_to_parsed_url'] = path.resolve(__dirname, '../tasks/mocks/uiAbsoluteToParsedUrl');
config.resolve.alias['ui/chrome'] = path.resolve(__dirname, '../tasks/mocks/uiChrome');
config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public');
config.resolve.alias['src/legacy/ui/public/styles/styling_constants'] = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss');
config.resolve.alias.ng_mock$ = path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock');
return config;

View file

@ -1,5 +1,3 @@
@import 'src/legacy/ui/public/styles/styling_constants';
// Canvas core
@import 'hackery';
@import 'main';

View file

@ -188,7 +188,7 @@ module.exports = {
prependData(loaderContext) {
return `@import ${stringifyRequest(
loaderContext,
path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_styling_constants.scss')
path.resolve(KIBANA_ROOT, 'src/legacy/ui/public/styles/_globals_v7light.scss')
)};\n`;
},
webpackImporter: false,

View file

@ -1,6 +1,3 @@
// Import the EUI global scope so we can use EUI constants
@import 'src/legacy/ui/public/styles/_styling_constants';
// Index management plugin styles
// Prefix all styles with "ind" to avoid conflicts.

View file

@ -1,6 +1,3 @@
// Import the EUI global scope so we can use EUI constants
@import 'src/legacy/ui/public/styles/_styling_constants';
/* Infra plugin styles */
.infra-container-element {

View file

@ -1,8 +1,5 @@
/* GIS plugin styles */
// Import the EUI global scope so we can use EUI constants
@import 'src/legacy/ui/public/styles/_styling_constants';
// Prefix all styles with "map" to avoid conflicts.
// Examples
// mapChart

View file

@ -1,6 +1,3 @@
// Should import both the EUI constants and any Kibana ones that are considered global
@import 'src/legacy/ui/public/styles/styling_constants';
// ML has it's own variables for coloring
@import 'variables';

View file

@ -1,6 +1,3 @@
// Import the EUI global scope so we can use EUI constants
@import 'src/legacy/ui/public/styles/_styling_constants';
// Snapshot and Restore plugin styles
// Prefix all styles with "snapshotRestore" to avoid conflicts.

View file

@ -1,6 +1,3 @@
// Import the EUI global scope so we can use EUI constants
@import 'src/legacy/ui/public/styles/_styling_constants';
// Transform plugin styles
// Prefix all styles with "transform" to avoid conflicts.

View file

@ -1,3 +1 @@
@import 'src/legacy/ui/public/styles/_styling_constants';
@import 'components/index';