Merge branch 'release-3.4' into feature/http-proxy-3

# Conflicts:
#	package.json
This commit is contained in:
Nacho Codoñer
2025-10-20 16:45:45 +02:00
187 changed files with 4412 additions and 2736 deletions

2
meteor
View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash
BUNDLE_VERSION=22.18.0.22
BUNDLE_VERSION=22.18.0.36
# OS Check. Put here because here is where we download the precompiled
# bundles that are arch specific.

View File

@@ -1,9 +0,0 @@
{
const ctx = import.meta.webpackContext('/', {
recursive: true,
regExp: /\.app-(?:test|spec)s?\.[^.]+$/,
exclude: /(^|\/)(node_modules|\.meteor|_build)(\/|$)/,
mode: 'eager',
});
ctx.keys().forEach(ctx);
}

View File

@@ -1,9 +0,0 @@
{
const ctx = import.meta.webpackContext('/', {
recursive: true,
regExp: /\.(?:test|spec)s?\.[^.]+$/,
exclude: /(^|\/)(node_modules|\.meteor|_build)(\/|$)/,
mode: 'eager',
});
ctx.keys().forEach(ctx);
}

View File

@@ -1,5 +1,5 @@
import { defineConfig as rspackDefineConfig } from '@rspack/cli';
import HtmlRspackPlugin from './plugins/HtmlRspackPlugin.js';
const { defineConfig: rspackDefineConfig } = require('@rspack/cli');
const HtmlRspackPlugin = require('./plugins/HtmlRspackPlugin.js');
/**
* @typedef {import('rspack').Configuration & {
@@ -16,12 +16,13 @@ import HtmlRspackPlugin from './plugins/HtmlRspackPlugin.js';
* @param {ConfigFactory} factory
* @returns {ReturnType<typeof rspackDefineConfig>}
*/
export function defineConfig(factory) {
function defineConfig(factory) {
return rspackDefineConfig(factory);
}
// Export our helper plus passthrough
export default defineConfig;
// Export our helper plus passthrough as default export
module.exports = defineConfig;
// Export the HtmlRspackPlugin
export { HtmlRspackPlugin };
// Export the HtmlRspackPlugin and defineConfig as named exports
module.exports.defineConfig = defineConfig;
module.exports.HtmlRspackPlugin = HtmlRspackPlugin;

View File

@@ -0,0 +1,139 @@
var fs = require('fs');
var path = require('path');
/**
* Reads the .meteorignore file from the given project directory and returns
* the parsed entries. Empty lines and comment lines (starting with #) are filtered out.
*
* @param {string} projectDir - The project directory path
* @returns {string[]} - Array of ignore patterns
*/
const getMeteorIgnoreEntries = function (projectDir) {
const meteorIgnorePath = path.join(projectDir, '.meteorignore');
// Check if .meteorignore file exists
try {
const fileContent = fs.readFileSync(meteorIgnorePath, 'utf8');
// Process each line in the file
const entries = fileContent.split(/\r?\n/)
.map(line => line.trim())
.filter(line => line !== '' && !line.startsWith('#'));
return entries;
} catch (e) {
// If the file doesn't exist or can't be read, return empty array
return [];
}
};
/**
* Creates a glob config array for ignoring specified patterns.
* Transforms .gitignore-style entries into chokidar-compatible glob patterns.
* @param {string[]} entries - Array of .gitignore-style patterns
* @returns {string[]} - Array of glob patterns for chokidar
*/
function createIgnoreGlobConfig(entries = []) {
if (!Array.isArray(entries)) {
throw new Error('Entries must be an array');
}
const globPatterns = [];
entries.forEach(entry => {
// Skip empty entries
if (!entry.trim()) {
return;
}
// Handle comments
if (entry.startsWith('#')) {
return;
}
// Check if it's a negation pattern
const isNegation = entry.startsWith('!');
let pattern = isNegation ? entry.substring(1).trim() : entry.trim();
// Remove leading ./ or / if present
pattern = pattern.replace(/^(\.\/|\/)/g, '');
// If it ends with /, it's a directory pattern, add ** to match all contents
if (pattern.endsWith('/')) {
pattern = pattern.slice(0, -1) + '/**';
}
// If it doesn't include a /, it could match anywhere in the path
if (!pattern.includes('/')) {
pattern = '**/' + pattern;
} else if (!pattern.startsWith('**/') && !pattern.startsWith('/')) {
// If it has a / but doesn't start with **/, add **/ to match anywhere
pattern = '**/' + pattern;
}
// Add the negation back if it was present
if (isNegation) {
pattern = '!' + pattern;
}
globPatterns.push(pattern);
});
return globPatterns;
}
/**
* Creates a regex pattern to match the specified glob patterns.
* Converts glob patterns with * and ** into regex equivalents.
*
* @param {string[]} globPatterns - Array of glob patterns from createIgnoreGlobConfig
* @returns {RegExp} - Regex pattern to match the specified patterns
*/
function createIgnoreRegex(globPatterns) {
if (!Array.isArray(globPatterns) || globPatterns.length === 0) {
throw new Error('globPatterns must be a non-empty array');
}
// Process each glob pattern and convert to regex
const regexPatterns = globPatterns.map(pattern => {
// Skip negation patterns for the regex
if (pattern.startsWith('!')) {
return null;
}
// Escape special regex characters, but not * and /
let regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
// Use a temporary placeholder for ** that won't be affected by the * replacement
// This is necessary because if we directly replace ** with .* and then replace * with [^/]*
const DOUBLE_ASTERISK_PLACEHOLDER = '__DOUBLE_ASTERISK__';
regexPattern = regexPattern.replace(/\*\*/g, DOUBLE_ASTERISK_PLACEHOLDER);
// Convert * to regex equivalent (any number of characters except /)
regexPattern = regexPattern.replace(/\*/g, '[^/]*');
// Convert the ** placeholder to its regex equivalent (any number of characters including /)
regexPattern = regexPattern.replace(new RegExp(DOUBLE_ASTERISK_PLACEHOLDER, 'g'), '.*');
// For absolute paths, we don't want to force the pattern to match from the beginning
// but we still want to ensure it matches to the end of the path segment
regexPattern = '(?:^|/)' + regexPattern + '$';
return regexPattern;
}).filter(pattern => pattern !== null);
if (regexPatterns.length === 0) {
// If all patterns were negations, return a regex that matches nothing
return new RegExp('^$');
}
// Join all patterns with | to create a single regex
const combinedPattern = regexPatterns.join('|');
return new RegExp(combinedPattern);
}
module.exports = {
createIgnoreRegex,
getMeteorIgnoreEntries,
createIgnoreGlobConfig,
};

View File

@@ -3,12 +3,13 @@
* overlapping file extensions in module rules.
*/
import { mergeWithCustomize } from 'webpack-merge';
const { mergeWithCustomize } = require('webpack-merge');
const isEqual = require('fast-deep-equal');
/**
* File extensions to check when determining rule overlaps.
*/
export const EXT_CATALOG = [
const EXT_CATALOG = [
'.tsx', '.ts', '.mts', '.cts',
'.jsx', '.js', '.mjs', '.cjs',
];
@@ -87,6 +88,10 @@ function splitOverlapRulesMerge(aRules, bRules) {
for (let i = 0; i < result.length; i++) {
const aRule = result[i];
const isMergeableRule = isEqual(aRule?.include || [], bRule?.include || []);
if (!isMergeableRule) continue;
// Determine which extensions each rule matches (within our catalog)
const aExts = EXT_CATALOG.filter(ext => ruleMatchesExt(aRule, ext));
const bExts = EXT_CATALOG.filter(ext => ruleMatchesExt(bRule, ext));
@@ -130,7 +135,7 @@ function splitOverlapRulesMerge(aRules, bRules) {
* @param {Function} getter - Function to get the identifier from the plugin
* @returns {Function} Customizer function
*/
export function unique(key, pluginNames = [], getter = item => item.constructor && item.constructor.name) {
function unique(key, pluginNames = [], getter = item => item.constructor && item.constructor.name) {
return (a, b, k) => {
if (k !== key) return undefined;
@@ -183,7 +188,7 @@ export function unique(key, pluginNames = [], getter = item => item.constructor
* @param {Function} [options.warningFn] - Custom warning function that receives the path string
* @returns {Object} The cleaned object with specified paths removed
*/
export function cleanOmittedPaths(obj, options = {}) {
function cleanOmittedPaths(obj, options = {}) {
if (!obj || typeof obj !== 'object') {
return obj;
}
@@ -252,13 +257,35 @@ export function cleanOmittedPaths(obj, options = {}) {
return result;
}
/**
* Normalizes externals configuration to ensure consistent handling.
* @param {Object} config - The configuration object
* @returns {Object} - The normalized configuration
*/
function normalizeExternals(config) {
if (!config || !config.externals) return config;
// Create a deep clone of the config to avoid modifying the original
const result = { ...config };
// If externals is not an array, convert it to an array
if (!Array.isArray(result.externals)) {
result.externals = [result.externals];
}
return result;
}
/**
* Merges webpack/rspack configs with smart handling of overlapping rules.
*
* @param {...Object} configs - Configs to merge
* @returns {Object} Merged config
*/
export function mergeSplitOverlap(...configs) {
function mergeSplitOverlap(...configs) {
// Normalize externals in all configs before merging
const normalizedConfigs = configs.map(normalizeExternals);
return mergeWithCustomize({
customizeArray(a, b, key) {
if (key === 'module.rules') {
@@ -267,6 +294,14 @@ export function mergeSplitOverlap(...configs) {
return splitOverlapRulesMerge(aRules, bRules);
}
// Ensure custom extensions first
if (key === 'resolve.extensions') {
const aRules = Array.isArray(a) ? a : [];
const bRules = Array.isArray(b) ? b : [];
const merged = [...bRules, ...aRules];
return [...new Set(merged)];
}
// Handle plugins uniqueness
if (key === 'plugins') {
return unique(
@@ -279,5 +314,12 @@ export function mergeSplitOverlap(...configs) {
// fall through to default merging
return undefined;
}
})(...configs);
})(...normalizedConfigs);
}
module.exports = {
EXT_CATALOG,
unique,
cleanOmittedPaths,
mergeSplitOverlap
};

View File

@@ -0,0 +1,99 @@
// meteorRspackConfigFactory.js
const { mergeSplitOverlap } = require("./mergeRulesSplitOverlap.js");
const DEFAULT_PREFIX = "meteorRspackConfig";
let counter = 0;
/**
* Create a uniquely keyed Rspack config fragment.
* Example return: { meteorRspackConfig1: { ...customConfig } }
*
* @param {object} customConfig
* @param {{ key?: number|string, prefix?: string }} [opts]
* @returns {Record<string, object>}
*/
function prepareMeteorRspackConfig(customConfig, opts = {}) {
if (!customConfig || typeof customConfig !== "object") {
throw new TypeError("customConfig must be an object");
}
const prefix = opts.prefix || DEFAULT_PREFIX;
let name;
if (opts.key != null) {
const k = String(opts.key).trim();
if (/^\d+$/.test(k)) name = `${prefix}${k}`;
else if (k.startsWith(prefix) && /^\d+$/.test(k.slice(prefix.length)))
name = k;
else
throw new Error(`opts.key must be a positive integer or "${prefix}<n>"`);
const n = parseInt(name.slice(prefix.length), 10);
if (Number.isFinite(n) && n > counter) counter = n;
} else {
counter += 1;
name = `${prefix}${counter}`;
}
return { [name]: customConfig };
}
/**
* Merge all `{prefix}<n>` fragments into `config` using `mergeSplitOverlap`,
* then remove those temporary keys. Mutates `config`.
*
* Position-aware merge:
* Walk the config in insertion order and fold:
* - for a fragment key: out = mergeSplitOverlap(out, fragment)
* - for a normal key: out = mergeSplitOverlap(out, { [key]: value })
*
* Result: fragments behave like spreads at their exact position;
* later inline keys override earlier ones (including fragments).
*
* @param {object} config
* @param {{ prefix?: string }} [opts]
* @returns {object} same (mutated) config
*/
function mergeMeteorRspackFragments(config, opts = {}) {
if (!config || typeof config !== "object" || Array.isArray(config)) {
throw new TypeError("config must be a plain object");
}
const prefix = opts.prefix || DEFAULT_PREFIX;
let out = {};
for (const key of Object.keys(config)) {
const val = config[key];
const isFragment =
typeof key === "string" &&
key.startsWith(prefix) &&
/^\d+$/.test(key.slice(prefix.length));
if (isFragment) {
if (!val || typeof val !== "object" || Array.isArray(val)) {
throw new Error(`Fragment "${key}" must be a plain object`);
}
out = mergeSplitOverlap(out, val);
} else {
out = mergeSplitOverlap(out, { [key]: val });
}
}
// keep object identity; fragments disappear because `out` doesn't include them
replaceObject(config, out);
return config;
}
function replaceObject(target, source) {
for (const k of Object.keys(target)) {
if (!(k in source)) delete target[k];
}
for (const k of Object.keys(source)) {
target[k] = source[k];
}
}
module.exports = {
prepareMeteorRspackConfig,
mergeMeteorRspackFragments,
};

View File

@@ -0,0 +1,106 @@
const path = require("path");
const { prepareMeteorRspackConfig } = require("./meteorRspackConfigFactory");
const { builtinModules } = require("module");
/**
* Resolve a package directory from node resolution.
* @param {string} pkg
* @returns {string} absolute directory of the package
*/
function pkgDir(pkg) {
const resolved = require.resolve(`${pkg}/package.json`, {
paths: [process.cwd()],
});
return path.dirname(resolved);
}
/**
* Wrap externals for Meteor runtime (marks deps as externals).
* Usage: compileWithMeteor(["sharp", "vimeo", "fs"])
*
* @param {string[]} deps - package names or module IDs
* @returns {Record<string, object>} `{ meteorRspackConfigX: { externals: [...] } }`
*/
function compileWithMeteor(deps) {
const flat = deps.flat().filter(Boolean);
return prepareMeteorRspackConfig({
externals: flat,
});
}
/**
* Add SWC transpilation rules limited to specific deps (monorepo-friendly).
* Usage: compileWithRspack(["@org/lib-a", "zod"])
*
* Requires global `Meteor.swcConfigOptions` (as in your setup).
*
* @param {string[]} deps - package names to include in SWC loader
* @returns {Record<string, object>} `{ meteorRspackConfigX: { module: { rules: [...] } } }`
*/
function compileWithRspack(deps, { options = {} } = {}) {
const includeDirs = deps.flat().filter(Boolean).map(pkgDir);
return prepareMeteorRspackConfig({
module: {
rules: [
{
test: /\.(?:[mc]?js|jsx|[mc]?ts|tsx)$/i,
include: includeDirs,
loader: "builtin:swc-loader",
options,
},
],
},
});
}
/**
* Enable or disable Rspack cache config
* Usage: setCache(false)
*
* @param {boolean} enabled
* @param {Record<string, object>} cacheConfig
* @returns {Record<string, object>} `{ meteorRspackConfigX: { cache: {} } }`
*/
function setCache(
enabled,
cacheConfig = { cache: true, experiments: { cache: true } },
) {
return prepareMeteorRspackConfig(
enabled
? cacheConfig
: {
cache: false, // disable cache
experiments: {
cache: false, // disable persistent cache (experimental flag)
},
},
);
}
/**
* Build an alias map that disables ALL Node core modules in a web build.
* - Includes both 'fs' and 'node:fs' keys
* - Optional extras let you block non-core modules too
*/
function makeWebNodeBuiltinsAlias(extras = []) {
// Strip potential 'node:' prefixes then add both forms
const core = new Set(builtinModules.map((m) => m.replace(/^node:/, "")));
const names = new Set();
for (const m of core) {
names.add(m); // e.g. 'fs'
names.add(`node:${m}`); // e.g. 'node:fs'
}
for (const x of extras) names.add(x);
// Map every name to false (causes hard error if imported)
return Object.fromEntries([...names].map((m) => [m, false]));
}
module.exports = {
compileWithMeteor,
compileWithRspack,
setCache,
makeWebNodeBuiltinsAlias,
};

View File

@@ -1,12 +1,12 @@
import fs from 'fs';
import vm from 'vm';
const fs = require('fs');
const vm = require('vm');
/**
* Reads and parses the SWC configuration file.
* @param {string} file - The name of the SWC configuration file (default: '.swcrc')
* @returns {Object|undefined} The parsed SWC configuration or undefined if an error occurs
*/
export function getMeteorAppSwcrc(file = '.swcrc') {
function getMeteorAppSwcrc(file = '.swcrc') {
try {
const filePath = `${process.cwd()}/${file}`;
if (file.endsWith('.js')) {
@@ -42,7 +42,7 @@ export function getMeteorAppSwcrc(file = '.swcrc') {
* If the configuration has a baseUrl property, it will be set to process.cwd().
* @returns {Object|undefined} The SWC configuration or undefined if no configuration exists
*/
export function getMeteorAppSwcConfig() {
function getMeteorAppSwcConfig() {
const hasSwcRc = fs.existsSync(`${process.cwd()}/.swcrc`);
const hasSwcJs = !hasSwcRc && fs.existsSync(`${process.cwd()}/swc.config.js`);
@@ -60,3 +60,8 @@ export function getMeteorAppSwcConfig() {
return config;
}
module.exports = {
getMeteorAppSwcrc,
getMeteorAppSwcConfig
};

View File

@@ -0,0 +1,62 @@
const fs = require('fs');
const path = require('path');
const { createIgnoreRegex, createIgnoreGlobConfig } = require("./ignore.js");
/**
* Generates eager test files dynamically
* @param {Object} options - Options for generating the test file
* @param {boolean} options.isAppTest - Whether this is an app test
* @param {string} options.projectDir - The project directory
* @param {string} options.buildContext - The build context
* @param {string[]} options.entries - Array of ignore patterns
* @returns {string} The path to the generated file
*/
const generateEagerTestFile = ({
isAppTest,
projectDir,
buildContext,
entries = [],
}) => {
const distDir = path.resolve(projectDir, ".meteor/local/test");
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}
// Combine all ignore entries
const ignoreEntries = [
"**/node_modules/**",
"**/.meteor/**",
"**/public/**",
"**/private/**",
`**/${buildContext}/**`,
...entries,
];
// Create regex from ignore entries
const excludeFoldersRegex = createIgnoreRegex(
createIgnoreGlobConfig(ignoreEntries)
);
const filename = isAppTest ? "eager-app-tests.mjs" : "eager-tests.mjs";
const filePath = path.resolve(distDir, filename);
const regExp = isAppTest
? "/\\.app-(?:test|spec)s?\\.[^.]+$/"
: "/\\.(?:test|spec)s?\\.[^.]+$/";
const content = `{
const ctx = import.meta.webpackContext('/', {
recursive: true,
regExp: ${regExp},
exclude: ${excludeFoldersRegex.toString()},
mode: 'eager',
});
ctx.keys().forEach(ctx);
}`;
fs.writeFileSync(filePath, content);
return filePath;
};
module.exports = {
generateEagerTestFile,
};

View File

@@ -1,14 +1,15 @@
{
"name": "@meteorjs/rspack",
"version": "0.0.36",
"version": "0.0.60",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@meteorjs/rspack",
"version": "0.0.36",
"version": "0.0.60",
"license": "ISC",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"ignore-loader": "^0.1.2",
"webpack-merge": "^6.0.1"
},
@@ -1398,8 +1399,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.6",

View File

@@ -1,12 +1,13 @@
{
"name": "@meteorjs/rspack",
"version": "0.0.36",
"version": "0.0.60",
"description": "Configuration logic for using Rspack in Meteor projects",
"main": "index.js",
"type": "module",
"type": "commonjs",
"author": "",
"license": "ISC",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"ignore-loader": "^0.1.2",
"webpack-merge": "^6.0.1"
},

View File

@@ -1,10 +1,11 @@
import RspackMeteorHtmlPlugin, { loadHtmlRspackPluginFromHost } from './RspackMeteorHtmlPlugin.js';
const RspackMeteorHtmlPlugin = require('./RspackMeteorHtmlPlugin.js');
const { loadHtmlRspackPluginFromHost } = RspackMeteorHtmlPlugin;
/**
* A plugin that composes the original HtmlRspackPlugin from @rspack/core
* and RspackMeteorHtmlPlugin, in that order.
*/
export default class HtmlRspackPlugin {
class HtmlRspackPlugin {
constructor(options = {}) {
this.options = options;
}
@@ -26,3 +27,5 @@ export default class HtmlRspackPlugin {
meteorPlugin.apply(compiler);
}
}
module.exports = HtmlRspackPlugin;

View File

@@ -7,10 +7,10 @@
// the externalMap function if provided.
// Used for Blaze to translate require of html files to require of js files bundled by Meteor.
import fs from 'fs';
import path from 'path';
const fs = require('fs');
const path = require('path');
export class RequireExternalsPlugin {
class RequireExternalsPlugin {
constructor({
filePath,
// Externals can be:
@@ -487,3 +487,5 @@ export class RequireExternalsPlugin {
return existing;
}
}
module.exports = { RequireExternalsPlugin };

View File

@@ -1,7 +1,7 @@
import path from 'node:path';
import { createRequire } from 'node:module';
const path = require('node:path');
const { createRequire } = require('node:module');
export function loadHtmlRspackPluginFromHost(compiler) {
function loadHtmlRspackPluginFromHost(compiler) {
// Prefer the compiler's context; fall back to process.cwd()
const ctx = compiler.options?.context || compiler.context || process.cwd();
const requireFromHost = createRequire(path.join(ctx, 'package.json'));
@@ -16,7 +16,7 @@ export function loadHtmlRspackPluginFromHost(compiler) {
* 1. Remove the injected `*-rspack.js` script tags
* 2. Strip <!doctype> and <html>…</html> wrappers from the final HTML
*/
export default class RspackMeteorHtmlPlugin {
class RspackMeteorHtmlPlugin {
apply(compiler) {
const HtmlRspackPlugin = loadHtmlRspackPluginFromHost(compiler);
if (!HtmlRspackPlugin?.getCompilationHooks) {
@@ -45,3 +45,6 @@ export default class RspackMeteorHtmlPlugin {
});
}
}
module.exports = RspackMeteorHtmlPlugin;
module.exports.loadHtmlRspackPluginFromHost = loadHtmlRspackPluginFromHost;

View File

@@ -1,16 +1,22 @@
import { DefinePlugin, BannerPlugin } from '@rspack/core';
import fs from 'fs';
import { createRequire } from 'module';
import { inspect } from 'node:util';
import path from 'path';
import { merge } from 'webpack-merge';
const { DefinePlugin, BannerPlugin, NormalModuleReplacementPlugin } = require('@rspack/core');
const fs = require('fs');
const { inspect } = require('node:util');
const path = require('path');
const { merge } = require('webpack-merge');
import { cleanOmittedPaths, mergeSplitOverlap } from "./lib/mergeRulesSplitOverlap.js";
import { getMeteorAppSwcConfig } from './lib/swc.js';
import HtmlRspackPlugin from './plugins/HtmlRspackPlugin.js';
import { RequireExternalsPlugin } from './plugins/RequireExtenalsPlugin.js';
const require = createRequire(import.meta.url);
const { cleanOmittedPaths, mergeSplitOverlap } = require("./lib/mergeRulesSplitOverlap.js");
const { getMeteorAppSwcConfig } = require('./lib/swc.js');
const HtmlRspackPlugin = require('./plugins/HtmlRspackPlugin.js');
const { RequireExternalsPlugin } = require('./plugins/RequireExtenalsPlugin.js');
const { generateEagerTestFile } = require("./lib/test.js");
const { getMeteorIgnoreEntries, createIgnoreGlobConfig } = require("./lib/ignore");
const { mergeMeteorRspackFragments } = require("./lib/meteorRspackConfigFactory.js");
const {
compileWithMeteor,
compileWithRspack,
setCache,
makeWebNodeBuiltinsAlias,
} = require('./lib/meteorRspackHelpers.js');
// Safe require that doesn't throw if the module isn't found
function safeRequire(moduleName) {
@@ -28,17 +34,52 @@ function safeRequire(moduleName) {
}
// Persistent filesystem cache strategy
function createCacheStrategy(mode) {
function createCacheStrategy(mode, side, { projectConfigPath, configPath } = {}) {
// Check for configuration files
const tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
const hasTsconfig = fs.existsSync(tsconfigPath);
const babelRcConfig = path.join(process.cwd(), '.babelrc');
const hasBabelRcConfig = fs.existsSync(babelRcConfig);
const babelJsConfig = path.join(process.cwd(), 'babel.config.js');
const hasBabelJsConfig = fs.existsSync(babelJsConfig);
const swcrcPath = path.join(process.cwd(), '.swcrc');
const hasSwcrcConfig = fs.existsSync(swcrcPath);
const swcJsPath = path.join(process.cwd(), 'swc.config.js');
const hasSwcJsConfig = fs.existsSync(swcJsPath);
const postcssConfigPath = path.join(process.cwd(), 'postcss.config.js');
const hasPostcssConfig = fs.existsSync(postcssConfigPath);
const packageLockPath = path.join(process.cwd(), 'package-lock.json');
const hasPackageLock = fs.existsSync(packageLockPath);
const yarnLockPath = path.join(process.cwd(), 'yarn.lock');
const hasYarnLock = fs.existsSync(yarnLockPath);
// Build dependencies array
const buildDependencies = [
...(projectConfigPath ? [projectConfigPath] : []),
...(configPath ? [configPath] : []),
...(hasTsconfig ? [tsconfigPath] : []),
...(hasBabelRcConfig ? [babelRcConfig] : []),
...(hasBabelJsConfig ? [babelJsConfig] : []),
...(hasSwcrcConfig ? [swcrcPath] : []),
...(hasSwcJsConfig ? [swcJsPath] : []),
...(hasPostcssConfig ? [postcssConfigPath] : []),
...(hasPackageLock ? [packageLockPath] : []),
...(hasYarnLock ? [yarnLockPath] : []),
].filter(Boolean);
return {
cache: true,
experiments: {
cache: {
version: `swc-${mode}`,
type: 'persistent',
version: `cache-${mode}${(side && `-${side}`) || ""}`,
type: "persistent",
storage: {
type: 'filesystem',
directory: 'node_modules/.cache/rspack',
type: "filesystem",
directory: `node_modules/.cache/rspack${(side && `/${side}`) || ""}`,
},
...(buildDependencies.length > 0 && {
buildDependencies: buildDependencies,
})
},
},
};
@@ -47,32 +88,48 @@ function createCacheStrategy(mode) {
// SWC loader rule (JSX/JS)
function createSwcConfig({
isTypescriptEnabled,
isReactEnabled,
isJsxEnabled,
isTsxEnabled,
externalHelpers,
isDevEnvironment,
isClient,
}) {
const defaultConfig = {
jsc: {
baseUrl: process.cwd(),
paths: { '/*': ['*'] },
paths: { '/*': ['*', '/*'] },
parser: {
syntax: isTypescriptEnabled ? 'typescript' : 'ecmascript',
...(isTsxEnabled && { tsx: true }),
...(isJsxEnabled && { jsx: true }),
},
target: 'es2015',
transform: {
react: {
development: isDevEnvironment,
refresh: isDevEnvironment,
...(isReactEnabled && {
transform: {
react: {
development: isDevEnvironment,
...(isClient && { refresh: isDevEnvironment }),
},
},
},
}),
externalHelpers,
},
};
// Swcrc config not customizable
const omitPaths = [
'jsc.target',
];
// Define warning function
const warningFn = path => {
console.warn(
`[.swcrc] Ignored custom "${path}" — reserved for Meteor-Rspack integration.`,
);
};
const customConfig = getMeteorAppSwcConfig() || {};
const swcConfig = merge(defaultConfig, customConfig);
const cleanedCustomConfig = cleanOmittedPaths(customConfig, { omitPaths, warningFn });
const swcConfig = merge(defaultConfig, cleanedCustomConfig);
return {
test: /\.(?:[mc]?js|jsx|[mc]?ts|tsx)$/i,
exclude: /node_modules|\.meteor\/local/,
@@ -81,6 +138,43 @@ function createSwcConfig({
};
}
function createRemoteDevServerConfig() {
const rootUrl = process.env.ROOT_URL;
let hostname;
let protocol;
let port;
if (rootUrl) {
try {
const url = new URL(rootUrl);
// Detect if it's remote (not localhost or 127.x)
const isLocal =
url.hostname.includes('localhost') ||
url.hostname.startsWith('127.') ||
url.hostname.endsWith('.local');
if (!isLocal) {
hostname = url.hostname;
protocol = url.protocol === 'https:' ? 'wss' : 'ws';
port = url.port ? Number(url.port) : (url.protocol === 'https:' ? 443 : 80);
return {
client: {
webSocketURL: {
hostname,
port,
protocol,
},
},
};
}
} catch (err) {
console.warn(`Invalid ROOT_URL "${rootUrl}", falling back to localhost`);
}
}
// If local doesn't provide any extra config
return {};
}
// Keep files outside of build folders
function keepOutsideBuild() {
@@ -92,17 +186,12 @@ function keepOutsideBuild() {
};
}
// Watch options shared across both builds
const defaultWatchOptions = {
ignored: ['**/.meteor/local/**', '**/dist/**'],
};
/**
* @param {{ isClient: boolean; isServer: boolean; isDevelopment?: boolean; isProduction?: boolean; isTest?: boolean }} Meteor
* @param {{ mode?: string; clientEntry?: string; serverEntry?: string; clientOutputFolder?: string; serverOutputFolder?: string; chunksContext?: string; assetsContext?: string; serverAssetsContext?: string }} argv
* @returns {import('@rspack/cli').Configuration[]}
* @returns {Promise<import('@rspack/cli').Configuration[]>}
*/
export default function (inMeteor = {}, argv = {}) {
module.exports = async function (inMeteor = {}, argv = {}) {
// Transform Meteor env properties to proper boolean values
const Meteor = { ...inMeteor };
// Convert string boolean values to actual booleans
@@ -118,7 +207,9 @@ export default function (inMeteor = {}, argv = {}) {
const isDev = !!Meteor.isDevelopment || !isProd;
const isTest = !!Meteor.isTest;
const isClient = !!Meteor.isClient;
const isServer = !!Meteor.isServer;
const isRun = !!Meteor.isRun;
const isBuild = !!Meteor.isBuild;
const isReactEnabled = !!Meteor.isReactEnabled;
const isTestModule = !!Meteor.isTestModule;
const isTestEager = !!Meteor.isTestEager;
@@ -126,6 +217,9 @@ export default function (inMeteor = {}, argv = {}) {
const swcExternalHelpers = !!Meteor.swcExternalHelpers;
const isNative = !!Meteor.isNative;
const mode = isProd ? 'production' : 'development';
const projectDir = process.cwd();
const projectConfigPath = Meteor.projectConfigPath || path.resolve(projectDir, 'rspack.config.js');
const configPath = Meteor.configPath;
const isTypescriptEnabled = Meteor.isTypescriptEnabled || false;
const isJsxEnabled =
@@ -147,11 +241,11 @@ export default function (inMeteor = {}, argv = {}) {
const runPath = Meteor.runPath;
// Determine banner
const bannerOutput = JSON.parse(Meteor.bannerOutput || '');
const bannerOutput = JSON.parse(Meteor.bannerOutput || process.env.RSPACK_BANNER || '""');
// Determine output directories
const clientOutputDir = path.resolve(process.cwd(), 'public');
const serverOutputDir = path.resolve(process.cwd(), 'private');
const clientOutputDir = path.resolve(projectDir, 'public');
const serverOutputDir = path.resolve(projectDir, 'private');
// Determine context for bundles and assets
const buildContext = Meteor.buildContext || '_build';
@@ -159,9 +253,27 @@ export default function (inMeteor = {}, argv = {}) {
const chunksContext = Meteor.chunksContext || 'build-chunks';
// Determine build output and pass to Meteor
const buildOutputDir = path.resolve(process.cwd(), buildContext, outputDir);
const buildOutputDir = path.resolve(projectDir, buildContext, outputDir);
Meteor.buildOutputDir = buildOutputDir;
const cacheStrategy = createCacheStrategy(
mode,
(Meteor.isClient && 'client') || 'server',
{ projectConfigPath, configPath }
);
// Expose Meteor's helpers to expand Rspack configs
Meteor.compileWithMeteor = deps => compileWithMeteor(deps);
Meteor.compileWithRspack = deps =>
compileWithRspack(deps, {
options: Meteor.swcConfigOptions,
});
Meteor.setCache = enabled =>
setCache(
!!enabled,
enabled === 'memory' ? undefined : cacheStrategy
);
// Add HtmlRspackPlugin function to Meteor
Meteor.HtmlRspackPlugin = (options = {}) => {
return new HtmlRspackPlugin({
@@ -184,18 +296,26 @@ export default function (inMeteor = {}, argv = {}) {
});
};
// Set watch options
// Get Meteor ignore entries
const meteorIgnoreEntries = getMeteorIgnoreEntries(projectDir);
// Additional ignore entries
const additionalEntries = [
"**/.meteor/local/**",
"**/dist/**",
...(isTest && isTestEager
? [`**/${buildContext}/**`, "**/.meteor/local/**", "node_modules/**"]
: []),
];
// Set default watch options
const watchOptions = {
...defaultWatchOptions,
...(isTest &&
isTestEager && {
ignored: [
...defaultWatchOptions.ignored,
'**/_build/**',
'**/.meteor/local/**',
'**/node_modules/**',
],
}),
ignored: [
...createIgnoreGlobConfig([
...meteorIgnoreEntries,
...additionalEntries,
]),
],
};
if (Meteor.isDebug || Meteor.isVerbose) {
@@ -203,13 +323,16 @@ export default function (inMeteor = {}, argv = {}) {
console.log('[i] Meteor flags:', Meteor);
}
const enableSwcExternalHelpers = !isServer && swcExternalHelpers;
const isDevEnvironment = isRun && isDev && !isTest && !isNative;
const swcConfigRule = createSwcConfig({
isTypescriptEnabled,
isReactEnabled,
isJsxEnabled,
isTsxEnabled,
externalHelpers: swcExternalHelpers,
externalHelpers: enableSwcExternalHelpers,
isDevEnvironment,
isClient,
});
// Expose swc config to use in custom configs
Meteor.swcConfigOptions = swcConfigRule.options;
@@ -217,10 +340,14 @@ export default function (inMeteor = {}, argv = {}) {
const externals = [
/^meteor.*/,
...(isReactEnabled ? [/^react$/, /^react-dom$/] : []),
...(isServer ? [/^bcrypt$/] : []),
];
const alias = {
'/': path.resolve(process.cwd()),
};
const fallback = {
...(isClient && makeWebNodeBuiltinsAlias()),
};
const extensions = [
'.ts',
'.tsx',
@@ -248,16 +375,26 @@ export default function (inMeteor = {}, argv = {}) {
lastImports: [`./${outputFilename}`],
}),
}),
enableGlobalPolyfill: isDevEnvironment,
enableGlobalPolyfill: isDevEnvironment && !isServer,
});
const rsdoctorModule = isBundleVisualizerEnabled
? safeRequire('@rsdoctor/rspack-plugin')
: null;
const doctorPluginConfig = isBundleVisualizerEnabled && rsdoctorModule?.RsdoctorRspackPlugin
const doctorPluginConfig = isRun && isBundleVisualizerEnabled && rsdoctorModule?.RsdoctorRspackPlugin
? [
new rsdoctorModule.RsdoctorRspackPlugin({
port: isClient ? 8081 : 8082,
port: isClient
? (parseInt(Meteor.rsdoctorClientPort || '8888', 10))
: (parseInt(Meteor.rsdoctorServerPort || '8889', 10)),
}),
]
: [];
const bannerPluginConfig = !isBuild
? [
new BannerPlugin({
banner: bannerOutput,
entryOnly: true,
}),
]
: [];
@@ -269,13 +406,24 @@ export default function (inMeteor = {}, argv = {}) {
let clientConfig = {
name: clientNameConfig,
target: 'web',
mode: 'development',
mode,
entry: path.resolve(process.cwd(), buildContext, entryPath),
output: {
path: clientOutputDir,
filename: () =>
isDevEnvironment ? outputFilename : `../${buildContext}/${outputPath}`,
libraryTarget: 'commonjs',
filename: (_module) => {
const chunkName = _module.chunk?.name;
const isMainChunk = !chunkName || chunkName === "main";
const chunkSuffix = `${chunksContext}/[id]${
isProd ? '.[chunkhash]' : ''
}.js`;
if (isDevEnvironment) {
if (isMainChunk) return outputFilename;
return chunkSuffix;
}
if (isMainChunk) return `../${buildContext}/${outputPath}`;
return chunkSuffix;
},
libraryTarget: 'commonjs2',
publicPath: '/',
chunkFilename: `${chunksContext}/[id]${isProd ? '.[chunkhash]' : ''}.js`,
assetModuleFilename: `${assetsContext}/[hash][ext][query]`,
@@ -305,7 +453,7 @@ export default function (inMeteor = {}, argv = {}) {
...extraRules,
],
},
resolve: { extensions, alias },
resolve: { extensions, alias, fallback },
externals,
plugins: [
...[
@@ -322,17 +470,18 @@ export default function (inMeteor = {}, argv = {}) {
'Meteor.isDevelopment': JSON.stringify(isDev),
'Meteor.isProduction': JSON.stringify(isProd),
}),
new BannerPlugin({
banner: bannerOutput,
entryOnly: true,
}),
...bannerPluginConfig,
Meteor.HtmlRspackPlugin(),
...doctorPluginConfig,
new NormalModuleReplacementPlugin(/^node:(.*)$/, (res) => {
res.request = res.request.replace(/^node:/, '');
}),
],
watchOptions,
devtool: isDevEnvironment || isNative || isTest ? 'source-map' : 'hidden-source-map',
...(isDevEnvironment && {
devServer: {
...createRemoteDevServerConfig(),
static: { directory: clientOutputDir, publicPath: '/__rspack__/' },
hot: true,
liveReload: true,
@@ -344,15 +493,26 @@ export default function (inMeteor = {}, argv = {}) {
},
},
}),
experiments: { css: true },
...merge(cacheStrategy, { experiments: { css: true } })
};
const serverEntry =
isTest && isTestEager && isTestFullApp
? path.resolve(process.cwd(), 'node_modules/@meteorjs/rspack/entries/eager-app-tests.js')
? generateEagerTestFile({
isAppTest: true,
projectDir,
buildContext,
entries: meteorIgnoreEntries,
})
: isTest && isTestEager
? path.resolve(process.cwd(), 'node_modules/@meteorjs/rspack/entries/eager-tests.js')
: path.resolve(process.cwd(), buildContext, entryPath);
? generateEagerTestFile({
isAppTest: false,
projectDir,
buildContext,
entries: meteorIgnoreEntries,
})
: path.resolve(projectDir, buildContext, entryPath);
const serverNameConfig = `[${(isTest && 'test-') || ''}${
(isTestModule && 'module') || 'server'
}-rspack]`;
@@ -365,12 +525,16 @@ export default function (inMeteor = {}, argv = {}) {
output: {
path: serverOutputDir,
filename: () => `../${buildContext}/${outputPath}`,
libraryTarget: 'commonjs',
libraryTarget: 'commonjs2',
chunkFilename: `${chunksContext}/[id]${isProd ? '.[chunkhash]' : ''}.js`,
assetModuleFilename: `${assetsContext}/[hash][ext][query]`,
...(isProd && { clean: { keep: keepOutsideBuild() } }),
},
optimization: { usedExports: true },
optimization: {
usedExports: true,
splitChunks: false,
runtimeChunk: false,
},
module: {
rules: [swcConfigRule, ...extraRules],
parser: {
@@ -383,10 +547,11 @@ export default function (inMeteor = {}, argv = {}) {
resolve: {
extensions,
alias,
modules: ['node_modules', path.resolve(process.cwd())],
modules: ['node_modules', path.resolve(projectDir)],
conditionNames: ['import', 'require', 'node', 'default'],
},
externals,
externalsPresets: { node: true },
plugins: [
new DefinePlugin(
isTest && (isTestModule || isTestEager)
@@ -404,27 +569,47 @@ export default function (inMeteor = {}, argv = {}) {
'Meteor.isProduction': JSON.stringify(isProd),
},
),
new BannerPlugin({
banner: bannerOutput,
entryOnly: true,
}),
isTestModule && requireExternalsPlugin,
...bannerPluginConfig,
requireExternalsPlugin,
...doctorPluginConfig,
],
watchOptions,
devtool: isDevEnvironment || isNative || isTest ? 'source-map' : 'hidden-source-map',
...((isDevEnvironment || (isTest && !isTestEager) || isNative) &&
createCacheStrategy(mode)),
cacheStrategy),
};
// Load and apply project-level overrides for the selected build
const projectConfigPath = path.resolve(process.cwd(), 'rspack.config.js');
// Check if we're in a Meteor package directory by looking at the path
const isMeteorPackageConfig = process.cwd().includes('/packages/rspack');
const isMeteorPackageConfig = projectDir.includes('/packages/rspack');
if (fs.existsSync(projectConfigPath) && !isMeteorPackageConfig) {
const projectConfig =
require(projectConfigPath)?.default || require(projectConfigPath);
// Check if there's a .mjs or .cjs version of the config file
const mjsConfigPath = projectConfigPath.replace(/\.js$/, '.mjs');
const cjsConfigPath = projectConfigPath.replace(/\.js$/, '.cjs');
let configPath = projectConfigPath;
if (fs.existsSync(mjsConfigPath)) {
configPath = mjsConfigPath;
} else if (fs.existsSync(cjsConfigPath)) {
configPath = cjsConfigPath;
}
// Use require for CommonJS modules and dynamic import for ES modules
let projectConfig;
try {
if (path.extname(configPath) === '.mjs') {
// For ESM modules, we need to use dynamic import
const fileUrl = `file://${configPath}`;
const module = await import(fileUrl);
projectConfig = module.default || module;
} else {
// For CommonJS modules, we can use require
projectConfig = require(configPath)?.default || require(configPath);
}
} catch (error) {
console.error(`Error loading rspack config from ${configPath}:`, error);
throw error;
}
const userConfig =
typeof projectConfig === 'function'
@@ -432,29 +617,38 @@ export default function (inMeteor = {}, argv = {}) {
: projectConfig;
const omitPaths = [
'name',
'target',
'entry',
'output.path',
'output.filename',
'output.publicPath',
];
"name",
"target",
"entry",
"output.path",
"output.filename",
"output.publicPath",
...(Meteor.isServer
? ["optimization.splitChunks", "optimization.runtimeChunk"]
: []),
].filter(Boolean);
const warningFn = path => {
console.warn(
`[rspack.config.js] Ignored custom "${path}" — reserved for Meteor-Rspack integration.`,
);
};
let nextUserConfig = cleanOmittedPaths(userConfig, {
omitPaths,
warningFn,
});
nextUserConfig = mergeMeteorRspackFragments(nextUserConfig);
if (Meteor.isClient) {
clientConfig = mergeSplitOverlap(
clientConfig,
cleanOmittedPaths(userConfig, { omitPaths, warningFn }),
nextUserConfig
);
}
if (Meteor.isServer) {
serverConfig = mergeSplitOverlap(
serverConfig,
cleanOmittedPaths(userConfig, { omitPaths, warningFn }),
nextUserConfig
);
}
}

View File

@@ -11,10 +11,6 @@
"url": "https://github.com/meteor/meteor/issues"
},
"homepage": "https://www.meteor.com/",
"scripts": {
"install:modern": "cd tools/modern-tests && npm install && npx playwright install && npx playwright install --with-deps",
"test:modern": "cd tools/modern-tests && npm test -- "
},
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/eslint-parser": "^7.21.3",
@@ -39,7 +35,9 @@
"typescript": "^5.4.5"
},
"scripts": {
"test:idle-bot": "node --test .github/scripts/__tests__/inactive-issues.test.js"
"install:modern": "cd tools/modern-tests && npm install && npx playwright install && npx playwright install --with-deps",
"test:idle-bot": "node --test .github/scripts/__tests__/inactive-issues.test.js",
"test:modern": "cd tools/modern-tests && npm test -- "
},
"jshintConfig": {
"esversion": 11

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "A user account system",
version: "3.1.2",
version: "3.2.0-beta340.11",
});
Package.onUse((api) => {

View File

@@ -11,6 +11,7 @@ Package.describe({
Npm.depends({
bcrypt: "5.0.1",
argon2: "0.41.1",
"node-gyp-build": "4.8.4",
});
Package.onUse((api) => {

View File

@@ -246,7 +246,7 @@ BCp.processOneFileForTarget = function (inputFile, source) {
// Check if the file is a Rspack output file
// If it is, bypass SWC/Babel and just read the file and its map file
// as the contents are already transpiled by Rspack.
if (Plugin?.rspackHelpers?.isRspackOutputFile(inputFilePath)) {
if (Plugin?.rspackHelpers?.isRspackOutputFile(inputFilePath)) {
try {
// Get the full path to the file
const fullPath = inputFile.getPathInPackage();
@@ -260,6 +260,17 @@ BCp.processOneFileForTarget = function (inputFile, source) {
toBeAdded.sourceMap = JSON.parse(mapContent);
}
if (this.isVerbose()) {
const arch = inputFile.getArch();
logTranspilation({
usedRspack: true,
inputFilePath,
packageName,
cacheHit: true,
arch,
});
}
return toBeAdded;
} catch (e) {
// If there's an error reading the file or map, log it and continue with normal processing
@@ -281,7 +292,8 @@ BCp.processOneFileForTarget = function (inputFile, source) {
const features = Object.assign({}, this.extraFeatures);
const arch = inputFile.getArch();
if (arch.startsWith("os.")) {
const isNodeTarget = arch.startsWith("os.");
if (isNodeTarget) {
// Start with a much simpler set of Babel presets and plugins if
// we're compiling for Node 8.
features.nodeMajorVersion = parseInt(process.versions.node, 10);
@@ -355,8 +367,11 @@ BCp.processOneFileForTarget = function (inputFile, source) {
tsx: hasTSXSupport,
},
...(hasSwcHelpersAvailable &&
!isNodeTarget &&
(packageName == null ||
!['modules-runtime'].includes(packageName)) && {
!['core-runtime', 'modules', 'modules-runtime'].includes(
packageName,
)) && {
externalHelpers: true,
}),
},
@@ -373,6 +388,7 @@ BCp.processOneFileForTarget = function (inputFile, source) {
// Merge with app-level SWC config
if (lastModifiedSwcConfig) {
swcOptions = deepMerge(swcOptions, lastModifiedSwcConfig, [
'jsc.target',
'env.targets',
'module.type',
]);
@@ -1120,14 +1136,16 @@ function logTranspilation({
packageName,
inputFilePath,
usedSwc,
usedRspack,
cacheHit,
isNodeModulesCode,
arch,
errorMessage = '',
tip = '',
}) {
const transpiler = usedSwc ? 'SWC' : 'Babel';
const transpilerColor = usedSwc ? 32 : 33;
let transpiler = usedSwc ? 'SWC' : 'Babel';
transpiler = usedRspack ? 'Rspack' : transpiler;
const transpilerColor = usedSwc || usedRspack ? 32 : 33;
const label = color('[Transpiler]', 36);
const transpilerPart = `${label} Used ${color(
transpiler,
@@ -1150,7 +1168,7 @@ function logTranspilation({
: color(originPaddedRaw, 35);
const cacheStatus = errorMessage
? color('⚠️ Fallback', 33)
: usedSwc
: usedSwc || usedRspack
? cacheHit
? color('🟢 Cache hit', 32)
: color('🔴 Cache miss', 31)

View File

@@ -1,14 +1,14 @@
Package.describe({
name: "babel-compiler",
summary: "Parser/transpiler for ECMAScript 2015+ syntax",
version: '7.12.2',
version: '7.13.0-beta340.11',
});
Npm.depends({
'@meteorjs/babel': '7.20.1',
'json5': '2.2.3',
'semver': '7.6.3',
"@meteorjs/swc-core": "1.12.14",
"@meteorjs/swc-core": "1.13.5",
});
Package.onUse(function (api) {

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "Generates the boilerplate html from program's manifest",
version: '2.0.2',
version: '2.1.0-beta340.11',
});
Npm.depends({

View File

@@ -1,6 +1,6 @@
Package.describe({
name: 'ecmascript',
version: '0.16.13',
version: '0.17.0-beta340.11',
summary: 'Compiler plugin that supports ES2015+ in all .js files',
documentation: 'README.md',
});

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "The Meteor command-line tool",
version: "3.3.1",
version: "3.4.0-beta.11",
});
Package.includeTool();

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "JavaScript minifier",
version: '3.0.4',
version: '3.1.0-beta340.11',
});
Npm.depends({

View File

@@ -743,7 +743,7 @@ export default class LocalCollection {
for (const id of specificIds) {
const doc = this._docs.get(id);
if (doc && !fn(doc, id)) {
if (doc && fn(doc, id) === false) {
break
}
}

View File

@@ -59,6 +59,36 @@ Tinytest.add('minimongo - wrapTransform', test => {
handle.stop();
});
Tinytest.add('minimongo - bulk remove with $in operator removes all matching documents', function(test) {
const coll = new LocalCollection();
// Insert multiple documents
const ids = ['id1', 'id2', 'id3', 'id4'];
ids.forEach(id => {
coll.insert({ _id: id, value: `item-${id}` });
});
// Verify we have 4 documents
test.equal(coll.find().count(), 4);
// Remove 2 documents using $in operator
const removedCount = coll.remove({ _id: { $in: ['id1', 'id2'] } });
// This should remove 2 documents, not just 1
test.equal(removedCount, 2);
// Verify only 2 documents remain
test.equal(coll.find().count(), 2);
// Verify the correct documents were removed
test.isUndefined(coll.findOne('id1'));
test.isUndefined(coll.findOne('id2'));
// Verify the other documents still exist
test.isNotUndefined(coll.findOne('id3'));
test.isNotUndefined(coll.findOne('id4'));
});
if (Meteor.isClient) {
Tinytest.add('minimongo - $geoIntersects should throw error', function(test) {
const collection = new LocalCollection();

View File

@@ -4061,4 +4061,4 @@ Tinytest.addAsync('minimongo - operation result fields (async)', async test => {
// Test remove
const removeResult = await c.removeAsync({name: 'doc1'});
test.equal(removeResult, 1, 'remove should return removed count');
});
});

View File

@@ -62,8 +62,15 @@ Mongo.Collection = function Collection(name, options) {
setupAutopublish(this, name, options);
Mongo._collections.set(name, this);
// Apply collection extensions
CollectionExtensions._applyExtensions(this, name, options);
};
// Apply static methods to the Collection constructor
CollectionExtensions._applyStaticMethods(Mongo.Collection);
Object.assign(Mongo.Collection.prototype, {
_getFindSelector(args) {
if (args.length == 0) return {};
@@ -153,6 +160,118 @@ Object.assign(Mongo.Collection, {
return selector;
},
// Collection Extensions API - delegate to CollectionExtensions
/**
* @summary Add a constructor extension function that runs when collections are created.
* @locus Anywhere
* @memberof Mongo.Collection
* @static
* @param {Function} extension Extension function called with (name, options) and 'this' bound to collection instance
*/
addExtension(extension) {
return CollectionExtensions.addExtension(extension);
},
/**
* @summary Add a prototype method to all collection instances.
* @locus Anywhere
* @memberof Mongo.Collection
* @static
* @param {String} name The name of the method to add
* @param {Function} method The method function, bound to the collection instance
*/
addPrototypeMethod(name, method) {
return CollectionExtensions.addPrototypeMethod(name, method);
},
/**
* @summary Add a static method to the Mongo.Collection constructor.
* @locus Anywhere
* @memberof Mongo.Collection
* @static
* @param {String} name The name of the static method to add
* @param {Function} method The static method function
*/
addStaticMethod(name, method) {
return CollectionExtensions.addStaticMethod(name, method);
},
/**
* @summary Remove a constructor extension (useful for testing).
* @locus Anywhere
* @memberof Mongo.Collection
* @static
* @param {Function} extension The extension function to remove
*/
removeExtension(extension) {
return CollectionExtensions.removeExtension(extension);
},
/**
* @summary Remove a prototype method from all collection instances.
* @locus Anywhere
* @memberof Mongo.Collection
* @static
* @param {String} name The name of the method to remove
*/
removePrototypeMethod(name) {
return CollectionExtensions.removePrototypeMethod(name);
},
/**
* @summary Remove a static method from the Mongo.Collection constructor.
* @locus Anywhere
* @memberof Mongo.Collection
* @static
* @param {String} name The name of the static method to remove
*/
removeStaticMethod(name) {
return CollectionExtensions.removeStaticMethod(name);
},
/**
* @summary Clear all extensions, prototype methods, and static methods (useful for testing).
* @locus Anywhere
* @memberof Mongo.Collection
* @static
*/
clearExtensions() {
return CollectionExtensions.clearExtensions();
},
/**
* @summary Get all registered constructor extensions (useful for debugging).
* @locus Anywhere
* @memberof Mongo.Collection
* @static
* @returns {Array<Function>} Array of registered extension functions
*/
getExtensions() {
return CollectionExtensions.getExtensions();
},
/**
* @summary Get all registered prototype methods (useful for debugging).
* @locus Anywhere
* @memberof Mongo.Collection
* @static
* @returns {Map<String, Function>} Map of method names to functions
*/
getPrototypeMethods() {
return CollectionExtensions.getPrototypeMethods();
},
/**
* @summary Get all registered static methods (useful for debugging).
* @locus Anywhere
* @memberof Mongo.Collection
* @static
* @returns {Map<String, Function>} Map of method names to functions
*/
getStaticMethods() {
return CollectionExtensions.getStaticMethods();
}
});
Object.assign(Mongo.Collection.prototype, ReplicationMethods, SyncMethods, AsyncMethods, IndexMethods);
@@ -230,6 +349,13 @@ Object.assign(Mongo, {
* @protected
*/
_collections: new Map(),
/**
* @summary Collection Extensions API
* @memberof Mongo
* @static
*/
CollectionExtensions: CollectionExtensions
})

View File

@@ -0,0 +1,146 @@
/**
* Collection Extensions System
*
* Provides a clean way to extend Mongo.Collection functionality
* without monkey patching. Supports constructor extensions,
* prototype methods, and static methods.
*/
if (Package['lai:collection-extensions']) {
console.warn('lai:collection-extensions is not deprecated. Use Mongo.Collection.addExtension instead.');
}
CollectionExtensions = {
_extensions: [],
_prototypeMethods: new Map(),
_staticMethods: new Map(),
/**
* Add a constructor extension function
* Extension function is called with (name, options) and 'this' bound to collection instance
*/
addExtension(extension) {
if (typeof extension !== 'function') {
throw new Error('Extension must be a function');
}
this._extensions.push(extension);
},
/**
* Add a prototype method to all collection instances
* Method is bound to the collection instance
*/
addPrototypeMethod(name, method) {
if (typeof name !== 'string' || !name) {
throw new Error('Prototype method name must be a non-empty string');
}
if (typeof method !== 'function') {
throw new Error('Prototype method must be a function');
}
this._prototypeMethods.set(name, method);
},
/**
* Add a static method to the Mongo.Collection constructor
*/
addStaticMethod(name, method) {
if (typeof name !== 'string' || !name) {
throw new Error('Static method name must be a non-empty string');
}
if (typeof method !== 'function') {
throw new Error('Static method must be a function');
}
this._staticMethods.set(name, method);
},
/**
* Remove an extension (useful for testing)
*/
removeExtension(extension) {
const index = this._extensions.indexOf(extension);
if (index > -1) {
this._extensions.splice(index, 1);
}
},
/**
* Remove a prototype method
*/
removePrototypeMethod(name) {
this._prototypeMethods.delete(name);
},
/**
* Remove a static method
*/
removeStaticMethod(name) {
this._staticMethods.delete(name);
},
/**
* Clear all extensions (useful for testing)
*/
clearExtensions() {
this._extensions.length = 0;
this._prototypeMethods.clear();
this._staticMethods.clear();
},
/**
* Get all registered extensions (useful for debugging)
*/
getExtensions() {
return [...this._extensions];
},
/**
* Get all registered prototype methods (useful for debugging)
*/
getPrototypeMethods() {
return new Map(this._prototypeMethods);
},
/**
* Get all registered static methods (useful for debugging)
*/
getStaticMethods() {
return new Map(this._staticMethods);
},
/**
* Apply all extensions to a collection instance
* Called during collection construction
*/
_applyExtensions(instance, name, options) {
// Apply constructor extensions
for (const extension of this._extensions) {
try {
extension.call(instance, name, options);
} catch (error) {
// Provide helpful error context
throw new Error(`Extension failed for collection '${name}': ${error.message}`);
}
}
// Apply prototype methods
for (const [methodName, method] of this._prototypeMethods) {
instance[methodName] = method.bind(instance);
}
},
/**
* Apply static methods to the Mongo.Collection constructor
* Called during package initialization
*/
_applyStaticMethods(CollectionConstructor) {
for (const [methodName, method] of this._staticMethods) {
CollectionConstructor[methodName] = method;
}
},
};

View File

@@ -53,6 +53,50 @@ export namespace Mongo {
? T
: U;
/**
* Configuration options for Mongo Collection constructor
*/
interface CollectionOptions<T = any, U = T> {
/**
* The server connection that will manage this collection. Uses the default connection if not specified.
* Pass the return value of calling `DDP.connect` to specify a different server. Pass `null` to specify
* no connection. Unmanaged (`name` is null) collections cannot specify a connection.
*/
connection?: DDP.DDPStatic | null | undefined;
/**
* The method of generating the `_id` fields of new documents in this collection. Possible values:
* - **`'STRING'`**: random strings
* - **`'MONGO'`**: random [`Mongo.ObjectID`](#mongo_object_id) values
*
* The default id generation technique is `'STRING'`.
*/
idGeneration?: string | undefined;
/**
* An optional transformation function. Documents will be passed through this function before being
* returned from `fetch` or `findOne`, and before being passed to callbacks of `observe`, `map`,
* `forEach`, `allow`, and `deny`. Transforms are *not* applied for the callbacks of `observeChanges`
* or to cursors returned from publish functions.
*/
transform?: (doc: T) => U;
/**
* Set to `false` to skip setting up the mutation methods that enable insert/update/remove from client code.
* Default `true`.
*/
defineMutationMethods?: boolean | undefined;
// Internal options (from normalizeOptions function)
/** @internal */
_driver?: any;
/** @internal */
_preventAutopublish?: boolean;
// Allow additional properties for extensibility
[key: string]: any;
}
var Collection: CollectionStatic;
interface CollectionStatic {
/**
@@ -61,27 +105,7 @@ export namespace Mongo {
*/
new <T extends NpmModuleMongodb.Document, U = T>(
name: string | null,
options?: {
/**
* The server connection that will manage this collection. Uses the default connection if not specified. Pass the return value of calling `DDP.connect` to specify a different
* server. Pass `null` to specify no connection. Unmanaged (`name` is null) collections cannot specify a connection.
*/
connection?: DDP.DDPStatic | null | undefined;
/** The method of generating the `_id` fields of new documents in this collection. Possible values:
* - **`'STRING'`**: random strings
* - **`'MONGO'`**: random [`Mongo.ObjectID`](#mongo_object_id) values
*
* The default id generation technique is `'STRING'`.
*/
idGeneration?: string | undefined;
/**
* An optional transformation function. Documents will be passed through this function before being returned from `fetch` or `findOne`, and before being passed to callbacks of
* `observe`, `map`, `forEach`, `allow`, and `deny`. Transforms are *not* applied for the callbacks of `observeChanges` or to cursors returned from publish functions.
*/
transform?: (doc: T) => U;
/** Set to `false` to skip setting up the mutation methods that enable insert/update/remove from client code. Default `true`. */
defineMutationMethods?: boolean | undefined;
}
options?: CollectionOptions<T, U>
): Collection<T, U>;
/**
@@ -92,6 +116,68 @@ export namespace Mongo {
getCollection<
TCollection extends Collection<any, any> | undefined = Collection<NpmModuleMongodb.Document> | undefined
>(name: string): TCollection;
// Collection Extensions API
/**
* Add a constructor extension function that runs when collections are created.
* @param extension Extension function called with (name, options) and 'this' bound to collection instance
*/
addExtension<T = any, U = T>(extension: (this: Collection<T, U>, name: string | null, options?: CollectionOptions<T, U>) => void): void;
/**
* Add a prototype method to all collection instances.
* @param name The name of the method to add
* @param method The method function, bound to the collection instance
*/
addPrototypeMethod<T = any, U = T>(name: string, method: (this: Collection<T, U>, ...args: any[]) => any): void;
/**
* Add a static method to the Mongo.Collection constructor.
* @param name The name of the static method to add
* @param method The static method function
*/
addStaticMethod(name: string, method: Function): void;
/**
* Remove a constructor extension (useful for testing).
* @param extension The extension function to remove
*/
removeExtension(extension: Function): void;
/**
* Remove a prototype method from all collection instances.
* @param name The name of the method to remove
*/
removePrototypeMethod(name: string): void;
/**
* Remove a static method from the Mongo.Collection constructor.
* @param name The name of the static method to remove
*/
removeStaticMethod(name: string): void;
/**
* Clear all extensions, prototype methods, and static methods (useful for testing).
*/
clearExtensions(): void;
/**
* Get all registered constructor extensions (useful for debugging).
* @returns Array of registered extension functions
*/
getExtensions(): Array<Function>;
/**
* Get all registered prototype methods (useful for debugging).
* @returns Map of method names to functions
*/
getPrototypeMethods(): Map<string, Function>;
/**
* Get all registered static methods (useful for debugging).
* @returns Map of method names to functions
*/
getStaticMethods(): Map<string, Function>;
}
interface Collection<T extends NpmModuleMongodb.Document, U = T> {
allow<Fn extends Transform<T> = undefined>(options: {
@@ -479,6 +565,87 @@ export namespace Mongo {
equals(otherID: ObjectID): boolean;
}
/**
* Collection Extensions API
*/
interface CollectionExtensions {
/**
* Add a constructor extension function that runs when collections are created.
* @param extension Extension function called with (name, options) and 'this' bound to collection instance
*/
addExtension<T = any, U = T>(extension: (this: Collection<T, U>, name: string | null, options?: CollectionOptions<T, U>) => void): void;
/**
* Add a prototype method to all collection instances.
* @param name The name of the method to add
* @param method The method function, bound to the collection instance
*/
addPrototypeMethod<T = any, U = T>(name: string, method: (this: Collection<T, U>, ...args: any[]) => any): void;
/**
* Add a static method to the Mongo.Collection constructor.
* @param name The name of the static method to add
* @param method The static method function
*/
addStaticMethod(name: string, method: Function): void;
/**
* Remove a constructor extension (useful for testing).
* @param extension The extension function to remove
*/
removeExtension(extension: Function): void;
/**
* Remove a prototype method from all collection instances.
* @param name The name of the method to remove
*/
removePrototypeMethod(name: string): void;
/**
* Remove a static method from the Mongo.Collection constructor.
* @param name The name of the static method to remove
*/
removeStaticMethod(name: string): void;
/**
* Clear all extensions, prototype methods, and static methods (useful for testing).
*/
clearExtensions(): void;
/**
* Get all registered constructor extensions (useful for debugging).
* @returns Array of registered extension functions
*/
getExtensions(): Array<Function>;
/**
* Get all registered prototype methods (useful for debugging).
* @returns Map of method names to functions
*/
getPrototypeMethods(): Map<string, Function>;
/**
* Get all registered static methods (useful for debugging).
* @returns Map of method names to functions
*/
getStaticMethods(): Map<string, Function>;
}
var CollectionExtensions: CollectionExtensions;
/**
* Retrieve a Meteor collection instance by name. Only collections defined with `new Mongo.Collection(...)` are available with this method.
* @param name Name of your collection as it was defined with `new Mongo.Collection()`.
* @returns The collection instance or undefined if not found
*/
function getCollection<T extends Collection<any, any> | undefined = Collection<NpmModuleMongodb.Document> | undefined>(name: string): T;
/**
* A record of all defined Mongo.Collection instances, indexed by collection name.
* @internal
*/
var _collections: Map<string, Collection<any, any>>;
function setConnectionOptions(options: any): void;
}

View File

@@ -41,6 +41,8 @@ export class OplogHandle {
excludeCollections?: string[];
includeCollections?: string[];
};
private _includeNSRegex?: RegExp;
private _excludeNSRegex?: RegExp;
private _stopped: boolean;
private _tailHandle: any;
private _readyPromiseResolver: (() => void) | null;
@@ -82,6 +84,18 @@ export class OplogHandle {
}
this._oplogOptions = { includeCollections, excludeCollections };
if (includeCollections?.length) {
const incAlt = includeCollections.map((c) => Meteor._escapeRegExp(c)).join('|');
this._includeNSRegex = new RegExp(`^${Meteor._escapeRegExp(this._dbName)}\\.(?:${incAlt})$`);
}
if (excludeCollections?.length) {
const excAlt = excludeCollections.map((c) => Meteor._escapeRegExp(c)).join('|');
this._excludeNSRegex = new RegExp(`^${Meteor._escapeRegExp(this._dbName)}\\.(?:${excAlt})$`);
}
this._catchingUpResolvers = [];
this._lastProcessedTS = null;
@@ -92,6 +106,15 @@ export class OplogHandle {
this._startTrailingPromise = this._startTailing();
}
private _nsAllowed(ns: string | undefined): boolean {
if (!ns) return false;
if (ns === 'admin.$cmd') return true;
if (this._includeNSRegex && !this._includeNSRegex.test(ns)) return false;
if (this._excludeNSRegex && this._excludeNSRegex.test(ns)) return false;
return true;
}
private _getOplogSelector(lastProcessedTS?: any): any {
const oplogCriteria: any = [
{
@@ -104,40 +127,55 @@ export class OplogHandle {
},
];
const nsRegex = new RegExp(
"^(?:" +
[
// @ts-ignore
Meteor._escapeRegExp(this._dbName + "."),
// @ts-ignore
Meteor._escapeRegExp("admin.$cmd"),
].join("|") +
")"
);
if (this._oplogOptions.excludeCollections?.length) {
oplogCriteria.push({
ns: {
$regex: nsRegex,
$nin: this._oplogOptions.excludeCollections.map(
(collName: string) => `${this._dbName}.${collName}`
),
},
});
} else if (this._oplogOptions.includeCollections?.length) {
const nsRegex = new RegExp(
'^(?:' +
[
// @ts-ignore
Meteor._escapeRegExp(this._dbName + '.'),
].join('|') +
')'
);
const excludeNs = {
$regex: nsRegex,
$nin: this._oplogOptions.excludeCollections.map(
(collName: string) => `${this._dbName}.${collName}`
),
};
oplogCriteria.push({
$or: [
{ ns: /^admin\.\$cmd/ },
{ ns: excludeNs },
{
ns: {
$in: this._oplogOptions.includeCollections.map(
(collName: string) => `${this._dbName}.${collName}`
),
},
ns: /^admin\.\$cmd/,
'o.applyOps': { $elemMatch: { ns: excludeNs } },
},
],
});
} else if (this._oplogOptions.includeCollections?.length) {
const includeNs = {
$in: this._oplogOptions.includeCollections.map(
(collName: string) => `${this._dbName}.${collName}`
),
};
oplogCriteria.push({
$or: [
{
ns: includeNs,
},
{ ns: /^admin\.\$cmd/, 'o.applyOps.ns': includeNs },
],
});
} else {
const nsRegex = new RegExp(
"^(?:" +
[
// @ts-ignore
Meteor._escapeRegExp(this._dbName + "."),
// @ts-ignore
Meteor._escapeRegExp("admin.$cmd"),
].join("|") +
")"
);
oplogCriteria.push({
ns: nsRegex,
});
@@ -411,6 +449,11 @@ async function handleDoc(handle: OplogHandle, doc: OplogEntry): Promise<void> {
op.ts = nextTimestamp;
nextTimestamp = nextTimestamp.add(Long.ONE);
}
// Only forward sub-ops whose ns is allowed
// See https://github.com/meteor/meteor/issues/13945
if (!handle['_nsAllowed'](op.ns)) {
continue;
}
await handleDoc(handle, op);
}
return;

View File

@@ -79,6 +79,7 @@ Package.onUse(function (api) {
api.export("MongoInternals", "server");
api.export("Mongo");
api.export("CollectionExtensions");
api.export("ObserveMultiplexer", "server", { testOnly: true });
api.addFiles(
@@ -100,6 +101,7 @@ Package.onUse(function (api) {
);
api.addFiles("local_collection_driver.js", ["client", "server"]);
api.addFiles("remote_collection_driver.ts", "server");
api.addFiles("collection/collection_extensions.js", ["client", "server"]);
api.addFiles("collection/collection.js", ["client", "server"]);
api.addFiles("connection_options.ts", "server");
// For zodern:types to pick up our published types.
@@ -130,6 +132,7 @@ Package.onTest(function (api) {
api.addFiles("tests/collection_tests.js", ["client", "server"]);
api.addFiles("tests/collection_async_tests.js", ["client", "server"]);
api.addFiles("tests/observe_changes_tests.js", ["client", "server"]);
api.addFiles("tests/collection_extensions_tests.js", ["client", "server"]);
api.addFiles("tests/oplog_tests.js", "server");
api.addFiles("tests/oplog_v2_converter_tests.js", "server");
api.addFiles("tests/doc_fetcher_tests.js", "server");

View File

@@ -0,0 +1,233 @@
import { Tinytest } from "meteor/tinytest";
import { Mongo } from "meteor/mongo";
import { CollectionExtensions } from "meteor/mongo";
import { Random } from "meteor/random";
// Test setup and teardown
function setupTest() {
CollectionExtensions.clearExtensions();
}
function teardownTest() {
CollectionExtensions.clearExtensions();
}
Tinytest.add("CollectionExtensions - constructor extension", function (test) {
setupTest();
let extensionCallCount = 0;
let extensionData = null;
CollectionExtensions.addExtension(function(name, options) {
extensionCallCount++;
extensionData = { name, options, instance: this };
});
const testCollection = new Mongo.Collection(Random.id());
test.equal(extensionCallCount, 1);
test.equal(extensionData.name, testCollection._name);
test.equal(extensionData.instance, testCollection);
test.isTrue(extensionData.options && typeof extensionData.options === 'object');
teardownTest();
});
Tinytest.add("CollectionExtensions - multiple extensions", function (test) {
setupTest();
let callOrder = [];
CollectionExtensions.addExtension(function(name, options) {
callOrder.push('extension1');
});
CollectionExtensions.addExtension(function(name, options) {
callOrder.push('extension2');
});
CollectionExtensions.addExtension(function(name, options) {
callOrder.push('extension3');
});
const testCollection = new Mongo.Collection(Random.id());
test.equal(callOrder, ['extension1', 'extension2', 'extension3']);
teardownTest();
});
Tinytest.add("CollectionExtensions - prototype methods", function (test) {
setupTest();
CollectionExtensions.addPrototypeMethod('testMethod', function() {
return 'testResult';
});
const testCollection = new Mongo.Collection(Random.id());
test.isTrue(typeof testCollection.testMethod === 'function');
test.equal(testCollection.testMethod(), 'testResult');
teardownTest();
});
// Test prototype method with collection context
Tinytest.add("CollectionExtensions - prototype method context", function (test) {
setupTest();
// Add prototype method that uses collection context
CollectionExtensions.addPrototypeMethod('getCollectionName', function() {
return this._name;
});
// Create collection
const testCollection = new Mongo.Collection(Random.id());
// Verify method has correct context
test.equal(testCollection.getCollectionName(), testCollection._name);
teardownTest();
});
// Test static methods
Tinytest.add("CollectionExtensions - static methods", function (test) {
setupTest();
// Add static method
CollectionExtensions.addStaticMethod('testStaticMethod', function() {
return 'staticResult';
});
// Apply static methods (this happens automatically in real usage)
CollectionExtensions._applyStaticMethods(Mongo.Collection);
// Verify static method was added
test.isTrue(typeof Mongo.Collection.testStaticMethod === 'function');
test.equal(Mongo.Collection.testStaticMethod(), 'staticResult');
// Clean up
delete Mongo.Collection.testStaticMethod;
teardownTest();
});
// Test error handling in extensions
Tinytest.add("CollectionExtensions - extension error handling", function (test) {
setupTest();
// Add extension that throws error
CollectionExtensions.addExtension(function(name, options) {
throw new Error('Test extension error');
});
// Creating collection should throw with helpful error message
test.throws(() => {
new Mongo.Collection(Random.id());
}, /Extension failed for collection/);
teardownTest();
});
// Test extension removal
Tinytest.add("CollectionExtensions - extension removal", function (test) {
setupTest();
let callCount = 0;
const extension = function(name, options) {
callCount++;
};
CollectionExtensions.addExtension(extension);
const testCollection1 = new Mongo.Collection(Random.id());
test.equal(callCount, 1);
CollectionExtensions.removeExtension(extension);
// Create another collection - should not call extension
const testCollection2 = new Mongo.Collection(Random.id());
test.equal(callCount, 1); // Still 1, not 2
teardownTest();
});
Tinytest.add("CollectionExtensions - prototype method removal", function (test) {
setupTest();
CollectionExtensions.addPrototypeMethod('testMethod', function() {
return 'test';
});
const testCollection1 = new Mongo.Collection(Random.id());
test.isTrue(typeof testCollection1.testMethod === 'function');
CollectionExtensions.removePrototypeMethod('testMethod');
const testCollection2 = new Mongo.Collection(Random.id());
test.isUndefined(testCollection2.testMethod);
teardownTest();
});
Tinytest.add("CollectionExtensions - input validation", function (test) {
setupTest();
test.throws(() => {
CollectionExtensions.addExtension("not a function");
}, /Extension must be a function/);
test.throws(() => {
CollectionExtensions.addPrototypeMethod("", function() {});
}, /Prototype method name must be a non-empty string/);
test.throws(() => {
CollectionExtensions.addPrototypeMethod(123, function() {});
}, /Prototype method name must be a non-empty string/);
test.throws(() => {
CollectionExtensions.addPrototypeMethod("test", "not a function");
}, /Prototype method must be a function/);
test.throws(() => {
CollectionExtensions.addStaticMethod("", function() {});
}, /Static method name must be a non-empty string/);
test.throws(() => {
CollectionExtensions.addStaticMethod("test", "not a function");
}, /Static method must be a function/);
teardownTest();
});
Tinytest.add("CollectionExtensions - introspection", function (test) {
setupTest();
const extension1 = function() {};
const extension2 = function() {};
test.equal(CollectionExtensions.getExtensions(), []);
test.equal(CollectionExtensions.getPrototypeMethods().size, 0);
test.equal(CollectionExtensions.getStaticMethods().size, 0);
CollectionExtensions.addExtension(extension1);
CollectionExtensions.addExtension(extension2);
CollectionExtensions.addPrototypeMethod('test1', function() {});
CollectionExtensions.addStaticMethod('test2', function() {});
// Test introspection
const extensions = CollectionExtensions.getExtensions();
test.equal(extensions.length, 2);
test.equal(extensions[0], extension1);
test.equal(extensions[1], extension2);
const prototypeMethods = CollectionExtensions.getPrototypeMethods();
test.equal(prototypeMethods.size, 1);
test.isTrue(prototypeMethods.has('test1'));
const staticMethods = CollectionExtensions.getStaticMethods();
test.equal(staticMethods.size, 1);
test.isTrue(staticMethods.has('test2'));
teardownTest();
});

View File

@@ -2776,6 +2776,53 @@ const setsEqual = function (a, b) {
});
});
// Test operation result fields with allow/deny rules (similar to issue #12159)
if (Meteor.isServer) {
testAsyncMulti('mongo-livedata - operation result fields with allow/deny, ' + idGeneration, [
async function(test, expect) {
var collectionName = 'test_operation_results_' + Random.id();
var coll = new Mongo.Collection(collectionName, { idGeneration: idGeneration });
// Set up allow rules for all operations
coll.allow({
insert: function() { return true; },
update: function() { return true; },
remove: function() { return true; }
});
// Test insert
var insertedId = await coll.insertAsync({name: 'doc1'});
test.isTrue(insertedId !== undefined, 'insert should return an ID');
// Test update
var updateResult = await coll.updateAsync({name: 'doc1'}, {$set: {value: 1}});
test.equal(updateResult, 1, 'update should return affected count');
// Test upsert (update case)
var upsertUpdateResult = await coll.upsertAsync({name: 'doc1'}, {$set: {value: 2}});
test.equal(upsertUpdateResult.numberAffected, 1);
test.isFalse(upsertUpdateResult.hasOwnProperty('insertedId'));
// Test upsert (insert case)
var upsertInsertResult = await coll.upsertAsync({name: 'doc2'}, {$set: {value: 3}});
test.equal(upsertInsertResult.numberAffected, 1);
test.isTrue(upsertInsertResult.hasOwnProperty('insertedId'));
// Test remove
var removeResult = await coll.removeAsync({name: 'doc1'});
test.equal(removeResult, 1, 'remove should return removed count');
// Test insert with explicit ID
var explicitId = idGeneration === 'MONGO' ? new Mongo.ObjectID() : 'explicit-test-id';
var insertExplicitResult = await coll.insertAsync({_id: explicitId, name: 'explicit-doc'});
test.equal(insertExplicitResult, explicitId, 'insert with explicit ID should return that ID');
// Clean up
await coll.dropCollectionAsync();
}
]);
}
}); // end idGeneration parametrization
Tinytest.add('mongo-livedata - rewrite selector', function(test) {

View File

@@ -181,11 +181,46 @@ process.env.MONGO_OPLOG_URL &&
const defaultOplogHandle = MongoInternals.defaultRemoteCollectionDriver().mongo._oplogHandle;
let previousMongoPackageSettings = {};
async function oplogSimpleInsertion(IncludeCollection, ExcludeCollection) {
await IncludeCollection.rawCollection().insertOne({ include: 'yes', foo: 'bar' });
await ExcludeCollection.rawCollection().insertOne({ include: 'no', foo: 'bar' });
}
async function oplogInsertionTransaction(IncludeCollection, ExcludeCollection) {
const client = MongoInternals.defaultRemoteCollectionDriver().mongo.client;
const session = client.startSession();
try {
await session.withTransaction(async () => {
await IncludeCollection.rawCollection().insertOne({ include: 'yes', foo: 'bar' }, { session });
await ExcludeCollection.rawCollection().insertOne({ include: 'no', foo: 'bar' }, { session });
});
} finally {
await session.endSession();
}
}
async function oplogMassiveInsertion(IncludeCollection, ExcludeCollection) {
const totalDocuments = 10000;
const documentInclude = Array.from(
{ length: totalDocuments },
(_, index) => ({ include: "yes", foo: "bar" + index })
);
const documentExclude = Array.from(
{ length: totalDocuments },
(_, index) => ({ include: "no", foo: "bar" + index })
);
await IncludeCollection.rawCollection().insertMany(documentInclude);
await ExcludeCollection.rawCollection().insertMany(documentExclude);
}
async function oplogOptionsTest({
test,
includeCollectionName,
excludeCollectionName,
mongoPackageSettings = {}
mongoPackageSettings = {},
functionToRun
}) {
try {
previousMongoPackageSettings = { ...(Meteor.settings?.packages?.mongo || {}) };
@@ -199,9 +234,11 @@ async function oplogOptionsTest({
const IncludeCollection = new Mongo.Collection(includeCollectionName);
const ExcludeCollection = new Mongo.Collection(excludeCollectionName);
const shouldBeTracked = new Promise((resolve) => {
IncludeCollection.find({ include: 'yes' }).observeChanges({
added(id, fields) { resolve(true) }
const shouldBeTracked = new Promise((resolve, reject) => {
IncludeCollection.find({ include: "yes" }).observeChanges({
added(id, fields) {
resolve(true);
},
});
});
const shouldBeIgnored = new Promise((resolve, reject) => {
@@ -218,8 +255,7 @@ async function oplogOptionsTest({
});
// do the inserts:
await IncludeCollection.rawCollection().insertOne({ include: 'yes', foo: 'bar' });
await ExcludeCollection.rawCollection().insertOne({ include: 'no', foo: 'bar' });
await functionToRun(IncludeCollection, ExcludeCollection);
test.equal(await shouldBeTracked, true);
test.equal(await shouldBeIgnored, true);
@@ -229,6 +265,73 @@ async function oplogOptionsTest({
MongoInternals.defaultRemoteCollectionDriver().mongo._setOplogHandle(defaultOplogHandle);
}
}
async function oplogTailingOptionsTest({
test,
includeCollectionName,
excludeCollectionName,
mongoPackageSettings = {},
functionToRun
}) {
let stopRaw;
try {
previousMongoPackageSettings = { ...(Meteor.settings?.packages?.mongo || {}) };
if (!Meteor.settings.packages) Meteor.settings.packages = {};
Meteor.settings.packages.mongo = mongoPackageSettings;
const myOplogHandle = new MongoInternals.OplogHandle(process.env.MONGO_OPLOG_URL, 'meteor');
await myOplogHandle._startTrailingPromise;
const IncludeCollection = new Mongo.Collection(includeCollectionName);
const ExcludeCollection = new Mongo.Collection(excludeCollectionName);
// Listen for INCLUDE collection oplog entries
const includeSeen = new Promise(async (resolve, reject) => {
const includeStop = await myOplogHandle.onOplogEntry(
{ dropCollection: false, dropDatabase: false, collection: includeCollectionName },
({ op, collection, id }) => {
try {
// Only accept actual inserts for the include collection
if (op?.op === 'i' && collection === includeCollectionName && op?.o?.include === 'yes') {
includeStop.stop();
resolve(true);
}
} catch (e) {
includeStop.stop();
reject(e);
}
}
);
});
// Ensure EXCLUDE collection does NOT get processed
const excludeNotSeen = new Promise(async (resolve, reject) => {
const excludeStop = await myOplogHandle.onOplogEntry(
{ dropCollection: false, dropDatabase: false, collection: excludeCollectionName },
({ op, collection, id }) => {
// If anything for excluded collection arrives, fail
excludeStop.stop();
reject("Recieved a document in a excluded collection");
}
);
// Resolve after 2s if nothing arrived
setTimeout(() => {
excludeStop.stop();
resolve(true);
}, 2000);
});
// Do the inserts (e.g., oplogInsertionTransaction or your chosen function)
await functionToRun(IncludeCollection, ExcludeCollection);
// Await raw-oplog assertions
test.equal(await includeSeen, true);
test.equal(await excludeNotSeen, true);
} finally {
if (stopRaw?.stop) await stopRaw.stop();
// Reset:
Meteor.settings.packages.mongo = { ...previousMongoPackageSettings };
}
}
process.env.MONGO_OPLOG_URL && Tinytest.addAsync(
'mongo-livedata - oplog - oplogSettings - oplogExcludeCollections',
@@ -242,7 +345,8 @@ process.env.MONGO_OPLOG_URL && Tinytest.addAsync(
test,
includeCollectionName: collectionNameA,
excludeCollectionName: collectionNameB,
mongoPackageSettings
mongoPackageSettings,
functionToRun: oplogSimpleInsertion
});
}
);
@@ -259,7 +363,8 @@ process.env.MONGO_OPLOG_URL && Tinytest.addAsync(
test,
includeCollectionName: collectionNameB,
excludeCollectionName: collectionNameA,
mongoPackageSettings
mongoPackageSettings,
functionToRun: oplogSimpleInsertion
});
}
);
@@ -279,7 +384,8 @@ process.env.MONGO_OPLOG_URL && Tinytest.addAsync(
test,
includeCollectionName: collectionNameA,
excludeCollectionName: collectionNameB,
mongoPackageSettings
mongoPackageSettings,
functionToRun: oplogSimpleInsertion
});
test.fail();
} catch (err) {
@@ -350,6 +456,78 @@ process.env.MONGO_OPLOG_URL &&
}
);
process.env.MONGO_OPLOG_URL && Tinytest.addAsync(
'mongo-livedata - oplog - oplogSettings - massiveInsertion - oplogIncludeCollections',
async test => {
const collectionNameA = "oplog-a-massive-" + Random.id();
const collectionNameB = "oplog-b-massive-" + Random.id();
const mongoPackageSettings = {
oplogIncludeCollections: [collectionNameA]
};
await oplogTailingOptionsTest({
test,
includeCollectionName: collectionNameA,
excludeCollectionName: collectionNameB,
mongoPackageSettings,
functionToRun: oplogMassiveInsertion
});
}
);
process.env.MONGO_OPLOG_URL && Tinytest.addAsync(
'mongo-livedata - oplog - oplogSettings - massiveInsertion - oplogExcludeCollections',
async test => {
const collectionNameA = "oplog-a-massive-" + Random.id();
const collectionNameB = "oplog-b-massive-" + Random.id();
const mongoPackageSettings = {
oplogExcludeCollections: [collectionNameA]
};
await oplogTailingOptionsTest({
test,
includeCollectionName: collectionNameB,
excludeCollectionName: collectionNameA,
mongoPackageSettings,
functionToRun: oplogMassiveInsertion
});
}
);
process.env.MONGO_OPLOG_URL && Tinytest.addAsync(
'mongo-livedata - oplog - oplogSettings - transaction - oplogExcludeCollections',
async test => {
const collectionNameA = "oplog-a-transaction-" + Random.id();
const collectionNameB = "oplog-b-transaction-" + Random.id();
const mongoPackageSettings = {
oplogExcludeCollections: [collectionNameA]
};
await oplogTailingOptionsTest({
test,
includeCollectionName: collectionNameB,
excludeCollectionName: collectionNameA,
mongoPackageSettings,
functionToRun: oplogInsertionTransaction
});
}
);
process.env.MONGO_OPLOG_URL && Tinytest.addAsync(
'mongo-livedata - oplog - oplogSettings - transaction - oplogIncludeCollections',
async test => {
const collectionNameA = "oplog-a-transaction-" + Random.id();
const collectionNameB = "oplog-b-transaction-" + Random.id();
const mongoPackageSettings = {
oplogIncludeCollections: [collectionNameA]
};
await oplogTailingOptionsTest({
test,
includeCollectionName: collectionNameA,
excludeCollectionName: collectionNameB,
mongoPackageSettings,
functionToRun: oplogInsertionTransaction
});
}
);
// TODO this is commented for now, but we need to find out the cause
// PR: https://github.com/meteor/meteor/pull/12057
// Meteor.isServer && Tinytest.addAsync(

View File

@@ -7,6 +7,8 @@ import { RSPACK_DOCTOR_CONTEXT } from "./constants";
const fs = require('fs');
const path = require('path');
const { getCustomConfigFilePath } = require('./processes');
const { logError } = require('meteor/tools-core/lib/log');
const { capitalizeFirstLetter } = require('meteor/tools-core/lib/string');
@@ -577,13 +579,22 @@ export function cleanBuildContextFiles() {
/**
* Ensures the rspack.config.js file exists at the project level
* Creates the file if it doesn't exist with the required template
* @returns {string} Path to the rspack.config.js file
* Will not create a new file if rspack.config.mjs or rspack.config.cjs exists
* @returns {string} Path to the rspack.config file (.js, .mjs, or .cjs)
*/
export function ensureRspackConfigExists() {
const appDir = getMeteorAppDir();
const configPath = path.join(appDir, 'rspack.config.js');
const configTemplate = `import { defineConfig } from '@meteorjs/rspack';
// Check if any config file already exists using the helper function
const existingConfigPath = getCustomConfigFilePath(appDir);
if (existingConfigPath) {
return existingConfigPath;
}
// If no config file exists, we'll create a .js one
const jsConfigPath = path.join(appDir, 'rspack.config.js');
const configTemplate = `const { defineConfig } = require('@meteorjs/rspack');
/**
* Rspack configuration for Meteor projects.
@@ -595,19 +606,19 @@ export function ensureRspackConfigExists() {
*
* Use these flags to adjust your build settings based on environment.
*/
export default defineConfig(Meteor => {
module.exports = defineConfig(Meteor => {
return {};
});
`;
if (!fs.existsSync(configPath)) {
if (!fs.existsSync(jsConfigPath)) {
try {
fs.writeFileSync(configPath, configTemplate, 'utf8');
fs.writeFileSync(jsConfigPath, configTemplate, 'utf8');
} catch (error) {
logError(`Failed to create rspack.config.js file: ${error.message}`);
throw error;
}
}
return configPath;
return jsConfigPath;
}

View File

@@ -4,6 +4,7 @@
*/
import { glob } from 'glob';
import path from 'path';
import fs from 'fs';
const { logInfo } = require('meteor/tools-core/lib/log');
const {
@@ -21,13 +22,63 @@ const {
isMeteorLessProject,
isMeteorScssProject,
getMeteorEnvPackageDirs,
getMeteorAppConfig,
getMeteorAppDir,
} = require('meteor/tools-core/lib/meteor');
const { buildUnignorePatterns } = require('meteor/tools-core/lib/ignore');
import { getInitialEntrypoints } from './build-context';
const { ensureModuleFilesExist, getBuildFilePath } = require('./build-context');
const { RSPACK_BUILD_CONTEXT, FILE_ROLE } = require('./constants');
/**
* Checks if entries exist in .meteorignore file
* @param {string[]} entries - Entries to check
* @returns {Object} Results with entry keys and boolean values
*/
function checkMeteorIgnoreExactEntries(entries) {
const meteorIgnorePath = path.join(getMeteorAppDir(), '.meteorignore');
const results = {};
// Initialize results object with false for each entry
entries.forEach(entry => {
results[entry] = false;
});
// Check if .meteorignore file exists
if (!fs.existsSync(meteorIgnorePath)) {
return results;
}
// Read the .meteorignore file
try {
const content = fs.readFileSync(meteorIgnorePath, 'utf8');
const lines = content.split('\n');
// Check each line against all entries
lines.forEach(line => {
// Skip empty lines and comments
if (!line.trim() || line.trim().startsWith('#')) {
return;
}
const trimmedLine = line.trim();
// Check for exact matches
entries.forEach(entry => {
if (trimmedLine === entry) {
results[entry] = true;
}
});
});
} catch (error) {
// If there's an error reading the file, return the initialized results
}
return results;
}
/**
* Gets the list of file extensions to ignore based on project type
* For Blaze projects, it excludes .html as used by Blaze
@@ -92,6 +143,7 @@ function getFileExtensionsToIgnore() {
* @returns {void}
*/
export function configureMeteorForRspack() {
const meteorAppConfig = getMeteorAppConfig();
const initialEntrypoints = getInitialEntrypoints();
// Ignore node_modules to prevent Meteor from processing them
@@ -138,13 +190,59 @@ export function configureMeteorForRspack() {
extraFoldersToIgnore = [];
}
// Skip immediate html and css children from intial entrypoint contexts
// Skip CSS/HTML files in entrypoint contexts
extraFilesToIgnore = [
...extraFilesToIgnore,
...initialEntrypointContexts.flatMap(entrypoint => [
`!${entrypoint}/*.html`,
`!${entrypoint}/*.css`,
]),
...initialEntrypointContexts.flatMap(entrypoint => {
const cssPattern = `${entrypoint}/*.css`;
const htmlPattern = `${entrypoint}/*.html`;
const cssFiles = glob.sync(cssPattern);
const htmlFiles = glob.sync(htmlPattern);
const entriesToCheck = [
cssPattern,
htmlPattern,
...cssFiles,
...htmlFiles
];
const entryResults = checkMeteorIgnoreExactEntries(entriesToCheck);
const hasMatchingCssPattern = entryResults[cssPattern];
const hasMatchingHtmlPattern = entryResults[htmlPattern];
const hasAnyCssFileInMeteorIgnore = cssFiles.some(file => entryResults[file]);
const hasAnyHtmlFileInMeteorIgnore = htmlFiles.some(file => entryResults[file]);
const result = [];
// Handle HTML files
if (hasAnyHtmlFileInMeteorIgnore) {
// Add individual HTML files that are not in meteorignore
htmlFiles.forEach(file => {
if (!entryResults[file]) {
result.push(`!${file}`);
}
});
} else if (!hasMatchingHtmlPattern) {
// Skip HTML pattern if not in meteorignore
result.push(`!${htmlPattern}`);
}
// Handle CSS files
if (hasAnyCssFileInMeteorIgnore) {
// Add individual CSS files that are not in meteorignore
cssFiles.forEach(file => {
if (!entryResults[file]) {
result.push(`!${file}`);
}
});
} else if (!hasMatchingCssPattern) {
// Skip CSS pattern if not in meteorignore
result.push(`!${cssPattern}`);
}
return result;
}),
];
const testIgnorePath = `${RSPACK_BUILD_CONTEXT}/${path.dirname(
@@ -176,13 +274,24 @@ export function configureMeteorForRspack() {
].filter(Boolean);
const rootFilesToIgnore = [
...projectRootFilesAndFolders.files.filter(
file => !['package.json', '.meteorignore', 'tsconfig.json'].includes(file),
file =>
![
'package.json',
'.meteorignore',
'tsconfig.json',
'postcss.config.js',
'scss-config.json',
].includes(file),
),
];
const filesToIgnore = [...rootFilesToIgnore, ...extraFilesToIgnore];
const unignoredFilesAndFolders = buildUnignorePatterns(
meteorAppConfig?.modules || [],
{ skipLevel: 1 },
);
const meteorAppIgnores = `${foldersToIgnore.join(' ')} ${filesToIgnore.join(
' ',
)}`;
)} ${unignoredFilesAndFolders.join(' ')}`.trim();
setMeteorAppIgnore(meteorAppIgnores);
if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) {

View File

@@ -3,9 +3,9 @@
* @description Constants and global state keys for Rspack plugin
*/
export const DEFAULT_RSPACK_VERSION = '1.5.0';
export const DEFAULT_RSPACK_VERSION = '1.5.3';
export const DEFAULT_METEOR_RSPACK_VERSION = '0.0.36';
export const DEFAULT_METEOR_RSPACK_VERSION = '0.0.60';
export const DEFAULT_METEOR_RSPACK_REACT_HMR_VERSION = '1.4.3';
@@ -36,38 +36,50 @@ export const GLOBAL_STATE_KEYS = {
RSPACK_REACT_INSTALLATION_CHECKED: 'rspack.rspackReactInstallationChecked',
RSPACK_DOCTOR_INSTALLATION_CHECKED: 'rspack.rspackDoctorInstallationChecked',
REACT_CHECKED: 'rspack.reactChecked',
TYPESCRIPT_CHECKED: 'rspack.typescriptChecked',
INITIAL_ENTRYPONTS: 'meteor.initialEntrypoints',
CLIENT_FIRST_COMPILE: 'rspack.clientFirstCompile',
SERVER_FIRST_COMPILE: 'rspack.serverFirstCompile',
BUILD_CONTEXT_FILES_CLEANED: 'rspack.buildContextFilesCleaned',
};
/**
* Default port for Rspack dev server
* @type {string|any|number}
*/
export const RSPACK_DEVSERVER_PORT = process.env.RSPACK_DEVSERVER_PORT || 8080;
const meteorConfig = typeof Plugin !== 'undefined' ? Plugin?.getMeteorConfig() : null;
/**
* Directory name for Rspack build context
* Can be overridden with RSPACK_BUILD_CONTEXT environment variable
* @constant {string}
*/
export const RSPACK_BUILD_CONTEXT = process.env.RSPACK_BUILD_CONTEXT || '_build';
export const RSPACK_BUILD_CONTEXT =
meteorConfig?.buildContext ||
process.env.RSPACK_BUILD_CONTEXT ||
'_build';
process.env.RSPACK_BUILD_CONTEXT = RSPACK_BUILD_CONTEXT;
/**
* Directory name for Rspack assets context
* Can be overridden with RSPACK_ASSETS_CONTEXT environment variable
* @constant {string}
*/
export const RSPACK_ASSETS_CONTEXT = process.env.RSPACK_ASSETS_CONTEXT || 'build-assets';
export const RSPACK_ASSETS_CONTEXT =
meteorConfig?.assetsContext ||
process.env.RSPACK_ASSETS_CONTEXT ||
'build-assets';
process.env.RSPACK_ASSETS_CONTEXT = RSPACK_ASSETS_CONTEXT;
/**
* Directory name for Rspack bundles context
* Can be overridden with RSPACK_ASSETS_CONTEXT environment variable
* @constant {string}
*/
export const RSPACK_CHUNKS_CONTEXT = process.env.RSPACK_CHUNKS_CONTEXT || 'build-chunks';
export const RSPACK_CHUNKS_CONTEXT =
meteorConfig?.chunksContext ||
process.env.RSPACK_CHUNKS_CONTEXT ||
'build-chunks';
process.env.RSPACK_CHUNKS_CONTEXT = RSPACK_CHUNKS_CONTEXT;
/**
* Directory name for Rspack doctor context
@@ -81,18 +93,6 @@ export const RSPACK_DOCTOR_CONTEXT = '.rsdoctor';
*/
export const RSPACK_HOT_UPDATE_REGEX = /^\/(.+\.hot-update\.(?:json|js))$/;
/**
* Regex pattern for rspack bundles
* @constant {RegExp}
*/
export const RSPACK_BUNDLES_REGEX = new RegExp(`^\/${RSPACK_CHUNKS_CONTEXT}\/(.+)$`);
/**
* Regex pattern for rspack assets
* @constant {RegExp}
*/
export const RSPACK_ASSETS_REGEX = new RegExp(`^\/${RSPACK_ASSETS_CONTEXT}\/(.+)$`);
export const FILE_ROLE = {
build: 'build',
entry: 'entry',

View File

@@ -81,13 +81,25 @@ async function ensureDependenciesInstalled(dependencies, globalStateKey, package
logInfo(`${dep}`);
});
// Check if this is a Yarn project
const isYarnProj = process.env.YARN_ENABLED === 'true';
// Install dev dependencies
const devDepsToInstall = allDepsToInstall.filter(dep => dep.dev === true || dep.dev == null);
if (devDepsToInstall.length > 0) {
const devDepsStrings = devDepsToInstall.map(dep => `${dep.name}@${dep.version}`);
// Log progress for dev dependencies
logProgress(
`🔧 Installing ${devDepsToInstall.length} dev dependenc${
devDepsToInstall.length === 1 ? "y" : "ies"
}...`
);
success = await installNpmDependency(devDepsStrings, {
cwd: appDir,
dev: true,
yarn: isYarnProj,
});
}
@@ -95,22 +107,37 @@ async function ensureDependenciesInstalled(dependencies, globalStateKey, package
const depsToInstall = allDepsToInstall.filter(dep => dep.dev === false);
if (depsToInstall.length > 0) {
const depsStrings = depsToInstall.map(dep => `${dep.name}@${dep.version}`);
const depsSuccess = await installNpmDependency(depsStrings, {
// Log progress for regular dependencies
logProgress(
`🔧 Installing ${depsToInstall.length} dependenc${
devDepsToInstall.length === 1 ? "y" : "ies"
}...`
);
let depsSuccess;
depsSuccess = await installNpmDependency(depsStrings, {
cwd: appDir,
dev: false,
yarn: isYarnProj,
});
success = success && depsSuccess;
}
if (!success) {
const isYarnProj = process.env.YARN_ENABLED === 'true';
const installCommand = isYarnProj
? `yarn add --dev ${dependencyStrings.join(' ').trim()}`
: `meteor npm install -D ${dependencyStrings.join(' ').trim()}`;
logError(`\n┌─────────────────────────────────────────────────`);
logError(`│ ❌ ${packageName} Installation Failed`);
logError(`└─────────────────────────────────────────────────`);
logError(`Run: meteor npm install -D ${joinWithAnd(dependencyStrings)}`);
logError(`Run: ${installCommand}`);
throw new Error(
`Failed to install ${packageName} dependencies. Please install them manually with: meteor npm install -D ${joinWithAnd(dependencyStrings)}`
`Failed to install ${packageName} dependencies. Please install them manually with: ${installCommand}`
);
}
@@ -199,3 +226,31 @@ export async function ensureRspackDoctorInstalled() {
'Rspack Doctor'
);
}
/**
* Checks if TypeScript is installed and sets global state accordingly
* Sets global state and environment variables based on TypeScript detection
* @returns {boolean} Whether TypeScript is installed
*/
export function checkTypescriptInstalled() {
// Skip if already checked
if (getGlobalState(GLOBAL_STATE_KEYS.TYPESCRIPT_CHECKED, false)) {
return;
}
const appDir = getMeteorAppDir();
// Check if TypeScript is a dependency in the project
const isTypescriptInstalled = checkNpmDependencyExists('typescript', { cwd: appDir });
if (isTypescriptInstalled) {
// Set environment variable to indicate TypeScript is enabled
process.env.METEOR_TYPESCRIPT_ENABLED = 'true';
} else {
process.env.METEOR_TYPESCRIPT_ENABLED = 'false';
}
// Mark as checked
setGlobalState(GLOBAL_STATE_KEYS.TYPESCRIPT_CHECKED, true);
return isTypescriptInstalled;
}

View File

@@ -2,8 +2,9 @@
* @module processes
* @description Functions for managing Rspack processes
*/
import { checkNpmDependencyExists, getNpxCommand } from 'meteor/tools-core/lib/npm';
import { RSPACK_DEVSERVER_PORT } from "./constants";
import fs from 'fs';
import path from 'path';
const {
spawnProcess,
@@ -31,8 +32,15 @@ const {
getMeteorInitialAppEntrypoints,
isMeteorAppConfigModernVerbose,
isMeteorBundleVisualizerProject,
getMeteorAppPort,
} = require('meteor/tools-core/lib/meteor');
const {
checkNpmDependencyExists,
getNpxCommand,
getMonorepoPath,
} = require('meteor/tools-core/lib/npm');
const {
getGlobalState,
setGlobalState
@@ -51,19 +59,114 @@ const {
} = require('./build-context');
/**
* Gets the appropriate config file name based on environment
* @returns {string} The name of the Rspack config file
* Calculates the devServerPort based on process.env.PORT
* Base port is 8077, and we add the sum of the digits of process.env.PORT
* @returns {number} The calculated devServerPort
*/
export function getConfigFileName() {
return `${process.cwd()}/node_modules/@meteorjs/rspack/rspack.config.js`;
export function calculateDevServerPort() {
const port = getMeteorAppPort();
const basePort = 8077;
// Sum the digits of the port
const digitSum = port.split('').reduce((sum, digit) => sum + parseInt(digit, 10), 0);
return basePort + digitSum;
}
/**
* Gets the appropriate Rspack environment variables
* Calculates the Rsdoctor client port based on process.env.PORT
* Base port is 8885, and we add the sum of the digits of process.env.PORT
* @returns {number} The calculated Rsdoctor client port
*/
export function calculateRsdoctorClientPort() {
const port = getMeteorAppPort();
const basePort = 8885;
// Sum the digits of the port
const digitSum = port.split('').reduce((sum, digit) => sum + parseInt(digit, 10), 0);
return basePort + digitSum;
}
/**
* Calculates the Rsdoctor server port based on process.env.PORT
* Base port is 8885, and we add the sum of the digits of process.env.PORT + 1
* @returns {number} The calculated Rsdoctor server port
*/
export function calculateRsdoctorServerPort() {
const port = getMeteorAppPort();
const basePort = 8885;
// Sum the digits of the port
const digitSum = port.split('').reduce((sum, digit) => sum + parseInt(digit, 10), 0);
// Add 1 to differentiate from client port
return basePort + digitSum + 1;
}
/**
* Helper function to check for a file with different extensions in order of priority
* @param {string} basePath - The base directory path (without 'rspack.config' and extension)
* @returns {string|null} The full path with extension if found, null otherwise
*/
export function getCustomConfigFilePath(basePath = getMeteorAppDir()) {
const configBasePath = path.join(basePath, 'rspack.config');
// Check for .js extension first (highest priority)
const jsPath = `${configBasePath}.js`;
if (fs.existsSync(jsPath)) {
return jsPath;
}
// Check for .mjs extension next
const mjsPath = `${configBasePath}.mjs`;
if (fs.existsSync(mjsPath)) {
return mjsPath;
}
// Check for .cjs extension last
const cjsPath = `${configBasePath}.cjs`;
if (fs.existsSync(cjsPath)) {
return cjsPath;
}
// No valid config file found with any extension
return null;
}
/**
* Gets the appropriate config file name based on environment
* @returns {string} The name of the Rspack config file
* @throws {Error} If no valid config file is found
*/
export function getConfigFilePath() {
// Check if the config file exists at the current path with any of the supported extensions
const defaultConfigBasePath = path.join(process.cwd(), 'node_modules/@meteorjs/rspack');
const defaultConfigPath = getCustomConfigFilePath(defaultConfigBasePath);
if (defaultConfigPath) {
return defaultConfigPath;
}
// If not found, check if we're in a monorepo and look for alternative config
const monorepoPath = getMonorepoPath();
if (monorepoPath) {
const alternativeConfigBasePath = path.join(monorepoPath, 'node_modules/@meteorjs/rspack');
const alternativeConfigPath = getCustomConfigFilePath(alternativeConfigBasePath);
if (alternativeConfigPath) {
return alternativeConfigPath;
}
}
// If no config file is found, throw an error
throw new Error('Could not find rspack.config.js, rspack.config.mjs, or rspack.config.cjs. Make sure @meteorjs/rspack is installed correctly.');
}
/**
* Gets the appropriate Rspack environment variables and command line arguments
* @param {Object} options - Options for environment variables
* @param {boolean} options.isClient - Whether this is for client-side build
* @param {boolean} options.isServer - Whether this is for server-side build
* @returns {string[]} Array of command line arguments for Rspack
* @returns {Object} Object containing params (command line arguments) and envs (environment variables)
*/
export function getRspackEnv({ isClient, isServer, isTest: inIsTest }) {
const RSPACK_BUILD_CONTEXT = require('./constants').RSPACK_BUILD_CONTEXT;
@@ -90,17 +193,23 @@ export function getRspackEnv({ isClient, isServer, isTest: inIsTest }) {
const entryKey = `${isTest && isTestModule ? 'test' : 'main'}${isClient ? 'Client' : 'Server'}`;
const inputFilePath = isTest && isTestModule ? initialEntrypoints.testModule : initialEntrypoints[entryKey];
const isTypescriptEnabled = inputFilePath?.endsWith('.ts') || inputFilePath?.endsWith('.tsx');
const isTsxEnabled = inputFilePath?.endsWith('.tsx');
const isJsxEnabled = inputFilePath?.endsWith('.jsx');
const isTypescriptEnabled = process.env.METEOR_TYPESCRIPT_ENABLED === 'true' ||
inputFilePath?.endsWith('.ts') ||
inputFilePath?.endsWith('.tsx');
const isReactEnabled = process.env.METEOR_REACT_ENABLED === 'true';
const isTsxEnabled = isTypescriptEnabled && (inputFilePath?.endsWith('.tsx') || isReactEnabled);
const isJsxEnabled = !isTypescriptEnabled && (inputFilePath?.endsWith('.jsx') || isReactEnabled);
const isReactEnabled = !!process.env.METEOR_REACT_ENABLED;
const isBlazeEnabled = isMeteorBlazeProject();
const isBlazeHotEnabled = isMeteorBlazeHotProject();
const isBundleVisualizerEnabled = isMeteorBundleVisualizerProject();
const swcExternalHelpers = checkNpmDependencyExists('@swc/helpers');
const configPath = getConfigFilePath();
const projectConfigPath = getCustomConfigFilePath();
const pairs = [
['isDevelopment', isMeteorAppDevelopment()],
['isProduction', isMeteorAppProduction()],
@@ -127,11 +236,12 @@ export function getRspackEnv({ isClient, isServer, isTest: inIsTest }) {
}),
],
['runPath', getBuildFilePath({ ...module, ...env, ...side, ...commandRole }) ],
['bannerOutput', JSON.stringify(getBuildFileContent({ ...module, ...env, ...side, role: FILE_ROLE.output }))],
['buildContext', RSPACK_BUILD_CONTEXT],
['chunksContext', RSPACK_CHUNKS_CONTEXT],
['assetsContext', RSPACK_ASSETS_CONTEXT],
['devServerPort', RSPACK_DEVSERVER_PORT],
['devServerPort', process.env.RSPACK_DEVSERVER_PORT],
['projectConfigPath', projectConfigPath],
['configPath', configPath],
...(swcExternalHelpers && [['swcExternalHelpers', swcExternalHelpers]] || []),
...(isReactEnabled && [['isReactEnabled', isReactEnabled]] || []),
...(isBlazeEnabled && [['isBlazeEnabled', isBlazeEnabled]] || []),
@@ -139,13 +249,26 @@ export function getRspackEnv({ isClient, isServer, isTest: inIsTest }) {
...(isTypescriptEnabled && [['isTypescriptEnabled', isTypescriptEnabled]] || []),
...(isTsxEnabled && [['isTsxEnabled', isTsxEnabled]] || []),
...(isJsxEnabled && [['isJsxEnabled', isJsxEnabled]] || []),
...(isBundleVisualizerEnabled && [['isBundleVisualizerEnabled', isBundleVisualizerEnabled]] || []),
...(isBundleVisualizerEnabled && [
['isBundleVisualizerEnabled', isBundleVisualizerEnabled],
['rsdoctorClientPort', process.env.RSDOCTOR_CLIENT_PORT],
['rsdoctorServerPort', process.env.RSDOCTOR_SERVER_PORT],
] || []),
].filter(Boolean);
return pairs.flatMap(([key, val]) => [
// Create environment variables object with bannerOutput
const envs = {
RSPACK_BANNER: JSON.stringify(getBuildFileContent({ ...module, ...env, ...side, role: FILE_ROLE.output }))
};
// Create params from pairs
const params = pairs.flatMap(([key, val]) => [
'--env',
`${key}=${val}`
]);
return { params, envs };
}
/**
@@ -165,12 +288,14 @@ export function startRspackClientServe(options = {}) {
}
const appDir = getMeteorAppDir();
const configFile = getConfigFileName();
const { command, args } = getNpxCommand(['rspack', 'serve', '--config', configFile, ...getRspackEnv({ isClient: true, isServer: false })]);
const configFile = getConfigFilePath();
const { params, envs } = getRspackEnv({ isClient: true, isServer: false });
const { command, args } = getNpxCommand(['rspack', 'serve', '--config', configFile, ...params]);
const newClientProcess = spawnProcess(
command,
args, {
cwd: appDir,
env: { ...process.env, ...envs },
onStdout: (data) => {
logInfo(`[Rspack Client] ${data}`);
if (onCompile && data.trim().includes("compiled")) {
@@ -187,11 +312,19 @@ export function startRspackClientServe(options = {}) {
if (data.includes('Loopback:') || data.includes('Project is running at:')) {
logInfo(`[Rspack Client] ${data}`);
} else {
// Check if this is the "npm error could not determine executable to run" error
if (data.includes('npm error could not determine executable to run')) {
const errorMsg = '[Rspack Client Error] Try running "meteor npm install" to ensure rspack is available';
logError(errorMsg);
throw new Error(errorMsg);
}
logError(`[Rspack Client Error] ${data}`);
}
},
onError: (err) => {
logError(`Rspack Error: ${err.message}`);
const errorMsg = `Rspack Error: ${err.message}`;
logError(errorMsg);
throw new Error(errorMsg);
}
});
@@ -218,12 +351,14 @@ export function startRspackServerWatch(options = {}) {
}
const appDir = getMeteorAppDir();
const configFile = getConfigFileName();
const { command, args } = getNpxCommand(['rspack', 'build', '--watch', '--config', configFile, ...getRspackEnv({ isClient: false, isServer: true })]);
const configFile = getConfigFilePath();
const { params, envs } = getRspackEnv({ isClient: false, isServer: true });
const { command, args } = getNpxCommand(['rspack', 'build', '--watch', '--config', configFile, ...params]);
const newServerProcess = spawnProcess(
command,
args, {
cwd: appDir,
env: { ...process.env, ...envs },
onStdout: (data) => {
logInfo(`[Rspack Server] ${data}`);
if (onCompile && data.trim().includes("compiled")) {
@@ -235,11 +370,19 @@ export function startRspackServerWatch(options = {}) {
if (data.includes('Project is running at:')) {
logInfo(`[Rspack Server] ${data}`);
} else {
// Check if this is the "npm error could not determine executable to run" error
if (data.includes('npm error could not determine executable to run')) {
const errorMsg = '[Rspack Server Error] Try running "meteor npm install" to ensure rspack is available';
logError(errorMsg);
throw new Error(errorMsg);
}
logError(`[Rspack Server Error] ${data}`);
}
},
onError: (err) => {
logError(`Rspack Error: ${err.message}`);
const errorMsg = `Rspack Error: ${err.message}`;
logError(errorMsg);
throw new Error(errorMsg);
}
});
@@ -262,18 +405,19 @@ export function startRspackServerWatch(options = {}) {
*/
export async function runRspackBuild({ isClient, isServer, isTest, isTestModule, onCompile, watch, label = 'Build' } = {}) {
const appDir = getMeteorAppDir();
const configFile = getConfigFileName();
const configFile = getConfigFilePath();
const endpoint = isTestModule ? 'Module' : isClient ? 'Client' : 'Server';
// Use a promise to ensure Meteor waits until Rspack finishes
return new Promise((resolve, reject) => {
const { params, envs } = getRspackEnv({ isClient, isServer, isTest, isTestModule });
const rspackArgs = [
'rspack',
'build',
'--config',
configFile,
...(watch && ['--watch']) || [],
...getRspackEnv({ isClient, isServer, isTest, isTestModule }),
...params,
].filter(Boolean);
const { command, args } = getNpxCommand(rspackArgs);
spawnProcess(
@@ -281,6 +425,7 @@ export async function runRspackBuild({ isClient, isServer, isTest, isTestModule,
args,
{
cwd: appDir,
env: { ...process.env, ...envs },
onStdout: (data) => {
logInfo(`[Rspack ${label} ${endpoint}] ${data}`);
if (onCompile && data.trim().includes("compiled")) {
@@ -292,6 +437,12 @@ export async function runRspackBuild({ isClient, isServer, isTest, isTestModule,
if (data.includes('Project is running at:')) {
logInfo(`[Rspack ${label} ${endpoint}] ${data}`);
} else {
// Check if this is the "npm error could not determine executable to run" error
if (data.includes('npm error could not determine executable to run')) {
const errorMsg = `[Rspack ${label} Error ${endpoint}] Try running "meteor npm install" to ensure rspack is available`;
logError(errorMsg);
throw new Error(errorMsg);
}
logError(`[Rspack ${label} Error ${endpoint}] ${data}`);
}
},

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "Integrate rspack into the Meteor lifecycle to run the bundler independently",
version: '1.0.0-beta340.0',
version: '1.0.0-beta340.11',
});
Package.registerBuildPlugin({

View File

@@ -22,8 +22,8 @@ const {
const {
ensureRspackInstalled,
checkReactInstalled,
checkTypescriptInstalled,
ensureRspackReactInstalled,
ensureRspackDoctorInstalled,
} = require('./lib/dependencies');
const {
@@ -37,6 +37,11 @@ const {
startRspackServerWatch,
runRspackBuild,
cleanup,
calculateDevServerPort,
calculateRsdoctorClientPort,
calculateRsdoctorServerPort,
getConfigFilePath,
getCustomConfigFilePath,
} = require('./lib/processes');
const {
@@ -66,6 +71,7 @@ const {
isMeteorAppDebug,
isMeteorAppConfigModernVerbose,
isMeteorAppNative,
isMeteorBundleVisualizerProject,
} = require('meteor/tools-core/lib/meteor');
const {
@@ -76,17 +82,29 @@ const {
const {
getNpxCommand,
getNpmCommand,
getYarnCommand,
isYarnProject,
} = require('meteor/tools-core/lib/npm');
const { getMeteorAppConfig, hasMeteorAppConfigAutoInstallDeps } = require("../tools-core/lib/meteor");
if (isMeteorAppRun() || isMeteorAppBuild() || isMeteorAppTest()) {
// Get entry points from Meteor configuration
setGlobalState(GLOBAL_STATE_KEYS.INITIAL_ENTRYPONTS, getMeteorAppEntrypoints());
let isYarnProj = process.env.YARN_ENABLED === 'true';
// Main entry point - using top-level await
try {
// Check if the project is a Yarn project and store the result in environment variable if not already set
if (process.env.YARN_ENABLED === undefined) {
isYarnProj = isYarnProject();
process.env.YARN_ENABLED = isYarnProj ? 'true' : 'false';
}
if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) {
logInfo(`[i] Meteor Npx prefix: ${getNpxCommand([])?.prefix}`);
logInfo(`[i] Meteor Npm prefix: ${getNpmCommand([])?.prefix}`);
if (isYarnProj) {
logInfo(`[i] Meteor Yarn prefix: ${getYarnCommand([])?.prefix}`);
}
}
// Clean build context files only if they haven't been cleaned yet
@@ -95,14 +113,23 @@ if (isMeteorAppRun() || isMeteorAppBuild() || isMeteorAppTest()) {
setGlobalState(GLOBAL_STATE_KEYS.BUILD_CONTEXT_FILES_CLEANED, true);
}
// Ensure Rspack is installed
await ensureRspackInstalled();
// Auto install deps (by default enabled)
if (hasMeteorAppConfigAutoInstallDeps()) {
// Ensure Rspack is installed
await ensureRspackInstalled();
}
// Check if Rspack React is installed
if (checkReactInstalled()) {
await ensureRspackReactInstalled();
// Auto install deps (by default enabled)
if (hasMeteorAppConfigAutoInstallDeps()) {
await ensureRspackReactInstalled();
}
}
// Check if TypeScript is installed
checkTypescriptInstalled();
// Ensure the Rspack build context directory exists
ensureRspackBuildContextExists();
@@ -112,6 +139,38 @@ if (isMeteorAppRun() || isMeteorAppBuild() || isMeteorAppTest()) {
// Configure Meteor settings for Rspack
configureMeteorForRspack();
// Calculate and set the devServerPort at boot
if (!process.env.RSPACK_DEVSERVER_PORT) {
process.env.RSPACK_DEVSERVER_PORT = calculateDevServerPort();
if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) {
logInfo(`[i] Rspack DevServer Port: ${process.env.RSPACK_DEVSERVER_PORT}`);
}
}
if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) {
const configFile = getConfigFilePath();
logInfo(`[i] Rspack default config: ${configFile}`);
const projectConfigFile = getCustomConfigFilePath();
logInfo(`[i] Rspack custom config: ${projectConfigFile}`);
}
// Calculate and set the Rsdoctor client and server ports at boot only if bundle visualizer is enabled
if (isMeteorBundleVisualizerProject()) {
if (!process.env.RSDOCTOR_CLIENT_PORT) {
process.env.RSDOCTOR_CLIENT_PORT = calculateRsdoctorClientPort();
if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) {
logInfo(`[i] Rsdoctor Client Port: ${process.env.RSDOCTOR_CLIENT_PORT}`);
}
}
if (!process.env.RSDOCTOR_SERVER_PORT) {
process.env.RSDOCTOR_SERVER_PORT = calculateRsdoctorServerPort();
if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) {
logInfo(`[i] Rsdoctor Server Port: ${process.env.RSDOCTOR_SERVER_PORT}`);
}
}
}
// Register cleanup handler
process.on('exit', cleanup);
process.on('SIGINT', () => {

View File

@@ -1,19 +1,38 @@
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { WebApp, WebAppInternals } from 'meteor/webapp';
import { shuffleString } from 'meteor/tools-core/lib/string';
import { createProxyMiddleware } from 'http-proxy-middleware';
import path from 'path';
import { parse as parseUrl } from 'url';
import {
RSPACK_CHUNKS_CONTEXT,
RSPACK_ASSETS_CONTEXT,
RSPACK_HOT_UPDATE_REGEX,
RSPACK_BUNDLES_REGEX,
RSPACK_ASSETS_REGEX,
RSPACK_DEVSERVER_PORT,
} from "./lib/constants";
// Define constants for both development and production
const rspackChunksContext = process.env.RSPACK_CHUNKS_CONTEXT || RSPACK_CHUNKS_CONTEXT;
const rspackAssetsContext = process.env.RSPACK_ASSETS_CONTEXT || RSPACK_ASSETS_CONTEXT;
/**
* Regex pattern for rspack bundles
* @constant {RegExp}
*/
const RSPACK_CHUNKS_REGEX = new RegExp(
`^\/${rspackChunksContext}\/(.+)$`,
);
/**
* Regex pattern for rspack assets
* @constant {RegExp}
*/
const RSPACK_ASSETS_REGEX = new RegExp(
`^\/${rspackAssetsContext}\/(.+)$`,
);
if (Meteor.isDevelopment) {
// Target URL for the Rspack dev server
const target = `http://localhost:${RSPACK_DEVSERVER_PORT}`;
const target = `http://localhost:${process.env.RSPACK_DEVSERVER_PORT}`;
// Proxy HMR websocket upgrade requests
WebApp.connectHandlers.use('/ws',
@@ -53,10 +72,10 @@ if (Meteor.isDevelopment) {
}
// 2) match "/build-chunks/<anything>"
const bundlesMatch = req.url.match(RSPACK_BUNDLES_REGEX);
const bundlesMatch = req.url.match(RSPACK_CHUNKS_REGEX);
if (bundlesMatch) {
// Redirect "/bundles/foo.js" → "/__rspack__/build-chunks/foo.js"
const target = `/__rspack__/${RSPACK_CHUNKS_CONTEXT}/${bundlesMatch[1]}`;
const target = `/__rspack__/${rspackChunksContext}/${bundlesMatch[1]}`;
res.writeHead(307, { Location: target });
return res.end();
}
@@ -65,7 +84,7 @@ if (Meteor.isDevelopment) {
const assetsMatch = req.url.match(RSPACK_ASSETS_REGEX);
if (assetsMatch) {
// Redirect "/build-assets/foo.js" → "/__rspack__/build-assets/foo.js"
const target = `/__rspack__/${RSPACK_ASSETS_CONTEXT}/${assetsMatch[1]}`;
const target = `/__rspack__/${rspackAssetsContext}/${assetsMatch[1]}`;
res.writeHead(307, { Location: target });
return res.end();
}
@@ -105,3 +124,80 @@ if (Meteor.isDevelopment) {
// Enable client reload on server startup
enableClientReloadOnServerStart();
}
/**
* Register a single rspack static asset with WebAppInternals.staticFilesByArch
* @param {string} arch - The architecture to register the asset for
* @param {string} pathname - The pathname of the asset
* @param {string} filePath - The absolute path to the asset on disk
* @returns {Object} The static file info object
*/
function registerRspackStaticAsset(arch, pathname, filePath) {
// Ensure the architecture exists in staticFilesByArch
if (!WebAppInternals.staticFilesByArch[arch]) {
WebAppInternals.staticFilesByArch[arch] = Object.create(null);
}
// Get the static files object for this architecture
const staticFiles = WebAppInternals.staticFilesByArch[arch];
// Skip if already registered
if (staticFiles[pathname]) {
// Ensure the entry is marked as cacheable
staticFiles[pathname].cacheable = true;
return staticFiles[pathname];
}
// Determine file type based on extension
const type = pathname.endsWith(".js") ? "js" :
pathname.endsWith(".css") ? "css" :
pathname.endsWith(".json") ? "json" : undefined;
// Extract hash from filename (assuming it's the second part after splitting by '.')
const filename = pathname.split("/").pop();
const hash = filename.split(".")[1];
// Register the asset
staticFiles[pathname] = {
absolutePath: filePath,
cacheable: true, // Most rspack assets are cacheable
hash,
type
};
return staticFiles[pathname];
}
// Store the original staticFilesMiddleware
const originalStaticFilesMiddleware = WebAppInternals.staticFilesMiddleware;
// Handle rspack assets on-demand to add Meteor's static files headers
WebAppInternals.staticFilesMiddleware = async function(staticFilesByArch, req, res, next) {
const pathname = parseUrl(req.url).pathname;
try {
// Check if this is a rspack asset request
const chunksMatch = pathname.match(RSPACK_CHUNKS_REGEX);
const assetsMatch = pathname.match(RSPACK_ASSETS_REGEX);
if (chunksMatch || assetsMatch) {
const cwd = process.cwd();
const architectures = ["web.browser", "web.browser.legacy", "web.cordova"];
WebApp.categorizeRequest(req);
// Try to find the file on disk
const context = chunksMatch ? rspackChunksContext : rspackAssetsContext;
const filename = (chunksMatch ? chunksMatch[1] : assetsMatch[1]);
const filePath = path.join(cwd, context, filename);
architectures.forEach(archName => {
registerRspackStaticAsset(archName, pathname, filePath);
});
}
} catch (e) {
console.error(`Error handling rspack asset: ${e.message}`);
}
// Call the original middleware
return originalStaticFilesMiddleware(staticFilesByArch, req, res, next);
};

View File

@@ -1,6 +1,6 @@
Package.describe({
name: "shell-server",
version: '0.6.2',
version: '0.7.0-beta340.11',
summary: "Server-side component of the `meteor shell` command.",
documentation: "README.md"
});

View File

@@ -12,7 +12,7 @@ Package.registerBuildPlugin({
'ecmascript'
],
npmDependencies: {
'@meteorjs/swc-core': '1.12.14',
'@meteorjs/swc-core': '1.13.5',
'acorn': '8.10.0',
"@babel/runtime": "7.18.9",
'@babel/parser': '7.22.7',

File diff suppressed because it is too large Load Diff

View File

@@ -44,12 +44,15 @@
<ul class="navbar-nav mr-auto">
{{#each groupPaths}}
<li class="nav-item"><span class="nav-link">&nbsp;-&nbsp;</span></li>
<li class="nav-item"><a class="nav-link" href="#">{{name}}</a></li>
<li class="nav-item"><a class="nav-link group" href="#">{{name}}</a></li>
{{/each}}
</ul>
<form class="navbar-form pull-right">
<span id="current-client-test"></span>
{{#if isFiltered}}
<button class="btn btn-secondary run-all">Run All Tests</button>
{{/if}}
<button class="btn btn-primary rerun">
{{#if rerunScheduled}}
Rerun scheduled...

View File

@@ -1,9 +1,13 @@
////
//// Setup
////
import { diff_match_patch } from './diff_match_patch_uncompressed'
import { diffChars } from 'diff'
import 'bootstrap/dist/css/bootstrap.min.css';
const arraysEqual = (a, b) => {
return a.length === b.length && a.every((v, i) => v === b[i]);
}
// dependency for the count of tests running/passed/failed, etc. drives
// the navbar and the like.
var countDep = new Tracker.Dependency;
@@ -43,12 +47,118 @@ window.onerror = (message, source, line) => {
Session.set("uncaughtErrors", Array.from(uncaughtErrors));
};
Session.setDefault("groupPath", ["tinytest"]);
var getGroupPathFromURL = function() {
var pathname = window.location.pathname;
var match = pathname.match(/^\/group\/(.+)$/);
if (match) {
try {
return JSON.parse(decodeURIComponent(match[1]));
} catch (e) {
console.warn('Invalid group path in URL:', match[1]);
}
}
return ["tinytest"];
};
var setGroupPathInURL = function(groupPath, pushState = true) {
var newURL = '/';
if (!arraysEqual(groupPath, ['tinytest'])) {
newURL = '/group/' + encodeURIComponent(JSON.stringify(groupPath));
}
var historyState = { groupPath: groupPath };
if (pushState) {
window.history.pushState(historyState, '', newURL);
} else {
window.history.replaceState(historyState, '', newURL);
}
};
// Initialize group path from URL, fallback to default
var initialGroupPath = getGroupPathFromURL();
Session.setDefault("groupPath", initialGroupPath);
Session.set("rerunScheduled", false);
// Safeguards for rapid navigation
var isNavigating = false;
var lastNavigationTime = 0;
var navigationDebounceMs = 200; // Minimum time between navigations
// Handle browser back/forward navigation
window.addEventListener('popstate', function(event) {
var now = Date.now();
// Safeguard 1: Prevent overlapping navigations
if (isNavigating) {
console.log('Navigation already in progress, ignoring');
return;
}
// Safeguard 2: Debounce rapid successive calls
if (now - lastNavigationTime < navigationDebounceMs) {
console.log('Navigation too rapid, ignoring');
return;
}
var newGroupPath = getGroupPathFromURL();
var currentGroupPath = Session.get("groupPath");
if (!arraysEqual(newGroupPath, currentGroupPath)) {
// Set navigation flag
isNavigating = true;
lastNavigationTime = now;
// Emulate the EXACT same sequence as changeToPath
// 1. URL is already changed by browser, but ensure it's correct
setGroupPathInURL(newGroupPath, false); // replaceState, don't create new entry
// 2. Update session state (SAME as changeToPath)
Session.set("groupPath", newGroupPath);
Session.set("rerunScheduled", true);
// 3. Clean reload (SAME as changeToPath)
Reload._reload();
}
});
// This function is exported. It's called on client startup by the
// bundle generated by `meteor test` or `meteor test-packages`.
runTests = function () {
// Reset navigation safeguards when app starts
isNavigating = false;
// Reset all test state before starting
running = true;
totalCount = 0;
passedCount = 0;
failedCount = 0;
failedTests = [];
resultTree = [];
// Reset dependencies to trigger UI updates
countDep.changed();
topLevelGroupsDep.changed();
// Get current group path from URL (in case of refresh)
var currentGroupPath = getGroupPathFromURL();
Session.set("groupPath", currentGroupPath);
// Only update URL if it's actually different from what we expect
var expectedURL;
if (currentGroupPath && currentGroupPath.length > 0 && JSON.stringify(currentGroupPath) !== JSON.stringify(["tinytest"])) {
expectedURL = '/group/' + encodeURIComponent(JSON.stringify(currentGroupPath));
} else {
expectedURL = '/';
}
// Only do replaceState if the URL doesn't match what we expect
if (window.location.pathname !== expectedURL) {
setGroupPathInURL(currentGroupPath, false);
}
document.body.innerHTML = "";
document.head.title = "Tests";
@@ -62,7 +172,7 @@ runTests = function () {
Tracker.flush();
Meteor.connection._unsubscribeAll();
}, Session.get("groupPath"));
}, currentGroupPath);
};
@@ -322,6 +432,10 @@ Template.progressBar.helpers({
//// Template - groupNav
var changeToPath = function (path) {
// Update URL with new group path (pushState creates new history entry)
setGroupPathInURL(path, true);
// Update session to trigger UI updates
Session.set("groupPath", path);
Session.set("rerunScheduled", true);
// pretend there's just been a hot code push
@@ -340,6 +454,10 @@ Template.groupNav.helpers({
},
rerunScheduled: function () {
return Session.get("rerunScheduled");
},
isFiltered: function () {
var groupPath = Session.get("groupPath");
return groupPath.length > 1 || groupPath[0] !== "tinytest";
}
});
@@ -350,6 +468,9 @@ Template.groupNav.events({
'click .rerun': function () {
Session.set("rerunScheduled", true);
Reload._reload();
},
'click .run-all': function () {
changeToPath(["tinytest"]);
}
});
@@ -507,10 +628,13 @@ Template.event.events({
// e.g. doDiff('abc', 'bcd') => [[-1, 'a'], [0, 'bc'], [1, 'd']]
var doDiff = function (str1, str2) {
var D = new diff_match_patch();
var pieces = D.diff_main(str1, str2, false);
D.diff_cleanupSemantic(pieces);
return pieces;
const diff = diffChars(str1, str2);
return diff.map(part => {
if (part.added) return [1, part.value];
if (part.removed) return [-1, part.value];
return [0, part.value];
});
};
Template.event.helpers({

View File

@@ -1,11 +1,12 @@
Package.describe({
summary: "Run tests interactively in the browser",
version: '1.4.0',
version: '1.5.0-beta340.11',
documentation: null
});
Npm.depends({
'bootstrap': '4.3.1',
'diff': '8.0.2'
});
Package.onUse(function (api) {

View File

@@ -0,0 +1,87 @@
/**
* Build gitignore-style "unignore" patterns for specific files/folders.
*
* Rules:
* - Files: !a/ !a/b/ !a/b/c.txt
* - Folders (must end with '/'):
* !a/ !a/b/ !a/b/c/ !a/b/c/**
*
* @param {string[]} inputPaths Paths to keep. Use '/' for dirs (e.g. 'assets/public/').
* @param {Object} [options]
* @param {boolean} [options.includeAllAncestors=true] If false, only include the immediate parent dir.
* @param {boolean} [options.includeGlobForDirs=true] Emit '**' for directories.
* @param {number} [options.skipLevel=0] Skip this many levels from the beginning.
* @returns {string[]} Negation patterns, in correct order.
*/
export function buildUnignorePatterns(inputPaths, {
includeAllAncestors = true,
includeGlobForDirs = true,
skipLevel = 0,
} = {}) {
const out = [];
const seen = new Set();
const push = (p) => {
if (!seen.has(p)) {
seen.add(p);
out.push(p);
}
};
for (let raw of inputPaths) {
if (!raw || typeof raw !== 'string') continue;
// Normalize: forward slashes, drop leading './', collapse double slashes
let anchored = raw.startsWith('/');
let p = raw.replace(/\\/g, '/')
.replace(/^\.\/+/, '')
.replace(/\/{2,}/g, '/');
// detect dir by trailing slash
const isDir = p.endsWith('/');
// strip leading + trailing slashes for splitting, but remember anchoring
const core = p.replace(/^\/+/, '').replace(/\/+$/, '');
if (!core) continue;
const parts = core.split('/');
// Process based on skipLevel
if (skipLevel >= parts.length) {
// Skip everything if skipLevel is greater than or equal to the number of parts
continue;
}
// Ancestors (top-down)
if (includeAllAncestors) {
// Start from skipLevel + 1 to skip the specified number of levels
const startLevel = Math.max(1, skipLevel + 1);
for (let i = startLevel; i <= parts.length - 1; i++) {
const anc = (anchored ? '/' : '') + parts.slice(0, i).join('/') + '/';
push('!' + anc);
}
} else if (parts.length > 1) {
// Only immediate parent
// For minimal mode with skipLevel, we need to check if the parent is at a level we should skip
if (skipLevel < parts.length - 1) {
// Check if the parent's level is greater than skipLevel
const parentLevel = parts.length - 1;
if (parentLevel > skipLevel) {
const parent = (anchored ? '/' : '') + parts.slice(0, parts.length - 1).join('/') + '/';
push('!' + parent);
}
}
}
// Add the file/directory pattern
if (isDir) {
const dir = (anchored ? '/' : '') + parts.join('/') + '/';
push('!' + dir);
if (includeGlobForDirs) push('!' + dir + '**');
} else {
const file = (anchored ? '/' : '') + parts.join('/');
push('!' + file);
}
}
return out;
}

View File

@@ -29,6 +29,14 @@ export function getMeteorAppConfig() {
: getMeteorAppPackageJson()?.meteor;
}
/**
* Get Meteor's app port
* @returns {false|*}
*/
export function getMeteorAppPort() {
return Package?.meteor?.global?.currentCommand?.options?.['port'] || process.env.PORT || '3000';
}
/**
* Retrieves the modern configuration from the application's package.json.
* @returns {Object|undefined} The modern configuration object or undefined if not found.
@@ -46,6 +54,15 @@ export function isMeteorAppConfigModernVerbose() {
getMeteorAppConfigModern()?.transpiler?.verbose || false;
}
/**
* Retrieves the auto install deps flag from the app's package.json.
* @returns {Boolean|*}
*/
export function hasMeteorAppConfigAutoInstallDeps() {
const { autoInstallDeps = true } = getMeteorAppConfig() || {};
return !!autoInstallDeps;
}
/**
* Retrieves the entry points for the Meteor application from the configuration.
* Uses Plugin.getMeteorConfig() if available, otherwise falls back to getMeteorAppConfig().
@@ -404,6 +421,14 @@ export function isMeteorBundleVisualizerProject() {
return getMeteorAppPackages().includes('bundle-visualizer');
}
/**
* Checks if the Meteor application is a Typescript project.
* @returns {boolean} True if the application is a Typescript project, false otherwise.
*/
export function isMeteorTypescriptProject() {
return getMeteorAppPackages().includes('typescript');
}
/**
* Checks if the current Meteor command is 'test-packages'.
* @returns {boolean} True if the current command is 'test-packages', false otherwise.

View File

@@ -136,6 +136,37 @@ function buildNpmInstallArgs(dependencies, options = {}) {
return args;
}
/**
* Builds yarn install arguments based on options and dependencies
*
* @param {string|string[]} dependencies - The npm dependency or dependencies to install
* @param {Object} [options] - Options for the installation
* @param {boolean} [options.dev=false] - If true, install as a dev dependency
* @param {boolean} [options.exact=false] - If true, install with exact version
* @returns {string[]} Array of arguments for the yarn add command
*/
function buildYarnInstallArgs(dependencies, options = {}) {
const args = ['add'];
// Add flags based on options
if (options.dev) {
args.push('--dev');
}
if (options.exact) {
args.push('--exact');
}
// Add dependencies to the command
if (Array.isArray(dependencies)) {
args.push(...dependencies);
} else {
args.push(dependencies);
}
return args;
}
/**
* Executes a command and returns a promise that resolves to true if successful
*
@@ -161,17 +192,26 @@ function executeCommand(command, args, options) {
/**
* Installs a npm dependency using direct npm binary if available, otherwise falls back to `meteor npm install`.
* If yarn option is true, uses yarn instead.
*
* @param {string|string[]} dependencies - The npm dependency or dependencies to install
* @param {Object} [options] - Options for the installation
* @param {string} [options.cwd] - Current working directory (defaults to process.cwd())
* @param {boolean} [options.dev=false] - If true, install as a dev dependency
* @param {boolean} [options.exact=false] - If true, install with exact version
* @param {boolean} [options.yarn=false] - If true, use yarn instead of npm
* @returns {Promise<boolean>} A promise that resolves to true if installation succeeded, false otherwise
*/
export function installNpmDependency(dependencies, options = {}) {
const cwd = options.cwd || process.cwd();
// If yarn option is true, use yarn
if (options.yarn) {
const { command, args: baseArgs } = getYarnCommand([]);
const args = buildYarnInstallArgs(dependencies, options);
return executeCommand(command, [...baseArgs, ...args], { cwd });
}
// Try to get the npm binary path
const npmBinaryPath = getNodeBinaryPath('npm');
@@ -319,3 +359,129 @@ export function getNpxCommand(args) {
prefix: `meteor npx`,
};
}
/**
* Checks if the current project is a Yarn project.
* Looks for yarn.lock file in the current working directory and checks packageManager in package.json.
*
* @param {Object} [options] - Options for the check
* @param {string} [options.cwd] - Current working directory (defaults to process.cwd())
* @returns {boolean} True if it's a Yarn project, false otherwise
*/
export function isYarnProject(options = {}) {
const cwd = options.cwd || process.cwd();
// Check if yarn.lock exists
const yarnLockPath = path.join(cwd, 'yarn.lock');
if (fs.existsSync(yarnLockPath)) {
return true;
}
// Check packageManager field in package.json
try {
const packageJsonPath = path.join(cwd, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
// Check if packageManager contains "yarn"
if (packageJson.packageManager && packageJson.packageManager.includes('yarn')) {
return true;
}
}
} catch (error) {
// If there's an error reading or parsing package.json, continue
}
return false;
}
/**
* Gets the yarn command and arguments
* @param {string[]} args - The arguments to pass to yarn
* @returns {Object} An object with command, args, and base properties
*/
export function getYarnCommand(args) {
// Try to get the yarn binary path
const yarnBinaryPath = getNodeBinaryPath('yarn');
// If we have a direct path to yarn, use it
if (yarnBinaryPath && fs.existsSync(yarnBinaryPath)) {
return {
command: yarnBinaryPath,
args,
prefix: `${yarnBinaryPath}`,
};
}
// Fall back to using 'yarn' directly
return {
command: 'yarn',
args,
prefix: `yarn`,
};
}
/**
* Gets the path to the monorepo root by checking for common monorepo indicators.
* Traverses up the directory tree until it finds a monorepo indicator or reaches the root.
*
* @param {Object} [options] - Options for the detection
* @param {string} [options.cwd] - Current working directory (defaults to process.cwd())
* @returns {string|null} Path to the monorepo root if found, null otherwise
*/
export function getMonorepoPath(options = {}) {
const cwd = options.cwd || process.cwd();
let currentDir = cwd;
// Function to check if directory has monorepo indicators
const hasMonorepoIndicators = (dir) => {
try {
// Check for npm/yarn workspaces in package.json
const packageJsonPath = path.join(dir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (packageJson.workspaces) {
return true;
}
}
// Check for Lerna
const lernaJsonPath = path.join(dir, 'lerna.json');
if (fs.existsSync(lernaJsonPath)) {
return true;
}
// Check for pnpm workspaces
const pnpmWorkspacePath = path.join(dir, 'pnpm-workspace.yaml');
if (fs.existsSync(pnpmWorkspacePath)) {
return true;
}
return false;
} catch (error) {
return false;
}
};
// Traverse up the directory tree
while (currentDir !== path.dirname(currentDir)) { // Stop when we reach the root directory
if (hasMonorepoIndicators(currentDir)) {
return currentDir;
}
currentDir = path.dirname(currentDir);
}
// Check the root directory as well
return hasMonorepoIndicators(currentDir) ? currentDir : null;
}
/**
* Detects if a directory is within a monorepo by checking for common monorepo indicators.
*
* @param {Object} [options] - Options for the detection
* @param {string} [options.cwd] - Current working directory (defaults to process.cwd())
* @returns {boolean} True if the directory is within a monorepo, false otherwise
*/
export function isMonorepo(options = {}) {
return getMonorepoPath(options) !== null;
}

View File

@@ -25,6 +25,7 @@ export function spawnProcess(command, args, options = {}) {
cwd: options.cwd || process.cwd(),
stdio: ['pipe', 'pipe', 'pipe'],
detached: options.detached || false,
...(process.platform === 'win32' && { shell: true }),
});
// Add a reference to track if the process is running

View File

@@ -1,6 +1,6 @@
Package.describe({
summary: "Helpers for managing modern tools in Meteor",
version: '1.0.0-beta340.0',
version: '1.0.0-beta340.11',
});
Package.onUse(function (api) {

View File

@@ -5,3 +5,4 @@ export * from './lib/process';
export * from './lib/global-state';
export * from './lib/git';
export * from './lib/string';
export * from './lib/ignore';

View File

@@ -1,6 +1,6 @@
Package.describe({
name: 'typescript',
version: '5.6.6',
version: '5.7.0-beta340.11',
summary:
'Compiler plugin that compiles TypeScript and ECMAScript in .ts and .tsx files',
documentation: 'README.md',

View File

@@ -1,6 +1,6 @@
{
"track": "METEOR",
"version": "3.3.1-rc.2",
"version": "3.4-beta.11",
"recommended": false,
"official": false,
"description": "Meteor experimental release"

View File

@@ -12,6 +12,7 @@ var packageJson = {
// and we want to make sure there are no dependencies on a higher version
npm: "10.9.3",
"node-gyp": "10.2.0",
"node-gyp-build": "4.8.4",
"@mapbox/node-pre-gyp": "1.0.11",
typescript: "5.6.3",
"@meteorjs/babel": "7.20.0",

View File

@@ -690,6 +690,7 @@ function getExamplesJSON(){
const DEFAULT_SKELETON = "react";
export const AVAILABLE_SKELETONS = [
"apollo",
"babel",
"bare",
"blaze",
"full",
@@ -702,6 +703,7 @@ export const AVAILABLE_SKELETONS = [
"chakra-ui",
"solid",
"legacy",
"coffeescript"
];
const SKELETON_INFO = {
@@ -716,8 +718,10 @@ const SKELETON_INFO = {
"svelte": "To create a basic Svelte app",
"tailwind": "To create an app using React and Tailwind",
"chakra-ui": "To create an app Chakra UI and React",
"solid": "To create a basic Solid app"
}
"solid": "To create a basic Solid app",
"coffeescript": "To create a basic CoffeeScript app",
"babel": "To create a React app with Babel support",
};
main.registerCommand({
name: 'create',
@@ -727,6 +731,7 @@ main.registerCommand({
list: { type: Boolean },
example: { type: String },
package: { type: Boolean },
babel: { type: Boolean },
bare: { type: Boolean },
minimal: { type: Boolean },
full: { type: Boolean },
@@ -738,6 +743,7 @@ main.registerCommand({
svelte: { type: Boolean },
tailwind: { type: Boolean },
'chakra-ui': { type: Boolean },
coffeescript: { type: Boolean },
solid: { type: Boolean },
legacy: { type: Boolean },
prototype: { type: Boolean },
@@ -842,7 +848,7 @@ main.registerCommand({
return transform(f);
},
transformContents: async function (contents, f) {
if (/(\.html|\.[jt]sx?|\.css)/.test(f)) {
if (/(\.html|\.[jt]sx?|\.css|\.coffee)/.test(f)) {
return Buffer.from(await transform(contents.toString()));
} else {
return contents;
@@ -1227,7 +1233,7 @@ main.registerCommand({
return Buffer.from(contents.toString().replace(/~prototype~/g, ""));
}
}
if (/(\.html|\.[jt]sx?|\.css)/.test(f)) {
if (/(\.html|\.[jt]sx?|\.css|\.coffee)/.test(f)) {
return Buffer.from(transform(contents.toString()));
} else {
return contents;

View File

@@ -153,7 +153,7 @@ Options:
>>> create
Create a new project.
Usage: meteor create [--release <release>] [--bare|--minimal|--full|--react|--vue|--apollo|--svelte|--blaze|--tailwind|--chakra-ui|--solid] <path>
Usage: meteor create [--release <release>] [--bare|--minimal|--full|--react|--vue|--apollo|--svelte|--blaze|--tailwind|--chakra-ui|--solid|--babel|--coffeescript] <path>
meteor create [--release <release>] --example <example_name> [<path>]
meteor create [--release <release>] --from <git_url> [<path>]
meteor create --list
@@ -195,6 +195,8 @@ Options:
--blaze Create a basic blaze-based app.
--tailwind Create a basic react-based app, with tailwind configured.
--chakra-ui Create a basic react-based app, with chakra-ui configured.
--coffeescript Create a basic coffescript app, with react.
--babel Create a React app with Babel support.
--solid Create a basic solid-based app.
--prototype Create a prototype app with the insecure & autopublish packages. Can be used along with other app commands

View File

@@ -2,6 +2,10 @@ var showRequireProfile = ('METEOR_PROFILE_REQUIRE' in process.env);
if (showRequireProfile) {
require('../tool-env/profile-require.js').start();
}
const { initMeteorConfig } = require('../tool-env/meteor-config');
// Initialize meteorConfig globally
initMeteorConfig();
var assert = require("assert");
var _ = require('underscore');
@@ -287,16 +291,12 @@ main.captureAndExit = async function (header, title, f) {
// NB: files required up to this point may not define commands
const { initMeteorConfig } = require('../tool-env/meteor-config');
require('./commands.js');
require('./commands-packages.js');
require('./commands-packages-query.js');
require('./commands-cordova.js');
require('./commands-aliases.js');
// Initialize meteorConfig globally
initMeteorConfig();
///////////////////////////////////////////////////////////////////////////////
// Record all the top-level commands as JSON
///////////////////////////////////////////////////////////////////////////////

View File

@@ -1,4 +1,4 @@
import { defineConfig } from '@meteorjs/rspack';
const { defineConfig } = require('@meteorjs/rspack');
/**
* Rspack configuration for Meteor projects.
@@ -10,7 +10,7 @@ import { defineConfig } from '@meteorjs/rspack';
*
* Use these flags to adjust your build settings based on environment.
*/
export default defineConfig(Meteor => {
module.exports = defineConfig(Meteor => {
return {
module: {
rules: [

View File

@@ -0,0 +1 @@
install-strategy=nested

View File

@@ -0,0 +1,22 @@
# Meteor packages used by this project, one per line.
# Check this file (and the other files in this directory) into your repository.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
meteor-base # Packages every Meteor app needs to have
mobile-experience # Packages for a great mobile UX
mongo # The database Meteor supports right now
reactive-var # Reactive variable for tracker
standard-minifier-css # CSS minifier run for production mode
standard-minifier-js # JS minifier run for production mode
es5-shim # ECMAScript 5 compatibility for older browsers
ecmascript # Enable ECMAScript2015+ syntax in app code
typescript # Enable TypeScript syntax in .ts and .tsx modules
shell-server # Server-side component of the `meteor shell` command
hot-module-replacement # Update client in development without reloading the page
static-html # Define static page content in .html files
react-meteor-data # React higher-order component for reactively tracking Meteor data

View File

@@ -0,0 +1,2 @@
server
browser

View File

@@ -0,0 +1 @@
none

View File

@@ -0,0 +1 @@
**/ignore*

View File

@@ -0,0 +1,8 @@
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true
}
}
}

View File

@@ -0,0 +1,8 @@
<head>
<title>monorepo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div id="react-target"></div>
</body>

View File

@@ -0,0 +1,10 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { Meteor } from 'meteor/meteor';
import { App } from '/imports/ui/App';
Meteor.startup(() => {
const container = document.getElementById('react-target');
const root = createRoot(container);
root.render(<App />);
});

View File

@@ -0,0 +1 @@
throw new Error("This file should be ignored");

View File

@@ -0,0 +1,8 @@
import { Mongo } from 'meteor/mongo';
import { Buffer } from "node:buffer";
// Don't trigger an error on using node modules
// as ignored to enable shared client/server code
console.log("Buffer loaded: ", !!Buffer);
export const LinksCollection = new Mongo.Collection('links');

View File

@@ -0,0 +1,15 @@
import { Button, Html } from '@react-email/components';
import * as React from 'react';
export default function TestEmail() {
return (
<Html>
<Button
href='https://example.com'
style={{ background: '#000', color: '#fff', padding: '12px 20px' }}
>
Click me
</Button>
</Html>
);
}

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { Hello } from './Hello.jsx';
import { Info } from './Info.jsx';
export const App = () => (
<div>
<h1>Welcome to Meteor!</h1>
<Hello/>
<Info/>
</div>
);

View File

@@ -0,0 +1,16 @@
import React, { useState } from 'react';
export const Hello = () => {
const [counter, setCounter] = useState(0);
const increment = () => {
setCounter(counter + 1);
};
return (
<div>
<button onClick={increment}>Click Me</button>
<p>You've pressed the button {counter} times.</p>
</div>
);
};

View File

@@ -0,0 +1,23 @@
import React from 'react';
import { useFind, useSubscribe } from 'meteor/react-meteor-data';
import { LinksCollection } from '../api/links';
export const Info = () => {
const isLoading = useSubscribe('links');
const links = useFind(() => LinksCollection.find());
if(isLoading()) {
return <div>Loading...</div>;
}
return (
<div>
<h2>Learn Meteor!</h2>
<ul>{links.map(
link => <li key={link._id}>
<a href={link.url} target="_blank">{link.title}</a>
</li>
)}</ul>
</div>
);
};

View File

@@ -0,0 +1,33 @@
{
"name": "app",
"private": true,
"scripts": {
"start": "meteor run",
"test": "meteor test --once --driver-package meteortesting:mocha",
"test-app": "TEST_WATCH=1 meteor test --full-app --driver-package meteortesting:mocha",
"visualize": "meteor --production --extra-packages bundle-visualizer"
},
"dependencies": {
"@babel/runtime": "^7.23.5",
"@react-email/components": "0.5.3",
"@swc/helpers": "^0.5.17",
"grubba-rpc": "^0.12.8",
"meteor-node-stubs": "^1.2.12",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"thread-stream": "^3.1.0",
"zod": "^3.24.4"
},
"devDependencies": {
"playwright": "^1.54.2"
},
"meteor": {
"mainModule": {
"client": "client/main.jsx",
"server": "server/main.js"
},
"modern": true
}
}

View File

@@ -1,5 +1,4 @@
import { defineConfig } from '@meteorjs/rspack';
import { TsCheckerRspackPlugin } from 'ts-checker-rspack-plugin';
const { defineConfig } = require('@meteorjs/rspack');
/**
* Rspack configuration for Meteor projects.
@@ -11,8 +10,9 @@ import { TsCheckerRspackPlugin } from 'ts-checker-rspack-plugin';
*
* Use these flags to adjust your build settings based on environment.
*/
export default defineConfig(Meteor => {
module.exports = defineConfig(Meteor => {
return {
plugins: [new TsCheckerRspackPlugin()],
...Meteor.compileWithMeteor(["thread-stream"]),
...Meteor.compileWithRspack(["grubba-rpc"]),
};
});

View File

@@ -0,0 +1,64 @@
import { Meteor } from 'meteor/meteor';
import pino from 'pino';
import { createClient } from 'grubba-rpc';
import { LinksCollection } from '/imports/api/links';
import { TestEmail } from '/imports/emails/TestEmail';
console.log('-> TestEmail loaded', !!TestEmail);
const logger = pino({
transport: {
target: "pino-pretty",
options: {
colorize: true,
levelFirst: true,
translateTime: "UTC:yyyy-mm-dd HH:MM:ss.l o",
ignore: "pid,hostname"
}
},
browser: { asObject: true }
});
// Issue with thread-stream "Cannot find module '/_build/main-dev/lib/worker.js'"
// Ensure `Meteor.compileWithMeteor(["thread-stream"])` works
console.log("logger loaded", !!logger);
// Issue with npm deps that require compilation as not transpiled
// Ensure `Meteor.compileWithRspack(["grubba-rpc"])` works
console.log("grubba-rpc's createClient", !!createClient);
async function insertLink({ title, url }) {
await LinksCollection.insertAsync({ title, url, createdAt: new Date() });
}
Meteor.startup(async () => {
// If the Links collection is empty, add some data.
if (await LinksCollection.find().countAsync() === 0) {
await insertLink({
title: 'Do the Tutorial',
url: 'https://react-tutorial.meteor.com/simple-todos/01-creating-app.html',
});
await insertLink({
title: 'Follow the Guide',
url: 'https://guide.meteor.com',
});
await insertLink({
title: 'Read the Docs',
url: 'https://docs.meteor.com',
});
await insertLink({
title: 'Discussions',
url: 'https://forums.meteor.com',
});
}
// We publish the entire Links collection to all clients.
// In order to be fetched in real-time to the clients
Meteor.publish("links", function () {
return LinksCollection.find();
});
});

View File

@@ -0,0 +1 @@
throw new Error("This file should be ignored");

View File

@@ -0,0 +1 @@
throw new Error("This file should be ignored");

View File

@@ -0,0 +1,25 @@
import assert from "assert";
describe("monorepo", function () {
it("package.json has correct name", async function () {
const { name } = await import("../package.json");
assert.strictEqual(name, "app");
});
if (Meteor.isClient) {
it("client is not server", function () {
assert.strictEqual(Meteor.isServer, false);
});
}
if (Meteor.isServer) {
it("server is not client", function () {
assert.strictEqual(Meteor.isClient, false);
});
}
it("is test", function () {
assert.strictEqual(Meteor.isTest, true);
assert.strictEqual(Meteor.isAppTest, false);
});
});

View File

@@ -0,0 +1,10 @@
{
"name": "monorepo",
"private": true,
"workspaces": [
"app"
],
"engines": {
"node": ">=22.0.0"
}
}

View File

@@ -9,7 +9,7 @@ import "./Global.less";
// Dynamically import components
const Home = lazy(() =>
import("./Home.jsx").then((module) => ({ default: module.Home }))
import(/* webpackPrefetch: true */ "./Home.jsx").then((module) => ({ default: module.Home }))
);
const NotFound = lazy(() =>
import("./NotFound.jsx").then((module) => ({ default: module.NotFound }))

View File

@@ -11,6 +11,7 @@
"@babel/runtime": "^7.23.5",
"@modelcontextprotocol/sdk": "^1.17.3",
"@swc/helpers": "^0.5.17",
"bcrypt": "^6.0.0",
"meteor-node-stubs": "^1.2.12",
"react": "^18.3.1",
"react-compiler-runtime": "^19.1.0-rc.2",
@@ -26,13 +27,15 @@
"babel-loader": "^9.1.3",
"less": "^4.4.0",
"less-loader": "^12.3.0",
"playwright": "^1.54.2"
"playwright": "^1.54.2",
"typescript": "^5.9.2"
},
"meteor": {
"mainModule": {
"client": "client/main.jsx",
"server": "server/main.js"
},
"modules": ["styles/module.css"],
"modern": true
}
}

View File

@@ -1,4 +1,4 @@
import { defineConfig } from '@meteorjs/rspack';
const { defineConfig } = require('@meteorjs/rspack');
/**
* Rspack configuration for Meteor projects.
@@ -10,13 +10,14 @@ import { defineConfig } from '@meteorjs/rspack';
*
* Use these flags to adjust your build settings based on environment.
*/
export default defineConfig(Meteor => {
module.exports = defineConfig(Meteor => {
return {
resolve: {
alias: {
'@helper/alias': '/imports/helpers/alias.js',
'@react/alias': '/node_modules/react',
},
extensions: ['.jsx'],
},
module: {
rules: [
@@ -42,6 +43,22 @@ export default defineConfig(Meteor => {
},
],
},
...(Meteor.isClient && {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
reactRouter: {
test: /[\\/]node_modules[\\/](react-router|react-router-dom)[\\/]/,
name: 'react-router',
priority: 40,
enforce: true,
},
vendor: { test: /node_modules/, name: 'vendors' },
},
},
},
}),
plugins: [
Meteor.HtmlRspackPlugin({
title: 'react-router',

View File

@@ -4,8 +4,13 @@ import { LinksCollection } from '/imports/api/links';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import '@helper/alias';
import ReactAlias from '@react/alias';
import './resolve-extensions/first';
import { TypescriptEnabled } from './ts/helpers';
import bcrypt from "bcrypt";
console.log('@react/alias loaded', ReactAlias.version);
console.log('TypescriptEnabled', TypescriptEnabled);
console.log("bcrypt loaded", !!bcrypt);
async function insertLink({ title, url }) {
await LinksCollection.insertAsync({ title, url, createdAt: new Date() });

View File

@@ -0,0 +1 @@
console.log('first.jsx loaded');

View File

@@ -0,0 +1 @@
console.log('first.tsx loaded');

View File

@@ -0,0 +1 @@
export const TypescriptEnabled: boolean = true;

View File

@@ -0,0 +1,3 @@
body {
align-content: center;
}

View File

@@ -54,14 +54,14 @@ reactive-var@1.0.13
reload@1.3.2
retry@1.1.1
routepolicy@1.1.2
rspack@1.0.0-beta340.0
rspack@1.0.0-beta340.11
shell-server@0.6.1
socket-stream-client@0.6.1
standard-minifier-css@1.9.3
standard-minifier-js@3.1.1
static-html@1.4.0
static-html-tools@1.0.0
tools-core@1.0.0-beta340.0
tools-core@1.0.0-beta340.11
tracker@1.3.4
typescript@5.6.5
webapp@2.0.7

View File

@@ -1,4 +1,4 @@
import { defineConfig } from '@meteorjs/rspack';
const { defineConfig } = require('@meteorjs/rspack');
/**
* Rspack configuration for Meteor projects.
@@ -10,7 +10,7 @@ import { defineConfig } from '@meteorjs/rspack';
*
* Use these flags to adjust your build settings based on environment.
*/
export default defineConfig(Meteor => {
module.exports = defineConfig(Meteor => {
return {
...Meteor.isClient && {
module: {

View File

@@ -53,14 +53,14 @@ react-fast-refresh@0.2.9
reload@1.3.2
retry@1.1.1
routepolicy@1.1.2
rspack@1.0.0-beta340.0
rspack@1.0.0-beta340.11
shell-server@0.6.1
socket-stream-client@0.6.1
standard-minifier-css@1.9.3
standard-minifier-js@3.1.1
static-html@1.4.0
static-html-tools@1.0.0
tools-core@1.0.0-beta340.0
tools-core@1.0.0-beta340.11
tracker@1.3.4
typescript@5.6.5
webapp@2.0.7

View File

@@ -1,9 +1,6 @@
import { defineConfig } from '@meteorjs/rspack';
import path from 'path';
import sveltePreprocess from 'svelte-preprocess';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const { defineConfig } = require('@meteorjs/rspack');
const path = require('path');
const sveltePreprocess = require('svelte-preprocess');
/**
* Rspack configuration for Meteor projects.
@@ -15,7 +12,7 @@ const require = createRequire(import.meta.url);
*
* Use these flags to adjust your build settings based on environment.
*/
export default defineConfig(Meteor => {
module.exports = defineConfig(Meteor => {
return {
...Meteor.isClient && {
resolve: {

View File

@@ -17,7 +17,7 @@ typescript # Enable TypeScript syntax in .ts and .tsx modules
shell-server # Server-side component of the `meteor shell` command
hot-module-replacement # Update client in development without reloading the page
fourseven:scss@5.0.0-rc.1 # Enable SCSS syntax in .scss using Meteor
static-html # Define static page content in .html files
react-meteor-data # React higher-order component for reactively tracking Meteor data
zodern:types # Pull in type declarations from other Meteor packages

View File

@@ -0,0 +1,4 @@
body {
padding: 10px;
font-family: sans-serif;
}

Some files were not shown because too many files have changed in this diff Show More