rip-lang 3.15.4 → 3.16.1

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 (112) hide show
  1. package/README.md +6 -4
  2. package/bin/rip +167 -12
  3. package/docs/AGENTS.md +1 -1
  4. package/docs/RIP-APP.md +808 -0
  5. package/docs/RIP-DUCKDB.md +477 -0
  6. package/docs/RIP-INTRO.md +396 -0
  7. package/docs/RIP-LANG.md +59 -5
  8. package/docs/RIP-SCHEMA.md +191 -8
  9. package/docs/RIP-TYPES.md +74 -103
  10. package/docs/demo/README.md +4 -3
  11. package/docs/dist/rip.js +3627 -1470
  12. package/docs/dist/rip.min.js +671 -244
  13. package/docs/dist/rip.min.js.br +0 -0
  14. package/docs/example/index.json +7 -7
  15. package/docs/example/index.json.br +0 -0
  16. package/docs/extensions/duckdb/manifest.json +1 -1
  17. package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
  18. package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
  19. package/docs/extensions/vscode/print/index.html +2 -1
  20. package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
  21. package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
  22. package/docs/extensions/vscode/print/print-latest.vsix +0 -0
  23. package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
  24. package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
  25. package/docs/ui/bundle.json +61 -0
  26. package/docs/ui/bundle.json.br +0 -0
  27. package/docs/ui/hljs-rip.js +0 -7
  28. package/docs/ui/index.css +66 -23
  29. package/docs/ui/index.html +6 -6
  30. package/package.json +9 -3
  31. package/rip-loader.js +64 -2
  32. package/src/AGENTS.md +63 -36
  33. package/src/browser.js +96 -14
  34. package/src/compiler.js +960 -143
  35. package/src/components.js +794 -88
  36. package/src/{types-emit.js → dts.js} +181 -71
  37. package/src/grammar/README.md +1 -1
  38. package/src/grammar/grammar.rip +111 -97
  39. package/src/lexer.js +132 -18
  40. package/src/parser.js +203 -205
  41. package/src/repl.js +74 -6
  42. package/src/schema/runtime-orm.js +168 -4
  43. package/src/schema/runtime-validate.js +146 -2
  44. package/src/schema/runtime.generated.js +314 -6
  45. package/src/schema/schema.js +5 -5
  46. package/src/sourcemaps.js +277 -1
  47. package/src/stdlib.js +253 -0
  48. package/src/typecheck.js +2023 -106
  49. package/src/types.js +127 -7
  50. package/docs/ui/accordion.rip +0 -103
  51. package/docs/ui/alert-dialog.rip +0 -53
  52. package/docs/ui/autocomplete.rip +0 -115
  53. package/docs/ui/avatar.rip +0 -37
  54. package/docs/ui/badge.rip +0 -15
  55. package/docs/ui/breadcrumb.rip +0 -47
  56. package/docs/ui/button-group.rip +0 -26
  57. package/docs/ui/button.rip +0 -23
  58. package/docs/ui/card.rip +0 -25
  59. package/docs/ui/carousel.rip +0 -110
  60. package/docs/ui/checkbox-group.rip +0 -61
  61. package/docs/ui/checkbox.rip +0 -33
  62. package/docs/ui/collapsible.rip +0 -50
  63. package/docs/ui/combobox.rip +0 -130
  64. package/docs/ui/context-menu.rip +0 -88
  65. package/docs/ui/date-picker.rip +0 -206
  66. package/docs/ui/dialog.rip +0 -60
  67. package/docs/ui/drawer.rip +0 -58
  68. package/docs/ui/editable-value.rip +0 -82
  69. package/docs/ui/field.rip +0 -53
  70. package/docs/ui/fieldset.rip +0 -22
  71. package/docs/ui/form.rip +0 -39
  72. package/docs/ui/grid.rip +0 -901
  73. package/docs/ui/input-group.rip +0 -28
  74. package/docs/ui/input.rip +0 -36
  75. package/docs/ui/label.rip +0 -16
  76. package/docs/ui/menu.rip +0 -134
  77. package/docs/ui/menubar.rip +0 -151
  78. package/docs/ui/meter.rip +0 -36
  79. package/docs/ui/multi-select.rip +0 -203
  80. package/docs/ui/native-select.rip +0 -33
  81. package/docs/ui/nav-menu.rip +0 -126
  82. package/docs/ui/number-field.rip +0 -162
  83. package/docs/ui/otp-field.rip +0 -89
  84. package/docs/ui/pagination.rip +0 -123
  85. package/docs/ui/popover.rip +0 -93
  86. package/docs/ui/preview-card.rip +0 -75
  87. package/docs/ui/progress.rip +0 -25
  88. package/docs/ui/radio-group.rip +0 -57
  89. package/docs/ui/resizable.rip +0 -123
  90. package/docs/ui/scroll-area.rip +0 -145
  91. package/docs/ui/select.rip +0 -151
  92. package/docs/ui/separator.rip +0 -17
  93. package/docs/ui/skeleton.rip +0 -22
  94. package/docs/ui/slider.rip +0 -165
  95. package/docs/ui/spinner.rip +0 -17
  96. package/docs/ui/table.rip +0 -27
  97. package/docs/ui/tabs.rip +0 -113
  98. package/docs/ui/textarea.rip +0 -48
  99. package/docs/ui/toast.rip +0 -87
  100. package/docs/ui/toggle-group.rip +0 -71
  101. package/docs/ui/toggle.rip +0 -24
  102. package/docs/ui/toolbar.rip +0 -38
  103. package/docs/ui/tooltip.rip +0 -85
  104. package/src/app.rip +0 -1571
  105. package/src/sourcemap-merge.js +0 -287
  106. /package/docs/demo/{components → routes}/_layout.rip +0 -0
  107. /package/docs/demo/{components → routes}/about.rip +0 -0
  108. /package/docs/demo/{components → routes}/card.rip +0 -0
  109. /package/docs/demo/{components → routes}/counter.rip +0 -0
  110. /package/docs/demo/{components → routes}/index.rip +0 -0
  111. /package/docs/demo/{components → routes}/todos.rip +0 -0
  112. /package/src/schema/{dts-emit.js → dts.js} +0 -0
