@wuchale/vite-plugin
Version:
Protobuf-like i18n from normal code
194 lines • 7.71 kB
JavaScript
// $$ cd ../.. && npm run test
import { relative, resolve } from "node:path";
import { getConfig as getConfig } from "wuchale/config";
import { AdapterHandler } from "wuchale/handler";
import { Logger } from "wuchale/log";
import { catalogVarName } from "wuchale/runtime";
const pluginName = 'wuchale';
const virtualPrefix = `virtual:${pluginName}/`;
const virtualResolvedPrefix = '\0';
class Plugin {
name = pluginName;
#config;
#locales = [];
#projectRoot = '';
#server;
#adapters = {};
#adaptersByLoaderPath = {};
#adaptersByCatalogPath = {};
#lastHMRTime = 0;
#log;
#configPath;
constructor(configPath) {
this.#configPath = configPath;
}
#init = async (mode) => {
this.#config = await getConfig(this.#configPath);
this.#locales = [this.#config.sourceLocale, ...this.#config.otherLocales];
this.#log = new Logger(this.#config.messages);
if (Object.keys(this.#config.adapters).length === 0) {
throw Error('At least one adapter is needed.');
}
for (const [key, adapter] of Object.entries(this.#config.adapters)) {
const handler = new AdapterHandler(adapter, key, this.#config, mode, virtualPrefix, this.#projectRoot, this.#log);
await handler.init();
this.#adapters[key] = handler;
this.#adaptersByLoaderPath[resolve(handler.loaderPath)] = handler;
for (const fname of Object.keys(handler.catalogPathsToLocales)) {
this.#adaptersByCatalogPath[fname] = handler;
}
}
};
configResolved = async (config) => {
let mode;
if (config.env.DEV) {
mode = 'dev';
}
else {
mode = 'prod';
}
this.#projectRoot = config.root;
await this.#init(mode);
};
configureServer = (server) => {
this.#server = server;
// initial load
for (const [key, adapter] of Object.entries(this.#adapters)) {
for (const loc of this.#locales) {
const event = adapter.virtModEvent(loc, null);
server.ws.on(event, (payload, client) => {
const eventSend = adapter.virtModEvent(loc, payload.loadID);
if (!this.#config.adapters[key].granularLoad) {
client.send(eventSend, adapter.compiled[loc].items);
return;
}
const compiled = adapter.granularStateByID[payload.loadID].compiled[loc];
client.send(eventSend, compiled.items);
});
}
}
};
#sendUpdateToClient = (adapter, locales) => {
if (!this.#server) {
// maybe not in dev mode
return;
}
for (const loc of locales) {
if (!this.#config.adapters[adapter.key].granularLoad) {
this.#server.ws.send(adapter.virtModEvent(loc, null), adapter.compiled[loc].items);
return;
}
for (const [loadID, state] of Object.entries(adapter.granularStateByID)) {
const eventName = adapter.virtModEvent(loc, loadID);
this.#server.ws.send(eventName, state.compiled[loc].items);
}
}
};
handleHotUpdate = async (ctx) => {
if (!(ctx.file in this.#adaptersByCatalogPath)) {
this.#lastHMRTime = performance.now();
return;
}
if (performance.now() - this.#lastHMRTime < 1000) { // too soon
return;
}
// PO file edit -> JS HMR
const adapter = this.#adaptersByCatalogPath[ctx.file];
const loc = adapter.catalogPathsToLocales[ctx.file];
await adapter.loadCatalogNCompile(loc);
this.#sendUpdateToClient(adapter, [loc]);
let invalidatedLoadIDs = [null];
if (this.#config.adapters[adapter.key].granularLoad) {
invalidatedLoadIDs = Object.keys(adapter.granularStateByID);
}
const allModules = [];
const invalidatedModules = new Set();
for (const loadID of invalidatedLoadIDs) {
const moduleID = `${virtualResolvedPrefix}${adapter.virtModEvent(loc, loadID)}`;
const modules = ctx.server.moduleGraph.getModulesByFile(moduleID);
for (const module of modules) {
ctx.server.moduleGraph.invalidateModule(module, invalidatedModules, ctx.timestamp, true);
allModules.push(module);
}
}
return allModules;
};
resolveId = (source, importer) => {
if (!source.startsWith(virtualPrefix)) {
return null;
}
return `${virtualResolvedPrefix}${source}?importer=${importer}`;
};
load = (id) => {
const prefix = virtualResolvedPrefix + virtualPrefix;
if (!id.startsWith(prefix)) {
return null;
}
const [path, importer] = id.slice(prefix.length).split('?importer=');
const [part, ...rest] = path.split('/');
if (part === 'catalog') {
const [adapterKey, loadID, locale] = rest;
const adapter = this.#adapters[adapterKey];
if (adapter == null) {
this.#log.error(`Adapter not found for key: ${adapterKey}`);
return null;
}
const module = adapter.loadDataModule(locale, loadID);
if (this.#server == null) { // prod build
return module;
}
const eventSend = adapter.virtModEvent(locale, loadID);
const eventReceive = adapter.virtModEvent(locale, null);
return `
${module}
if (import.meta.hot) {
import.meta.hot.on('${eventSend}', newData => {
for (let i = 0; i < newData.length; i++) {
if (JSON.stringify(${catalogVarName}[i]) !== JSON.stringify(newData[i])) {
${catalogVarName}[i] = newData[i]
}
}
})
import.meta.hot.send('${eventReceive}'${loadID == null ? '' : `, {loadID: '${loadID}'}`})
}
`;
}
if (part === 'locales') {
return `export const locales = ['${this.#locales.join("', '")}']`;
}
if (part !== 'proxy') {
this.#log.error(`Unknown virtual request: ${id}`);
return null;
}
// loader proxy
const adapter = this.#adaptersByLoaderPath[importer];
if (adapter == null) {
console.log(id);
this.#log.error(`Adapter not found for filename: ${importer}`);
return;
}
if (rest[0] === 'sync') {
return adapter.getProxySync();
}
return adapter.getProxy();
};
#transformHandler = async (code, id) => {
if (!this.#config.hmr) {
return {};
}
const filename = relative(this.#projectRoot, id);
for (const adapter of Object.values(this.#adapters)) {
if (adapter.fileMatches(filename)) {
const { catalogChanged, ...output } = await adapter.transform(code, filename);
if (catalogChanged && this.#lastHMRTime > 0) {
this.#sendUpdateToClient(adapter, this.#locales);
}
return output;
}
}
return {};
};
transform = { order: 'pre', handler: this.#transformHandler };
}
export const wuchale = (configPath) => new Plugin(configPath);
//# sourceMappingURL=index.js.map