@barefootjs/mojolicious 0.16.0 → 0.17.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 (53) hide show
  1. package/dist/adapter/analysis/component-tree.d.ts +30 -0
  2. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  3. package/dist/adapter/emit-context.d.ts +96 -0
  4. package/dist/adapter/emit-context.d.ts.map +1 -0
  5. package/dist/adapter/expr/array-method.d.ts +75 -0
  6. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  7. package/dist/adapter/expr/emitters.d.ts +77 -0
  8. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  9. package/dist/adapter/expr/operand.d.ts +34 -0
  10. package/dist/adapter/expr/operand.d.ts.map +1 -0
  11. package/dist/adapter/index.js +1490 -1336
  12. package/dist/adapter/lib/constants.d.ts +27 -0
  13. package/dist/adapter/lib/constants.d.ts.map +1 -0
  14. package/dist/adapter/lib/ir-scope.d.ts +40 -0
  15. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  16. package/dist/adapter/lib/perl-naming.d.ts +29 -0
  17. package/dist/adapter/lib/perl-naming.d.ts.map +1 -0
  18. package/dist/adapter/lib/types.d.ts +28 -0
  19. package/dist/adapter/lib/types.d.ts.map +1 -0
  20. package/dist/adapter/memo/seed.d.ts +43 -0
  21. package/dist/adapter/memo/seed.d.ts.map +1 -0
  22. package/dist/adapter/mojo-adapter.d.ts +46 -104
  23. package/dist/adapter/mojo-adapter.d.ts.map +1 -1
  24. package/dist/adapter/props/prop-classes.d.ts +48 -0
  25. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  26. package/dist/adapter/spread/spread-codegen.d.ts +64 -0
  27. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  28. package/dist/adapter/value/parsed-literal.d.ts +26 -0
  29. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  30. package/dist/build.js +1490 -1336
  31. package/dist/index.js +1490 -1336
  32. package/dist/test-render.d.ts.map +1 -1
  33. package/lib/BarefootJS/Backend/Mojo.pm +1 -1
  34. package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +1 -1
  35. package/lib/Mojolicious/Plugin/BarefootJS.pm +1 -1
  36. package/package.json +3 -3
  37. package/src/__tests__/mojo-adapter.test.ts +124 -60
  38. package/src/__tests__/query-href.test.ts +94 -0
  39. package/src/adapter/analysis/component-tree.ts +128 -0
  40. package/src/adapter/emit-context.ts +107 -0
  41. package/src/adapter/expr/array-method.ts +408 -0
  42. package/src/adapter/expr/emitters.ts +607 -0
  43. package/src/adapter/expr/operand.ts +55 -0
  44. package/src/adapter/lib/constants.ts +39 -0
  45. package/src/adapter/lib/ir-scope.ts +70 -0
  46. package/src/adapter/lib/perl-naming.ts +36 -0
  47. package/src/adapter/lib/types.ts +31 -0
  48. package/src/adapter/memo/seed.ts +126 -0
  49. package/src/adapter/mojo-adapter.ts +223 -1476
  50. package/src/adapter/props/prop-classes.ts +87 -0
  51. package/src/adapter/spread/spread-codegen.ts +181 -0
  52. package/src/adapter/value/parsed-literal.ts +34 -0
  53. package/src/test-render.ts +2 -0
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Prop classification for the Mojolicious EP template adapter.
3
+ *
4
+ * Extracted from `mojo-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D). Pure functions over `ir.metadata` that derive the per-compile
6
+ * prop/name sets the adapter consults during lowering. Mirror of the Go
7
+ * adapter's `props/prop-types.ts`. No adapter instance state.
8
+ */
9
+
10
+ import type { ComponentIR } from '@barefootjs/jsx'
11
+ import { isStringTypeInfo, isBareStringLiteral } from '../value/parsed-literal.ts'
12
+
13
+ /**
14
+ * (#1971) SSR-resolvable context-value names: props, signal getters, memos.
15
+ * A `<Ctx.Provider value>` member NOT in this set is a client-only function
16
+ * with no SSR value, lowered to `undef`.
17
+ */
18
+ export function collectProviderDataNames(ir: ComponentIR): Set<string> {
19
+ return new Set<string>([
20
+ ...ir.metadata.propsParams.map(p => p.name),
21
+ ...(ir.metadata.signals ?? []).map(s => s.getter),
22
+ ...(ir.metadata.memos ?? []).map(m => m.name),
23
+ ])
24
+ }
25
+
26
+ /**
27
+ * Props whose declared TS type is boolean — a bare binding of one
28
+ * (`data-active={props.isActive}`) must stringify as JS `String(boolean)`
29
+ * ("true"/"false"), not Perl's native `1`/`''` (#1897, pagination's
30
+ * data-active).
31
+ */
32
+ export function collectBooleanTypedProps(ir: ComponentIR): Set<string> {
33
+ return new Set(
34
+ ir.metadata.propsParams
35
+ .filter(prop => prop.type?.primitive === 'boolean' || prop.type?.raw === 'boolean')
36
+ .map(prop => prop.name),
37
+ )
38
+ }
39
+
40
+ /**
41
+ * No-destructure-default props → `undef` when the caller omits them → guard
42
+ * their bare-reference attribute emission with Perl `defined` so the
43
+ * attribute drops instead of rendering `attr=""` (Hono-style nullish
44
+ * omission). A prop WITH a destructure default (`value = ''`) is never
45
+ * `undef` in the body and must stay unconditional, so it is excluded. This
46
+ * mirrors the Go adapter's nillable-field guard: there the witness is the
47
+ * resolved `interface{}` field type; here it is the absence of a default (the
48
+ * analyzer reports `rows` — a `TextareaHTMLAttributes` member destructured
49
+ * without a default — as no-default, `type.kind: 'unknown'`).
50
+ * Excludes concrete-primitive types (`string`/`number`/`boolean`) to match
51
+ * the Go adapter's scope, which guards only `interface{}` (nillable) fields
52
+ * and leaves concrete fields unconditional. So a required, no-default
53
+ * `string` prop still emits `attr=""` like Hono, and only nillable
54
+ * (`unknown`/object/array) no-default props guard.
55
+ */
56
+ export function collectNullableOptionalProps(ir: ComponentIR): Set<string> {
57
+ return new Set(
58
+ ir.metadata.propsParams
59
+ .filter(
60
+ p =>
61
+ p.defaultValue === undefined &&
62
+ !p.isRest &&
63
+ p.type?.kind !== 'primitive',
64
+ )
65
+ .map(p => p.name),
66
+ )
67
+ }
68
+
69
+ /**
70
+ * String-typed signals and props, so equality comparisons against them lower
71
+ * to `eq`/`ne` (#1672). A signal is string-typed when its inferred type is
72
+ * `string` (the analyzer infers this from a string-literal initial value) or,
73
+ * defensively, when its initial value is a bare string literal; a prop when
74
+ * its annotated type is `string`.
75
+ */
76
+ export function collectStringValueNames(ir: ComponentIR): Set<string> {
77
+ const names = new Set<string>()
78
+ for (const s of ir.metadata.signals) {
79
+ if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
80
+ names.add(s.getter)
81
+ }
82
+ }
83
+ for (const p of ir.metadata.propsParams) {
84
+ if (isStringTypeInfo(p.type)) names.add(p.name)
85
+ }
86
+ return names
87
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Object-literal / conditional-spread → Perl hashref lowering for the
3
+ * Mojolicious EP template adapter.
4
+ *
5
+ * Extracted from `mojo-adapter.ts` (domain-module refactor, issue #2018
6
+ * track D). Free functions taking a `MojoSpreadContext` (built by the adapter's
7
+ * `spreadCtx` getter) so the cluster depends on the narrow seam — the recursive
8
+ * expression entry plus per-compile bookkeeping — rather than the whole
9
+ * adapter class. Mirror of the Go adapter's `spread/spread-codegen.ts`.
10
+ *
11
+ * The conditional-spread / object-literal entries read the IR-carried
12
+ * structured `ParsedExpr` tree (#2018, mirroring go-template's U5/U6) instead
13
+ * of re-parsing the source with `ts.createSourceFile`. The condition and scalar
14
+ * values are threaded straight into `ctx.convertExpressionToPerl` as its
15
+ * `preParsed` argument (cf. go-template's
16
+ * `convertExpressionToGo(jsExpr, out?, preParsed?)`), so no stringify→re-parse
17
+ * round-trip occurs — the emitted Perl is byte-identical to the former path
18
+ * because the carried tree is exactly what re-parsing the stringified text
19
+ * produced. The `ts.factory` rebuild in `recordIndexAccessToPerl` only
20
+ * reconstructs the `IDENT[KEY]` node the shared `parseRecordIndexAccess` parser
21
+ * accepts; no source-text re-parse. `stringifyParsedExpr` is retained solely for
22
+ * the BF101 diagnostic message (display purposes).
23
+ */
24
+
25
+ import ts from 'typescript'
26
+ import { parseRecordIndexAccess, stringifyParsedExpr } from '@barefootjs/jsx'
27
+ import type { ParsedExpr } from '@barefootjs/jsx'
28
+
29
+ import type { MojoSpreadContext } from '../emit-context.ts'
30
+
31
+ /**
32
+ * Lower a `cond ? {…} : {…}` conditional-spread expression — carried as the
33
+ * IR's structured `ParsedExpr` tree — to a Perl ternary over two hashrefs, or
34
+ * null when it isn't that shape. `parseExpression` already strips redundant
35
+ * parentheses, so the conditional / object-literal shapes surface directly.
36
+ */
37
+ export function conditionalSpreadToPerl(
38
+ ctx: MojoSpreadContext,
39
+ expr: ParsedExpr | undefined,
40
+ ): string | null {
41
+ if (!expr || expr.kind !== 'conditional') return null
42
+ const whenTrue = expr.consequent
43
+ const whenFalse = expr.alternate
44
+ if (whenTrue.kind !== 'object-literal' || whenFalse.kind !== 'object-literal') {
45
+ return null
46
+ }
47
+ // Thread the condition's carried `ParsedExpr` tree straight through as
48
+ // `preParsed` (#2018) — no stringify→re-parse round-trip; `convertExpressionToPerl`
49
+ // uses the tree directly and derives any diagnostic text from it.
50
+ const condPerl = ctx.convertExpressionToPerl('', expr.test)
51
+ const truePerl = objectLiteralToPerlHashref(ctx, whenTrue)
52
+ const falsePerl = objectLiteralToPerlHashref(ctx, whenFalse)
53
+ if (truePerl === null || falsePerl === null) return null
54
+ return `${condPerl} ? ${truePerl} : ${falsePerl}`
55
+ }
56
+
57
+ /**
58
+ * (#1971 Perl) Lower a bare object-literal expression (`{ align: 'start' }`),
59
+ * carried as the IR's structured `ParsedExpr` tree, to a Perl hashref via
60
+ * `objectLiteralToPerlHashref`, or null when it isn't a plain object literal.
61
+ * Used for inline object-literal child props (carousel `opts`).
62
+ */
63
+ export function objectLiteralExprToPerlHashref(
64
+ ctx: MojoSpreadContext,
65
+ expr: ParsedExpr | undefined,
66
+ ): string | null {
67
+ if (!expr || expr.kind !== 'object-literal') return null
68
+ return objectLiteralToPerlHashref(ctx, expr)
69
+ }
70
+
71
+ /**
72
+ * Convert a static object literal into a Perl hashref string for a
73
+ * conditional spread. Only static string/identifier keys are allowed;
74
+ * values resolve via `convertExpressionToPerl`. Returns null for any
75
+ * computed/spread/dynamic key. Empty object → `{}`.
76
+ */
77
+ export function objectLiteralToPerlHashref(
78
+ ctx: MojoSpreadContext,
79
+ obj: Extract<ParsedExpr, { kind: 'object-literal' }>,
80
+ ): string | null {
81
+ const entries: string[] = []
82
+ for (const prop of obj.properties) {
83
+ // Shorthand `{ a }` was a `ShorthandPropertyAssignment` (not a
84
+ // `PropertyAssignment`), so the former parser rejected it — keep refusing.
85
+ if (prop.shorthand) return null
86
+ // A numeric key (`{ 1: x }`) was rejected by the former parser (only
87
+ // identifier / string-literal names were accepted); `keyKind`
88
+ // distinguishes it from a same-text string `'1'` key.
89
+ if (prop.keyKind === 'numeric') return null
90
+ const key = prop.key
91
+ const val = prop.value
92
+ const indexed = recordIndexAccessToPerl(ctx, val)
93
+ if (
94
+ indexed === null &&
95
+ val.kind === 'index-access' &&
96
+ !isLiteralIndex(val.index)
97
+ ) {
98
+ // Variable-index record access (`sizeMap[size]`) that the
99
+ // static-inline path couldn't resolve — a non-scalar record
100
+ // value, or a non-const receiver. Since #1897 made variable
101
+ // indices parseable (`index-access`), the generic value lowering
102
+ // would now emit `$sizeMap->{$size}` against an UNBOUND module
103
+ // const instead of refusing. Record BF101 and bail so the whole
104
+ // spread surfaces the out-of-shape diagnostic, matching the
105
+ // pre-#1897 behaviour (the refusal then was a side effect of the
106
+ // value lowering). (A bound receiver — a signal getter like
107
+ // `selected()[index]` — is an attribute value, not a spread
108
+ // member, and never reaches here.)
109
+ ctx.errors.push({
110
+ code: 'BF101',
111
+ severity: 'error',
112
+ message: `Spread object value '${stringifyParsedExpr(val)}' indexes a record map whose values aren't scalar literals — it can't lower to an inline Perl hashref.`,
113
+ loc: { file: ctx.componentName + '.tsx', start: { line: 1, column: 0 }, end: { line: 1, column: 0 } },
114
+ suggestion: {
115
+ message: 'Index a record whose values are number/string literals, or move the spread into a `\'use client\'` component so hydration computes it.',
116
+ },
117
+ })
118
+ return null
119
+ }
120
+ const valPerl =
121
+ indexed !== null
122
+ ? indexed
123
+ // Thread the carried `val` tree straight through as `preParsed` (#2018)
124
+ // — no stringify→re-parse round-trip.
125
+ : ctx.convertExpressionToPerl('', val)
126
+ entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`)
127
+ }
128
+ return entries.length === 0 ? '{}' : `{ ${entries.join(', ')} }`
129
+ }
130
+
131
+ /** True when a parsed index is a numeric or string literal (`arr[0]`, `m['k']`). */
132
+ function isLiteralIndex(index: ParsedExpr): boolean {
133
+ return (
134
+ index.kind === 'literal' &&
135
+ (index.literalType === 'number' || index.literalType === 'string')
136
+ )
137
+ }
138
+
139
+ /**
140
+ * Lower a spread-object VALUE of the form `IDENT[KEY]` where:
141
+ * - `IDENT` resolves via `localConstants` to a MODULE-scope object
142
+ * literal whose property values are all scalar (number/string)
143
+ * literals under static (string-literal or identifier) keys
144
+ * (a `Record<staticKeys, scalar>` map like `sizeMap`), AND
145
+ * - `KEY` is a bare identifier that is a prop.
146
+ * Emits an inline indexed Perl hashref:
147
+ * `{ 'sm' => 16, 'md' => 20, ... }->{$size}`
148
+ *
149
+ * Returns the Perl string when convertible, else `null` so the caller
150
+ * falls back to its normal value lowering (which records BF101 for an
151
+ * unsupported shape). Mirror of the Go adapter's `recordIndexAccessToGoMap`.
152
+ */
153
+ export function recordIndexAccessToPerl(ctx: MojoSpreadContext, val: ParsedExpr): string | null {
154
+ // `parseRecordIndexAccess` (the shared single-source-of-truth parser) takes a
155
+ // `ts.Expression`. The only shape it accepts is `IDENT[KEY]` with identifier
156
+ // object and index, so rebuild exactly that node from the carried tree via
157
+ // `ts.factory` — no source-text re-parse needed. Any other shape can't match
158
+ // and short-circuits to `null` here.
159
+ if (
160
+ val.kind !== 'index-access' ||
161
+ val.object.kind !== 'identifier' ||
162
+ val.index.kind !== 'identifier'
163
+ ) {
164
+ return null
165
+ }
166
+ const tsVal = ts.factory.createElementAccessExpression(
167
+ ts.factory.createIdentifier(val.object.name),
168
+ ts.factory.createIdentifier(val.index.name),
169
+ )
170
+ // Shared structural parse (single source of truth in `@barefootjs/jsx`);
171
+ // this wrapper only does the Perl-specific emit (single-quote escaping)
172
+ // from the structured result.
173
+ const parsed = parseRecordIndexAccess(tsVal, ctx.localConstants, ctx.propsParams)
174
+ if (!parsed) return null
175
+ const entries = parsed.entries.map(e => {
176
+ const mapVal =
177
+ e.value.kind === 'number' ? e.value.text : `'${e.value.text.replace(/'/g, "\\'")}'`
178
+ return `'${e.key.replace(/'/g, "\\'")}' => ${mapVal}`
179
+ })
180
+ return `{ ${entries.join(', ')} }->{$${parsed.indexPropName}}`
181
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * String-type value helpers for the Mojolicious EP template adapter.
3
+ *
4
+ * Extracted from `mojo-adapter.ts` (domain-module refactor, issue #2018
5
+ * track D). Pure functions over analyzer type info / const-initializer text —
6
+ * no adapter instance state.
7
+ *
8
+ * SHARED CANDIDATE: `isStringTypeInfo` and `isBareStringLiteral` are
9
+ * byte-identical to the Xslate adapter's copies and adapter-agnostic —
10
+ * extraction candidates for a shared Perl-family codegen module (groundwork
11
+ * for the future Perl evaluator integration, issue #2018 track D).
12
+ *
13
+ * (#2018) The former `parsePureStringLiteral` (the file's only
14
+ * `ts.createSourceFile` re-parse) was removed: module-scope pure-string-const
15
+ * inlining is owned by the shared `collectModuleStringConsts` in
16
+ * `@barefootjs/jsx` (consumed via `moduleStringConsts`), so the adapter-local
17
+ * copy had no callers. Dropping it keeps the adapter free of emit-time source
18
+ * re-parsing without changing any output.
19
+ */
20
+
21
+ import type { TypeInfo } from '@barefootjs/jsx'
22
+
23
+ /** True when `type` is the `string` primitive. */
24
+ export function isStringTypeInfo(type: TypeInfo | undefined): boolean {
25
+ return type?.kind === 'primitive' && type.primitive === 'string'
26
+ }
27
+
28
+ /** True when `initialValue` is a bare string-literal expression (`'x'` /
29
+ * `"x"`), used as a fallback for signals whose type wasn't inferred. */
30
+ export function isBareStringLiteral(initialValue: string | undefined): boolean {
31
+ if (!initialValue) return false
32
+ const v = initialValue.trim()
33
+ return (v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))
34
+ }
@@ -453,6 +453,7 @@ function buildChildDefaultsPerl(ir: ComponentIR): string {
453
453
  }
454
454
  }
455
455
  for (const signal of ir.metadata.signals) {
456
+ if (signal.envReader) continue // env signal is the request reader, not a stashed value (#2057)
456
457
  const value = evaluateSignalInit(signal.initialValue.trim(), undefined)
457
458
  entries.push(`${signal.getter} => ${value !== null ? toPerlLiteral(value) : 'undef'}`)
458
459
  }
@@ -599,6 +600,7 @@ function buildPerlProps(
599
600
  // vars with `Global symbol "$x" requires explicit package name`. Same
600
601
  // rule `buildChildDefaultsPerl` applies to child signals (#1897).
601
602
  for (const signal of ir.metadata.signals) {
603
+ if (signal.envReader) continue // env signal bound below via search_params('') (#2057)
602
604
  const value = evaluateSignalInit(signal.initialValue.trim(), props)
603
605
  entries.push(`${signal.getter} => ${value !== null ? toPerlLiteral(value) : 'undef'}`)
604
606
  }