Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(watchConfig): support hmr #78

Merged
merged 4 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Smart Configuration Loader.
- Reads config from the nearest `package.json` file
- [Extends configurations](https://github.com/unjs/c12#extending-configuration) from multiple local or git sources
- Overwrite with [environment-specific configuration](#environment-specific-configuration)
- Config watcher with auto reload
- Config watcher with auto reload and HMR support

## Usage

Expand Down Expand Up @@ -237,7 +237,13 @@ c12 tries to match [`envName`](#envname) and override environment config if spec

## Watching Configuration

you can use `watchConfig` instead of `loadConfig` to load config and watch for changes, add and removals in all expected configuration paths and auto reload with new config,
you can use `watchConfig` instead of `loadConfig` to load config and watch for changes, add and removals in all expected configuration paths and auto reload with new config.

### Lifecycle hooks

- `onWatch`: This function is always called when a config is updated, added or removed before attempting to reload config.
- `acceptHMR`: By implementing this function, you can compare old and new function and return `true` if a full reload is not needed.
- `onUpdate`: This function is always called after new config is updated. If `acceptHMR` returns true, it will be skipped.

```ts
import { watchConfig } from "c12";
Expand All @@ -246,16 +252,27 @@ const config = watchConfig({
cwd: ".",
// chokidarOptions: {}, // Default is { ignoreInitial: true }
// debounce: 200 // Default is 100. You can set to fale to disable debounced watcher
onChange: ({ config, oldConfig, path, type }) => {
console.log("[watcher]", type, path);
onWatch: (event) => {
console.log("[watcher]", event.type, event.path);
},
acceptHMR({ oldConfig, newConfig, getDiff }) {
const diff = getDiff();
if (diff.length === 0) {
console.log("No config changed detected!");
return true; // No changes!
}
},
onUpdate({ oldConfig, newConfig, getDiff }) {
const diff = getDiff();
console.log("Config updated:\n" + diff.map((i) => i.toJSON()).join("\n"));
},
});

console.log("initial config", config.config, config.layers);
console.log("watching config files:", config.watchingFiles);
console.log("initial config", config.config);

// When exiting process
await config.unwatch();
// Stop watcher when not needed anymore
// await config.unwatch();
```

## 💻 Development
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"giget": "^1.1.2",
"jiti": "^1.18.2",
"mlly": "^1.2.0",
"ohash": "^1.1.1",
"pathe": "^1.1.0",
"perfect-debounce": "^0.1.3",
"pkg-types": "^1.0.2",
Expand All @@ -53,4 +54,4 @@
"vitest": "^0.30.1"
},
"packageManager": "pnpm@8.3.0"
}
}
18 changes: 14 additions & 4 deletions playground/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,23 @@ async function main() {
extend: {
extendKey: ["theme", "extends"],
},
onChange: ({ config, path, type }) => {
console.log("[watcher]", type, path);
console.log(config.config);
onWatch: (event) => {
console.log("[watcher]", event.type, event.path);
},
acceptHMR({ oldConfig, newConfig, getDiff }) {
const diff = getDiff();
if (diff.length === 0) {
console.log("No config changed detected!");
return true; // No changes!
}
},
onUpdate({ oldConfig, newConfig, getDiff }) {
const diff = getDiff();
console.log("Config updated:\n" + diff.map((i) => i.toJSON()).join("\n"));
},
});
console.log("initial config", config.config, config.layers);
console.log("watching config files:", config.watchingFiles);
console.log("initial config", config.config);
}

// eslint-disable-next-line unicorn/prefer-top-level-await
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 40 additions & 12 deletions src/watch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { watch, WatchOptions } from "chokidar";
import { debounce } from "perfect-debounce";
import { resolve } from "pathe";
import { diff } from "ohash";
import type {
UserInputConfig,
ConfigLayerMeta,
Expand All @@ -17,19 +18,30 @@ export type ConfigWatcher<
unwatch: () => Promise<void>;
};

export type WatchConfigOptions<
export interface WatchConfigOptions<
T extends UserInputConfig = UserInputConfig,
MT extends ConfigLayerMeta = ConfigLayerMeta
> = {
> extends LoadConfigOptions<T, MT> {
chokidarOptions?: WatchOptions;
debounce?: false | number;
onChange?: (payload: {

onWatch?: (event: {
type: "created" | "updated" | "removed";
path: string;
config: ResolvedConfig<T, MT>;
}) => void | Promise<void>;

acceptHMR?: (context: {
getDiff: () => ReturnType<typeof diff>;
newConfig: ResolvedConfig<T, MT>;
oldConfig: ResolvedConfig<T, MT>;
}) => void;
};
}) => void | boolean | Promise<void | boolean>;

onUpdate?: (context: {
getDiff: () => ReturnType<typeof diff>;
newConfig: ResolvedConfig<T, MT>;
oldConfig: ResolvedConfig<T, MT>;
}) => void | Promise<void>;
}

const eventMap = {
add: "created",
Expand All @@ -40,9 +52,7 @@ const eventMap = {
export async function watchConfig<
T extends UserInputConfig = UserInputConfig,
MT extends ConfigLayerMeta = ConfigLayerMeta
>(
options: LoadConfigOptions<T, MT> & WatchConfigOptions
): Promise<ConfigWatcher<T, MT>> {
>(options: WatchConfigOptions<T, MT>): Promise<ConfigWatcher<T, MT>> {
let config = await loadConfig<T, MT>(options);

const configName = options.name || "config";
Expand Down Expand Up @@ -79,10 +89,28 @@ export async function watchConfig<
if (!type) {
return;
}
if (options.onWatch) {
await options.onWatch({
type,
path,
});
}
const oldConfig = config;
config = await loadConfig(options);
if (options.onChange) {
options.onChange({ type, path, config, oldConfig });
const newConfig = await loadConfig(options);
config = newConfig;
const changeCtx = {
newConfig,
oldConfig,
getDiff: () => diff(oldConfig.config, config.config),
};
if (options.acceptHMR) {
const changeHandled = await options.acceptHMR(changeCtx);
if (changeHandled) {
return;
}
}
if (options.onUpdate) {
await options.onUpdate(changeCtx);
}
};

Expand Down
2 changes: 2 additions & 0 deletions test/fixture/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ export default {
},
configFile: true,
overriden: false,
// foo: "bar",
// x: "123",
array: ["a"],
};