mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
200 lines
5.6 KiB
JavaScript
200 lines
5.6 KiB
JavaScript
var files = require('../fs/files');
|
|
var httpHelpers = require('../utils/http-helpers.js');
|
|
var Console = require('../console/console.js').Console;
|
|
const { exec } = 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() {
|
|
return files.pathJoin(files.getHomeDir(), '.meteor', '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) {
|
|
const cachePath = getCachePath();
|
|
files.writeFile(cachePath, JSON.stringify(data, null, 2), 'utf8');
|
|
}
|
|
|
|
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) {
|
|
// 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 } = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
exec('git --version', (error) => {
|
|
if (error) {
|
|
reject(new Error('git is not installed'));
|
|
return;
|
|
}
|
|
|
|
process.env.GIT_TERMINAL_PROMPT = 0;
|
|
|
|
const isWindows = process.platform === 'win32';
|
|
const dest = isWindows
|
|
? `"${files.convertToOSPath(destPath)}"`
|
|
: destPath;
|
|
const branchArg = branch ? `--branch ${branch} ` : '';
|
|
const command = `git clone --progress ${branchArg}${url} ${dest}`;
|
|
|
|
exec(command, { env: process.env }, async (cloneError) => {
|
|
if (cloneError) {
|
|
// git clone writes progress to stderr, so only reject on real errors
|
|
// "Cloning into" on stderr is normal git output, not an error
|
|
const msg = cloneError.message || '';
|
|
if (!msg.includes('Cloning into')) {
|
|
reject(new Error(`Failed to clone ${url}: ${msg}`));
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Remove .git folder from the cloned repo
|
|
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 branchArg = branch ? `--branch ${branch} ` : '';
|
|
const command = `git clone --progress ${branchArg}${repoUrl} ${tempDir}`;
|
|
|
|
process.env.GIT_TERMINAL_PROMPT = 0;
|
|
|
|
await new Promise((resolve, reject) => {
|
|
exec(command, { env: process.env }, (error) => {
|
|
if (error) {
|
|
const msg = error.message || '';
|
|
if (!msg.includes('Cloning into')) {
|
|
reject(new Error(`Failed to clone ${repoUrl}: ${msg}`));
|
|
return;
|
|
}
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
const subdirPath = files.pathJoin(tempDir, subdir);
|
|
if (!files.exists(subdirPath)) {
|
|
throw new Error(
|
|
`Directory '${subdir}' not found in the repository. The examples list may be outdated — try 'meteor create --list --refresh'.`
|
|
);
|
|
}
|
|
|
|
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
|
|
};
|