diff --git a/.github/skills/testing/SKILL.md b/.github/skills/testing/SKILL.md index 8d5d0ddbe4..d54d674e95 100644 --- a/.github/skills/testing/SKILL.md +++ b/.github/skills/testing/SKILL.md @@ -22,8 +22,11 @@ Test patterns, commands, and utilities for the Meteor codebase. ./meteor test-packages mongo # Test specific package TINYTEST_FILTER="collection" ./meteor test-packages # Filter specific tests -# Package tests in console (headless via Puppeteer) +# Package tests in console (headless via Puppeteer — prints results to terminal) +# Use this for automation or when you need terminal output without a browser. PUPPETEER_DOWNLOAD_PATH=~/.npm/chromium ./packages/test-in-console/run.sh +./packages/test-in-console/run.sh # Test all core packages +./packages/test-in-console/run.sh "mongo" # Test specific package # E2E tests (Jest + Playwright) npm run install:e2e # Install dependencies diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ed3d711da6..ce29d07dfb 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -79,6 +79,7 @@ jobs: echo "Current branch: ${{ github.head_ref || github.ref_name }}" if [[ "${{ github.head_ref || github.ref_name }}" == release-* ]]; then echo "NPM_LINK_RSPACK=false" >> $GITHUB_ENV + echo "::warning::NPM_LINK_RSPACK=false on release branch. E2E tests will install @meteorjs/rspack from npm — make sure the latest version is published or tests may fail." fi - name: Prepare Meteor diff --git a/AGENTS.md b/AGENTS.md index 23a6969d69..6e04b3ad4b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,11 +8,17 @@ Full-stack JavaScript platform for modern web and mobile applications. ./meteor run # Run from source ./meteor create my-app # Create app ./meteor self-test # CLI tests -./meteor test-packages ./packages/ # Package tests +./meteor test-packages ./packages/ # Package tests (browser UI at localhost:3000) +./packages/test-in-console/run.sh "" # Package tests (terminal output via Puppeteer) npm run test:unit # Unit tests (Jest) npm run test:e2e # E2E tests (Jest + Playwright) ``` +> **Note:** `./meteor test-packages` starts a web server and waits for a browser — +> it produces no terminal output. For automated/headless runs, use +> `./packages/test-in-console/run.sh ""` instead, which runs the same tests +> via Puppeteer and prints pass/fail results to stdout. + ## Structure ``` diff --git a/dev/modern-tools/rspack/E2E_COVERAGE.md b/dev/modern-tools/rspack/E2E_COVERAGE.md index 8b1a8c20fc..1199c73a50 100644 --- a/dev/modern-tools/rspack/E2E_COVERAGE.md +++ b/dev/modern-tools/rspack/E2E_COVERAGE.md @@ -85,16 +85,19 @@ Full Blaze app (with `imports/` structure for tests). ### typescript -TypeScript with SCSS, type checking, and `.ts` rspack config. +TypeScript with SCSS, type checking, `.ts` rspack config, and `.ts` SWC config. | What is covered | Phase | |----------------|-------| | TypeScript rspack config (`rspack.config.ts`) | All | +| TypeScript SWC config (`swc.config.ts`) with automatic JSX runtime | All | +| `@swc/core` type-only import for SWC config typings | All | | Custom build dir (`build`) | All | | Custom asset/chunk context dirs (`assets`, `chunks`) | All | | SCSS styles support (`white-space: break-spaces`) | Run, Prod | | TypeScript + TSX environment detection | Run, Prod, Test, Build | | Portable build (Meteor.isDevelopment/isProduction not defined) | Run, Prod, Build | +| `Meteor.extendSwcConfig` with path aliases (`@ui/*`, `@api/*`) | All | | `TsCheckerRspackPlugin` type checking (no errors) | Run | | `.meteor/local/types` directory generated | Run | | Separate client/server test files | Test | @@ -232,11 +235,12 @@ Several apps import specific npm packages to verify that Meteor + Rspack handles | `@apollo/server/express4` | ESM subpath export (middleware from deep path) | | `graphql` | Peer dependency, dual CJS/ESM package | -### typescript (`apps/typescript/rspack.config.ts`) +### typescript (`apps/typescript/rspack.config.ts`, `apps/typescript/swc.config.ts`) | Package | Reason | |---------|--------| | `node:module` (`createRequire`) | Node.js built-in in a `.ts` config file — tests CJS interop via `createRequire(import.meta.url)` in an ESM context | +| `@swc/core` | Type-only import (`import type { Config }`) — provides typings for `swc.config.ts`, stripped at compile time | --- @@ -250,6 +254,7 @@ Where each feature is tested across apps and skeletons. | HMR disabled (prod) | all apps with HMR | | | HMR incompatible | blaze, full-blaze | | | Custom rspack config | react (.cjs), react-router, babel (.mjs), monorepo (.cjs), typescript (.ts) | | +| Custom SWC config (.ts) | typescript | | | Config override file | react-router, monorepo | | | Custom build dir | react, typescript | | | Custom asset/chunk context dirs | typescript | | @@ -272,6 +277,7 @@ Where each feature is tested across apps and skeletons. | Module rules override | babel | | | Custom NODE_ENV compilation | babel | | | Portable build (no isDev/isProd defines) | typescript | | +| `Meteor.extendSwcConfig` (path aliases) | typescript | | | `meteor reset` cleanup | all apps | all skeletons | | Skeleton creation | | all 14 skeletons | | Body style assertions | | react, tailwind (custom); most others (default) | diff --git a/npm-packages/meteor-rspack/index.d.ts b/npm-packages/meteor-rspack/index.d.ts index 449e6d1a85..20a17a1e7d 100644 --- a/npm-packages/meteor-rspack/index.d.ts +++ b/npm-packages/meteor-rspack/index.d.ts @@ -57,10 +57,21 @@ type MeteorEnv = Record & { */ splitVendorChunk: () => Record; /** - * Extend Rspack SWC loader config. + * Extend the SWC loader config by smart-merging custom options on top of + * Meteor's defaults. Only the properties you specify are overridden; + * everything else is preserved. + * @param swcConfig - SWC loader options to merge with defaults * @returns A config object with SWC loader config */ extendSwcConfig: (swcConfig: SwcLoaderOptions) => Record; + /** + * Replace the SWC loader config entirely, discarding Meteor's defaults. + * Use this when you need full control over SWC options and don't want any + * automatic merging with Meteor's built-in configuration. + * @param swcConfig - Complete SWC loader options (replaces defaults) + * @returns A config object with SWC loader config + */ + replaceSwcConfig: (swcConfig: SwcLoaderOptions) => Record; /** * Extend Rspack configs. * @returns A config object with merged configs diff --git a/npm-packages/meteor-rspack/lib/meteorRspackHelpers.js b/npm-packages/meteor-rspack/lib/meteorRspackHelpers.js index d287773000..2fbbac3027 100644 --- a/npm-packages/meteor-rspack/lib/meteorRspackHelpers.js +++ b/npm-packages/meteor-rspack/lib/meteorRspackHelpers.js @@ -132,10 +132,14 @@ function splitVendorChunk() { } /** - * Extend SWC loader config - * Usage: extendSwcConfig() + * Extend SWC loader config by smart-merging custom options on top of Meteor's + * defaults (via `mergeSplitOverlap`). Only the properties you specify are + * overridden; everything else is preserved. * - * @returns {Record} `{ meteorRspackConfigX: { optimization: { ... } } }` + * Usage: Meteor.extendSwcConfig({ jsc: { parser: { decorators: true } } }) + * + * @param {object} swcConfig - SWC loader options to merge with defaults + * @returns {Record} config fragment for spreading into rspack config */ function extendSwcConfig(swcConfig) { return prepareMeteorRspackConfig({ @@ -152,6 +156,31 @@ function extendSwcConfig(swcConfig) { }); } +/** + * Replace the SWC loader config entirely, discarding Meteor's defaults. + * Use this when you need full control over SWC options and don't want any + * automatic merging with Meteor's built-in configuration. + * + * Usage: Meteor.replaceSwcConfig({ jsc: { parser: { syntax: 'typescript' }, target: 'es2020' } }) + * + * @param {object} swcConfig - Complete SWC loader options (replaces defaults) + * @returns {Record} config fragment for spreading into rspack config + */ +function replaceSwcConfig(swcConfig) { + return prepareMeteorRspackConfig({ + module: { + rules: [ + { + test: /\.(?:[mc]?js|jsx|[mc]?ts|tsx)$/i, + exclude: /node_modules|\.meteor\/local/, + loader: 'builtin:swc-loader', + options: swcConfig, + }, + ], + }, + }); +} + /** * Signal that `Meteor.isDevelopment` and `Meteor.isProduction` should be omitted * from DefinePlugin, making the bundle portable across Meteor environments. @@ -227,6 +256,7 @@ module.exports = { setCache, splitVendorChunk, extendSwcConfig, + replaceSwcConfig, makeWebNodeBuiltinsAlias, disablePlugins, outputMeteorRspack, diff --git a/npm-packages/meteor-rspack/lib/swc.js b/npm-packages/meteor-rspack/lib/swc.js index 8b43703059..e113ad1626 100644 --- a/npm-packages/meteor-rspack/lib/swc.js +++ b/npm-packages/meteor-rspack/lib/swc.js @@ -9,11 +9,38 @@ const vm = require('vm'); function getMeteorAppSwcrc(file = '.swcrc') { try { const filePath = `${process.cwd()}/${file}`; - if (file.endsWith('.js')) { + if (file.endsWith('.js') || file.endsWith('.ts')) { let content = fs.readFileSync(filePath, 'utf-8'); - // Check if the content uses ES module syntax (export default) + + if (file.endsWith('.ts')) { + try { + const swc = require('@swc/core'); + const result = swc.transformSync(content, { + jsc: { + parser: { + syntax: 'typescript', + }, + target: 'es2015', + }, + }); + content = result.code; + } catch (swcError) { + content = content + .replace(/import\s+type\s+.*?from\s+['"][^'"]+['"];?/g, '') + .replace(/import\s+.*?from\s+['"][^'"]+['"];?/g, '') + .replace(/import\s+['"][^'"]+['"];?/g, '') + .replace(/export\s+default\s+/, 'module.exports = ') + .replace(/export\s+/g, '') + .replace(/:\s*\w+(\[\])?(\s*=)/g, '$2') + .replace(/\(([^)]*?):\s*\w+(\[\])?\)/g, '($1)') + .replace(/\):\s*\w+(\[\])?\s*\{/g, ') {') + .replace(/interface\s+\w+\s*\{[^}]*\}/g, '') + .replace(/type\s+\w+\s*=\s*[^;]+;/g, '') + .replace(/as\s+\w+(\[\])?/g, ''); + } + } + if (content.includes('export default')) { - // Transform ES module syntax to CommonJS content = content.replace(/export\s+default\s+/, 'module.exports = '); } const script = new vm.Script(` @@ -27,7 +54,9 @@ function getMeteorAppSwcrc(file = '.swcrc') { })() `); const context = vm.createContext({ process }); - return script.runInContext(context); + const result = script.runInContext(context); + // Handle CJS interop wrapper (e.g. { __esModule: true, default: config }) + return result && result.__esModule && result.default ? result.default : result; } else { // For .swcrc and other JSON files, parse as JSON return JSON.parse(fs.readFileSync(filePath, 'utf-8')); @@ -45,12 +74,13 @@ function getMeteorAppSwcrc(file = '.swcrc') { function getMeteorAppSwcConfig() { const hasSwcRc = fs.existsSync(`${process.cwd()}/.swcrc`); const hasSwcJs = !hasSwcRc && fs.existsSync(`${process.cwd()}/swc.config.js`); + const hasSwcTs = !hasSwcRc && !hasSwcJs && fs.existsSync(`${process.cwd()}/swc.config.ts`); - if (!hasSwcRc && !hasSwcJs) { + if (!hasSwcRc && !hasSwcJs && !hasSwcTs) { return undefined; } - const swcFile = hasSwcJs ? 'swc.config.js' : '.swcrc'; + const swcFile = hasSwcTs ? 'swc.config.ts' : hasSwcJs ? 'swc.config.js' : '.swcrc'; const config = getMeteorAppSwcrc(swcFile); // Set baseUrl to process.cwd() if it exists diff --git a/npm-packages/meteor-rspack/package-lock.json b/npm-packages/meteor-rspack/package-lock.json index 0919f2882e..7792758d69 100644 --- a/npm-packages/meteor-rspack/package-lock.json +++ b/npm-packages/meteor-rspack/package-lock.json @@ -1,12 +1,12 @@ { "name": "@meteorjs/rspack", - "version": "1.1.0-beta.30", + "version": "1.1.0-beta.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@meteorjs/rspack", - "version": "1.1.0-beta.30", + "version": "1.1.0-beta.31", "license": "ISC", "dependencies": { "fast-deep-equal": "^3.1.3", diff --git a/npm-packages/meteor-rspack/package.json b/npm-packages/meteor-rspack/package.json index 3d530801ac..2bcbb679c2 100644 --- a/npm-packages/meteor-rspack/package.json +++ b/npm-packages/meteor-rspack/package.json @@ -1,6 +1,6 @@ { "name": "@meteorjs/rspack", - "version": "1.1.0-beta.30", + "version": "1.1.0-beta.31", "description": "Configuration logic for using Rspack in Meteor projects", "main": "index.js", "type": "commonjs", diff --git a/npm-packages/meteor-rspack/rspack.config.js b/npm-packages/meteor-rspack/rspack.config.js index 0bfb343543..ea1fa3da79 100644 --- a/npm-packages/meteor-rspack/rspack.config.js +++ b/npm-packages/meteor-rspack/rspack.config.js @@ -19,6 +19,7 @@ const { setCache, splitVendorChunk, extendSwcConfig, + replaceSwcConfig, makeWebNodeBuiltinsAlias, disablePlugins, outputMeteorRspack, @@ -59,6 +60,8 @@ function createCacheStrategy( const hasSwcrcConfig = fs.existsSync(swcrcPath); const swcJsPath = path.join(process.cwd(), 'swc.config.js'); const hasSwcJsConfig = fs.existsSync(swcJsPath); + const swcTsPath = path.join(process.cwd(), 'swc.config.ts'); + const hasSwcTsConfig = fs.existsSync(swcTsPath); const postcssConfigPath = path.join(process.cwd(), 'postcss.config.js'); const hasPostcssConfig = fs.existsSync(postcssConfigPath); const packageLockPath = path.join(process.cwd(), 'package-lock.json'); @@ -75,6 +78,7 @@ function createCacheStrategy( ...(hasBabelJsConfig ? [babelJsConfig] : []), ...(hasSwcrcConfig ? [swcrcPath] : []), ...(hasSwcJsConfig ? [swcJsPath] : []), + ...(hasSwcTsConfig ? [swcTsPath] : []), ...(hasPostcssConfig ? [postcssConfigPath] : []), ...(hasPackageLock ? [packageLockPath] : []), ...(hasYarnLock ? [yarnLockPath] : []), @@ -325,7 +329,11 @@ module.exports = async function (inMeteor = {}, argv = {}) { setCache(!!enabled, enabled === "memory" ? undefined : cacheStrategy); Meteor.splitVendorChunk = () => splitVendorChunk(); Meteor.extendSwcConfig = (customSwcConfig) => - extendSwcConfig(customSwcConfig); + extendSwcConfig( + mergeSplitOverlap(Meteor.swcConfigOptions, customSwcConfig) + ); + Meteor.replaceSwcConfig = (customSwcConfig) => + replaceSwcConfig(customSwcConfig); Meteor.extendConfig = (...configs) => mergeSplitOverlap(...configs); Meteor.disablePlugins = (matchers) => prepareMeteorRspackConfig({ @@ -817,9 +825,9 @@ module.exports = async function (inMeteor = {}, argv = {}) { delete config["meteor.enablePortableBuild"]; - // if (Meteor.isDebug || Meteor.isVerbose) { + if (Meteor.isDebug || Meteor.isVerbose) { console.log("Config:", inspect(config, { depth: null, colors: true })); - // } + } // Check if lazyCompilation is enabled and warn the user if ( diff --git a/packages/babel-compiler/babel-compiler.js b/packages/babel-compiler/babel-compiler.js index 0de0637df2..2fdb836de0 100644 --- a/packages/babel-compiler/babel-compiler.js +++ b/packages/babel-compiler/babel-compiler.js @@ -101,17 +101,18 @@ let lastModifiedSwcConfigTime; BCp.initializeMeteorAppSwcrc = function () { const hasSwcRc = fs.existsSync(`${getMeteorAppDir()}/.swcrc`); const hasSwcJs = !hasSwcRc && fs.existsSync(`${getMeteorAppDir()}/swc.config.js`); - if (!lastModifiedSwcConfig && !hasSwcRc && !hasSwcJs) { + const hasSwcTs = !hasSwcRc && !hasSwcJs && fs.existsSync(`${getMeteorAppDir()}/swc.config.ts`); + if (!lastModifiedSwcConfig && !hasSwcRc && !hasSwcJs && !hasSwcTs) { return; } - const swcFile = hasSwcJs ? 'swc.config.js' : '.swcrc'; + const swcFile = hasSwcTs ? 'swc.config.ts' : hasSwcJs ? 'swc.config.js' : '.swcrc'; const filePath = `${getMeteorAppDir()}/${swcFile}`; const fileStats = fs.statSync(filePath); const fileModTime = fileStats?.mtime?.getTime(); let currentLastModifiedConfigTime; - if (hasSwcJs) { - // For dynamic JS files, first get the resolved configuration + if (hasSwcJs || hasSwcTs) { + // For dynamic JS/TS files, first get the resolved configuration const resolvedConfig = lastModifiedSwcConfigTime?.includes(`${fileModTime}`) ? lastModifiedSwcConfig || getMeteorAppSwcrc(swcFile) : getMeteorAppSwcrc(swcFile); @@ -1073,8 +1074,37 @@ function getMeteorAppPackageJson() { function getMeteorAppSwcrc(file = '.swcrc') { try { const filePath = `${getMeteorAppDir()}/${file}`; - if (file.endsWith('.js')) { + if (file.endsWith('.js') || file.endsWith('.ts')) { let content = fs.readFileSync(filePath, 'utf-8'); + + if (file.endsWith('.ts')) { + try { + const swc = require('@meteorjs/swc-core'); + const result = swc.transformSync(content, { + jsc: { + parser: { + syntax: 'typescript', + }, + target: 'es2015', + }, + }); + content = result.code; + } catch (swcError) { + content = content + .replace(/import\s+type\s+.*?from\s+['"][^'"]+['"];?/g, '') + .replace(/import\s+.*?from\s+['"][^'"]+['"];?/g, '') + .replace(/import\s+['"][^'"]+['"];?/g, '') + .replace(/export\s+default\s+/, 'module.exports = ') + .replace(/export\s+/g, '') + .replace(/:\s*\w+(\[\])?(\s*=)/g, '$2') + .replace(/\(([^)]*?):\s*\w+(\[\])?\)/g, '($1)') + .replace(/\):\s*\w+(\[\])?\s*\{/g, ') {') + .replace(/interface\s+\w+\s*\{[^}]*\}/g, '') + .replace(/type\s+\w+\s*=\s*[^;]+;/g, '') + .replace(/as\s+\w+(\[\])?/g, ''); + } + } + // Check if the content uses ES module syntax (export default) if (content.includes('export default')) { // Transform ES module syntax to CommonJS @@ -1091,7 +1121,9 @@ function getMeteorAppSwcrc(file = '.swcrc') { })() `); const context = vm.createContext({ process }); - return script.runInContext(context); + const result = script.runInContext(context); + // Handle CJS interop wrapper (e.g. { __esModule: true, default: config }) + return result && result.__esModule && result.default ? result.default : result; } else { // For .swcrc and other JSON files, parse as JSON return JSON.parse(fs.readFileSync(filePath, 'utf-8')); diff --git a/packages/rspack/lib/config.js b/packages/rspack/lib/config.js index 5fab01f4d4..5771d25fb2 100644 --- a/packages/rspack/lib/config.js +++ b/packages/rspack/lib/config.js @@ -15,6 +15,7 @@ const { isMeteorAppDevelopment, isMeteorAppRun, isMeteorAppBuild, + isMeteorAppNative, isMeteorAppDebug, isMeteorAppTest, isMeteorAppTestFullApp, @@ -370,7 +371,7 @@ export function configureMeteorForRspack() { ensureModuleFilesExist(); // Write content to module files - if (isMeteorAppRun() && isMeteorAppDevelopment()) { + if (isMeteorAppRun() && isMeteorAppDevelopment() && !isMeteorAppNative()) { const customScriptUrl = `/__rspack__/${getBuildFilePath({ ...env, isMain: true, diff --git a/packages/rspack/lib/constants.js b/packages/rspack/lib/constants.js index 718ca52301..1341f54ce7 100644 --- a/packages/rspack/lib/constants.js +++ b/packages/rspack/lib/constants.js @@ -7,7 +7,7 @@ import path from 'path'; export const DEFAULT_RSPACK_VERSION = '1.7.1'; -export const DEFAULT_METEOR_RSPACK_VERSION = '1.1.0-beta.30'; +export const DEFAULT_METEOR_RSPACK_VERSION = '1.1.0-beta.31'; export const DEFAULT_METEOR_RSPACK_REACT_HMR_VERSION = '1.4.3'; diff --git a/packages/rspack/rspack_plugin.js b/packages/rspack/rspack_plugin.js index 502f49ae82..73c6a6eb5c 100644 --- a/packages/rspack/rspack_plugin.js +++ b/packages/rspack/rspack_plugin.js @@ -177,6 +177,11 @@ if (isMeteorAppRun() || isMeteorAppBuild() || isMeteorAppTest()) { // Configure Meteor settings for Rspack configureMeteorForRspack(); + // Set native mode flag so the server module can skip dev proxy setup + if (isMeteorAppNative()) { + process.env.RSPACK_NATIVE = 'true'; + } + // Calculate and set the devServerPort at boot if (!process.env.RSPACK_DEVSERVER_PORT) { process.env.RSPACK_DEVSERVER_PORT = calculateDevServerPort(); diff --git a/packages/rspack/rspack_server.js b/packages/rspack/rspack_server.js index 97ca990878..9c55294738 100644 --- a/packages/rspack/rspack_server.js +++ b/packages/rspack/rspack_server.js @@ -28,7 +28,11 @@ const RSPACK_ASSETS_REGEX = new RegExp( `^\/${rspackAssetsContext}\/(.+)$`, ); -if (global?.Package?.['tools-core'] != null && Meteor.isDevelopment) { +const shouldEnableDevHMRProxy = + global?.Package?.["tools-core"] != null && + Meteor.isDevelopment && + !process.env.RSPACK_NATIVE; +if (shouldEnableDevHMRProxy) { const { shuffleString } = require('meteor/tools-core/lib/string'); const { createProxyMiddleware } = require('http-proxy-middleware'); diff --git a/packages/test-in-console/puppeteer_runner.js b/packages/test-in-console/puppeteer_runner.js index 991bbd4631..7065341278 100644 --- a/packages/test-in-console/puppeteer_runner.js +++ b/packages/test-in-console/puppeteer_runner.js @@ -16,7 +16,7 @@ async function runNextUrl(browser) { if (text.includes('Permissions policy violation')) { return; } - if (msg._text !== undefined) console.log(msg._text); + if (text) console.log(text); else { testNumber++; const currentClientTest = diff --git a/tools/e2e-tests/apps/solid/package.json b/tools/e2e-tests/apps/solid/package.json index 104f5258ef..5ce93de638 100644 --- a/tools/e2e-tests/apps/solid/package.json +++ b/tools/e2e-tests/apps/solid/package.json @@ -22,7 +22,7 @@ "modern": true }, "devDependencies": { - "@meteorjs/rspack": "^0.0.29", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rspack/cli": "^1.4.8", "@rspack/core": "^1.4.8", "babel-loader": "10.0.0", diff --git a/tools/e2e-tests/apps/svelte/package.json b/tools/e2e-tests/apps/svelte/package.json index a9873d4aa5..cba0d4e7a0 100644 --- a/tools/e2e-tests/apps/svelte/package.json +++ b/tools/e2e-tests/apps/svelte/package.json @@ -13,7 +13,7 @@ "meteor-node-stubs": "^1.2.12" }, "devDependencies": { - "@meteorjs/rspack": "^0.0.28", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rspack/cli": "^1.4.8", "@rspack/core": "^1.4.8", "playwright": "1.58.0", diff --git a/tools/e2e-tests/apps/typescript/client/main.tsx b/tools/e2e-tests/apps/typescript/client/main.tsx index e576e1b803..4eb40f49d1 100644 --- a/tools/e2e-tests/apps/typescript/client/main.tsx +++ b/tools/e2e-tests/apps/typescript/client/main.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { createRoot } from 'react-dom/client'; import { Meteor } from 'meteor/meteor'; import { App } from '@ui/App'; diff --git a/tools/e2e-tests/apps/typescript/imports/ui/App.tsx b/tools/e2e-tests/apps/typescript/imports/ui/App.tsx index 52f71448cc..94679c5547 100644 --- a/tools/e2e-tests/apps/typescript/imports/ui/App.tsx +++ b/tools/e2e-tests/apps/typescript/imports/ui/App.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import './Global.scss'; import { Hello } from './Hello'; import { Info } from './Info'; diff --git a/tools/e2e-tests/apps/typescript/imports/ui/Hello.tsx b/tools/e2e-tests/apps/typescript/imports/ui/Hello.tsx index 15e0f185ac..527d5af607 100644 --- a/tools/e2e-tests/apps/typescript/imports/ui/Hello.tsx +++ b/tools/e2e-tests/apps/typescript/imports/ui/Hello.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import { useState } from 'react'; export const Hello = () => { const [counter, setCounter] = useState(0); diff --git a/tools/e2e-tests/apps/typescript/imports/ui/Info.tsx b/tools/e2e-tests/apps/typescript/imports/ui/Info.tsx index 23cb8f07a3..809fbc6716 100644 --- a/tools/e2e-tests/apps/typescript/imports/ui/Info.tsx +++ b/tools/e2e-tests/apps/typescript/imports/ui/Info.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { useFind, useSubscribe } from "meteor/react-meteor-data"; import { LinksCollection, Link } from "../api/links"; diff --git a/tools/e2e-tests/apps/typescript/package.json b/tools/e2e-tests/apps/typescript/package.json index d1ac310541..f0359b6094 100644 --- a/tools/e2e-tests/apps/typescript/package.json +++ b/tools/e2e-tests/apps/typescript/package.json @@ -15,6 +15,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { + "@swc/core": "^1.15.18", "@types/meteor": "^2.9.9", "@types/mocha": "^8.2.3", "@types/node": "^22.10.6", diff --git a/tools/e2e-tests/apps/typescript/swc.config.ts b/tools/e2e-tests/apps/typescript/swc.config.ts new file mode 100644 index 0000000000..699a33a40d --- /dev/null +++ b/tools/e2e-tests/apps/typescript/swc.config.ts @@ -0,0 +1,13 @@ +import type { Config } from "@swc/core"; + +const config: Config = { + jsc: { + transform: { + react: { + runtime: "automatic", + }, + }, + }, +}; + +export default config; diff --git a/tools/e2e-tests/apps/vue/package.json b/tools/e2e-tests/apps/vue/package.json index 4d13d28fd1..31c39838ad 100644 --- a/tools/e2e-tests/apps/vue/package.json +++ b/tools/e2e-tests/apps/vue/package.json @@ -17,7 +17,7 @@ "vue-router": "^4.2.5" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rspack/cli": "^1.4.8", "@rspack/core": "^1.4.8", "@tailwindcss/postcss": "^4.1.12", diff --git a/tools/e2e-tests/assertions.js b/tools/e2e-tests/assertions.js index 437bdec437..5abaad4daf 100644 --- a/tools/e2e-tests/assertions.js +++ b/tools/e2e-tests/assertions.js @@ -102,7 +102,7 @@ export async function assertFileExist(tempDir, filePath, options = {}) { return checkFile(); } // If we've exceeded the timeout, fail the test - expect(fileExists).toBe(true); + throw new Error(`Expected file to exist but it was not found: ${fullPath}`); return false; } diff --git a/tools/e2e-tests/test-helpers.js b/tools/e2e-tests/test-helpers.js index f1728472d5..52c11201f3 100644 --- a/tools/e2e-tests/test-helpers.js +++ b/tools/e2e-tests/test-helpers.js @@ -35,6 +35,12 @@ const isCI = process.env.GITHUB_ACTIONS === "true"; // Link local npm-packages/meteor-rspack so tests run against the latest dev version. // Set NPM_LINK_RSPACK=false to disable. const npmLinkLocalRspack = process.env.NPM_LINK_RSPACK !== 'false'; +if (!npmLinkLocalRspack) { + console.warn( + '\x1b[33m⚠ NPM_LINK_RSPACK=false — tests will install @meteorjs/rspack from npm.\n' + + ' If CI fails, ensure the latest @meteorjs/rspack version has been published.\x1b[0m' + ); +} const WAIT_ON = isCI ? 2000 : 500; diff --git a/tools/static-assets/skel-angular/package.json b/tools/static-assets/skel-angular/package.json index 1140972385..fa3105b024 100644 --- a/tools/static-assets/skel-angular/package.json +++ b/tools/static-assets/skel-angular/package.json @@ -22,7 +22,7 @@ }, "devDependencies": { "@angular/compiler-cli": "^20.0.0", - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@nx/angular-rspack": "^21.1.0", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", diff --git a/tools/static-assets/skel-apollo/package.json b/tools/static-assets/skel-apollo/package.json index 50c58e0f22..5fd18f2131 100644 --- a/tools/static-assets/skel-apollo/package.json +++ b/tools/static-assets/skel-apollo/package.json @@ -20,7 +20,7 @@ "devDependencies": { "@graphql-tools/webpack-loader": "^7.0.0", "@rsdoctor/rspack-plugin": "^1.2.3", - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", "@rspack/plugin-react-refresh": "^1.4.3", diff --git a/tools/static-assets/skel-babel/package.json b/tools/static-assets/skel-babel/package.json index 647024e2c8..8c9c45753f 100644 --- a/tools/static-assets/skel-babel/package.json +++ b/tools/static-assets/skel-babel/package.json @@ -17,7 +17,7 @@ "devDependencies": { "@babel/preset-env": "^7.28.3", "@babel/preset-react": "^7.23.3", - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-blaze/package.json b/tools/static-assets/skel-blaze/package.json index 4d1b0a785e..6dd838aaf6 100644 --- a/tools/static-assets/skel-blaze/package.json +++ b/tools/static-assets/skel-blaze/package.json @@ -14,7 +14,7 @@ "meteor-node-stubs": "^1.2.12" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-chakra-ui/package.json b/tools/static-assets/skel-chakra-ui/package.json index 6e3e3f7c8d..a30ef9f180 100644 --- a/tools/static-assets/skel-chakra-ui/package.json +++ b/tools/static-assets/skel-chakra-ui/package.json @@ -21,7 +21,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-coffeescript/package.json b/tools/static-assets/skel-coffeescript/package.json index af84b38f91..6f2ebbce93 100644 --- a/tools/static-assets/skel-coffeescript/package.json +++ b/tools/static-assets/skel-coffeescript/package.json @@ -15,7 +15,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-full/package.json b/tools/static-assets/skel-full/package.json index 8c77c7dbb5..e7f8bb952e 100644 --- a/tools/static-assets/skel-full/package.json +++ b/tools/static-assets/skel-full/package.json @@ -12,7 +12,7 @@ "meteor-node-stubs": "^1.2.12" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-react/package.json b/tools/static-assets/skel-react/package.json index 77d0137aaf..1091b1727a 100644 --- a/tools/static-assets/skel-react/package.json +++ b/tools/static-assets/skel-react/package.json @@ -15,7 +15,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-solid/package.json b/tools/static-assets/skel-solid/package.json index 6e2cc4ba61..d14262c517 100644 --- a/tools/static-assets/skel-solid/package.json +++ b/tools/static-assets/skel-solid/package.json @@ -14,7 +14,7 @@ "picocolors": "^1.1.1" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-svelte/package.json b/tools/static-assets/skel-svelte/package.json index 937c185741..d864cf7c23 100644 --- a/tools/static-assets/skel-svelte/package.json +++ b/tools/static-assets/skel-svelte/package.json @@ -13,7 +13,7 @@ "meteor-node-stubs": "^1.2.12" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-tailwind/package.json b/tools/static-assets/skel-tailwind/package.json index 482a0bcab0..79935caba4 100644 --- a/tools/static-assets/skel-tailwind/package.json +++ b/tools/static-assets/skel-tailwind/package.json @@ -16,7 +16,7 @@ "react-dom": "^17.0.2" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-typescript/package.json b/tools/static-assets/skel-typescript/package.json index af8e9e10c5..59017498e0 100644 --- a/tools/static-assets/skel-typescript/package.json +++ b/tools/static-assets/skel-typescript/package.json @@ -15,7 +15,7 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/static-assets/skel-typescript/tsconfig.json b/tools/static-assets/skel-typescript/tsconfig.json index 4cc0e0d73b..aff98434d9 100644 --- a/tools/static-assets/skel-typescript/tsconfig.json +++ b/tools/static-assets/skel-typescript/tsconfig.json @@ -36,7 +36,8 @@ "resolveJsonModule": true, "types": ["node", "mocha"], "esModuleInterop": true, - "preserveSymlinks": true + "preserveSymlinks": true, + "skipLibCheck": true }, "exclude": [ "./.meteor/**", diff --git a/tools/static-assets/skel-vue/package.json b/tools/static-assets/skel-vue/package.json index 0f4debcbdd..a156645d98 100644 --- a/tools/static-assets/skel-vue/package.json +++ b/tools/static-assets/skel-vue/package.json @@ -17,7 +17,7 @@ "vue-router": "^4.2.5" }, "devDependencies": { - "@meteorjs/rspack": "^1.0.0-beta.1", + "@meteorjs/rspack": "^1.1.0-beta.31", "@rsdoctor/rspack-plugin": "^1.2.3", "@rspack/cli": "^1.7.1", "@rspack/core": "^1.7.1", diff --git a/tools/tests/typescript.js b/tools/tests/typescript.js index 2828cc6a2b..7933ee5265 100644 --- a/tools/tests/typescript.js +++ b/tools/tests/typescript.js @@ -1,27 +1,28 @@ -// var selftest = require('../tool-testing/selftest.js'); -// var Sandbox = selftest.Sandbox; -// -// selftest.define("typescript template works", function () { -// const s = new Sandbox; -// -// let run = s.run("create", "--typescript", "typescript"); -// -// run.waitSecs(60); -// run.match("Created a new Meteor app in 'typescript'."); -// run.match("To run your new app"); -// -// s.cd("typescript"); -// -// run = s.run("npm", "install"); -// run.expectExit(0); -// -// run = s.run("lint"); -// run.waitSecs(60); -// run.match("[zodern:types] Exiting \"meteor lint\" early"); -// run.expectExit(0); -// -// run = s.run("npx", "tsc"); -// run.waitSecs(60); -// run.expectEnd(); -// run.expectExit(0); -// }); +var selftest = require('../tool-testing/selftest.js'); +var Sandbox = selftest.Sandbox; + +selftest.define("typescript template works", async function () { + const s = new Sandbox(); + await s.init(); + + let run = s.run("create", "--typescript", "typescript"); + + run.waitSecs(60); + await run.match("Created a new Meteor app in 'typescript'."); + await run.match("To run your new app"); + + s.cd("typescript"); + + run = s.run("npm", "install"); + await run.expectExit(0); + + run = s.run("lint"); + run.waitSecs(60); + await run.match('[zodern:types] Exiting "meteor lint" early'); + await run.expectExit(0); + + run = s.run("npx", "tsc"); + run.waitSecs(60); + await run.expectEnd(); + await run.expectExit(0); +}); diff --git a/v3-docs/docs/.gitignore b/v3-docs/docs/.gitignore index bcd2cabb66..60c63498eb 100644 --- a/v3-docs/docs/.gitignore +++ b/v3-docs/docs/.gitignore @@ -3,4 +3,7 @@ /.vitepress/dist /data/data.js -/data/names.json \ No newline at end of file +/data/names.json + +# Generated API reference for LLMs +/public/api-reference.json \ No newline at end of file diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 97af62202c..e25264bda4 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -677,7 +677,27 @@ export default defineConfig({ vite: { plugins: [ llmstxt({ - title: "Meteor.js 3 Docs", + title: "Meteor.js 3 Documentation", + domain: "https://docs.meteor.com", + description: "Full-stack JavaScript platform for modern web and mobile applications.", + details: ` +Meteor is a full-stack JavaScript platform for developing web and mobile applications. + +Key capabilities: +- Real-time data synchronization with publications and subscriptions +- Built-in accounts and authentication system +- Frontend agnostic (React, Vue, Solid, Blaze, Svelte) +- Zero-config build system with modern tooling (SWC, Rspack) +- One-command deployment to Galaxy Cloud +- TypeScript support with full type inference + +Current version: Meteor ${metadata.currentVersion}. + +## Structured API Data + +For complete API documentation in machine-readable format, see: +- [api-reference.json](/api-reference.json) - Full API reference with all functions, parameters, and types + `.trim(), }), ], }, diff --git a/v3-docs/docs/about/modern-build-stack/meteor-bundler-optimizations.md b/v3-docs/docs/about/modern-build-stack/meteor-bundler-optimizations.md index 6c76240f18..6c3d84cb53 100644 --- a/v3-docs/docs/about/modern-build-stack/meteor-bundler-optimizations.md +++ b/v3-docs/docs/about/modern-build-stack/meteor-bundler-optimizations.md @@ -165,7 +165,9 @@ You can use `.swcrc` config in the root of your project to describe specific [SW You can also configure other options using the `.swcrc` format. For custom SWC configs, see the [SWC configuration API](https://swc.rs/docs/configuration/compilation). -Use `swc.config.js` in your project root for dynamic configuration. Meteor will import and apply the SWC config automatically. This lets you choose a config based on environment variables or other runtime factors. +Use `swc.config.js` in your project root for dynamic configuration. Meteor will import and apply the SWC config automatically. This lets you choose a config based on environment variables or other runtime factors. If you prefer TypeScript, `swc.config.ts` is also supported, Meteor will transpile and load it automatically. + +Meteor checks for config files in this order: `.swcrc` > `swc.config.js` > `swc.config.ts`. Only the first one found is used. You can also review these migration topics that use custom `.swcrc` configs: diff --git a/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md b/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md index 35a34f1295..46ee9d8fe2 100644 --- a/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md +++ b/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md @@ -157,7 +157,8 @@ You can use flags to control the final configuration based on the environment. T | `compileWithRspack` | function | Forces given npm deps ([Condition](https://rspack.rs/config/module#condition)[]) to be compiled by Rspack | | `setCache` | function | Enables or disables cache. Accepts true (persistent, default), false, or 'memory' | | `splitVendorChunk` | function | Splits vendor libraries so they are automatically served from a separate chunk | -| `extendSwcConfig` | function | Extends the [SWC loader configuration](https://rspack.rs/guide/features/builtin-swc-loader#options) to apply only to the app code | +| `extendSwcConfig` | function | Smart-merges custom options into Meteor's default [SWC loader configuration](https://rspack.rs/guide/features/builtin-swc-loader#options), applying only to app code | +| `replaceSwcConfig` | function | Replaces Meteor's default [SWC loader configuration](https://rspack.rs/guide/features/builtin-swc-loader#options) entirely with the provided options, applying only to app code | | `extendConfig` | function | Extends the config by applying merged object configs | | `enablePortableBuild` | function | Omits `Meteor.isDevelopment` and `Meteor.isProduction` from the bundle, making it portable across environments | @@ -421,6 +422,46 @@ With the Meteor–Rspack integration, `zodern:melte` no longer works. Use the of Meteor-Rspack comes with built-in CSS support. You can import any CSS file into your code, and it will be processed and included in your HTML skeleton automatically. In addition, any CSS file placed in the same folder as your Meteor entry point will be processed and added as global styles without the need for explicit imports. +### CSS Modules + +[CSS Modules](https://rspack.rs/guide/tech/css#css-modules) are supported out of the box — any file named `*.module.css` is automatically scoped locally. + +By default, rspack uses **named exports**, so imports look like: + +``` js +import { app } from './App.module.css'; +``` + +If you prefer **default imports** (`import styles from './App.module.css'`), disable `namedExports` on both the `css/auto` and `css/module` parsers: + +``` js +module.exports = defineConfig(Meteor => ({ + module: { + parser: { + 'css/auto': { + namedExports: false, + }, + 'css/module': { + namedExports: false, + }, + }, + }, +})); +``` + +#### TypeScript + +When using CSS Modules with TypeScript, add a declaration file (e.g. `imports/css-modules.d.ts`) so the compiler recognizes `.module.css` imports: + +``` typescript +declare module '*.module.css' { + const classes: { readonly [key: string]: string }; + export default classes; +} +``` + +For more details, check [the official Rspack CSS Modules guide](https://rspack.rs/guide/tech/css#css-modules). + ### Less Less support is available in Meteor-Rspack. You need to replace the existing [Meteor `less` package](https://github.com/meteor/meteor/tree/master/packages/non-core/less) or similar with the Rspack configuration. @@ -628,27 +669,62 @@ module.exports = defineConfig(Meteor => ({ This is a quick configuration for split chunks all within `node_modules` as a `vendor` chunk, if you need more control you can use the [official Rspack split chunks integration guide](https://rspack.rs/guide/optimization/code-splitting#splitchunksplugin). -### Extending SWC config +### Customizing SWC config Rspack uses the SWC configuration to transpile your app code. By default, it inherits any settings from the `.swcrc` file, which also [impacts how Meteor transpiles core and package code](meteor-bundler-optimizations.md#custom-swcrc). -If you want a configuration to apply only to your app code, you can extend the SWC setup using the `Meteor.extendSwcConfig` helper: +If you want a configuration to apply only to your app code (not Meteor packages), two helpers are available: + +#### `Meteor.extendSwcConfig` - smart merge (recommended) + +Merges your custom options on top of Meteor's defaults using a deep merge strategy (the same used by `Meteor.extendConfig`). Only the properties you specify are overridden; everything else (parser settings, React refresh, external helpers, etc) is preserved. ```js const { defineConfig } = require('@meteorjs/rspack'); module.exports = defineConfig(Meteor => ({ - // Extend SWC config + // Add decorator support while keeping all Meteor defaults ...Meteor.extendSwcConfig({ jsc: { parser: { - syntax: 'typescript', + decorators: true, }, }, }), })); ``` +#### `Meteor.replaceSwcConfig` - full replacement + +Discards Meteor's defaults entirely and uses the provided config as-is. Use this when you need complete control over SWC and the smart merge doesn't fit your use case. + +```js +const { defineConfig } = require('@meteorjs/rspack'); + +module.exports = defineConfig(Meteor => ({ + // Full SWC config — no Meteor defaults applied + ...Meteor.replaceSwcConfig({ + jsc: { + parser: { + syntax: 'typescript', + tsx: true, + decorators: true, + }, + target: 'es2020', + transform: { + react: { + runtime: 'automatic', + }, + }, + }, + }), +})); +``` + +:::warning +When using `replaceSwcConfig`, you are responsible for providing all necessary SWC options. Features like React refresh, external helpers and parser defaults that Meteor configures (`Meteor.swcConfigOptions`) will not be applied unless you include them yourself. +::: + ### Interop for Default Imports Meteor originally handled default imports from CommonJS modules automatically. This allowed you to write: diff --git a/v3-docs/docs/community-packages/index.md b/v3-docs/docs/community-packages/index.md index ee31736a45..7de8ec0565 100644 --- a/v3-docs/docs/community-packages/index.md +++ b/v3-docs/docs/community-packages/index.md @@ -24,6 +24,10 @@ Please bear in mind if you are adding a package to this list, try providing as m ## List of Community Packages +#### AI/LLM helpers + +- [Wormhole](./wormhole.md) Meteor Wormhole, MCP and REST API endpoint creator + #### Method/Subscription helpers - [`meteor-rpc`](./meteor-rpc.md), Meteor Methods Evolved with type checking and runtime validation diff --git a/v3-docs/docs/community-packages/wormhole.md b/v3-docs/docs/community-packages/wormhole.md new file mode 100644 index 0000000000..5ddd5a72f4 --- /dev/null +++ b/v3-docs/docs/community-packages/wormhole.md @@ -0,0 +1,138 @@ +# Jam Method + +- `Who maintains the package` – [William Reiske](https://github.com/wreiske/meteor-wormhole/commits?author=wreiske) + +[[toc]] + +## What Is It? + +Meteor Wormhole is a **server-only, Meteor 3.4+ package** that bridges your Meteor methods to the outside world through: + +- **[MCP (Model Context Protocol)](https://modelcontextprotocol.io/)** — The open standard for connecting AI assistants to tools and data. Your methods become MCP tools that Claude, GPT, Cursor, VS Code Copilot, and any MCP-compatible client can discover and invoke. +- **REST API** — Every exposed method also gets a `POST /api/` endpoint. +- **OpenAPI 3.1 spec** — Auto-generated from your method schemas. +- **Swagger UI** — Built-in interactive API docs at `/api/docs`. + +## How It Works + +Two lines to get started: + +```js +import { Wormhole } from 'meteor/wreiske:meteor-wormhole'; + +Wormhole.init(); // That's it — all your methods are now MCP tools +``` + +By default it runs in **"all-in" mode**, which automatically exposes every `Meteor.methods()` call (minus DDP internals, private `_`-prefixed methods, and Accounts methods). You can also run in **"opt-in" mode** for explicit control: + +```js +Wormhole.init({ mode: 'opt-in' }); + +Wormhole.expose('todos.add', { + description: 'Add a new todo item', + inputSchema: { + type: 'object', + properties: { + title: { type: 'string', description: 'The todo title' }, + priority: { type: 'string', enum: ['low', 'medium', 'high'] } + }, + required: ['title'] + } +}); +``` + +Add richer schemas and descriptions, and AI agents get better context about what your tools do and how to call them. + +## Features at a Glance + +- **Zero-config MCP server** — Streamable HTTP transport at `/mcp`, session management, JSON-RPC 2.0 +- **Optional REST bridge** — Enable with `rest: { enabled: true }` for traditional HTTP clients +- **Auto-generated OpenAPI 3.1 spec** with Swagger UI +- **Optional API key auth** — Covers both MCP and REST endpoints +- **Smart exclusions** — Automatically skips DDP internals, `_private` methods, and Accounts methods; add your own patterns +- **Input validation** — JSON Schema → Zod conversion for parameter validation +- **Error propagation** — `Meteor.Error` details are properly passed through to clients +- **Enrich existing methods** — Add descriptions and schemas to auto-registered methods with `Wormhole.expose()` + +## Configuration Options + +```js +Wormhole.init({ + mode: 'all', // 'all' or 'opt-in' + path: '/mcp', // MCP endpoint path + name: 'my-app', // MCP server name + apiKey: 'secret', // Optional bearer token auth + exclude: [/^admin\./], // Additional exclusion patterns + rest: { + enabled: true, // Enable REST API + path: '/api', // REST base path + docs: true // Swagger UI at /api/docs + } +}); +``` + +## Point Your MCP Client at It + +If you use Claude Desktop, Cursor, VS Code Copilot, or any other MCP-compatible client, you can connect to a Wormhole-enabled app and your AI assistant will immediately see all the exposed methods as callable tools. Just point it at your app's `/mcp` endpoint. + +## API Reference + +### `Wormhole.init(options)` + +Initialize the MCP bridge. + +| Option | Type | Default | Description | +| --------- | ---------------------- | ------------------- | ---------------------------------- | +| `mode` | `'all' \| 'opt-in'` | `'all'` | Exposure mode | +| `path` | `string` | `'/mcp'` | HTTP endpoint path | +| `name` | `string` | `'meteor-wormhole'` | MCP server name | +| `version` | `string` | `'1.0.0'` | MCP server version | +| `apiKey` | `string \| null` | `null` | Bearer token for auth | +| `exclude` | `(string \| RegExp)[]` | `[]` | Methods to exclude (all-in mode) | +| `rest` | `object \| boolean` | `false` | REST API configuration (see below) | + +#### `rest` options + +| Option | Type | Default | Description | +| --------- | ---------------- | ----------- | -------------------------------------------- | +| `enabled` | `boolean` | `false` | Enable REST endpoints | +| `path` | `string` | `'/api'` | Base path for REST endpoints | +| `docs` | `boolean` | `true` | Serve Swagger UI at `/docs` | +| `apiKey` | `string \| null` | _inherited_ | API key for REST (defaults to main `apiKey`) | + +Shorthand: `rest: true` enables REST with all defaults. + +### `Wormhole.expose(methodName, options)` + +Explicitly expose a method as an MCP tool. + +| Option | Type | Description | +| -------------- | -------- | --------------------------------------------------------------------------------------- | +| `description` | `string` | Human-readable tool description | +| `inputSchema` | `object` | JSON Schema for method parameters | +| `outputSchema` | `object` | JSON Schema for the return value (wrapped inside `{ result }` envelope in OpenAPI/REST) | + +### `Wormhole.unexpose(methodName)` + +Remove a method from MCP exposure. + +## How It Works + +1. **Registration**: In all-in mode, the package monkey-patches `Meteor.methods` to intercept every method registration. In opt-in mode, you call `Wormhole.expose()` manually. + +2. **MCP Server**: A Streamable HTTP MCP server is mounted at the configured path (default `/mcp`) on Meteor's `WebApp`. + +3. **Tool Mapping**: Each exposed Meteor method becomes an MCP tool. Method names are sanitized (e.g., `todos.add` → `todos_add`). + +4. **Invocation**: When an AI agent calls a tool, the bridge invokes the corresponding Meteor method via `Meteor.callAsync()` and returns the result. + +5. **REST API** (optional): When enabled, a parallel REST bridge mounts at the configured path. Each method gets a `POST` endpoint. An OpenAPI 3.1 spec is auto-generated from the registry's metadata and input schemas, and Swagger UI provides interactive documentation. + + +## Links + +- **GitHub:** https://github.com/wreiske/meteor-wormhole +- **Live Demo:** https://wormhole.meteorapp.com/ +- **Swagger UI:** https://wormhole.meteorapp.com/api/docs +- **Atmosphere:** [https://atmospherejs.com/wreiske/meteor-wormhole](https://atmospherejs.com/wreiske/meteor-wormhole) +- **Packosphere:** [https://packosphere.com/wreiske/meteor-wormhole](https://packosphere.com/wreiske/meteor-wormhole) diff --git a/v3-docs/docs/generators/api-export/generateApiJson.js b/v3-docs/docs/generators/api-export/generateApiJson.js new file mode 100644 index 0000000000..d58c53e3a7 --- /dev/null +++ b/v3-docs/docs/generators/api-export/generateApiJson.js @@ -0,0 +1,57 @@ +/** + * Generates a public JSON file from the JSDoc API data. + * This file is accessible to LLMs at /api-reference.json + */ + +const fs = require('fs'); +const path = require('path'); + +function parseApiData(dataSource) { + const json = dataSource + .replace(/^(?:\/\/.*\n\s*)*export default\s*/, '') + .replace(/;\s*$/, ''); + return JSON.parse(json); +} + +exports.generateApiJson = async function generateApiJson() { + console.log("📦 Generating API reference JSON for LLMs..."); + + const dataPath = path.join(__dirname, '../../data/data.js'); + const publicDir = path.join(__dirname, '../../public'); + const outputPath = path.join(publicDir, 'api-reference.json'); + + // Check if data.js exists + if (!fs.existsSync(dataPath)) { + console.log("⚠️ data/data.js not found. Run 'npm run generate-jsdoc' first."); + return; + } + + try { + const apiData = parseApiData(fs.readFileSync(dataPath, 'utf8')); + + // Create public directory if it doesn't exist + if (!fs.existsSync(publicDir)) { + fs.mkdirSync(publicDir, { recursive: true }); + } + + // Add metadata + const output = { + _meta: { + generator: "Meteor Docs API Export", + generated: new Date().toISOString(), + description: "API reference for Meteor.js - for LLM consumption", + url: "https://docs.meteor.com/api-reference.json" + }, + apis: apiData + }; + + // Write the JSON file + fs.writeFileSync(outputPath, JSON.stringify(output, null, 2)); + + const apiCount = Object.keys(apiData).length; + console.log(`✅ Generated api-reference.json with ${apiCount} APIs`); + + } catch (err) { + console.error("❌ Error generating API JSON:", err.message); + } +}; diff --git a/v3-docs/docs/generators/codegen.js b/v3-docs/docs/generators/codegen.js index a6ed7ab00a..fae6b7a8af 100644 --- a/v3-docs/docs/generators/codegen.js +++ b/v3-docs/docs/generators/codegen.js @@ -1,11 +1,14 @@ const { generateChangelog } = require("./changelog/script.js"); const { listPackages } = require("./packages-listing/script.js"); const { generateMeteorVersions } = require("./meteor-versions/script.js"); +const { generateApiJson } = require("./api-export/generateApiJson.js"); + async function main() { console.log("🚂 Started codegen 🚂"); await generateChangelog(); await listPackages(); await generateMeteorVersions(); + await generateApiJson(); console.log("🚀 Done codegen 🚀"); }