diff --git a/README.md b/README.md index 8aba0aa29..795718561 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,11 @@ Started in 68ms and updated in 1ms for a demo react project as below. ![img](./assets/performance.png) **Features**: -* 🔥 **Super Fast**: Start a react / vue(incoming) project in milliseconds. -* ⚡ **"1ms" HMR**: Finish a HMR within 10ms for the most situations. -* 🧰 **Fully Pluggable**: Support both rust plugins and js plugins. -* ⚙️ **Native Web Assets Compiling Supported**: Support support compiling JS/TS/JSX/TSX, css, html natively. + +- 🔥 **Super Fast**: Start a react / vue(incoming) project in milliseconds. +- ⚡ **"1ms" HMR**: Finish a HMR within 10ms for the most situations. +- 🧰 **Fully Pluggable**: Support both rust plugins and js plugins. +- ⚙️ **Native Web Assets Compiling Supported**: Support support compiling JS/TS/JSX/TSX, css, html natively.
@@ -32,7 +33,9 @@ Started in 68ms and updated in 1ms for a demo react project as below.
## Getting Started + Install Farm Cli: + ```sh npm install -g @farmfe/cli ``` @@ -42,3 +45,113 @@ We provided a experience react project for now. Using `farm create` to create a ```sh farm create && cd farm-react && npm i && npm start ``` + +## Configuring + +> Official docs site is on the way... + +Farm load configuration file from `farm.config.ts`. The available config as below: + +```ts +export interface UserConfig { + /** current root of this project, default to current working directory */ + root?: string; + /** js plugin(which is a javascript object) and rust plugin(which is string refer to a .farm file or a package) */ + plugins?: (RustPlugin | JsPlugin)[]; + /** config related to compilation */ + compilation?: { + input?: Record; + output?: { + filename?: string; + path?: string; + publicPath?: string; + }; + resolve?: { + extensions?: string[]; + alias?: Record; + mainFields?: string[]; + conditions?: string[]; + symlinks: boolean; + }; + external?: string[]; + mode?: 'development' | 'production'; + root?: string; + runtime?: { + path: string; + plugins?: string[]; + swcHelpersPath?: string; + }; + script?: { + // specify target es version + target?: + | 'es3' + | 'es5' + | 'es2015' + | 'es2016' + | 'es2017' + | 'es2018' + | 'es2019' + | 'es2020' + | 'es2021' + | 'es2022'; + // config swc parser + parser?: { + esConfig?: { + jsx?: boolean; + fnBind: boolean; + // Enable decorators. + decorators: boolean; + + // babel: `decorators.decoratorsBeforeExport` + // + // Effective only if `decorator` is true. + decoratorsBeforeExport: boolean; + exportDefaultFrom: boolean; + // Stage 3. + importAssertions: boolean; + privateInObject: boolean; + allowSuperOutsideMethod: boolean; + allowReturnOutsideFunction: boolean; + }; + tsConfig?: { + tsx: boolean; + decorators: boolean; + /// `.d.ts` + dts: boolean; + noEarlyErrors: boolean; + }; + }; + }; + partialBundling?: { + moduleBuckets?: { + name: string; + test: string[]; + }[]; + }; + lazyCompilation?: boolean; + }; + /** config related to dev server */ + server?: UserServerConfig; +} + +export type RustPlugin = + | string + | [ + string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Record + ]; + +export interface JsPlugin { + resolve: JsPluginHook< + { + importers: string[]; + specifiers: string[]; + }, + PluginResolveHookParam, + PluginResolveHookResult + >; + + // load: JsPluginHook<{ filters: { ids: string[] }}>; +} +``` diff --git a/crates/compiler/src/generate/finalize_resources.rs b/crates/compiler/src/generate/finalize_resources.rs index 0e58bb4d0..09bda9a5c 100644 --- a/crates/compiler/src/generate/finalize_resources.rs +++ b/crates/compiler/src/generate/finalize_resources.rs @@ -1,17 +1,76 @@ -use std::sync::Arc; +use std::{ + fs::{create_dir_all, read_dir, remove_file, File}, + io::Write, + path::{Path, PathBuf}, + sync::Arc, +}; -use farmfe_core::context::CompilationContext; +use farmfe_core::{context::CompilationContext, relative_path::RelativePath}; use farmfe_toolkit::tracing; #[tracing::instrument(skip_all)] pub fn finalize_resources(context: &Arc) -> farmfe_core::error::Result<()> { tracing::trace!("Staring finalize_resources..."); - let mut resources_map = context.resources_map.lock(); - context - .plugin_driver - .finalize_resources(&mut *resources_map, context)?; + { + let mut resources_map = context.resources_map.lock(); + + context + .plugin_driver + .finalize_resources(&mut *resources_map, context)?; + } + + write_resources(context); tracing::trace!("Finished finalize_resources."); Ok(()) } + +pub fn write_resources(context: &Arc) { + let resources = context.resources_map.lock(); + let output_dir = if Path::new(&context.config.output.path).is_absolute() { + PathBuf::from(&context.config.output.path) + } else { + RelativePath::new(&context.config.output.path).to_logical_path(&context.config.root) + }; + + if !output_dir.exists() { + create_dir_all(output_dir.clone()).unwrap(); + } + + // Remove useless resources + let existing_resources = read_dir(output_dir.clone()) + .unwrap() + .map(|entry| { + let entry = entry.unwrap(); + let path = entry.path(); + let file_name = path.file_name().unwrap().to_str().unwrap().to_string(); + + file_name + }) + .collect::>(); + + for pre_resource in &existing_resources { + let file_path = RelativePath::new(pre_resource).to_logical_path(&output_dir); + // always remove html file + if pre_resource.ends_with(".html") { + remove_file(file_path).unwrap(); + continue; + } + + if !resources.contains_key(pre_resource) && file_path.exists() { + remove_file(file_path).unwrap(); + } + } + + // add new resources + for resource in resources.values() { + let file_path = RelativePath::new(&resource.name).to_logical_path(&output_dir); + // only write expose non-emitted resource + if !resource.emitted && !file_path.exists() { + let mut file = File::create(file_path).unwrap(); + + file.write_all(&resource.bytes).unwrap(); + } + } +} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 90121276a..882e71aa5 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -1,6 +1,11 @@ #![deny(clippy::all)] -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::HashMap, + fs::remove_dir_all, + path::{Path, PathBuf}, + sync::Arc, +}; use farmfe_compiler::{update::UpdateType, Compiler}; @@ -9,6 +14,7 @@ pub mod plugin_adapters; use farmfe_core::{ config::{Config, Mode}, module::ModuleId, + relative_path::RelativePath, }; use farmfe_toolkit::tracing_subscriber::{self, fmt, prelude::*, EnvFilter}; use napi::{bindgen_prelude::FromNapiValue, Env, JsObject, NapiRaw, Status}; @@ -108,19 +114,34 @@ impl JsCompiler { /// TODO: usage example #[napi] pub async fn compile(&self) -> napi::Result<()> { + let context = self.compiler.context(); + let output_dir = if Path::new(&context.config.output.path).is_absolute() { + PathBuf::from(&context.config.output.path) + } else { + RelativePath::new(&context.config.output.path).to_logical_path(&context.config.root) + }; + + if output_dir.exists() { + remove_dir_all(&output_dir).map_err(|e| { + napi::Error::new( + Status::GenericFailure, + format!("remove output dir error: {}", e), + ) + })?; + } + self .compiler .compile() - .map_err(|e| napi::Error::new(Status::GenericFailure, format!("{}", e))) + .map_err(|e| napi::Error::new(Status::GenericFailure, format!("{}", e)))?; + + Ok(()) } /// sync compile #[napi] pub fn compile_sync(&self) -> napi::Result<()> { - self - .compiler - .compile() - .map_err(|e| napi::Error::new(Status::GenericFailure, format!("{}", e))) + unimplemented!("sync compile is not supported yet") } /// async update, return promise @@ -179,23 +200,6 @@ impl JsCompiler { unimplemented!("sync update"); } - #[napi] - pub fn resources(&self) -> HashMap> { - let context = self.compiler.context(); - let resources = context.resources_map.lock(); - - let mut result = HashMap::new(); - - for resource in resources.values() { - // only write expose non-emitted resource - if !resource.emitted { - result.insert(resource.name.clone(), resource.bytes.clone()); - } - } - - result - } - #[napi] pub fn has_module(&self, resolved_path: String) -> bool { let context = self.compiler.context(); diff --git a/crates/plugin_partial_bundling/src/lib.rs b/crates/plugin_partial_bundling/src/lib.rs index 44eca4180..c5bf30bc2 100644 --- a/crates/plugin_partial_bundling/src/lib.rs +++ b/crates/plugin_partial_bundling/src/lib.rs @@ -152,18 +152,6 @@ impl Plugin for FarmPluginPartialBundling { .as_bytes(), 8, ); - // let id = format!( - // "{}-{}-{}-{}", - // module_bucket.id.to_string(), - // module_type.to_string(), - // module_ids - // .iter() - // .map(|m| m.to_string()) - // .collect::>() - // .join("_"), - // immutable - // ) - // .replace("/", "+"); let mut resource_pot = ResourcePot::new(ResourcePotId::new(id), module_type.into()); resource_pot.immutable = immutable; diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index c5e88664c..137ff8add 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,14 @@ # @farmfe/core +## 0.3.2 + +### Patch Changes + +- write resources to disk to optimize resources loading time +- Updated dependencies + - @farmfe/runtime-plugin-hmr@3.0.2 + - @farmfe/runtime@0.3.2 + ## 0.3.1 ### Patch Changes diff --git a/packages/core/binding/binding.d.ts b/packages/core/binding/binding.d.ts index c6d866800..01d3f7ec9 100644 --- a/packages/core/binding/binding.d.ts +++ b/packages/core/binding/binding.d.ts @@ -41,6 +41,5 @@ export class Compiler { update(paths: Array): Promise; /** sync update */ updateSync(paths: Array): JsUpdateResult; - resources(): Record>; hasModule(resolvedPath: string): boolean; } diff --git a/packages/core/package.json b/packages/core/package.json index ccc4f9d36..4eb642eb7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@farmfe/core", - "version": "0.3.1", + "version": "0.3.2", "main": "dist/index.js", "types": "dist/index.d.ts", "type": "module", @@ -61,8 +61,8 @@ "type-check": "tsc -p tsconfig.build.json --noEmit" }, "dependencies": { - "@farmfe/runtime": "workspace:^0.3.1", - "@farmfe/runtime-plugin-hmr": "workspace:^3.0.1", + "@farmfe/runtime": "workspace:^0.3.2", + "@farmfe/runtime-plugin-hmr": "workspace:^3.0.2", "@swc/helpers": "^0.4.9", "boxen": "^7.0.1", "chalk": "^5.2.0", diff --git a/packages/core/src/compiler/index.ts b/packages/core/src/compiler/index.ts index a38e54cef..16208952f 100644 --- a/packages/core/src/compiler/index.ts +++ b/packages/core/src/compiler/index.ts @@ -1,7 +1,3 @@ -import { existsSync, mkdirSync } from 'fs'; -import fs from 'fs/promises'; -import path from 'path'; - import type { Config, JsUpdateResult } from '../../binding/index.js'; import { Compiler as BindingCompiler } from '../../binding/index.js'; @@ -48,28 +44,7 @@ export class Compiler { return res; } - resources(): Record { - return this._bindingCompiler.resources(); - } - hasModule(resolvedPath: string): boolean { return this._bindingCompiler.hasModule(resolvedPath); } - - async writeResourcesToDisk(): Promise { - const resources = this.resources(); - const promises = []; - - for (const [name, resource] of Object.entries(resources)) { - const filePath = path.join(this.config.config.output.path, name); - - if (!existsSync(path.dirname(filePath))) { - mkdirSync(path.dirname(filePath), { recursive: true }); - } - - promises.push(fs.writeFile(filePath, Buffer.from(resource))); - } - - await Promise.all(promises); - } } diff --git a/packages/core/src/config/index.ts b/packages/core/src/config/index.ts index 9d9ea1083..0fb6b060d 100644 --- a/packages/core/src/config/index.ts +++ b/packages/core/src/config/index.ts @@ -133,7 +133,6 @@ export const DEFAULT_DEV_SERVER_OPTIONS: NormalizedServerConfig = { port: 9000, https: false, // http2: false, - writeToDisk: false, hmr: DEFAULT_HMR_OPTIONS, }; @@ -206,15 +205,21 @@ async function readConfigFile( // if config is written in typescript, we need to compile it to javascript using farm first if (resolvedPath.endsWith('.ts')) { const Compiler = (await import('../compiler/index.js')).Compiler; + const outputPath = path.join(os.tmpdir(), 'farmfe'); + const fileName = 'farm.config.bundle.mjs'; const normalizedConfig = await normalizeUserCompilationConfig({ compilation: { input: { config: resolvedPath, }, + output: { + filename: fileName, + path: outputPath, + }, partialBundling: { moduleBuckets: [ { - name: 'farm.config.bundle.js', + name: fileName, test: ['.+'], }, ], @@ -226,18 +231,9 @@ async function readConfigFile( }); const compiler = new Compiler(normalizedConfig); await compiler.compile(); - const resources = compiler.resources(); - // should only emit one config file bundled with all dependencies - const configCode = Buffer.from(Object.values(resources)[0]).toString(); - // Change to vm.module of node or loaders as far as it is stable - const filePath = path.join( - os.tmpdir(), - 'farmfe', - `temp-config-${Date.now()}.mjs` - ); - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, configCode); + const filePath = path.join(outputPath, fileName); + // Change to vm.module of node or loaders as far as it is stable if (process.platform === 'win32') { return (await import(pathToFileURL(filePath).toString())).default; } else { diff --git a/packages/core/src/config/types.ts b/packages/core/src/config/types.ts index 7e5da35c2..8afb9c39d 100644 --- a/packages/core/src/config/types.ts +++ b/packages/core/src/config/types.ts @@ -6,7 +6,6 @@ export interface UserServerConfig { port?: number; https?: boolean; // http2?: boolean; - writeToDisk?: boolean; hmr?: boolean | UserHmrConfig; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index eb8f4aa6c..e9d48052e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -58,5 +58,4 @@ export async function build(options: { const compiler = new Compiler(normalizedConfig); await compiler.compile(); logger.info(`Build completed in ${chalk.green(`${Date.now() - start}ms`)}!`); - compiler.writeResourcesToDisk(); } diff --git a/packages/core/src/server/index.ts b/packages/core/src/server/index.ts index f724f25e7..9c7d66be8 100644 --- a/packages/core/src/server/index.ts +++ b/packages/core/src/server/index.ts @@ -13,7 +13,6 @@ import { NormalizedServerConfig, normalizeDevServerOptions, } from '../config/index.js'; -import { resources } from './middlewares/resources.js'; import { hmr } from './middlewares/hmr.js'; import { HmrEngine } from './hmr-engine.js'; import { brandColor, Logger } from '../logger.js'; @@ -49,11 +48,7 @@ export class DevServer { mkdirSync(this._dist, { recursive: true }); } - if (this.config.writeToDisk) { - this._app.use(serve(this._dist)); - } else { - this._app.use(resources(this._compiler)); - } + this._app.use(serve(this._dist)); if (this.config.hmr) { this.ws = new WebSocketServer({ @@ -79,10 +74,6 @@ export class DevServer { await this._compiler.compile(); const end = Date.now(); - if (this.config.writeToDisk) { - this._compiler.writeResourcesToDisk(); - } - this._app.listen(this.config.port); const version = JSON.parse( readFileSync( diff --git a/packages/core/src/server/middlewares/resources.ts b/packages/core/src/server/middlewares/resources.ts deleted file mode 100644 index 87a4c382f..000000000 --- a/packages/core/src/server/middlewares/resources.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Serve resources that stored in memory. This middleware will be enabled when server.writeToDisk is false. - */ - -import { Context, Next } from 'koa'; -import { extname } from 'path'; -import { Compiler } from '../../compiler/index.js'; - -export function resources(compiler: Compiler) { - return async (ctx: Context, next: Next) => { - await next(); - - if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return; - // the response is already handled - if (ctx.body || ctx.status !== 404) return; - - const resourcePath = ctx.path.slice(1) || 'index.html'; // remove leading slash - ctx.type = extname(resourcePath); - const resource = compiler.resources()[resourcePath]; - - if (!resource) return; - - ctx.body = Buffer.from(resource); - }; -} diff --git a/packages/core/tests/config.spec.ts b/packages/core/tests/config.spec.ts index 9114e42de..264d94d49 100644 --- a/packages/core/tests/config.spec.ts +++ b/packages/core/tests/config.spec.ts @@ -37,10 +37,8 @@ test('normalize-dev-server-options', () => { let options = normalizeDevServerOptions({}); expect(options.https).toBe(DEFAULT_DEV_SERVER_OPTIONS.https); expect(options.port).toBe(DEFAULT_DEV_SERVER_OPTIONS.port); - expect(options.writeToDisk).toBe(DEFAULT_DEV_SERVER_OPTIONS.writeToDisk); - options = normalizeDevServerOptions({ writeToDisk: true }); + options = normalizeDevServerOptions({ port: 8080 }); expect(options.https).toBe(DEFAULT_DEV_SERVER_OPTIONS.https); - expect(options.port).toBe(DEFAULT_DEV_SERVER_OPTIONS.port); - expect(options.writeToDisk).toBe(true); + expect(options.port).toBe(8080); }); diff --git a/packages/runtime-plugin-hmr/CHANGELOG.md b/packages/runtime-plugin-hmr/CHANGELOG.md index b8a42250c..efcbd3ed6 100644 --- a/packages/runtime-plugin-hmr/CHANGELOG.md +++ b/packages/runtime-plugin-hmr/CHANGELOG.md @@ -1,5 +1,11 @@ # @farmfe/runtime-plugin-hmr +## 3.0.2 + +### Patch Changes + +- write resources to disk to optimize resources loading time + ## 3.0.1 ### Patch Changes diff --git a/packages/runtime-plugin-hmr/package.json b/packages/runtime-plugin-hmr/package.json index 63eaa1cfe..0404b7e03 100644 --- a/packages/runtime-plugin-hmr/package.json +++ b/packages/runtime-plugin-hmr/package.json @@ -1,6 +1,6 @@ { "name": "@farmfe/runtime-plugin-hmr", - "version": "3.0.1", + "version": "3.0.2", "description": "Runtime hmr plugin of Farm", "author": { "name": "bright wu", @@ -11,6 +11,6 @@ "build": "tsc -p tsconfig.json --noEmit" }, "devDependencies": { - "@farmfe/runtime": "workspace:^0.3.1" + "@farmfe/runtime": "workspace:^0.3.2" } } diff --git a/packages/runtime/CHANGELOG.md b/packages/runtime/CHANGELOG.md index b3d667150..c8d4ba82e 100644 --- a/packages/runtime/CHANGELOG.md +++ b/packages/runtime/CHANGELOG.md @@ -1,5 +1,11 @@ # @farmfe/runtime +## 0.3.2 + +### Patch Changes + +- write resources to disk to optimize resources loading time + ## 0.3.1 ### Patch Changes diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 02ab2a344..08534f5d3 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@farmfe/runtime", - "version": "0.3.1", + "version": "0.3.2", "description": "Runtime of Farm", "author": { "name": "bright wu", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b5f8134c..8f6dd625b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,8 +90,8 @@ importers: packages/core: specifiers: - '@farmfe/runtime': workspace:^0.3.1 - '@farmfe/runtime-plugin-hmr': workspace:^3.0.1 + '@farmfe/runtime': workspace:^0.3.2 + '@farmfe/runtime-plugin-hmr': workspace:^3.0.2 '@napi-rs/cli': ^2.10.0 '@swc/helpers': ^0.4.9 '@types/figlet': ^1.5.5 @@ -145,7 +145,7 @@ importers: packages/runtime-plugin-hmr: specifiers: - '@farmfe/runtime': workspace:^0.3.1 + '@farmfe/runtime': workspace:^0.3.2 devDependencies: '@farmfe/runtime': link:../runtime