mirror of
https://github.com/kevin-DL/sapper.git
synced 2026-01-13 11:35:28 +00:00
Compare commits
12 Commits
v0.15.4
...
proxy-data
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b799a3f1e | ||
|
|
18d15c0120 | ||
|
|
b20e15721c | ||
|
|
de4f99807f | ||
|
|
eae8351f77 | ||
|
|
d386308301 | ||
|
|
13afbc84d7 | ||
|
|
31327b3780 | ||
|
|
81f483d7b8 | ||
|
|
1bcf20511b | ||
|
|
003fa8ab2c | ||
|
|
06cc22b10d |
@@ -1,5 +1,11 @@
|
||||
# sapper changelog
|
||||
|
||||
## 0.15.5
|
||||
|
||||
* Faster `export` with more explanatory output ([#335](https://github.com/sveltejs/sapper/pull/335))
|
||||
* Only blur `activeElement` if it exists ([#332](https://github.com/sveltejs/sapper/issues/332))
|
||||
* Don't emit `client_info.json` or `server_info.json` ([#318](https://github.com/sveltejs/sapper/issues/318))
|
||||
|
||||
## 0.15.4
|
||||
|
||||
* Add `ignore` option ([#326](https://github.com/sveltejs/sapper/pull/326))
|
||||
|
||||
3062
package-lock.json
generated
3062
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@
|
||||
"html-minifier": "^3.5.16",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-fetch": "^2.1.1",
|
||||
"port-authority": "^1.0.2",
|
||||
"port-authority": "^1.0.5",
|
||||
"pretty-bytes": "^5.0.0",
|
||||
"pretty-ms": "^3.1.0",
|
||||
"require-relative": "^0.8.7",
|
||||
|
||||
@@ -4,8 +4,7 @@ import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
import { EventEmitter } from 'events';
|
||||
import { minify_html } from './utils/minify_html';
|
||||
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core'
|
||||
import { locations } from '../config';
|
||||
import { create_compilers, create_main_manifests, create_routes, create_serviceworker_manifest } from '../core';
|
||||
import * as events from './interfaces';
|
||||
|
||||
export function build(opts: {}) {
|
||||
@@ -62,7 +61,6 @@ async function execute(emitter: EventEmitter, {
|
||||
});
|
||||
|
||||
const client_info = client_stats.toJson();
|
||||
fs.writeFileSync(path.join(dest, 'client_info.json'), JSON.stringify(client_info));
|
||||
fs.writeFileSync(path.join(dest, 'client_assets.json'), JSON.stringify(client_info.assetsByChunkName));
|
||||
|
||||
const server_stats = await compile(server);
|
||||
|
||||
@@ -9,6 +9,7 @@ import format_messages from 'webpack-format-messages';
|
||||
import { locations } from '../config';
|
||||
import { EventEmitter } from 'events';
|
||||
import { create_routes, create_main_manifests, create_compilers, create_serviceworker_manifest } from '../core';
|
||||
import Deferred from './utils/Deferred';
|
||||
import * as events from './interfaces';
|
||||
|
||||
export function dev(opts) {
|
||||
@@ -168,8 +169,6 @@ class Watcher extends EventEmitter {
|
||||
},
|
||||
|
||||
result: info => {
|
||||
fs.writeFileSync(path.join(dest, 'server_info.json'), JSON.stringify(info, null, ' '));
|
||||
|
||||
this.deferreds.client.promise.then(() => {
|
||||
const restart = () => {
|
||||
log = '';
|
||||
@@ -225,11 +224,8 @@ class Watcher extends EventEmitter {
|
||||
});
|
||||
|
||||
this.proc.on('message', message => {
|
||||
if (message.__sapper__ && message.event === 'basepath') {
|
||||
this.emit('basepath', {
|
||||
basepath: message.basepath
|
||||
});
|
||||
}
|
||||
if (!message.__sapper__) return;
|
||||
this.emit(message.event, message);
|
||||
});
|
||||
|
||||
this.proc.on('exit', emitFatal);
|
||||
@@ -252,7 +248,6 @@ class Watcher extends EventEmitter {
|
||||
},
|
||||
|
||||
result: info => {
|
||||
fs.writeFileSync(path.join(dest, 'client_info.json'), JSON.stringify(info));
|
||||
fs.writeFileSync(path.join(dest, 'client_assets.json'), JSON.stringify(info.assetsByChunkName, null, ' '));
|
||||
this.deferreds.client.fulfil();
|
||||
|
||||
@@ -401,19 +396,6 @@ function mungeWebpackError(message: string, duplicate: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
class Deferred {
|
||||
promise: Promise<any>;
|
||||
fulfil: (value?: any) => void;
|
||||
reject: (error: Error) => void;
|
||||
|
||||
constructor() {
|
||||
this.promise = new Promise((fulfil, reject) => {
|
||||
this.fulfil = fulfil;
|
||||
this.reject = reject;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const INTERVAL = 10000;
|
||||
|
||||
class DevServer {
|
||||
|
||||
@@ -7,7 +7,7 @@ import fetch from 'node-fetch';
|
||||
import * as ports from 'port-authority';
|
||||
import { EventEmitter } from 'events';
|
||||
import { minify_html } from './utils/minify_html';
|
||||
import { locations } from '../config';
|
||||
import Deferred from './utils/Deferred';
|
||||
import * as events from './interfaces';
|
||||
|
||||
export function exporter(opts: {}) {
|
||||
@@ -52,6 +52,10 @@ async function execute(emitter: EventEmitter, {
|
||||
|
||||
const origin = `http://localhost:${port}`;
|
||||
|
||||
emitter.emit('info', {
|
||||
message: `Crawling ${origin}`
|
||||
});
|
||||
|
||||
const proc = child_process.fork(path.resolve(`${build}/server.js`), [], {
|
||||
cwd: process.cwd(),
|
||||
env: Object.assign({
|
||||
@@ -64,11 +68,21 @@ async function execute(emitter: EventEmitter, {
|
||||
|
||||
const seen = new Set();
|
||||
const saved = new Set();
|
||||
const deferreds = new Map();
|
||||
|
||||
function get_deferred(pathname: string) {
|
||||
if (!deferreds.has(pathname)) {
|
||||
deferreds.set(pathname, new Deferred()) ;
|
||||
}
|
||||
|
||||
return deferreds.get(pathname);
|
||||
}
|
||||
|
||||
proc.on('message', message => {
|
||||
if (!message.__sapper__ || message.event !== 'file') return;
|
||||
|
||||
let file = new URL(message.url, origin).pathname.slice(1);
|
||||
const pathname = new URL(message.url, origin).pathname;
|
||||
let file = pathname.slice(1);
|
||||
let { body } = message;
|
||||
|
||||
if (saved.has(file)) return;
|
||||
@@ -83,24 +97,26 @@ async function execute(emitter: EventEmitter, {
|
||||
|
||||
emitter.emit('file', <events.FileEvent>{
|
||||
file,
|
||||
size: body.length
|
||||
size: body.length,
|
||||
status: message.status
|
||||
});
|
||||
|
||||
sander.writeFileSync(export_dir, file, body);
|
||||
|
||||
get_deferred(pathname).fulfil();
|
||||
});
|
||||
|
||||
async function handle(url: URL) {
|
||||
const pathname = url.pathname || '/';
|
||||
|
||||
if (seen.has(pathname)) return;
|
||||
seen.add(pathname);
|
||||
|
||||
const deferred = get_deferred(pathname);
|
||||
|
||||
const r = await fetch(url.href);
|
||||
const range = ~~(r.status / 100);
|
||||
|
||||
if (range >= 4) {
|
||||
emitter.emit('failure', <events.FailureEvent>{
|
||||
status: r.status,
|
||||
pathname: url.pathname
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (range === 2) {
|
||||
if (r.headers.get('Content-Type') === 'text/html') {
|
||||
const body = await r.text();
|
||||
@@ -111,16 +127,14 @@ async function execute(emitter: EventEmitter, {
|
||||
|
||||
$('a[href]').each((i: number, $a) => {
|
||||
const url = new URL($a.attribs.href, base.href);
|
||||
|
||||
if (url.origin === origin && !seen.has(url.pathname)) {
|
||||
seen.add(url.pathname);
|
||||
urls.push(url);
|
||||
}
|
||||
if (url.origin === origin) urls.push(url);
|
||||
});
|
||||
|
||||
await Promise.all(urls.map(handle));
|
||||
}
|
||||
}
|
||||
|
||||
await deferred.promise;
|
||||
}
|
||||
|
||||
return ports.wait(port)
|
||||
|
||||
12
src/api/utils/Deferred.ts
Normal file
12
src/api/utils/Deferred.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export default class Deferred {
|
||||
promise: Promise<any>;
|
||||
fulfil: (value?: any) => void;
|
||||
reject: (error: Error) => void;
|
||||
|
||||
constructor() {
|
||||
this.promise = new Promise((fulfil, reject) => {
|
||||
this.fulfil = fulfil;
|
||||
this.reject = reject;
|
||||
});
|
||||
}
|
||||
}
|
||||
20
src/cli.ts
20
src/cli.ts
@@ -1,11 +1,8 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as child_process from 'child_process';
|
||||
import sade from 'sade';
|
||||
import * as colors from 'ansi-colors';
|
||||
import prettyMs from 'pretty-ms';
|
||||
// import upgrade from './cli/upgrade';
|
||||
import * as ports from 'port-authority';
|
||||
import * as pkg from '../package.json';
|
||||
|
||||
const prog = sade('sapper').version(pkg.version);
|
||||
@@ -65,19 +62,22 @@ prog.command('start [dir]')
|
||||
|
||||
prog.command('export [dest]')
|
||||
.describe('Export your app as static files (if possible)')
|
||||
.option('--build', '(Re)build app before exporting', true)
|
||||
.option('--build-dir', 'Specify a custom temporary build directory', '.sapper/prod')
|
||||
.option('--basepath', 'Specify a base path')
|
||||
.action(async (dest = 'export', opts: { basepath?: string }) => {
|
||||
console.log(`> Building...`);
|
||||
|
||||
.action(async (dest = 'export', opts: { build: boolean, 'build-dir': string, basepath?: string }) => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.SAPPER_DEST = '.sapper/.export';
|
||||
process.env.SAPPER_DEST = opts['build-dir'];
|
||||
|
||||
const start = Date.now();
|
||||
|
||||
try {
|
||||
const { build } = await import('./cli/build');
|
||||
await build();
|
||||
console.error(`\n> Built in ${elapsed(start)}. Crawling site...`);
|
||||
if (opts.build) {
|
||||
console.log(`> Building...`);
|
||||
const { build } = await import('./cli/build');
|
||||
await build();
|
||||
console.error(`\n> Built in ${elapsed(start)}`);
|
||||
}
|
||||
|
||||
const { exporter } = await import('./cli/export');
|
||||
await exporter(dest, opts);
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as path from 'path';
|
||||
import * as colors from 'ansi-colors';
|
||||
import * as child_process from 'child_process';
|
||||
import prettyMs from 'pretty-ms';
|
||||
import pb from 'pretty-bytes';
|
||||
import { dev as _dev } from '../api/dev';
|
||||
import * as events from '../api/interfaces';
|
||||
|
||||
@@ -44,6 +45,32 @@ export function dev(opts: { port: number, open: boolean }) {
|
||||
if (event.log) console.log(event.log);
|
||||
});
|
||||
|
||||
watcher.on('preload', (event) => {
|
||||
if (event.size > 25000) {
|
||||
console.log(colors.bold.yellow(`${event.url} — large amount of preloaded data`));
|
||||
console.log(`${colors.bold(pb(event.size))} of data was preloaded in total, above the recommended limit of ${pb(25000)}`);
|
||||
}
|
||||
});
|
||||
|
||||
watcher.on('unused_data', (event) => {
|
||||
console.log(colors.bold.yellow(`${event.url} — unused preloaded data`));
|
||||
console.log(`More data was returned from \`preload\` than was used during the initial render. Consider only returning essential data.`);
|
||||
|
||||
event.discrepancies.forEach(discrepancy => {
|
||||
console.log(`${colors.bold(discrepancy.file)} loaded ${colors.bold(pb(discrepancy.preloaded))}, of which ${discrepancy.rendered > 2 ? `only ${colors.bold(pb(discrepancy.rendered))}` : 'none'} was used. The following properties were not referenced:`);
|
||||
|
||||
const slice = discrepancy.props.length > 12
|
||||
? discrepancy.props.slice(0, 10)
|
||||
: discrepancy.props;
|
||||
|
||||
console.log(slice.map((prop: string) => `• ${prop}`).join('\n'));
|
||||
|
||||
if (discrepancy.props.length > slice.length) {
|
||||
console.log(`...and ${discrepancy.props.length - slice.length} more`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
watcher.on('build', (event: events.BuildEvent) => {
|
||||
if (event.errors.length) {
|
||||
console.log(`${colors.bold.red(`✗ ${event.type}`)}`);
|
||||
|
||||
@@ -3,6 +3,11 @@ import * as colors from 'ansi-colors';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { locations } from '../config';
|
||||
|
||||
function left_pad(str: string, len: number) {
|
||||
while (str.length < len) str = ` ${str}`;
|
||||
return str;
|
||||
}
|
||||
|
||||
export function exporter(export_dir: string, { basepath = '' }) {
|
||||
return new Promise((fulfil, reject) => {
|
||||
try {
|
||||
@@ -13,11 +18,19 @@ export function exporter(export_dir: string, { basepath = '' }) {
|
||||
});
|
||||
|
||||
emitter.on('file', event => {
|
||||
console.log(`${colors.bold.cyan(event.file)} ${colors.gray(`(${prettyBytes(event.size)})`)}`);
|
||||
const pb = prettyBytes(event.size);
|
||||
const size_color = event.size > 150000 ? colors.bold.red : event.size > 50000 ? colors.bold.yellow : colors.bold.gray;
|
||||
const size_label = size_color(left_pad(prettyBytes(event.size), 10));
|
||||
|
||||
const file_label = event.status === 200
|
||||
? event.file
|
||||
: colors.bold[event.status >= 400 ? 'red' : 'yellow'](`(${event.status}) ${event.file}`);
|
||||
|
||||
console.log(`${size_label} ${file_label}`);
|
||||
});
|
||||
|
||||
emitter.on('failure', event => {
|
||||
console.log(`${colors.red(`> Received ${event.status} response when fetching ${event.pathname}`)}`);
|
||||
emitter.on('info', event => {
|
||||
console.log(colors.bold.cyan(`> ${event.message}`));
|
||||
});
|
||||
|
||||
emitter.on('error', event => {
|
||||
|
||||
@@ -6,5 +6,5 @@ export const locations = {
|
||||
base: () => path.resolve(process.env.SAPPER_BASE || ''),
|
||||
app: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_APP || 'app'),
|
||||
routes: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_ROUTES || 'routes'),
|
||||
dest: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_DEST || '.sapper')
|
||||
dest: () => path.resolve(process.env.SAPPER_BASE || '', process.env.SAPPER_DEST || `.sapper/${dev() ? 'dev' : 'prod'}`)
|
||||
};
|
||||
@@ -156,6 +156,7 @@ function generate_server(
|
||||
|
||||
const props = [
|
||||
`name: "${part.component.name}"`,
|
||||
`file: "${part.component.file}"`,
|
||||
`component: ${part.component.name}`
|
||||
];
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import fetch from 'node-fetch';
|
||||
import { lookup } from './middleware/mime';
|
||||
import { locations, dev } from './config';
|
||||
import sourceMapSupport from 'source-map-support';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import { wrap_data } from './middleware/wrap_data';
|
||||
import { list_unused_properties } from './middleware/list_unused_properties';
|
||||
|
||||
sourceMapSupport.install();
|
||||
|
||||
@@ -290,6 +293,8 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
|
||||
const { server_routes, pages } = manifest;
|
||||
const error_route = manifest.error;
|
||||
|
||||
const should_wrap_data = dev() || process.env.SAPPER_EXPORT;
|
||||
|
||||
function handle_error(req: Req, res: ServerResponse, statusCode: number, error: Error | string) {
|
||||
handle_page({
|
||||
pattern: null,
|
||||
@@ -402,10 +407,24 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
|
||||
return []; // appease TypeScript
|
||||
}).then(preloaded => {
|
||||
if (redirect) {
|
||||
const location = `${req.baseUrl}/${redirect.location}`;
|
||||
|
||||
res.statusCode = redirect.statusCode;
|
||||
res.setHeader('Location', `${req.baseUrl}/${redirect.location}`);
|
||||
res.setHeader('Location', location);
|
||||
res.end();
|
||||
|
||||
if (process.send) {
|
||||
process.send({
|
||||
__sapper__: true,
|
||||
event: 'file',
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
status: redirect.statusCode,
|
||||
type: 'text/html',
|
||||
body: `<script>window.location.href = "${location}"</script>`
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -414,11 +433,6 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
|
||||
return;
|
||||
}
|
||||
|
||||
const serialized = {
|
||||
preloaded: `[${preloaded.map(data => try_serialize(data)).join(',')}]`,
|
||||
store: store && try_serialize(store.get())
|
||||
};
|
||||
|
||||
const segments = req.path.split('/').filter(Boolean);
|
||||
|
||||
const props: Props = {
|
||||
@@ -440,6 +454,15 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
|
||||
}
|
||||
});
|
||||
|
||||
// in dev and export modes, we wrap data in proxies to see
|
||||
// how much of it is used in the initial render
|
||||
const wrapped = should_wrap_data && wrap_data(preloaded);
|
||||
|
||||
// this is an easy way to 'reify' top-level values
|
||||
const _preloaded = should_wrap_data
|
||||
? wrapped.data.map((x: any) => x)
|
||||
: preloaded;
|
||||
|
||||
let level = data.child;
|
||||
for (let i = 0; i < page.parts.length; i += 1) {
|
||||
const part = page.parts[i];
|
||||
@@ -451,7 +474,7 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
|
||||
component: part.component,
|
||||
props: Object.assign({}, props, {
|
||||
params: get_params(match)
|
||||
}, preloaded[i + 1])
|
||||
}, _preloaded[i + 1])
|
||||
});
|
||||
|
||||
level.props.child = <Props["child"]>{
|
||||
@@ -470,6 +493,47 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
|
||||
.map(file => `<script src='${req.baseUrl}/client/${file}'></script>`)
|
||||
.join('');
|
||||
|
||||
const unwrapped = should_wrap_data && wrapped.unwrap();
|
||||
|
||||
const preloaded_serialized = preloaded.map(try_serialize);
|
||||
|
||||
if (should_wrap_data && process.send) {
|
||||
const discrepancies = [];
|
||||
|
||||
unwrapped.forEach((clone, i) => {
|
||||
const loaded = preloaded_serialized[i];
|
||||
if (!loaded) return;
|
||||
|
||||
const rendered = try_serialize(clone);
|
||||
|
||||
if (rendered !== loaded) {
|
||||
const part = page.parts[i - 1];
|
||||
const file = part ? part.file : '_layout.html';
|
||||
|
||||
discrepancies.push({
|
||||
file,
|
||||
preloaded: loaded.length,
|
||||
rendered: rendered.length,
|
||||
props: list_unused_properties(preloaded[i], clone)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (discrepancies.length) {
|
||||
process.send({
|
||||
__sapper__: true,
|
||||
event: 'unused_data',
|
||||
url: req.url,
|
||||
discrepancies
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const serialized = {
|
||||
preloaded: `[${preloaded_serialized.join(',')}]`,
|
||||
store: store && try_serialize(store.get())
|
||||
};
|
||||
|
||||
let inline_script = `__SAPPER__={${[
|
||||
error && `error:1`,
|
||||
`baseUrl:"${req.baseUrl}"`,
|
||||
@@ -493,12 +557,19 @@ function get_page_handler(manifest: Manifest, store_getter: (req: Req) => Store)
|
||||
res.end(body);
|
||||
|
||||
if (process.send) {
|
||||
process.send({
|
||||
__sapper__: true,
|
||||
event: 'preload',
|
||||
url: req.url,
|
||||
size: serialized.preloaded.length
|
||||
});
|
||||
|
||||
process.send({
|
||||
__sapper__: true,
|
||||
event: 'file',
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
status: 200,
|
||||
status,
|
||||
type: 'text/html',
|
||||
body
|
||||
});
|
||||
|
||||
34
src/middleware/list_unused_properties.ts
Normal file
34
src/middleware/list_unused_properties.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export function list_unused_properties(all: any, used: any) {
|
||||
const props: string[] = [];
|
||||
|
||||
const seen = new Set();
|
||||
|
||||
function walk(keypath: string, a: any, b: any) {
|
||||
if (seen.has(a)) return;
|
||||
seen.add(a);
|
||||
|
||||
if (!a || typeof a !== 'object') return;
|
||||
|
||||
const is_array = Array.isArray(a);
|
||||
|
||||
for (const key in a) {
|
||||
const child_keypath = keypath
|
||||
? is_array ? `${keypath}[${key}]` : `${keypath}.${key}`
|
||||
: key;
|
||||
|
||||
if (hasProp.call(b, key)) {
|
||||
const a_child = a[key];
|
||||
const b_child = b[key];
|
||||
|
||||
walk(child_keypath, a_child, b_child);
|
||||
} else {
|
||||
props.push(child_keypath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(null, all, used);
|
||||
return props;
|
||||
}
|
||||
|
||||
const hasProp = Object.prototype.hasOwnProperty;
|
||||
85
src/middleware/wrap_data.ts
Normal file
85
src/middleware/wrap_data.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
type Obj = Record<string, any>;
|
||||
|
||||
export function wrap_data(data: any) {
|
||||
const proxies = new Map();
|
||||
const clones = new Map();
|
||||
|
||||
const handler = {
|
||||
get(target: any, property: string): any {
|
||||
const value = target[property];
|
||||
const intercepted = intercept(value);
|
||||
|
||||
const target_clone = clones.get(target);
|
||||
const child_clone = clones.get(value);
|
||||
|
||||
if (target_clone && target.hasOwnProperty(property)) {
|
||||
target_clone[property] = child_clone || value;
|
||||
}
|
||||
|
||||
return intercepted;
|
||||
},
|
||||
};
|
||||
|
||||
function get_or_create_proxy(obj: any) {
|
||||
if (!proxies.has(obj)) {
|
||||
proxies.set(obj, new Proxy(obj, handler));
|
||||
}
|
||||
|
||||
return proxies.get(obj);
|
||||
}
|
||||
|
||||
function intercept(obj: any) {
|
||||
if (clones.has(obj)) return obj;
|
||||
|
||||
if (obj && typeof obj === 'object') {
|
||||
if (Array.isArray(obj)) {
|
||||
clones.set(obj, []);
|
||||
return get_or_create_proxy(obj);
|
||||
}
|
||||
|
||||
else if (isPlainObject(obj)) {
|
||||
clones.set(obj, {});
|
||||
return get_or_create_proxy(obj);
|
||||
}
|
||||
}
|
||||
|
||||
clones.set(obj, obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
return {
|
||||
data: intercept(data),
|
||||
unwrap: () => {
|
||||
return clones.get(data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const objectProtoOwnPropertyNames = Object.getOwnPropertyNames(Object.prototype).sort().join('\0')
|
||||
|
||||
function isPlainObject(obj: any) {
|
||||
const proto = Object.getPrototypeOf(obj);
|
||||
|
||||
if (
|
||||
proto !== Object.prototype &&
|
||||
proto !== null &&
|
||||
Object.getOwnPropertyNames(proto).sort().join('\0') !== objectProtoOwnPropertyNames
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Object.getOwnPropertySymbols(obj).length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function pick(obj: Obj, props: string[]) {
|
||||
const picked: Obj = {};
|
||||
props.forEach(prop => {
|
||||
picked[prop] = obj[prop];
|
||||
});
|
||||
return picked;
|
||||
}
|
||||
@@ -298,7 +298,7 @@ async function navigate(target: Target, id: number): Promise<any> {
|
||||
await goto(redirect.location, { replaceState: true });
|
||||
} else {
|
||||
render(data, nullable_depth, scroll_history[id], token);
|
||||
document.activeElement.blur();
|
||||
if (document.activeElement) document.activeElement.blur();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user