fix: Hash based file naming

- For better HTTP caching and cache busting
- assets.json is created under [app]/dist folder which contains the map
of input file and output file name, this is used to get the correct path for
bundled assets
This commit is contained in:
Faris Ansari 2021-04-29 13:30:07 +05:30
parent 3b1cb347ef
commit dd69f1ab43
9 changed files with 136 additions and 85 deletions

View file

@ -13,6 +13,7 @@ let {
app_list,
assets_path,
apps_path,
sites_path,
get_app_path,
get_public_path,
get_cli_arg
@ -39,18 +40,26 @@ const NODE_PATHS = [].concat(
}
console.time(TOTAL_BUILD_TIME);
build_assets_for_apps(apps)
.then(() => {
console.timeEnd(TOTAL_BUILD_TIME);
console.log();
if (WATCH_MODE) {
console.log('Watching for changes...')
}
})
.catch(() => {
let error = chalk.white.bgRed(" ERROR ");
console.error(`${error} There were some problems during build`);
});
await clean_dist_folders(apps);
let result;
try {
result = await build_assets_for_apps(apps);
} catch (e) {
let error = chalk.white.bgRed(" ERROR ");
console.error(`${error} There were some problems during build`);
console.log();
console.log(chalk.dim(e.stack));
}
if (!WATCH_MODE) {
log_built_assets(result.metafile);
console.timeEnd(TOTAL_BUILD_TIME);
console.log();
await write_meta_file(result.metafile);
} else {
console.log("Watching for changes...");
}
})();
function build_assets_for_apps(apps) {
@ -117,48 +126,57 @@ function get_files_to_build(apps) {
}
function build_files({ files, outdir }) {
return esbuild
.build({
entryPoints: files,
outdir,
sourcemap: true,
bundle: true,
metafile: true,
// minify: true,
nodePaths: NODE_PATHS,
define: {
"process.env.NODE_ENV": "'development'"
},
plugins: [
html_plugin,
ignore_assets,
vue(),
postCssPlugin({
plugins: [require("autoprefixer")],
sassOptions: sass_options
})
],
return esbuild.build({
entryPoints: files,
entryNames: "[dir]/[name].[hash]",
outdir,
sourcemap: true,
bundle: true,
metafile: true,
// minify: true,
nodePaths: NODE_PATHS,
define: {
"process.env.NODE_ENV": "'development'"
},
plugins: [
html_plugin,
ignore_assets,
vue(),
postCssPlugin({
plugins: [require("autoprefixer")],
sassOptions: sass_options
})
],
watch: WATCH_MODE
? {
onRebuild(error, result) {
if (error)
console.error("watch build failed:", error);
else {
console.log(`${new Date().toLocaleTimeString()}: Compiled changes...`)
// log_build_meta(result.metafile);
}
watch: WATCH_MODE
? {
onRebuild(error, result) {
if (error) console.error("watch build failed:", error);
else {
console.log(
`${new Date().toLocaleTimeString()}: Compiled changes...`
);
}
}
: null
})
.then(result => {
log_build_meta(result.metafile);
});
}
}
: null
});
}
function log_build_meta(metafile) {
let column_widths = [40, 20];
async function clean_dist_folders(apps) {
for (let app of apps) {
let public_path = get_public_path(app);
await fs.promises.rmdir(path.resolve(public_path, "dist", "js"), {
recursive: true
});
await fs.promises.rmdir(path.resolve(public_path, "dist", "css"), {
recursive: true
});
}
}
function log_built_assets(metafile) {
let column_widths = [60, 20];
cliui.div(
{
text: chalk.cyan.bold("File"),
@ -217,3 +235,19 @@ function log_build_meta(metafile) {
}
console.log(cliui.toString());
}
function write_meta_file(metafile) {
let out = {};
for (let output in metafile.outputs) {
let info = metafile.outputs[output];
let asset_path = "/" + path.relative(sites_path, output);
if (info.entryPoint) {
out[path.basename(info.entryPoint)] = asset_path;
}
}
return fs.promises.writeFile(
path.resolve(assets_path, "frappe", "dist", "assets.json"),
JSON.stringify(out, null, 4)
);
}

View file

@ -13,6 +13,8 @@ common_default_keys = ["__default", "__global"]
doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map',
'milestone_tracker_map', 'event_consumer_document_type_map')
bench_cache_keys = ('assets_json',)
global_cache_keys = ("app_hooks", "installed_apps", 'all_apps',
"app_modules", "module_app", "system_settings",
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
@ -58,6 +60,7 @@ def clear_global_cache():
clear_doctype_cache()
clear_website_cache()
frappe.cache().delete_value(global_cache_keys)
frappe.cache().delete_value(bench_cache_keys)
frappe.setup_module_map()
def clear_defaults_cache(user=None):

View file

@ -29,16 +29,16 @@ page_js = {
# website
app_include_js = [
"/assets/frappe/dist/js/libs.bundle.js",
"/assets/frappe/dist/js/desk.bundle.js",
"/assets/frappe/dist/js/list.bundle.js",
"/assets/frappe/dist/js/form.bundle.js",
"/assets/frappe/dist/js/controls.bundle.js",
"/assets/frappe/dist/js/report.bundle.js",
"libs.bundle.js",
"desk.bundle.js",
"list.bundle.js",
"form.bundle.js",
"controls.bundle.js",
"report.bundle.js",
]
app_include_css = [
"/assets/frappe/dist/css/desk.bundle.css",
"/assets/frappe/dist/css/report.bundle.css",
"desk.bundle.css",
"report.bundle.css",
]
doctype_js = {

View file

@ -30,11 +30,11 @@
{%- if theme.name != 'Standard' -%}
<link type="text/css" rel="stylesheet" href="{{ theme.theme_url }}">
{%- else -%}
<link type="text/css" rel="stylesheet" href="/assets/frappe/dist/css/website.bundle.css">
{{ include_style('website.bundle.css') }}
{%- endif -%}
{%- for link in web_include_css %}
<link type="text/css" rel="stylesheet" href="{{ link|abs_url }}">
{{ include_style(link) }}
{%- endfor -%}
{%- endblock -%}
@ -96,11 +96,11 @@
{% block base_scripts %}
<!-- js should be loaded in body! -->
<script type="text/javascript" src="/assets/frappe/dist/js/frappe-web.bundle.js"></script>
{{ include_script('frappe-web.bundle.js') }}
{% endblock %}
{%- for link in web_include_js %}
<script type="text/javascript" src="{{ link | abs_url }}?ver={{ build_version }}"></script>
{{ include_script(link) }}
{%- endfor -%}
{%- block script %}

View file

@ -765,6 +765,22 @@ def get_build_version():
# this is not a major problem so send fallback
return frappe.utils.random_string(8)
def get_assets_json():
if not hasattr(frappe.local, "assets_json"):
assets_json = frappe.cache().get_value("assets_json", shared=True)
if not assets_json:
import json
assets_json = json.loads(
frappe.read_file("assets/frappe/dist/assets.json")
)
frappe.cache().set_value("assets_json", assets_json, shared=True)
frappe.local.assets_json = assets_json
return frappe.local.assets_json
def get_bench_relative_path(file_path):
"""Fixes paths relative to the bench root directory if exists and returns the absolute path

View file

@ -69,23 +69,21 @@ def web_blocks(blocks):
return html
def script(path):
path = assets_url(path)
if '/public/' in path:
path = path.replace('/public/', '/dist/')
def include_script(path):
if not path.startswith("/assets") and ".bundle." in path:
path = bundled_asset_path(path)
return f'<script type="text/javascript" src="{path}"></script>'
def style(path):
path = assets_url(path)
if '/public/' in path:
path = path.replace('/public/', '/dist/')
if path.endswith(('.scss', '.sass', '.less', '.styl')):
path = path.rsplit('.', 1)[0] + '.css'
def include_style(path):
if not path.startswith("/assets") and ".bundle." in path:
path = bundled_asset_path(path)
return f'<link type="text/css" rel="stylesheet" href="{path}">'
def assets_url(path):
if not path.startswith('/'):
path = '/' + path
if not path.startswith('/assets'):
path = '/assets' + path
return path
def bundled_asset_path(path):
from frappe.utils import get_assets_json
bundled_assets = get_assets_json()
return bundled_assets.get(path)

View file

@ -28,7 +28,7 @@ class RedisWrapper(redis.Redis):
return "{0}|{1}".format(frappe.conf.db_name, key).encode('utf-8')
def set_value(self, key, val, user=None, expires_in_sec=None):
def set_value(self, key, val, user=None, expires_in_sec=None, shared=False):
"""Sets cache value.
:param key: Cache key
@ -36,7 +36,7 @@ class RedisWrapper(redis.Redis):
:param user: Prepends key with User
:param expires_in_sec: Expire value of this key in X seconds
"""
key = self.make_key(key, user)
key = self.make_key(key, user, shared)
if not expires_in_sec:
frappe.local.cache[key] = val
@ -50,7 +50,7 @@ class RedisWrapper(redis.Redis):
except redis.exceptions.ConnectionError:
return None
def get_value(self, key, generator=None, user=None, expires=False):
def get_value(self, key, generator=None, user=None, expires=False, shared=False):
"""Returns cache value. If not found and generator function is
given, it will call the generator.
@ -59,7 +59,7 @@ class RedisWrapper(redis.Redis):
:param expires: If the key is supposed to be with an expiry, don't store it in frappe.local
"""
original_key = key
key = self.make_key(key, user)
key = self.make_key(key, user, shared)
if key in frappe.local.cache:
val = frappe.local.cache[key]

View file

@ -21,7 +21,7 @@
<link rel="icon"
href="{{ favicon or "/assets/frappe/images/frappe-favicon.svg" }}" type="image/x-icon">
{% for include in include_css -%}
{{ style(include) }}
{{ include_style(include) }}
{%- endfor -%}
</head>
<body>
@ -51,7 +51,7 @@
</script>
{% for include in include_js %}
{{ script(include) }}
{{ include_script(include) }}
{% endfor %}
{% include "templates/includes/app_analytics/google_analytics.html" %}

View file

@ -53,7 +53,7 @@
{% endmacro %}
{% block head_include %}
<link type="text/css" rel="stylesheet" href="/assets/frappe/dist/css/login.bundle.css">
{{ include_style('login.bundle.css') }}
{% endblock %}
{% macro logo_section() %}