@@ -891,14 +891,19 @@ User
891
891
  Every `:model` instance carries:
892
892
 
893
893
  ```coffee
894
- user.save! # validate, run hooks, INSERT or UPDATE
895
- user.destroy! # run hooks, DELETE (or UPDATE deleted_at for @softDelete)
896
- user.ok() # boolean — current fields validate
897
- user.errors() # SchemaIssue[] — current fields' errors
898
- user.toJSON() # plain object of own enumerable properties
899
- # (id, declared fields, @timestamps columns, @softDelete
900
- # deletedAt, @belongs_to FKs, !> eager-derived — but NOT
901
- # methods, ~> computed getters, or internal state)
894
+ user.save! # validate, run hooks, INSERT or UPDATE
895
+ user.destroy! # run hooks, DELETE (or UPDATE deleted_at for @softDelete)
896
+ user.ok() # boolean — current fields validate
897
+ user.errors() # SchemaIssue[] — current fields' errors
898
+ user.toJSON() # plain object of own enumerable properties
899
+ # (id, declared fields, @timestamps columns, @softDelete
900
+ # deletedAt, @belongs_to FKs, !> eager-derived — but NOT
901
+ # methods, ~> computed getters, or internal state)
902
+ user.savedChanges # Map<fieldName, [oldValue, newValue]> from the most
903
+ # recent save() — empty Map when nothing was written
904
+ user.markDirty 'name' # force a column into the next UPDATE; escape hatch
905
+ # for in-place mutations of object-valued fields
906
+ # that === can't see (json, Date, etc.)
902
907
  ```
903
908
 
904
909
  Plus any methods, computed getters, and relation accessors you declared
@@ -907,6 +912,114 @@ on the schema. Naming tip: methods that produce a fresh projection
907
912
  `to` / `as` / `from` / `parse` conversion convention — see
