From 116ffa942e60d259131e54f45c6a6ed4538853d1 Mon Sep 17 00:00:00 2001 From: brightwu <1521488775@qq.com> Date: Thu, 25 Jan 2024 15:21:52 +0800 Subject: [PATCH] fix: vite migrate bugs (#912) * fix: vite migrate bugs * chore: update tests * fix: default node buildin externals --------- Co-authored-by: brightwwu --- .changeset/mean-ducks-confess.md | 5 + ROADMAP.md | 2 +- ROADMAP.zh-CN.md | 2 +- crates/compiler/src/build/mod.rs | 1 + crates/plugin_static_assets/src/lib.rs | 14 +- crates/utils/src/lib.rs | 6 +- cspell.json | 3 +- packages/core/binding/index.d.ts | 1 + packages/core/src/compiler/index.ts | 8 +- packages/core/src/config/index.ts | 8 +- .../normalize-config/normalize-external.ts | 37 +++- packages/core/src/config/schema.ts | 3 + .../src/plugin/js/apply-html-transform.ts | 198 ++++++++++++++++++ .../core/src/plugin/js/farm-to-vite-config.ts | 35 +++- .../core/src/plugin/js/vite-plugin-adapter.ts | 26 ++- rust-plugins/sass/src/lib.rs | 16 +- 16 files changed, 310 insertions(+), 55 deletions(-) create mode 100644 .changeset/mean-ducks-confess.md create mode 100644 packages/core/src/plugin/js/apply-html-transform.ts diff --git a/.changeset/mean-ducks-confess.md b/.changeset/mean-ducks-confess.md new file mode 100644 index 0000000000..bef3bd3692 --- /dev/null +++ b/.changeset/mean-ducks-confess.md @@ -0,0 +1,5 @@ +--- +'@farmfe/core': patch +--- + +Fix bugs && Support object result of transformIndexHtml Hook diff --git a/ROADMAP.md b/ROADMAP.md index a87bde9093..38a44bdb22 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -12,7 +12,7 @@ Farm has implemented all basic features for a web building tool. - [x] Tree Shaking - [x] CSS Modules - [x] Official Plugins like Sass -- [ ] Persistent Cache +- [x] Persistent Cache - [x] Polyfill See milestones: https://github.com/farm-fe/farm/milestones diff --git a/ROADMAP.zh-CN.md b/ROADMAP.zh-CN.md index eafeb47716..3bb5effdcd 100644 --- a/ROADMAP.zh-CN.md +++ b/ROADMAP.zh-CN.md @@ -12,7 +12,7 @@ Farm 目前已经实现了 Web 构建工具的所有基本功能。 - [x] Tree Shaking - [x] CSS Modules - [x] Official Plugins like Sass -- [ ] Persistent Cache +- [x] Persistent Cache - [x] Polyfill 请参阅里程碑: https://github.com/farm-fe/farm/milestones diff --git a/crates/compiler/src/build/mod.rs b/crates/compiler/src/build/mod.rs index 0581e4147a..2463f7159e 100644 --- a/crates/compiler/src/build/mod.rs +++ b/crates/compiler/src/build/mod.rs @@ -254,6 +254,7 @@ impl Compiler { // try load source map after load module content. // TODO load source map in load hook and add a context.load_source_map method + // TODO load css source map if context.config.sourcemap.enabled(module.immutable) && load_result.content.contains("//# sourceMappingURL") { diff --git a/crates/plugin_static_assets/src/lib.rs b/crates/plugin_static_assets/src/lib.rs index e39e275f52..8079f01de4 100644 --- a/crates/plugin_static_assets/src/lib.rs +++ b/crates/plugin_static_assets/src/lib.rs @@ -21,12 +21,13 @@ use farmfe_toolkit::{ fs::{read_file_raw, read_file_utf8, transform_output_filename}, lazy_static::lazy_static, }; +use farmfe_utils::stringify_query; // Default supported static assets: png, jpg, jpeg, gif, svg, webp, mp4, webm, wav, mp3, wma, m4a, aac, ico, ttf, woff, woff2 lazy_static! { static ref DEFAULT_STATIC_ASSETS: Vec<&'static str> = vec![ "png", "jpg", "jpeg", "gif", "svg", "webp", "mp4", "webm", "wav", "mp3", "wma", "m4a", "aac", - "ico", "ttf", "woff", "woff2", "txt" + "ico", "ttf", "woff", "woff2", "txt", "eot" ]; } @@ -178,10 +179,6 @@ impl Plugin for FarmPluginStaticAssets { ignore_previous_source_map: false, })); } else { - let filename = Path::new(param.resolved_path) - .file_prefix() - .and_then(|s| s.to_str()) - .unwrap(); let bytes = if param.content.is_empty() { read_file_raw(param.resolved_path)? } else { @@ -192,11 +189,16 @@ impl Plugin for FarmPluginStaticAssets { ) )? }; + let ext = Path::new(param.resolved_path) .extension() .and_then(|s| s.to_str()) .unwrap(); + let filename = Path::new(param.resolved_path) + .file_prefix() + .and_then(|s| s.to_str()) + .unwrap(); let resource_name = transform_output_filename( context.config.output.assets_filename.clone(), filename, @@ -225,7 +227,7 @@ impl Plugin for FarmPluginStaticAssets { // TODO refactor this to support cache context.emit_file(EmitFileParams { resolved_path: param.module_id.clone(), - name: resource_name, + name: resource_name + stringify_query(¶m.query).as_str(), content: bytes, resource_type: ResourceType::Asset(ext.to_string()), }); diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index eec9932780..4eb5b4425e 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -7,9 +7,9 @@ pub use pathdiff::diff_paths; pub mod hash; -pub const PARSE_QUERY_TRUE: &str = "true"; +pub const PARSE_QUERY_TRUE: &str = ""; -/// parse `?a=b` to `HashMap { a: b }`, `?a` to `HashMap { a: "true" }` +/// parse `?a=b` to `HashMap { a: b }`, `?a` to `HashMap { a: "" }` pub fn parse_query(path: &str) -> Vec<(String, String)> { if !path.contains('?') { return vec![]; @@ -138,7 +138,7 @@ mod tests { assert_eq!( parsed_query, vec![ - ("inline".to_string(), "true".to_string()), + ("inline".to_string(), "".to_string()), ("b".to_string(), "c".to_string()) ] ); diff --git a/cspell.json b/cspell.json index 045e065c50..bcaa354c76 100644 --- a/cspell.json +++ b/cspell.json @@ -140,7 +140,8 @@ "apng", "jfif", "pjpeg", - "flac" + "flac", + "Yuxi" ], "ignorePaths": [ "pnpm-lock.yaml", diff --git a/packages/core/binding/index.d.ts b/packages/core/binding/index.d.ts index c6cf94b348..cc8f08aaed 100644 --- a/packages/core/binding/index.d.ts +++ b/packages/core/binding/index.d.ts @@ -345,6 +345,7 @@ export interface Config { * Configure the imports that are external, and the imports that are external will not appear in the compiled product. */ external?: string[]; + externalNodeBuiltins?: boolean | string[]; mode?: 'development' | 'production'; root?: string; runtime?: RuntimeConfig; diff --git a/packages/core/src/compiler/index.ts b/packages/core/src/compiler/index.ts index dc480e61cd..626dc2e4d3 100644 --- a/packages/core/src/compiler/index.ts +++ b/packages/core/src/compiler/index.ts @@ -132,11 +132,11 @@ export class Compiler { : path.join(this.config.config.root, configOutputPath); for (const [name, resource] of Object.entries(resources)) { - if (process.env.NODE_ENV === 'test') { - console.log('Writing', name, 'to disk'); - } + // remove query params and hash of name + const nameWithoutQuery = name.split('?')[0]; + const nameWithoutHash = nameWithoutQuery.split('#')[0]; - const filePath = path.join(outputPath, base, name); + const filePath = path.join(outputPath, base, nameWithoutHash); if (!existsSync(path.dirname(filePath))) { mkdirSync(path.dirname(filePath), { recursive: true }); diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts index ef837e4d1d..89456f6b40 100644 --- a/packages/core/src/config/index.ts +++ b/packages/core/src/config/index.ts @@ -119,7 +119,7 @@ export async function resolveConfig( ); let vitePluginAdapters: JsPlugin[] = []; - const vitePlugins = userConfig?.vitePlugins ?? []; + const vitePlugins = (userConfig?.vitePlugins ?? []).filter(Boolean); // run config and configResolved hook if (vitePlugins.length) { vitePluginAdapters = await handleVitePlugins( @@ -222,12 +222,6 @@ export async function normalizeUserCompilationConfig( config.coreLibPath = bindingPath; - config.external = [ - ...module.builtinModules.map((m) => `^${m}$`), - ...module.builtinModules.map((m) => `^node:${m}$`), - ...(Array.isArray(config.external) ? config.external : []) - ]; - normalizeOutput(config, isProduction); normalizeExternal(config); diff --git a/packages/core/src/config/normalize-config/normalize-external.ts b/packages/core/src/config/normalize-config/normalize-external.ts index dac2e47480..fb24f4455a 100644 --- a/packages/core/src/config/normalize-config/normalize-external.ts +++ b/packages/core/src/config/normalize-config/normalize-external.ts @@ -1,15 +1,42 @@ import module from 'node:module'; import { Config } from '../../../binding/index.js'; +import { existsSync, readFileSync } from 'node:fs'; +import path from 'node:path'; export function normalizeExternal(config: Config['config']) { - const defaultExternals = [...module.builtinModules, ...module.builtinModules] - .filter((m) => !config.resolve?.alias?.[m]) - .map((m) => `^${m}$`); + const defaultExternals: string[] = []; + const externalNodeBuiltins = config.externalNodeBuiltins ?? true; + + if (externalNodeBuiltins) { + if (Array.isArray(externalNodeBuiltins)) { + defaultExternals.push(...externalNodeBuiltins); + } else if (externalNodeBuiltins === true) { + let packageJson: any = {}; + const pkgPath = path.join(config.root || process.cwd(), 'package.json'); + // the project installed polyfill + if (existsSync(pkgPath)) { + try { + packageJson = JSON.parse(readFileSync(pkgPath, 'utf8')); + } catch { + /**/ + } + } + + defaultExternals.push( + ...[...module.builtinModules].filter( + (m) => + !config.resolve?.alias?.[m] && + !packageJson?.devDependencies?.[m] && + !packageJson?.dependencies?.[m] + ) + ); + } + } config.external = [ ...(config.external ?? []), - ...defaultExternals.map((m) => `^${m}($|/)`), - ...defaultExternals.map((m) => `^node:${m}($|/)`) + ...defaultExternals.map((m) => `^${m}($|/promises$)`), + ...defaultExternals.map((m) => `^node:${m}($|/promises$)`) ]; } diff --git a/packages/core/src/config/schema.ts b/packages/core/src/config/schema.ts index 4fc7fc45e4..ac46df470f 100644 --- a/packages/core/src/config/schema.ts +++ b/packages/core/src/config/schema.ts @@ -46,6 +46,9 @@ const compilationConfigSchema = z .optional(), define: z.record(z.any()).optional(), external: z.array(z.string()).optional(), + externalNodeBuiltins: z + .union([z.boolean(), z.array(z.string())]) + .optional(), mode: z.string().optional(), watch: z .union([ diff --git a/packages/core/src/plugin/js/apply-html-transform.ts b/packages/core/src/plugin/js/apply-html-transform.ts new file mode 100644 index 0000000000..a26d441dfd --- /dev/null +++ b/packages/core/src/plugin/js/apply-html-transform.ts @@ -0,0 +1,198 @@ +/** + * @license MIT Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors. + * This file is the same as https://github.com/vitejs/vite/blob/main/packages/vite/src/node/plugins/html.ts#L1185 + */ + +export interface HtmlTagDescriptor { + tag: string; + attrs?: Record; + children?: string | HtmlTagDescriptor[]; + /** + * default: 'head-prepend' + */ + injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'; +} + +export function applyHtmlTransform( + html: string, + res: HtmlTagDescriptor[] | { html: string; tags: HtmlTagDescriptor[] } +): string { + let tags: HtmlTagDescriptor[]; + if (Array.isArray(res)) { + tags = res; + } else { + html = res.html || html; + tags = res.tags; + } + + let headTags: HtmlTagDescriptor[] | undefined; + let headPrependTags: HtmlTagDescriptor[] | undefined; + let bodyTags: HtmlTagDescriptor[] | undefined; + let bodyPrependTags: HtmlTagDescriptor[] | undefined; + + for (const tag of tags) { + switch (tag.injectTo) { + case 'body': + (bodyTags ??= []).push(tag); + break; + case 'body-prepend': + (bodyPrependTags ??= []).push(tag); + break; + case 'head': + (headTags ??= []).push(tag); + break; + default: + (headPrependTags ??= []).push(tag); + } + } + + if (headPrependTags) html = injectToHead(html, headPrependTags, true); + if (headTags) html = injectToHead(html, headTags); + if (bodyPrependTags) html = injectToBody(html, bodyPrependTags, true); + if (bodyTags) html = injectToBody(html, bodyTags); + + return html; +} + +const headInjectRE = /([ \t]*)<\/head>/i; +const headPrependInjectRE = /([ \t]*)]*>/i; + +const htmlInjectRE = /<\/html>/i; +const htmlPrependInjectRE = /([ \t]*)]*>/i; + +const bodyInjectRE = /([ \t]*)<\/body>/i; +const bodyPrependInjectRE = /([ \t]*)]*>/i; + +const doctypePrependInjectRE = //i; + +function injectToHead( + html: string, + tags: HtmlTagDescriptor[], + prepend = false +) { + if (tags.length === 0) return html; + + if (prepend) { + // inject as the first element of head + if (headPrependInjectRE.test(html)) { + return html.replace( + headPrependInjectRE, + (match, p1) => `${match}\n${serializeTags(tags, incrementIndent(p1))}` + ); + } + } else { + // inject before head close + if (headInjectRE.test(html)) { + // respect indentation of head tag + return html.replace( + headInjectRE, + (match, p1) => `${serializeTags(tags, incrementIndent(p1))}${match}` + ); + } + // try to inject before the body tag + if (bodyPrependInjectRE.test(html)) { + return html.replace( + bodyPrependInjectRE, + (match, p1) => `${serializeTags(tags, p1)}\n${match}` + ); + } + } + // if no head tag is present, we prepend the tag for both prepend and append + return prependInjectFallback(html, tags); +} + +function injectToBody( + html: string, + tags: HtmlTagDescriptor[], + prepend = false +) { + if (tags.length === 0) return html; + + if (prepend) { + // inject after body open + if (bodyPrependInjectRE.test(html)) { + return html.replace( + bodyPrependInjectRE, + (match, p1) => `${match}\n${serializeTags(tags, incrementIndent(p1))}` + ); + } + // if no there is no body tag, inject after head or fallback to prepend in html + if (headInjectRE.test(html)) { + return html.replace( + headInjectRE, + (match, p1) => `${match}\n${serializeTags(tags, p1)}` + ); + } + return prependInjectFallback(html, tags); + } else { + // inject before body close + if (bodyInjectRE.test(html)) { + return html.replace( + bodyInjectRE, + (match, p1) => `${serializeTags(tags, incrementIndent(p1))}${match}` + ); + } + // if no body tag is present, append to the html tag, or at the end of the file + if (htmlInjectRE.test(html)) { + return html.replace(htmlInjectRE, `${serializeTags(tags)}\n$&`); + } + return html + `\n` + serializeTags(tags); + } +} + +function prependInjectFallback(html: string, tags: HtmlTagDescriptor[]) { + // prepend to the html tag, append after doctype, or the document start + if (htmlPrependInjectRE.test(html)) { + return html.replace(htmlPrependInjectRE, `$&\n${serializeTags(tags)}`); + } + if (doctypePrependInjectRE.test(html)) { + return html.replace(doctypePrependInjectRE, `$&\n${serializeTags(tags)}`); + } + return serializeTags(tags) + html; +} + +const unaryTags = new Set(['link', 'meta', 'base']); + +function serializeTag( + { tag, attrs, children }: HtmlTagDescriptor, + indent = '' +): string { + if (unaryTags.has(tag)) { + return `<${tag}${serializeAttrs(attrs)}>`; + } else { + return `<${tag}${serializeAttrs(attrs)}>${serializeTags( + children, + incrementIndent(indent) + )}`; + } +} + +function serializeTags( + tags: HtmlTagDescriptor['children'], + indent = '' +): string { + if (typeof tags === 'string') { + return tags; + } else if (tags && tags.length) { + return tags + .map((tag) => `${indent}${serializeTag(tag, indent)}\n`) + .join(''); + } + return ''; +} + +function serializeAttrs(attrs: HtmlTagDescriptor['attrs']): string { + let res = ''; + for (const key in attrs) { + if (typeof attrs[key] === 'boolean') { + res += attrs[key] ? ` ${key}` : ``; + } else { + res += ` ${key}=${JSON.stringify(attrs[key])}`; + } + } + return res; +} + +function incrementIndent(indent = '') { + return `${indent}${indent[0] === '\t' ? '\t' : ' '}`; +} diff --git a/packages/core/src/plugin/js/farm-to-vite-config.ts b/packages/core/src/plugin/js/farm-to-vite-config.ts index ca4e7e1464..9d4a216316 100644 --- a/packages/core/src/plugin/js/farm-to-vite-config.ts +++ b/packages/core/src/plugin/js/farm-to-vite-config.ts @@ -9,7 +9,7 @@ import { Logger } from '../../index.js'; import { VITE_DEFAULT_ASSETS } from './constants.js'; export function farmUserConfigToViteConfig(config: UserConfig): ViteUserConfig { - const vitePlugins = config.vitePlugins.map((plugin) => { + const vitePlugins = config.vitePlugins.filter(Boolean).map((plugin) => { if (typeof plugin === 'function') { return plugin().vitePlugin; } else { @@ -49,7 +49,11 @@ export function farmUserConfigToViteConfig(config: UserConfig): ViteUserConfig { strictPort: config.server?.strictPort, https: config.server?.https, proxy: config.server?.proxy as any, - open: config.server?.open + open: config.server?.open, + watch: + typeof config.server?.hmr === 'object' + ? config.server.hmr?.watchOptions ?? {} + : {} // other options are not supported in farm }, // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -124,7 +128,9 @@ export function proxyViteConfig( 'ssr', 'logLevel', 'experimental', - 'test' + 'test', + 'clearScreen', + 'customLogger' ]; if (allowedKeys.includes(String(key))) { @@ -174,7 +180,8 @@ export function proxyViteConfig( 'https', 'proxy', 'open', - 'origin' + 'origin', + 'watch' ]; return new Proxy(target.server || {}, { @@ -208,7 +215,8 @@ export function proxyViteConfig( 'devSourcemap', 'transformer', 'modules', - 'postcss' + 'postcss', + 'preprocessorOptions' ]; return new Proxy(target.css || {}, { @@ -245,7 +253,8 @@ export function proxyViteConfig( 'cssMinify', 'ssr', 'watch', - 'rollupOptions' + 'rollupOptions', + 'assetsDir' ]; return new Proxy(target.build || {}, { @@ -377,6 +386,20 @@ export function viteConfigToFarmConfig( farmConfig.server.hmr = config.server.hmr; farmConfig.server.port = config.server.port; + if (config.server.watch) { + if ( + farmConfig.server?.hmr === true || + farmConfig.server?.hmr === undefined + ) { + farmConfig.server.hmr = { + ...(typeof origFarmConfig?.server?.hmr === 'object' + ? origFarmConfig.server.hmr + : {}), + watchOptions: config.server.watch + }; + } + } + if (typeof config.server.host === 'string') { farmConfig.server.host = config.server.host; } diff --git a/packages/core/src/plugin/js/vite-plugin-adapter.ts b/packages/core/src/plugin/js/vite-plugin-adapter.ts index 7b75022c5c..a74800622d 100644 --- a/packages/core/src/plugin/js/vite-plugin-adapter.ts +++ b/packages/core/src/plugin/js/vite-plugin-adapter.ts @@ -37,6 +37,7 @@ import type { ModuleNode, ConfigEnv } from 'vite'; + import type { ResolveIdResult, RenderChunkHook, @@ -71,6 +72,7 @@ import { transformFarmConfigToRollupNormalizedInputOptions } from './utils.js'; import { Logger } from '../../index.js'; +import { applyHtmlTransform } from './apply-html-transform.js'; type OmitThis any> = T extends ( this: any, @@ -315,7 +317,7 @@ export class VitePluginAdapter implements JsPlugin { const logWarn = (name: string) => { this._logger.warn( - `Farm does not support '${name}' property of vite plugin ${this.name} hook ${hookName} for now. It will be ignored.` + `Farm does not support '${name}' property of vite plugin ${this.name} hook ${hookName} for now. '${name}' property will be ignored.` ); }; // TODO support order, if a hook has order, it should be split into two plugins @@ -761,21 +763,17 @@ export class VitePluginAdapter implements JsPlugin { transformIndexHtmlHook?: (...args: any[]) => Promise, bundles?: OutputBundle ) { - const result = await transformIndexHtmlHook?.( - Buffer.from(resource.bytes).toString(), - { - path: resource.name, - filename: resource.name, - server: bundles === undefined ? this._viteDevServer : undefined, - bundle: bundles, - chunk: transformResourceInfo2RollupResource(resource) - } - ); + const html = Buffer.from(resource.bytes).toString(); + const result = await transformIndexHtmlHook?.(html, { + path: resource.name, + filename: resource.name, + server: bundles === undefined ? this._viteDevServer : undefined, + bundle: bundles, + chunk: transformResourceInfo2RollupResource(resource) + }); if (result && typeof result !== 'string') { - throw new Error( - `Vite plugin "${this.name}" is not compatible with Farm for now. Cause it uses transformIndexHtmlHook and return non-string value. Farm only supports string return for transformIndexHtmlHook` - ); + return applyHtmlTransform(html, result); } else if (typeof result === 'string') { return result; } diff --git a/rust-plugins/sass/src/lib.rs b/rust-plugins/sass/src/lib.rs index 2f1262151f..ecef250fdf 100755 --- a/rust-plugins/sass/src/lib.rs +++ b/rust-plugins/sass/src/lib.rs @@ -40,11 +40,11 @@ impl FarmPluginSass { pub fn get_sass_options( &self, - url: String, + resolved_path_with_query: String, sourcemap_enabled: bool, ) -> (StringOptions, HashMap) { let options = serde_json::from_str(&self.sass_options).unwrap_or_default(); - get_options(options, url, sourcemap_enabled) + get_options(options, resolved_path_with_query, sourcemap_enabled) } } @@ -148,7 +148,7 @@ impl Plugin for FarmPluginSass { }); let (mut string_options, additional_options) = self.get_sass_options( - format!("/{}", param.module_id), + ModuleId::from(param.module_id.as_str()).resolved_path_with_query(&context.config.root), context.sourcemap_enabled(¶m.module_id.to_string()), ); @@ -284,12 +284,14 @@ fn get_exe_path(context: &Arc) -> PathBuf { fn get_options( options: Value, - url: String, + resolved_path_with_query: String, sourcemap_enabled: bool, ) -> (StringOptions, HashMap) { let mut builder = StringOptionsBuilder::new(); - builder = builder - .url(Url::from_file_path(&url).unwrap_or_else(|e| panic!("invalid url: {url}. Error: {e:?}"))); + builder = builder.url( + Url::from_file_path(&resolved_path_with_query) + .unwrap_or_else(|e| panic!("invalid path: {resolved_path_with_query}. Error: {e:?}")), + ); if let Some(source_map) = options.get("sourceMap") { builder = builder.source_map(source_map.as_bool().unwrap_or(sourcemap_enabled)); } else { @@ -334,7 +336,7 @@ fn get_options( ); } - if url.ends_with(".sass") { + if resolved_path_with_query.ends_with(".sass") { builder = builder.syntax(Syntax::Indented); }