wuchale 0.14.6 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,10 @@
1
- export { MixedVisitor, type MixedScope } from './mixed-visitor.js';
2
- export declare const runtimeVars: {
1
+ export { MixedVisitor } from './mixed-visitor.js';
2
+ export declare const varNames: {
3
+ rt: string;
3
4
  hmrUpdate: string;
4
5
  rtWrap: string;
5
- rtConst: string;
6
+ };
7
+ export declare function runtimeVars(wrapFunc: (expr: string) => string): {
6
8
  rtTrans: string;
7
9
  rtTPlural: string;
8
10
  rtPlural: string;
@@ -11,5 +13,6 @@ export declare const runtimeVars: {
11
13
  /** for when nesting, used in adapters with elements */
12
14
  nestCtx: string;
13
15
  };
16
+ export type RuntimeVars = ReturnType<typeof runtimeVars>;
14
17
  export declare function nonWhitespaceText(msgStr: string): [number, string, number];
15
18
  export declare function getDependencies(): Promise<Set<string>>;
@@ -1,19 +1,21 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  export { MixedVisitor } from './mixed-visitor.js';
3
- // for runtime
4
- const rtConst = '_w_runtime_';
5
- export const runtimeVars = {
3
+ export const varNames = {
4
+ rt: '_w_runtime_',
6
5
  hmrUpdate: '_w_hmrUpdate_',
7
6
  rtWrap: '_w_to_rt_',
8
- rtConst,
9
- rtTrans: `${rtConst}.t`,
10
- rtTPlural: `${rtConst}.tp`,
11
- rtPlural: `${rtConst}._.p`,
12
- rtCtx: `${rtConst}.cx`,
13
- rtTransCtx: `${rtConst}.tx`,
14
- /** for when nesting, used in adapters with elements */
15
- nestCtx: '_w_ctx_',
16
7
  };
8
+ export function runtimeVars(wrapFunc) {
9
+ return {
10
+ rtTrans: `${wrapFunc(varNames.rt)}.t`,
11
+ rtTPlural: `${wrapFunc(varNames.rt)}.tp`,
12
+ rtPlural: `${wrapFunc(varNames.rt)}._.p`,
13
+ rtCtx: `${wrapFunc(varNames.rt)}.cx`,
14
+ rtTransCtx: `${wrapFunc(varNames.rt)}.tx`,
15
+ /** for when nesting, used in adapters with elements */
16
+ nestCtx: '_w_ctx_',
17
+ };
18
+ }
17
19
  export function nonWhitespaceText(msgStr) {
18
20
  let trimmedS = msgStr.trimStart();
19
21
  const startWh = msgStr.length - trimmedS.length;
@@ -1,7 +1,9 @@
1
1
  import type MagicString from "magic-string";
2
2
  import { IndexTracker, Message, type CommentDirectives, type HeuristicDetailsBase, type HeuristicFunc } from "../adapters.js";
3
+ import { type RuntimeVars } from "./index.js";
3
4
  type NestedRanges = [number, number, boolean][];
4
5
  type InitProps<NodeT> = {
6
+ vars: () => RuntimeVars;
5
7
  mstr: MagicString;
6
8
  getRange: (node: NodeT) => {
7
9
  start: number;
@@ -1,6 +1,6 @@
1
1
  // Shared logic between adapters for handling nested / mixed elements within elements / fragments
2
2
  import { IndexTracker, Message } from "../adapters.js";
3
- import { nonWhitespaceText, runtimeVars } from "./index.js";
3
+ import { nonWhitespaceText } from "./index.js";
4
4
  export class MixedVisitor {
5
5
  constructor(props) {
6
6
  Object.assign(this, props);
@@ -149,10 +149,10 @@ export class MixedVisitor {
149
149
  let begin = '{';
150
150
  let end = ')}';
151
151
  if (props.inCompoundText) {
152
- begin += `${runtimeVars.rtTransCtx}(${runtimeVars.nestCtx}`;
152
+ begin += `${this.vars().rtTransCtx}(${this.vars().nestCtx}`;
153
153
  }
154
154
  else {
155
- begin += `${runtimeVars.rtTrans}(${this.index.get(msgInfo.toKey())}`;
155
+ begin += `${this.vars().rtTrans}(${this.index.get(msgInfo.toKey())}`;
156
156
  }
157
157
  if (iArg > 0) {
158
158
  begin += ', [';
@@ -1,5 +1,5 @@
1
1
  import type { AdapterArgs, Adapter } from "../adapters.js";
2
2
  import { Transformer } from "./transformer.js";
3
3
  export { Transformer };
4
- export { parseScript, scriptParseOptions, scriptParseOptionsWithComments, initRuntimeStmt } from './transformer.js';
4
+ export { parseScript, scriptParseOptions, scriptParseOptionsWithComments } from './transformer.js';
5
5
  export declare const adapter: (args?: AdapterArgs) => Adapter;
@@ -1,10 +1,10 @@
1
1
  // $$ cd .. && npm run test
2
2
  import { defaultGenerateLoadID, defaultHeuristicFuncOnly } from '../adapters.js';
3
3
  import { deepMergeObjects } from "../config.js";
4
- import { initRuntimeStmt, Transformer } from "./transformer.js";
4
+ import { Transformer } from "./transformer.js";
5
5
  import { getDependencies } from '../adapter-utils/index.js';
6
6
  export { Transformer };
7
- export { parseScript, scriptParseOptions, scriptParseOptionsWithComments, initRuntimeStmt } from './transformer.js';
7
+ export { parseScript, scriptParseOptions, scriptParseOptionsWithComments } from './transformer.js';
8
8
  const defaultArgs = {
9
9
  files: { include: 'src/**/*.{js,ts}', ignore: '**/*.d.ts' },
10
10
  catalog: './src/locales/{locale}',
@@ -14,11 +14,22 @@ const defaultArgs = {
14
14
  bundleLoad: false,
15
15
  generateLoadID: defaultGenerateLoadID,
16
16
  writeFiles: {},
17
+ runtime: {
18
+ useReactive: () => ({
19
+ init: false,
20
+ use: false
21
+ }),
22
+ plain: {
23
+ importName: 'default',
24
+ wrapInit: expr => expr,
25
+ wrapUse: expr => expr,
26
+ }
27
+ }
17
28
  };
18
29
  export const adapter = (args = defaultArgs) => {
19
- const { heuristic, pluralsFunc, ...rest } = deepMergeObjects(args, defaultArgs);
30
+ const { heuristic, pluralsFunc, runtime, ...rest } = deepMergeObjects(args, defaultArgs);
20
31
  return {
21
- transform: ({ content, filename, index, header }) => new Transformer(content, filename, index, heuristic, pluralsFunc, initRuntimeStmt(header.expr)).transform(header.head),
32
+ transform: ({ content, filename, index, header }) => new Transformer(content, filename, index, heuristic, pluralsFunc, header.expr, runtime).transform(header.head),
22
33
  loaderExts: ['.js', '.ts'],
23
34
  defaultLoaders: async () => {
24
35
  if (rest.bundleLoad) {
@@ -34,6 +45,7 @@ export const adapter = (args = defaultArgs) => {
34
45
  defaultLoaderPath: (loader) => {
35
46
  return new URL(`../../src/adapter-vanilla/loaders/${loader}.js`, import.meta.url).pathname;
36
47
  },
48
+ runtime,
37
49
  ...rest,
38
50
  docsUrl: 'https://wuchale.dev/adapters/vanilla'
39
51
  };
@@ -1,12 +1,12 @@
1
1
  import MagicString from "magic-string";
2
- import type Estree from 'estree';
3
- import type { Program, Options as ParserOptions } from "acorn";
2
+ import type * as Estree from "acorn";
4
3
  import { Message } from '../adapters.js';
5
- import type { CommentDirectives, HeuristicDetailsBase, HeuristicFunc, IndexTracker, ScriptDeclType, TransformOutput } from "../adapters.js";
6
- export declare const scriptParseOptions: ParserOptions;
7
- export declare function scriptParseOptionsWithComments(): [ParserOptions, Estree.Comment[][]];
8
- export declare function parseScript(content: string): [Program, Estree.Comment[][]];
9
- export declare function initRuntimeStmt(catalogExpr: string): string;
4
+ import type { CommentDirectives, HeuristicDetailsBase, HeuristicFunc, IndexTracker, ScriptDeclType, TransformOutput, RuntimeConf, CatalogExpr } from "../adapters.js";
5
+ import { type RuntimeVars } from "../adapter-utils/index.js";
6
+ export declare const scriptParseOptions: Estree.Options;
7
+ export declare function scriptParseOptionsWithComments(): [Estree.Options, Estree.Comment[][]];
8
+ export declare function parseScript(content: string): [Estree.Program, Estree.Comment[][]];
9
+ declare function initRuntimeStmt(rtConf: RuntimeConf, expr: CatalogExpr): (file: string, funcName: string, parentFunc: string, additional: object) => string;
10
10
  export declare class Transformer {
11
11
  index: IndexTracker;
12
12
  heuristic: HeuristicFunc;
@@ -15,14 +15,18 @@ export declare class Transformer {
15
15
  filename: string;
16
16
  mstr: MagicString;
17
17
  pluralFunc: string;
18
- initRuntime: string | null;
18
+ initRuntime: ReturnType<typeof initRuntimeStmt>;
19
+ vars: () => RuntimeVars;
19
20
  commentDirectives: CommentDirectives;
20
21
  insideProgram: boolean;
21
22
  declaring: ScriptDeclType;
23
+ currentFuncNested: boolean;
22
24
  currentFuncDef: string | null;
23
25
  currentCall: string;
24
26
  currentTopLevelCall: string;
25
- constructor(content: string, filename: string, index: IndexTracker, heuristic: HeuristicFunc, pluralsFunc: string, initRuntime: string | null);
27
+ /** for subclasses. right now for svelte, if in <script module> */
28
+ additionalState: object;
29
+ constructor(content: string, filename: string, index: IndexTracker, heuristic: HeuristicFunc, pluralsFunc: string, catalogExpr: CatalogExpr, rtConf: RuntimeConf);
26
30
  checkHeuristicBool: HeuristicFunc<HeuristicDetailsBase>;
27
31
  checkHeuristic: (msgStr: string, detailsBase: HeuristicDetailsBase) => [boolean, Message];
28
32
  visitLiteral: (node: Estree.Literal & {
@@ -55,6 +59,7 @@ export declare class Transformer {
55
59
  visitVariableDeclaration: (node: Estree.VariableDeclaration) => Message[];
56
60
  visitExportNamedDeclaration: (node: Estree.ExportNamedDeclaration) => Message[];
57
61
  visitExportDefaultDeclaration: (node: Estree.ExportNamedDeclaration) => Message[];
62
+ getRealBodyStart: (nodes: (Estree.Statement | Estree.ModuleDeclaration)[]) => number;
58
63
  visitFunctionBody: (node: Estree.BlockStatement | Estree.Expression, name: string | null) => Message[];
59
64
  visitFunctionDeclaration: (node: Estree.FunctionDeclaration) => Message[];
60
65
  visitArrowFunctionExpression: (node: Estree.ArrowFunctionExpression) => Message[];
@@ -65,7 +70,8 @@ export declare class Transformer {
65
70
  visitTemplateLiteral: (node: Estree.TemplateLiteral) => Message[];
66
71
  visitProgram: (node: Estree.Program) => Message[];
67
72
  processCommentDirectives: (data: string) => CommentDirectives;
68
- visit: (node: Estree.BaseNode) => Message[];
73
+ visit: (node: Estree.AnyNode) => Message[];
69
74
  finalize: (msgs: Message[], hmrHeaderIndex: number) => TransformOutput;
70
75
  transform: (headerHead: string) => TransformOutput;
71
76
  }
77
+ export {};
@@ -3,7 +3,7 @@ import MagicString from "magic-string";
3
3
  import { Parser } from 'acorn';
4
4
  import { tsPlugin } from '@sveltejs/acorn-typescript';
5
5
  import { defaultHeuristicFuncOnly, Message } from '../adapters.js';
6
- import { runtimeVars } from "../adapter-utils/index.js";
6
+ import { runtimeVars, varNames } from "../adapter-utils/index.js";
7
7
  export const scriptParseOptions = {
8
8
  sourceType: 'module',
9
9
  ecmaVersion: 'latest',
@@ -27,6 +27,8 @@ export function scriptParseOptionsWithComments() {
27
27
  accumulateComments.push({
28
28
  type: block ? 'Block' : 'Line',
29
29
  value: comment,
30
+ start: null,
31
+ end: null,
30
32
  });
31
33
  }
32
34
  },
@@ -37,8 +39,17 @@ export function parseScript(content) {
37
39
  const [opts, comments] = scriptParseOptionsWithComments();
38
40
  return [ScriptParser.parse(content, opts), comments];
39
41
  }
40
- export function initRuntimeStmt(catalogExpr) {
41
- return `const ${runtimeVars.rtConst} = ${runtimeVars.rtWrap}(${catalogExpr})`;
42
+ function initRuntimeStmt(rtConf, expr) {
43
+ return (file, funcName, parentFunc, additional) => {
44
+ const useReactive = rtConf.useReactive({ funcName, nested: parentFunc != null, file, additional });
45
+ if (useReactive.init == null) {
46
+ return;
47
+ }
48
+ const wrapInit = useReactive.init ? rtConf.reactive.wrapInit : rtConf.plain.wrapInit;
49
+ const catalogExpr = useReactive.init ? expr.reactive : expr.plain;
50
+ const runtimeExpr = `${varNames.rtWrap}(${catalogExpr})`;
51
+ return `\nconst ${varNames.rt} = ${wrapInit(runtimeExpr)}\n`;
52
+ };
42
53
  }
43
54
  export class Transformer {
44
55
  index;
@@ -49,22 +60,42 @@ export class Transformer {
49
60
  filename;
50
61
  mstr;
51
62
  pluralFunc;
52
- // null possible because subclasses may disable init inside functions
53
63
  initRuntime;
64
+ vars;
54
65
  // state
55
66
  commentDirectives = {};
56
67
  insideProgram = false;
57
68
  declaring = null;
69
+ currentFuncNested = false;
58
70
  currentFuncDef = null;
59
71
  currentCall;
60
72
  currentTopLevelCall;
61
- constructor(content, filename, index, heuristic, pluralsFunc, initRuntime) {
73
+ /** for subclasses. right now for svelte, if in <script module> */
74
+ additionalState = {};
75
+ constructor(content, filename, index, heuristic, pluralsFunc, catalogExpr, rtConf) {
62
76
  this.index = index;
63
77
  this.heuristic = heuristic;
64
78
  this.pluralFunc = pluralsFunc;
65
79
  this.content = content;
66
80
  this.filename = filename;
67
- this.initRuntime = initRuntime;
81
+ this.initRuntime = initRuntimeStmt(rtConf, catalogExpr);
82
+ const topLevelUseReactive = rtConf.useReactive({
83
+ funcName: null,
84
+ nested: false,
85
+ file: filename,
86
+ additional: this.additionalState,
87
+ });
88
+ const reactiveVars = rtConf.reactive?.wrapUse && runtimeVars(rtConf.reactive.wrapUse);
89
+ const plainVars = rtConf.plain?.wrapUse && runtimeVars(rtConf.plain.wrapUse);
90
+ this.vars = () => {
91
+ const useReactive = rtConf.useReactive({
92
+ funcName: this.currentFuncDef,
93
+ nested: this.currentFuncNested,
94
+ file: filename,
95
+ additional: this.additionalState,
96
+ }) ?? topLevelUseReactive;
97
+ return useReactive.use ? reactiveVars : plainVars;
98
+ };
68
99
  }
69
100
  checkHeuristicBool = (msgStr, detailsBase) => {
70
101
  if (!msgStr) {
@@ -105,7 +136,7 @@ export class Transformer {
105
136
  if (!pass) {
106
137
  return [];
107
138
  }
108
- this.mstr.update(start, end, `${runtimeVars.rtTrans}(${this.index.get(msgInfo.toKey())})`);
139
+ this.mstr.update(start, end, `${this.vars().rtTrans}(${this.index.get(msgInfo.toKey())})`);
109
140
  return [msgInfo];
110
141
  };
111
142
  visitArrayExpression = (node) => node.elements.map(this.visit).flat();
@@ -115,9 +146,7 @@ export class Transformer {
115
146
  visitProperty = (node) => {
116
147
  const msgs = this.visit(node.key);
117
148
  if (msgs.length && node.key.type === 'Literal' && typeof node.key.value === 'string' && !node.computed) {
118
- // @ts-expect-error
119
149
  this.mstr.appendRight(node.key.start, '[');
120
- // @ts-expect-error
121
150
  this.mstr.appendLeft(node.key.end, ']');
122
151
  }
123
152
  msgs.push(...this.visit(node.value));
@@ -158,7 +187,7 @@ export class Transformer {
158
187
  const msgInfo = new Message(candidates, 'script', this.commentDirectives.context);
159
188
  msgInfo.plural = true;
160
189
  const index = this.index.get(msgInfo.toKey());
161
- const pluralUpdate = `${runtimeVars.rtTPlural}(${index}), ${runtimeVars.rtPlural}`;
190
+ const pluralUpdate = `${this.vars().rtTPlural}(${index}), ${this.vars().rtPlural}`;
162
191
  // @ts-ignore
163
192
  this.mstr.update(secondArg.start, node.end - 1, pluralUpdate);
164
193
  return [msgInfo];
@@ -266,23 +295,34 @@ export class Transformer {
266
295
  };
267
296
  visitExportNamedDeclaration = (node) => node.declaration ? this.visit(node.declaration) : [];
268
297
  visitExportDefaultDeclaration = this.visitExportNamedDeclaration;
298
+ getRealBodyStart = (nodes) => {
299
+ for (const node of nodes) {
300
+ if (node.type === 'ExpressionStatement') {
301
+ continue;
302
+ }
303
+ return node.start;
304
+ }
305
+ return nodes[0].start;
306
+ };
269
307
  visitFunctionBody = (node, name) => {
270
308
  const prevFuncDef = this.currentFuncDef;
271
- this.currentFuncDef = node.type === 'BlockStatement' ? name : prevFuncDef;
309
+ const prevFuncNested = this.currentFuncNested;
310
+ const isBlock = node.type === 'BlockStatement';
311
+ this.currentFuncDef = isBlock ? name : prevFuncDef;
312
+ this.currentFuncNested = isBlock && name != null && prevFuncDef != null;
272
313
  const msgs = this.visit(node);
273
- let initRuntimeHere = prevFuncDef == null && this.currentFuncDef != null;
274
- if (msgs.length > 0 && initRuntimeHere && node.type === 'BlockStatement') {
275
- this.initRuntime && this.mstr.prependLeft(
276
- // @ts-expect-error
277
- node.start + 1, `\n${this.initRuntime}\n`);
314
+ if (msgs.length > 0 && isBlock) {
315
+ const initRuntime = this.initRuntime(this.filename, this.currentFuncDef, prevFuncDef, this.additionalState);
316
+ initRuntime && this.mstr.prependLeft(this.getRealBodyStart(node.body), initRuntime);
278
317
  }
318
+ this.currentFuncNested = prevFuncNested;
279
319
  this.currentFuncDef = prevFuncDef;
280
320
  return msgs;
281
321
  };
282
322
  visitFunctionDeclaration = (node) => {
283
323
  const declaring = this.declaring;
284
324
  this.declaring = 'function';
285
- const msgs = this.visitFunctionBody(node.body, node.id?.name ?? null);
325
+ const msgs = this.visitFunctionBody(node.body, node.id?.name ?? '');
286
326
  this.declaring = declaring;
287
327
  return msgs;
288
328
  };
@@ -329,7 +369,7 @@ export class Transformer {
329
369
  this.mstr.update(end, end + 2, ', ');
330
370
  }
331
371
  const msgInfo = new Message(msgStr, 'script', this.commentDirectives.context);
332
- let begin = `${runtimeVars.rtTrans}(${this.index.get(msgInfo.toKey())}`;
372
+ let begin = `${this.vars().rtTrans}(${this.index.get(msgInfo.toKey())}`;
333
373
  let end = ')';
334
374
  if (node.expressions.length) {
335
375
  begin += ', [';
@@ -370,8 +410,8 @@ export class Transformer {
370
410
  visit = (node) => {
371
411
  // for estree
372
412
  const commentDirectives = { ...this.commentDirectives };
373
- // @ts-expect-error
374
413
  const comments = this.comments[node.start];
414
+ // @ts-expect-error
375
415
  for (const comment of node.leadingComments ?? comments ?? []) {
376
416
  this.commentDirectives = this.processCommentDirectives(comment.value.trim());
377
417
  }
@@ -394,7 +434,7 @@ export class Transformer {
394
434
  return {};
395
435
  }
396
436
  if (hmrData) {
397
- this.mstr.prependRight(hmrHeaderIndex, `\nconst ${runtimeVars.hmrUpdate} = ${JSON.stringify(hmrData)}\n`);
437
+ this.mstr.prependRight(hmrHeaderIndex, `\nconst ${varNames.hmrUpdate} = ${JSON.stringify(hmrData)}\n`);
398
438
  }
399
439
  return {
400
440
  code: this.mstr.toString(),
@@ -408,7 +448,7 @@ export class Transformer {
408
448
  this.mstr = new MagicString(this.content);
409
449
  const msgs = this.visit(ast);
410
450
  if (msgs.length) {
411
- this.mstr.appendRight(0, headerHead + '\n');
451
+ this.mstr.appendRight(this.getRealBodyStart(ast.body), headerHead + '\n');
412
452
  }
413
453
  return this.finalize(msgs, 0);
414
454
  };
@@ -38,9 +38,13 @@ export type GlobConf = string | string[] | {
38
38
  include: string | string[];
39
39
  ignore: string | string[];
40
40
  };
41
+ export type CatalogExpr = {
42
+ plain: string;
43
+ reactive: string;
44
+ };
41
45
  export type TransformHeader = {
42
46
  head: string;
43
- expr: string;
47
+ expr: CatalogExpr;
44
48
  };
45
49
  type TransformCtx = {
46
50
  content: string;
@@ -61,19 +65,45 @@ export type TransformOutput = {
61
65
  msgs: Message[];
62
66
  };
63
67
  export type TransformFunc = (ctx: TransformCtx) => TransformOutput;
68
+ export type WrapFunc = (expr: string) => string;
69
+ export type UseReactiveFunc = (details: {
70
+ funcName?: string;
71
+ nested: boolean;
72
+ file: string;
73
+ additional: object;
74
+ }) => {
75
+ /** null to disable initializing */
76
+ init: boolean | null;
77
+ use: boolean;
78
+ };
79
+ type RuntimeConfDetails = {
80
+ wrapInit: WrapFunc;
81
+ wrapUse: WrapFunc;
82
+ importName: 'default' | string;
83
+ };
84
+ export type RuntimeConf = {
85
+ useReactive: UseReactiveFunc;
86
+ plain: RuntimeConfDetails;
87
+ reactive: RuntimeConfDetails;
88
+ };
89
+ export type LoaderPath = {
90
+ client: string;
91
+ ssr: string;
92
+ };
64
93
  export type AdapterPassThruOpts = {
65
94
  files: GlobConf;
66
95
  catalog: string;
67
96
  granularLoad: boolean;
68
97
  bundleLoad: boolean;
69
98
  generateLoadID: (filename: string) => string;
70
- loaderPath?: string;
99
+ loaderPath?: string | LoaderPath;
71
100
  writeFiles: {
72
101
  compiled?: boolean;
73
102
  proxy?: boolean;
74
103
  transformed?: boolean;
75
104
  outDir?: string;
76
105
  };
106
+ runtime: Partial<RuntimeConf>;
77
107
  };
78
108
  export type Adapter = AdapterPassThruOpts & {
79
109
  transform: TransformFunc;
@@ -81,7 +111,7 @@ export type Adapter = AdapterPassThruOpts & {
81
111
  loaderExts: string[];
82
112
  /** available loader names, can do auto detection logic to sort, dependencies given */
83
113
  defaultLoaders: () => string[] | Promise<string[]>;
84
- defaultLoaderPath: (loaderName: string) => string;
114
+ defaultLoaderPath: (loaderName: string) => LoaderPath | string;
85
115
  docsUrl: string;
86
116
  };
87
117
  export type AdapterArgs = Partial<AdapterPassThruOpts> & {
package/dist/cli/init.js CHANGED
@@ -19,7 +19,7 @@ export async function init(config, locales, logger) {
19
19
  const loaders = await adapter.defaultLoaders();
20
20
  let existing = false;
21
21
  if (loaderPath) {
22
- if (!empty) {
22
+ if (!Object.values(empty).some(side => side)) { // all non empty
23
23
  loaders.unshift('existing');
24
24
  existing = true;
25
25
  }
@@ -27,8 +27,7 @@ export async function init(config, locales, logger) {
27
27
  else {
28
28
  loaderPath = handler.getLoaderPaths()[0];
29
29
  }
30
- logger.log(`${existing ? 'Edit' : 'Create'} loader for ${adapterName} at ${color.cyan(loaderPath)}`);
31
- await mkdir(dirname(loaderPath), { recursive: true });
30
+ logger.log(`${existing ? 'Edit' : 'Create'} loader for ${adapterName}`);
32
31
  let loader = loaders[0];
33
32
  if (loaders.length > 1) {
34
33
  loader = await ask(loaders, `Select default loader for adapter: ${adapterName}`, logger);
@@ -37,19 +36,18 @@ export async function init(config, locales, logger) {
37
36
  logger.log('Keep existing loader');
38
37
  continue;
39
38
  }
40
- await copyFile(adapter.defaultLoaderPath(loader), loaderPath);
39
+ const defaultLoader = adapter.defaultLoaderPath(loader);
40
+ const defaultPaths = typeof defaultLoader === 'string' ? {
41
+ client: defaultLoader,
42
+ ssr: defaultLoader,
43
+ } : defaultLoader;
44
+ for (const [side, path] of Object.entries(defaultPaths)) {
45
+ await mkdir(dirname(path), { recursive: true });
46
+ await copyFile(path, loaderPath[side]);
47
+ keysByLoaderPath[path] = key;
48
+ }
41
49
  logger.log(`Initial extract for ${adapterName}`);
42
50
  await extractAdap(handler, sharedState, adapter.files, locales, false, logger);
43
- if (handler.loaderPath in keysByLoaderPath) {
44
- throw new Error([
45
- 'While catalogs can be shared, the same loader cannot be used by multiple adapters',
46
- `Conflicting: ${adapterName} and ${color.cyan(keysByLoaderPath[handler.loaderPath])}`,
47
- 'Specify a different loaderPath for one of them.'
48
- ].join('\n'));
49
- }
50
- else {
51
- keysByLoaderPath[handler.loaderPath] = key;
52
- }
53
51
  extractedNew = true;
54
52
  logger.log(`\n${adapterName}: Read more at ${color.cyan(adapter.docsUrl)}.`);
55
53
  }
@@ -36,17 +36,17 @@ export async function status(config, locales, logger) {
36
36
  const { total, obsolete, untranslated } = stats;
37
37
  const locName = getLanguageName(locale);
38
38
  logger.log([
39
- ` ${locName}: ${color.cyan(`total: ${total} `)}`,
40
- color.yellow(`untranslated: ${untranslated} `),
39
+ ` ${locName}: ${color.cyan(`total: ${total}`)}`,
40
+ color.yellow(`untranslated: ${untranslated}`),
41
41
  color.grey(`obsolete: ${obsolete}`),
42
- ].join(' '));
43
- }
44
- if (loaderPath && !empty) {
45
- logger.log(` Loader file: ${color.cyan(loaderPath)}`);
46
- continue;
42
+ ].join(', '));
47
43
  }
48
44
  if (loaderPath) {
49
- logger.warn(` Loader file empty at ${color.cyan(loaderPath)}`);
45
+ logger.log(` Loader files:`);
46
+ for (const [side, path] of Object.entries(loaderPath)) {
47
+ logger.log(` ${color.cyan(side)}: ${color.cyan(path)}${empty[side] ? color.yellow(' (empty)') : ''}`);
48
+ }
49
+ continue;
50
50
  }
51
51
  else {
52
52
  logger.warn(' No loader file found.');
package/dist/handler.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IndexTracker } from "./adapters.js";
2
- import type { Adapter, GlobConf } from "./adapters.js";
2
+ import type { Adapter, GlobConf, LoaderPath } from "./adapters.js";
3
3
  import { type CompiledElement } from "./compile.js";
4
4
  import { type ItemType } from "./gemini.js";
5
5
  import { type Matcher } from 'picomatch';
@@ -35,10 +35,13 @@ type GranularState = {
35
35
  compiled: CompiledCatalogs;
36
36
  indexTracker: IndexTracker;
37
37
  };
38
+ type LoaderPathEmpty = {
39
+ [K in keyof LoaderPath]: boolean;
40
+ };
38
41
  export declare class AdapterHandler {
39
42
  #private;
40
43
  key: string;
41
- loaderPath: string;
44
+ loaderPath: LoaderPath;
42
45
  proxyPath: string;
43
46
  outDir: string;
44
47
  compiledHead: Record<string, string>;
@@ -47,11 +50,11 @@ export declare class AdapterHandler {
47
50
  granularStateByFile: Record<string, GranularState>;
48
51
  granularStateByID: Record<string, GranularState>;
49
52
  catalogPathsToLocales: Record<string, string>;
50
- constructor(adapter: Adapter, key: string | number, config: ConfigPartial, mode: Mode, virtualPrefix: string, projectRoot: string, log: Logger);
51
- getLoaderPaths(): string[];
53
+ constructor(adapter: Adapter, key: string, config: ConfigPartial, mode: Mode, virtualPrefix: string, projectRoot: string, log: Logger);
54
+ getLoaderPaths(): LoaderPath[];
52
55
  getLoaderPath(): Promise<{
53
- path: string | null;
54
- empty: boolean;
56
+ path: LoaderPath | null;
57
+ empty: LoaderPathEmpty;
55
58
  }>;
56
59
  /** Get both catalog virtual module names AND HMR event names */
57
60
  virtModEvent: (locale: string, loadID: string | null) => string;
@@ -70,7 +73,7 @@ export declare class AdapterHandler {
70
73
  ignore: string[];
71
74
  }];
72
75
  savePoAndCompile: (loc: string) => Promise<void>;
73
- transform: (content: string, filename: string, hmrVersion?: number) => Promise<{
76
+ transform: (content: string, filename: string, hmrVersion?: number, ssr?: boolean) => Promise<{
74
77
  code?: string;
75
78
  map?: any;
76
79
  }>;
package/dist/handler.js CHANGED
@@ -10,7 +10,7 @@ import { normalize } from "node:path";
10
10
  import { getLanguageName } from "./config.js";
11
11
  import { color } from './log.js';
12
12
  import { catalogVarName } from './runtime.js';
13
- import { runtimeVars } from './adapter-utils/index.js';
13
+ import { varNames } from './adapter-utils/index.js';
14
14
  const defaultPluralRule = {
15
15
  nplurals: 2,
16
16
  plural: 'n == 1 ? 0 : 1',
@@ -96,37 +96,53 @@ export class AdapterHandler {
96
96
  }
97
97
  getLoaderPaths() {
98
98
  if (this.#adapter.loaderPath != null) {
99
+ if (typeof this.#adapter.loaderPath === 'string') {
100
+ return [{
101
+ client: this.#adapter.loaderPath,
102
+ ssr: this.#adapter.loaderPath,
103
+ }];
104
+ }
99
105
  return [this.#adapter.loaderPath];
100
106
  }
101
107
  const catalogToLoader = this.#adapter.catalog.replace('{locale}', 'loader');
102
108
  const paths = [];
103
109
  for (const ext of this.#adapter.loaderExts) {
104
- let path = catalogToLoader + ext;
110
+ let path = catalogToLoader;
105
111
  if (path.startsWith('./')) {
106
112
  path = path.slice(2);
107
113
  }
108
- paths.push(path);
114
+ const pathClient = path + ext;
115
+ paths.push({ client: pathClient, ssr: path + '.ssr' + ext }, { client: pathClient, ssr: pathClient });
109
116
  }
110
117
  return paths;
111
118
  }
112
119
  async getLoaderPath() {
120
+ const empty = { client: true, ssr: true };
113
121
  for (const path of this.getLoaderPaths()) {
114
- try {
115
- const contents = await readFile(path);
116
- return { path, empty: contents.toString().trim() === '' };
117
- }
118
- catch (err) {
119
- if (err.code !== 'ENOENT') {
120
- throw err;
122
+ let bothExist = true;
123
+ for (const side in empty) {
124
+ try {
125
+ const contents = await readFile(path[side]);
126
+ empty[side] = contents.toString().trim() === '';
127
+ }
128
+ catch (err) {
129
+ if (err.code !== 'ENOENT') {
130
+ throw err;
131
+ }
132
+ bothExist = false;
133
+ break;
121
134
  }
135
+ }
136
+ if (!bothExist) {
122
137
  continue;
123
138
  }
139
+ return { path, empty };
124
140
  }
125
- return { path: null, empty: true };
141
+ return { path: null, empty };
126
142
  }
127
143
  async #initPaths() {
128
144
  const { path: loaderPath, empty } = await this.getLoaderPath();
129
- if (!loaderPath || empty) {
145
+ if (!loaderPath || Object.values(empty).some(side => side)) {
130
146
  throw new Error('No valid loader file found.');
131
147
  }
132
148
  this.loaderPath = loaderPath;
@@ -366,7 +382,7 @@ export class AdapterHandler {
366
382
  globConfToArgs = (conf) => {
367
383
  let patterns = [];
368
384
  // ignore generated files
369
- const options = { ignore: [this.loaderPath] };
385
+ const options = { ignore: [this.loaderPath.client, this.loaderPath.ssr] };
370
386
  if (this.#adapter.writeFiles.proxy) {
371
387
  options.ignore.push(this.proxyPath);
372
388
  }
@@ -430,36 +446,67 @@ export class AdapterHandler {
430
446
  await this.compile(loc);
431
447
  }
432
448
  };
433
- #prepareHeader = (filename, loadID, hasHmr) => {
449
+ #putImportSpec = (varName, alias, importsFuncs) => {
450
+ if (!varName) {
451
+ return;
452
+ }
453
+ if (varName === 'default') {
454
+ importsFuncs.unshift(alias); // default imports are first
455
+ }
456
+ else {
457
+ importsFuncs.push(`{${varName} as ${alias}}`);
458
+ }
459
+ };
460
+ #hmrUpdateFunc = (getFuncName, getFuncNameHmr) => {
461
+ const catalogVar = '_w_catalog_';
462
+ return `
463
+ function ${getFuncName}(loadID) {
464
+ const ${catalogVar} = ${getFuncNameHmr}(loadID)
465
+ ${catalogVar}?.update?.(${varNames.hmrUpdate})
466
+ return ${catalogVar}
467
+ }
468
+ `;
469
+ };
470
+ #prepareHeader = (filename, loadID, hasHmr, ssr) => {
434
471
  let loaderRelTo = filename;
435
472
  if (this.#adapter.writeFiles.transformed) {
436
473
  loaderRelTo = resolve(this.outDir + '/' + filename);
437
474
  }
438
- let loaderPath = relative(dirname(loaderRelTo), this.loaderPath);
475
+ let loaderPath = relative(dirname(loaderRelTo), ssr ? this.loaderPath.ssr : this.loaderPath.client);
439
476
  if (!loaderPath.startsWith('.')) {
440
477
  loaderPath = `./${loaderPath}`;
441
478
  }
442
- const getFuncName = '_w_load_';
443
- let head = `import ${runtimeVars.rtWrap} from 'wuchale/runtime'\n`;
479
+ const importsFuncs = [];
480
+ const runtimeConf = this.#adapter.runtime;
481
+ let getFuncPlain = '_w_load_';
482
+ let getFuncReactive = getFuncPlain + 'rx_';
483
+ let head = [];
444
484
  if (hasHmr) {
445
- const getFuncHmr = `_w_load_hmr_`;
446
- const catalogVar = '_w_catalog_';
447
- head += `
448
- import ${getFuncHmr} from "${loaderPath}"
449
- function ${getFuncName}(loadID) {
450
- const ${catalogVar} = ${getFuncHmr}(loadID)
451
- ${catalogVar}?.update?.(${runtimeVars.hmrUpdate})
452
- return ${catalogVar}
453
- }
454
- `;
455
- }
456
- else {
457
- head += `import ${getFuncName} from "${loaderPath}"`;
485
+ const getFuncPlainHmr = getFuncPlain;
486
+ const getFuncReactiveHmr = getFuncReactive;
487
+ getFuncPlain += 'hmr_';
488
+ getFuncReactive += 'hmr_';
489
+ if (runtimeConf.plain?.importName) {
490
+ head.push(this.#hmrUpdateFunc(getFuncPlainHmr, getFuncPlain));
491
+ }
492
+ if (runtimeConf.reactive?.importName) {
493
+ head.push(this.#hmrUpdateFunc(getFuncReactiveHmr, getFuncReactive));
494
+ }
458
495
  }
496
+ this.#putImportSpec(runtimeConf.plain?.importName, getFuncPlain, importsFuncs);
497
+ this.#putImportSpec(runtimeConf.reactive?.importName, getFuncReactive, importsFuncs);
498
+ head = [
499
+ `import ${varNames.rtWrap} from 'wuchale/runtime'`,
500
+ `import ${importsFuncs.join(',')} from "${loaderPath}"`,
501
+ ...head,
502
+ ];
459
503
  if (!this.#adapter.bundleLoad) {
460
504
  return {
461
- head,
462
- expr: `${getFuncName}('${loadID}')`,
505
+ head: head.join('\n'),
506
+ expr: {
507
+ plain: `${getFuncPlain}('${loadID}')`,
508
+ reactive: `${getFuncReactive}('${loadID}')`,
509
+ }
463
510
  };
464
511
  }
465
512
  const imports = [];
@@ -472,14 +519,17 @@ export class AdapterHandler {
472
519
  const catalogsVarName = '_w_catalogs_';
473
520
  return {
474
521
  head: [
475
- head,
476
522
  ...imports,
523
+ ...head,
477
524
  `const ${catalogsVarName} = {${objElms.join(',')}}`
478
525
  ].join('\n'),
479
- expr: `${getFuncName}(${catalogsVarName})`,
526
+ expr: {
527
+ plain: `${getFuncPlain}(${catalogsVarName})`,
528
+ reactive: `${getFuncReactive}(${catalogsVarName})`,
529
+ }
480
530
  };
481
531
  };
482
- transform = async (content, filename, hmrVersion = -1) => {
532
+ transform = async (content, filename, hmrVersion = -1, ssr = false) => {
483
533
  let indexTracker = this.sharedState.indexTracker;
484
534
  let loadID = this.key;
485
535
  let compiled = this.sharedState.compiled;
@@ -493,7 +543,7 @@ export class AdapterHandler {
493
543
  content,
494
544
  filename,
495
545
  index: indexTracker,
496
- header: this.#prepareHeader(filename, loadID, hmrVersion >= 0),
546
+ header: this.#prepareHeader(filename, loadID, hmrVersion >= 0, ssr),
497
547
  });
498
548
  const hmrKeys = {};
499
549
  for (const loc of this.#locales) {
package/dist/index.d.ts CHANGED
@@ -4,4 +4,4 @@ export { AdapterHandler } from './handler.js';
4
4
  export type { Mode, SharedStates } from './handler.js';
5
5
  export { Logger } from './log.js';
6
6
  export { Message, IndexTracker, defaultGenerateLoadID, defaultHeuristic, } from './adapters.js';
7
- export type { Adapter, AdapterArgs, AdapterPassThruOpts, HeuristicFunc, TransformOutput, TransformHeader, CommentDirectives, } from './adapters.js';
7
+ export type { Adapter, AdapterArgs, AdapterPassThruOpts, RuntimeConf, CatalogExpr, HeuristicFunc, TransformOutput, TransformHeader, CommentDirectives, UseReactiveFunc, } from './adapters.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wuchale",
3
- "version": "0.14.6",
3
+ "version": "0.15.0",
4
4
  "description": "Protobuf-like i18n from plain code",
5
5
  "scripts": {
6
6
  "dev": "tsc --watch",