Skip to content

Commit

Permalink
Merge pull request #132 from WJSoftware/JP/Logger_Object
Browse files Browse the repository at this point in the history
feat:  Add logger object to CSS mounting
  • Loading branch information
webJose authored Apr 25, 2024
2 parents b838b4e + 0e9c84a commit a28f819
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 13 deletions.
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,21 @@ is very important that the name passed to the factory coincides *exactly* with t
The object created by the factory (in the example, stored in the `cssLc` variable), **must** be used for every
exported/created `single-spa` lifecycle object that comes out of the same file (module).

**NOTE**: The optional factory options control the behavior of the FOUC-prevention feature.
**NOTE**: The optional factory options control the behavior of the FOUC-prevention feature and control over what is
logged to the console.

### Console Logging

> Since **v0.7.0**
The option named `logger` in the options parameter of the `cssLifecycleFactory()` function can be used to control what
gets logged to the browser's console from the CSS mounting algorithms. Its default value is `true` and means "log to
the console". If set to `false`, then nothing is logged to the console.

There is a third possiblity: To provide a custom logger object. The logger only needs to implement the 4 console
methods `debug()`, `info()`, `warn()` and `error()`. Use this approach if you need fine-grained control over what
gets logged to the console, or to even do fancier logging, such as posting the console entries to a remote structured
log storage, like a **Seq** server.

### CSS Strategy

Expand Down Expand Up @@ -371,12 +385,15 @@ The following options, which are set when calling `cssLifecycleFactory()`, contr

```typescript
export type CssLifecycleFactoryOptions = {
logger?: boolean | ILogger;
loadTimeout?: number;
failOnTimeout?: boolean;
failOnError?: boolean;
};
```

> Refer to **Console Logging** previously in this document to see about the `logger` option.
Set the `loadTimeout` property to the amount of time to wait for the `load` or `error` events to fire before taking
action. Which action? That depends on the other two properties.

Expand Down Expand Up @@ -463,7 +480,7 @@ understand how this plug-in works and the reasons behind its behavior.
- [x] Logging options
- [x] Asset file name pattern
- [x] CSS `load` event to prevent FOUC
- [ ] Logger object for cssLifecycleFactory to allow full control of what renders in the console
- [x] Logger object for cssLifecycleFactory to allow full control of what renders in the console
- [ ] Input file name pattern?
- [ ] Specify import maps as objects instead of file names
- [ ] Possibly remove the need for CSS strategies (modify `multiMife` so it can re-bootstrap safely)
Expand Down
53 changes: 46 additions & 7 deletions src/css-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CssLifecycleFactoryOptions } from "vite-plugin-single-spa/ex";
import { CssLifecycleFactoryOptions, ILogger } from "vite-plugin-single-spa/ex";

/**
* Defines the return type of the promises generated to ensure CSS loading before micro-frontend mounting.
Expand Down Expand Up @@ -37,11 +37,50 @@ export type LinkLoadResult = {
* while calling `cssLifecycleFactory()`.
*/
export const defaultFactoryOptions: Required<CssLifecycleFactoryOptions> = {
logger: true,
loadTimeout: 1500,
failOnTimeout: false,
failOnError: false
};

/**
* Dud function to implement the silent logger.
*/
function noop() { };

/**
* Silent logger used whenever no logging is desired.
*/
export const silentLogger: ILogger = {
debug: noop,
info: noop,
warn: noop,
error: noop
};

/**
* Module-level variable for the logger of choice.
*/
let logger: ILogger;

/**
* Sets the logger object according to the given logging option.
* @param option Desired logging option.
*/
export function setLogger(option: Required<CssLifecycleFactoryOptions>['logger']) {
logger = option === true ? console : option === false ? silentLogger : option;
}

/**
* Obtains a reference to the current logger object.
*
* **NOTE**: This logger object must have been previously set with a call to `setLogger()`.
* @returns The current logger object.
*/
export function getLogger() {
return logger;
}

