mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
Merge branch 'release-3.4.1' into devel
This commit is contained in:
30
.github/workflows/e2e-tests.yml
vendored
30
.github/workflows/e2e-tests.yml
vendored
@@ -33,6 +33,7 @@ jobs:
|
||||
- Babel
|
||||
- Blaze
|
||||
- Coffeescript
|
||||
- Examples
|
||||
- Monorepo
|
||||
- Other
|
||||
- React
|
||||
@@ -86,10 +87,25 @@ jobs:
|
||||
run: ./meteor --get-ready
|
||||
|
||||
- name: Run tests for ${{ matrix.category }}
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
max_attempts: 3
|
||||
retry_on: error
|
||||
timeout_minutes: 15
|
||||
retry_wait_seconds: 90
|
||||
command: npm run test:e2e -- -t="${{ matrix.category }}"
|
||||
id: test-run
|
||||
continue-on-error: true
|
||||
timeout-minutes: 15
|
||||
run: npm run test:e2e -- -t="${{ matrix.category }}"
|
||||
|
||||
- name: Retry failed tests for ${{ matrix.category }} (attempt 2)
|
||||
id: test-retry-1
|
||||
if: steps.test-run.outcome == 'failure'
|
||||
continue-on-error: true
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
echo "::warning::First attempt failed, retrying..."
|
||||
sleep 90
|
||||
npm run test:e2e -- -t="${{ matrix.category }}"
|
||||
|
||||
- name: Retry failed tests for ${{ matrix.category }} (attempt 3)
|
||||
if: steps.test-retry-1.outcome == 'failure'
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
echo "::warning::Second attempt failed, retrying..."
|
||||
sleep 90
|
||||
npm run test:e2e -- -t="${{ matrix.category }}"
|
||||
|
||||
@@ -53,6 +53,14 @@ Current Reviewers:
|
||||
- [@zodern](https://github.com/zodern)
|
||||
- [@radekmie](https://github.com/radekmie)
|
||||
|
||||
##### Testing a contributor's branch locally
|
||||
|
||||
To quickly check out a PR branch from a fork for local testing, see the [Testing a fork branch](DEVELOPMENT.md#testing-a-fork-branch) section in `DEVELOPMENT.md`, or run:
|
||||
|
||||
```sh
|
||||
npm run checkout:pr -- https://github.com/meteor/meteor/pull/<PR-number>
|
||||
```
|
||||
|
||||
#### Core Committer
|
||||
|
||||
The contributors with commit access to meteor/meteor are employees of Meteor Software LP or community members who have distinguished themselves in other contribution areas or members of partner companies. If you want to become a core committer, please start writing PRs.
|
||||
|
||||
@@ -64,6 +64,35 @@ can run Meteor directly from a Git checkout using these steps:
|
||||
>
|
||||
> Then you can use the chrome debugger inside `chrome://inspect`.
|
||||
|
||||
### Testing a fork branch
|
||||
|
||||
When reviewing a pull request or testing changes from a contributor's fork, use the `checkout-pr.js` script to set up a local branch automatically:
|
||||
|
||||
```sh
|
||||
# From a PR URL (requires gh CLI or falls back to GitHub API via curl)
|
||||
$ npm run checkout:pr -- https://github.com/meteor/meteor/pull/<PR-number>
|
||||
|
||||
# From a user:branch shorthand
|
||||
$ npm run checkout:pr -- <user>:<branch>
|
||||
|
||||
# From a full fork repo URL and branch name (HTTPS)
|
||||
$ npm run checkout:pr -- <fork-repo-url> <branch>
|
||||
|
||||
# From a full fork repo URL and branch name (SSH)
|
||||
$ npm run checkout:pr -- git@github.com:<user>/<repo>.git <branch>
|
||||
```
|
||||
|
||||
The script will:
|
||||
|
||||
1. Add the fork as a git remote (named after the fork owner) if not already present
|
||||
2. Fetch the target branch
|
||||
3. Create (or update) a local branch named `fork/<owner>/<branch>`
|
||||
4. Print instructions for switching back to your previous branch
|
||||
|
||||
For upstream PRs (branches on `meteor/meteor` itself), the script detects the existing `origin` remote and checks out the branch directly without the `fork/` prefix.
|
||||
|
||||
If you run the script again for the same fork branch, it will fetch the latest changes and update the local branch.
|
||||
|
||||
### Notes when running from a checkout
|
||||
|
||||
The following are some distinct differences you must pay attention to when running Meteor from a checkout:
|
||||
|
||||
@@ -57,7 +57,7 @@ Full-featured React Router app with custom packages, Less, and advanced rspack c
|
||||
| Compiler output cached in dev (babel.config.js) | Run |
|
||||
| 404 page routing (renders "Page Not Found") | Run, Prod |
|
||||
| Less stylesheet support (`white-space: break-spaces`) | Run, Prod |
|
||||
| Meteor modules config styles (`align-content: center`) | Run, Prod |
|
||||
| `meteor.modules` config styles (`align-content: center`) | Run, Prod |
|
||||
| Custom HTML meta tags (`theme-color`) | Run, Prod |
|
||||
| Default + custom package loading | Run |
|
||||
| `resolve.extensions` loading (`.jsx`) | Run |
|
||||
@@ -135,12 +135,15 @@ CoffeeScript language support.
|
||||
|
||||
### vue
|
||||
|
||||
Vue.js framework with Tailwind CSS.
|
||||
Vue.js framework with Tailwind CSS, CSS auto-delegation, and `meteor.modules` config.
|
||||
|
||||
| What is covered | Phase |
|
||||
|----------------|-------|
|
||||
| Vue single-file components | All |
|
||||
| Tailwind CSS styles (`.p-8` padding) | Run, Prod |
|
||||
| CSS auto-delegation (`client/main.css` processed by Rspack, not Meteor) | All |
|
||||
| `meteor.modules` config preserves `client/meteor.css` for Meteor processing | All |
|
||||
| Rspack CSS + Meteor CSS coexistence in same entry folder | All |
|
||||
| HMR works in dev, disabled in prod | Run, Prod |
|
||||
|
||||
### solid
|
||||
@@ -271,7 +274,7 @@ Where each feature is tested across apps and skeletons.
|
||||
| Static asset bundling | react-router, monorepo | |
|
||||
| Less styles | react-router | |
|
||||
| SCSS styles | typescript | |
|
||||
| Tailwind CSS | vue | tailwind |
|
||||
| Tailwind CSS | vue (PostCSS) | tailwind |
|
||||
| Image asset loading | react | |
|
||||
| 404 routing | react-router | |
|
||||
| Meta tags | react-router | |
|
||||
@@ -288,6 +291,8 @@ Where each feature is tested across apps and skeletons.
|
||||
| Custom NODE_ENV compilation | babel | |
|
||||
| Portable build (no isDev/isProd defines) | typescript | |
|
||||
| `Meteor.extendSwcConfig` (path aliases) | typescript | |
|
||||
| CSS auto-delegation (entry folder filtering) | vue | |
|
||||
| `meteor.modules` config (preserve files for Meteor) | react-router, vue | |
|
||||
| `meteor reset` cleanup | all apps | all skeletons |
|
||||
| Skeleton creation | | all 14 skeletons |
|
||||
| Body style assertions | | react, tailwind (custom); most others (default) |
|
||||
|
||||
@@ -14,6 +14,7 @@ When Meteor runs with the Rspack bundler enabled, this package is what generates
|
||||
- **Asset externals and HTML generation** through custom Rspack plugins
|
||||
- **A `defineConfig` helper** that accepts a factory function receiving Meteor environment flags and build utilities
|
||||
- **Customizable config** via `rspack.config.js` in your project root, with safe merging that warns if you try to override reserved settings
|
||||
- **Automatic CSS delegation** when rspack is configured with CSS, Less, or SCSS loaders, Meteor automatically detects the handled extensions after the first compilation and stops processing those files itself in the entry folder context. No `.meteorignore` entries needed.
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
20
npm-packages/meteor-rspack/package-lock.json
generated
20
npm-packages/meteor-rspack/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@meteorjs/rspack",
|
||||
"version": "1.1.0-beta.31",
|
||||
"version": "1.1.0-beta.33",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@meteorjs/rspack",
|
||||
"version": "1.1.0-beta.31",
|
||||
"version": "1.1.0-beta.33",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
@@ -14,6 +14,9 @@
|
||||
"node-polyfill-webpack-plugin": "^4.1.0",
|
||||
"webpack-merge": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"semver": "^7.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rspack/cli": ">=1.3.0",
|
||||
"@rspack/core": ">=1.3.0",
|
||||
@@ -4470,6 +4473,19 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
{
|
||||
"name": "@meteorjs/rspack",
|
||||
"version": "1.1.0-beta.31",
|
||||
"version": "1.1.0-beta.33",
|
||||
"description": "Configuration logic for using Rspack in Meteor projects",
|
||||
"main": "index.js",
|
||||
"type": "commonjs",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"scripts": {
|
||||
"bump": "node ./scripts/bump-version.js",
|
||||
"publish:beta": "bash ./scripts/publish-beta.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ignore-loader": "^0.1.2",
|
||||
"node-polyfill-webpack-plugin": "^4.1.0",
|
||||
"webpack-merge": "^6.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"semver": "^7.7.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rspack/cli": ">=1.3.0",
|
||||
"@rspack/core": ">=1.3.0",
|
||||
|
||||
@@ -6,6 +6,96 @@
|
||||
|
||||
const { outputMeteorRspack } = require('../lib/meteorRspackHelpers');
|
||||
|
||||
/**
|
||||
* Extracts file extensions that rspack is configured to handle
|
||||
* from the resolved module.rules test patterns.
|
||||
* @param {import('@rspack/core').Compiler} compiler
|
||||
* @returns {Set<string>} Set of extensions like .css, .less, .scss
|
||||
*/
|
||||
function extractConfiguredExtensions(compiler) {
|
||||
const delegatableExtensions = ['.css', '.less', '.scss', '.sass', '.styl'];
|
||||
const found = new Set();
|
||||
|
||||
function inspectRules(rules) {
|
||||
for (const rule of rules) {
|
||||
if (!rule) continue;
|
||||
if (rule.test) {
|
||||
const testStr = rule.test instanceof RegExp
|
||||
? rule.test.source
|
||||
: String(rule.test);
|
||||
for (const ext of delegatableExtensions) {
|
||||
const escaped = ext.replace('.', '\\.');
|
||||
if (testStr.includes(escaped)) {
|
||||
found.add(ext);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rule.oneOf) inspectRules(rule.oneOf);
|
||||
if (rule.rules) inspectRules(rule.rules);
|
||||
}
|
||||
}
|
||||
|
||||
inspectRules(compiler.options.module?.rules || []);
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts file extensions that rspack both has rules for AND actually compiled
|
||||
* from files within entry folder paths (e.g. client/, server/).
|
||||
* An extension is only delegated if Rspack compiled a file with that extension
|
||||
* from an entry folder. Files in non-entry folders (e.g. imports/) don't count,
|
||||
* since delegation only ignores entry folder files for Meteor.
|
||||
* @param {import('@rspack/core').Stats} stats
|
||||
* @param {import('@rspack/core').Compiler} compiler
|
||||
* @returns {string[]} Array of extensions like ['.css', '.less', '.scss']
|
||||
*/
|
||||
function extractDelegatedExtensions(stats, compiler) {
|
||||
const configured = extractConfiguredExtensions(compiler);
|
||||
if (configured.size === 0) return [];
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const appRoot = compiler.options.context || process.cwd();
|
||||
|
||||
// Read entry folders from package.json meteor.mainModule
|
||||
const entryFolders = new Set();
|
||||
try {
|
||||
const pkgPath = path.join(appRoot, 'package.json');
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
||||
const mainModule = pkg?.meteor?.mainModule || {};
|
||||
for (const entry of Object.values(mainModule)) {
|
||||
if (typeof entry === 'string') {
|
||||
const folder = entry.split('/')[0];
|
||||
if (folder) entryFolders.add(folder);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If we can't read package.json, fall back to config-only
|
||||
return Array.from(configured);
|
||||
}
|
||||
|
||||
if (entryFolders.size === 0) return Array.from(configured);
|
||||
|
||||
const found = new Set();
|
||||
|
||||
for (const module of stats.compilation.modules) {
|
||||
const resource = module.resource || module.userRequest;
|
||||
if (!resource) continue;
|
||||
|
||||
const relativePath = path.relative(appRoot, resource);
|
||||
const topFolder = relativePath.split(path.sep)[0];
|
||||
if (!entryFolders.has(topFolder)) continue;
|
||||
|
||||
const ext = path.extname(resource);
|
||||
if (configured.has(ext)) {
|
||||
found.add(ext);
|
||||
if (found.size === configured.size) break;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(found);
|
||||
}
|
||||
|
||||
class MeteorRspackOutputPlugin {
|
||||
constructor(options = {}) {
|
||||
this.pluginName = 'MeteorRspackOutputPlugin';
|
||||
@@ -26,6 +116,7 @@ class MeteorRspackOutputPlugin {
|
||||
...(this.getData(stats, {
|
||||
compilationCount: this.compilationCount,
|
||||
isRebuild: this.compilationCount > 1,
|
||||
compiler,
|
||||
}) || {}),
|
||||
};
|
||||
outputMeteorRspack(data);
|
||||
@@ -33,4 +124,4 @@ class MeteorRspackOutputPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MeteorRspackOutputPlugin };
|
||||
module.exports = { MeteorRspackOutputPlugin, extractDelegatedExtensions };
|
||||
|
||||
@@ -10,7 +10,7 @@ const { getMeteorAppSwcConfig } = require('./lib/swc.js');
|
||||
const HtmlRspackPlugin = require('./plugins/HtmlRspackPlugin.js');
|
||||
const { RequireExternalsPlugin } = require('./plugins/RequireExtenalsPlugin.js');
|
||||
const { AssetExternalsPlugin } = require('./plugins/AssetExternalsPlugin.js');
|
||||
const { MeteorRspackOutputPlugin } = require('./plugins/MeteorRspackOutputPlugin.js');
|
||||
const { MeteorRspackOutputPlugin, extractDelegatedExtensions } = require('./plugins/MeteorRspackOutputPlugin.js');
|
||||
const { generateEagerTestFile } = require("./lib/test.js");
|
||||
const { getMeteorIgnoreEntries, createIgnoreGlobConfig } = require("./lib/ignore");
|
||||
const {
|
||||
@@ -851,7 +851,7 @@ module.exports = async function (inMeteor = {}, argv = {}) {
|
||||
|
||||
// Add MeteorRspackOutputPlugin as the last plugin to output compilation info
|
||||
const meteorRspackOutputPlugin = new MeteorRspackOutputPlugin({
|
||||
getData: (stats, { isRebuild, compilationCount }) => ({
|
||||
getData: (stats, { isRebuild, compilationCount, compiler }) => ({
|
||||
name: config.name,
|
||||
mode: config.mode,
|
||||
hasErrors: stats.hasErrors(),
|
||||
@@ -860,6 +860,9 @@ module.exports = async function (inMeteor = {}, argv = {}) {
|
||||
statsOverrided,
|
||||
compilationCount,
|
||||
isRebuild,
|
||||
...(!isRebuild && compiler && {
|
||||
delegatedExtensions: extractDelegatedExtensions(stats, compiler),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
config.plugins = [meteorRspackOutputPlugin, ...(config.plugins || [])];
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"install:e2e": "cd tools/e2e-tests && npm install && npx playwright install --with-deps chromium chromium-headless-shell",
|
||||
"test:e2e": "cd tools/e2e-tests && npm test -- ",
|
||||
"create-app:e2e": "cd tools/e2e-tests && node scripts/create-app.js",
|
||||
"test:idle-bot": "node --test .github/scripts/__tests__/inactive-issues.test.js"
|
||||
"test:idle-bot": "node --test .github/scripts/__tests__/inactive-issues.test.js",
|
||||
"checkout:pr": "node scripts/checkout-pr.js"
|
||||
},
|
||||
"jshintConfig": {
|
||||
"esversion": 11
|
||||
|
||||
@@ -112,7 +112,7 @@ if (Meteor.isClient) {
|
||||
loginAsUser1((err) => {
|
||||
test.isUndefined(err, 'Unexpected error logging in as user1');
|
||||
test.equal(
|
||||
Object.keys(DDP._reconnectHook.callbacks).length,
|
||||
DDP._reconnectHook.size(),
|
||||
1,
|
||||
'Only one onReconnect callback should be registered'
|
||||
);
|
||||
@@ -122,7 +122,7 @@ if (Meteor.isClient) {
|
||||
setTimeout(() => {
|
||||
test.isTrue(Meteor.status().connected);
|
||||
test.equal(
|
||||
Object.keys(DDP._reconnectHook.callbacks).length,
|
||||
DDP._reconnectHook.size(),
|
||||
1,
|
||||
'Only one onReconnect callback should be registered'
|
||||
);
|
||||
|
||||
@@ -36,34 +36,84 @@
|
||||
// callback will propagate up to the iterator function, and will
|
||||
// terminate calling the remaining callbacks if not caught.
|
||||
|
||||
const hasOwn = Object.prototype.hasOwnProperty;
|
||||
|
||||
export class Hook {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
this.nextCallbackId = 0;
|
||||
this.callbacks = Object.create(null);
|
||||
// Whether to wrap callbacks with Meteor.bindEnvironment
|
||||
this.bindEnvironment = true;
|
||||
if (options.bindEnvironment === false) {
|
||||
this.bindEnvironment = false;
|
||||
}
|
||||
/**
|
||||
* Creates a new Hook instance.
|
||||
* @param {object} [options={}] - Configuration options for the hook.
|
||||
* @param {boolean} [options.bindEnvironment=true] - Whether to automatically wrap registered callbacks with `Meteor.bindEnvironment`.
|
||||
* If `true`, callbacks will run in the Meteor environment of the code that registered them.
|
||||
* @param {boolean} [options.wrapAsync=true] - Whether to automatically wrap registered callbacks with `Meteor.wrapFn`.
|
||||
* If `true`, callbacks will be prepared to run asynchronously.
|
||||
* @param {Function} [options.exceptionHandler] - A custom function to handle exceptions thrown by registered callbacks.
|
||||
* This function will be called with the exception as its argument.
|
||||
* If provided, `options.debugPrintExceptions` will be ignored.
|
||||
* @param {string} [options.debugPrintExceptions] - If an `exceptionHandler` is not provided, and this option is a string,
|
||||
* exceptions thrown by callbacks will be logged to `Meteor._debug` with this string as a description.
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
this.callbacks = new Set();
|
||||
|
||||
this.wrapAsync = true;
|
||||
if (options.wrapAsync === false) {
|
||||
this.wrapAsync = false;
|
||||
}
|
||||
// Whether to wrap callbacks with Meteor.bindEnvironment
|
||||
const { bindEnvironment = true, wrapAsync = true } = options;
|
||||
this.bindEnvironment = !!bindEnvironment;
|
||||
this.wrapAsync = !!wrapAsync;
|
||||
|
||||
if (options.exceptionHandler) {
|
||||
this.exceptionHandler = options.exceptionHandler;
|
||||
} else if (options.debugPrintExceptions) {
|
||||
if (typeof options.debugPrintExceptions !== "string") {
|
||||
throw new Error("Hook option debugPrintExceptions should be a string");
|
||||
if (options.exceptionHandler) {
|
||||
this.exceptionHandler = options.exceptionHandler;
|
||||
} else if (options.debugPrintExceptions) {
|
||||
if (typeof options.debugPrintExceptions !== "string") {
|
||||
throw new Error("Hook option debugPrintExceptions should be a string");
|
||||
}
|
||||
this.exceptionHandler = options.debugPrintExceptions;
|
||||
}
|
||||
this.exceptionHandler = options.debugPrintExceptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all registered callbacks from this Hook instance.
|
||||
* After calling this method, the hook will have no callbacks registered.
|
||||
*/
|
||||
clear() {
|
||||
this.callbacks.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of callbacks currently registered with this Hook instance.
|
||||
* @returns {number} The number of registered callbacks.
|
||||
*/
|
||||
size() {
|
||||
return this.callbacks.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all registered callbacks as a new Array.
|
||||
* This provides a snapshot of the current callbacks.
|
||||
* @returns {Array<Function>} An array containing all registered callback functions.
|
||||
*/
|
||||
asArray() {
|
||||
return Array.from(this.callbacks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the current set of registered callbacks with a new set derived from the given array.
|
||||
*
|
||||
* @param {Array<Function>} arr An array of callback functions to register with this hook.
|
||||
* @throws {Error} If the provided argument `arr` is not an array.
|
||||
*/
|
||||
fromArray(arr) {
|
||||
if (!Array.isArray(arr)) {
|
||||
throw new Error("Method fromArray expects an array");
|
||||
}
|
||||
this.callbacks = new Set(arr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new callback with this Hook instance.
|
||||
*
|
||||
* @param {Function} callback The function to register. This function will be called when the hook is iterated over.
|
||||
* @returns {{callback: Function, stop: Function}} An object containing:
|
||||
* - `callback`: The actual callback function that was added to the hook's internal set (after any wrapping).
|
||||
* - `stop`: A function that, when called, unregisters this specific callback from the hook.
|
||||
*/
|
||||
register(callback) {
|
||||
const exceptionHandler = this.exceptionHandler || function (exception) {
|
||||
// Note: this relies on the undocumented fact that if bindEnvironment's
|
||||
@@ -75,29 +125,23 @@ export class Hook {
|
||||
if (this.bindEnvironment) {
|
||||
callback = Meteor.bindEnvironment(callback, exceptionHandler);
|
||||
} else {
|
||||
callback = dontBindEnvironment(callback, exceptionHandler);
|
||||
callback = wrapHookWithErrorHandling(callback, exceptionHandler);
|
||||
}
|
||||
|
||||
if (this.wrapAsync) {
|
||||
callback = Meteor.wrapFn(callback);
|
||||
}
|
||||
|
||||
const id = this.nextCallbackId++;
|
||||
this.callbacks[id] = callback;
|
||||
this.callbacks.add(callback);
|
||||
|
||||
return {
|
||||
callback,
|
||||
stop: () => {
|
||||
delete this.callbacks[id];
|
||||
this.callbacks.delete(callback);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.nextCallbackId = 0;
|
||||
this.callbacks = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* For each registered callback, call the passed iterator function with the callback.
|
||||
*
|
||||
@@ -110,17 +154,8 @@ export class Hook {
|
||||
* @param iterator
|
||||
*/
|
||||
forEach(iterator) {
|
||||
|
||||
const ids = Object.keys(this.callbacks);
|
||||
for (let i = 0; i < ids.length; ++i) {
|
||||
const id = ids[i];
|
||||
// check to see if the callback was removed during iteration
|
||||
if (hasOwn.call(this.callbacks, id)) {
|
||||
const callback = this.callbacks[id];
|
||||
if (! iterator(callback)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const callback of this.callbacks) {
|
||||
if (!iterator(callback)) break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,16 +168,8 @@ export class Hook {
|
||||
* @see forEach
|
||||
*/
|
||||
async forEachAsync(iterator) {
|
||||
const ids = Object.keys(this.callbacks);
|
||||
for (let i = 0; i < ids.length; ++i) {
|
||||
const id = ids[i];
|
||||
// check to see if the callback was removed during iteration
|
||||
if (hasOwn.call(this.callbacks, id)) {
|
||||
const callback = this.callbacks[id];
|
||||
if (!await iterator(callback)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (const callback of this.callbacks) {
|
||||
if (!await iterator(callback)) break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,27 +180,61 @@ export class Hook {
|
||||
each(iterator) {
|
||||
return this.forEach(iterator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the Hook instance iterable, allowing it to be used in `for...of` loops.
|
||||
* It iterates over the registered callbacks.
|
||||
* @returns {Iterator<Function>} An iterator for the registered callbacks.
|
||||
*/
|
||||
[Symbol.iterator]() {
|
||||
return this.callbacks[Symbol.iterator]();
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from Meteor.bindEnvironment and removed all the env stuff.
|
||||
function dontBindEnvironment(func, onException, _this) {
|
||||
if (!onException || typeof(onException) === 'string') {
|
||||
const description = onException || "callback of async function";
|
||||
onException = function (error) {
|
||||
Meteor._debug(
|
||||
"Exception in " + description,
|
||||
error
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return function (...args) {
|
||||
/**
|
||||
* Wraps a given function with error handling. If the wrapped function throws an exception,
|
||||
* it will be caught and passed to the provided exception handler.
|
||||
* This is similar to `Meteor.bindEnvironment` but without the Meteor environment binding.
|
||||
*
|
||||
* @param {Function} func The function to wrap.
|
||||
* @param {Function|string} onException The exception handler function to call if `func` throws,
|
||||
* or a string description for default exception logging.
|
||||
* @param {any} _this The `this` context to bind to `func` when it is called.
|
||||
* @returns {Function} A new function that executes `func` with error handling.
|
||||
*/
|
||||
function wrapHookWithErrorHandling(func, onException, _this) {
|
||||
const exceptionHandler = normalizeHookExceptionHandler(onException);
|
||||
return function executeHookWithErrorHandling(...args) {
|
||||
let ret;
|
||||
try {
|
||||
ret = func.apply(_this, args);
|
||||
} catch (e) {
|
||||
onException(e);
|
||||
exceptionHandler(e);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes an exception handler, ensuring it is a function.
|
||||
* If a function is provided, it is returned directly.
|
||||
* If a string is provided, it is used as a description for a default handler that logs exceptions.
|
||||
* Otherwise, a generic default handler that logs exceptions with a default description is returned.
|
||||
*
|
||||
* @param {Function|string} exceptionHandler The exception handler to normalize. Can be a function,
|
||||
* a string description for logging, or any other value (which defaults to generic logging).
|
||||
* @returns {Function} A function that handles exceptions.
|
||||
*/
|
||||
function normalizeHookExceptionHandler(exceptionHandler) {
|
||||
if (typeof exceptionHandler === 'function') {
|
||||
return exceptionHandler;
|
||||
}
|
||||
|
||||
const description = typeof exceptionHandler === 'string'
|
||||
? exceptionHandler
|
||||
: "callback of async function";
|
||||
|
||||
return function defaultHookExceptionHandler(error) {
|
||||
Meteor._debug(`Exception in ${description}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Package.describe({
|
||||
summary: "Register callbacks on a hook",
|
||||
version: '1.6.1',
|
||||
version: '1.6.2',
|
||||
});
|
||||
|
||||
Package.onUse(function (api) {
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
Tinytest.add("spiderable - default hooks registered", function (test, expect) {
|
||||
test.equal(
|
||||
_.keys(Spiderable._onReadyHook.callbacks).length,
|
||||
2
|
||||
);
|
||||
test.equal(Spiderable._onReadyHook.size(), 2);
|
||||
});
|
||||
|
||||
Tinytest.add("spiderable - is not ready while initial subscriptions aren't started", function (test, expect) {
|
||||
@@ -39,21 +36,12 @@ Tinytest.add("spiderable - default hooks can ready", function (test, expect) {
|
||||
});
|
||||
|
||||
Tinytest.add("spiderable - is not ready with a custom hook", function (test, expect) {
|
||||
var callbacks = {}
|
||||
test.equal(
|
||||
_.keys(Spiderable._onReadyHook.callbacks).length,
|
||||
2
|
||||
);
|
||||
test.equal(Spiderable._onReadyHook.size(), 2);
|
||||
|
||||
//clear all/default callbacks
|
||||
_.each(Spiderable._onReadyHook.callbacks, function (value,key,list) {
|
||||
callbacks[key] = value;
|
||||
delete list[key];
|
||||
});
|
||||
test.equal(
|
||||
_.keys(Spiderable._onReadyHook.callbacks).length,
|
||||
0
|
||||
);
|
||||
var callbacks = Spiderable._onReadyHook.asArray()
|
||||
Spiderable._onReadyHook.clear();
|
||||
test.equal(Spiderable._onReadyHook.size(), 0);
|
||||
|
||||
|
||||
// actually test not ready
|
||||
@@ -62,41 +50,21 @@ Tinytest.add("spiderable - is not ready with a custom hook", function (test, exp
|
||||
|
||||
|
||||
// clear new callback
|
||||
_.each(Spiderable._onReadyHook.callbacks, function (value,key,list) {
|
||||
delete list[key];
|
||||
});
|
||||
test.equal(
|
||||
_.keys(Spiderable._onReadyHook.callbacks).length,
|
||||
0
|
||||
);
|
||||
Spiderable._onReadyHook.clear();
|
||||
test.equal(Spiderable._onReadyHook.size(), 0);
|
||||
|
||||
// restore callbacks
|
||||
_.each(callbacks, function (value,key,list) {
|
||||
Spiderable._onReadyHook.callbacks[key] = value;
|
||||
});
|
||||
test.equal(
|
||||
_.keys(Spiderable._onReadyHook.callbacks).length,
|
||||
2
|
||||
);
|
||||
Spiderable._onReadyHook.fromArray(callbacks);
|
||||
test.equal(Spiderable._onReadyHook.size(), 2);
|
||||
});
|
||||
|
||||
Tinytest.add("spiderable - is ready with a custom hook", function (test, expect) {
|
||||
var callbacks = {}
|
||||
test.equal(
|
||||
_.keys(Spiderable._onReadyHook.callbacks).length,
|
||||
2
|
||||
);
|
||||
test.equal(Spiderable._onReadyHook.size(), 2);
|
||||
|
||||
//clear all callbacks
|
||||
_.each(Spiderable._onReadyHook.callbacks, function (value,key,list) {
|
||||
callbacks[key] = value;
|
||||
delete list[key];
|
||||
});
|
||||
test.equal(
|
||||
_.keys(Spiderable._onReadyHook.callbacks).length,
|
||||
0
|
||||
);
|
||||
|
||||
var callbacks = Spiderable._onReadyHook.asArray();
|
||||
Spiderable._onReadyHook.clear();
|
||||
test.equal(Spiderable._onReadyHook.size(), 0);
|
||||
|
||||
// actually test ready
|
||||
Spiderable.addReadyCondition(function () { return true; });
|
||||
@@ -104,20 +72,10 @@ Tinytest.add("spiderable - is ready with a custom hook", function (test, expect)
|
||||
|
||||
|
||||
// clear new callback
|
||||
_.each(Spiderable._onReadyHook.callbacks, function (value,key,list) {
|
||||
delete list[key];
|
||||
});
|
||||
test.equal(
|
||||
_.keys(Spiderable._onReadyHook.callbacks).length,
|
||||
0
|
||||
);
|
||||
Spiderable._onReadyHook.clear();
|
||||
test.equal(Spiderable._onReadyHook.size(), 0);
|
||||
|
||||
// restore callbacks
|
||||
_.each(callbacks, function (value,key,list) {
|
||||
Spiderable._onReadyHook.callbacks[key] = value;
|
||||
});
|
||||
test.equal(
|
||||
_.keys(Spiderable._onReadyHook.callbacks).length,
|
||||
2
|
||||
);
|
||||
Spiderable._onReadyHook.fromArray(callbacks);
|
||||
test.equal(Spiderable._onReadyHook.size(), 2);
|
||||
});
|
||||
|
||||
18
packages/meteor/meteor.d.ts
vendored
18
packages/meteor/meteor.d.ts
vendored
@@ -328,26 +328,30 @@ export namespace Meteor {
|
||||
function defer(func: Function): void;
|
||||
|
||||
/**
|
||||
* Wrap a function so that it only runs in the specified environments.
|
||||
* Wrap a function so that it only runs in background in specified environments.
|
||||
* @param func The function to wrap
|
||||
* @param options An object with an `on` property that is an array of environment names: `"development"`, `"production"`, and/or `"test"`.
|
||||
*/
|
||||
function deferrable<T extends Function>(
|
||||
func: T,
|
||||
function deferrable<T>(
|
||||
func: () => T,
|
||||
options: { on: Array<"development" | "production" | "test"> }
|
||||
): T | void;
|
||||
|
||||
/**
|
||||
* Wrap a function so that it only runs in development environment.
|
||||
* Wrap a function to run in the background in development (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)).
|
||||
* @param func The function to wrap
|
||||
*/
|
||||
function deferDev<T extends Function>(func: T): T | void;
|
||||
function deferDev<T>(
|
||||
func: () => T
|
||||
): T | void;
|
||||
|
||||
/**
|
||||
* Wrap a function so that it only runs in production environment.
|
||||
* Wrap a function to run in the background in production (similar to Meteor.isProduction ? Meteor.defer(fn) : Meteor.startup(fn)).
|
||||
* @param func The function to wrap
|
||||
*/
|
||||
function deferProd<T extends Function>(func: T): T | void;
|
||||
function deferProd<T>(
|
||||
func: () => T
|
||||
): T | void;
|
||||
/** Timeout **/
|
||||
|
||||
/** utils **/
|
||||
|
||||
@@ -86,7 +86,7 @@ Meteor.defer = function (f) {
|
||||
|
||||
/**
|
||||
* @memberOf Meteor
|
||||
* @summary Defer execution of a function to run asynchronously in the background based on environment (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)).
|
||||
* @summary Wrap a function so that it only runs in background in specified environments..
|
||||
* @locus Anywhere
|
||||
* @param {Function} func The function to run
|
||||
* @param {Object} options The options object
|
||||
@@ -115,7 +115,7 @@ Meteor.deferrable = function (f, options) {
|
||||
|
||||
/**
|
||||
* @memberOf Meteor
|
||||
* @summary Defer execution of a function to run asynchronously in the background in development (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)).
|
||||
* @summary Wrap a function to run in the background in development (similar to Meteor.isDevelopment ? Meteor.defer(fn) : Meteor.startup(fn)).
|
||||
* @locus Anywhere
|
||||
* @param {Function} func The function to run
|
||||
* @param {Object} options The options object
|
||||
@@ -126,7 +126,7 @@ Meteor.deferDev = function (f) {
|
||||
|
||||
/**
|
||||
* @memberOf Meteor
|
||||
* @summary Defer execution of a function to run asynchronously in the background in production (similar to Meteor.isProduction ? Meteor.defer(fn) : Meteor.startup(fn)).
|
||||
* @summary Wrap a function to run in the background in production (similar to Meteor.isProduction ? Meteor.defer(fn) : Meteor.startup(fn)).
|
||||
* @locus Anywhere
|
||||
* @param {Function} func The function to run
|
||||
* @param {Object} options The options object
|
||||
|
||||
@@ -16,6 +16,8 @@ const {
|
||||
setGlobalState
|
||||
} = require('meteor/tools-core/lib/global-state');
|
||||
|
||||
const { applyDelegatedExtensions } = require('./config');
|
||||
|
||||
// Helper function to format milliseconds with comma separators
|
||||
function formatMilliseconds(ms) {
|
||||
return ms.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
@@ -124,10 +126,15 @@ export function setupCompilationTracking() {
|
||||
};
|
||||
|
||||
// Define separate onCompile callbacks for client and server
|
||||
const onCompileClient = (data) => {
|
||||
const onCompileClient = (data, config) => {
|
||||
// Resolve the promise if it's the first compilation
|
||||
const clientState = getGlobalState(GLOBAL_STATE_KEYS.CLIENT_FIRST_COMPILE, clientFirstCompile);
|
||||
if (!clientState?.resolved) {
|
||||
// Apply delegated extensions before resolving (so they're set before Meteor scans)
|
||||
if (config?.delegatedExtensions?.length > 0) {
|
||||
applyDelegatedExtensions(config.delegatedExtensions);
|
||||
}
|
||||
|
||||
clientState.resolved = true;
|
||||
clientState.resolve();
|
||||
setGlobalState(GLOBAL_STATE_KEYS.CLIENT_FIRST_COMPILE, clientState);
|
||||
|
||||
@@ -386,3 +386,52 @@ export function configureMeteorForRspack() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies delegated extension ignore patterns for entry folder files.
|
||||
* Called after rspack's first compilation reports which extensions it handles.
|
||||
* Since Meteor awaits rspack compilation before scanning files, these patterns
|
||||
* are in place before Meteor processes any application files.
|
||||
*
|
||||
* Uses gitignore semantics: a later positive pattern (client/*.css) overrides
|
||||
* an earlier negation (!client/*.css) that was set in configureMeteorForRspack.
|
||||
*
|
||||
* @param {string[]} extensions - Array of extensions like ['.css', '.less']
|
||||
*/
|
||||
export function applyDelegatedExtensions(extensions) {
|
||||
if (!extensions || extensions.length === 0) return;
|
||||
|
||||
const initialEntrypoints = getInitialEntrypoints();
|
||||
const entrypointContexts = [
|
||||
initialEntrypoints.mainClient,
|
||||
initialEntrypoints.mainServer,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.map(entrypoint => path.dirname(entrypoint));
|
||||
|
||||
const ignorePatterns = [];
|
||||
for (const dir of entrypointContexts) {
|
||||
for (const ext of extensions) {
|
||||
// ext comes as '.css', glob needs '*.css'
|
||||
ignorePatterns.push(`${dir}/*${ext}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (ignorePatterns.length > 0) {
|
||||
// Re-append meteor.modules unignore patterns after the delegation ignores
|
||||
// so they take precedence (gitignore semantics: last match wins)
|
||||
const meteorAppConfig = getMeteorAppConfig();
|
||||
const unignoredFilesAndFolders = buildUnignorePatterns(
|
||||
meteorAppConfig?.modules || [],
|
||||
{ skipLevel: 1 },
|
||||
);
|
||||
|
||||
setMeteorAppIgnore(
|
||||
[...ignorePatterns, ...unignoredFilesAndFolders].join(' ')
|
||||
);
|
||||
|
||||
if (isMeteorAppDebug() || isMeteorAppConfigModernVerbose()) {
|
||||
logInfo(`[i] Rspack delegated extensions: ${extensions.join(', ')} (ignored in entry folders)\n ${process.env.METEOR_IGNORE}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import path from 'path';
|
||||
|
||||
export const DEFAULT_RSPACK_VERSION = '1.7.1';
|
||||
|
||||
export const DEFAULT_METEOR_RSPACK_VERSION = '1.1.0-beta.31';
|
||||
export const DEFAULT_METEOR_RSPACK_VERSION = '1.1.0-beta.33';
|
||||
|
||||
export const DEFAULT_METEOR_RSPACK_REACT_HMR_VERSION = '1.4.3';
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ export const watchAndHashDeps = Profile(
|
||||
if (dep.type === 'dependency') {
|
||||
fileCount += 1;
|
||||
const fileHash = hashAndWatchFile(dep.file);
|
||||
hash.update(fileHash).update('\0');
|
||||
hash.update(fileHash || 'deleted').update('\0');
|
||||
} else if (dep.type === 'dir-dependency') {
|
||||
if (dep.dir in globsByDir) {
|
||||
globsByDir[dep.dir].push(dep.glob || '**');
|
||||
|
||||
303
scripts/checkout-pr.js
Executable file
303
scripts/checkout-pr.js
Executable file
@@ -0,0 +1,303 @@
|
||||
#!/usr/bin/env node
|
||||
//
|
||||
// checkout-pr.js — prepare a local branch from a fork contribution
|
||||
//
|
||||
// Usage:
|
||||
// node scripts/checkout-pr.js <PR-number>
|
||||
// node scripts/checkout-pr.js <PR-URL>
|
||||
// node scripts/checkout-pr.js <user>:<branch>
|
||||
// node scripts/checkout-pr.js <fork-repo-url> <branch>
|
||||
// node scripts/checkout-pr.js git@github.com:<user>/<repo>.git <branch>
|
||||
//
|
||||
// Examples:
|
||||
// node scripts/checkout-pr.js 123
|
||||
// node scripts/checkout-pr.js https://github.com/meteor/meteor/pull/<PR-number>
|
||||
// node scripts/checkout-pr.js <user>:<branch>
|
||||
// node scripts/checkout-pr.js <fork-repo-url> <branch>
|
||||
// node scripts/checkout-pr.js git@github.com:<user>/<repo>.git <branch>
|
||||
|
||||
'use strict';
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const https = require('https');
|
||||
|
||||
// Colors (disabled if stdout is not a TTY)
|
||||
const isTTY = process.stdout.isTTY;
|
||||
const c = {
|
||||
red: isTTY ? '\x1b[0;31m' : '',
|
||||
green: isTTY ? '\x1b[0;32m' : '',
|
||||
yellow: isTTY ? '\x1b[0;33m' : '',
|
||||
cyan: isTTY ? '\x1b[0;36m' : '',
|
||||
bold: isTTY ? '\x1b[1m' : '',
|
||||
reset: isTTY ? '\x1b[0m' : '',
|
||||
};
|
||||
|
||||
function info(msg) { console.log(`${c.cyan}\u2192${c.reset} ${msg}`); }
|
||||
function ok(msg) { console.log(`${c.green}\u2713${c.reset} ${msg}`); }
|
||||
function warn(msg) { console.log(`${c.yellow}\u26A0${c.reset} ${msg}`); }
|
||||
function err(msg) { console.error(`${c.red}\u2717${c.reset} ${msg}`); }
|
||||
|
||||
function die(msg) {
|
||||
err(msg);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function usage() {
|
||||
console.log(`Usage:
|
||||
npm run checkout:pr -- <PR-number>
|
||||
npm run checkout:pr -- <PR-URL>
|
||||
npm run checkout:pr -- <user>:<branch>
|
||||
npm run checkout:pr -- <fork-repo-url> <branch>
|
||||
npm run checkout:pr -- git@github.com:<user>/<repo>.git <branch>
|
||||
|
||||
Prepares a local branch from a fork contribution for testing and review.
|
||||
|
||||
Examples:
|
||||
npm run checkout:pr -- 123
|
||||
npm run checkout:pr -- https://github.com/meteor/meteor/pull/<PR-number>
|
||||
npm run checkout:pr -- <user>:<branch>
|
||||
npm run checkout:pr -- <fork-repo-url> <branch>
|
||||
npm run checkout:pr -- git@github.com:<user>/<repo>.git <branch>`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function git(cmd, { silent = false } = {}) {
|
||||
try {
|
||||
return execSync(`git ${cmd}`, {
|
||||
encoding: 'utf8',
|
||||
stdio: silent ? ['pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'inherit'],
|
||||
}).trim();
|
||||
} catch (e) {
|
||||
if (silent) return null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function ghCli(args) {
|
||||
try {
|
||||
return execSync(`gh ${args}`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function hasCommand(name) {
|
||||
try {
|
||||
execSync(process.platform === 'win32' ? `where ${name}` : `command -v ${name}`, { stdio: 'pipe' });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function httpsGet(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
https.get(url, { headers: { 'User-Agent': 'meteor-checkout-pr' } }, (res) => {
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
return reject(new Error(`HTTP ${res.statusCode}`));
|
||||
}
|
||||
let data = '';
|
||||
res.on('data', (chunk) => { data += chunk; });
|
||||
res.on('end', () => resolve(data));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function extractFromPrUrl(prUrl) {
|
||||
const match = prUrl.match(/^https?:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/);
|
||||
if (!match) die(`could not parse PR URL: ${prUrl}`);
|
||||
const [, repoPath, prNumber] = match;
|
||||
|
||||
// Try gh CLI first
|
||||
if (hasCommand('gh')) {
|
||||
const result = ghCli(`pr view "${prUrl}" --json headRepositoryOwner,headRefName`);
|
||||
if (result) {
|
||||
try {
|
||||
const data = JSON.parse(result);
|
||||
const owner = data.headRepositoryOwner?.login;
|
||||
const branch = data.headRefName;
|
||||
if (owner && branch) return { owner, branch };
|
||||
} catch { /* fall through */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to GitHub REST API
|
||||
const apiUrl = `https://api.github.com/repos/${repoPath}/pulls/${prNumber}`;
|
||||
let body;
|
||||
try {
|
||||
body = await httpsGet(apiUrl);
|
||||
} catch (e) {
|
||||
die(`could not fetch PR data from ${apiUrl} (${e.message})`);
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
const owner = data.head?.user?.login;
|
||||
const branch = data.head?.ref;
|
||||
if (owner && branch) return { owner, branch };
|
||||
} catch { /* fall through */ }
|
||||
|
||||
die(`could not extract fork owner/branch from PR #${prNumber}`);
|
||||
}
|
||||
|
||||
function getRepoPathFromOrigin() {
|
||||
const originUrl = git('remote get-url origin', { silent: true });
|
||||
if (!originUrl) return null;
|
||||
// Match HTTPS: https://github.com/owner/repo(.git)
|
||||
const httpsMatch = originUrl.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/);
|
||||
if (httpsMatch) return httpsMatch[1];
|
||||
// Match SSH: git@github.com:owner/repo(.git)
|
||||
const sshMatch = originUrl.match(/github\.com:([^/]+\/[^/]+?)(?:\.git)?$/);
|
||||
if (sshMatch) return sshMatch[1];
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractOwnerFromUrl(url) {
|
||||
const match = url.match(/github\.com[:/]([^/]+)\//);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function normalizeUrl(url) {
|
||||
return url
|
||||
.replace(/\.git$/, '')
|
||||
.replace(/\/$/, '')
|
||||
.replace(/^https?:\/\//, '')
|
||||
.replace(/^git@github\.com:/, 'github.com/');
|
||||
}
|
||||
|
||||
function buildForkUrl(owner) {
|
||||
// Match origin's protocol (SSH vs HTTPS)
|
||||
const originUrl = git('remote get-url origin', { silent: true }) || '';
|
||||
if (originUrl.startsWith('git@')) {
|
||||
return `git@github.com:${owner}/meteor.git`;
|
||||
}
|
||||
return `https://github.com/${owner}/meteor.git`;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// Ensure we're inside a git repo
|
||||
if (!git('rev-parse --is-inside-work-tree', { silent: true })) {
|
||||
die('not inside a git repository');
|
||||
}
|
||||
|
||||
let forkOwner, forkBranch, forkRepoUrl;
|
||||
|
||||
if (args.length === 1) {
|
||||
const arg = args[0];
|
||||
const prNumberMatch = arg.match(/^\d+$/);
|
||||
const prMatch = arg.match(/^https?:\/\/github\.com\/.*\/pull\/\d+/);
|
||||
const sshMatch = arg.match(/^git@[^:]+:/);
|
||||
const httpsRepoMatch = arg.match(/^https?:\/\/.*\.git$/);
|
||||
// user:branch — must not start with git@ (SSH) or contain / before : (URLs)
|
||||
const shortMatch = !sshMatch && arg.match(/^([^/:]+):(.+)$/);
|
||||
|
||||
if (prNumberMatch) {
|
||||
const repoPath = getRepoPathFromOrigin();
|
||||
if (!repoPath) die('could not determine repository from origin remote');
|
||||
const prUrl = `https://github.com/${repoPath}/pull/${arg}`;
|
||||
info(`resolved PR #${arg} → ${c.bold}${prUrl}${c.reset}`);
|
||||
const result = await extractFromPrUrl(prUrl);
|
||||
forkOwner = result.owner;
|
||||
forkBranch = result.branch;
|
||||
forkRepoUrl = buildForkUrl(forkOwner);
|
||||
} else if (prMatch) {
|
||||
const result = await extractFromPrUrl(arg);
|
||||
forkOwner = result.owner;
|
||||
forkBranch = result.branch;
|
||||
forkRepoUrl = buildForkUrl(forkOwner);
|
||||
} else if (sshMatch || httpsRepoMatch) {
|
||||
die(`repo URL requires a branch argument: node scripts/checkout-pr.js ${arg} <branch>`);
|
||||
} else if (shortMatch) {
|
||||
forkOwner = shortMatch[1];
|
||||
forkBranch = shortMatch[2];
|
||||
forkRepoUrl = buildForkUrl(forkOwner);
|
||||
} else {
|
||||
err(`unrecognized format: ${arg}`);
|
||||
console.error('');
|
||||
return usage();
|
||||
}
|
||||
} else if (args.length === 2) {
|
||||
forkRepoUrl = args[0];
|
||||
forkBranch = args[1];
|
||||
forkOwner = extractOwnerFromUrl(forkRepoUrl);
|
||||
if (!forkOwner) die(`could not extract owner from URL: ${forkRepoUrl}`);
|
||||
} else {
|
||||
return usage();
|
||||
}
|
||||
|
||||
const previousBranch = git('symbolic-ref --short HEAD', { silent: true })
|
||||
|| git('rev-parse --short HEAD', { silent: true })
|
||||
|| 'HEAD';
|
||||
|
||||
// Detect if the PR is from the upstream repo (not a fork)
|
||||
let remoteName = '';
|
||||
let isUpstream = false;
|
||||
const originUrl = git('remote get-url origin', { silent: true });
|
||||
if (originUrl) {
|
||||
const normFork = normalizeUrl(forkRepoUrl);
|
||||
const normOrigin = normalizeUrl(originUrl);
|
||||
if (normFork === normOrigin) {
|
||||
remoteName = 'origin';
|
||||
isUpstream = true;
|
||||
}
|
||||
}
|
||||
|
||||
let localBranch;
|
||||
if (isUpstream) {
|
||||
localBranch = forkBranch;
|
||||
} else {
|
||||
remoteName = forkOwner;
|
||||
localBranch = `fork/${forkOwner}/${forkBranch}`;
|
||||
}
|
||||
|
||||
console.log(`${c.bold}--- checkout-pr ---${c.reset}`);
|
||||
info(`owner: ${c.bold}${forkOwner}${c.reset}`);
|
||||
info(`branch: ${c.bold}${forkBranch}${c.reset}`);
|
||||
info(`repo: ${c.bold}${forkRepoUrl}${c.reset}`);
|
||||
if (isUpstream) {
|
||||
info(`upstream: yes (using remote '${c.bold}${remoteName}${c.reset}')`);
|
||||
}
|
||||
info(`local branch: ${c.bold}${localBranch}${c.reset}`);
|
||||
console.log('');
|
||||
|
||||
// Add remote if needed (skip for upstream PRs)
|
||||
if (!isUpstream) {
|
||||
const existingUrl = git(`remote get-url "${remoteName}"`, { silent: true });
|
||||
if (existingUrl) {
|
||||
warn(`remote '${remoteName}' already exists, reusing it`);
|
||||
} else {
|
||||
info(`adding remote '${remoteName}' \u2192 ${forkRepoUrl}`);
|
||||
git(`remote add "${remoteName}" "${forkRepoUrl}"`);
|
||||
ok(`remote '${remoteName}' added`);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the branch
|
||||
info(`fetching '${forkBranch}' from '${remoteName}'...`);
|
||||
const fetchResult = git(`fetch "${remoteName}" "${forkBranch}"`, { silent: true });
|
||||
if (fetchResult === null) {
|
||||
die(`failed to fetch branch '${forkBranch}' from '${remoteName}' \u2014 check that the fork and branch exist`);
|
||||
}
|
||||
ok(`fetched latest from '${remoteName}'`);
|
||||
|
||||
// Create or switch to local branch
|
||||
const branchExists = git(`show-ref --verify "refs/heads/${localBranch}"`, { silent: true });
|
||||
if (branchExists) {
|
||||
warn(`branch '${localBranch}' already exists, switching and updating...`);
|
||||
git(`checkout "${localBranch}"`);
|
||||
git(`reset --hard "refs/remotes/${remoteName}/${forkBranch}"`);
|
||||
} else {
|
||||
info(`creating branch '${localBranch}'...`);
|
||||
git(`checkout -b "${localBranch}" "refs/remotes/${remoteName}/${forkBranch}"`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
ok(`ready on branch: ${c.bold}${localBranch}${c.reset}`);
|
||||
info(`to switch back: ${c.bold}git checkout ${previousBranch}${c.reset}`);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
die(e.message);
|
||||
});
|
||||
@@ -16,6 +16,9 @@ var stats = require('../meteor-services/stats.js');
|
||||
var Console = require('../console/console.js').Console;
|
||||
const {
|
||||
blue,
|
||||
bold,
|
||||
cyan,
|
||||
dim,
|
||||
green,
|
||||
purple,
|
||||
red,
|
||||
@@ -124,7 +127,7 @@ import { ensureDevBundleDependencies } from '../cordova/index.js';
|
||||
import { CordovaRunner } from '../cordova/runner.js';
|
||||
import { iOSRunTarget, AndroidRunTarget } from '../cordova/run-targets.js';
|
||||
|
||||
import { EXAMPLE_REPOSITORIES } from './example-repositories.js';
|
||||
import { getExamples, findExample, cloneRepo, cloneSubdirectory, validateMeteorApp, EXAMPLES_REPO, EXAMPLES_BRANCH } from './examples.js';
|
||||
|
||||
// The architecture used by Meteor Software's hosted servers; it's the
|
||||
// architecture used by 'meteor deploy'.
|
||||
@@ -675,17 +678,6 @@ main.registerCommand({
|
||||
* Resolves into json with
|
||||
* @returns {Promise<[Skeletons, null]> | Promise<[null, Error]>}
|
||||
*/
|
||||
function getExamplesJSON(){
|
||||
return tryRun(async () => {
|
||||
const response = await httpHelpers.request({
|
||||
url: "https://cdn.meteor.com/static/meteor.json",
|
||||
method: "GET",
|
||||
useSessionHeader: true,
|
||||
useAuthHeader: true,
|
||||
});
|
||||
return JSON.parse(response.body);
|
||||
});
|
||||
}
|
||||
|
||||
const DEFAULT_SKELETON = "react";
|
||||
export const AVAILABLE_SKELETONS = [
|
||||
@@ -697,6 +689,7 @@ export const AVAILABLE_SKELETONS = [
|
||||
"minimal",
|
||||
DEFAULT_SKELETON,
|
||||
"typescript",
|
||||
"typescript-tailwind",
|
||||
"vue",
|
||||
"svelte",
|
||||
"tailwind",
|
||||
@@ -715,6 +708,7 @@ const SKELETON_INFO = {
|
||||
"minimal": "To create an app with as few Meteor packages as possible",
|
||||
"react": "To create a basic React-based app",
|
||||
"typescript": "To create an app using TypeScript and React",
|
||||
"typescript-tailwind": "To create an app using TypeScript, React, and Tailwind",
|
||||
"vue": "To create a basic Vue3-based app",
|
||||
"svelte": "To create a basic Svelte app",
|
||||
"tailwind": "To create an app using React and Tailwind",
|
||||
@@ -722,7 +716,7 @@ const SKELETON_INFO = {
|
||||
"solid": "To create a basic Solid app",
|
||||
"coffeescript": "To create a basic CoffeeScript app",
|
||||
"babel": "To create a React app with Babel support",
|
||||
"angular": "To create a basic Angular app",
|
||||
"angular": "To create a basic Angular app"
|
||||
};
|
||||
|
||||
main.registerCommand({
|
||||
@@ -741,6 +735,7 @@ main.registerCommand({
|
||||
react: { type: Boolean },
|
||||
vue: { type: Boolean },
|
||||
typescript: { type: Boolean },
|
||||
'typescript-tailwind': { type: Boolean },
|
||||
apollo: { type: Boolean },
|
||||
svelte: { type: Boolean },
|
||||
tailwind: { type: Boolean },
|
||||
@@ -751,6 +746,8 @@ main.registerCommand({
|
||||
legacy: { type: Boolean },
|
||||
prototype: { type: Boolean },
|
||||
from: { type: String },
|
||||
'from-dir': { type: String },
|
||||
'from-branch': { type: String },
|
||||
},
|
||||
pretty: false,
|
||||
catalogRefresh: new catalog.Refresh.Never()
|
||||
@@ -774,6 +771,11 @@ main.registerCommand({
|
||||
Console.error();
|
||||
throw new main.ShowUsage();
|
||||
}
|
||||
if (options.from || options['from-dir'] || options['from-branch']) {
|
||||
Console.error("Package creation does not support --from, --from-dir, or --from-branch.");
|
||||
Console.error();
|
||||
throw new main.ShowUsage();
|
||||
}
|
||||
|
||||
if (!packageName) {
|
||||
Console.error("Please specify the name of the package.");
|
||||
@@ -895,26 +897,37 @@ main.registerCommand({
|
||||
}
|
||||
|
||||
if (options.list) {
|
||||
Console.info("Available examples:");
|
||||
const [json, err] = await getExamplesJSON()
|
||||
if (err) {
|
||||
Console.error("Failed to fetch examples:", err.message);
|
||||
Console.info("Using cached examples.json");
|
||||
}
|
||||
const examples = err ? EXAMPLE_REPOSITORIES : json;
|
||||
_.each(examples, function (repoInfo, name) {
|
||||
const branchInfo = repoInfo.branch ? `/tree/${repoInfo.branch}` : "";
|
||||
Console.info(
|
||||
Console.command(`${name}: ${repoInfo.repo}${branchInfo}`),
|
||||
Console.options({ indent: 2 })
|
||||
);
|
||||
});
|
||||
try {
|
||||
const examples = await getExamples();
|
||||
Console.rawInfo(`\n ${bold`Meteor Examples`} ${dim`${examples.length} available`}\n\n`);
|
||||
|
||||
Console.info();
|
||||
Console.info(
|
||||
"To create an example, simply",
|
||||
Console.command("'meteor create <app-name> --example <name>'")
|
||||
);
|
||||
examples.forEach((ex, i) => {
|
||||
const version = ex.meteorVersion ? dim` v${ex.meteorVersion}` : '';
|
||||
Console.rawInfo(` ${cyan`${ex.slug}`}${version}\n`);
|
||||
if (ex.why) {
|
||||
Console.rawInfo(` ${ex.why}\n`);
|
||||
}
|
||||
if (ex.stack && ex.stack.length) {
|
||||
Console.rawInfo(` ${dim`Tech:`} ${ex.stack.join(' · ')}\n`);
|
||||
}
|
||||
const repoUrl = ex.repositoryUrl || `${EXAMPLES_REPO}/tree/${EXAMPLES_BRANCH}/${ex.internalPath}`;
|
||||
if (ex.demo) {
|
||||
Console.rawInfo(` ${dim`Demo:`} ${ex.demo}\n`);
|
||||
}
|
||||
if (ex.tutorial) {
|
||||
Console.rawInfo(` ${dim`Tutorial:`} ${ex.tutorial}\n`);
|
||||
}
|
||||
Console.rawInfo(` ${dim`Repo:`} ${repoUrl}\n`);
|
||||
if (i < examples.length - 1) {
|
||||
Console.rawInfo('\n');
|
||||
}
|
||||
});
|
||||
|
||||
Console.rawInfo(`\n ${dim`Usage:`} meteor create ${bold`<app>`} --example ${cyan`<slug>`}\n\n`);
|
||||
} catch (err) {
|
||||
Console.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1151,57 +1164,68 @@ main.registerCommand({
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} url
|
||||
*/
|
||||
const setupExampleByURL = async (url) => {
|
||||
const [ok, err] = await bash`git --version`;
|
||||
if (err) throw new Error("git is not installed");
|
||||
const isWindows = process.platform === "win32";
|
||||
|
||||
// Set GIT_TERMINAL_PROMPT=0 to disable prompting
|
||||
process.env.GIT_TERMINAL_PROMPT = 0;
|
||||
|
||||
const gitCommand = isWindows
|
||||
? `git clone --progress ${url} "${files.convertToOSPath(appPath)}"`
|
||||
: `git clone --progress ${url} ${appPath}`;
|
||||
const [okClone, errClone] = await bash`${gitCommand}`;
|
||||
const errorMessage = errClone && typeof errClone === "string" ? errClone : errClone?.message;
|
||||
if (errorMessage && errorMessage.includes("Cloning into")) {
|
||||
throw new Error("error cloning skeleton");
|
||||
}
|
||||
// remove .git folder from the example
|
||||
await files.rm_recursive_async(files.pathJoin(appPath, ".git"));
|
||||
await setupMessages();
|
||||
};
|
||||
|
||||
if (options.example) {
|
||||
const [json, err] = await getExamplesJSON();
|
||||
try {
|
||||
let examples = await getExamples();
|
||||
let example = findExample(examples, options.example);
|
||||
|
||||
if (err) {
|
||||
Console.error("Failed to fetch examples:", err.message);
|
||||
Console.info("Using cached examples.json");
|
||||
}
|
||||
if (!example) {
|
||||
examples = await getExamples({ refresh: true });
|
||||
example = findExample(examples, options.example);
|
||||
}
|
||||
|
||||
const examples = err ? EXAMPLE_REPOSITORIES : json;
|
||||
const repoInfo = examples[options.example];
|
||||
if (!repoInfo) {
|
||||
Console.error(`${options.example}: no such example.`);
|
||||
Console.error(
|
||||
"List available applications with",
|
||||
Console.command("'meteor create --list'") + "."
|
||||
);
|
||||
if (!example) {
|
||||
Console.error(`'${options.example}' is not a known example.`);
|
||||
Console.error('Run', Console.command("'meteor create --list'"), 'to see available examples.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (example.isInternal) {
|
||||
await cloneSubdirectory(EXAMPLES_REPO, EXAMPLES_BRANCH, example.internalPath, appPath);
|
||||
} else {
|
||||
await cloneRepo(example.repositoryUrl, appPath);
|
||||
}
|
||||
|
||||
await setupMessages();
|
||||
} catch (err) {
|
||||
Console.error('Error creating example:', err.message);
|
||||
return 1;
|
||||
}
|
||||
// repoInfo.repo is the URL of the repo, and repoInfo.branch is the branch
|
||||
await setupExampleByURL(repoInfo.repo);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ((options['from-dir'] || options['from-branch']) && !options.from) {
|
||||
Console.error('--from-dir and --from-branch require --from to specify the source repository.');
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (options.from) {
|
||||
await setupExampleByURL(options.from);
|
||||
const branch = options['from-branch'] || null;
|
||||
try {
|
||||
if (options['from-dir']) {
|
||||
let repoUrl = options.from;
|
||||
try {
|
||||
const examples = await getExamples();
|
||||
const example = findExample(examples, options.from);
|
||||
if (example) {
|
||||
repoUrl = example.repositoryUrl || EXAMPLES_REPO;
|
||||
}
|
||||
} catch (e) {
|
||||
// If examples fetch fails, treat --from as a URL
|
||||
}
|
||||
|
||||
await cloneSubdirectory(repoUrl, branch, options['from-dir'], appPath);
|
||||
validateMeteorApp(appPath);
|
||||
} else {
|
||||
await cloneRepo(options.from, appPath, { branch });
|
||||
validateMeteorApp(appPath);
|
||||
}
|
||||
|
||||
await setupMessages();
|
||||
} catch (err) {
|
||||
Console.error(err.message);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1271,8 +1295,8 @@ main.registerCommand({
|
||||
// using it as it was before 2.x
|
||||
if (release.explicit) throw new Error("Using release option");
|
||||
|
||||
// If local skeleton doesn't exist, use setupExampleByURL
|
||||
await setupExampleByURL(`https://github.com/meteor/skel-${skeleton}`);
|
||||
// If local skeleton doesn't exist, clone from GitHub
|
||||
await cloneRepo(`https://github.com/meteor/skel-${skeleton}`, appPath);
|
||||
} catch (e) {
|
||||
if (
|
||||
e.message !== "Using prototype option" &&
|
||||
|
||||
@@ -37,12 +37,7 @@ async function getDevBundleDir() {
|
||||
}
|
||||
|
||||
const devBundleLink = path.join(localDir, "dev_bundle");
|
||||
const devBundleStat = statOrNull(devBundleLink);
|
||||
if (devBundleStat) {
|
||||
return new Promise(function (resolve) {
|
||||
resolve(links.readLink(devBundleLink));
|
||||
});
|
||||
}
|
||||
const devBundleReleaseFile = path.join(localDir, "dev_bundle_release");
|
||||
|
||||
const release = fs.readFileSync(
|
||||
releaseFile, "utf8"
|
||||
@@ -52,10 +47,38 @@ async function getDevBundleDir() {
|
||||
return DEFAULT_DEV_BUNDLE_DIR;
|
||||
}
|
||||
|
||||
// Check if the cached dev_bundle link still matches the current release.
|
||||
// After a git branch switch, .meteor/release changes but
|
||||
// .meteor/local/dev_bundle (which is gitignored) keeps pointing to
|
||||
// the old release's dev_bundle.
|
||||
const devBundleStat = statOrNull(devBundleLink);
|
||||
if (devBundleStat) {
|
||||
var cachedRelease = null;
|
||||
try {
|
||||
cachedRelease = fs.readFileSync(
|
||||
devBundleReleaseFile, "utf8"
|
||||
).replace(/^\s+|\s+$/g, "");
|
||||
} catch (e) {
|
||||
// If the release cache file doesn't exist, invalidate the cache
|
||||
// so we re-resolve the dev_bundle for the current release.
|
||||
}
|
||||
|
||||
if (cachedRelease === release) {
|
||||
return new Promise(function (resolve) {
|
||||
resolve(links.readLink(devBundleLink));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const devBundleDir = await getDevBundleForRelease(release);
|
||||
|
||||
if (devBundleDir) {
|
||||
links.makeLink(devBundleDir, devBundleLink);
|
||||
try {
|
||||
fs.writeFileSync(devBundleReleaseFile, release, "utf8");
|
||||
} catch (e) {
|
||||
// Non-fatal: the link itself was created successfully.
|
||||
}
|
||||
return devBundleDir;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
export const EXAMPLE_REPOSITORIES = {
|
||||
"vue": { "repo": "https://github.com/meteor/skel-vue" },
|
||||
"react": { "repo": "https://github.com/meteor/skel-react" },
|
||||
"full": { "repo": "https://github.com/meteor/skel-full" },
|
||||
"bare": { "repo": "https://github.com/meteor/skel-bare" },
|
||||
"blaze": { "repo": "https://github.com/meteor/skel-blaze" },
|
||||
"chakra-ui": { "repo": "https://github.com/meteor/skel-chakra-ui" },
|
||||
"apollo": { "repo": "https://github.com/meteor/skel-apollo" },
|
||||
"minimal": { "repo": "https://github.com/meteor/skel-minimal" },
|
||||
"solid": { "repo": "https://github.com/meteor/skel-solid" },
|
||||
"svelte": { "repo": "https://github.com/meteor/skel-svelte" },
|
||||
"tailwind": { "repo": "https://github.com/meteor/skel-tailwind" },
|
||||
"typescript": { "repo": "https://github.com/meteor/skel-typescript" },
|
||||
};
|
||||
208
tools/cli/examples.js
Normal file
208
tools/cli/examples.js
Normal file
@@ -0,0 +1,208 @@
|
||||
var files = require('../fs/files');
|
||||
var httpHelpers = require('../utils/http-helpers.js');
|
||||
var Console = require('../console/console.js').Console;
|
||||
const { execFile } = require('child_process');
|
||||
|
||||
const EXAMPLES_REPO = 'https://github.com/meteor/examples';
|
||||
const EXAMPLES_BRANCH = 'migrate-examples';
|
||||
const EXAMPLES_JSON_URL =
|
||||
`https://raw.githubusercontent.com/meteor/examples/${EXAMPLES_BRANCH}/examples.json`;
|
||||
|
||||
function validateExamplesData(data, { warn = (msg) => Console.warn(msg) } = {}) {
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Invalid examples.json format: expected a JSON array.');
|
||||
}
|
||||
return data.filter(entry => {
|
||||
if (!entry.slug || typeof entry.slug !== 'string') {
|
||||
warn('Skipping example entry with missing slug');
|
||||
return false;
|
||||
}
|
||||
if (!entry.repositoryUrl || typeof entry.repositoryUrl !== 'string') {
|
||||
warn(`Skipping example '${entry.slug}' with missing repositoryUrl`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function getCachePath() {
|
||||
var tropohouse = require('../packaging/tropohouse.js');
|
||||
return files.pathJoin(tropohouse.default.root, 'examples-cache.json');
|
||||
}
|
||||
|
||||
function readCache() {
|
||||
const cachePath = getCachePath();
|
||||
if (!files.exists(cachePath)) return null;
|
||||
try {
|
||||
const raw = files.readFile(cachePath, 'utf8');
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeCache(data) {
|
||||
try {
|
||||
const cachePath = getCachePath();
|
||||
files.writeFile(cachePath, JSON.stringify(data, null, 2), 'utf8');
|
||||
} catch (e) {
|
||||
// Don't fail the command if it can't write
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchExamplesJson() {
|
||||
const result = await httpHelpers.request({
|
||||
url: EXAMPLES_JSON_URL,
|
||||
method: 'GET',
|
||||
});
|
||||
if (result.response.statusCode !== 200) {
|
||||
throw new Error(
|
||||
`Failed to fetch examples.json (HTTP ${result.response.statusCode})`
|
||||
);
|
||||
}
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(result.body);
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON received from examples repository.');
|
||||
}
|
||||
return validateExamplesData(data);
|
||||
}
|
||||
|
||||
async function getExamples({ refresh = false } = {}) {
|
||||
// Always try network first, use cache as fallback for offline
|
||||
try {
|
||||
const examples = await fetchExamplesJson();
|
||||
|
||||
if (examples.length === 0) {
|
||||
throw new Error('No valid examples found in examples.json.');
|
||||
}
|
||||
|
||||
writeCache({
|
||||
fetchedAt: new Date().toISOString(),
|
||||
branch: EXAMPLES_BRANCH,
|
||||
examples,
|
||||
});
|
||||
|
||||
return examples;
|
||||
} catch (fetchError) {
|
||||
// When refresh is requested, don't fall back to stale cache
|
||||
if (refresh) {
|
||||
throw fetchError;
|
||||
}
|
||||
|
||||
// Network failed — fall back to cache if available
|
||||
const cached = readCache();
|
||||
if (cached && cached.examples) {
|
||||
return cached.examples;
|
||||
}
|
||||
|
||||
// No cache either — surface the original fetch error
|
||||
throw fetchError;
|
||||
}
|
||||
}
|
||||
|
||||
function findExample(examples, slug) {
|
||||
return examples.find(e => e.slug === slug) || null;
|
||||
}
|
||||
|
||||
async function cloneRepo(url, destPath, { branch = null } = {}) {
|
||||
const env = Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: '0' });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile('git', ['--version'], (error) => {
|
||||
if (error) {
|
||||
reject(new Error('git is not installed'));
|
||||
return;
|
||||
}
|
||||
|
||||
const args = ['clone', '--progress'];
|
||||
if (branch) {
|
||||
args.push('--branch', branch);
|
||||
}
|
||||
args.push(url, files.convertToOSPath(destPath));
|
||||
|
||||
execFile('git', args, { env }, async (cloneError) => {
|
||||
if (cloneError) {
|
||||
reject(new Error(`Failed to clone ${url}: ${cloneError.message}`));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await files.rm_recursive_async(files.pathJoin(destPath, '.git'));
|
||||
resolve();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function cloneSubdirectory(repoUrl, branch, subdir, destPath) {
|
||||
const tempDir = files.mkdtemp('meteor-example-');
|
||||
try {
|
||||
const env = Object.assign({}, process.env, { GIT_TERMINAL_PROMPT: '0' });
|
||||
const args = ['clone', '--progress'];
|
||||
if (branch) {
|
||||
args.push('--branch', branch);
|
||||
}
|
||||
args.push(repoUrl, tempDir);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
execFile('git', args, { env }, (error) => {
|
||||
if (error) {
|
||||
reject(new Error(`Failed to clone ${repoUrl}: ${error.message}`));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const path = require('path');
|
||||
const resolvedTemp = path.resolve(tempDir) + path.sep;
|
||||
const subdirPath = path.resolve(files.pathJoin(tempDir, subdir));
|
||||
if (!subdirPath.startsWith(resolvedTemp)) {
|
||||
throw new Error(
|
||||
`Invalid subdirectory '${subdir}': path escapes the repository.`
|
||||
);
|
||||
}
|
||||
if (!files.exists(subdirPath)) {
|
||||
throw new Error(
|
||||
`Directory '${subdir}' not found in the repository. The examples list may be outdated — try 'meteor create --list' to see current examples.`
|
||||
);
|
||||
}
|
||||
|
||||
await files.cp_r(subdirPath, destPath);
|
||||
|
||||
// Remove .git if it was copied
|
||||
const destGit = files.pathJoin(destPath, '.git');
|
||||
if (files.exists(destGit)) {
|
||||
await files.rm_recursive_async(destGit);
|
||||
}
|
||||
} finally {
|
||||
// Clean up temp directory
|
||||
await files.rm_recursive_async(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
function validateMeteorApp(dirPath) {
|
||||
const meteorDir = files.pathJoin(dirPath, '.meteor');
|
||||
if (!files.exists(meteorDir)) {
|
||||
throw new Error(
|
||||
`The directory '${files.convertToOSPath(dirPath)}' is not a Meteor app (no .meteor directory found).`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
validateExamplesData,
|
||||
getExamples,
|
||||
findExample,
|
||||
cloneRepo,
|
||||
cloneSubdirectory,
|
||||
validateMeteorApp,
|
||||
EXAMPLES_REPO,
|
||||
EXAMPLES_BRANCH,
|
||||
EXAMPLES_JSON_URL
|
||||
};
|
||||
@@ -154,8 +154,8 @@ Options:
|
||||
>>> create
|
||||
Create a new project.
|
||||
Usage: meteor create [--release <release>] [--bare|--minimal|--full|--react|--vue|--apollo|--svelte|--blaze|--tailwind|--chakra-ui|--solid|--babel|--coffeescript|--angular] <path>
|
||||
meteor create [--release <release>] --example <example_name> [<path>]
|
||||
meteor create [--release <release>] --from <git_url> [<path>]
|
||||
meteor create [--release <release>] --example <slug> [<path>]
|
||||
meteor create [--release <release>] --from <url> [--from-branch <branch>] [--from-dir <dir>] [<path>]
|
||||
meteor create --list
|
||||
meteor create --package [<package_name>]
|
||||
|
||||
@@ -176,30 +176,35 @@ package created in an app, will be created using the application's version of
|
||||
meteor and a package created outside a meteor app will use the latest release).
|
||||
|
||||
You can pass --example to start off with a copy of one of the Meteor
|
||||
sample applications. Use --list to see the available examples. There are
|
||||
currently no package examples.
|
||||
community examples. Use --list to see the available examples.
|
||||
|
||||
Options:
|
||||
--package Create a new meteor package instead of an app.
|
||||
--example Example template to use.
|
||||
--from Clones a meteor project from a url.
|
||||
--list Show list of available examples.
|
||||
--bare Create an empty app.
|
||||
--minimal Create an app with as few Meteor packages as possible.
|
||||
--full Create a fully scaffolded app.
|
||||
--react Create a basic react-based app, same as default.
|
||||
--vue Create a basic vue3-based app.
|
||||
--apollo Create a basic apollo-based app.
|
||||
--svelte Create a basic svelte-based app.
|
||||
--typescript Create a basic Typescript React-based app.
|
||||
--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.
|
||||
Skeleton options:
|
||||
--bare Create an empty app.
|
||||
--minimal Create an app with as few Meteor packages as possible.
|
||||
--full Create a fully scaffolded app.
|
||||
--react Create a basic react-based app, same as default.
|
||||
--vue Create a basic vue3-based app.
|
||||
--apollo Create a basic apollo-based app.
|
||||
--svelte Create a basic svelte-based app.
|
||||
--typescript Create a basic Typescript React-based app.
|
||||
--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.
|
||||
--angular Create a basic Angular app.
|
||||
--prototype Create a prototype app with the insecure & autopublish packages. Can be used along with other app commands
|
||||
--babel Create a React app with Babel support.
|
||||
--solid Create a basic solid-based app.
|
||||
--angular Create a basic Angular app.
|
||||
--prototype Create a prototype app with the insecure & autopublish packages.
|
||||
|
||||
Example options:
|
||||
--example Create from a community example (use --list to browse).
|
||||
--list Show detailed list of available examples.
|
||||
--from Clone a Meteor project from a Git URL.
|
||||
--from-branch Branch to clone from (use with --from).
|
||||
--from-dir Extract only a subdirectory (use with --from).
|
||||
|
||||
Other options:
|
||||
--package Create a new meteor package instead of an app.
|
||||
|
||||
|
||||
>>> update
|
||||
|
||||
@@ -1335,6 +1335,18 @@ const green =
|
||||
const blue =
|
||||
(text, ...values) =>
|
||||
`\x1b[34m${ String.raw({ raw: text }, ...values) }\x1b[0m`;
|
||||
const cyan =
|
||||
(text, ...values) =>
|
||||
`\x1b[36m${ String.raw({ raw: text }, ...values) }\x1b[0m`;
|
||||
const dim =
|
||||
(text, ...values) =>
|
||||
`\x1b[2m${ String.raw({ raw: text }, ...values) }\x1b[0m`;
|
||||
const bold =
|
||||
(text, ...values) =>
|
||||
`\x1b[1m${ String.raw({ raw: text }, ...values) }\x1b[0m`;
|
||||
|
||||
const link = (url, text) =>
|
||||
`\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
|
||||
|
||||
const colors = {
|
||||
yellow,
|
||||
@@ -1342,6 +1354,10 @@ const colors = {
|
||||
purple,
|
||||
green,
|
||||
blue,
|
||||
cyan,
|
||||
dim,
|
||||
bold,
|
||||
link,
|
||||
};
|
||||
|
||||
exports.colors = colors;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"modern": true
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rspack/cli": "^1.4.8",
|
||||
"@rspack/core": "^1.4.8",
|
||||
"babel-loader": "10.0.0",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"meteor-node-stubs": "^1.2.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rspack/cli": "^1.4.8",
|
||||
"@rspack/core": "^1.4.8",
|
||||
"playwright": "1.58.0",
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# Ignore client/main.css file so that is processed by Rspack import
|
||||
client/main.css
|
||||
@@ -17,7 +17,7 @@
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rspack/cli": "^1.4.8",
|
||||
"@rspack/core": "^1.4.8",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
@@ -34,6 +34,7 @@
|
||||
"client": "client/main.js",
|
||||
"server": "server/main.js"
|
||||
},
|
||||
"modules": ["client/meteor.css"],
|
||||
"testModule": "tests/main.js"
|
||||
}
|
||||
}
|
||||
|
||||
97
tools/e2e-tests/example.test.js
Normal file
97
tools/e2e-tests/example.test.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
|
||||
import { runMeteorCommand, cleanupTempDir } from './helpers';
|
||||
|
||||
function tempApp(prefix) {
|
||||
const suffix = Math.random().toString(36).substring(2, 10);
|
||||
const appName = `meteortest-${prefix}-${suffix}`;
|
||||
return { appName, tempDir: path.join(os.tmpdir(), appName) };
|
||||
}
|
||||
|
||||
describe('Examples /', () => {
|
||||
it('meteor create --list returns available examples', async () => {
|
||||
const { processResult } = await runMeteorCommand(
|
||||
'create', ['--list'], os.tmpdir(),
|
||||
{ captureOutput: true, checkExitCode: true }
|
||||
);
|
||||
expect(processResult.outputLines.join('\n')).toMatch(/Meteor Examples/);
|
||||
});
|
||||
|
||||
it('meteor create --example creates a Meteor app', async () => {
|
||||
const { appName, tempDir } = tempApp('example');
|
||||
try {
|
||||
await runMeteorCommand(
|
||||
'create', ['--example', 'tic-tac-toe', appName], os.tmpdir(),
|
||||
{ checkExitCode: true }
|
||||
);
|
||||
expect(fs.existsSync(path.join(tempDir, '.meteor'))).toBe(true);
|
||||
} finally {
|
||||
await cleanupTempDir(tempDir);
|
||||
}
|
||||
});
|
||||
|
||||
it('meteor create --from clones an external repo', async () => {
|
||||
const { appName, tempDir } = tempApp('from');
|
||||
try {
|
||||
await runMeteorCommand(
|
||||
'create', ['--from', 'https://github.com/fredmaiaarantes/simpletasks', appName], os.tmpdir(),
|
||||
{ checkExitCode: true }
|
||||
);
|
||||
expect(fs.existsSync(path.join(tempDir, '.meteor'))).toBe(true);
|
||||
} finally {
|
||||
await cleanupTempDir(tempDir);
|
||||
}
|
||||
});
|
||||
|
||||
it('meteor create --from with --from-branch and --from-dir extracts a subdirectory', async () => {
|
||||
const { appName, tempDir } = tempApp('fromdir');
|
||||
try {
|
||||
await runMeteorCommand(
|
||||
'create', [
|
||||
'--from', 'https://github.com/meteor/examples',
|
||||
'--from-branch', 'migrate-examples',
|
||||
'--from-dir', 'parties',
|
||||
appName
|
||||
], os.tmpdir(),
|
||||
{ checkExitCode: true }
|
||||
);
|
||||
expect(fs.existsSync(path.join(tempDir, '.meteor'))).toBe(true);
|
||||
} finally {
|
||||
await cleanupTempDir(tempDir);
|
||||
}
|
||||
});
|
||||
|
||||
it('meteor create --from with --from-dir fails for non-existing directory', async () => {
|
||||
const { appName, tempDir } = tempApp('baddir');
|
||||
try {
|
||||
await expect(runMeteorCommand(
|
||||
'create', [
|
||||
'--from', 'https://github.com/meteor/examples',
|
||||
'--from-branch', 'migrate-examples',
|
||||
'--from-dir', 'this-dir-does-not-exist',
|
||||
appName
|
||||
], os.tmpdir(),
|
||||
{ captureOutput: true, checkExitCode: true }
|
||||
)).rejects.toThrow();
|
||||
} finally {
|
||||
await cleanupTempDir(tempDir);
|
||||
}
|
||||
});
|
||||
|
||||
it('meteor create --from fails for a non-Meteor repository', async () => {
|
||||
const { appName, tempDir } = tempApp('nonmeteor');
|
||||
try {
|
||||
await expect(runMeteorCommand(
|
||||
'create', [
|
||||
'--from', 'https://github.com/meteor/meteor',
|
||||
appName
|
||||
], os.tmpdir(),
|
||||
{ captureOutput: true, checkExitCode: true }
|
||||
)).rejects.toThrow();
|
||||
} finally {
|
||||
await cleanupTempDir(tempDir);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -30,4 +30,8 @@ module.exports = {
|
||||
}
|
||||
},
|
||||
maxWorkers: 1,
|
||||
reporters: [
|
||||
'default',
|
||||
'<rootDir>/summary-reporter.js',
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
// jest.setup.js
|
||||
import chalk from 'chalk';
|
||||
|
||||
const isCI = process.env.GITHUB_ACTIONS === "true";
|
||||
if (isCI) {
|
||||
jest.retryTimes(2);
|
||||
console.log('Set 2 retries on Jest level');
|
||||
}
|
||||
|
||||
// Clear NODE_ENV so meteor commands don't inherit any value from the test runner environment
|
||||
process.env.NODE_ENV = '';
|
||||
|
||||
|
||||
@@ -216,6 +216,33 @@ describe('Meteor Skeletons /', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
describe(
|
||||
"Typescript Tailwind Skeleton /",
|
||||
testMeteorSkeleton({
|
||||
skeletonName: "typescript-tailwind",
|
||||
port: 3221,
|
||||
filePaths: {
|
||||
client: "client/main.tsx",
|
||||
server: "server/main.ts",
|
||||
test: "tests/main.ts",
|
||||
},
|
||||
customAssertions: {
|
||||
afterCreate({ tempDir }) {
|
||||
if (isCI) {
|
||||
const rspackConfigPath = path.join(tempDir, "rspack.config.ts");
|
||||
// Remove the TsCheckerRspackPlugin plugin as is resource-intense, CI gets exhausted and fails
|
||||
let configContent = fs.readFileSync(rspackConfigPath, "utf8");
|
||||
configContent = configContent.replace(
|
||||
/\s*new\s+TsCheckerRspackPlugin\(\)/,
|
||||
""
|
||||
);
|
||||
fs.writeFileSync(rspackConfigPath, configContent);
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
describe(
|
||||
'Vue Skeleton /',
|
||||
testMeteorSkeleton({
|
||||
|
||||
100
tools/e2e-tests/summary-reporter.js
Normal file
100
tools/e2e-tests/summary-reporter.js
Normal file
@@ -0,0 +1,100 @@
|
||||
const chalk = require('chalk');
|
||||
|
||||
/**
|
||||
* Custom Jest reporter that prints a structured summary of all test results,
|
||||
* including detailed error logs for failures.
|
||||
*/
|
||||
class SummaryReporter {
|
||||
constructor(globalConfig) {
|
||||
this._globalConfig = globalConfig;
|
||||
}
|
||||
|
||||
onRunComplete(_contexts, results) {
|
||||
const passed = [];
|
||||
const failed = [];
|
||||
const skipped = [];
|
||||
|
||||
for (const suite of results.testResults) {
|
||||
for (const test of suite.testResults) {
|
||||
const entry = {
|
||||
name: test.fullName || test.title,
|
||||
suite: suite.testFilePath.replace(this._globalConfig.rootDir + '/', ''),
|
||||
duration: test.duration,
|
||||
status: test.status,
|
||||
};
|
||||
|
||||
if (test.status === 'passed') {
|
||||
passed.push(entry);
|
||||
} else if (test.status === 'failed') {
|
||||
entry.errors = test.failureMessages || [];
|
||||
failed.push(entry);
|
||||
} else {
|
||||
skipped.push(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._printConsole(passed, failed, skipped);
|
||||
}
|
||||
|
||||
_printConsole(passed, failed, skipped) {
|
||||
const hasFails = failed.length > 0;
|
||||
const divider = chalk.dim('═'.repeat(70));
|
||||
const thinDivider = chalk.dim('─'.repeat(70));
|
||||
|
||||
console.log('\n' + divider);
|
||||
console.log(hasFails
|
||||
? chalk.bold.red(' E2E TEST SUMMARY')
|
||||
: chalk.bold.green(' E2E TEST SUMMARY'));
|
||||
console.log(divider);
|
||||
|
||||
if (passed.length > 0) {
|
||||
console.log(chalk.green(`\n PASSED (${passed.length}):`));
|
||||
console.log(thinDivider);
|
||||
for (const t of passed) {
|
||||
const duration = t.duration ? chalk.dim(` (${(t.duration / 1000).toFixed(1)}s)`) : '';
|
||||
console.log(` ${chalk.green('✓')} ${t.name}${duration}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (skipped.length > 0 && process.env.E2E_SHOW_SKIPPED) {
|
||||
console.log(chalk.yellow(`\n SKIPPED (${skipped.length}):`));
|
||||
console.log(thinDivider);
|
||||
for (const t of skipped) {
|
||||
console.log(` ${chalk.yellow('○')} ${chalk.dim(t.name)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed.length > 0) {
|
||||
console.log(chalk.red(`\n FAILED (${failed.length}):`));
|
||||
console.log(thinDivider);
|
||||
for (const t of failed) {
|
||||
const duration = t.duration ? chalk.dim(` (${(t.duration / 1000).toFixed(1)}s)`) : '';
|
||||
console.log(`\n ${chalk.red('✕')} ${chalk.bold(t.name)}${duration}`);
|
||||
console.log(` ${chalk.dim('Suite:')} ${chalk.dim(t.suite)}`);
|
||||
for (const err of t.errors) {
|
||||
const indented = err
|
||||
.split('\n')
|
||||
.map(line => ` ${chalk.red(line)}`)
|
||||
.join('\n');
|
||||
console.log(indented);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalTime = [...passed, ...failed, ...skipped]
|
||||
.reduce((sum, t) => sum + (t.duration || 0), 0);
|
||||
|
||||
console.log('\n' + divider);
|
||||
console.log(
|
||||
` ${chalk.bold('TOTAL:')} ${passed.length + failed.length + skipped.length} ${chalk.dim('|')} ` +
|
||||
`${chalk.green('PASSED:')} ${chalk.green(passed.length)} ${chalk.dim('|')} ` +
|
||||
`${chalk.red('FAILED:')} ${chalk.red(failed.length)} ${chalk.dim('|')} ` +
|
||||
`${chalk.yellow('SKIPPED:')} ${chalk.yellow(skipped.length)} ${chalk.dim('|')} ` +
|
||||
`${chalk.dim('TIME:')} ${chalk.dim((totalTime / 1000).toFixed(1) + 's')}`
|
||||
);
|
||||
console.log(divider + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SummaryReporter;
|
||||
@@ -91,7 +91,7 @@ export class CssFile extends InputFile {
|
||||
const filePath = convertToPosixPath(path);
|
||||
|
||||
const hash = optimisticHashOrNull(filePath);
|
||||
const contents = optimisticReadFile(filePath);
|
||||
const contents = hash !== null ? optimisticReadFile(filePath) : null;
|
||||
this._watchSet.addFile(filePath, hash);
|
||||
|
||||
return {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler-cli": "^20.0.0",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@nx/angular-rspack": "^21.1.0",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"devDependencies": {
|
||||
"@graphql-tools/webpack-loader": "^7.0.0",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
"@rspack/plugin-react-refresh": "^1.4.3",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.28.3",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"meteor-node-stubs": "^1.2.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"meteor-node-stubs": "^1.2.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"meteor-node-stubs": "^1.2.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"react-dom": "^17.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
|
||||
7
tools/static-assets/skel-typescript-tailwind/.gitignore
vendored
Normal file
7
tools/static-assets/skel-typescript-tailwind/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
|
||||
# Meteor Modern-Tools build context directories
|
||||
_build
|
||||
*/build-assets
|
||||
*/build-chunks
|
||||
.rsdoctor
|
||||
1
tools/static-assets/skel-typescript-tailwind/.meteor/.gitignore
vendored
Normal file
1
tools/static-assets/skel-typescript-tailwind/.meteor/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
local
|
||||
@@ -0,0 +1,26 @@
|
||||
# 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
|
||||
|
||||
~prototype~
|
||||
static-html # Define static page content in .html files
|
||||
react-meteor-data # React higher-order component for reactively tracking Meteor data
|
||||
|
||||
rspack # Integrate Rspack into Meteor for client and server app bundling
|
||||
|
||||
zodern:types # Pull in type declarations from other Meteor packages
|
||||
@@ -0,0 +1,2 @@
|
||||
server
|
||||
browser
|
||||
@@ -0,0 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
body {
|
||||
padding: 10px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<head>
|
||||
<title>~name~</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="bg-gray-100" id="react-target"></div>
|
||||
</body>
|
||||
11
tools/static-assets/skel-typescript-tailwind/client/main.tsx
Normal file
11
tools/static-assets/skel-typescript-tailwind/client/main.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from '/imports/ui/App';
|
||||
import './main.css';
|
||||
|
||||
Meteor.startup(() => {
|
||||
const target = document.getElementById('react-target');
|
||||
if (target) {
|
||||
createRoot(target).render(<App/>);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Mongo } from 'meteor/mongo';
|
||||
|
||||
export interface Link {
|
||||
_id?: string;
|
||||
title: string;
|
||||
url: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export const LinksCollection = new Mongo.Collection<Link>('links');
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Hello } from './Hello';
|
||||
import { Info } from './Info';
|
||||
|
||||
export const App = () => (
|
||||
<div className="max-w-3xl min-h-screen mx-auto sm:pt-10">
|
||||
<Hello/>
|
||||
<Info/>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export const Hello = () => {
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
||||
const increment = () => {
|
||||
setCounter(counter + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white shadow sm:rounded-lg mb-4">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<div className="sm:flex sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<h1 className="text-3xl text-gray-900 font-bold">
|
||||
Welcome to Meteor!
|
||||
</h1>
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50" viewBox="0 0 400 400"><g fill="#DE4F4F"><path d="M286.575 306.886L44.755 49.922l256.962 241.82c4.312 4.056 4.518 10.837.46 15.146-4.053 4.31-10.832 4.518-15.144.46-.15-.14-.318-.31-.458-.462M251.032 325.01L68.692 127.528 266.177 309.87c4.35 4.013 4.618 10.794.604 15.144-4.018 4.35-10.794 4.617-15.146.604-.2-.19-.413-.406-.602-.607M214.083 325.542L92.907 194.272 224.18 315.446c2.898 2.676 3.077 7.197.402 10.098-2.677 2.896-7.195 3.082-10.097.402-.136-.125-.277-.272-.402-.405M315.612 234.685L189.102 98.078 325.71 224.585c2.896 2.684 3.067 7.203.387 10.1-2.682 2.895-7.2 3.066-10.098.387-.13-.123-.268-.258-.388-.387M304.697 272.93L121.567 74.655l198.274 183.13c4.35 4.017 4.62 10.796.605 15.144-4.017 4.352-10.797 4.617-15.146.604-.205-.19-.418-.404-.603-.605M176.31 314.783l-57.647-62.695 62.692 57.65c1.453 1.334 1.547 3.596.215 5.045-1.338 1.453-3.598 1.55-5.05.215-.072-.07-.144-.143-.21-.215M311.093 189.297l-57.65-62.694 62.696 57.646c1.45 1.335 1.546 3.597.21 5.048-1.335 1.45-3.595 1.547-5.05.21-.07-.065-.143-.143-.207-.21"/></g></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 max-w-xl text-gray-500 text-lg">
|
||||
<p>
|
||||
You've pressed the button <b>{counter}</b> times.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-0 sm:ml-6 sm:shrink-0 sm:flex sm:items-center">
|
||||
<button
|
||||
onClick={increment}
|
||||
type="button"
|
||||
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 text-lg cursor-pointer"
|
||||
>
|
||||
Click Me
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useFind, useSubscribe } from "meteor/react-meteor-data/suspense";
|
||||
import { LinksCollection } from "../api/links";
|
||||
|
||||
export const Info = () => {
|
||||
useSubscribe("links");
|
||||
const data = useFind(LinksCollection, []);
|
||||
|
||||
return (
|
||||
<div className="rounded-lg bg-gray-200 overflow-hidden shadow divide-y divide-gray-200 sm:divide-y-0 sm:grid sm:grid-cols-2 sm:gap-px">
|
||||
{data.map((link) => (
|
||||
<div
|
||||
key={link._id}
|
||||
className="relative group bg-white p-6 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-500"
|
||||
>
|
||||
<div>
|
||||
<span className="bg-indigo-50 text-indigo-700 rounded-lg inline-flex p-3 ring-4 ring-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<h3 className="text-lg font-medium">
|
||||
<a href={link.url} target="_blank" className="focus:outline-none">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{link.title}
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
47
tools/static-assets/skel-typescript-tailwind/package.json
Normal file
47
tools/static-assets/skel-typescript-tailwind/package.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"name": "~name~",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "meteor lint && 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",
|
||||
"generate-types": "meteor lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.5",
|
||||
"@swc/helpers": "^0.5.17",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"meteor-node-stubs": "^1.2.12",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.0.1",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
"@swc/core": "^1.15.18",
|
||||
"@rspack/plugin-react-refresh": "^1.4.3",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@types/meteor": "^2.9.9",
|
||||
"@types/mocha": "^8.2.3",
|
||||
"@types/node": "^22.10.6",
|
||||
"@types/react": "^18.2.5",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"react-refresh": "^0.17.0",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"ts-checker-rspack-plugin": "^1.1.5",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"meteor": {
|
||||
"mainModule": {
|
||||
"client": "client/main.tsx",
|
||||
"server": "server/main.ts"
|
||||
},
|
||||
"testModule": "tests/main.ts",
|
||||
"modern": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { defineConfig } from "@meteorjs/rspack";
|
||||
import { TsCheckerRspackPlugin } from "ts-checker-rspack-plugin";
|
||||
|
||||
/**
|
||||
* Rspack configuration for Meteor projects.
|
||||
*
|
||||
* Provides typed flags on the `Meteor` object, such as:
|
||||
* - `Meteor.isClient` / `Meteor.isServer`
|
||||
* - `Meteor.isDevelopment` / `Meteor.isProduction`
|
||||
* - …and other flags available
|
||||
*
|
||||
* Use these flags to adjust your build settings based on environment.
|
||||
*/
|
||||
export default defineConfig(Meteor => {
|
||||
return {
|
||||
...Meteor.isClient && {
|
||||
plugins: [
|
||||
...(!Meteor.isTest && !Meteor.isAppTest ? [new TsCheckerRspackPlugin()] : []),
|
||||
],
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ["postcss-loader"],
|
||||
type: "css",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
37
tools/static-assets/skel-typescript-tailwind/server/main.ts
Normal file
37
tools/static-assets/skel-typescript-tailwind/server/main.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { Link, LinksCollection } from '/imports/api/links';
|
||||
|
||||
async function insertLink({ title, url }: Pick<Link, '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: 'http://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();
|
||||
});
|
||||
});
|
||||
13
tools/static-assets/skel-typescript-tailwind/swc.config.ts
Normal file
13
tools/static-assets/skel-typescript-tailwind/swc.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Config } from "@swc/core";
|
||||
|
||||
const config: Config = {
|
||||
jsc: {
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
21
tools/static-assets/skel-typescript-tailwind/tests/main.ts
Normal file
21
tools/static-assets/skel-typescript-tailwind/tests/main.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import assert from "assert";
|
||||
import { Meteor } from "meteor/meteor";
|
||||
|
||||
describe("~name~", function () {
|
||||
it("package.json has correct name", async function () {
|
||||
const { name } = await import("../package.json");
|
||||
assert.strictEqual(name, "~name~");
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
26
tools/static-assets/skel-typescript-tailwind/tsconfig.json
Normal file
26
tools/static-assets/skel-typescript-tailwind/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react-jsx",
|
||||
"strict": false,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"/*": ["./*"],
|
||||
"meteor/react-meteor-data/suspense": [
|
||||
".meteor/local/types/node_modules/package-types/react-meteor-data/package/os/suspense/react-meteor-data.d.ts"
|
||||
],
|
||||
"meteor/*": [
|
||||
"node_modules/@types/meteor/*",
|
||||
".meteor/local/types/packages.d.ts"
|
||||
]
|
||||
},
|
||||
"types": ["meteor", "mocha"]
|
||||
},
|
||||
"exclude": ["node_modules", ".meteor"]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"jsc": {
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,11 @@
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
"@swc/core": "^1.15.18",
|
||||
"@rspack/plugin-react-refresh": "^1.4.3",
|
||||
"@types/meteor": "^2.9.9",
|
||||
"@types/mocha": "^8.2.3",
|
||||
|
||||
13
tools/static-assets/skel-typescript/swc.config.ts
Normal file
13
tools/static-assets/skel-typescript/swc.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Config } from "@swc/core";
|
||||
|
||||
const config: Config = {
|
||||
jsc: {
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -17,7 +17,7 @@
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@meteorjs/rspack": "^1.1.0-beta.31",
|
||||
"@meteorjs/rspack": "^1.1.0-beta.33",
|
||||
"@rsdoctor/rspack-plugin": "^1.2.3",
|
||||
"@rspack/cli": "^1.7.1",
|
||||
"@rspack/core": "^1.7.1",
|
||||
|
||||
@@ -45,7 +45,7 @@ selftest.define("create main", async function () {
|
||||
await run.stop();
|
||||
|
||||
run = s.run("create", "--list");
|
||||
await run.read('Available');
|
||||
await run.match('Meteor Examples');
|
||||
await run.match('react');
|
||||
await run.expectExit(0);
|
||||
});
|
||||
|
||||
@@ -25,8 +25,8 @@ selftest.define("help", async function () {
|
||||
var checkCommandHelp = async function (run) {
|
||||
await run.read("Usage: meteor create");
|
||||
await run.match("create a new Meteor app");
|
||||
await run.match("Options:");
|
||||
await run.match(/--list\s*Show list/);
|
||||
await run.match("Skeleton options:");
|
||||
await run.match(/--list\s/);
|
||||
await run.expectExit(0);
|
||||
};
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ Ensure your app defines these entry files with the correct paths where each modu
|
||||
|
||||
Defining entry points improves performance even with the Meteor bundler, as Meteor stops scanning and eagerly loading unnecessary files. For Meteor-Rspack integration, this is required, since it does not support automatic code discovery for efficiency.
|
||||
|
||||
In Meteor-Rspack integration, all app code is ignored by Meteor and handled by Rspack. By default, Meteor still processes eagerly CSS and HTML files in the entry folder (e.g. `client/*.[html|css]` in most apps).
|
||||
In Meteor-Rspack integration, all app code is ignored by Meteor and handled by Rspack. By default, Meteor still processes eagerly HTML files in the entry folder (e.g. `client/*.html` in most apps). CSS files in the entry folder are automatically delegated to Rspack when a CSS loader is configured, see [CSS](#css) for details. If no CSS loader is present, Meteor handles them as before.
|
||||
|
||||
If you need Meteor to handle CSS or HTML files outside the main entry folder, add them to the `modules` field. This field accepts an array of strings, each pointing to a file or folder.
|
||||
|
||||
@@ -422,6 +422,8 @@ With the Meteor–Rspack integration, `zodern:melte` no longer works. Use the of
|
||||
|
||||
Meteor-Rspack comes with built-in CSS support. You can import any CSS file into your code, and it will be processed and included in your HTML skeleton automatically. In addition, any CSS file placed in the same folder as your Meteor entry point will be processed and added as global styles without the need for explicit imports.
|
||||
|
||||
When Rspack is configured with a CSS rule, whether through `postcss-loader`, `type: "css"`, or any other CSS-handling loader, Meteor automatically detects the handled file extensions after Rspack's first compilation and stops processing those files itself. This means you do not need to manually add CSS files to `.meteorignore` or otherwise tell Meteor to skip them. The same automatic delegation applies to Less and SCSS when their respective loaders are configured. If no CSS rule is present in the rspack configuration, Meteor continues to handle stylesheets as it normally would.
|
||||
|
||||
### CSS Modules
|
||||
|
||||
[CSS Modules](https://rspack.rs/guide/tech/css#css-modules) are supported out of the box — any file named `*.module.css` is automatically scoped locally.
|
||||
|
||||
@@ -246,6 +246,7 @@ If you run `meteor create` without arguments, Meteor will launch an interactive
|
||||
Minimal # To create an app with as few Meteor packages as possible
|
||||
React # To create a basic React-based app
|
||||
Typescript # To create an app using TypeScript and React
|
||||
Typescript-tailwind # To create an app using TypeScript, React, and Tailwind
|
||||
Vue # To create a basic Vue3-based app
|
||||
Svelte # To create a basic Svelte app
|
||||
Tailwind # To create an app using React and Tailwind
|
||||
@@ -260,10 +261,7 @@ If you run `meteor create` without arguments, Meteor will launch an interactive
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--from <url>` | Clone a Meteor project from a URL |
|
||||
| `--example <name>` | Use a specific example template |
|
||||
| `--list` | Show list of available examples |
|
||||
| `--release <version>` | Specify Meteor version (e.g., `--release 2.8`) |
|
||||
| `--release <version>` | Specify Meteor version (e.g., `--release 3.4`) |
|
||||
| `--prototype` | Include `autopublish` and `insecure` packages for rapid prototyping (not for production) |
|
||||
|
||||
### Application Types
|
||||
@@ -278,6 +276,7 @@ If you run `meteor create` without arguments, Meteor will launch an interactive
|
||||
| `--apollo` | React + Apollo (GraphQL) | [Meteor 2 with GraphQL](https://react-tutorial.meteor.com/simple-todos-graphql/) |
|
||||
| `--typescript` | React + TypeScript | [TypeScript Guide](/about/build-tool#typescript) |
|
||||
| `--tailwind` | React + Tailwind CSS | - |
|
||||
| `--typescript-tailwind` | React + TypeScript + Tailwind CSS | - |
|
||||
| `--chakra-ui` | React + Chakra UI | [Simple Tasks Example](https://github.com/fredmaiaarantes/simpletasks) |
|
||||
| `--coffeescript` | CoffeeScript | - |
|
||||
| `--babel` | React with Babel support | - |
|
||||
@@ -361,6 +360,47 @@ The `--prototype` option adds packages that make development faster but shouldn'
|
||||
To learn more about the recommended file structure for Meteor apps, check the [Meteor Guide](/tutorials/application-structure/#javascript-structure).
|
||||
:::
|
||||
|
||||
### Community Examples
|
||||
|
||||
Meteor ships with a collection of example apps that cover specific use cases, great for studying how features work in practice and drawing inspiration from more complete codebases. Official examples live in the [meteor/examples](https://github.com/meteor/examples) repository, while community-contributed ones link to their own repos.
|
||||
|
||||
To browse available examples with descriptions, tech stack, demo links, and repository URLs:
|
||||
|
||||
```bash
|
||||
meteor create --list
|
||||
```
|
||||
|
||||
To create a new app from an example:
|
||||
|
||||
```bash
|
||||
meteor create my-app --example simple-tasks
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--example <slug>` | Create from a community example |
|
||||
| `--list` | Show detailed list of available examples |
|
||||
|
||||
### Create from a Git Repository
|
||||
|
||||
You can create a new Meteor app by cloning any Git repository:
|
||||
|
||||
```bash
|
||||
meteor create my-app --from https://github.com/fredmaiaarantes/simpletasks
|
||||
```
|
||||
|
||||
To extract a specific subdirectory from a repository, use `--from-dir`. You can also specify a branch with `--from-branch`:
|
||||
|
||||
```bash
|
||||
meteor create my-app --from https://github.com/meteor/examples --from-branch migrate-examples --from-dir parties
|
||||
```
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--from <url>` | Clone a Meteor project from a Git URL |
|
||||
| `--from-branch <branch>` | Branch to clone from (use with `--from`) |
|
||||
| `--from-dir <dir>` | Extract only a subdirectory (use with `--from`) |
|
||||
|
||||
## meteor generate {meteorgenerate}
|
||||
|
||||
``meteor generate`` is a command to generate boilerplate for your current project. `meteor generate` receives a name as a parameter, and generates files containing code to create a [Collection](https://docs.meteor.com/api/collections.html) with that name, [Methods](https://docs.meteor.com/api/meteor.html#methods) to perform basic CRUD operations on that Collection, and a [Subscription](https://docs.meteor.com/api/meteor.html#Meteor-publish) to read its data with reactivity from the client.
|
||||
|
||||
Reference in New Issue
Block a user