initial commit
This commit is contained in:
commit
bea3138401
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.tgz
|
||||||
|
.idea
|
||||||
3267
package-lock.json
generated
Normal file
3267
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
package.json
Normal file
33
package.json
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "rspack-waite-page-plugin",
|
||||||
|
"version": "0.2.4",
|
||||||
|
"description": "Shows a build progress page in the browser while rspack is compiling",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"dev": "tsc --watch"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"rspack",
|
||||||
|
"plugin",
|
||||||
|
"dev-server",
|
||||||
|
"progress",
|
||||||
|
"wait-page"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@rspack/core": ">=0.7.0",
|
||||||
|
"@rspack/dev-server": ">=0.7.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rspack/core": "^1.0.0",
|
||||||
|
"@rspack/dev-server": "^1.0.0",
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/index.ts
Normal file
2
src/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { RspackWaitPagePlugin } from './plugin';
|
||||||
|
export type { RspackWaitPagePluginOptions } from './plugin';
|
||||||
74
src/middleware.ts
Normal file
74
src/middleware.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import type { IncomingMessage, ServerResponse } from 'http';
|
||||||
|
import { BuildState } from './state';
|
||||||
|
import { renderWaitPage } from './template';
|
||||||
|
|
||||||
|
export interface MiddlewareOptions {
|
||||||
|
title: string;
|
||||||
|
disableAfterFirstBuild: boolean;
|
||||||
|
pollInterval: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NextFunction = () => void;
|
||||||
|
|
||||||
|
export const PROGRESS_ENDPOINT = '/rspack-wait-page/progress';
|
||||||
|
|
||||||
|
export function createWaitPageMiddleware(
|
||||||
|
buildState: BuildState,
|
||||||
|
options: MiddlewareOptions
|
||||||
|
) {
|
||||||
|
return function waitPageMiddleware(
|
||||||
|
req: IncomingMessage,
|
||||||
|
res: ServerResponse,
|
||||||
|
next: NextFunction
|
||||||
|
): void {
|
||||||
|
// Serve the JSON progress endpoint (used by the polling script)
|
||||||
|
if (req.method === 'GET' && req.url === PROGRESS_ENDPOINT) {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
isBuilding: buildState.isBuilding,
|
||||||
|
percentage: buildState.percentage,
|
||||||
|
message: buildState.message,
|
||||||
|
moduleName: buildState.moduleName,
|
||||||
|
});
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
});
|
||||||
|
res.end(body);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After first successful build, let the real app handle everything
|
||||||
|
if (options.disableAfterFirstBuild && buildState.hasBeenValid) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not building — let the real app respond
|
||||||
|
if (!buildState.isBuilding) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only intercept top-level document navigations (not JS/CSS/fonts/etc.)
|
||||||
|
const accept = req.headers['accept'] ?? '';
|
||||||
|
if (!accept.includes('text/html')) {
|
||||||
|
next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = renderWaitPage({
|
||||||
|
title: options.title,
|
||||||
|
percentage: buildState.percentage,
|
||||||
|
message: buildState.message,
|
||||||
|
moduleName: buildState.moduleName,
|
||||||
|
pollInterval: options.pollInterval,
|
||||||
|
progressEndpoint: PROGRESS_ENDPOINT,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/html; charset=utf-8',
|
||||||
|
'Cache-Control': 'no-store',
|
||||||
|
});
|
||||||
|
res.end(html);
|
||||||
|
};
|
||||||
|
}
|
||||||
119
src/plugin.ts
Normal file
119
src/plugin.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import type { Compiler } from '@rspack/core';
|
||||||
|
import { createBuildState } from './state';
|
||||||
|
import { createWaitPageMiddleware } from './middleware';
|
||||||
|
|
||||||
|
export interface RspackWaitPagePluginOptions {
|
||||||
|
/** Browser tab title shown on the wait page. Default: "Building…" */
|
||||||
|
title?: string;
|
||||||
|
/** Stop intercepting requests after the first successful build. Default: true */
|
||||||
|
disableAfterFirstBuild?: boolean;
|
||||||
|
/**
|
||||||
|
* Artificial delay in ms added after the build finishes before the wait page
|
||||||
|
* disappears. Useful for testing/debugging the wait page UI. Default: 0
|
||||||
|
*/
|
||||||
|
delay?: number;
|
||||||
|
/**
|
||||||
|
* How often (in ms) the browser polls the /progress endpoint.
|
||||||
|
* Lower values = smoother progress bar, slightly more requests. Default: 100
|
||||||
|
*/
|
||||||
|
pollInterval?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLUGIN_NAME = 'RspackWaitPagePlugin';
|
||||||
|
|
||||||
|
export class RspackWaitPagePlugin {
|
||||||
|
private readonly options: Required<RspackWaitPagePluginOptions>;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(options: RspackWaitPagePluginOptions = {}) {
|
||||||
|
this.options = {
|
||||||
|
title: options.title ?? 'Building…',
|
||||||
|
disableAfterFirstBuild: options.disableAfterFirstBuild ?? true,
|
||||||
|
delay: options.delay ?? 0,
|
||||||
|
pollInterval: options.pollInterval ?? 100,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
apply(compiler: Compiler): void {
|
||||||
|
const buildState = createBuildState();
|
||||||
|
|
||||||
|
// Rspack ProgressPlugin internally does: userFn(percentage, msg, ...items)
|
||||||
|
// where items is string[] from Rust. In practice rspack passes exactly one
|
||||||
|
// item: either the current module path (file path) or a phase label like
|
||||||
|
// "compilation" / "finish make". There are no separate modules-count or
|
||||||
|
// active-modules fields — those are webpack-only.
|
||||||
|
new compiler.webpack.ProgressPlugin((
|
||||||
|
percentage: number,
|
||||||
|
message: string,
|
||||||
|
...args: string[]
|
||||||
|
) => {
|
||||||
|
buildState.percentage = Math.round(percentage * 100);
|
||||||
|
buildState.message = message ?? '';
|
||||||
|
|
||||||
|
// args[0] is either a module path (contains '/') or a phase label like
|
||||||
|
// "finish make" / "plugins". We keep both — the template decides display.
|
||||||
|
buildState.moduleName = args[0] ?? '';
|
||||||
|
}).apply(compiler);
|
||||||
|
|
||||||
|
// Initial build start (watch mode)
|
||||||
|
compiler.hooks.watchRun.tap(PLUGIN_NAME, () => {
|
||||||
|
buildState.isBuilding = true;
|
||||||
|
buildState.percentage = 0;
|
||||||
|
buildState.message = 'starting';
|
||||||
|
});
|
||||||
|
|
||||||
|
// File changed — rebuild started
|
||||||
|
compiler.hooks.invalid.tap(PLUGIN_NAME, () => {
|
||||||
|
buildState.isBuilding = true;
|
||||||
|
buildState.percentage = 0;
|
||||||
|
buildState.message = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build finished (success or error — let HMR overlay handle errors).
|
||||||
|
// tapPromise lets us hold `isBuilding=true` for an artificial delay before
|
||||||
|
// the browser polling detects completion and triggers location.reload().
|
||||||
|
compiler.hooks.done.tapPromise(PLUGIN_NAME, async () => {
|
||||||
|
buildState.percentage = 100;
|
||||||
|
buildState.message = 'done';
|
||||||
|
|
||||||
|
if (this.options.delay > 0) {
|
||||||
|
await new Promise<void>((resolve) => setTimeout(resolve, this.options.delay));
|
||||||
|
}
|
||||||
|
|
||||||
|
buildState.isBuilding = false;
|
||||||
|
buildState.hasBeenValid = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inject middleware by wrapping setupMiddlewares before the dev server starts.
|
||||||
|
// afterEnvironment fires after user config is applied but before server boot.
|
||||||
|
compiler.hooks.afterEnvironment.tap(PLUGIN_NAME, () => {
|
||||||
|
const devServerOptions = (compiler.options as { devServer?: Record<string, unknown> }).devServer;
|
||||||
|
|
||||||
|
// No devServer config — running as `rspack build`, nothing to do
|
||||||
|
if (!devServerOptions) return;
|
||||||
|
|
||||||
|
const originalSetupMiddlewares = devServerOptions['setupMiddlewares'] as
|
||||||
|
| ((middlewares: unknown[], devServer: unknown) => unknown[])
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
const middleware = createWaitPageMiddleware(buildState, {
|
||||||
|
title: this.options.title,
|
||||||
|
disableAfterFirstBuild: this.options.disableAfterFirstBuild,
|
||||||
|
pollInterval: this.options.pollInterval,
|
||||||
|
});
|
||||||
|
|
||||||
|
devServerOptions['setupMiddlewares'] = (
|
||||||
|
middlewares: unknown[],
|
||||||
|
devServer: unknown
|
||||||
|
): unknown[] => {
|
||||||
|
// Prepend our middleware so it runs before all others
|
||||||
|
middlewares.unshift({ name: PLUGIN_NAME, middleware });
|
||||||
|
|
||||||
|
if (typeof originalSetupMiddlewares === 'function') {
|
||||||
|
return originalSetupMiddlewares(middlewares, devServer);
|
||||||
|
}
|
||||||
|
return middlewares;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/state.ts
Normal file
18
src/state.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export interface BuildState {
|
||||||
|
isBuilding: boolean;
|
||||||
|
percentage: number; // 0–100
|
||||||
|
message: string;
|
||||||
|
/** currently processed module path (file path from rspack progress args) */
|
||||||
|
moduleName: string;
|
||||||
|
hasBeenValid: boolean; // true after first successful build
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBuildState(): BuildState {
|
||||||
|
return {
|
||||||
|
isBuilding: false,
|
||||||
|
percentage: 0,
|
||||||
|
message: '',
|
||||||
|
moduleName: '',
|
||||||
|
hasBeenValid: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
293
src/template.ts
Normal file
293
src/template.ts
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
export interface TemplateOptions {
|
||||||
|
title: string;
|
||||||
|
percentage: number;
|
||||||
|
message: string;
|
||||||
|
moduleName: string;
|
||||||
|
pollInterval: number;
|
||||||
|
progressEndpoint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderWaitPage({
|
||||||
|
title,
|
||||||
|
percentage,
|
||||||
|
message,
|
||||||
|
moduleName,
|
||||||
|
pollInterval,
|
||||||
|
progressEndpoint,
|
||||||
|
}: TemplateOptions): string {
|
||||||
|
const safeTitle = escapeHtml(title);
|
||||||
|
const safeMessage = escapeHtml(message);
|
||||||
|
const safeModuleName = escapeHtml(formatDetail(moduleName));
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>${safeTitle}</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #0f1117;
|
||||||
|
color: #e2e8f0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 520px;
|
||||||
|
max-width: calc(100vw - 48px);
|
||||||
|
padding: 40px 36px;
|
||||||
|
background: #1a1d27;
|
||||||
|
border: 1px solid #2d3148;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: linear-gradient(135deg, #e8612c, #f5a623);
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #94a3b8;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f1f5f9;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #64748b;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-track {
|
||||||
|
height: 6px;
|
||||||
|
background: #2d3148;
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
width: ${percentage}%;
|
||||||
|
background: linear-gradient(90deg, #e8612c, #f5a623);
|
||||||
|
border-radius: 999px;
|
||||||
|
transition: width 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-message {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-pct {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f5a623;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Module details panel */
|
||||||
|
.details {
|
||||||
|
background: #13151f;
|
||||||
|
border: 1px solid #252840;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 76px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
color: #475569;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 90px;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value.current { color: #818cf8; }
|
||||||
|
|
||||||
|
/* shown when value is empty */
|
||||||
|
.detail-value.is-empty {
|
||||||
|
color: #2d3148;
|
||||||
|
font-style: italic;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dots {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e8612c;
|
||||||
|
animation: bounce 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.dot:nth-child(3) { animation-delay: 0.4s; }
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
|
||||||
|
40% { transform: translateY(-5px); opacity: 1; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="logo-icon">R</div>
|
||||||
|
<span class="logo-text">rspack dev server</span>
|
||||||
|
</div>
|
||||||
|
<h1>Building your app<span class="dots"><span class="dot"></span><span class="dot"></span><span class="dot"></span></span></h1>
|
||||||
|
<p class="subtitle">${safeTitle}</p>
|
||||||
|
|
||||||
|
<div class="progress-track">
|
||||||
|
<div class="progress-fill" id="progress-fill"></div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-footer">
|
||||||
|
<span class="progress-message" id="progress-message">${safeMessage}</span>
|
||||||
|
<span class="progress-pct" id="progress-pct">${percentage}%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="details">
|
||||||
|
<div class="detail-row">
|
||||||
|
<span class="detail-label">Current</span>
|
||||||
|
<span class="detail-value current${safeModuleName ? '' : ' is-empty'}" id="detail-current">${safeModuleName || '—'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var fill = document.getElementById('progress-fill');
|
||||||
|
var msg = document.getElementById('progress-message');
|
||||||
|
var pct = document.getElementById('progress-pct');
|
||||||
|
var current = document.getElementById('detail-current');
|
||||||
|
|
||||||
|
function setDetail(el, val) {
|
||||||
|
if (val) {
|
||||||
|
el.textContent = val;
|
||||||
|
el.classList.remove('is-empty');
|
||||||
|
} else {
|
||||||
|
el.textContent = '—';
|
||||||
|
el.classList.add('is-empty');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDetail(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
if (s.indexOf('/') === -1) return s; // phase label — show as-is
|
||||||
|
var parts = s.replace(/\\\\/g, '/').split('/');
|
||||||
|
return parts.length > 2 ? ('\u2026/' + parts.slice(-2).join('/')) : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
fill.style.width = '${percentage}%';
|
||||||
|
|
||||||
|
(function poll() {
|
||||||
|
fetch('${progressEndpoint}')
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
fill.style.width = data.percentage + '%';
|
||||||
|
pct.textContent = data.percentage + '%';
|
||||||
|
msg.textContent = data.message || '';
|
||||||
|
setDetail(current, formatDetail(data.moduleName || ''));
|
||||||
|
|
||||||
|
if (!data.isBuilding) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
setTimeout(poll, ${pollInterval});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
setTimeout(poll, ${pollInterval * 5});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For file paths: keep last 2 segments to avoid overflow.
|
||||||
|
* For phase labels ("finish make", "plugins"): show as-is.
|
||||||
|
*/
|
||||||
|
function formatDetail(value: string): string {
|
||||||
|
if (!value) return '';
|
||||||
|
if (!value.includes('/')) return value; // phase label
|
||||||
|
const parts = value.replace(/\\/g, '/').split('/');
|
||||||
|
return parts.length > 2 ? '\u2026/' + parts.slice(-2).join('/') : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str: string): string {
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2018",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"lib": ["ES2018"],
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user