mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'release-3.4' into feature/http-proxy-3
# Conflicts: # package.json
This commit is contained in:
2
meteor
2
meteor
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
139
npm-packages/meteor-rspack/lib/ignore.js
Normal file
139
npm-packages/meteor-rspack/lib/ignore.js
Normal 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,
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
|
||||
99
npm-packages/meteor-rspack/lib/meteorRspackConfigFactory.js
Normal file
99
npm-packages/meteor-rspack/lib/meteorRspackConfigFactory.js
Normal 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,
|
||||
};
|
||||
106
npm-packages/meteor-rspack/lib/meteorRspackHelpers.js
Normal file
106
npm-packages/meteor-rspack/lib/meteorRspackHelpers.js
Normal 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,
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
|
||||
62
npm-packages/meteor-rspack/lib/test.js
Normal file
62
npm-packages/meteor-rspack/lib/test.js
Normal 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,
|
||||
};
|
||||
8
npm-packages/meteor-rspack/package-lock.json
generated
8
npm-packages/meteor-rspack/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "A user account system",
|
||||
version: "3.1.2",
|
||||
version: "3.2.0-beta340.11",
|
||||
});
|
||||
|
||||
Package.onUse((api) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "The Meteor command-line tool",
|
||||
version: "3.3.1",
|
||||
version: "3.4.0-beta.11",
|
||||
});
|
||||
|
||||
Package.includeTool();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "JavaScript minifier",
|
||||
version: '3.0.4',
|
||||
version: '3.1.0-beta340.11',
|
||||
});
|
||||
|
||||
Npm.depends({
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
|
||||
146
packages/mongo/collection/collection_extensions.js
Normal file
146
packages/mongo/collection/collection_extensions.js
Normal 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;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
209
packages/mongo/mongo.d.ts
vendored
209
packages/mongo/mongo.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
233
packages/mongo/tests/collection_extensions_tests.js
Normal file
233
packages/mongo/tests/collection_extensions_tests.js
Normal 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();
|
||||
});
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
|
||||
@@ -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
@@ -44,12 +44,15 @@
|
||||
<ul class="navbar-nav mr-auto">
|
||||
{{#each groupPaths}}
|
||||
<li class="nav-item"><span class="nav-link"> - </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...
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
87
packages/tools-core/lib/ignore.js
Normal file
87
packages/tools-core/lib/ignore.js
Normal 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;
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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: [
|
||||
|
||||
1
tools/modern-tests/apps/monorepo/.npmrc
Normal file
1
tools/modern-tests/apps/monorepo/.npmrc
Normal file
@@ -0,0 +1 @@
|
||||
install-strategy=nested
|
||||
22
tools/modern-tests/apps/monorepo/app/.meteor/packages
Normal file
22
tools/modern-tests/apps/monorepo/app/.meteor/packages
Normal 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
|
||||
2
tools/modern-tests/apps/monorepo/app/.meteor/platforms
Normal file
2
tools/modern-tests/apps/monorepo/app/.meteor/platforms
Normal file
@@ -0,0 +1,2 @@
|
||||
server
|
||||
browser
|
||||
1
tools/modern-tests/apps/monorepo/app/.meteor/release
Normal file
1
tools/modern-tests/apps/monorepo/app/.meteor/release
Normal file
@@ -0,0 +1 @@
|
||||
none
|
||||
1
tools/modern-tests/apps/monorepo/app/.meteorignore
Normal file
1
tools/modern-tests/apps/monorepo/app/.meteorignore
Normal file
@@ -0,0 +1 @@
|
||||
**/ignore*
|
||||
8
tools/modern-tests/apps/monorepo/app/.swcrc
Normal file
8
tools/modern-tests/apps/monorepo/app/.swcrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true
|
||||
}
|
||||
}
|
||||
}
|
||||
8
tools/modern-tests/apps/monorepo/app/client/main.html
Normal file
8
tools/modern-tests/apps/monorepo/app/client/main.html
Normal 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>
|
||||
10
tools/modern-tests/apps/monorepo/app/client/main.jsx
Normal file
10
tools/modern-tests/apps/monorepo/app/client/main.jsx
Normal 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 />);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
throw new Error("This file should be ignored");
|
||||
@@ -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');
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
11
tools/modern-tests/apps/monorepo/app/imports/ui/App.jsx
Normal file
11
tools/modern-tests/apps/monorepo/app/imports/ui/App.jsx
Normal 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>
|
||||
);
|
||||
16
tools/modern-tests/apps/monorepo/app/imports/ui/Hello.jsx
Normal file
16
tools/modern-tests/apps/monorepo/app/imports/ui/Hello.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
23
tools/modern-tests/apps/monorepo/app/imports/ui/Info.jsx
Normal file
23
tools/modern-tests/apps/monorepo/app/imports/ui/Info.jsx
Normal 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>
|
||||
);
|
||||
};
|
||||
33
tools/modern-tests/apps/monorepo/app/package.json
Normal file
33
tools/modern-tests/apps/monorepo/app/package.json
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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"]),
|
||||
};
|
||||
});
|
||||
64
tools/modern-tests/apps/monorepo/app/server/main.js
Normal file
64
tools/modern-tests/apps/monorepo/app/server/main.js
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
throw new Error("This file should be ignored");
|
||||
@@ -0,0 +1 @@
|
||||
throw new Error("This file should be ignored");
|
||||
25
tools/modern-tests/apps/monorepo/app/tests/main.test.js
Normal file
25
tools/modern-tests/apps/monorepo/app/tests/main.test.js
Normal 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);
|
||||
});
|
||||
});
|
||||
10
tools/modern-tests/apps/monorepo/package.json
Normal file
10
tools/modern-tests/apps/monorepo/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "monorepo",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"app"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
}
|
||||
}
|
||||
@@ -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 }))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
console.log('first.jsx loaded');
|
||||
@@ -0,0 +1 @@
|
||||
console.log('first.tsx loaded');
|
||||
@@ -0,0 +1 @@
|
||||
export const TypescriptEnabled: boolean = true;
|
||||
3
tools/modern-tests/apps/react-router/styles/module.css
Normal file
3
tools/modern-tests/apps/react-router/styles/module.css
Normal file
@@ -0,0 +1,3 @@
|
||||
body {
|
||||
align-content: center;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
4
tools/modern-tests/apps/typescript/client/main.scss
Normal file
4
tools/modern-tests/apps/typescript/client/main.scss
Normal 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
Reference in New Issue
Block a user