/**
* Create an HTML LINK element for a CSS resource that points to the given CSS URL.
* @param href URL to the CSS file.
Expand All @@ -66,7 +105,7 @@ export function createLinkElement(href: string) {
export function wireCssLinkElement(el: HTMLLinkElement, cssFileName: string, projectId: string, loadTimeout: number) {
let rslv: (result: LinkLoadResult) => void;
const timerId = setTimeout(() => {
console.debug('CSS file "%s" for project with ID "%s" timed out and might have failed to load. %d ms', cssFileName, projectId, loadTimeout);
logger.debug('CSS file "%s" for project with ID "%s" timed out and might have failed to load. %d ms', cssFileName, projectId, loadTimeout);
rslv({
status: 'timeout',
cssFileName
Expand All @@ -75,14 +114,14 @@ export function wireCssLinkElement(el: HTMLLinkElement, cssFileName: string, pro
el.removeEventListener('error', errorHandler);
}, loadTimeout);
const loadHandler = () => {
console.debug('CSS file "%s" loaded.', cssFileName);
logger.debug('CSS file "%s" loaded.', cssFileName);
clearTimeout(timerId);
rslv({ status: 'ok' });
el.removeEventListener('load', loadHandler);
el.removeEventListener('error', errorHandler);
};
const errorHandler = (ev: ErrorEvent) => {
console.debug('CSS file "%s" failed to load with error detail: %o', cssFileName, ev);
logger.debug('CSS file "%s" failed to load with error detail: %o', cssFileName, ev);
clearTimeout(timerId);
rslv({
status: 'error',
Expand Down Expand Up @@ -123,15 +162,15 @@ export async function processCssPromises(
throw new Error(`CSS load failed for file "${result.cssFileName}".`, { cause: result.detail });
}
// Not failing on error, so log a warning.
console.warn('CSS load failed for file "%s": %s', result.cssFileName, result.detail.message);
logger.warn('CSS load failed for file "%s": %s', result.cssFileName, result.detail.message);
}
else if (result.status === 'timeout') {
const msg = `CSS load for file "${result.cssFileName}" timed out and might not have loaded.`;
if (opts.failOnTimeout) {
throw new Error(msg);
}
// Not failing on timeout, so log a warning.
console.warn(msg);
logger.warn(msg);
}
}
}
}
18 changes: 18 additions & 0 deletions src/ex.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,28 @@ declare module 'vite-plugin-single-spa/ex' {
mode: string
};

/**
* Defines the functionality required from custom logger objects. Custom loggers are used to customize how and
* what gets into the browser's console, if anything.
*/
export type ILogger = {
debug: Console['debug'];
info: Console['info'];
warn: Console['warn'];
error: Console['error'];
}

/**
* Options for the `cssLifecycleFactory` function.
*/
export type CssLifecycleFactoryOptions = {
/**
* Specifies a logger object or a Boolean value that controls what gets logged to the browser's console.
*
* Set it to `true` (or don't specify it at all) to log to the browser's console; set it to `false` to turn
* off all logging to the console, or use it to pass a custom logger object to arbitrarily handle logging.
*/
logger?: boolean | ILogger;
/**
* Specifies the amount of time to wait for a CSS LINK element to load before potentially aborting the mount
* operation.
Expand Down
8 changes: 5 additions & 3 deletions src/multiMife-css.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// <reference types="vite/client" />

import { type CssLifecycleFactoryOptions } from "vite-plugin-single-spa/ex";
import { createLinkElement, defaultFactoryOptions, processCssPromises, wireCssLinkElement, type LinkLoadResult } from "./css-helpers.js";
import { createLinkElement, defaultFactoryOptions, getLogger, processCssPromises, setLogger, wireCssLinkElement, type LinkLoadResult } from "./css-helpers.js";

let observer: MutationObserver | undefined;
let autoLinkEls: HTMLLinkElement[] = [];
Expand All @@ -18,6 +18,7 @@ export function cssLifecycleFactory(entryPoint: string, options?: CssLifecycleFa
...defaultFactoryOptions,
...options
};
setLogger(opts.logger);
const cssFileNames = cssMap[entryPoint] ?? [];

return {
Expand Down Expand Up @@ -97,12 +98,13 @@ function mountAutoCss() {
*/
function unmountCssFile(cssFileName: string) {
let map = cssFileMap[cssFileName];
const logger = getLogger();
if (!map) {
console.warn('A request to unmount CSS file %s was made, but said file has no file map.', cssFileName);
logger.warn('A request to unmount CSS file %s was made, but said file has no file map.', cssFileName);
return;
}
if (map.count <= 0) {
console.warn('A request to unmount CSS file %s was made, but its count is already %d.', cssFileName, map.count);
logger.warn('A request to unmount CSS file %s was made, but its count is already %d.', cssFileName, map.count);
return;
}
map.linkElement.disabled = --map.count === 0;
Expand Down
3 changes: 2 additions & 1 deletion src/singleMife-css.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// <reference types="vite/client" />

import { CssLifecycleFactoryOptions } from "vite-plugin-single-spa/ex";
import { createLinkElement, defaultFactoryOptions, processCssPromises, wireCssLinkElement, type LinkLoadResult } from "./css-helpers.js";
import { createLinkElement, defaultFactoryOptions, processCssPromises, setLogger, wireCssLinkElement, type LinkLoadResult } from "./css-helpers.js";

let observer: MutationObserver | undefined;
let vpssLinkEls: HTMLLinkElement[];
Expand Down Expand Up @@ -100,6 +100,7 @@ export function cssLifecycleFactory(entryPoint: string, options?: CssLifecycleFa
...defaultFactoryOptions,
...options
};
setLogger(opts.logger);
const cssFiles = cssMap[entryPoint] ?? [];
return {
bootstrap: bootstrap.bind(null, cssFiles),
Expand Down

0 comments on commit a28f819

Please sign in to comment.