ferrovue 0.3.0 → 0.4.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.
Files changed (85) hide show
  1. package/README.md +32 -13
  2. package/dist/attrs.d.ts +2 -52
  3. package/dist/attrs.js +18 -448
  4. package/dist/calls.d.ts +4 -0
  5. package/dist/calls.js +219 -0
  6. package/dist/children.d.ts +9 -0
  7. package/dist/children.js +273 -0
  8. package/dist/classes.d.ts +13 -0
  9. package/dist/classes.js +176 -0
  10. package/dist/cli.js +0 -17
  11. package/dist/client.js +0 -12
  12. package/dist/compiler.js +25 -90
  13. package/dist/component.js +10 -86
  14. package/dist/context.d.ts +4 -66
  15. package/dist/context.js +2 -33
  16. package/dist/diff.d.ts +0 -3
  17. package/dist/diff.js +0 -9
  18. package/dist/emitter.d.ts +0 -8
  19. package/dist/emitter.js +0 -12
  20. package/dist/expr.d.ts +0 -70
  21. package/dist/expr.js +27 -890
  22. package/dist/fallthrough.d.ts +9 -0
  23. package/dist/fallthrough.js +78 -0
  24. package/dist/fixture.d.ts +2 -1
  25. package/dist/fixture.js +37 -22
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +1 -0
  28. package/dist/islands.d.ts +1 -0
  29. package/dist/islands.js +1 -0
  30. package/dist/lists.d.ts +0 -11
  31. package/dist/lists.js +4 -39
  32. package/dist/loops.d.ts +3 -0
  33. package/dist/loops.js +164 -0
  34. package/dist/model.d.ts +14 -143
  35. package/dist/model.js +27 -12
  36. package/dist/narrowing.d.ts +19 -0
  37. package/dist/narrowing.js +126 -0
  38. package/dist/numbers.d.ts +8 -0
  39. package/dist/numbers.js +73 -0
  40. package/dist/parens.d.ts +0 -27
  41. package/dist/parens.js +0 -44
  42. package/dist/plugin.d.ts +84 -0
  43. package/dist/plugin.js +26 -0
  44. package/dist/plugins/i18n.d.ts +23 -0
  45. package/dist/{i18n.js → plugins/i18n.js} +76 -19
  46. package/dist/plugins/index.d.ts +2 -0
  47. package/dist/plugins/index.js +8 -0
  48. package/dist/plugins/router-link.d.ts +8 -0
  49. package/dist/{router.js → plugins/router-link.js} +19 -61
  50. package/dist/plugins/router.d.ts +42 -0
  51. package/dist/plugins/router.js +256 -0
  52. package/dist/plugins/scoped.d.ts +12 -0
  53. package/dist/plugins/scoped.js +125 -0
  54. package/dist/plugins/shared-types.d.ts +2 -0
  55. package/dist/plugins/shared-types.js +7 -0
  56. package/dist/plugins/stores.d.ts +37 -0
  57. package/dist/plugins/stores.js +377 -0
  58. package/dist/plugins/teleport.d.ts +5 -0
  59. package/dist/plugins/teleport.js +55 -0
  60. package/dist/rust.d.ts +1 -7
  61. package/dist/rust.js +38 -160
  62. package/dist/script.d.ts +0 -6
  63. package/dist/script.js +13 -128
  64. package/dist/slots.d.ts +13 -0
  65. package/dist/slots.js +160 -0
  66. package/dist/strings.d.ts +13 -0
  67. package/dist/strings.js +208 -0
  68. package/dist/styles.d.ts +14 -0
  69. package/dist/styles.js +181 -0
  70. package/dist/template.d.ts +1 -42
  71. package/dist/template.js +14 -738
  72. package/dist/testing.d.ts +1 -0
  73. package/dist/testing.js +1 -8
  74. package/dist/types.d.ts +2 -0
  75. package/dist/typescript.d.ts +0 -13
  76. package/dist/typescript.js +8 -41
  77. package/dist/vite.d.ts +1 -0
  78. package/dist/vite.js +1 -23
  79. package/package.json +7 -2
  80. package/dist/i18n.d.ts +0 -27
  81. package/dist/router.d.ts +0 -17
  82. package/dist/scoped.d.ts +0 -24
  83. package/dist/scoped.js +0 -177
  84. package/dist/stores.d.ts +0 -6
  85. package/dist/stores.js +0 -229
package/dist/script.js CHANGED
@@ -1,27 +1,22 @@
1
- /* `<script setup>` read: what the server evaluates, and what stays on the client. */
2
1
  import { basename } from "node:path";
3
- import { fail, GenError, snake, STR, takesAttrs } from "./model.js";
2
+ import { fail, GenError, snake, takesAttrs } from "./model.js";
4
3
  import { CONFIG_FILE, ctx } from "./context.js";
5
4
  import { definePropsType } from "./typescript.js";
6
- import { expr, fieldVal, storeGetter, theRoute } from "./expr.js";
5
+ import { expr, fieldVal } from "./expr.js";
7
6
  import { collected, heldList } from "./lists.js";
8
- import { storeImport } from "./stores.js";
9
7
  import { bare, operand, UNARY } from "./parens.js";
