polymer-bundler
Advanced tools
Comparing version 2.2.0 to 2.3.0
@@ -11,2 +11,11 @@ # Change Log | ||
## 2.3.0 - 2017-07-12 | ||
- Fixed issue where inlined `<link rel="import" type="css" ...>` "stylesheet imports" (deprecated but still supported) were inlined in reverse order. Fixes [issue #575](https://github.com/Polymer/polymer-bundler/issues/575). | ||
- Added a `missingImports` property to the `Bundle` class (part of the `BundleManifest`). | ||
- The JSON generated by `--manifest-out` now includes a `_missing` url collection when any are encountered that could not be loaded. | ||
- Fixed [issue #579](https://github.com/Polymer/polymer-bundler/issues/579) where `url()` values inside `<style>` tags inside of `<dom-module>` tags of inlined html imports were rewritten without consideration of the module's `assetpath` property. | ||
- Fixed issue when stylesheet imports are inlined inside of a `<dom-module>` the url resolution now takes into consideration the `assetpath`. | ||
- Added missing `--rewrite-urls-in-templates` option to `bin/polymer-bundler`. | ||
- Fixed issue where HTML imports which were not inlined, because they matched an entry in the `excludes` option, were being erroneously added to the bundle's `missingImports` set. | ||
## 2.2.0 - 2017-06-28 | ||
@@ -13,0 +22,0 @@ - Fixed issue where bundles produced by shell strategy incorrectly included eager html imports of the shell. https://github.com/Polymer/polymer-bundler/issues/471 |
@@ -102,2 +102,9 @@ #!/usr/bin/env node | ||
{ | ||
name: 'rewrite-urls-in-templates', | ||
type: Boolean, | ||
description: 'Fix URLs found inside certain element attributes ' + | ||
'(`action`, `assetpath`, `href`, `src`, and`style`) inside ' + | ||
'`<template>` tags.' | ||
}, | ||
{ | ||
name: 'sourcemaps', | ||
@@ -168,2 +175,3 @@ type: Boolean, | ||
options.inlineCss = Boolean(options['inline-css']); | ||
options.rewriteUrlsInTemplates = Boolean(options['rewrite-urls-in-templates']); | ||
if (options.redirect) { | ||
@@ -196,2 +204,3 @@ const redirections = options.redirect | ||
const json = {}; | ||
const missingImports = new Set(); | ||
for (const [url, bundle] of manifest.bundles) { | ||
@@ -203,2 +212,5 @@ json[url] = [ | ||
]; | ||
for (const missingImport of bundle.missingImports) { | ||
missingImports.add(missingImport); | ||
} | ||
if (bundle.entrypoints.size > 1) { | ||
@@ -218,2 +230,5 @@ continue; | ||
} | ||
if (missingImports.size > 0) { | ||
json['_missing'] = [...missingImports]; | ||
} | ||
return json; | ||
@@ -220,0 +235,0 @@ } |
@@ -25,2 +25,3 @@ import { UrlString } from './url-utils'; | ||
stripImports: Set<string>; | ||
missingImports: Set<string>; | ||
inlinedHtmlImports: Set<string>; | ||
@@ -27,0 +28,0 @@ inlinedScripts: Set<string>; |
@@ -25,2 +25,4 @@ "use strict"; | ||
this.stripImports = new Set(); | ||
// Set of imports which could not be loaded. | ||
this.missingImports = new Set(); | ||
// These sets are updated as bundling occurs. | ||
@@ -27,0 +29,0 @@ this.inlinedHtmlImports = new Set(); |
@@ -135,3 +135,3 @@ import { Analyzer } from 'polymer-analyzer'; | ||
*/ | ||
private _moveDomModuleStyleIntoTemplate(style); | ||
private _moveDomModuleStyleIntoTemplate(style, refStyle?); | ||
/** | ||
@@ -138,0 +138,0 @@ * When an HTML Import is encountered in the head of the document, it needs |
@@ -315,3 +315,3 @@ "use strict"; | ||
for (const htmlImport of htmlImports) { | ||
yield importUtils.inlineHtmlImport(this.analyzer, document, htmlImport, stripImports, bundle, bundleManifest, this.sourcemaps, this.rewriteUrlsInTemplates); | ||
yield importUtils.inlineHtmlImport(this.analyzer, document, htmlImport, stripImports, bundle, bundleManifest, this.sourcemaps, this.rewriteUrlsInTemplates, this.excludes); | ||
} | ||
@@ -340,6 +340,8 @@ }); | ||
const cssImports = dom5.queryAll(ast, matchers.stylesheetImport); | ||
let lastInlined; | ||
for (const cssLink of cssImports) { | ||
const style = yield importUtils.inlineStylesheet(this.analyzer, document, cssLink, bundle, excludes); | ||
if (style) { | ||
this._moveDomModuleStyleIntoTemplate(style); | ||
this._moveDomModuleStyleIntoTemplate(style, lastInlined); | ||
lastInlined = style; | ||
} | ||
@@ -372,3 +374,3 @@ } | ||
*/ | ||
_moveDomModuleStyleIntoTemplate(style) { | ||
_moveDomModuleStyleIntoTemplate(style, refStyle) { | ||
const domModule = dom5.nodeWalkAncestors(style, dom5.predicates.hasTagName('dom-module')); | ||
@@ -385,3 +387,9 @@ if (!domModule) { | ||
astUtils.removeElementAndNewline(style); | ||
astUtils.prepend(parse5_1.treeAdapters.default.getTemplateContent(template), style); | ||
// keep ordering if previding with a reference style | ||
if (!refStyle) { | ||
astUtils.prepend(parse5_1.treeAdapters.default.getTemplateContent(template), style); | ||
} | ||
else { | ||
astUtils.insertAfter(refStyle, style); | ||
} | ||
} | ||
@@ -388,0 +396,0 @@ /** |
@@ -10,3 +10,3 @@ import { ASTNode } from 'parse5'; | ||
*/ | ||
export declare function inlineHtmlImport(analyzer: Analyzer, document: Document, linkTag: ASTNode, stripImports: Set<UrlString>, docBundle: AssignedBundle, manifest: BundleManifest, enableSourcemaps: boolean, rewriteUrlsInTemplates?: boolean): Promise<void>; | ||
export declare function inlineHtmlImport(analyzer: Analyzer, document: Document, linkTag: ASTNode, stripImports: Set<UrlString>, docBundle: AssignedBundle, manifest: BundleManifest, enableSourcemaps: boolean, rewriteUrlsInTemplates?: boolean, excludes?: string[]): Promise<void>; | ||
/** | ||
@@ -13,0 +13,0 @@ * Inlines the contents of the document returned by the script tag's src url |
@@ -42,3 +42,3 @@ "use strict"; | ||
*/ | ||
function inlineHtmlImport(analyzer, document, linkTag, stripImports, docBundle, manifest, enableSourcemaps, rewriteUrlsInTemplates) { | ||
function inlineHtmlImport(analyzer, document, linkTag, stripImports, docBundle, manifest, enableSourcemaps, rewriteUrlsInTemplates, excludes) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
@@ -61,9 +61,15 @@ const isLazy = dom5.getAttribute(linkTag, 'rel').match(/lazy-import/i); | ||
} | ||
// We've never seen this import before, so we'll add it to the stripImports | ||
// Set to guard against inlining it again in the future. | ||
// We've never seen this import before, so we'll add it to the | ||
// stripImports Set to guard against inlining it again in the future. | ||
stripImports.add(resolvedImportUrl); | ||
} | ||
// If we can't find a bundle for the referenced import, record that we've | ||
// processed it, but don't remove the import link. Browser will handle it. | ||
// If we can't find a bundle for the referenced import, we will just leave the | ||
// import link alone. Unless the file was specifically excluded, we need to | ||
// record it as a "missing import". | ||
if (!importBundle) { | ||
if (!excludes || | ||
!excludes.some((u) => u === resolvedImportUrl || | ||
resolvedImportUrl.startsWith(urlUtils.ensureTrailingSlash(u)))) { | ||
docBundle.bundle.missingImports.add(resolvedImportUrl); | ||
} | ||
return; | ||
@@ -91,4 +97,4 @@ } | ||
// already been imported. A special exclusion is for lazy imports, which | ||
// are not deduplicated here, since we can not infer developer's intent from | ||
// here. | ||
// are not deduplicated here, since we can not infer developer's intent | ||
// from here. | ||
if (stripLinkToImportBundle && !isLazy) { | ||
@@ -139,3 +145,3 @@ astUtils.removeElementAndNewline(linkTag); | ||
for (const nestedImport of nestedImports) { | ||
yield inlineHtmlImport(analyzer, document, nestedImport, stripImports, docBundle, manifest, enableSourcemaps, rewriteUrlsInTemplates); | ||
yield inlineHtmlImport(analyzer, document, nestedImport, stripImports, docBundle, manifest, enableSourcemaps, rewriteUrlsInTemplates, excludes); | ||
} | ||
@@ -164,2 +170,3 @@ }); | ||
if (!scriptImport) { | ||
docBundle.bundle.missingImports.add(resolvedImportUrl); | ||
return; | ||
@@ -202,2 +209,3 @@ } | ||
if (!stylesheetImport) { | ||
docBundle.bundle.missingImports.add(resolvedImportUrl); | ||
return; | ||
@@ -207,3 +215,15 @@ } | ||
const media = dom5.getAttribute(cssLink, 'media'); | ||
const resolvedStylesheetContent = rewriteCssTextBaseUrl(stylesheetContent, resolvedImportUrl, document.url); | ||
let newBaseUrl = document.url; | ||
// If the css link we are about to inline is inside of a dom-module, the new | ||
// base url must be calculated using the assetpath of the dom-module if | ||
// present, since Polymer will honor assetpath when resolving urls in | ||
// `<style>` tags, even inside of `<template>` tags. | ||
const parentDomModule = findAncestor(cssLink, dom5.predicates.hasTagName('dom-module')); | ||
if (parentDomModule && dom5.hasAttribute(parentDomModule, 'assetpath')) { | ||
const assetPath = dom5.getAttribute(parentDomModule, 'assetpath') || ''; | ||
if (assetPath) { | ||
newBaseUrl = urlLib.resolve(newBaseUrl, assetPath); | ||
} | ||
} | ||
const resolvedStylesheetContent = rewriteCssTextBaseUrl(stylesheetContent, resolvedImportUrl, newBaseUrl); | ||
const styleNode = dom5.constructors.element('style'); | ||
@@ -255,3 +275,3 @@ if (media) { | ||
rewriteElementAttrsBaseUrl(ast, oldBaseUrl, newBaseUrl, rewriteUrlsInTemplates); | ||
rewriteStyleTagsBaseUrl(ast, oldBaseUrl, newBaseUrl); | ||
rewriteStyleTagsBaseUrl(ast, oldBaseUrl, newBaseUrl, rewriteUrlsInTemplates); | ||
setDomModuleAssetpaths(ast, oldBaseUrl, newBaseUrl); | ||
@@ -284,2 +304,18 @@ } | ||
/** | ||
* Walk the ancestor nodes from parentNode up to document root, returning the | ||
* first one matching the predicate function. | ||
*/ | ||
function findAncestor(ast, predicate) { | ||
// The visited set protects us against circular references. | ||
const visited = new Set(); | ||
while (ast.parentNode && !visited.has(ast.parentNode)) { | ||
if (predicate(ast.parentNode)) { | ||
return ast.parentNode; | ||
} | ||
visited.add(ast.parentNode); | ||
ast = ast.parentNode; | ||
} | ||
return undefined; | ||
} | ||
/** | ||
* Simple utility function used to find an item in a set with a predicate | ||
@@ -337,4 +373,19 @@ * function. Analagous to Array.find(), without requiring converting the set | ||
*/ | ||
function rewriteStyleTagsBaseUrl(ast, oldBaseUrl, newBaseUrl) { | ||
const styleNodes = dom5.queryAll(ast, matchers.styleMatcher, undefined, dom5.childNodesIncludeTemplate); | ||
function rewriteStyleTagsBaseUrl(ast, oldBaseUrl, newBaseUrl, rewriteUrlsInTemplates = false) { | ||
const childNodesOption = rewriteUrlsInTemplates ? | ||
dom5.childNodesIncludeTemplate : | ||
dom5.defaultChildNodes; | ||
// If `rewriteUrlsInTemplates` is `true`, include `<style>` tags that are | ||
// inside `<template>`. | ||
const styleNodes = dom5.queryAll(ast, matchers.styleMatcher, undefined, childNodesOption); | ||
// However, if a `<style>` tag is anywhere inside a `<dom-module>` tag, then | ||
// it should not have its urls rewritten. | ||
for (const domModule of dom5.queryAll(ast, dom5.predicates.hasTagName('dom-module'))) { | ||
for (const styleNode of dom5.queryAll(domModule, matchers.styleMatcher, undefined, childNodesOption)) { | ||
const styleNodeIndex = styleNodes.indexOf(styleNode); | ||
if (styleNodeIndex > -1) { | ||
styleNodes.splice(styleNodeIndex, 1); | ||
} | ||
} | ||
} | ||
for (const node of styleNodes) { | ||
@@ -341,0 +392,0 @@ let styleText = dom5.getTextContent(node); |
@@ -315,2 +315,18 @@ "use strict"; | ||
})); | ||
test('Excluded imports are not listed as missing', () => __awaiter(this, void 0, void 0, function* () { | ||
const bundler = new bundler_1.Bundler({ | ||
analyzer: new polymer_analyzer_1.Analyzer({ urlLoader: new polymer_analyzer_1.FSUrlLoader('test/html') }), | ||
excludes: [ | ||
'this/does/not/exist.html', | ||
'this/does/not/exist.js', | ||
'this/does/not/exist.css' | ||
], | ||
}); | ||
const manifest = yield bundler.generateManifest([ | ||
'absolute-paths.html', | ||
]); | ||
const result = yield bundler.bundle(manifest); | ||
assert.deepEqual([...result.manifest.bundles.get('absolute-paths.html') | ||
.missingImports], []); | ||
})); | ||
test('Excluded CSS file urls is not inlined', () => __awaiter(this, void 0, void 0, function* () { | ||
@@ -408,4 +424,7 @@ const doc = yield bundle('test/html/external.html', { excludes: ['external/external.css'] }); | ||
const scripts = dom5.queryAll(doc, matchers.externalJavascript); | ||
assert.equal(scripts.length, 1); | ||
assert.equal(scripts.length, 2); | ||
assert.deepEqual(dom5.getAttribute(scripts[0], 'src'), '/absolute-paths/script.js'); | ||
// A missing script will not be inlined and the script tag will not | ||
// be removed. | ||
assert.deepEqual(dom5.getAttribute(scripts[1], 'src'), '/this/does/not/exist.js'); | ||
})); | ||
@@ -438,3 +457,7 @@ test('Escape inline <script>', () => __awaiter(this, void 0, void 0, function* () { | ||
yield bundle(inputPath); | ||
assert.deepEqual([...documentBundle.inlinedStyles].sort(), ['imports/regular-style.css', 'imports/simple-style.css']); | ||
assert.deepEqual([...documentBundle.inlinedStyles].sort(), [ | ||
'imports/import-linked-style.css', | ||
'imports/regular-style.css', | ||
'imports/simple-style.css', | ||
]); | ||
})); | ||
@@ -446,4 +469,18 @@ test('External links are replaced with inlined styles', () => __awaiter(this, void 0, void 0, function* () { | ||
assert.equal(links.length, 0); | ||
assert.equal(styles.length, 2); | ||
assert.equal(styles.length, 3); | ||
assert.match(dom5.getTextContent(styles[0]), /regular-style/); | ||
assert.match(dom5.getTextContent(styles[1]), /simple-style/); | ||
assert.match(dom5.getTextContent(styles[2]), /import-linked-style/); | ||
// Verify the inlined url() values in the stylesheet are not rewritten | ||
// to use the "imports/" prefix, since the stylesheet is inside a | ||
// `<dom-module>` with an assetpath that defines the base url as | ||
// "imports/". | ||
assert.match(dom5.getTextContent(styles[1]), /url\("assets\/platform\.png"\)/); | ||
})); | ||
test('Inlined styles observe containing dom-module assetpath', () => __awaiter(this, void 0, void 0, function* () { | ||
const doc = yield bundle('test/html/style-rewriting.html', { inlineCss: true }); | ||
const style = dom5.query(doc, matchers.styleMatcher, dom5.childNodesIncludeTemplate); | ||
assert.isNotNull(style); | ||
assert.match(dom5.getTextContent(style), /url\("styles\/unicorn.png"\)/); | ||
})); | ||
test('Inlined styles have proper paths', () => __awaiter(this, void 0, void 0, function* () { | ||
@@ -474,3 +511,5 @@ const doc = yield bundle('test/html/inline-styles.html', options); | ||
const styles = dom5.queryAll(template, matchers.styleMatcher, [], dom5.childNodesIncludeTemplate); | ||
assert.equal(styles.length, 1); | ||
assert.equal(styles.length, 2); | ||
assert.match(dom5.getTextContent(styles[0]), /simple-style/, 'simple-style.css'); | ||
assert.match(dom5.getTextContent(styles[1]), /import-linked-style/, 'import-linked-style.css'); | ||
})); | ||
@@ -477,0 +516,0 @@ test('Inlined Polymer styles force dom-module to have template', () => __awaiter(this, void 0, void 0, function* () { |
@@ -67,6 +67,10 @@ "use strict"; | ||
<style>:host { background-image: url(background.svg); }</style> | ||
<div style="position: absolute;"></div> | ||
<div style="background-image: url(background.svg)"></div> | ||
</template> | ||
<script>Polymer({is: "my-element"})</script> | ||
</dom-module> | ||
<script>Polymer({is: "my-element"})</script> | ||
<template is="dom-bind"> | ||
<style>.outside-dom-module { background-image: url(outside-dom-module.png); }</style> | ||
</template> | ||
<style>.outside-template { background-image: url(outside-template.png); }</style> | ||
`; | ||
@@ -79,7 +83,11 @@ const expected = ` | ||
<img src="neato.gif"> | ||
<style>:host { background-image: url("my-element/background.svg"); }</style> | ||
<div style="position: absolute;"></div> | ||
<style>:host { background-image: url(background.svg); }</style> | ||
<div style="background-image: url(background.svg)"></div> | ||
</template> | ||
<script>Polymer({is: "my-element"})</script> | ||
</dom-module> | ||
<script>Polymer({is: "my-element"})</script> | ||
<template is="dom-bind"> | ||
<style>.outside-dom-module { background-image: url(outside-dom-module.png); }</style> | ||
</template> | ||
<style>.outside-template { background-image: url("my-element/outside-template.png"); }</style> | ||
`; | ||
@@ -98,6 +106,10 @@ const ast = astUtils.parse(html); | ||
<style>:host { background-image: url(background.svg); }</style> | ||
<div style="position: absolute;"></div> | ||
<div style="background-image: url(background.svg)"></div> | ||
</template> | ||
<script>Polymer({is: "my-element"})</script> | ||
</dom-module> | ||
<script>Polymer({is: "my-element"})</script> | ||
<template is="dom-bind"> | ||
<style>.something { background-image: url(something.png); }</style> | ||
</template> | ||
<style>.outside-template { background-image: url(outside-template.png); }</style> | ||
`; | ||
@@ -109,7 +121,11 @@ const expected = ` | ||
<template> | ||
<style>:host { background-image: url("my-element/background.svg"); }</style> | ||
<div style="position: absolute;"></div> | ||
<style>:host { background-image: url(background.svg); }</style> | ||
<div style="background-image: url("my-element/background.svg")"></div> | ||
</template> | ||
<script>Polymer({is: "my-element"})</script> | ||
</dom-module> | ||
<script>Polymer({is: "my-element"})</script> | ||
<template is="dom-bind"> | ||
<style>.something { background-image: url("my-element/something.png"); }</style> | ||
</template> | ||
<style>.outside-template { background-image: url("my-element/outside-template.png"); }</style> | ||
`; | ||
@@ -152,4 +168,4 @@ const ast = astUtils.parse(html); | ||
<template> | ||
<style>:host { background-image: url("components/my-element/background.svg"); }</style> | ||
<img src="components/my-element/bloop.gif"> | ||
<style>:host { background-image: url(background.svg); }</style> | ||
<img src="bloop.gif"> | ||
</template> | ||
@@ -159,3 +175,3 @@ </dom-module> | ||
const ast = astUtils.parse(htmlBase); | ||
importUtils.rewriteAstToEmulateBaseTag(ast, 'the/doc/url', true); | ||
importUtils.rewriteAstToEmulateBaseTag(ast, 'the/doc/url'); | ||
const actual = parse5.serialize(ast); | ||
@@ -184,4 +200,4 @@ assert.deepEqual(stripSpace(actual), stripSpace(expectedBase), 'base'); | ||
<template> | ||
<style>:host { background-image: url("components/background.svg"); }</style> | ||
<img src="components/bloop.gif"> | ||
<style>:host { background-image: url(background.svg); }</style> | ||
<img src="bloop.gif"> | ||
</template> | ||
@@ -192,3 +208,3 @@ </dom-module> | ||
const ast = astUtils.parse(htmlBase); | ||
importUtils.rewriteAstToEmulateBaseTag(ast, 'the/doc/url', true); | ||
importUtils.rewriteAstToEmulateBaseTag(ast, 'the/doc/url'); | ||
const actual = parse5.serialize(ast); | ||
@@ -195,0 +211,0 @@ assert.deepEqual(stripSpace(actual), stripSpace(expectedBase), 'base'); |
@@ -71,2 +71,12 @@ "use strict"; | ||
})); | ||
test('a single in-html file with deep path stays deep', () => __awaiter(this, void 0, void 0, function* () { | ||
const projectRoot = path.resolve(__dirname, '../../test'); | ||
const tempdir = fs.mkdtempSync(path.join(os.tmpdir(), ' ').trim()); | ||
child_process_1.execSync(`cd ${projectRoot} && ` + | ||
`node ${cliPath} html/default.html ` + | ||
`--out-dir ${tempdir}`) | ||
.toString(); | ||
const html = fs.readFileSync(path.join(tempdir, 'html/default.html')).toString(); | ||
assert.notEqual(html, ''); | ||
})); | ||
}); | ||
@@ -91,2 +101,7 @@ suite('--manifest-out', () => { | ||
], | ||
'_missing': [ | ||
'this/does/not/exist.html', | ||
'this/does/not/exist.js', | ||
'this/does/not/exist.css', | ||
] | ||
}); | ||
@@ -93,0 +108,0 @@ })); |
@@ -67,2 +67,5 @@ "use strict"; | ||
}); | ||
test('Rewrite paths when new base url has trailing slash', () => { | ||
testRewrite('pic.png', 'foo/bar/baz.html', 'foo/', 'bar/pic.png'); | ||
}); | ||
}); | ||
@@ -69,0 +72,0 @@ suite('Relative URL calculations', () => { |
@@ -108,3 +108,6 @@ /** | ||
parsedFrom.host === parsedTo.host) { | ||
let dirFrom = path.posix.dirname(parsedFrom.pathname || ''); | ||
let dirFrom = path.posix.dirname( | ||
// Have to append a '_' to the path because path.posix.dirname('foo/') | ||
// returns '.' instead of 'foo'. | ||
parsedFrom.pathname ? parsedFrom.pathname + '_' : ''); | ||
let pathTo = parsedTo.pathname || ''; | ||
@@ -111,0 +114,0 @@ if (isAbsolutePath(oldBaseUrl) || isAbsolutePath(newBaseUrl)) { |
{ | ||
"name": "polymer-bundler", | ||
"version": "2.2.0", | ||
"version": "2.3.0", | ||
"description": "Process Web Components into one output file", | ||
@@ -5,0 +5,0 @@ "main": "lib/bundler.js", |
@@ -38,2 +38,3 @@ [![Build Status](https://travis-ci.org/Polymer/polymer-bundler.svg?branch=master)](https://travis-ci.org/Polymer/polymer-bundler) | ||
- `--redirect <prefix>|<path>`: Routes URLs with arbitrary `<prefix>`, possibly including a protocol, hostname, and/or path prefix to a `<path>` on local filesystem. For example `--redirect "myapp://|src"` would route `myapp://main/home.html` to `./src/main/home.html`. Multiple redirects may be specified; the earliest ones have the highest priority. | ||
- `--rewrite-urls-in-templates`: Fix URLs found inside certain element attributes (`action`, `assetpath`, `href`, `src`, and `style`) inside `<template>` tags. | ||
- `--shell`: Uses a bundling strategy which puts inlines shared dependencies into a specified html app "shell". | ||
@@ -40,0 +41,0 @@ - `--strip-comments`: Strips all HTML comments from the document which do not contain an `@license`, or start with `<!--#` or `<!--!`. |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
403138
4770
168