From 5f2648d112eeb2c469287ecab909f771a221c2a0 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 15 Dec 2025 23:18:52 -0300 Subject: [PATCH 1/7] FEATURE: Update React skeleton to use React Router We long discussed having a more modernized base React skeleton that uses a router and has _some_ basic navigation and styling. Check below a video and some images of the new React skeleton in action. --- .../static-assets/skel-react/client/main.jsx | 6 +- .../skel-react/imports/ui/About.jsx | 16 +++ .../skel-react/imports/ui/App.jsx | 9 +- .../imports/ui/{Hello.jsx => Counter.jsx} | 6 +- .../skel-react/imports/ui/Header.jsx | 11 ++ .../skel-react/imports/ui/Info.jsx | 9 +- .../skel-react/imports/ui/Routes.jsx | 19 +++ .../skel-react/imports/ui/styles.css | 117 ++++++++++++++++++ tools/static-assets/skel-react/package.json | 3 +- tools/static-assets/skel-react/server/main.js | 7 ++ 10 files changed, 189 insertions(+), 14 deletions(-) create mode 100644 tools/static-assets/skel-react/imports/ui/About.jsx rename tools/static-assets/skel-react/imports/ui/{Hello.jsx => Counter.jsx} (63%) create mode 100644 tools/static-assets/skel-react/imports/ui/Header.jsx create mode 100644 tools/static-assets/skel-react/imports/ui/Routes.jsx create mode 100644 tools/static-assets/skel-react/imports/ui/styles.css diff --git a/tools/static-assets/skel-react/client/main.jsx b/tools/static-assets/skel-react/client/main.jsx index c7e0c1f05e..94a5f1be0f 100644 --- a/tools/static-assets/skel-react/client/main.jsx +++ b/tools/static-assets/skel-react/client/main.jsx @@ -1,9 +1,11 @@ import { createRoot } from 'react-dom/client'; import { Meteor } from 'meteor/meteor'; -import { App } from '/imports/ui/App'; +import { RouterProvider } from "react-router/dom"; +import { Routes } from '/imports/ui/Routes.jsx'; +import '/imports/ui/styles.css'; Meteor.startup(() => { const container = document.getElementById('react-target'); const root = createRoot(container); - root.render(); + root.render(); }); diff --git a/tools/static-assets/skel-react/imports/ui/About.jsx b/tools/static-assets/skel-react/imports/ui/About.jsx new file mode 100644 index 0000000000..63b5d77ff8 --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/About.jsx @@ -0,0 +1,16 @@ +import { useLoaderData } from "react-router"; +import { NavLink } from "react-router"; + +export const About = () => { + const { about } = useLoaderData(); + return ( +
+ Home +
+

About This Application

+ Reload this page +
+

{about}

+
+ ); +} \ No newline at end of file diff --git a/tools/static-assets/skel-react/imports/ui/App.jsx b/tools/static-assets/skel-react/imports/ui/App.jsx index eeeee2161a..bbd4ef5ba2 100644 --- a/tools/static-assets/skel-react/imports/ui/App.jsx +++ b/tools/static-assets/skel-react/imports/ui/App.jsx @@ -1,10 +1,11 @@ -import { Hello } from './Hello.jsx'; +import { Counter } from './Counter.jsx'; import { Info } from './Info.jsx'; +import { Header } from './Header.jsx'; export const App = () => ( -
-

Welcome to Meteor!

- +
+
+
); diff --git a/tools/static-assets/skel-react/imports/ui/Hello.jsx b/tools/static-assets/skel-react/imports/ui/Counter.jsx similarity index 63% rename from tools/static-assets/skel-react/imports/ui/Hello.jsx rename to tools/static-assets/skel-react/imports/ui/Counter.jsx index 527d5af607..469ad65404 100644 --- a/tools/static-assets/skel-react/imports/ui/Hello.jsx +++ b/tools/static-assets/skel-react/imports/ui/Counter.jsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -export const Hello = () => { +export const Counter = () => { const [counter, setCounter] = useState(0); const increment = () => { @@ -8,8 +8,8 @@ export const Hello = () => { }; return ( -
- +
+

You've pressed the button {counter} times.

); diff --git a/tools/static-assets/skel-react/imports/ui/Header.jsx b/tools/static-assets/skel-react/imports/ui/Header.jsx new file mode 100644 index 0000000000..28feebc312 --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/Header.jsx @@ -0,0 +1,11 @@ +import { NavLink } from "react-router"; + +export const Header = () => { + return ( +
+ +

Welcome to Meteor!

+ About Page +
+ ) +} \ No newline at end of file diff --git a/tools/static-assets/skel-react/imports/ui/Info.jsx b/tools/static-assets/skel-react/imports/ui/Info.jsx index 7154a4776d..ea0b0fb4ad 100644 --- a/tools/static-assets/skel-react/imports/ui/Info.jsx +++ b/tools/static-assets/skel-react/imports/ui/Info.jsx @@ -5,16 +5,17 @@ export const Info = () => { const isLoading = useSubscribe('links'); const links = useFind(() => LinksCollection.find()); - if(isLoading()) { + if (isLoading()) { return
Loading...
; } return (
+

Learn Meteor!

-
diff --git a/tools/static-assets/skel-react/imports/ui/Routes.jsx b/tools/static-assets/skel-react/imports/ui/Routes.jsx new file mode 100644 index 0000000000..9f1d09e82d --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/Routes.jsx @@ -0,0 +1,19 @@ +import { createBrowserRouter } from "react-router"; +import { App } from "./App"; +import { About } from "./About"; + +// https://reactrouter.com/start/data/routing +export const Routes = createBrowserRouter([ + { + path: "/", + element: , + }, + { + path: "/about", + element: , + loader: async () => { + const about = await Meteor.callAsync('about'); + return { about }; + } + } +]); \ No newline at end of file diff --git a/tools/static-assets/skel-react/imports/ui/styles.css b/tools/static-assets/skel-react/imports/ui/styles.css new file mode 100644 index 0000000000..d083a398f2 --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/styles.css @@ -0,0 +1,117 @@ +/* this file is imported in client/main.jsx */ + +:root { + --primary-color: #007bff; + --secondary-color: #6c757d; + --hover-color: #0056b3; + --background-color: #white; + --text-color: #212529; + --selection-bg-color: #339af0; + --selection-text-color: #ffffff; +} + +::selection { + background-color: var(--selection-bg-color); + color: var(--selection-text-color); +} + +.home-page { + background-color: var(--background-color); + padding: 1.5rem; + border-radius: 0.5rem; +} + +.header { + display: flex; + justify-self: center; + font-weight: bold; + width: 100%; + align-items: center; + justify-content: space-around; +} + +.logo { + width: 5rem; + height: 5rem; +} + +.button { + background-color: var(--primary-color); + color: white; + border: none; + padding: 0.5rem 1rem; + border-radius: 0.25rem; + cursor: pointer; +} + +.button:hover { + background-color: var(--hover-color); +} + +.link { + padding: 0; + margin: 0; + display: flex; + align-items: center; +} + +.link:hover { + color: var(--hover-color); +} + +.section { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + margin-top: 1rem; + padding: 1rem; + + background-color: var(--background-color); + border-radius: 0.5rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.links-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 1rem; + list-style-type: none; + padding: 0; + margin: 0; +} + +.links-list .link { + text-decoration: none; + color: var(--text-color); + + font-weight: 500; + padding: 0.5rem; + + border-radius: 0.25rem; + transition: background-color 0.3s ease; + + display: flex; + justify-content: center; + width: 100%; + height: 100%; +} + +.links-list .section { + transition: box-shadow 0.3s ease; + cursor: pointer; +} + +.links-list .section:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +} + +.about-page { + padding: 1.5rem; + background-color: var(--background-color); + border-radius: 0.5rem; +} + +.about-page p { + line-height: 1.6; +} diff --git a/tools/static-assets/skel-react/package.json b/tools/static-assets/skel-react/package.json index f38fe589d3..15b9600965 100644 --- a/tools/static-assets/skel-react/package.json +++ b/tools/static-assets/skel-react/package.json @@ -12,7 +12,8 @@ "@swc/helpers": "^0.5.17", "meteor-node-stubs": "^1.2.12", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-router": "^7.10.1" }, "devDependencies": { "@meteorjs/rspack": "^0.2.54", diff --git a/tools/static-assets/skel-react/server/main.js b/tools/static-assets/skel-react/server/main.js index 49452ad352..3519e3aa83 100644 --- a/tools/static-assets/skel-react/server/main.js +++ b/tools/static-assets/skel-react/server/main.js @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { LinksCollection } from '/imports/api/links'; +import { Random } from 'meteor/random'; async function insertLink({ title, url }) { await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); @@ -35,3 +36,9 @@ Meteor.startup(async () => { return LinksCollection.find(); }); }); + +Meteor.methods({ + about() { + return `This is a Meteor application running React with React Router. this is a generated id: ${Random.id()}`; + } +}) From 5837919fe7bf763f1465e472aeaab5086bcf3cbb Mon Sep 17 00:00:00 2001 From: shanky Date: Tue, 9 Dec 2025 11:29:51 +0530 Subject: [PATCH 2/7] fix: handle Watchman RootResolveError for symlinked packages Fixes #13883 --- tools/fs/safe-watcher.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 07fe605fb7..eb49afbdcd 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -305,9 +305,13 @@ async function ensureWatchRoot(dirPath: string): Promise { if (/Events were dropped/.test(err.message)) { return; } + if (/RootResolveError/.test(err.message) || /failed to resolve root/.test(err.message)) { + console.warn(`Parcel watcher root resolve error on ${osDirPath}, ignoring: ${err.message}`); + ignoredWatchRoots.add(dirPath); + watchRoots.delete(dirPath); + return; + } console.error(`Parcel watcher error on ${osDirPath}:`, err); - // Only disable native watching for critical errors (like ENOSPC). - // @ts-ignore if (err.code === "ENOSPC" || err.errno === require("constants").ENOSPC) { fallbackToPolling(); } @@ -333,9 +337,11 @@ async function ensureWatchRoot(dirPath: string): Promise { (e.code === "ENOTDIR" || /Not a directory/.test(e.message) || e.code === "EBADF" || - /Bad file descriptor/.test(e.message)) + /Bad file descriptor/.test(e.message) || + /RootResolveError/.test(e.message) || + /failed to resolve root/.test(e.message)) ) { - console.warn(`Skipping watcher for ${osDirPath}: not a directory`); + console.warn(`Skipping watcher for ${osDirPath}: ${e.message || 'not watchable'}`); ignoredWatchRoots.add(dirPath); } else { console.error(`Failed to start watcher for ${osDirPath}:`, e); From b3bea1438c5f8e5168cc39e93406aae74ecf20d9 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Tue, 16 Dec 2025 20:22:34 -0300 Subject: [PATCH 3/7] DEV: use logo as svg, add svgr/webpack and use `""` --- .../skel-react/imports/ui/About.jsx | 2 +- .../skel-react/imports/ui/App.jsx | 12 +++--- .../skel-react/imports/ui/Counter.jsx | 6 ++- .../skel-react/imports/ui/Header.jsx | 11 ++++-- .../skel-react/imports/ui/Info.jsx | 21 +++++----- .../skel-react/imports/ui/Routes.jsx | 10 ++--- .../skel-react/imports/ui/meteor-logo.svg | 19 ++++++++++ tools/static-assets/skel-react/package.json | 1 + .../static-assets/skel-react/rspack.config.js | 17 +++++++-- tools/static-assets/skel-react/server/main.js | 38 ++++++++++++------- 10 files changed, 93 insertions(+), 44 deletions(-) create mode 100644 tools/static-assets/skel-react/imports/ui/meteor-logo.svg diff --git a/tools/static-assets/skel-react/imports/ui/About.jsx b/tools/static-assets/skel-react/imports/ui/About.jsx index 63b5d77ff8..6a241e8f70 100644 --- a/tools/static-assets/skel-react/imports/ui/About.jsx +++ b/tools/static-assets/skel-react/imports/ui/About.jsx @@ -13,4 +13,4 @@ export const About = () => {

{about}

); -} \ No newline at end of file +}; diff --git a/tools/static-assets/skel-react/imports/ui/App.jsx b/tools/static-assets/skel-react/imports/ui/App.jsx index bbd4ef5ba2..2c551d533e 100644 --- a/tools/static-assets/skel-react/imports/ui/App.jsx +++ b/tools/static-assets/skel-react/imports/ui/App.jsx @@ -1,11 +1,11 @@ -import { Counter } from './Counter.jsx'; -import { Info } from './Info.jsx'; -import { Header } from './Header.jsx'; +import { Counter } from "./Counter.jsx"; +import { Header } from "./Header.jsx"; +import { Info } from "./Info.jsx"; export const App = () => (
-
- - +
+ +
); diff --git a/tools/static-assets/skel-react/imports/ui/Counter.jsx b/tools/static-assets/skel-react/imports/ui/Counter.jsx index 469ad65404..c43cf91125 100644 --- a/tools/static-assets/skel-react/imports/ui/Counter.jsx +++ b/tools/static-assets/skel-react/imports/ui/Counter.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState } from "react"; export const Counter = () => { const [counter, setCounter] = useState(0); @@ -9,7 +9,9 @@ export const Counter = () => { return (
- +

You've pressed the button {counter} times.

); diff --git a/tools/static-assets/skel-react/imports/ui/Header.jsx b/tools/static-assets/skel-react/imports/ui/Header.jsx index 28feebc312..7c70356315 100644 --- a/tools/static-assets/skel-react/imports/ui/Header.jsx +++ b/tools/static-assets/skel-react/imports/ui/Header.jsx @@ -1,11 +1,14 @@ import { NavLink } from "react-router"; +import MeteorLogo from "./meteor-logo.svg"; export const Header = () => { return (
- +

Welcome to Meteor!

- About Page + + About Page +
- ) -} \ No newline at end of file + ); +}; diff --git a/tools/static-assets/skel-react/imports/ui/Info.jsx b/tools/static-assets/skel-react/imports/ui/Info.jsx index ea0b0fb4ad..33dc8662b9 100644 --- a/tools/static-assets/skel-react/imports/ui/Info.jsx +++ b/tools/static-assets/skel-react/imports/ui/Info.jsx @@ -1,8 +1,8 @@ -import { useFind, useSubscribe } from 'meteor/react-meteor-data'; -import { LinksCollection } from '../api/links'; +import { useFind, useSubscribe } from "meteor/react-meteor-data"; +import { LinksCollection } from "../api/links"; export const Info = () => { - const isLoading = useSubscribe('links'); + const isLoading = useSubscribe("links"); const links = useFind(() => LinksCollection.find()); if (isLoading()) { @@ -11,13 +11,16 @@ export const Info = () => { return (
-

Learn Meteor!

- +
); }; diff --git a/tools/static-assets/skel-react/imports/ui/Routes.jsx b/tools/static-assets/skel-react/imports/ui/Routes.jsx index 9f1d09e82d..da3fcb5d98 100644 --- a/tools/static-assets/skel-react/imports/ui/Routes.jsx +++ b/tools/static-assets/skel-react/imports/ui/Routes.jsx @@ -1,6 +1,6 @@ import { createBrowserRouter } from "react-router"; -import { App } from "./App"; import { About } from "./About"; +import { App } from "./App"; // https://reactrouter.com/start/data/routing export const Routes = createBrowserRouter([ @@ -12,8 +12,8 @@ export const Routes = createBrowserRouter([ path: "/about", element: , loader: async () => { - const about = await Meteor.callAsync('about'); + const about = await Meteor.callAsync("about"); return { about }; - } - } -]); \ No newline at end of file + }, + }, +]); diff --git a/tools/static-assets/skel-react/imports/ui/meteor-logo.svg b/tools/static-assets/skel-react/imports/ui/meteor-logo.svg new file mode 100644 index 0000000000..09eab6d947 --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/meteor-logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tools/static-assets/skel-react/package.json b/tools/static-assets/skel-react/package.json index 15b9600965..3b612ebddf 100644 --- a/tools/static-assets/skel-react/package.json +++ b/tools/static-assets/skel-react/package.json @@ -21,6 +21,7 @@ "@rspack/cli": "^1.6.5", "@rspack/core": "^1.6.5", "@rspack/plugin-react-refresh": "^1.4.3", + "@svgr/webpack": "^8.1.0", "react-refresh": "^0.17.0" }, "meteor": { diff --git a/tools/static-assets/skel-react/rspack.config.js b/tools/static-assets/skel-react/rspack.config.js index 33f02e3bbe..dc159ce4d2 100644 --- a/tools/static-assets/skel-react/rspack.config.js +++ b/tools/static-assets/skel-react/rspack.config.js @@ -1,4 +1,4 @@ -const { defineConfig } = require('@meteorjs/rspack'); +const { defineConfig } = require("@meteorjs/rspack"); /** * Rspack configuration for Meteor projects. @@ -10,6 +10,17 @@ const { defineConfig } = require('@meteorjs/rspack'); * * Use these flags to adjust your build settings based on environment. */ -module.exports = defineConfig(Meteor => { - return {}; +module.exports = defineConfig((Meteor) => { + return { + module: { + rules: [ + // Add support for importing SVGs as React components + { + test: /\.svg$/i, + issuer: /\.[jt]sx?$/, + use: ["@svgr/webpack"], + }, + ], + }, + }; }); diff --git a/tools/static-assets/skel-react/server/main.js b/tools/static-assets/skel-react/server/main.js index 3519e3aa83..b6adcc3a56 100644 --- a/tools/static-assets/skel-react/server/main.js +++ b/tools/static-assets/skel-react/server/main.js @@ -1,6 +1,6 @@ -import { Meteor } from 'meteor/meteor'; -import { LinksCollection } from '/imports/api/links'; -import { Random } from 'meteor/random'; +import { Meteor } from "meteor/meteor"; +import { LinksCollection } from "/imports/api/links"; +import { Random } from "meteor/random"; async function insertLink({ title, url }) { await LinksCollection.insertAsync({ title, url, createdAt: new Date() }); @@ -8,25 +8,35 @@ async function insertLink({ title, url }) { Meteor.startup(async () => { // If the Links collection is empty, add some data. - if (await LinksCollection.find().countAsync() === 0) { + if ((await LinksCollection.find().countAsync()) === 0) { await insertLink({ - title: 'Do the Tutorial', - url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html', + title: "Do the Tutorial", + url: "https://docs.meteor.com/tutorials/react/", }); await insertLink({ - title: 'Follow the Guide', - url: 'https://guide.meteor.com', + title: "Follow the Guide", + url: "https://docs.meteor.com/tutorials/application-structure/", }); await insertLink({ - title: 'Read the Docs', - url: 'https://docs.meteor.com', + title: "Read the Docs", + url: "https://docs.meteor.com", }); await insertLink({ - title: 'Discussions', - url: 'https://forums.meteor.com', + title: "Discussions", + url: "https://forums.meteor.com", + }); + + await insertLink({ + title: "Join us on Discord", + url: "https://discord.gg/6mS3wHNg", + }); + + await insertLink({ + title: "Deploying in Galaxy", + url: "https://www.meteor.com/hosting", }); } @@ -40,5 +50,5 @@ Meteor.startup(async () => { Meteor.methods({ about() { return `This is a Meteor application running React with React Router. this is a generated id: ${Random.id()}`; - } -}) + }, +}); From 0179e82b9e0b1e1590f7d29c47cf16a8b1ad0134 Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Wed, 17 Dec 2025 20:27:59 +0100 Subject: [PATCH 4/7] Icon for IntelliJ IDEs --- .gitignore | 1 + .idea/icon.svg | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100755 .idea/icon.svg diff --git a/.gitignore b/.gitignore index 4742e13056..36de1287c6 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ node_modules \#*\# .\#* .idea +!.idea/icon.svg *.iml *.sublime-project *.sublime-workspace diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100755 index 0000000000..16ecae9b10 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,21 @@ + + + + + image/svg+xml + + + + + + + + + + + + + + + + \ No newline at end of file From 2a6d28d44cdcbe0818c2972fb62c914c7226afeb Mon Sep 17 00:00:00 2001 From: Jan Dvorak Date: Wed, 17 Dec 2025 21:28:38 +0100 Subject: [PATCH 5/7] Do not throw error on forgotPassword when ambiguous message set #11336 --- packages/accounts-password/password_server.js | 1 + packages/accounts-password/password_tests.js | 5 +- packages/tinytest/README.md | 10 ++- packages/tinytest/tinytest.js | 70 +++++++++++++++++-- 4 files changed, 78 insertions(+), 8 deletions(-) diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index 151f9c8d5f..60f68f14b5 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -513,6 +513,7 @@ Meteor.methods({forgotPassword: async options => { const user = await Accounts.findUserByEmail(options.email, { fields: { emails: 1 } }); if (!user) { + if (Accounts._options.ambiguousErrorMessages) return; Accounts._handleError("User not found"); } diff --git a/packages/accounts-password/password_tests.js b/packages/accounts-password/password_tests.js index 49f94544a0..9bdd79381a 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1415,9 +1415,8 @@ if (Meteor.isServer) (() => { ); Accounts._options.ambiguousErrorMessages = true; - await test.throwsAsync( - async () => await Meteor.callAsync('forgotPassword', wrongOptions), - 'Something went wrong. Please check your credentials' + await test.doesNotThrowsAsync( + async () => await Meteor.callAsync("forgotPassword", wrongOptions) ); Accounts._options.ambiguousErrorMessages = false; diff --git a/packages/tinytest/README.md b/packages/tinytest/README.md index d93c5f1222..514ae883a7 100644 --- a/packages/tinytest/README.md +++ b/packages/tinytest/README.md @@ -272,7 +272,13 @@ EXPERIMENTAL way to compare two strings that results in a nicer display in the t ### Assertions without optional fail messages -`test.throws(func, expected);` +`test.throws(func, expected[, message]);` + +`test.throwsAsync(func, expected[, message]);` + +`test.doesNotThrows(func[, failureMessage]);` + +`test.doesNotThrowsAsync(func[, failureMessage]);` `expected` can be: @@ -281,6 +287,8 @@ EXPERIMENTAL way to compare two strings that results in a nicer display in the t - `regexp`: pass if the exception message passes the regexp. - `function`: call the function as a predicate with the exception. +`doesNotThrows` and `doesNotThrowsAsync` assert that the function does not throw. If the function throws, the assertion fails. The optional `failureMessage` is only used to annotate the failure. + Note: Node's `assert.throws` also accepts a constructor to test whether the error is of the expected class. But since JavaScript can't distinguish between constructors and plain functions and Node's `assert.throws` also accepts a predicate function, if the error fails the `instanceof` test with the constructor then the constructor is then treated as a predicate and called (!) The upshot is, if you want to test whether an error is of a particular class, use a predicate function. diff --git a/packages/tinytest/tinytest.js b/packages/tinytest/tinytest.js index b66097aafb..f8c29bab74 100644 --- a/packages/tinytest/tinytest.js +++ b/packages/tinytest/tinytest.js @@ -244,6 +244,19 @@ export class TestCaseResults { // The upshot is, if you want to test whether an error is of a // particular class, use a predicate function. // + /** + * Assert that `f` throws. + * + * `expected` can be: + * - undefined: accept any exception. + * - string: pass if the string is a substring of the exception message. + * - regexp: pass if the exception message passes the regexp. + * - function: call the function as a predicate with the exception. + * + * @param {Function} f + * @param {*} expected + * @param {String} message + */ throws(f, expected, message) { let actual; const predicate = this._guessPredicate(expected); @@ -258,10 +271,34 @@ export class TestCaseResults { } /** - * Same as throw, but accepts an async function as a parameter. - * @param f - * @param expected - * @param message + * Assert that `f` does not throw. + * @param {Function} f + * @param {String} failureMessage + */ + doesNotThrows(f, failureMessage) { + let actual; + + try { + f(); + } catch (exception) { + actual = exception; + } + + if (!actual) { + this.ok(); + } else { + this.fail({ + type: "throws", + message: ("threw an error unexpectedly: " + actual.message) + (failureMessage ? ": " + failureMessage : ""), + }); + } + } + + /** + * Same as `throws`, but accepts an async function as a parameter. + * @param {Function} f + * @param {*} expected + * @param {String} message * @returns {Promise} */ async throwsAsync(f, expected, message) { @@ -276,6 +313,31 @@ export class TestCaseResults { this._assertActual(actual, predicate, message); } + /** + * Same as `doesNotThrows`, but accepts an async function as a parameter. + * @param {Function} f + * @param {String} failureMessage + * @returns {Promise} + */ + async doesNotThrowsAsync(f, failureMessage) { + let actual; + + try { + await f(); + } catch (exception) { + actual = exception; + } + + if (!actual) { + this.ok(); + } else { + this.fail({ + type: "throws", + message: ("threw an error unexpectedly: " + actual.message) + (failureMessage ? ": " + failureMessage : ""), + }); + } + } + isTrue(v, msg) { if (v) this.ok(); From d51f723b87cf2e9fe193eb06d8282a6714ee475f Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Fri, 19 Dec 2025 10:59:36 -0300 Subject: [PATCH 6/7] UX: update styling for skeleton --- .../skel-react/imports/ui/App.jsx | 8 +- .../skel-react/imports/ui/Counter.jsx | 16 +- .../skel-react/imports/ui/Header.jsx | 12 +- .../skel-react/imports/ui/Info.jsx | 16 +- .../skel-react/imports/ui/meteor-logo.svg | 7 +- .../skel-react/imports/ui/styles.css | 328 ++++++++++++++---- 6 files changed, 293 insertions(+), 94 deletions(-) diff --git a/tools/static-assets/skel-react/imports/ui/App.jsx b/tools/static-assets/skel-react/imports/ui/App.jsx index 2c551d533e..6b12ade912 100644 --- a/tools/static-assets/skel-react/imports/ui/App.jsx +++ b/tools/static-assets/skel-react/imports/ui/App.jsx @@ -3,9 +3,11 @@ import { Header } from "./Header.jsx"; import { Info } from "./Info.jsx"; export const App = () => ( -
+
- - +
+ + +
); diff --git a/tools/static-assets/skel-react/imports/ui/Counter.jsx b/tools/static-assets/skel-react/imports/ui/Counter.jsx index c43cf91125..7fcb4d8404 100644 --- a/tools/static-assets/skel-react/imports/ui/Counter.jsx +++ b/tools/static-assets/skel-react/imports/ui/Counter.jsx @@ -8,11 +8,17 @@ export const Counter = () => { }; return ( -
- -

You've pressed the button {counter} times.

+
+
+ +

+ You've pressed the button{" "} + {counter}{" "} + {counter === 1 ? "time" : "times"}. +

+
); }; diff --git a/tools/static-assets/skel-react/imports/ui/Header.jsx b/tools/static-assets/skel-react/imports/ui/Header.jsx index 7c70356315..9024b823c5 100644 --- a/tools/static-assets/skel-react/imports/ui/Header.jsx +++ b/tools/static-assets/skel-react/imports/ui/Header.jsx @@ -1,14 +1,14 @@ -import { NavLink } from "react-router"; import MeteorLogo from "./meteor-logo.svg"; export const Header = () => { return (
- -

Welcome to Meteor!

- - About Page - +
); }; diff --git a/tools/static-assets/skel-react/imports/ui/Info.jsx b/tools/static-assets/skel-react/imports/ui/Info.jsx index 33dc8662b9..823309d4d1 100644 --- a/tools/static-assets/skel-react/imports/ui/Info.jsx +++ b/tools/static-assets/skel-react/imports/ui/Info.jsx @@ -10,17 +10,21 @@ export const Info = () => { } return ( -
-

Learn Meteor!

-
+ ); }; diff --git a/tools/static-assets/skel-react/imports/ui/meteor-logo.svg b/tools/static-assets/skel-react/imports/ui/meteor-logo.svg index 09eab6d947..3610e0e026 100644 --- a/tools/static-assets/skel-react/imports/ui/meteor-logo.svg +++ b/tools/static-assets/skel-react/imports/ui/meteor-logo.svg @@ -1,12 +1,11 @@ - - + - + diff --git a/tools/static-assets/skel-react/imports/ui/styles.css b/tools/static-assets/skel-react/imports/ui/styles.css index d083a398f2..4b14c0d5ad 100644 --- a/tools/static-assets/skel-react/imports/ui/styles.css +++ b/tools/static-assets/skel-react/imports/ui/styles.css @@ -1,117 +1,305 @@ /* this file is imported in client/main.jsx */ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); :root { - --primary-color: #007bff; - --secondary-color: #6c757d; - --hover-color: #0056b3; - --background-color: #white; - --text-color: #212529; - --selection-bg-color: #339af0; - --selection-text-color: #ffffff; + /* Colors */ + --color-background: hsl(210, 20%, 98%); + --color-foreground: hsl(220, 20%, 15%); + --color-card: hsl(0, 0%, 100%); + --color-primary: hsl(4, 70%, 55%); + --color-primary-hover: hsl(4, 70%, 45%); + --color-muted: hsl(220, 10%, 50%); + --color-border: hsl(220, 14%, 90%); + + /* Shadows */ + --shadow-card: 0 1px 3px 0 hsl(220 20% 15% / 0.04), + 0 1px 2px -1px hsl(220 20% 15% / 0.04); + --shadow-card-hover: 0 10px 15px -3px hsl(220 20% 15% / 0.08), + 0 4px 6px -4px hsl(220 20% 15% / 0.04); + + /* Spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + --spacing-xl: 2rem; + --spacing-2xl: 3rem; + + /* Border radius */ + --radius: 0.75rem; + --radius-sm: 0.5rem; + + /* Transitions */ + --transition-fast: 150ms ease; + --transition-normal: 200ms ease; + --transition-slow: 250ms ease; } -::selection { - background-color: var(--selection-bg-color); - color: var(--selection-text-color); +/* ============ Reset & Base Styles ============ */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; } -.home-page { - background-color: var(--background-color); - padding: 1.5rem; - border-radius: 0.5rem; +body { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + sans-serif; + background-color: var(--color-background); + color: var(--color-foreground); + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -.header { - display: flex; - justify-self: center; - font-weight: bold; +a { + text-decoration: none; + color: inherit; +} + +/* ============ Layout ============ */ +.page { + min-height: 100vh; + background-color: var(--color-background); +} + +.container { width: 100%; + max-width: 1280px; + margin: 0 auto; + padding-left: var(--spacing-md); + padding-right: var(--spacing-md); +} + +@media (min-width: 768px) { + .container { + padding-left: var(--spacing-lg); + padding-right: var(--spacing-lg); + } +} + +/* ============ Header / Navigation ============ */ +.header { + border-bottom: 1px solid var(--color-border); + background-color: var(--color-card); + border-radius: var(--radius); +} + +.nav { + display: flex; align-items: center; - justify-content: space-around; + justify-content: space-between; + height: 4rem; +} + +.logo-container { + display: flex; + align-items: center; + gap: var(--spacing-sm); } .logo { - width: 5rem; - height: 5rem; + width: 4rem; + height: 4rem; } +.logo-text { + font-weight: 600; + color: var(--color-foreground); + display: none; +} + +@media (min-width: 640px) { + .logo-text { + display: inline; + } +} + +.page-title { + font-size: 1.25rem; + font-weight: 700; + color: var(--color-foreground); + letter-spacing: -0.025em; +} + +@media (min-width: 768px) { + .page-title { + font-size: 1.5rem; + } +} + +.nav-link { + font-size: 0.875rem; + font-weight: 500; + color: var(--color-primary); + transition: opacity var(--transition-fast); +} + +.nav-link:hover { + opacity: 0.8; +} + +/* ============ Main Content ============ */ +.main { + padding-top: var(--spacing-xl); + padding-bottom: var(--spacing-xl); +} + +@media (min-width: 768px) { + .main { + padding-top: var(--spacing-2xl); + padding-bottom: var(--spacing-2xl); + } +} + +/* ============ Card Component ============ */ +.card { + background-color: var(--color-card); + border-radius: var(--radius); + box-shadow: var(--shadow-card); + border: 1px solid var(--color-border); +} + +/* ============ Counter Section ============ */ +.counter-card { + padding: var(--spacing-lg); + margin-bottom: 2.5rem; +} + +@media (min-width: 768px) { + .counter-card { + padding: var(--spacing-xl); + margin-bottom: var(--spacing-2xl); + } +} + +.counter-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--spacing-md); +} + +@media (min-width: 640px) { + .counter-content { + flex-direction: row; + } +} + +.counter-text { + color: var(--color-muted); + text-align: center; +} + +@media (min-width: 640px) { + .counter-text { + text-align: left; + } +} + +.counter-value { + font-weight: 600; + color: var(--color-foreground); +} + +/* ============ Button Component ============ */ .button { - background-color: var(--primary-color); + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 120px; + padding: 0.625rem 1.5rem; + font-size: 0.875rem; + font-weight: 500; + font-family: inherit; color: white; + background-color: var(--color-primary); border: none; - padding: 0.5rem 1rem; - border-radius: 0.25rem; + border-radius: var(--radius-sm); cursor: pointer; + transition: background-color var(--transition-normal), + transform var(--transition-fast); } .button:hover { - background-color: var(--hover-color); + background-color: var(--color-primary-hover); } -.link { - padding: 0; - margin: 0; - display: flex; - align-items: center; +.button:active { + transform: scale(0.98); } -.link:hover { - color: var(--hover-color); +.button:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; } -.section { - display: flex; - align-items: center; - justify-content: center; - gap: 1rem; - margin-top: 1rem; - padding: 1rem; - - background-color: var(--background-color); - border-radius: 0.5rem; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +/* ============ Resources Section ============ */ +.section-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-foreground); + margin-bottom: var(--spacing-lg); + letter-spacing: -0.025em; } -.links-list { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); - gap: 1rem; +.resources-grid { list-style-type: none; - padding: 0; - margin: 0; + display: grid; + grid-template-columns: 1fr; + gap: var(--spacing-md); } -.links-list .link { - text-decoration: none; - color: var(--text-color); +@media (min-width: 640px) { + .resources-grid { + grid-template-columns: repeat(2, 1fr); + } +} - font-weight: 500; - padding: 0.5rem; +/* ============ Resource Card ============ */ +.resource-link { + display: block; +} - border-radius: 0.25rem; - transition: background-color 0.3s ease; +.resource-card { + padding: 1.25rem; + transition: box-shadow var(--transition-slow), + transform var(--transition-slow); +} +.resource-card:hover { + box-shadow: var(--shadow-card-hover); + transform: translateY(-2px); +} + +.resource-content { display: flex; - justify-content: center; - width: 100%; - height: 100%; + align-items: center; + justify-content: space-between; } -.links-list .section { - transition: box-shadow 0.3s ease; - cursor: pointer; +.resource-title { + font-weight: 500; + color: var(--color-foreground); + transition: color var(--transition-fast); } -.links-list .section:hover { - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); +.resource-link:hover .resource-title { + color: var(--color-primary); } -.about-page { - padding: 1.5rem; - background-color: var(--background-color); - border-radius: 0.5rem; +.resource-icon { + width: 1rem; + height: 1rem; + color: var(--color-muted); + opacity: 0; + transition: opacity var(--transition-fast), color var(--transition-fast); } -.about-page p { - line-height: 1.6; +.resource-link:hover .resource-icon { + opacity: 1; + color: var(--color-primary); } From 416ba8fa3653b9adb245bf586d7ed0d8a613a4f5 Mon Sep 17 00:00:00 2001 From: Gabriel Grubba Date: Mon, 22 Dec 2025 10:27:49 -0300 Subject: [PATCH 7/7] DEV: remove router --- .../static-assets/skel-react/client/main.jsx | 13 ++++++------- .../skel-react/imports/ui/About.jsx | 16 ---------------- .../skel-react/imports/ui/Routes.jsx | 19 ------------------- tools/static-assets/skel-react/package.json | 3 +-- 4 files changed, 7 insertions(+), 44 deletions(-) delete mode 100644 tools/static-assets/skel-react/imports/ui/About.jsx delete mode 100644 tools/static-assets/skel-react/imports/ui/Routes.jsx diff --git a/tools/static-assets/skel-react/client/main.jsx b/tools/static-assets/skel-react/client/main.jsx index 94a5f1be0f..879c8859a8 100644 --- a/tools/static-assets/skel-react/client/main.jsx +++ b/tools/static-assets/skel-react/client/main.jsx @@ -1,11 +1,10 @@ -import { createRoot } from 'react-dom/client'; -import { Meteor } from 'meteor/meteor'; -import { RouterProvider } from "react-router/dom"; -import { Routes } from '/imports/ui/Routes.jsx'; -import '/imports/ui/styles.css'; +import { createRoot } from "react-dom/client"; +import { Meteor } from "meteor/meteor"; +import { App } from "/imports/ui/App"; +import "/imports/ui/styles.css"; Meteor.startup(() => { - const container = document.getElementById('react-target'); + const container = document.getElementById("react-target"); const root = createRoot(container); - root.render(); + root.render(); }); diff --git a/tools/static-assets/skel-react/imports/ui/About.jsx b/tools/static-assets/skel-react/imports/ui/About.jsx deleted file mode 100644 index 6a241e8f70..0000000000 --- a/tools/static-assets/skel-react/imports/ui/About.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useLoaderData } from "react-router"; -import { NavLink } from "react-router"; - -export const About = () => { - const { about } = useLoaderData(); - return ( -
- Home -
-

About This Application

- Reload this page -
-

{about}

-
- ); -}; diff --git a/tools/static-assets/skel-react/imports/ui/Routes.jsx b/tools/static-assets/skel-react/imports/ui/Routes.jsx deleted file mode 100644 index da3fcb5d98..0000000000 --- a/tools/static-assets/skel-react/imports/ui/Routes.jsx +++ /dev/null @@ -1,19 +0,0 @@ -import { createBrowserRouter } from "react-router"; -import { About } from "./About"; -import { App } from "./App"; - -// https://reactrouter.com/start/data/routing -export const Routes = createBrowserRouter([ - { - path: "/", - element: , - }, - { - path: "/about", - element: , - loader: async () => { - const about = await Meteor.callAsync("about"); - return { about }; - }, - }, -]); diff --git a/tools/static-assets/skel-react/package.json b/tools/static-assets/skel-react/package.json index 786f777256..8a6257155f 100644 --- a/tools/static-assets/skel-react/package.json +++ b/tools/static-assets/skel-react/package.json @@ -12,8 +12,7 @@ "@swc/helpers": "^0.5.17", "meteor-node-stubs": "^1.2.12", "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router": "^7.10.1" + "react-dom": "^18.2.0" }, "devDependencies": { "@meteorjs/rspack": "^0.3.54",