10
- /** Lifecycle hooks, which never run on the server: setup may register them freely. */
8
+ import { known } from "./narrowing.js";
11
9
  export const CLIENT_HOOKS = new Set([
12
10
  "onMounted", "onBeforeMount", "onUnmounted", "onBeforeUnmount", "onUpdated", "onBeforeUpdate",
13
11
  "onActivated", "onDeactivated", "onErrorCaptured", "onRenderTracked", "onRenderTriggered",
14
12
  ]);
15
- /** Compiler macros and calls with no effect on the server's render. */
16
13
  export const INERT_CALLS = new Set(["defineEmits", "defineSlots", "defineOptions", "defineExpose", "defineProps", "withDefaults", "provide"]);
17
- /** A statement in setup that is a call on its own: allowed only when it cannot change the render. */
18
14
  export function setupStatement(comp, st) {
19
15
  const c = st.expression;
20
16
  const name = c?.type === "CallExpression" && c.callee.type === "Identifier" ? c.callee.name : null;
21
17
  if (name !== null && (CLIENT_HOOKS.has(name) || INERT_CALLS.has(name)))
22
18
  return;
23
19
  if (name === "watch") {
24
- // A watcher's callback runs on the server only with `immediate`, and could then change state.
25
20
  const options = c.arguments[2];
26
21
  const immediate = options?.type === "ObjectExpression" && options.properties.some((p) => p.type === "ObjectProperty" && (p.key.name ?? p.key.value) === "immediate" && !(p.value.type === "BooleanLiteral" && !p.value.value));
27
22
  if (!immediate)
@@ -35,8 +30,6 @@ export function setupStatement(comp, st) {
35
30
  fail(comp, "`onServerPrefetch` fetches on the server; pass the data in as props instead", st);
36
31
  fail(comp, `\`${st.expression?.type === "CallExpression" ? `${name ?? "a call"}()` : st.expression?.type}\` in setup could change what renders, and is not translated`, st);
37
32
  }
38
- /** What a setup binding's value is computed from: `ref(x)` and `shallowRef(x)` hold `x`,
39
- * `computed(() => x)` is `x`, and a plain `const y = x` is `x`. */
40
33
  export function setupSource(init) {
41
34
  if (init.type === "CallExpression" && init.callee.type === "Identifier") {
42
35
  const name = init.callee.name;
@@ -56,7 +49,6 @@ export function setupSource(init) {
56
49
  return setupSource(init.expression);
57
50
  return init;
58
51
  }
59
- /** The names a destructuring pattern binds. */
60
52
  export function patternNames(p) {
61
53
  switch (p?.type) {
62
54
  case "Identifier":
@@ -87,25 +79,20 @@ export function scopeFor(comp, ast, components) {
87
79
  narrowed: new Map(),
88
80
  selfAlias: { name: null },
89
81
  helperBytes: { n: 0 },
90
- router: new Map(),
91
82
  directives: new Map(),
92
- i18nT: new Set(),
93
83
  fill: false,
94
84
  vnode: false,
95
- // A component a parent may pass attributes takes them with the scope ids, as one `fv::Attrs`.
96
85
  attrs: comp.inherits ? (takesAttrs(comp) ? "fv_attrs.ids()" : "fv_attrs") : null,
97
86
  fallthrough: takesAttrs(comp) ? "fv_attrs" : null,
98
87
  attrsBindings: new Set(),
99
88
  sid: null,
89
+ plugins: new Map(),
100
90
  };
91
+ for (const p of ctx.plugins)
92
+ if (p.scope)
93
+ scope.plugins.set(p, p.scope(scope));
101
94
  const lets = [];
102
- const storeHooks = new Map();
103
- let storeToRefsName = null;
104
- let useRouteName = null;
105
- let useI18nName = null;
106
95
  let useAttrsName = null;
107
- /** Setup bindings holding a store, by name. */
108
- const storeValues = new Map();
109
96
  for (const st of ast) {
110
97
  if (st.type === "ImportDeclaration") {
111
98
  const from = st.source.value;
@@ -113,54 +100,16 @@ export function scopeFor(comp, ast, components) {
113
100
  const def = st.specifiers.find((x) => x.type === "ImportDefaultSpecifier");
114
101
  if (def)
115
102
  scope.children.set(def.local.name, basename(from, ".vue"));
103
+ continue;
116
104
  }
117
- else if (storeImport(comp, from)) {
118
- for (const sp of st.specifiers) {
119
- const hook = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
120
- // A type from the store's file, which `readComponent` has already resolved.
121
- if (st.importKind === "type" || sp.importKind === "type" || (hook !== null && ctx.storeStructs.has(hook)))
122
- continue;
123
- const store = hook ? ctx.stores.get(hook) : undefined;
124
- if (!store || store.module !== storeImport(comp, from))
125
- fail(comp, "import a store by its `use…` hook", sp);
126
- storeHooks.set(sp.local.name, store);
127
- }
128
- }
129
- else if (from === "vue-router") {
130
- for (const sp of st.specifiers) {
131
- const name = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
132
- if (name === "useRoute")
133
- useRouteName = sp.local.name;
134
- else if (name === "RouterLink" || name === "RouterView")
135
- scope.router.set(sp.local.name, name);
136
- else
137
- scope.clientOnly.set(sp.local.name, `\`${name}\` from vue-router does not run on the server`);
138
- }
139
- }
140
- else if (from === "vue-i18n") {
141
- for (const sp of st.specifiers) {
142
- const name = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
143
- if (name === "useI18n")
144
- useI18nName = sp.local.name;
145
- else
146
- scope.clientOnly.set(sp.local.name, `\`${name}\` from vue-i18n does not run on the server`);
147
- }
148
- }
149
- else if (from === "vue") {
105
+ if (ctx.plugins.some((p) => p.scriptImport?.(scope, st, from)))
106
+ continue;
107
+ if (from === "vue") {
150
108
  for (const sp of st.specifiers) {
151
109
  if (sp.type === "ImportSpecifier" && (sp.imported.name ?? sp.imported.value) === "useAttrs")
152
110
  useAttrsName = sp.local.name;
153
111
  }
154
112
  }
155
- else if (from === "pinia") {
156
- for (const sp of st.specifiers) {
157
- const name = sp.type === "ImportSpecifier" ? (sp.imported.name ?? sp.imported.value) : null;
158
- if (name === "storeToRefs")
159
- storeToRefsName = sp.local.name;
160
- else
161
- scope.clientOnly.set(sp.local.name, `\`${name}\` from Pinia does not run on the server`);
162
- }
163
- }
164
113
  else if (ctx.helperModule !== null && from === ctx.helperModule) {
165
114
  for (const sp of st.specifiers) {
166
115
  if (!ctx.helpers[sp.imported.name])
@@ -182,14 +131,10 @@ export function scopeFor(comp, ast, components) {
182
131
  setupStatement(comp, st);
183
132
  continue;
184
133
  }
185
- // Setup runs on the server too, so a statement there can change what renders: `x.value = ...`
186
- // after a `ref`, an `if`, a loop. None of it is translated, so none of it may exist.
187
134
  if (st.type !== "VariableDeclaration")
188
135
  fail(comp, `\`${st.type}\` in setup is not supported`, st);
189
136
  for (const d of st.declarations) {
190
137
  const init0 = d.init;
191
- // `const { size = "md", label: l } = defineProps<...>()`: each name reads that prop, whose
192
- // default `compileScript` already resolved.
193
138
  if (d.id.type === "ObjectPattern" && definePropsType(init0)) {
194
139
  for (const p of d.id.properties) {
195
140
  if (p.type !== "ObjectProperty" || p.computed)
@@ -202,73 +147,19 @@ export function scopeFor(comp, ast, components) {
202
147
  }
203
148
  continue;
204
149
  }
205
- const calls = (name) => name !== null && init0?.type === "CallExpression" && init0.callee.type === "Identifier" && init0.callee.name === name;
206
- // `const { t, locale } = useI18n()`: \`t\` translates, \`locale\` is the request's locale.
207
- if (d.id.type === "ObjectPattern" && calls(useI18nName)) {
208
- for (const p of d.id.properties) {
209
- if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
210
- fail(comp, "`useI18n()` is destructured into plain names", p);
211
- }
212
- const key = p.key.name ?? p.key.value;
213
- if (key === "t")
214
- scope.i18nT.add(p.value.name);
215
- else if (key === "locale") {
216
- scope.setup.set(p.value.name, { code: "fv_i18n.locale()", ty: STR });
217
- scope.refs.add(p.value.name);
218
- }
219
- else
220
- scope.clientOnly.set(p.value.name, `\`${key}\` from \`useI18n()\` does not run on the server`);
221
- }
222
- comp.readsI18n = true;
150
+ if (ctx.plugins.some((p) => p.scriptBinding?.(scope, d)))
223
151
  continue;
224
- }
225
- // `const { density, label: l } = storeToRefs(prefs)`: each name is that field of the state.
226
- if (d.id.type === "ObjectPattern" && calls(storeToRefsName)) {
227
- const arg = init0.arguments[0];
228
- const store = arg?.type === "Identifier" ? storeValues.get(arg.name) : undefined;
229
- if (!store || init0.arguments.length !== 1)
230
- fail(comp, "`storeToRefs` takes a store bound in this setup", d);
231
- for (const p of d.id.properties) {
232
- if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
233
- fail(comp, "`storeToRefs` is destructured into plain names", p);
234
- }
235
- const key = p.key.type === "Identifier" ? p.key.name : p.key.value;
236
- const state = { code: `fv_stores.${store.field}`, ty: { k: "struct", name: store.state, store: true } };
237
- scope.setup.set(p.value.name, storeGetter(scope, state, key, p) ?? fieldVal(comp, state.code, state.ty, key, p));
238
- scope.refs.add(p.value.name);
239
- }
240
- comp.readsStores = true;
241
- continue;
242
- }
243
152
  if (d.id.type !== "Identifier") {
244
- // Destructuring anything else: client-side state the template may not read.
245
153
  for (const name of patternNames(d.id))
246
154
  scope.clientOnly.set(name, "it is destructured from a value the server does not have");
247
155
  continue;
248
156
  }
249
157
  const local = d.id.name;
250
- // `const prefs = usePrefs()`: the store's state, as the server was given it.
251
- const hook = init0?.type === "CallExpression" && init0.callee.type === "Identifier" ? storeHooks.get(init0.callee.name) : undefined;
252
- if (hook) {
253
- if (init0.arguments.length)
254
- fail(comp, "a store hook takes no arguments", d);
255
- storeValues.set(local, hook);
256
- scope.setup.set(local, { code: `fv_stores.${hook.field}`, ty: { k: "struct", name: hook.state, store: true } });
257
- comp.readsStores = true;
258
- continue;
259
- }
260
- // `const route = useRoute()`: the reader's location.
261
- if (useRouteName !== null && init0?.type === "CallExpression" && init0.callee.type === "Identifier" && init0.callee.name === useRouteName) {
262
- scope.setup.set(local, theRoute(scope, d));
263
- continue;
264
- }
265
- // `const attrs = useAttrs()`: `$attrs`, which the template may bind whole.
266
158
  if (useAttrsName !== null && init0?.type === "CallExpression" && init0.callee.type === "Identifier" && init0.callee.name === useAttrsName) {
267
159
  scope.attrsBindings.add(local);
268
160
  scope.clientOnly.set(local, "`useAttrs()` is bound whole, with `v-bind`; a value read from it has no type");
269
161
  continue;
270
162
  }
271
- // `const model = defineModel<string>()`: the model's prop.
272
163
  const model = comp.models.get(local);
273
164
  if (model !== undefined) {
274
165
  scope.setup.set(local, fieldVal(comp, "props", { k: "struct", name: "Props" }, model, d));
@@ -291,9 +182,6 @@ export function scopeFor(comp, ast, components) {
291
182
  scope.clientOnly.set(local, "it is not a value the server computes: only `ref(…)`, `computed(() => …)` and plain expressions are");
292
183
  continue;
293
184
  }
294
- // Evaluated once, as setup is on the server. A value the server cannot evaluate is client-side
295
- // state — a template ref, an element, a `reactive` object — which the template may still name
296
- // from an event handler; a reference the server needs fails in `expr` with this reason.
297
185
  let v;
298
186
  try {
299
187
  v = expr(scope, source);
@@ -304,25 +192,22 @@ export function scopeFor(comp, ast, components) {
304
192
  scope.clientOnly.set(local, e.message.replace(/^[^:]*: /, ""));
305
193
  continue;
306
194
  }
307
- if (v.ty.k === "undef") {
195
+ if (v.ty.k === "undef" || (v.ty.k === "str" && known(v) !== undefined)) {
308
196
  scope.setup.set(local, v);
309
197
  continue;
310
198
  }
311
199
  const name = `s_${snake(local).replace(/^r#/, "")}`;
312
200
  const lone = v.lone ? { lone: true } : {};
313
- // A computed list is kept as its items, and read again by item.
314
201
  if (v.ty.k === "list" && v.iter !== undefined) {
315
202
  lets.push(`let ${name} = ${collected(v)};`);
316
203
  scope.setup.set(local, heldList(name, v.ty.of, v.lone));
317
204
  continue;
318
205
  }
319
- // An optional string a temporary owns is kept as that `Option<Cow>`.
320
206
  if (v.held !== undefined) {
321
207
  lets.push(`let ${name} = ${v.held};`);
322
208
  scope.setup.set(local, { code: `${name}.as_deref()`, ty: v.ty, ...lone });
323
209
  continue;
324
210
  }
325
- // A list, a record or an object is a place in the props: borrowed, never moved out of them.
326
211
  const place = v.ty.k === "list" || v.ty.k === "record" || v.ty.k === "struct" || v.ty.k === "child";
327
212
  lets.push(`let ${name} = ${place ? `&${operand(v.code, UNARY)}` : bare(v.code)};`);
328
213
  scope.setup.set(local, { code: name, ty: v.ty, ...lone, ...(v.konst !== undefined ? { konst: v.konst } : {}) });
@@ -0,0 +1,13 @@
1
+ import { type Component, type N, type Scope, type Ty, type Val } from "./model.ts";
2
+ import { Emitter } from "./emitter.ts";
3
+ export declare function slotBody(s: Scope, value: N): N[];
4
+ export declare function slotContent(s: Scope, value: N): {
5
+ body: N[];
6
+ param: N;
7
+ };
8
+ export declare function staticallyFilled(body: N[]): boolean;
9
+ export declare function slotTypeName(slotName: string, suffix: string): string;
10
+ export declare function slotFieldBorrows(ty: Ty): boolean;
11
+ export declare function slotFieldTy(ty: Ty, comp: Component): string;
12
+ export declare function slotFieldValue(s: Scope, v: Val, n: N): string;
13
+ export declare function slotOutlet(s: Scope, e: Emitter, c: N): void;
package/dist/slots.js ADDED
@@ -0,0 +1,160 @@
1
+ import { fail, GenError, rustStr, snake } from "./model.js";
2
+ import { expr } from "./expr.js";
3
+ import { atom, bare, operand, strArg, UNARY } from "./parens.js";
4
+ import { Emitter } from "./emitter.js";
5
+ import { rustTy } from "./rust.js";
6
+ import { fieldInit } from "./children.js";
7
+ import { pushesContent, statements } from "./template.js";
8
+ function borrowed(code) {
9
+ return code.startsWith("&") || /^\w+$/.test(code) || /^fv_sp\d+\.\w+$/.test(code) ? code : `&${operand(code, UNARY)}`;
10
+ }
11
+ export function slotBody(s, value) {
12
+ return slotContent(s, value).body;
13
+ }
14
+ export function slotContent(s, value) {
15
+ const fn = value.type === "CallExpression" && value.callee.type === "Identifier" && value.callee.name === "_withCtx"
16
+ ? value.arguments[0]
17
+ : null;
18
+ if (fn?.type !== "ArrowFunctionExpression" || fn.body.type !== "BlockStatement") {
19
+ fail(s.comp, "unexpected slot content", value);
20
+ }
21
+ const branch = fn.body.body.length === 1 ? fn.body.body[0] : null;
22
+ if (branch?.type !== "IfStatement" || branch.test.type !== "Identifier" || branch.test.name !== "_push") {
23
+ fail(s.comp, "unexpected slot content", fn);
24
+ }
25
+ const body = branch.consequent.type === "BlockStatement" ? branch.consequent.body : [branch.consequent];
26
+ return { body, param: fn.params[0] };
27
+ }
28
+ export function staticallyFilled(body) {
29
+ return body.some((st) => st.type === "ExpressionStatement" && st.expression.type === "CallExpression" &&
30
+ st.expression.callee.type === "Identifier" && st.expression.callee.name === "_push" &&
31
+ pushesContent(st.expression.arguments[0]) === true);
32
+ }
33
+ export function slotTypeName(slotName, suffix) {
34
+ return slotName.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") + suffix;
35
+ }
36
+ export function slotFieldBorrows(ty) {
37
+ const copied = (t) => t.k === "int" || t.k === "float" || t.k === "bool";
38
+ return !(copied(ty) || (ty.k === "opt" && copied(ty.of)));
39
+ }
40
+ export function slotFieldTy(ty, comp) {
41
+ const owned = (t) => rustTy(t, comp).replace(/'a\b/g, "'v");
42
+ switch (ty.k) {
43
+ case "int":
44
+ return "i64";
45
+ case "float":
46
+ return "f64";
47
+ case "bool":
48
+ return "bool";
49
+ case "str":
50
+ return "&'v str";
51
+ case "list":
52
+ return `&'v [${owned(ty.of)}]`;
53
+ case "struct":
54
+ case "child":
55
+ case "html":
56
+ return `&'v ${owned(ty)}`;
57
+ case "opt":
58
+ return `Option<${slotFieldTy(ty.of, comp)}>`;
59
+ default:
60
+ throw new GenError(`no Rust type for a slot prop of type ${JSON.stringify(ty)}`);
61
+ }
62
+ }
63
+ export function slotFieldValue(s, v, n) {
64
+ switch (v.ty.k) {
65
+ case "str":
66
+ return strArg(v.code);
67
+ case "int":
68
+ case "float":
69
+ case "bool":
70
+ return bare(v.code);
71
+ case "list":
72
+ if (v.code.startsWith("["))
73
+ fail(s.comp, "a slot prop is not an array literal: pass a list the component holds", n);
74
+ if (v.iter !== undefined)
75
+ fail(s.comp, "a slot prop is not a computed list: pass a list the component holds", n);
76
+ return borrowed(v.code);
77
+ case "struct":
78
+ case "child":
79
+ case "html":
80
+ return borrowed(v.code);
81
+ case "opt":
82
+ if (v.ty.of.k === "list")
83
+ return `${atom(v.code)}.map(|v| &v[..])`;
84
+ if (v.ty.of.k === "opt" || v.ty.of.k === "undef")
85
+ break;
86
+ return v.code;
87
+ }
88
+ return fail(s.comp, "a slot prop is a string, a number, a boolean, an object or a list", n);
89
+ }
90
+ export function slotOutlet(s, e, c) {
91
+ const [, nameNode, slotProps, fallback] = c.arguments;
92
+ if (nameNode?.type !== "StringLiteral")
93
+ fail(s.comp, "a slot's name is literal", c);
94
+ if (slotProps?.type !== "ObjectExpression")
95
+ fail(s.comp, "a slot's props are attributes or a `v-bind` object literal", c);
96
+ const slotName = nameNode.value;
97
+ const field = `fv_slots.${snake(slotName)}`;
98
+ const outlet = `\`<slot${slotName === "default" ? "" : ` name="${slotName}"`}>\``;
99
+ let fn = "fv::slot_into";
100
+ let args = field;
101
+ if (slotProps.properties.length) {
102
+ const fields = [];
103
+ const values = [];
104
+ for (const p of slotProps.properties) {
105
+ if (p.type !== "ObjectProperty" || p.computed)
106
+ fail(s.comp, "slot props hold plain names", p);
107
+ const key = p.key.name ?? p.key.value;
108
+ if (!/^[A-Za-z_$][\w$]*$/.test(key))
109
+ fail(s.comp, `slot prop \`${key}\` is not a plain name; write it in camelCase`, p);
110
+ const v = expr(s, p.value);
111
+ fields.push({ js: key, rust: snake(key), ty: v.ty });
112
+ values.push(fieldInit(snake(key), slotFieldValue(s, v, p.value)));
113
+ }
114
+ const name = slotTypeName(slotName, "SlotProps");
115
+ const shape = { name, fields, slot: true };
116
+ const had = s.comp.slotShapes.get(slotName);
117
+ if (had && JSON.stringify(had.fields) !== JSON.stringify(fields)) {
118
+ fail(s.comp, `every ${outlet} passes the same props, of the same types`, c);
119
+ }
120
+ if (!had) {
121
+ if (s.comp.structs.has(name) && !s.comp.structs.get(name).slot)
122
+ fail(s.comp, `\`${name}\` names this slot's props; rename the interface`, c);
123
+ s.comp.slotShapes.set(slotName, shape);
124
+ s.comp.structs.set(name, shape);
125
+ }
126
+ fn = "fv::scoped_slot_into";
127
+ args = `${field}, &${name} { ${values.join(", ")} }`;
128
+ }
129
+ else if (s.comp.slotShapes.has(slotName)) {
130
+ fail(s.comp, `every ${outlet} passes the same props, of the same types`, c);
131
+ }
132
+ const id = c.arguments[6];
133
+ let slotted = null;
134
+ if (id?.type === "StringLiteral")
135
+ slotted = rustStr(id.value);
136
+ else if (id?.type === "BinaryExpression" && id.operator === "+" && id.left.type === "StringLiteral" && id.right.type === "Identifier" && id.right.name === "_scopeId") {
137
+ slotted = s.sid === null ? rustStr(id.left.value) : `&[${rustStr(id.left.value)}, ${s.sid}].concat()`;
138
+ }
139
+ else if (id?.type === "Identifier" && id.name === "_scopeId")
140
+ slotted = s.sid;
141
+ else if (id && id.type !== "NullLiteral")
142
+ fail(s.comp, "unexpected slot scope id", id);
143
+ if (s.comp.passesSlotIds) {
144
+ fn = fn === "fv::slot_into" ? "fv::slot_into_slotted" : "fv::scoped_slot_into_slotted";
145
+ args += `, ${slotted ?? '""'}`;
146
+ }
147
+ else if (slotted !== null)
148
+ fail(s.comp, "a slot scope id passed to content that does not take one", c);
149
+ const call = (rest) => (s.fill ? `if ${fn}(out, ${args}, ${rest}) { filled = true; }` : `${fn}(out, ${args}, ${rest});`);
150
+ if (fallback?.type === "NullLiteral" || !fallback) {
151
+ e.stmt(call("None"));
152
+ return;
153
+ }
154
+ if (fallback.type !== "ArrowFunctionExpression" || fallback.body.type !== "BlockStatement") {
155
+ fail(s.comp, "unexpected slot fallback", fallback);
156
+ }
157
+ e.open(`${s.fill ? "if " : ""}${fn}(out, ${args}, Some(&mut |out: &mut String|`);
158
+ statements(s, e, fallback.body.body);
159
+ e.close(s.fill ? ")) { filled = true; }" : "));");
160
+ }
@@ -0,0 +1,13 @@
1
+ import { type Component, type N, type Scope, type Val } from "./model.ts";
2
+ export declare function lonely(what: string): string;
3
+ export declare function meet(comp: Component, a: Val, b: Val, what: string, n: N, how: "equal" | "order" | "search" | "join"): void;
4
+ export declare function loneOf(...vs: (Val | undefined)[]): {
5
+ lone?: true;
6
+ };
7
+ export declare function isTemporary(v: Val): boolean;
8
+ export declare function asCow(v: Val): string;
9
+ export declare function yieldsCow(code: string): boolean;
10
+ export declare function unquote(literal: string): string;
11
+ export declare function stringsEqual(a: Val, b: Val): string;
12
+ export declare function formatted(comp: Component, parts: (string | Val)[], n: N, what: string): Val;
13
+ export declare function stringMethod(s: Scope, target: Val, method: string, args: N[], n: N): Val | null;
@@ -0,0 +1,208 @@
1
+ import { BOOL, fail, INT, opt, rustStr, STR } from "./model.js";
2
+ import { atom, bare, binary, condition, enclosed, receiver, strArg } from "./parens.js";
3
+ import { computed } from "./lists.js";
4
+ import { expr } from "./expr.js";
5
+ import { known } from "./narrowing.js";
6
+ import { asF64, isNumber } from "./numbers.js";
7
+ export function lonely(what) {
8
+ return `${what} with a string that may hold half of a surrogate pair (cut by \`slice\`, \`substring\`, \`at\`, \`charAt\`, \`split("")\` and the like): JavaScript keeps the half, which can match, order or join with another where ferrovue holds U+FFFD`;
9
+ }
10
+ export function meet(comp, a, b, what, n, how) {
11
+ const literal = (v) => v.code.startsWith('"');
12
+ const fffd = (v) => literal(v) && v.code.includes("\u{FFFD}");
13
+ const low = (v) => literal(v) && !/[\u{D800}-\u{10FFFF}]/u.test(v.code);
14
+ const refused = how === "order" ? (a.lone && !low(b)) || (b.lone && !low(a))
15
+ : how === "join" ? a.lone && b.lone
16
+ : how === "search" ? b.lone || (a.lone && fffd(b))
17
+ : (a.lone && b.lone) || (a.lone && fffd(b)) || (b.lone && fffd(a));
18
+ if (refused)
19
+ fail(comp, lonely(what), n);
20
+ }
21
+ export function loneOf(...vs) {
22
+ return vs.some((v) => v?.lone) ? { lone: true } : {};
23
+ }
24
+ function nonEmptyLiteral(v) {
25
+ return /^"[^"]/.test(v.code);
26
+ }
27
+ export function isTemporary(v) {
28
+ if (v.ty.k !== "str")
29
+ return false;
30
+ const trimmed = /^fv::js_trim(?:_start|_end)?\(([^]*)\)$/.exec(v.code);
31
+ if (trimmed?.[1] !== undefined)
32
+ return isTemporary({ code: trimmed[1].replace(/^&(?!\*)/, "&*"), ty: v.ty });
33
+ return /^&\*(?!\**[\w.]+$)/.test(v.code) || /\.unwrap_or\(&\*(?!\**[\w.]+\)$)/.test(v.code);
34
+ }
35
+ export function asCow(v) {
36
+ if (!isTemporary(v))
37
+ return `std::borrow::Cow::<str>::Borrowed(${bare(v.code)})`;
38
+ const built = /^&\*(.*)$/s.exec(v.code)?.[1];
39
+ if (built === undefined)
40
+ return `std::borrow::Cow::<str>::Owned(${atom(v.code)}.to_owned())`;
41
+ if (enclosed(built))
42
+ return bare(built);
43
+ if (yieldsCow(built))
44
+ return built.startsWith("fv::") ? `std::borrow::Cow::<str>::Owned(${built}.into_owned())` : bare(built);
45
+ if (/^fv::js_(?:char_at|trim|trim_start|trim_end)\(/.test(built))
46
+ return `std::borrow::Cow::<str>::Owned(${built}.to_owned())`;
47
+ return `std::borrow::Cow::<str>::Owned(${built})`;
48
+ }
49
+ export function yieldsCow(code) {
50
+ const routine = /^fv::js_(?:slice|substring|replace|replace_all|pad_start|pad_end)(?=\()/.exec(code);
51
+ if (routine !== null)
52
+ return enclosed(code.slice(routine[0].length));
53
+ const fallback = code.lastIndexOf(".unwrap_or(std::borrow::Cow::<str>::");
54
+ return fallback >= 0 && enclosed(code.slice(fallback + ".unwrap_or".length));
55
+ }
56
+ export function unquote(literal) {
57
+ const escapes = { n: "\n", r: "\r", t: "\t" };
58
+ return literal.slice(1, -1).replace(/\\(?:u\{([0-9a-f]+)\}|(.))/g, (_, hex, c) => (hex ? String.fromCodePoint(parseInt(hex, 16)) : (escapes[c] ?? c)));
59
+ }
60
+ export function stringsEqual(a, b) {
61
+ if (known(a) !== undefined && known(b) !== undefined)
62
+ return String(unquote(a.code) === unquote(b.code));
63
+ if (a.code === '""' || b.code === '""')
64
+ return `${receiver(a.code === '""' ? b.code : a.code)}.is_empty()`;
65
+ return binary(a.code, "==", b.code);
66
+ }
67
+ export function formatted(comp, parts, n, what) {
68
+ let fmt = "";
69
+ const args = [];
70
+ let before;
71
+ const lone = loneOf(...parts.map((p) => (typeof p === "string" ? undefined : p)));
72
+ for (const part of parts) {
73
+ if (typeof part === "string" || (part.ty.k === "str" && known(part) !== undefined)) {
74
+ const text = typeof part === "string" ? part : unquote(part.code);
75
+ if (text !== "")
76
+ before = undefined;
77
+ fmt += text.replace(/[{}]/g, (c) => c + c);
78
+ continue;
79
+ }
80
+ if (before)
81
+ meet(comp, before, part, what, n, "join");
82
+ before = part;
83
+ if (part.format) {
84
+ fmt += part.format.text;
85
+ args.push(...part.format.args);
86
+ continue;
87
+ }
88
+ if (part.ty.k === "str")
89
+ args.push(receiver(part.code).replace(/^(fv::Js\(.*\))\.to_string\(\)$/s, "$1"));
90
+ else if (isNumber(part.ty))
91
+ args.push(`fv::Js(${bare(part.code)})`);
92
+ else if (part.ty.k === "bool")
93
+ args.push(`if ${condition(part.code)} { "true" } else { "false" }`);
94
+ else
95
+ fail(comp, "a template literal interpolates strings, numbers and booleans that are present", n);
96
+ fmt += "{}";
97
+ }
98
+ if (!args.length)
99
+ return { code: rustStr(fmt.replace(/\{\{|\}\}/g, (c) => c[0])), ty: STR };
100
+ const one = parts.filter((p) => (typeof p === "string" ? p !== "" : p.code !== '""'));
101
+ if (fmt === "{}" && one.length === 1 && typeof one[0] !== "string" && one[0].ty.k === "str")
102
+ return one[0];
103
+ return { code: `&*format!(${rustStr(fmt)}, ${args.join(", ")})`, ty: STR, format: { text: fmt, args }, ...lone };
104
+ }
105
+ export function stringMethod(s, target, method, args, n) {
106
+ const comp = s.comp;
107
+ const t = strArg(target.code);
108
+ const arity = (least, most) => {
109
+ if (args.length < least || args.length > most) {
110
+ fail(comp, `\`.${method}()\` takes ${least === most ? least : `${least} to ${most}`} argument${most === 1 ? "" : "s"} here`, n);
111
+ }
112
+ };
113
+ const str = (i) => {
114
+ if (args[i]?.type === "RegExpLiteral")
115
+ fail(comp, `\`.${method}()\` with a regular expression, which the server does not run: give it a string`, args[i]);
116
+ const v = expr(s, args[i]);
117
+ if (v.ty.k !== "str")
118
+ fail(comp, `\`.${method}()\` takes a string that is present`, args[i]);
119
+ return v;
120
+ };
121
+ const num = (i, fallback) => {
122
+ if (args[i] === undefined)
123
+ return fallback;
124
+ const v = expr(s, args[i]);
125
+ if (v.ty.k === "undef")
126
+ return fallback;
127
+ if (!isNumber(v.ty))
128
+ fail(comp, `\`.${method}()\` takes a number that is present`, args[i]);
129
+ return asF64(v);
130
+ };
131
+ const cut = { lone: true };
132
+ switch (method) {
133
+ case "includes":
134
+ case "startsWith":
135
+ case "endsWith": {
136
+ arity(1, 1);
137
+ const x = str(0);
138
+ meet(comp, target, x, `\`.${method}()\``, n, "search");
139
+ const rust = { includes: "contains", startsWith: "starts_with", endsWith: "ends_with" }[method];
140
+ return { code: `${receiver(target.code)}.${rust}(${bare(x.code)})`, ty: BOOL };
141
+ }
142
+ case "indexOf":
143
+ case "lastIndexOf": {
144
+ arity(1, 1);
145
+ const x = str(0);
146
+ meet(comp, target, x, `\`.${method}()\``, n, "search");
147
+ return { code: `fv::js_${method === "indexOf" ? "index_of" : "last_index_of"}(${t}, ${strArg(x.code)})`, ty: INT };
148
+ }
149
+ case "slice":
150
+ case "substring": {
151
+ arity(0, 2);
152
+ const end = num(1, "");
153
+ return { code: `&*fv::js_${method}(${t}, ${num(0, "0.0")}, ${end ? `Some(${end})` : "None"})`, ty: STR, ...cut };
154
+ }
155
+ case "at": {
156
+ arity(1, 1);
157
+ const at = `fv::js_at(${t}, ${num(0, "0.0")})`;
158
+ if (!isTemporary(target))
159
+ return { code: at, ty: opt(STR), ...cut };
160
+ const held = `${at}.map(|v| std::borrow::Cow::<str>::Owned(v.to_owned()))`;
161
+ return { code: `${atom(held)}.as_deref()`, ty: opt(STR), held, ...cut };
162
+ }
163
+ case "charAt":
164
+ arity(0, 1);
165
+ return { code: `${isTemporary(target) ? "&*" : ""}fv::js_char_at(${t}, ${num(0, "0.0")})`, ty: STR, ...cut };
166
+ case "split": {
167
+ arity(1, 1);
168
+ const sep = str(0);
169
+ meet(comp, target, sep, "`.split()`", n, "search");
170
+ return computed(`fv::js_split(${t}, ${strArg(sep.code)}).into_iter()`, STR, target.lone || !nonEmptyLiteral(sep));
171
+ }
172
+ case "replace":
173
+ case "replaceAll": {
174
+ arity(2, 2);
175
+ if (args[1].type === "ArrowFunctionExpression" || args[1].type === "FunctionExpression") {
176
+ fail(comp, `\`.${method}()\` with a function, which the server does not run: give it a string`, args[1]);
177
+ }
178
+ const [pattern, replacement] = [str(0), str(1)];
179
+ meet(comp, target, pattern, `\`.${method}()\``, n, "search");
180
+ meet(comp, target, replacement, `\`.${method}()\``, n, "join");
181
+ const lone = target.lone || replacement.lone || (method === "replaceAll" && !nonEmptyLiteral(pattern));
182
+ return { code: `&*fv::js_${method === "replace" ? "replace" : "replace_all"}(${t}, ${strArg(pattern.code)}, ${strArg(replacement.code)})`, ty: STR, ...(lone ? cut : {}) };
183
+ }
184
+ case "padStart":
185
+ case "padEnd": {
186
+ arity(1, 2);
187
+ const fill = args.length > 1 ? str(1) : { code: '" "', ty: STR };
188
+ const whole = fill.code.startsWith('"') && !/[\u{10000}-\u{10FFFF}]/u.test(fill.code);
189
+ if (fill.lone || (method === "padStart" && target.lone && !whole))
190
+ fail(comp, lonely(`\`.${method}()\``), n);
191
+ return { code: `&*fv::js_${method === "padStart" ? "pad_start" : "pad_end"}(${t}, ${num(0, "0.0")}, ${strArg(fill.code)})`, ty: STR, ...(target.lone || !whole ? cut : {}) };
192
+ }
193
+ case "repeat": {
194
+ arity(1, 1);
195
+ const c = args[0];
196
+ if ((c.type === "UnaryExpression" && c.operator === "-" && c.argument.type === "NumericLiteral" && c.argument.value > 0) || (c.type === "Identifier" && c.name === "Infinity")) {
197
+ fail(comp, "`.repeat()` with a negative or infinite count, which throws a `RangeError`", c);
198
+ }
199
+ if (target.lone)
200
+ fail(comp, lonely("`.repeat()`"), n);
201
+ return { code: `&*fv::js_repeat(${t}, ${num(0, "0.0")})`, ty: STR };
202
+ }
203
+ case "toLocaleUpperCase":
204
+ case "toLocaleLowerCase":
205
+ return fail(comp, `\`.${method}()\` maps case by the locale the server runs in, which the browser need not share: use \`.${method.replace("Locale", "")}()\``, n);
206
+ }
207
+ return null;
208
+ }