mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 15:42:13 +08:00
CDN: Adds support for serving assets over a CDN (#30691)
* CDN: Initial poc support for serving assets over a CDN * Minor fix * added build path and test * fix lint error * Added edition to cdn path * Move master builds to a separate path * Added error handling for the url parsing, changed setting name, and added docs * Updated sample.ini * Some property renames * updated * Minor update to html * index template improvements * Update docs/sources/administration/configuration.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update docs/sources/administration/configuration.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Added ContentDeliveryPrefix to Licence service * updated docs * Updated test mock Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
This commit is contained in:
@ -66,6 +66,9 @@ cert_key =
|
|||||||
# Unix socket path
|
# Unix socket path
|
||||||
socket = /tmp/grafana.sock
|
socket = /tmp/grafana.sock
|
||||||
|
|
||||||
|
# CDN Url
|
||||||
|
cdn_url =
|
||||||
|
|
||||||
#################################### Database ############################
|
#################################### Database ############################
|
||||||
[database]
|
[database]
|
||||||
# You can configure the database connection by specifying type, host, name, user and password
|
# You can configure the database connection by specifying type, host, name, user and password
|
||||||
|
@ -67,6 +67,9 @@
|
|||||||
# Unix socket path
|
# Unix socket path
|
||||||
;socket =
|
;socket =
|
||||||
|
|
||||||
|
# CDN Url
|
||||||
|
;cdn_url =
|
||||||
|
|
||||||
#################################### Database ####################################
|
#################################### Database ####################################
|
||||||
[database]
|
[database]
|
||||||
# You can configure the database connection by specifying type, host, name, user and password
|
# You can configure the database connection by specifying type, host, name, user and password
|
||||||
|
@ -259,6 +259,15 @@ Path to the certificate key file (if `protocol` is set to `https` or `h2`).
|
|||||||
|
|
||||||
Path where the socket should be created when `protocol=socket`. Make sure that Grafana has appropriate permissions before you change this setting.
|
Path where the socket should be created when `protocol=socket`. Make sure that Grafana has appropriate permissions before you change this setting.
|
||||||
|
|
||||||
|
### cdn_url
|
||||||
|
|
||||||
|
> **Note**: Available in Grafana v7.4 and later versions.
|
||||||
|
|
||||||
|
Specify a full HTTP URL address to the root of your Grafana CDN assets. Grafana will add edition and version paths.
|
||||||
|
|
||||||
|
For example, given a cdn url like `https://cdn.myserver.com` grafana will try to load a javascript file from
|
||||||
|
`http://cdn.myserver.com/grafana-oss/v7.4.0/public/build/app.<hash>.js`.
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
## [database]
|
## [database]
|
||||||
|
@ -25,6 +25,7 @@ type IndexViewData struct {
|
|||||||
AppleTouchIcon template.URL
|
AppleTouchIcon template.URL
|
||||||
AppTitle string
|
AppTitle string
|
||||||
Sentry *setting.Sentry
|
Sentry *setting.Sentry
|
||||||
|
ContentDeliveryURL string
|
||||||
// Nonce is a cryptographic identifier for use with Content Security Policy.
|
// Nonce is a cryptographic identifier for use with Content Security Policy.
|
||||||
Nonce string
|
Nonce string
|
||||||
}
|
}
|
||||||
|
@ -428,6 +428,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
|
|||||||
NavTree: navTree,
|
NavTree: navTree,
|
||||||
Sentry: &hs.Cfg.Sentry,
|
Sentry: &hs.Cfg.Sentry,
|
||||||
Nonce: c.RequestNonce,
|
Nonce: c.RequestNonce,
|
||||||
|
ContentDeliveryURL: hs.Cfg.GetContentDeliveryURL((hs.License.Edition())),
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting.DisableGravatar {
|
if setting.DisableGravatar {
|
||||||
|
@ -13,6 +13,9 @@ type Licensing interface {
|
|||||||
// Return edition
|
// Return edition
|
||||||
Edition() string
|
Edition() string
|
||||||
|
|
||||||
|
// Used to build content delivery URL
|
||||||
|
ContentDeliveryPrefix() string
|
||||||
|
|
||||||
LicenseURL(user *SignedInUser) string
|
LicenseURL(user *SignedInUser) string
|
||||||
|
|
||||||
StateInfo() string
|
StateInfo() string
|
||||||
|
@ -403,6 +403,10 @@ func (t *testLicensingService) StateInfo() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *testLicensingService) ContentDeliveryPrefix() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (t *testLicensingService) LicenseURL(user *models.SignedInUser) string {
|
func (t *testLicensingService) LicenseURL(user *models.SignedInUser) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,10 @@ func (*OSSLicensingService) StateInfo() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*OSSLicensingService) ContentDeliveryPrefix() string {
|
||||||
|
return "grafana-oss"
|
||||||
|
}
|
||||||
|
|
||||||
func (l *OSSLicensingService) LicenseURL(user *models.SignedInUser) string {
|
func (l *OSSLicensingService) LicenseURL(user *models.SignedInUser) string {
|
||||||
if user.IsGrafanaAdmin {
|
if user.IsGrafanaAdmin {
|
||||||
return l.Cfg.AppSubURL + "/admin/upgrading"
|
return l.Cfg.AppSubURL + "/admin/upgrading"
|
||||||
|
@ -197,6 +197,7 @@ type Cfg struct {
|
|||||||
SocketPath string
|
SocketPath string
|
||||||
RouterLogging bool
|
RouterLogging bool
|
||||||
Domain string
|
Domain string
|
||||||
|
CDNRootURL *url.URL
|
||||||
|
|
||||||
// build
|
// build
|
||||||
BuildVersion string
|
BuildVersion string
|
||||||
@ -767,7 +768,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
|||||||
provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "")
|
provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "")
|
||||||
cfg.ProvisioningPath = makeAbsolute(provisioning, HomePath)
|
cfg.ProvisioningPath = makeAbsolute(provisioning, HomePath)
|
||||||
|
|
||||||
if err := readServerSettings(iniFile, cfg); err != nil {
|
if err := cfg.readServerSettings(iniFile); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1251,7 +1252,7 @@ func readSnapshotsSettings(cfg *Cfg, iniFile *ini.File) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readServerSettings(iniFile *ini.File, cfg *Cfg) error {
|
func (cfg *Cfg) readServerSettings(iniFile *ini.File) error {
|
||||||
server := iniFile.Section("server")
|
server := iniFile.Section("server")
|
||||||
var err error
|
var err error
|
||||||
AppUrl, AppSubUrl, err = parseAppUrlAndSubUrl(server)
|
AppUrl, AppSubUrl, err = parseAppUrlAndSubUrl(server)
|
||||||
@ -1263,8 +1264,8 @@ func readServerSettings(iniFile *ini.File, cfg *Cfg) error {
|
|||||||
cfg.AppURL = AppUrl
|
cfg.AppURL = AppUrl
|
||||||
cfg.AppSubURL = AppSubUrl
|
cfg.AppSubURL = AppSubUrl
|
||||||
cfg.ServeFromSubPath = ServeFromSubPath
|
cfg.ServeFromSubPath = ServeFromSubPath
|
||||||
|
|
||||||
cfg.Protocol = HTTPScheme
|
cfg.Protocol = HTTPScheme
|
||||||
|
|
||||||
protocolStr := valueAsString(server, "protocol", "http")
|
protocolStr := valueAsString(server, "protocol", "http")
|
||||||
|
|
||||||
if protocolStr == "https" {
|
if protocolStr == "https" {
|
||||||
@ -1297,9 +1298,34 @@ func readServerSettings(iniFile *ini.File, cfg *Cfg) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cdnURL := valueAsString(server, "cdn_url", "")
|
||||||
|
if cdnURL != "" {
|
||||||
|
cfg.CDNRootURL, err = url.Parse(cdnURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetContentDeliveryURL returns full content delivery URL with /<edition>/<version> added to URL
|
||||||
|
func (cfg *Cfg) GetContentDeliveryURL(prefix string) string {
|
||||||
|
if cfg.CDNRootURL != nil {
|
||||||
|
url := *cfg.CDNRootURL
|
||||||
|
preReleaseFolder := ""
|
||||||
|
|
||||||
|
if strings.Contains(cfg.BuildVersion, "pre") || strings.Contains(cfg.BuildVersion, "alpha") {
|
||||||
|
preReleaseFolder = "pre-releases"
|
||||||
|
}
|
||||||
|
|
||||||
|
url.Path = path.Join(url.Path, prefix, preReleaseFolder, cfg.BuildVersion)
|
||||||
|
return url.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func (cfg *Cfg) readDataSourcesSettings() {
|
func (cfg *Cfg) readDataSourcesSettings() {
|
||||||
datasources := cfg.Raw.Section("datasources")
|
datasources := cfg.Raw.Section("datasources")
|
||||||
cfg.DataSourceLimit = datasources.Key("datasource_limit").MustInt(5000)
|
cfg.DataSourceLimit = datasources.Key("datasource_limit").MustInt(5000)
|
||||||
|
@ -2,6 +2,7 @@ package setting
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -389,3 +390,35 @@ func TestAuthDurationSettings(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, maxLifetimeDurationTest, cfg.LoginMaxLifetime)
|
require.Equal(t, maxLifetimeDurationTest, cfg.LoginMaxLifetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetCDNPath(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
cfg := NewCfg()
|
||||||
|
cfg.BuildVersion = "v7.5.0-11124"
|
||||||
|
cfg.CDNRootURL, err = url.Parse("http://cdn.grafana.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, "http://cdn.grafana.com/grafana-oss/v7.5.0-11124", cfg.GetContentDeliveryURL("grafana-oss"))
|
||||||
|
require.Equal(t, "http://cdn.grafana.com/grafana/v7.5.0-11124", cfg.GetContentDeliveryURL("grafana"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCDNPathWithPreReleaseVersionAndSubPath(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
cfg := NewCfg()
|
||||||
|
cfg.BuildVersion = "v7.5.0-11124pre"
|
||||||
|
cfg.CDNRootURL, err = url.Parse("http://cdn.grafana.com/sub")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "http://cdn.grafana.com/sub/grafana-oss/pre-releases/v7.5.0-11124pre", cfg.GetContentDeliveryURL("grafana-oss"))
|
||||||
|
require.Equal(t, "http://cdn.grafana.com/sub/grafana/pre-releases/v7.5.0-11124pre", cfg.GetContentDeliveryURL("grafana"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding a case for this in case we switch to proper semver version strings
|
||||||
|
func TestGetCDNPathWithAlphaVersion(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
cfg := NewCfg()
|
||||||
|
cfg.BuildVersion = "v7.5.0-alpha.11124"
|
||||||
|
cfg.CDNRootURL, err = url.Parse("http://cdn.grafana.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "http://cdn.grafana.com/grafana-oss/pre-releases/v7.5.0-alpha.11124", cfg.GetContentDeliveryURL("grafana-oss"))
|
||||||
|
require.Equal(t, "http://cdn.grafana.com/grafana/pre-releases/v7.5.0-alpha.11124", cfg.GetContentDeliveryURL("grafana"))
|
||||||
|
}
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
import app from './app';
|
declare let __webpack_public_path__: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we are hosting files on cdn and set webpack public path
|
||||||
|
*/
|
||||||
|
if ((window as any).public_cdn_path) {
|
||||||
|
__webpack_public_path__ = (window as any).public_cdn_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
import app from './app';
|
||||||
app.initEchoSrv();
|
app.initEchoSrv();
|
||||||
app.init();
|
app.init();
|
||||||
|
@ -3,16 +3,15 @@
|
|||||||
<head>
|
<head>
|
||||||
<script nonce="[[.Nonce]]">
|
<script nonce="[[.Nonce]]">
|
||||||
// https://github.com/GoogleChromeLabs/tti-polyfill
|
// https://github.com/GoogleChromeLabs/tti-polyfill
|
||||||
!(function() {
|
!(function () {
|
||||||
if ('PerformanceLongTaskTiming' in window) {
|
if ('PerformanceLongTaskTiming' in window) {
|
||||||
var g = (window.__tti = { e: [] });
|
var g = (window.__tti = { e: [] });
|
||||||
g.o = new PerformanceObserver(function(l) {
|
g.o = new PerformanceObserver(function (l) {
|
||||||
g.e = g.e.concat(l.getEntries());
|
g.e = g.e.concat(l.getEntries());
|
||||||
});
|
});
|
||||||
g.o.observe({ entryTypes: ['longtask'] });
|
g.o.observe({ entryTypes: ['longtask'] });
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
@ -25,16 +24,15 @@
|
|||||||
|
|
||||||
<link
|
<link
|
||||||
rel="preload"
|
rel="preload"
|
||||||
href="public/fonts/roboto/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2"
|
href="[[.ContentDeliveryURL]]/public/fonts/roboto/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2"
|
||||||
as="font"
|
as="font"
|
||||||
crossorigin
|
crossorigin
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<link rel="icon" type="image/png" href="[[.FavIcon]]" />
|
<link rel="icon" type="image/png" href="[[.FavIcon]]" />
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="[[.AppleTouchIcon]]" />
|
<link rel="apple-touch-icon" sizes="180x180" href="[[.AppleTouchIcon]]" />
|
||||||
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28" />
|
<link rel="mask-icon" href="[[.ContentDeliveryURL]]/public/img/grafana_mask_icon.svg" color="#F05A28" />
|
||||||
|
<link rel="stylesheet" href="[[.ContentDeliveryURL]]/public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css" />
|
||||||
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css" />
|
|
||||||
|
|
||||||
<script nonce="[[.Nonce]]">
|
<script nonce="[[.Nonce]]">
|
||||||
performance.mark('css done blocking');
|
performance.mark('css done blocking');
|
||||||
@ -199,14 +197,17 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
1. This could be caused by your reverse proxy settings.<br /><br />
|
1. This could be caused by your reverse proxy settings.<br /><br />
|
||||||
2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not using a reverse proxy make sure to set serve_from_sub_path to true.<br />
|
2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not
|
||||||
|
using a reverse proxy make sure to set serve_from_sub_path to true.<br />
|
||||||
<br />
|
<br />
|
||||||
3. If you have a local dev build make sure you build frontend using: yarn start, yarn start:hot, or yarn
|
3. If you have a local dev build make sure you build frontend using: yarn start, yarn start:hot, or yarn
|
||||||
build<br />
|
build<br />
|
||||||
<br />
|
<br />
|
||||||
4. Sometimes restarting grafana-server can help<br />
|
4. Sometimes restarting grafana-server can help<br />
|
||||||
<br />
|
<br />
|
||||||
5. You might be using a non-supported browser. To make sure read the documentation on <a href="https://grafana.com/docs/grafana/latest/installation/requirements/#supported-web-browsers">supported browsers</a>.
|
5. Check if you are using a non-supported browser. For more information, refer to the list of <a
|
||||||
|
href="https://grafana.com/docs/grafana/latest/installation/requirements/#supported-web-browsers">
|
||||||
|
supported browsers</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -217,7 +218,7 @@
|
|||||||
<search-wrapper></search-wrapper>
|
<search-wrapper></search-wrapper>
|
||||||
|
|
||||||
<div class="main-view">
|
<div class="main-view">
|
||||||
<div ng-view class="scroll-canvas"></div>
|
<div ng-view class="scroll-canvas"></div>
|
||||||
</div>
|
</div>
|
||||||
</grafana-app>
|
</grafana-app>
|
||||||
|
|
||||||
@ -235,57 +236,64 @@
|
|||||||
preloader[0].className = "preloader preloader--done";
|
preloader[0].className = "preloader preloader--done";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.public_cdn_path = '[[.ContentDeliveryURL]]/public/build/';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
[[if .GoogleTagManagerId]]
|
[[if .GoogleTagManagerId]]
|
||||||
<!-- Google Tag Manager -->
|
<!-- Google Tag Manager -->
|
||||||
<script nonce="[[.Nonce]]">
|
<script nonce="[[.Nonce]]">
|
||||||
dataLayer = [
|
dataLayer = [
|
||||||
{
|
{
|
||||||
IsSignedIn: '[[.User.IsSignedIn]]',
|
IsSignedIn: '[[.User.IsSignedIn]]',
|
||||||
Email: '[[.User.Email]]',
|
Email: '[[.User.Email]]',
|
||||||
Name: '[[.User.Name]]',
|
Name: '[[.User.Name]]',
|
||||||
UserId: '[[.User.Id]]',
|
UserId: '[[.User.Id]]',
|
||||||
OrgId: '[[.User.OrgId]]',
|
OrgId: '[[.User.OrgId]]',
|
||||||
OrgName: '[[.User.OrgName]]',
|
OrgName: '[[.User.OrgName]]',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
<noscript>
|
<noscript>
|
||||||
<iframe
|
<iframe
|
||||||
src="//www.googletagmanager.com/ns.html?id=[[.GoogleTagManagerId]]"
|
src="//www.googletagmanager.com/ns.html?id=[[.GoogleTagManagerId]]"
|
||||||
height="0"
|
height="0"
|
||||||
width="0"
|
width="0"
|
||||||
style="display:none;visibility:hidden"
|
style="display: none; visibility: hidden"
|
||||||
></iframe>
|
></iframe>
|
||||||
</noscript>
|
</noscript>
|
||||||
<script nonce="[[.Nonce]]">
|
<script nonce="[[.Nonce]]">
|
||||||
(function(w, d, s, l, i) {
|
(function (w, d, s, l, i) {
|
||||||
w[l] = w[l] || [];
|
w[l] = w[l] || [];
|
||||||
w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
|
w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
|
||||||
var f = d.getElementsByTagName(s)[0],
|
var f = d.getElementsByTagName(s)[0],
|
||||||
j = d.createElement(s),
|
j = d.createElement(s),
|
||||||
dl = l != 'dataLayer' ? '&l=' + l : '';
|
dl = l != 'dataLayer' ? '&l=' + l : '';
|
||||||
j.async = true;
|
j.async = true;
|
||||||
j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl;
|
j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl;
|
||||||
f.parentNode.insertBefore(j, f);
|
f.parentNode.insertBefore(j, f);
|
||||||
})(window, document, 'script', 'dataLayer', '[[.GoogleTagManagerId]]');
|
})(window, document, 'script', 'dataLayer', '[[.GoogleTagManagerId]]');
|
||||||
</script>
|
</script>
|
||||||
<!-- End Google Tag Manager -->
|
<!-- End Google Tag Manager -->
|
||||||
[[end]]
|
[[end]]
|
||||||
|
|
||||||
<%
|
<% for (key in htmlWebpackPlugin.files.chunks) { %>
|
||||||
for (key in htmlWebpackPlugin.files.chunks) { %><%
|
<% if (htmlWebpackPlugin.files.jsIntegrity) { %>
|
||||||
if (htmlWebpackPlugin.files.jsIntegrity) { %>
|
<script
|
||||||
<script nonce="[[.Nonce]]"
|
nonce="[[.Nonce]]"
|
||||||
src="<%= htmlWebpackPlugin.files.chunks[key].entry %>"
|
src="[[.ContentDeliveryURL]]/<%= htmlWebpackPlugin.files.chunks[key].entry %>"
|
||||||
type="text/javascript"
|
type="text/javascript"
|
||||||
integrity="<%= htmlWebpackPlugin.files.jsIntegrity[htmlWebpackPlugin.files.js.indexOf(htmlWebpackPlugin.files.chunks[key].entry)] %>"
|
integrity="<%= htmlWebpackPlugin.files.jsIntegrity[htmlWebpackPlugin.files.js.indexOf(htmlWebpackPlugin.files.chunks[key].entry)] %>"
|
||||||
crossorigin="<%= webpackConfig.output.crossOriginLoading %>"></script><%
|
crossorigin="<%= webpackConfig.output.crossOriginLoading %>">
|
||||||
} else { %>
|
</script>
|
||||||
<script nonce="[[.Nonce]]" src="<%= htmlWebpackPlugin.files.chunks[key].entry %>" type="text/javascript"></script><%
|
<% } else { %>
|
||||||
} %><%
|
<script
|
||||||
} %>
|
nonce="[[.Nonce]]"
|
||||||
|
src="[[.ContentDeliveryURL]]/<%= htmlWebpackPlugin.files.chunks[key].entry %>"
|
||||||
|
type="text/javascript">
|
||||||
|
</script>
|
||||||
|
<% } %>
|
||||||
|
<% } %>
|
||||||
<script nonce="[[.Nonce]]">
|
<script nonce="[[.Nonce]]">
|
||||||
performance.mark('js done blocking');
|
performance.mark('js done blocking');
|
||||||
</script>
|
</script>
|
||||||
|
Reference in New Issue
Block a user