908
913
  [RIP-LANG.md §15 "Conversion Method Naming"](./RIP-LANG.md#conversion-method-naming).
909
914
 
915
+ ### What `save()` actually writes
916
+
917
+ The runtime tracks a snapshot of declared-field and `@belongs_to` FK
918
+ column values at hydrate / INSERT / UPDATE time. On `.save()` it
919
+ compares current values against the snapshot and emits a column-
920
+ targeted UPDATE that touches **only the columns whose values changed**.
921
+ If nothing changed, no SQL is issued at all.
922
+
923
+ Two practical consequences:
924
+
925
+ 1. **No-op saves are free.** Calling `.save()` on an unchanged row is
926
+ a no-op — no DB round-trip, no row touched. Mirrors Active Record
927
+ with `partial_writes`.
928
+ 2. **`@timestamps` `updated_at` is bumped only on real writes.**
929
+ Calling `.save()` with no actual changes does NOT bump
930
+ `updated_at` (which would defeat the no-op-save optimization
931
+ entirely). Bumped on every UPDATE that does write something.
932
+
933
+ A third practical consequence on DuckDB specifically: column-targeted
934
+ UPDATEs sidestep DuckDB's foreign-key restriction on indexed-column
935
+ updates of referenced parent rows. See
936
+ [`docs/RIP-DUCKDB.md`](./RIP-DUCKDB.md) for the full rule, what works,
937
+ what doesn't, and how to design around it.
938
+
939
+ The diff is observable as `inst.savedChanges` after the save returns
940
+ (or inside `afterCreate` / `afterUpdate` / `afterSave` hooks). Same
941
+ shape as Active Record's `saved_changes`:
942
+
943
+ ```coffee
944
+ order = Order.find! 1 # snapshot captured
945
+ order.notes = "expedited"
946
+ order.userId = 9 # @belongs_to User FK
947
+ order.save!
948
+
949
+ order.savedChanges # Map(2) {"notes" => [null, "expedited"], "userId" => [7, 9]}
950
+ order.savedChanges.size # 2
951
+ order.savedChanges.has 'notes' # true
952
+ ```
953
+
954
+ INSERT records `[null, newValue]` for every field/FK that was written;
955
+ UPDATE records `[oldValue, newValue]` for every field/FK whose value
956
+ actually changed. `@timestamps` columns appear with the new ISO
957
+ timestamp on real INSERTs and UPDATEs.
958
+
959
+ Hook firing matches Active Record exactly: `before*` and `after*` hooks
960
+ fire on every successful `.save()`, regardless of whether SQL was
961
+ emitted. Hooks differentiate real writes from no-ops by checking
962
+ `@savedChanges.size` or specific keys.
963
+
964
+ ### In-place mutation of object-valued fields
965
+
966
+ The dirty check uses value identity (`===` with NaN handling). Setter
967
+ assignments are detected:
968
+
969
+ ```coffee
970
+ user.settings = {theme: "light", notifications: true} # new reference; detected
971
+ user.save!
972
+ ```
973
+
974
+ In-place mutations are **not**:
975
+
976
+ ```coffee
977
+ user.settings.theme = "light" # same reference; invisible
978
+ user.save! # nothing written
979
+ ```
980
+
981
+ This matches Active Record's behavior with serialized attributes —
982
+ "Active Record by default does not detect changes inside mutable
983
+ serialized attributes." The escape hatch is `markDirty`:
984
+
985
+ ```coffee
986
+ user.settings.theme = "light"
987
+ user.markDirty 'settings' # AR's `settings_will_change!`
988
+ user.save! # writes settings = '{"theme":"light",...}'
989
+ ```
990
+
991
+ `markDirty` accepts both camelCase and snake_case names, validates
992
+ against declared fields and `@belongs_to` FK column names, and throws
993
+ on unknown names or non-persisted instances (INSERT writes every set
994
+ field, so `markDirty` there would be a silent no-op).
995
+
996
+ Same caveat applies to `Date` fields:
997
+
998
+ ```coffee
999
+ order.collectedAt.setHours 5 # in-place; invisible
1000
+ order.markDirty 'collectedAt'
1001
+ order.save!
1002
+ ```
1003
+
1004
+ If you find yourself reaching for `markDirty` often, prefer immutable
1005
+ updates instead — they're cleaner and the dirty check sees them
1006
+ automatically:
1007
+
1008
+ ```coffee
1009
+ user.settings = { ...user.settings, theme: "light" }
1010
+ order.collectedAt = new Date order.collectedAt.getTime() + 3600000
1011
+ ```
1012
+
1013
+ ### Re-entry guard
1014
+
1015
+ `.save()` cannot be re-entered on the same instance while a save is
1016
+ already in flight. Calling `@save!` from inside this instance's
1017
+ `beforeSave` / `beforeUpdate` / `afterSave` hook throws — that's
1018
+ almost always a recursion bug, and silent infinite-loop debugging is
1019
+ worse than a clear error. The guard is per-instance: independent
1020
+ instances saving in parallel are unaffected, and sequential saves on
1021
+ the same instance work fine.
1022
+
910
1023
  ### Lifecycle hooks
911
1024
 
912
1025
  Hooks are methods whose name matches one of the [ten recognized hook
@@ -1083,6 +1196,76 @@ Order.create! userId: 7, total: 100 # same result
1083
1196
 
1084
1197
  Use whichever reads better alongside nearby raw SQL or JSON payloads.
1085
1198
 
1199
+ ### Field-name conventions
1200
+
1201
+ The snake_case ↔ camelCase bijection only works for identifiers
1202
+ that round-trip cleanly. The schema runtime enforces canonical
1203
+ camelCase at definition time:
1204
+
1205
+ ```coffee
1206
+ User = schema :model
1207
+ mdmId? string # OK — canonical
1208
+ mdmID? string # error — acronym style;
1209
+ # round-trips to mdm_i_d / mdmID
1210
+ # ambiguously
1211
+ ```
1212
+
1213
+ Rules:
1214
+
1215
+ - Lowercase-first
1216
+ - Alphanumeric body
1217
+ - No two consecutive uppercase letters anywhere
1218
+
1219
+ Same convention as Java Beans, Swift's "Acronyms in API names"
1220
+ guidance, and what most JS/TS codebases follow in practice.
1221
+
1222
+ The runtime also reserves the names of its instance API
1223
+ (`save`, `destroy`, `ok`, `errors`, `toJSON`, `savedChanges`,
1224
+ `markDirty`, `_dirty` / `_persisted` / `_snapshot` / `_saving`)
1225
+ and the implicit timestamp / soft-delete columns
1226
+ (`createdAt`, `updatedAt`, `deletedAt`). Declaring any of those
1227
+ as a user field on a `:model` raises a `'reserved ORM name'`
1228
+ collision error at definition time. (Mixins are exempt — they
1229
+ can declare `createdAt` / `updatedAt` for explicit control,
1230
+ which is the alternative to the `@timestamps` directive.)
1231
+
1232
+ ### Relation target names
1233
+
1234
+ `@belongs_to TargetName` derives the FK column from the target's
1235
+ PascalCase name via `__schemaSnake(target) + '_id'`. The same
1236
+ camelCase / snake_case bijection rule applies in reverse: target
1237
+ names should be canonical PascalCase (`User`, `UserOrg`, not
1238
+ `MDMUser`) so the derived FK column round-trips cleanly. Acronym-
1239
+ style target names aren't currently rejected at definition time,
1240
+ but writing `@belongs_to MDMUser` produces FK column `m_d_m_user_id`
1241
+ — almost certainly not what you want. Stick to PascalCase.
1242
+
1243
+ ### SQL reserved words
1244
+
1245
+ The runtime always quotes column names in generated SQL — every
1246
+ INSERT, UPDATE, and SELECT it emits surrounds column identifiers
1247
+ with `"..."`. So a field named `order` works fine through the
1248
+ ORM:
1249
+
1250
+ ```coffee
1251
+ Trade = schema :model
1252
+ order! integer # works through the ORM
1253
+ ```
1254
+
1255
+ The compiled SQL is `UPDATE "trades" SET "order" = ? WHERE "id" = ?`.
1256
+
1257
+ The catch is **raw SQL** that you write yourself via the adapter's
1258
+ `query()` method or via `query!`. There the reserved-word collision
1259
+ becomes your problem:
1260
+
1261
+ ```coffee
1262
+ result = query! "SELECT order FROM trades" # syntax error
1263
+ result = query! "SELECT \"order\" FROM trades" # works
1264
+ ```
1265
+
1266
+ This matches Active Record's behavior — the ORM-generated SQL is
1267
+ always quoted, and raw SQL is always the user's responsibility.
1268
+
1086
1269
  ---
1087
1270
 
1088
1271
  ## 9. Mixins
package/docs/RIP-TYPES.md CHANGED
@@ -59,10 +59,7 @@ Both compile to identical JavaScript.
59
59
  |-------|---------|---------|
60
60
  | `::` | Type annotation | `count:: number = 0` |
61
61
  | `type` | Type alias | `type ID = number` |
62
- | `?` | Optional value (`T \| undefined`) | `email:: string?` |
63
- | `??` | Nullable value (`T \| null \| undefined`) | `middle:: string??` |
64
- | `!` | Non-nullable (`NonNullable<T>`) | `id:: ID!` |
65
- | `?:` | Optional property | `email?: string` |
62
+ | `?::` | Optional parameter / field / prop | `email?:: string` |
66
63
  | `\|` | Union member | `"a" \| "b" \| "c"` |
67
64
  | `=>` | Function type arrow | `(a: number) => string` |
68
65
  | `<T>` | Generic parameter | `Container<T>` |
@@ -284,106 +281,78 @@ type Dictionary = {
284
281
 
285
282
  ## Optionality Modifiers
286
283
 
287
- Lightweight suffix operators that map directly to TypeScript unions.
284
+ Rip uses a single optionality marker: `?` placed on the **name**
285
+ (parameter, type-alias field, structural field, or component prop)
286
+ **before** the `::` annotation sigil. There are no `T?` / `T??` / `T!`
287
+ type-suffix operators — write `T | undefined`, `T | null | undefined`,
288
+ or `NonNullable<T>` directly when you need them.
288
289
 
289
- ### Optional: `T?`
290
-
291
- Indicates a value may be undefined.
290
+ ### Optional Parameter / Field: `name?:: T`
292
291
 
293
292
  ```coffee
294
- email:: string?
295
- callback:: Function?
296
- ```
297
-
298
- **Emits:**
299
-
300
- ```ts
301
- email: string | undefined
302
- callback: Function | undefined
303
- ```
293
+ def greet(name?:: string):: void
294
+ console.log "hello #{name ?? 'world'}"
304
295
 
305
- ### Nullable Optional: `T??`
306
-
307
- Indicates a value may be null or undefined.
308
-
309
- ```coffee
310
- middle:: string??
311
- cache:: Map<string, any>??
296
+ type User =
297
+ id:: number
298
+ name:: string
299
+ email?:: string # Optional field — may be absent
312
300
  ```
313
301
 
314
302
  **Emits:**
315
303
 
316
304
  ```ts
317
- middle: string | null | undefined
318
- cache: Map<string, any> | null | undefined
319
- ```
320
-
321
- ### Non-Nullable: `T!`
305
+ function greet(name?: string): void { ... }
322
306
 
323
- Asserts a value is never null or undefined.
324
-
325
- ```coffee
326
- id:: ID!
327
- user:: User!
307
+ type User = {
308
+ id: number;
309
+ name: string;
310
+ email?: string;
311
+ };
328
312
  ```
329
313
 
330
- **Emits:**
331
-
332
- ```ts
333
- id: NonNullable<ID>
334
- user: NonNullable<User>
335
- ```
314
+ ### Component Props: `@prop?:: T [:= default]`
336
315
 
337
- ### In Function Signatures
316
+ A `?` on a component prop name marks it optional in the generated
317
+ constructor type. A `:=` default is purely a runtime initializer — it
318
+ does NOT make the prop optional on its own. The three canonical shapes:
338
319
 
339
320
  ```coffee
340
- def findUser(id:: number):: User?
341
- db.find(id) or undefined
342
-
343
- def getUser(id:: number):: User!
344
- db.find(id) ?? throw new Error "Not found"
345
-
346
- def updateUser(id:: number, email:: string??):: boolean
347
- ...
321
+ export Counter = component
322
+ @value:: number # required, no default
323
+ @step?:: number # optional, no default
324
+ @label?:: string := "Count" # optional with default
348
325
  ```
349
326
 
350
327
  **Emits:**
351
328
 
352
329
  ```ts
353
- function findUser(id: number): User | undefined { ... }
354
- function getUser(id: number): NonNullable<User> { ... }
355
- function updateUser(id: number, email: string | null | undefined): boolean { ... }
330
+ export declare class Counter {
331
+ constructor(props: {
332
+ value: number;
333
+ step?: number;
334
+ label?: string;
335
+ });
336
+ }
356
337
  ```
357
338
 
358
- ### Optional Properties
339
+ Writing `@prop:: T := V` (typed, defaulted, no `?`) declares a
340
+ **required prop with a default** — the parent must still pass it. To
341
+ make a prop optional, add `?` to the name.
359
342
 
360
- In structural types, `?` after the property name makes it optional (the
361
- property itself may be absent), distinct from value optionality:
343
+ ### Expressing Value Optionality
362
344
 
363
- ```coffee
364
- type User =
365
- id: number
366
- name: string
367
- email?: string # Optional property — may be absent
368
- phone?: string? # Optional property that can also be undefined when present
369
- ```
370
-
371
- **Emits:**
345
+ When the **value** itself needs to permit `undefined` or `null`, write
346
+ the union explicitly:
372
347
 
373
- ```ts
374
- type User = {
375
- id: number;
376
- name: string;
377
- email?: string;
378
- phone?: string | undefined;
379
- };
348
+ ```coffee
349
+ email:: string | undefined
350
+ middle:: string | null | undefined
351
+ id:: NonNullable<ID>
380
352
  ```
381
353
 
382
- ### Key Distinction
383
-
384
- - `email?: string` — property may be absent
385
- - `email:: string?` — value may be undefined
386
- - `email?: string??` — property may be absent, value may be null or undefined
354
+ This is the same form TypeScript uses, so the emitted DTS is a
355
+ character-for-character match.
387
356
 
388
357
  ---
389
358
 
@@ -647,18 +616,20 @@ Types are optional and gradual:
647
616
 
648
617
  ### Project Level
649
618
 
650
- Configure via `rip.json` or the `"rip"` key in `package.json`:
619
+ Configure via the `"rip"` key in `package.json`:
651
620
 
652
621
  ```json
653
622
  {
654
623
  "strict": true,
624
+ "checkAll": true,
655
625
  "exclude": ["vendor/**", "legacy/**"]
656
626
  }
657
627
  ```
658
628
 
659
629
  | Key | Purpose |
660
630
  |-----|--------|
661
- | `strict` | Enable strict mode for `rip check` (default: `false`) |
631
+ | `strict` | Enable TS strict family for `rip check` (default: `false`) |
632
+ | `checkAll` | Type-check every non-`@nocheck` `.rip` file, not just annotated ones (default: `false`) |
662
633
  | `exclude` | Glob patterns for files to skip during `rip check` |
663
634
 
664
635
  ### File Level
@@ -1153,9 +1124,10 @@ collection — the token itself is the answer:
1153
1124
 
1154
1125
  **Other special cases:**
1155
1126
 
1156
- - **`?` / `??` / `!` suffixes**: These modify the type. When they appear
1157
- unspaced after an identifier at depth 0, they are part of the type:
1158
- `string?` `"string?"`, `ID!` `"ID!"`.
1127
+ - **`?` on names**: When `?` appears unspaced **before** a `::` annotation
1128
+ on a parameter, field, or component prop name, the lexer marks the name
1129
+ token as optional (`data.optional = true`). This is the sole optionality
1130
+ marker — there are no `?` / `??` / `!` suffixes on type expressions.
1159
1131
  - **Generic `<>` vs comparison**: Inside a type context (after `::`), always
1160
1132
  treat `<` as opening a generic bracket. The type context is unambiguous
1161
1133
  because we are already inside a `::` collection.
@@ -1341,12 +1313,12 @@ syntax into standard TypeScript:
1341
1313
  | Rip syntax | TypeScript equivalent |
1342
1314
  |-----------|---------------------|
1343
1315
  | `::` | `:` (annotation sigil to type separator) |
1344
- | `T?` | `T \| undefined` |
1345
- | `T??` | `T \| null \| undefined` |
1346
- | `T!` | `NonNullable<T>` |
1316
+ | `name?::` | `name?:` (optional parameter / field / prop) |
1347
1317
 
1348
1318
  Function type expressions use `=>` directly (same as TypeScript), so no
1349
- arrow conversion is needed.
1319
+ arrow conversion is needed. Value-level optionality is written with
1320
+ explicit unions (`T | undefined`, `T | null`, `NonNullable<T>`) — there
1321
+ are no type-suffix operators.
1350
1322
 
1351
1323
  The function returns a `.d.ts` string. Declarations without type
1352
1324
  annotations are skipped — only annotated code appears in the output.
@@ -1774,29 +1746,28 @@ Type annotations never affect `let` vs `const` in `.js` output:
1774
1746
 
1775
1747
  ### Edge Cases
1776
1748
 
1777
- #### Optionality Suffixes in Types
1749
+ #### Optionality Marker on Names
1778
1750
 
1779
- The `?`, `??`, and `!` suffixes appear unspaced after the type name:
1751
+ A `?` placed unspaced **before** the `::` annotation on a parameter,
1752
+ field, or component prop name marks the name as optional:
1780
1753
 
1781
1754
  ```coffee
1782
- email:: string? # type = "string?"
1783
- middle:: string?? # → type = "string??"
1784
- id:: ID! # → type = "ID!"
1785
- ```
1786
-
1787
- In `rewriteTypes()`, when collecting type tokens, if the next token is `?`,
1788
- `??`, or `!` and is **not spaced** from the previous token, include it as
1789
- part of the type string.
1755
+ def fetch(url:: string, opts?:: RequestInit):: Response
1756
+ ...
1790
1757
 
1791
- The `emitTypes()` function expands these suffixes into standard TypeScript:
1758
+ type User =
1759
+ id:: number
1760
+ email?:: string # optional field
1761
+ ```
1792
1762
 
1793
- | Rip suffix | TypeScript equivalent |
1794
- |-----------|---------------------|
1795
- | `T?` | `T \| undefined` |
1796
- | `T??` | `T \| null \| undefined` |
1797
- | `T!` | `NonNullable<T>` |
1763
+ In the lexer, when `?` appears immediately before `::`, it is stripped
1764
+ from the stream and `data.optional = true` is set on the preceding
1765
+ name token. `emitTypes()` reads that flag and emits `name?:` instead of
1766
+ `name:` in the generated TypeScript.
1798
1767
 
1799
- See §1.6 for the full Rip-to-TypeScript conversion table (including `::` → `:`).
1768
+ Value-level optionality is expressed with explicit unions there are
1769
+ no `T?` / `T??` / `T!` type-suffix operators. See §1.6 for the full
1770
+ Rip-to-TypeScript conversion table.
1800
1771
 
1801
1772
  #### File-Level Type Directives
1802
1773
 
@@ -6,7 +6,7 @@ The canonical "everything in one file" demo of the Rip App framework. Six compon
6
6
 
7
7
  ```
8
8
  docs/demo/
9
- ├── components/
9
+ ├── routes/
10
10
  │ ├── _layout.rip — root layout with nav + error boundary
11
11
  │ ├── index.rip — Home page (file-based routing: / → index.rip)
12
12
  │ ├── counter.rip — reactive state, := / ~> persistence to stash
@@ -22,7 +22,8 @@ docs/demo/
22
22
  ```bash
23
23
  bun run bundle:demo
24
24
  # wraps:
25
- # bun scripts/bundle-app.js docs/demo -o docs/example/index.json -t "Rip App Demo"
25
+ # bun scripts/bundle-app.js docs/demo/routes --prefix _route --css docs/demo/css \
26
+ # -o docs/example/index.json -t "Rip App Demo"
26
27
  ```
27
28
 
28
29
  Output: `docs/example/index.json` — a single ~17 KB file containing every component's raw `.rip` source plus all CSS, ready to ship to any static host. The launcher at `docs/example/index.html` fetches it once and runs the whole app from memory: no bundler, no build step, no per-component network requests.
@@ -38,6 +39,6 @@ Then `<script type="text/rip">launch bundle: bundle</script>` mounts everything.
38
39
 
39
40
  ## Iterating
40
41
 
41
- Edit any `.rip` file under `components/`, then re-run `bun run bundle:demo` to refresh the bundled JSON. The launcher HTML at `docs/example/` will pick up the new bundle on next load.
42
+ Edit any `.rip` file under `routes/`, then re-run `bun run bundle:demo` to refresh the bundled JSON. The launcher HTML at `docs/example/` will pick up the new bundle on next load.
42
43
 
43
44
  CSS is concatenated alphabetically by filename. Add `.css` files under `css/` to extend the design.