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 diff --git a/packages/accounts-password/password_server.js b/packages/accounts-password/password_server.js index cb7cb96112..4bb1de5149 100644 --- a/packages/accounts-password/password_server.js +++ b/packages/accounts-password/password_server.js @@ -507,6 +507,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 5df402d74d..f409516c59 100644 --- a/packages/accounts-password/password_tests.js +++ b/packages/accounts-password/password_tests.js @@ -1417,9 +1417,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(); diff --git a/tools/fs/safe-watcher.ts b/tools/fs/safe-watcher.ts index 6e2bebc023..38e57d0bc6 100644 --- a/tools/fs/safe-watcher.ts +++ b/tools/fs/safe-watcher.ts @@ -312,9 +312,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(); } @@ -340,9 +344,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); diff --git a/tools/static-assets/skel-react/client/main.jsx b/tools/static-assets/skel-react/client/main.jsx index c7e0c1f05e..879c8859a8 100644 --- a/tools/static-assets/skel-react/client/main.jsx +++ b/tools/static-assets/skel-react/client/main.jsx @@ -1,9 +1,10 @@ -import { createRoot } from 'react-dom/client'; -import { Meteor } from 'meteor/meteor'; -import { App } from '/imports/ui/App'; +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(); }); diff --git a/tools/static-assets/skel-react/imports/ui/App.jsx b/tools/static-assets/skel-react/imports/ui/App.jsx index eeeee2161a..6b12ade912 100644 --- a/tools/static-assets/skel-react/imports/ui/App.jsx +++ b/tools/static-assets/skel-react/imports/ui/App.jsx @@ -1,10 +1,13 @@ -import { Hello } from './Hello.jsx'; -import { Info } from './Info.jsx'; +import { Counter } from "./Counter.jsx"; +import { Header } from "./Header.jsx"; +import { Info } from "./Info.jsx"; export const App = () => ( -
-

Welcome to Meteor!

- - +
+
+
+ + +
); diff --git a/tools/static-assets/skel-react/imports/ui/Counter.jsx b/tools/static-assets/skel-react/imports/ui/Counter.jsx new file mode 100644 index 0000000000..7fcb4d8404 --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/Counter.jsx @@ -0,0 +1,24 @@ +import { useState } from "react"; + +export const Counter = () => { + const [counter, setCounter] = useState(0); + + const increment = () => { + setCounter(counter + 1); + }; + + return ( +
+
+ +

+ 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 new file mode 100644 index 0000000000..9024b823c5 --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/Header.jsx @@ -0,0 +1,14 @@ +import MeteorLogo from "./meteor-logo.svg"; + +export const Header = () => { + return ( +
+ +
+ ); +}; diff --git a/tools/static-assets/skel-react/imports/ui/Hello.jsx b/tools/static-assets/skel-react/imports/ui/Hello.jsx deleted file mode 100644 index 527d5af607..0000000000 --- a/tools/static-assets/skel-react/imports/ui/Hello.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import { useState } from 'react'; - -export const Hello = () => { - const [counter, setCounter] = useState(0); - - const increment = () => { - setCounter(counter + 1); - }; - - return ( -
- -

You've pressed the button {counter} times.

-
- ); -}; diff --git a/tools/static-assets/skel-react/imports/ui/Info.jsx b/tools/static-assets/skel-react/imports/ui/Info.jsx index 7154a4776d..823309d4d1 100644 --- a/tools/static-assets/skel-react/imports/ui/Info.jsx +++ b/tools/static-assets/skel-react/imports/ui/Info.jsx @@ -1,22 +1,30 @@ -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()) { + if (isLoading()) { return
Loading...
; } return ( -
-

Learn Meteor!

- -
+
+

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 new file mode 100644 index 0000000000..3610e0e026 --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/meteor-logo.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ 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..4b14c0d5ad --- /dev/null +++ b/tools/static-assets/skel-react/imports/ui/styles.css @@ -0,0 +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 { + /* 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; +} + +/* ============ Reset & Base Styles ============ */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +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; +} + +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-between; + height: 4rem; +} + +.logo-container { + display: flex; + align-items: center; + gap: var(--spacing-sm); +} + +.logo { + 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 { + 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; + border-radius: var(--radius-sm); + cursor: pointer; + transition: background-color var(--transition-normal), + transform var(--transition-fast); +} + +.button:hover { + background-color: var(--color-primary-hover); +} + +.button:active { + transform: scale(0.98); +} + +.button:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} + +/* ============ Resources Section ============ */ +.section-title { + font-size: 1.5rem; + font-weight: 700; + color: var(--color-foreground); + margin-bottom: var(--spacing-lg); + letter-spacing: -0.025em; +} + +.resources-grid { + list-style-type: none; + display: grid; + grid-template-columns: 1fr; + gap: var(--spacing-md); +} + +@media (min-width: 640px) { + .resources-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +/* ============ Resource Card ============ */ +.resource-link { + display: block; +} + +.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; + align-items: center; + justify-content: space-between; +} + +.resource-title { + font-weight: 500; + color: var(--color-foreground); + transition: color var(--transition-fast); +} + +.resource-link:hover .resource-title { + color: var(--color-primary); +} + +.resource-icon { + width: 1rem; + height: 1rem; + color: var(--color-muted); + opacity: 0; + transition: opacity var(--transition-fast), color var(--transition-fast); +} + +.resource-link:hover .resource-icon { + opacity: 1; + color: var(--color-primary); +} diff --git a/tools/static-assets/skel-react/package.json b/tools/static-assets/skel-react/package.json index 93b64a2e58..8a6257155f 100644 --- a/tools/static-assets/skel-react/package.json +++ b/tools/static-assets/skel-react/package.json @@ -20,6 +20,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 49452ad352..b6adcc3a56 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 { 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() }); @@ -7,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", }); } @@ -35,3 +46,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()}`; + }, +});