mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'release-3.4.1' into typescript-tailwind-skeleton
This commit is contained in:
@@ -40,6 +40,9 @@ Core React integration with custom Meteor local directory.
|
||||
| React + JSX environment detection | Run, Prod, Test, Build |
|
||||
| Image assets load (generated + public + background) | Run, Prod |
|
||||
| `Meteor.disablePlugins` suppresses rspack plugins | Run, Prod, Test, Build |
|
||||
| Unplugin transform hook fires on first run (fresh cache) | Init |
|
||||
| Unplugin factory created on cached run — #14031 regression | Run |
|
||||
| Unplugin transform + buildDependencies tracking in production | Prod |
|
||||
| Custom rspack config (`rspack.config.cjs`) | All |
|
||||
| HMR works in dev, disabled in prod | Run, Prod |
|
||||
|
||||
@@ -227,6 +230,12 @@ Several apps import specific npm packages to verify that Meteor + Rspack handles
|
||||
| `node:buffer` | `imports/api/links.js` | Node.js built-in via `node:` protocol in shared client/server code — must be ignored on client without errors |
|
||||
| `@react-email/components` | `imports/emails/TestEmail.jsx` | JSX-heavy ESM package with many subpath exports |
|
||||
|
||||
### react (`apps/react/plugins/demo-unplugin.js`)
|
||||
|
||||
| Package | Reason |
|
||||
|---------|--------|
|
||||
| `unplugin` | Unplugin transform hook integration — validates rspack cache tracks plugin dependency files (#14031) |
|
||||
|
||||
### babel (`apps/babel/server/apollo.js`)
|
||||
|
||||
| Package | Reason |
|
||||
@@ -269,6 +278,7 @@ Where each feature is tested across apps and skeletons.
|
||||
| Babel compiler plugin | react-router | |
|
||||
| TypeScript type checking | typescript | |
|
||||
| Meteor.disablePlugins | react | |
|
||||
| Unplugin transform with cache (#14031) | react | |
|
||||
| Custom package dirs | react-router | |
|
||||
| CoffeeScript compilation | coffeescript | coffeescript |
|
||||
| Server-only (no client) | server-only | |
|
||||
|
||||
184
npm-packages/meteor-rspack/lib/localDependenciesHelpers.js
Normal file
184
npm-packages/meteor-rspack/lib/localDependenciesHelpers.js
Normal file
@@ -0,0 +1,184 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* Extract local file dependencies from a config file by parsing require/import statements using AST
|
||||
* @param {string} configFilePath - Path to the config file to parse
|
||||
* @returns {string[]} - Array of absolute paths to local dependencies
|
||||
*/
|
||||
function extractLocalDependencies(configFilePath) {
|
||||
if (!configFilePath || !fs.existsSync(configFilePath)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const swc = require('@swc/core');
|
||||
const content = fs.readFileSync(configFilePath, 'utf-8');
|
||||
const configDir = path.dirname(configFilePath);
|
||||
const projectDir = process.cwd();
|
||||
const dependencies = [];
|
||||
|
||||
// Parse the file into an AST
|
||||
const ast = swc.parseSync(content, {
|
||||
syntax: 'ecmascript',
|
||||
dynamicImport: true,
|
||||
target: 'es2020',
|
||||
});
|
||||
|
||||
// Visit all nodes to find import/require statements
|
||||
visitNode(ast, (node) => {
|
||||
let modulePath = null;
|
||||
|
||||
// Handle require() calls: require('./plugin')
|
||||
if (node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
node.callee.value === 'require' &&
|
||||
node.arguments.length > 0) {
|
||||
const arg = node.arguments[0];
|
||||
if (arg.expression?.type === 'StringLiteral') {
|
||||
modulePath = arg.expression.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle dynamic import() calls: import('./plugin')
|
||||
if (node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Import' &&
|
||||
node.arguments.length > 0) {
|
||||
const arg = node.arguments[0];
|
||||
if (arg.expression?.type === 'StringLiteral') {
|
||||
modulePath = arg.expression.value;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle static imports: import x from './plugin'
|
||||
if (node.type === 'ImportDeclaration' && node.source?.type === 'StringLiteral') {
|
||||
modulePath = node.source.value;
|
||||
}
|
||||
|
||||
// Handle export re-exports: export * from './plugin'
|
||||
if (node.type === 'ExportAllDeclaration' && node.source?.type === 'StringLiteral') {
|
||||
modulePath = node.source.value;
|
||||
}
|
||||
|
||||
// Handle named export re-exports: export { x } from './plugin'
|
||||
if (node.type === 'ExportNamedDeclaration' && node.source?.type === 'StringLiteral') {
|
||||
modulePath = node.source.value;
|
||||
}
|
||||
|
||||
// If we found a module path, try to resolve it
|
||||
if (modulePath) {
|
||||
const resolvedPath = resolveLocalModule(modulePath, configDir, projectDir);
|
||||
if (resolvedPath) {
|
||||
dependencies.push(resolvedPath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Remove duplicates
|
||||
return [...new Set(dependencies)];
|
||||
} catch (error) {
|
||||
console.warn('[Rspack Cache] Failed to parse config dependencies:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively visit all nodes in an AST
|
||||
* @param {Object} node - AST node
|
||||
* @param {Function} callback - Function to call for each node
|
||||
*/
|
||||
function visitNode(node, callback) {
|
||||
if (!node || typeof node !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(node);
|
||||
|
||||
// Visit all properties of the node
|
||||
for (const key in node) {
|
||||
if (Object.prototype.hasOwnProperty.call(node, key)) {
|
||||
const value = node[key];
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(child => visitNode(child, callback));
|
||||
} else if (typeof value === 'object') {
|
||||
visitNode(value, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a module path to an absolute path if it's a local file
|
||||
* @param {string} modulePath - Module path from require/import statement
|
||||
* @param {string} configDir - Directory containing the config file
|
||||
* @param {string} projectDir - Project root directory
|
||||
* @returns {string|null} - Resolved absolute path or null
|
||||
*/
|
||||
function resolveLocalModule(modulePath, configDir, projectDir) {
|
||||
// Only process relative paths (starts with . or ..)
|
||||
if (!modulePath.startsWith('.')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
let resolvedPath = path.resolve(configDir, modulePath);
|
||||
const extensions = ['.js', '.mjs', '.cjs', '.ts', '.json'];
|
||||
|
||||
// If the path exists as-is, check if it's a directory needing index resolution
|
||||
if (fs.existsSync(resolvedPath)) {
|
||||
if (fs.statSync(resolvedPath).isDirectory()) {
|
||||
let found = false;
|
||||
for (const ext of extensions) {
|
||||
const indexPath = path.join(resolvedPath, `index${ext}`);
|
||||
if (fs.existsSync(indexPath)) {
|
||||
resolvedPath = indexPath;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Try common extensions if file doesn't exist as-is
|
||||
let found = false;
|
||||
|
||||
for (const ext of extensions) {
|
||||
const pathWithExt = resolvedPath + ext;
|
||||
if (fs.existsSync(pathWithExt)) {
|
||||
resolvedPath = pathWithExt;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If still not found, return null
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify file is within project (not node_modules)
|
||||
const resolvedReal = fs.realpathSync(resolvedPath);
|
||||
const projectReal = fs.realpathSync(projectDir);
|
||||
|
||||
const isWithinProject =
|
||||
resolvedReal === projectReal ||
|
||||
resolvedReal.startsWith(projectReal + path.sep);
|
||||
const hasNodeModulesSegment = resolvedReal.split(path.sep).includes('node_modules');
|
||||
|
||||
if (isWithinProject && !hasNodeModulesSegment) {
|
||||
return resolvedPath;
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently ignore resolution errors
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractLocalDependencies,
|
||||
resolveLocalModule,
|
||||
};
|
||||
265
npm-packages/meteor-rspack/package-lock.json
generated
265
npm-packages/meteor-rspack/package-lock.json
generated
@@ -16,7 +16,8 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rspack/cli": ">=1.3.0",
|
||||
"@rspack/core": ">=1.3.0"
|
||||
"@rspack/core": ">=1.3.0",
|
||||
"@swc/core": ">=1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discoveryjs/json-ext": {
|
||||
@@ -491,6 +492,268 @@
|
||||
"node": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.21.tgz",
|
||||
"integrity": "sha512-fkk7NJcBscrR3/F8jiqlMptRHP650NxqDnspBMrRe5d8xOoCy9MLL5kOBLFXjFLfMo3KQQHhk+/jUULOMlR1uQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3",
|
||||
"@swc/types": "^0.1.25"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.15.21",
|
||||
"@swc/core-darwin-x64": "1.15.21",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.15.21",
|
||||
"@swc/core-linux-arm64-gnu": "1.15.21",
|
||||
"@swc/core-linux-arm64-musl": "1.15.21",
|
||||
"@swc/core-linux-ppc64-gnu": "1.15.21",
|
||||
"@swc/core-linux-s390x-gnu": "1.15.21",
|
||||
"@swc/core-linux-x64-gnu": "1.15.21",
|
||||
"@swc/core-linux-x64-musl": "1.15.21",
|
||||
"@swc/core-win32-arm64-msvc": "1.15.21",
|
||||
"@swc/core-win32-ia32-msvc": "1.15.21",
|
||||
"@swc/core-win32-x64-msvc": "1.15.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": ">=0.5.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/helpers": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.21.tgz",
|
||||
"integrity": "sha512-SA8SFg9dp0qKRH8goWsax6bptFE2EdmPf2YRAQW9WoHGf3XKM1bX0nd5UdwxmC5hXsBUZAYf7xSciCler6/oyA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.21.tgz",
|
||||
"integrity": "sha512-//fOVntgowz9+V90lVsNCtyyrtbHp3jWH6Rch7MXHXbcvbLmbCTmssl5DeedUWLLGiAAW1wksBdqdGYOTjaNLw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.21.tgz",
|
||||
"integrity": "sha512-meNI4Sh6h9h8DvIfEc0l5URabYMSuNvyisLmG6vnoYAS43s8ON3NJR8sDHvdP7NJTrLe0q/x2XCn6yL/BeHcZg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.21.tgz",
|
||||
"integrity": "sha512-QrXlNQnHeXqU2EzLlnsPoWEh8/GtNJLvfMiPsDhk+ht6Xv8+vhvZ5YZ/BokNWSIZiWPKLAqR0M7T92YF5tmD3g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.21.tgz",
|
||||
"integrity": "sha512-8/yGCMO333ultDaMQivE5CjO6oXDPeeg1IV4sphojPkb0Pv0i6zvcRIkgp60xDB+UxLr6VgHgt+BBgqS959E9g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-ppc64-gnu": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.21.tgz",
|
||||
"integrity": "sha512-ucW0HzPx0s1dgRvcvuLSPSA/2Kk/VYTv9st8qe1Kc22Gu0Q0rH9+6TcBTmMuNIp0Xs4BPr1uBttmbO1wEGI49Q==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-s390x-gnu": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.21.tgz",
|
||||
"integrity": "sha512-ulTnOGc5I7YRObE/9NreAhQg94QkiR5qNhhcUZ1iFAYjzg/JGAi1ch+s/Ixe61pMIr8bfVrF0NOaB0f8wjaAfA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.21.tgz",
|
||||
"integrity": "sha512-D0RokxtM+cPvSqJIKR6uja4hbD+scI9ezo95mBhfSyLUs9wnPPl26sLp1ZPR/EXRdYm3F3S6RUtVi+8QXhT24Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.21.tgz",
|
||||
"integrity": "sha512-nER8u7VeRfmU6fMDzl1NQAbbB/G7O2avmvCOwIul1uGkZ2/acbPH+DCL9h5+0yd/coNcxMBTL6NGepIew+7C2w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.21.tgz",
|
||||
"integrity": "sha512-+/AgNBnjYugUA8C0Do4YzymgvnGbztv7j8HKSQLvR/DQgZPoXQ2B3PqB2mTtGh/X5DhlJWiqnunN35JUgWcAeQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.21.tgz",
|
||||
"integrity": "sha512-IkSZj8PX/N4HcaFhMQtzmkV8YSnuNoJ0E6OvMwFiOfejPhiKXvl7CdDsn1f4/emYEIDO3fpgZW9DTaCRMDxaDA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.21.tgz",
|
||||
"integrity": "sha512-zUyWso7OOENB6e1N1hNuNn8vbvLsTdKQ5WKLgt/JcBNfJhKy/6jmBmqI3GXk/MyvQKd5SLvP7A0F36p7TeDqvw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.26",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz",
|
||||
"integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rspack/cli": ">=1.3.0",
|
||||
"@rspack/core": ">=1.3.0"
|
||||
"@rspack/core": ">=1.3.0",
|
||||
"@swc/core": ">=1.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ const {
|
||||
} = require('./lib/meteorRspackHelpers.js');
|
||||
const { loadUserAndOverrideConfig } = require('./lib/meteorRspackConfigHelpers.js');
|
||||
const { prepareMeteorRspackConfig } = require("./lib/meteorRspackConfigFactory");
|
||||
const { extractLocalDependencies } = require('./lib/localDependenciesHelpers.js');
|
||||
|
||||
|
||||
// Safe require that doesn't throw if the module isn't found
|
||||
@@ -70,10 +71,16 @@ function createCacheStrategy(
|
||||
const yarnLockPath = path.join(process.cwd(), 'yarn.lock');
|
||||
const hasYarnLock = fs.existsSync(yarnLockPath);
|
||||
|
||||
// Extract local dependencies from project config (e.g., plugin files)
|
||||
const localDependencies = projectConfigPath
|
||||
? extractLocalDependencies(projectConfigPath)
|
||||
: [];
|
||||
|
||||
// Build dependencies array
|
||||
const buildDependencies = [
|
||||
...(projectConfigPath ? [projectConfigPath] : []),
|
||||
...(configPath ? [configPath] : []),
|
||||
...localDependencies,
|
||||
...(hasTsconfig ? [tsconfigPath] : []),
|
||||
...(hasBabelRcConfig ? [babelRcConfig] : []),
|
||||
...(hasBabelJsConfig ? [babelJsConfig] : []),
|
||||
|
||||
@@ -1680,13 +1680,13 @@ const defaultResumeLoginHandler = async (accounts, options) => {
|
||||
// {hashedToken, when} for a hashed token or {token, when} for an
|
||||
// unhashed token.
|
||||
let oldUnhashedStyleToken;
|
||||
let token = await user.services.resume.loginTokens.find(token =>
|
||||
let token = user.services.resume.loginTokens.find(token =>
|
||||
token.hashedToken === hashedToken
|
||||
);
|
||||
if (token) {
|
||||
oldUnhashedStyleToken = false;
|
||||
} else {
|
||||
token = await user.services.resume.loginTokens.find(token =>
|
||||
token = user.services.resume.loginTokens.find(token =>
|
||||
token.token === options.resume
|
||||
);
|
||||
oldUnhashedStyleToken = true;
|
||||
|
||||
@@ -1138,6 +1138,56 @@ if (Meteor.isClient) (() => {
|
||||
})();
|
||||
|
||||
|
||||
if (Meteor.isServer) {
|
||||
Tinytest.add(
|
||||
'passwords - passwordValidator accepts passwords within default maxLength',
|
||||
test => {
|
||||
// A password of 256 chars (default max) should be accepted
|
||||
const validPassword = 'a'.repeat(256);
|
||||
test.isTrue(
|
||||
Match.test(validPassword, Match.OneOf(
|
||||
Match.Where(str => Match.test(str, String) && str.length <= (Meteor.settings?.packages?.accounts?.passwordMaxLength || 256)),
|
||||
{ digest: Match.Where(str => Match.test(str, String) && str.length === 64), algorithm: Match.OneOf('sha-256') }
|
||||
)),
|
||||
'Password of exactly 256 chars should be accepted'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
'passwords - passwordValidator rejects passwords exceeding default maxLength',
|
||||
test => {
|
||||
// A password of 257 chars should be rejected
|
||||
const longPassword = 'a'.repeat(257);
|
||||
test.isFalse(
|
||||
Match.test(longPassword, Match.OneOf(
|
||||
Match.Where(str => Match.test(str, String) && str.length <= (Meteor.settings?.packages?.accounts?.passwordMaxLength || 256)),
|
||||
{ digest: Match.Where(str => Match.test(str, String) && str.length === 64), algorithm: Match.OneOf('sha-256') }
|
||||
)),
|
||||
'Password exceeding 256 chars should be rejected'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.add(
|
||||
'passwords - passwordValidator operator precedence is correct for maxLength fallback',
|
||||
test => {
|
||||
// This test verifies the fix: without proper parentheses around the || operator,
|
||||
// `str.length <= Meteor.settings?.packages?.accounts?.passwordMaxLength || 256`
|
||||
// would evaluate as `(str.length <= undefined) || 256` which is always truthy (256),
|
||||
// allowing passwords of any length.
|
||||
const veryLongPassword = 'a'.repeat(1000);
|
||||
test.isFalse(
|
||||
Match.test(veryLongPassword, Match.OneOf(
|
||||
Match.Where(str => Match.test(str, String) && str.length <= (Meteor.settings?.packages?.accounts?.passwordMaxLength || 256)),
|
||||
{ digest: Match.Where(str => Match.test(str, String) && str.length === 64), algorithm: Match.OneOf('sha-256') }
|
||||
)),
|
||||
'Very long password (1000 chars) should be rejected when no custom maxLength is configured'
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (Meteor.isServer) (() => {
|
||||
|
||||
Tinytest.add('passwords - setup more than one onCreateUserHook', test => {
|
||||
|
||||
@@ -1050,23 +1050,33 @@ Object.assign(Subscription.prototype, {
|
||||
// removed messages for the published objects; if that is necessary, call
|
||||
// _removeAllDocuments first.
|
||||
_deactivate: function() {
|
||||
var self = this;
|
||||
if (self._deactivated)
|
||||
if (this._deactivated)
|
||||
return;
|
||||
self._deactivated = true;
|
||||
self._callStopCallbacks();
|
||||
this._deactivated = true;
|
||||
this._callStopCallbacks().then(() => {
|
||||
// Break reference chains to allow GC of the Session and its data.
|
||||
// Without this, deactivated subscriptions retain live references
|
||||
// to the (now-closed) session indefinitely.
|
||||
this._session = null;
|
||||
this._documents = new Map();
|
||||
});
|
||||
Package['facts-base'] && Package['facts-base'].Facts.incrementServerFact(
|
||||
"livedata", "subscriptions", -1);
|
||||
},
|
||||
|
||||
_callStopCallbacks: function () {
|
||||
var self = this;
|
||||
// Tell listeners, so they can clean up
|
||||
var callbacks = self._stopCallbacks;
|
||||
self._stopCallbacks = [];
|
||||
callbacks.forEach(function (callback) {
|
||||
callback();
|
||||
});
|
||||
_callStopCallbacks: async function () {
|
||||
// In Meteor 3, onStop callbacks can be async (e.g. observeHandle.stop()
|
||||
// returns a Promise). We must await each one so that observer teardown
|
||||
// completes before the subscription is considered fully deactivated.
|
||||
const callbacks = this._stopCallbacks;
|
||||
this._stopCallbacks = [];
|
||||
for (const callback of callbacks) {
|
||||
try {
|
||||
await callback();
|
||||
} catch (e) {
|
||||
Meteor._debug("Exception in onStop callback:", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Send remove messages for every document.
|
||||
@@ -1145,8 +1155,7 @@ Object.assign(Subscription.prototype, {
|
||||
// destroyed but the deferred call to _deactivateAllSubscriptions hasn't
|
||||
// happened yet.
|
||||
_isDeactivated: function () {
|
||||
var self = this;
|
||||
return self._deactivated || self._session.inQueue === null;
|
||||
return this._deactivated || !this._session || this._session.inQueue === null;
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -593,4 +593,79 @@ function getTestConnections(test) {
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Async onStop cleanup tests (memory leak fix)
|
||||
// ============================================================================
|
||||
|
||||
const asyncCleanupTracker = {};
|
||||
|
||||
Meteor.publish('test_async_onstop_cleanup', function (trackerId) {
|
||||
this.onStop(async function () {
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
asyncCleanupTracker[trackerId] = true;
|
||||
});
|
||||
this.ready();
|
||||
});
|
||||
|
||||
Tinytest.addAsync(
|
||||
'livedata server - async onStop callbacks complete on unsubscribe',
|
||||
async function (test) {
|
||||
const trackerId = Random.id();
|
||||
asyncCleanupTracker[trackerId] = false;
|
||||
|
||||
const { clientConn } = await getTestConnections(test);
|
||||
const sub = clientConn.subscribe('test_async_onstop_cleanup', trackerId);
|
||||
|
||||
await waitUntil(
|
||||
() => sub.ready(),
|
||||
{ description: 'subscription is ready' }
|
||||
);
|
||||
|
||||
sub.stop();
|
||||
|
||||
await waitUntil(
|
||||
() => asyncCleanupTracker[trackerId] === true,
|
||||
{ description: 'async onStop callback completed after unsubscribe' }
|
||||
);
|
||||
|
||||
test.isTrue(
|
||||
asyncCleanupTracker[trackerId],
|
||||
'Async onStop callback should have completed'
|
||||
);
|
||||
|
||||
clientConn.disconnect();
|
||||
delete asyncCleanupTracker[trackerId];
|
||||
}
|
||||
);
|
||||
|
||||
Tinytest.addAsync(
|
||||
'livedata server - async onStop callbacks complete on disconnect',
|
||||
async function (test) {
|
||||
const trackerId = Random.id();
|
||||
asyncCleanupTracker[trackerId] = false;
|
||||
|
||||
const { clientConn } = await getTestConnections(test);
|
||||
clientConn.subscribe('test_async_onstop_cleanup', trackerId);
|
||||
|
||||
await waitUntil(
|
||||
() => clientConn.status().connected,
|
||||
{ description: 'client is connected' }
|
||||
);
|
||||
|
||||
clientConn.disconnect();
|
||||
|
||||
await waitUntil(
|
||||
() => asyncCleanupTracker[trackerId] === true,
|
||||
{ description: 'async onStop callback completed after disconnect' }
|
||||
);
|
||||
|
||||
test.isTrue(
|
||||
asyncCleanupTracker[trackerId],
|
||||
'Async onStop callback should have completed on disconnect'
|
||||
);
|
||||
|
||||
delete asyncCleanupTracker[trackerId];
|
||||
}
|
||||
);
|
||||
@@ -11,6 +11,7 @@
|
||||
"@babel/runtime": "^7.23.5",
|
||||
"@swc/helpers": "^0.5.17",
|
||||
"meteor-node-stubs": "^1.2.12",
|
||||
"unplugin": "^2.3.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
|
||||
30
tools/e2e-tests/apps/react/plugins/demo-unplugin.js
Normal file
30
tools/e2e-tests/apps/react/plugins/demo-unplugin.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const { createUnplugin } = require('unplugin');
|
||||
|
||||
const demoUnplugin = createUnplugin(() => {
|
||||
console.log('[demo-unplugin][factory-created]');
|
||||
return {
|
||||
name: 'demo-unplugin',
|
||||
transformInclude(id) {
|
||||
// Only process app source files, skip node_modules and .meteor
|
||||
if (id.includes('node_modules') || id.includes('.meteor')) {
|
||||
return false;
|
||||
}
|
||||
const ok =
|
||||
id.endsWith('.tsx') ||
|
||||
id.endsWith('.ts') ||
|
||||
id.endsWith('.jsx') ||
|
||||
id.endsWith('.js');
|
||||
|
||||
if (ok) {
|
||||
console.log('[demo-unplugin][transformInclude]', id, '=> true');
|
||||
}
|
||||
return ok;
|
||||
},
|
||||
transform(code, id) {
|
||||
console.log('[demo-unplugin][transform-enter]', id);
|
||||
return { code, map: null };
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
module.exports = { demoRspackPlugin: demoUnplugin.rspack };
|
||||
@@ -1,6 +1,7 @@
|
||||
const { defineConfig } = require('@meteorjs/rspack');
|
||||
const path = require('path');
|
||||
const CustomConsoleLogPlugin = require("./plugins/CustomConsoleLogPlugin");
|
||||
const { demoRspackPlugin } = require("./plugins/demo-unplugin");
|
||||
|
||||
/**
|
||||
* Rspack configuration for Meteor projects.
|
||||
@@ -39,6 +40,6 @@ module.exports = defineConfig(Meteor => {
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [new CustomConsoleLogPlugin()],
|
||||
plugins: [new CustomConsoleLogPlugin(), demoRspackPlugin()],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -78,6 +78,13 @@ describe('React App Bundling /', () => {
|
||||
buildDir: "_build-local-custom",
|
||||
env: { METEOR_LOCAL_DIR: ".meteor/local-custom" },
|
||||
customAssertions: {
|
||||
afterInit: async ({ result }) => {
|
||||
// Verify unplugin transform hook is called on first run (fresh cache)
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
/.*\[demo-unplugin\]\[transform-enter\].*/
|
||||
);
|
||||
},
|
||||
afterRun: async ({ result, tempDir }) => {
|
||||
const appDir = tempDir; // testMeteorRspackBundler uses tempDir as appDir if not monorepo
|
||||
|
||||
@@ -95,11 +102,20 @@ describe('React App Bundling /', () => {
|
||||
await assertImagesExistAndLoad();
|
||||
|
||||
// Check custom plugin is disabled with Meteor.disablePlugins
|
||||
// Use specific log prefix to avoid matching the filename in buildDependencies
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
/.*CustomConsoleLogPlugin.*/,
|
||||
/.*\[CustomConsoleLogPlugin\].*/,
|
||||
{ negate: true }
|
||||
);
|
||||
|
||||
// Verify unplugin factory is still created on second run (with cache)
|
||||
// This confirms the plugin is loaded and active even when rspack uses
|
||||
// cached transform results (#14031 regression test)
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
/.*\[demo-unplugin\]\[factory-created\].*/
|
||||
);
|
||||
},
|
||||
afterRunRebuildClient: async ({ allConsoleLogs }) => {
|
||||
// Check for HMR output as enabled by default
|
||||
@@ -115,11 +131,24 @@ describe('React App Bundling /', () => {
|
||||
await assertImagesExistAndLoad();
|
||||
|
||||
// Check custom plugin is disabled with Meteor.disablePlugins
|
||||
// Use specific log prefix to avoid matching the filename in buildDependencies
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
/.*CustomConsoleLogPlugin.*/,
|
||||
/.*\[CustomConsoleLogPlugin\].*/,
|
||||
{ negate: true }
|
||||
);
|
||||
|
||||
// Verify demo-unplugin.js is tracked in rspack buildDependencies (#14031)
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
/.*plugins\/demo-unplugin\.js.*/
|
||||
);
|
||||
|
||||
// Verify unplugin transform hook fires in production (separate cache version)
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
/.*\[demo-unplugin\]\[transform-enter\].*/
|
||||
);
|
||||
},
|
||||
afterRunProductionRebuildClient: async ({ allConsoleLogs }) => {
|
||||
// Check for HMR to not be enabled in production-like mode
|
||||
@@ -133,9 +162,10 @@ describe('React App Bundling /', () => {
|
||||
await waitForReactEnvs(result.outputLines);
|
||||
|
||||
// Check custom plugin is disabled with Meteor.disablePlugins
|
||||
// Use specific log prefix to avoid matching the filename in buildDependencies
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
/.*CustomConsoleLogPlugin.*/,
|
||||
/.*\[CustomConsoleLogPlugin\].*/,
|
||||
{ negate: true }
|
||||
);
|
||||
},
|
||||
@@ -143,9 +173,10 @@ describe('React App Bundling /', () => {
|
||||
await waitForReactEnvs(result.outputLines);
|
||||
|
||||
// Check custom plugin is disabled with Meteor.disablePlugins
|
||||
// Use specific log prefix to avoid matching the filename in buildDependencies
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
/.*CustomConsoleLogPlugin.*/,
|
||||
/.*\[CustomConsoleLogPlugin\].*/,
|
||||
{ negate: true }
|
||||
);
|
||||
},
|
||||
@@ -153,9 +184,10 @@ describe('React App Bundling /', () => {
|
||||
await waitForReactEnvs(result.outputLines, { isJsxEnabled: true });
|
||||
|
||||
// Check custom plugin is disabled with Meteor.disablePlugins
|
||||
// Use specific log prefix to avoid matching the filename in buildDependencies
|
||||
await waitForMeteorOutput(
|
||||
result.outputLines,
|
||||
/.*CustomConsoleLogPlugin.*/,
|
||||
/.*\[CustomConsoleLogPlugin\].*/,
|
||||
{ negate: true }
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user