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.
- package/README.md +6 -4
- package/bin/rip +167 -12
- package/docs/AGENTS.md +1 -1
- package/docs/RIP-APP.md +808 -0
- package/docs/RIP-DUCKDB.md +477 -0
- package/docs/RIP-INTRO.md +396 -0
- package/docs/RIP-LANG.md +59 -5
- package/docs/RIP-SCHEMA.md +191 -8
- package/docs/RIP-TYPES.md +74 -103
- package/docs/demo/README.md +4 -3
- package/docs/dist/rip.js +3627 -1470
- package/docs/dist/rip.min.js +671 -244
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/example/index.json +7 -7
- package/docs/example/index.json.br +0 -0
- package/docs/extensions/duckdb/manifest.json +1 -1
- package/docs/extensions/duckdb/v1.5.2/linux_amd64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/duckdb/v1.5.2/osx_arm64/ripdb.duckdb_extension.gz +0 -0
- package/docs/extensions/vscode/print/index.html +2 -1
- package/docs/extensions/vscode/print/print-1.0.13.vsix +0 -0
- package/docs/extensions/vscode/print/print-1.0.14.vsix +0 -0
- package/docs/extensions/vscode/print/print-latest.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-0.5.15.vsix +0 -0
- package/docs/extensions/vscode/rip/rip-latest.vsix +0 -0
- package/docs/ui/bundle.json +61 -0
- package/docs/ui/bundle.json.br +0 -0
- package/docs/ui/hljs-rip.js +0 -7
- package/docs/ui/index.css +66 -23
- package/docs/ui/index.html +6 -6
- package/package.json +9 -3
- package/rip-loader.js +64 -2
- package/src/AGENTS.md +63 -36
- package/src/browser.js +96 -14
- package/src/compiler.js +960 -143
- package/src/components.js +794 -88
- package/src/{types-emit.js → dts.js} +181 -71
- package/src/grammar/README.md +1 -1
- package/src/grammar/grammar.rip +111 -97
- package/src/lexer.js +132 -18
- package/src/parser.js +203 -205
- package/src/repl.js +74 -6
- package/src/schema/runtime-orm.js +168 -4
- package/src/schema/runtime-validate.js +146 -2
- package/src/schema/runtime.generated.js +314 -6
- package/src/schema/schema.js +5 -5
- package/src/sourcemaps.js +277 -1
- package/src/stdlib.js +253 -0
- package/src/typecheck.js +2023 -106
- package/src/types.js +127 -7
- package/docs/ui/accordion.rip +0 -103
- package/docs/ui/alert-dialog.rip +0 -53
- package/docs/ui/autocomplete.rip +0 -115
- package/docs/ui/avatar.rip +0 -37
- package/docs/ui/badge.rip +0 -15
- package/docs/ui/breadcrumb.rip +0 -47
- package/docs/ui/button-group.rip +0 -26
- package/docs/ui/button.rip +0 -23
- package/docs/ui/card.rip +0 -25
- package/docs/ui/carousel.rip +0 -110
- package/docs/ui/checkbox-group.rip +0 -61
- package/docs/ui/checkbox.rip +0 -33
- package/docs/ui/collapsible.rip +0 -50
- package/docs/ui/combobox.rip +0 -130
- package/docs/ui/context-menu.rip +0 -88
- package/docs/ui/date-picker.rip +0 -206
- package/docs/ui/dialog.rip +0 -60
- package/docs/ui/drawer.rip +0 -58
- package/docs/ui/editable-value.rip +0 -82
- package/docs/ui/field.rip +0 -53
- package/docs/ui/fieldset.rip +0 -22
- package/docs/ui/form.rip +0 -39
- package/docs/ui/grid.rip +0 -901
- package/docs/ui/input-group.rip +0 -28
- package/docs/ui/input.rip +0 -36
- package/docs/ui/label.rip +0 -16
- package/docs/ui/menu.rip +0 -134
- package/docs/ui/menubar.rip +0 -151
- package/docs/ui/meter.rip +0 -36
- package/docs/ui/multi-select.rip +0 -203
- package/docs/ui/native-select.rip +0 -33
- package/docs/ui/nav-menu.rip +0 -126
- package/docs/ui/number-field.rip +0 -162
- package/docs/ui/otp-field.rip +0 -89
- package/docs/ui/pagination.rip +0 -123
- package/docs/ui/popover.rip +0 -93
- package/docs/ui/preview-card.rip +0 -75
- package/docs/ui/progress.rip +0 -25
- package/docs/ui/radio-group.rip +0 -57
- package/docs/ui/resizable.rip +0 -123
- package/docs/ui/scroll-area.rip +0 -145
- package/docs/ui/select.rip +0 -151
- package/docs/ui/separator.rip +0 -17
- package/docs/ui/skeleton.rip +0 -22
- package/docs/ui/slider.rip +0 -165
- package/docs/ui/spinner.rip +0 -17
- package/docs/ui/table.rip +0 -27
- package/docs/ui/tabs.rip +0 -113
- package/docs/ui/textarea.rip +0 -48
- package/docs/ui/toast.rip +0 -87
- package/docs/ui/toggle-group.rip +0 -71
- package/docs/ui/toggle.rip +0 -24
- package/docs/ui/toolbar.rip +0 -38
- package/docs/ui/tooltip.rip +0 -85
- package/src/app.rip +0 -1571
- package/src/sourcemap-merge.js +0 -287
- /package/docs/demo/{components → routes}/_layout.rip +0 -0
- /package/docs/demo/{components → routes}/about.rip +0 -0
- /package/docs/demo/{components → routes}/card.rip +0 -0
- /package/docs/demo/{components → routes}/counter.rip +0 -0
- /package/docs/demo/{components → routes}/index.rip +0 -0
- /package/docs/demo/{components → routes}/todos.rip +0 -0
- /package/src/schema/{dts-emit.js → dts.js} +0 -0
package/docs/RIP-SCHEMA.md
CHANGED
|
@@ -891,14 +891,19 @@ User
|
|
|
891
891
|
Every `:model` instance carries:
|
|
892
892
|
|
|
893
893
|
```coffee
|
|
894
|
-
user.save!
|
|
895
|
-
user.destroy!
|
|
896
|
-
user.ok()
|
|
897
|
-
user.errors()
|
|
898
|
-
user.toJSON()
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
318
|
-
cache: Map<string, any> | null | undefined
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
### Non-Nullable: `T!`
|
|
305
|
+
function greet(name?: string): void { ... }
|
|
322
306
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
307
|
+
type User = {
|
|
308
|
+
id: number;
|
|
309
|
+
name: string;
|
|
310
|
+
email?: string;
|
|
311
|
+
};
|
|
328
312
|
```
|
|
329
313
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
```ts
|
|
333
|
-
id: NonNullable<ID>
|
|
334
|
-
user: NonNullable<User>
|
|
335
|
-
```
|
|
314
|
+
### Component Props: `@prop?:: T [:= default]`
|
|
336
315
|
|
|
337
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
330
|
+
export declare class Counter {
|
|
331
|
+
constructor(props: {
|
|
332
|
+
value: number;
|
|
333
|
+
step?: number;
|
|
334
|
+
label?: string;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
356
337
|
```
|
|
357
338
|
|
|
358
|
-
|
|
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
|
-
|
|
361
|
-
property itself may be absent), distinct from value optionality:
|
|
343
|
+
### Expressing Value Optionality
|
|
362
344
|
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
```
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
- **`?`
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
-
| `
|
|
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
|
|
1749
|
+
#### Optionality Marker on Names
|
|
1778
1750
|
|
|
1779
|
-
|
|
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
|
-
|
|
1783
|
-
|
|
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
|
-
|
|
1758
|
+
type User =
|
|
1759
|
+
id:: number
|
|
1760
|
+
email?:: string # optional field
|
|
1761
|
+
```
|
|
1792
1762
|
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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
|
-
|
|
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
|
|
package/docs/demo/README.md
CHANGED
|
@@ -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
|
-
├──
|
|
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
|
|
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 `
|
|
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.
|