rip-lang 3.16.0 → 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.
Binary file
@@ -103,7 +103,7 @@ export WidgetGallery = component
103
103
  return unless entry
104
104
  sourceName = entry.name
105
105
  sourceLines = entry.lines
106
- src = window.__RIP__?.components?.read("components/#{id}.rip")
106
+ src = window.__RIP__?.components?.read("_pkg/ui/#{id}.rip")
107
107
  return unless src
108
108
  sourceCode = src
109
109
  _closeSource: -> sourceCode = null
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "3.16.0",
3
+ "version": "3.16.1",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
7
7
  "workspaces": [
8
+ "examples/*",
8
9
  "packages/*"
9
10
  ],
11
+ "catalog": {
12
+ "typescript": "5.9.3"
13
+ },
10
14
  "browser": "docs/dist/rip.min.js",
11
15
  "exports": {
12
16
  ".": {
@@ -35,8 +39,8 @@
35
39
  "bump": "bun scripts/bump.js",
36
40
  "gen:dom": "bun scripts/gen-dom.js",
37
41
  "gallery": "bun scripts/gallery.js",
38
- "bundle:demo": "bun scripts/bundle-app.js docs/demo -o docs/example/index.json -t 'Rip App Demo'",
39
- "bundle:ui": "bun scripts/bundle-app.js packages/ui/browser -o docs/ui/bundle.json -t 'Rip UI'",
42
+ "bundle:demo": "bun scripts/bundle-app.js docs/demo/routes --prefix _route --css docs/demo/css -o docs/example/index.json -t 'Rip App Demo'",
43
+ "bundle:ui": "bun scripts/bundle-app.js packages/ui/browser/components --prefix _pkg/ui -o docs/ui/bundle.json -t 'Rip UI'",
40
44
  "parser": "bun src/grammar/solar.rip -o src/parser.js src/grammar/grammar.rip",
41
45
  "postinstall": "node scripts/postinstall.js --quiet",
42
46
  "link-local": "bun scripts/link-local.js",
@@ -87,6 +91,7 @@
87
91
  "author": "Steve Shreeve <steve.shreeve@gmail.com>",
88
92
  "license": "MIT",
89
93
  "devDependencies": {
90
- "typescript": "5.9.3"
94
+ "@types/bun": "1.3.14",
95
+ "typescript": "catalog:"
91
96
  }
92
97
  }
package/rip-loader.js CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { plugin } from "bun";
4
4
  import { fileURLToPath } from "url";
5
+ import { dirname, resolve as resolvePath } from "path";
6
+ import { readFileSync, existsSync } from "fs";
5
7
  import { compileToJS, formatError } from "./src/compiler.js";
6
8
  // Register the full schema runtime provider so .rip files containing
7
9
  // `schema :model` blocks compile correctly inside spawned workers.
@@ -9,11 +11,65 @@ import { compileToJS, formatError } from "./src/compiler.js";
9
11
  // would call compileToJS without ever registering a provider.
10
12
  import "./src/schema/loader-server.js";
11
13
 
14
+ // ── Undeclared-import diagnostic ────────────────────────────────────────
15
+ // Walk up from an importer to its nearest package.json, then verify that any
16
+ // `@rip-lang/<pkg>` specifier is declared in dependencies/devDependencies/
17
+ // peerDependencies/optionalDependencies (or is the package's own self-import).
18
+ //
19
+ // Throws a clear error before `import.meta.resolve` is even attempted, so
20
+ // "works on my machine" failures rooted in link-global rescue surface loudly
21
+ // instead of silently shipping.
22
+ const declarationCache = new Map(); // importerDir → { pkgName, declared } | null
23
+
24
+ function getDeclarationInfo(importerPath) {
25
+ const start = dirname(importerPath);
26
+ if (declarationCache.has(start)) return declarationCache.get(start);
27
+ let cur = start;
28
+ let info = null;
29
+ while (true) {
30
+ const pkgPath = resolvePath(cur, 'package.json');
31
+ if (existsSync(pkgPath)) {
32
+ try {
33
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
34
+ info = {
35
+ pkgName: pkg.name || null,
36
+ declared: new Set([
37
+ ...Object.keys(pkg.dependencies || {}),
38
+ ...Object.keys(pkg.devDependencies || {}),
39
+ ...Object.keys(pkg.peerDependencies || {}),
40
+ ...Object.keys(pkg.optionalDependencies || {}),
41
+ ]),
42
+ };
43
+ } catch {
44
+ info = { pkgName: null, declared: new Set() };
45
+ }
46
+ break;
47
+ }
48
+ const parent = dirname(cur);
49
+ if (parent === cur) break;
50
+ cur = parent;
51
+ }
52
+ declarationCache.set(start, info);
53
+ return info;
54
+ }
55
+
56
+ export function assertDeclaredRipImport(importerPath, specifier) {
57
+ const m = specifier.match(/^(@rip-lang\/[^\/]+)/);
58
+ if (!m) return;
59
+ const pkgKey = m[1];
60
+ const info = getDeclarationInfo(importerPath);
61
+ if (!info) return; // ad-hoc script outside any package — don't block
62
+ if (info.pkgName === pkgKey) return; // self-import
63
+ if (info.declared.has(pkgKey)) return;
64
+ throw new Error(
65
+ `Import of '${pkgKey}' is not declared in package.json. ` +
66
+ `Run \`bun add ${pkgKey}\` (or use \`workspace:*\` inside this monorepo).`
67
+ );
68
+ }
69
+
12
70
  await plugin({
13
71
  name: "rip-loader",
14
72
  async setup(build) {
15
- const { readFileSync } = await import("fs");
16
-
17
73
  // Handle .rip files
18
74
  build.onLoad({ filter: /\.rip$/ }, async (args) => {
19
75
  try {
@@ -25,6 +81,7 @@ await plugin({
25
81
  // is broken in plugin handlers — so we use import.meta.resolve, which
26
82
  // resolves from this file's location (inside the global node_modules tree).
27
83
  js = js.replace(/(from\s+|import\s*\()(['"])(@rip-lang\/[^'"]+)\2/g, (match, prefix, quote, specifier) => {
84
+ assertDeclaredRipImport(args.path, specifier);
28
85
  try {
29
86
  return `${prefix}${quote}${fileURLToPath(import.meta.resolve(specifier))}${quote}`;
30
87
  } catch {
package/src/AGENTS.md CHANGED
@@ -241,15 +241,15 @@ Complete node reference:
241
241
  Tokens are `[tag, val]` arrays with extra properties:
242
242
 
243
243
  - `.pre` — whitespace count before token
244
- - `.data` — metadata like `{ await, predicate, quote, invert, parsedValue }`
244
+ - `.data` — metadata like `{ await, optional, quote, invert, parsedValue }`
245
245
  - `.loc` — `{ r, c, n }`
246
246
  - `.spaced` — sugar for `.pre > 0`
247
247
  - `.newLine` — whether preceded by newline
248
248
 
249
249
  Identifier suffixes:
250
250
 
251
- - `!` sets `.data.await = true`
252
- - `?` sets `.data.predicate = true`
251
+ - `!` sets `.data.bang = true` — a neutral "trailing `!`" flag resolved by context downstream: dammit/`await` at a call site (`fetch!` → `await fetch()`), or the void marker at a function definition (`foo! = ->`, `def foo!` → no implicit return). Void-ness is stamped onto the function node as `isVoid` by `applyVoidMarker` and read locally by the arrow emitters; `def` reads `meta(name, 'bang')` directly.
252
+ - `?` sets `.data.optional = true` (existence check on values; optional marker on prop/type-field names)
253
253
  - `as!` in loops emits `FORASAWAIT` for `for await`
254
254
 
255
255
  Tagged template bridge:
@@ -415,7 +415,7 @@ Block factories need locals and `ctx.member` references instead of `this._elN` a
415
415
  - `_factoryVars` — variables that need local `let` declarations
416
416
  - `_fragChildren` — fragment-to-children tracking for removals
417
417
  - `_pushEffect(body)` — emits `__effect(...)` or `disposers.push(__effect(...))`
418
- - `_loopVarStack` — threads loop variables through nested factories
418
+ - `_loopVarStack` — threads loop variables through nested factories. Each frame is `{ itemVar, indexVar, reactiveSource }`; `reactiveSource` is computed once when the loop is emitted (via `hasReactiveDeps(collection)`) and tells `hasReactiveDeps` to treat direct member access rooted at `itemVar`/`indexVar` (`item.foo`, `item[0]`, `item.a.b`) as reactive. Alias and destructuring forms are not tracked.
419
419
 
420
420
  Factory mode is entered in `emitConditionBranch` and `emitTemplateLoop` via save/restore of `[_createLines, _setupLines, _factoryMode, _factoryVars]`.
421
421
 
@@ -653,7 +653,7 @@ enum Status
653
653
  Type emission is split across two files by execution context:
654
654
 
655
655
  - `types.js` (browser-side, ~21 KB) — `installTypeSupport(Lexer)` adds `rewriteTypes()` to strip type annotations from the token stream so user-typed Rip parses. This is the only thing the browser needs from type machinery.
656
- - `dts.js` (CLI/LSP only, ~38 KB) — `emitTypes(tokens, sexpr, source)` generates `.d.ts`, plus `expandSuffixes`, `emitComponentTypes`, and the intrinsic declaration tables (`INTRINSIC_TYPE_DECLS`, `SIGNAL_*`, `COMPUTED_*`, `EFFECT_*`, etc.). Registers itself with the compiler at module load via `setTypesEmitter()`.
656
+ - `dts.js` (CLI/LSP only, ~38 KB) — `emitTypes(tokens, sexpr, source)` generates `.d.ts`, plus `tsType`, `emitComponentTypes`, and the intrinsic declaration tables (`INTRINSIC_TYPE_DECLS`, `SIGNAL_*`, `COMPUTED_*`, `EFFECT_*`, etc.). Registers itself with the compiler at module load via `setTypesEmitter()`.
657
657
 
658
658
  `emitEnum` (runtime JS for `enum` blocks) lives in `compiler.js` next to the rest of the codegen dispatch — it's not type machinery, it's real runtime emission.
659
659
 
package/src/browser.js CHANGED
@@ -48,6 +48,40 @@ const dedent = s => {
48
48
  const sanitizeSourceURL = (url) =>
49
49
  String(url).replace(/[\r\n]/g, '').replace(/\s+$/g, '');
50
50
 
51
+ // Rewrite `import { … } from '@rip-lang/<pkg>'` into a `globalThis`
52
+ // destructure. The browser bundle copies every function export from
53
+ // `@rip-lang/app` (and friends) onto `globalThis` at startup (see
54
+ // `_entry.js` in scripts/build.js), so consumers can import them by name
55
+ // in source while the runtime form is a plain destructure. Named imports
56
+ // only — default / namespace forms warn and pass through.
57
+ function rewriteRipPkgImports(js) {
58
+ const re = /^(\s*)import\s+([^'"]+?)\s+from\s+['"](@rip-lang\/[^'"]+)['"];?\s*$/gm;
59
+ return js.replace(re, (full, indent, clause, spec) => {
60
+ const trimmed = clause.trim();
61
+ if (trimmed.startsWith('type ')) return `${indent}// type-only import erased: ${spec}`;
62
+ const open = trimmed.indexOf('{');
63
+ const close = trimmed.lastIndexOf('}');
64
+ if (open < 0 || close <= open) {
65
+ console.warn(`[Rip] Skipping non-named import from ${spec}; only \`import { … } from '@rip-lang/*'\` is supported in browser bundles.`);
66
+ return full;
67
+ }
68
+ const inside = trimmed.slice(open + 1, close);
69
+ const parts = [];
70
+ for (let raw of inside.split(',')) {
71
+ let name = raw.trim().replace(/^type\s+/, '');
72
+ if (!name) continue;
73
+ if (/\s+as\s+/.test(name)) {
74
+ const [orig, alias] = name.split(/\s+as\s+/).map(s => s.trim());
75
+ parts.push(`${orig}: ${alias}`);
76
+ } else {
77
+ parts.push(name);
78
+ }
79
+ }
80
+ if (parts.length === 0) return `${indent}// import erased: ${spec}`;
81
+ return `${indent}const { ${parts.join(', ')} } = globalThis;`;
82
+ });
83
+ }
84
+
51
85
  // Insert `//# sourceURL=<name>` BEFORE the existing `//# sourceMappingURL=...`
52
86
  // comment (or append at end if none). NEVER prepend — that would shift every
53
87
  // generated-line mapping by 1 line, breaking line-only source maps.
@@ -127,15 +161,17 @@ async function processRipScripts() {
127
161
  let lastBundle;
128
162
 
129
163
  // Step 1: Collect data-src URLs from the runtime script tag
130
- // When data-src is omitted, default to '/app' (auto-scanned bundle from serve middleware).
164
+ // When data-src is omitted, default to '/app' (auto-scanned bundle from
165
+ // serve middleware). The default is silent on failure — only explicit
166
+ // data-src URLs warn — so static / file:// pages aren't noisy.
131
167
  const runtimeTag = document.querySelector('script[src$="rip.min.js"], script[src$="rip.js"]');
132
168
  const dataSrc = runtimeTag?.getAttribute('data-src');
133
169
  if (dataSrc !== null && dataSrc !== undefined) {
134
170
  for (const url of dataSrc.trim().split(/\s+/)) {
135
171
  if (url) sources.push({ url });
136
172
  }
137
- } else if (runtimeTag) {
138
- sources.push({ url: '/app' });
173
+ } else if (runtimeTag && /^https?:$/.test(location.protocol)) {
174
+ sources.push({ url: '/app', optional: true });
139
175
  }
140
176
 
141
177
  // Step 2: Collect all <script type="text/rip"> tags (inline and external)
@@ -161,8 +197,11 @@ async function processRipScripts() {
161
197
  s.bundle = bundle;
162
198
  }
163
199
  }));
164
- for (const r of results) {
165
- if (r.status === 'rejected') console.warn('Rip: fetch failed:', r.reason.message);
200
+ for (let i = 0; i < results.length; i++) {
201
+ const r = results[i];
202
+ if (r.status === 'rejected' && !sources[i].optional) {
203
+ console.warn('Rip: fetch failed:', r.reason.message);
204
+ }
166
205
  }
167
206
 
168
207
  // Separate bundles from individual sources
@@ -209,7 +248,7 @@ async function processRipScripts() {
209
248
  ? { ...baseOpts, sourceMap: 'inline', filename: ripName }
210
249
  : baseOpts;
211
250
  let js;
212
- try { js = compileToJS(s.code, opts); }
251
+ try { js = rewriteRipPkgImports(compileToJS(s.code, opts)); }
213
252
  catch (e) { console.error(_formatError(e, { source: s.code, file: ripName, color: false })); continue; }
214
253
  try { await (0, eval)(debug ? wrapForEval(js, ripName) : `(async()=>{\n${js}\n})()`); }
215
254
  catch (e) { console.error(`Rip runtime error in ${ripName}:`, e); }
@@ -227,7 +266,8 @@ async function processRipScripts() {
227
266
  // No routing — expand bundles into individual sources, compile everything
228
267
  const expanded = [];
229
268
  for (const b of bundles) {
230
- for (const [name, code] of Object.entries(b.components || {})) {
269
+ const mods = b.modules || b.components || {};
270
+ for (const [name, code] of Object.entries(mods)) {
231
271
  expanded.push({ code, url: name });
232
272
  }
233
273
  if (b.data) {
@@ -247,7 +287,8 @@ async function processRipScripts() {
247
287
  if (bundles.length > 0 && typeof globalThis.createComponents === 'function') {
248
288
  const sourceStore = globalThis.createComponents();
249
289
  for (const b of bundles) {
250
- if (b.components) sourceStore.load(b.components);
290
+ const mods = b.modules || b.components;
291
+ if (mods) sourceStore.load(mods);
251
292
  }
252
293
  if (typeof window !== 'undefined') {
253
294
  if (!window.__RIP__) window.__RIP__ = {};
@@ -280,7 +321,7 @@ async function processRipScripts() {
280
321
  ? { ...baseOpts, sourceMap: 'inline', filename: ripName }
281
322
  : baseOpts;
282
323
  try {
283
- const js = compileToJS(s.code, opts);
324
+ const js = rewriteRipPkgImports(compileToJS(s.code, opts));
284
325
  compiled.push({ js, url: ripName });
285
326
  } catch (e) {
286
327
  console.error(_formatError(e, { source: s.code, file: ripName, color: false }));
@@ -289,7 +330,7 @@ async function processRipScripts() {
289
330
 
290
331
  // Create app stash
291
332
  if (!globalThis.__ripApp && runtimeTag) {
292
- const stashFn = globalThis.stash;
333
+ const stashFn = globalThis.createStash;
293
334
  if (stashFn) {
294
335
  let initial = {};
295
336
  const stateAttr = runtimeTag.getAttribute('data-state');
@@ -414,7 +455,7 @@ export async function importRip(url) {
414
455
  if (!r.ok) throw new Error(`importRip: ${url} (${r.status})`);
415
456
  return r.text();
416
457
  });
417
- const js = compileToJS(source);
458
+ const js = rewriteRipPkgImports(compileToJS(source));
418
459
  const header = `// ${url}\n`;
419
460
  const blob = new Blob([header + js], { type: 'application/javascript' });
420
461
  const blobUrl = URL.createObjectURL(blob);