feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,472 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const assert = require('assert');
const intersection = require('./utils/intersection');
const recast = require('recast');
const union = require('./utils/union');
const astTypes = recast.types;
var types = astTypes.namedTypes;
const NodePath = astTypes.NodePath;
const Node = types.Node;
/**
* This represents a generic collection of node paths. It only has a generic
* API to access and process the elements of the list. It doesn't know anything
* about AST types.
*
* @mixes traversalMethods
* @mixes mutationMethods
* @mixes transformMethods
* @mixes globalMethods
*/
class Collection {
/**
* @param {Array} paths An array of AST paths
* @param {Collection} parent A parent collection
* @param {Array} types An array of types all the paths in the collection
* have in common. If not passed, it will be inferred from the paths.
* @return {Collection}
*/
constructor(paths, parent, types) {
assert.ok(Array.isArray(paths), 'Collection is passed an array');
assert.ok(
paths.every(p => p instanceof NodePath),
'Array contains only paths'
);
this._parent = parent;
this.__paths = paths;
if (types && !Array.isArray(types)) {
types = _toTypeArray(types);
} else if (!types || Array.isArray(types) && types.length === 0) {
types = _inferTypes(paths);
}
this._types = types.length === 0 ? _defaultType : types;
}
/**
* Returns a new collection containing the nodes for which the callback
* returns true.
*
* @param {function} callback
* @return {Collection}
*/
filter(callback) {
return new this.constructor(this.__paths.filter(callback), this);
}
/**
* Executes callback for each node/path in the collection.
*
* @param {function} callback
* @return {Collection} The collection itself
*/
forEach(callback) {
this.__paths.forEach(
(path, i, paths) => callback.call(path, path, i, paths)
);
return this;
}
/**
* Tests whether at-least one path passes the test implemented by the provided callback.
*
* @param {function} callback
* @return {boolean}
*/
some(callback) {
return this.__paths.some(
(path, i, paths) => callback.call(path, path, i, paths)
);
}
/**
* Tests whether all paths pass the test implemented by the provided callback.
*
* @param {function} callback
* @return {boolean}
*/
every(callback) {
return this.__paths.every(
(path, i, paths) => callback.call(path, path, i, paths)
);
}
/**
* Executes the callback for every path in the collection and returns a new
* collection from the return values (which must be paths).
*
* The callback can return null to indicate to exclude the element from the
* new collection.
*
* If an array is returned, the array will be flattened into the result
* collection.
*
* @param {function} callback
* @param {Type} type Force the new collection to be of a specific type
*/
map(callback, type) {
const paths = [];
this.forEach(function(path) {
/*jshint eqnull:true*/
let result = callback.apply(path, arguments);
if (result == null) return;
if (!Array.isArray(result)) {
result = [result];
}
for (let i = 0; i < result.length; i++) {
if (paths.indexOf(result[i]) === -1) {
paths.push(result[i]);
}
}
});
return fromPaths(paths, this, type);
}
/**
* Returns the number of elements in this collection.
*
* @return {number}
*/
size() {
return this.__paths.length;
}
/**
* Returns the number of elements in this collection.
*
* @return {number}
*/
get length() {
return this.__paths.length;
}
/**
* Returns an array of AST nodes in this collection.
*
* @return {Array}
*/
nodes() {
return this.__paths.map(p => p.value);
}
paths() {
return this.__paths;
}
getAST() {
if (this._parent) {
return this._parent.getAST();
}
return this.__paths;
}
toSource(options) {
if (this._parent) {
return this._parent.toSource(options);
}
if (this.__paths.length === 1) {
return recast.print(this.__paths[0], options).code;
} else {
return this.__paths.map(p => recast.print(p, options).code);
}
}
/**
* Returns a new collection containing only the element at position index.
*
* In case of a negative index, the element is taken from the end:
*
* .at(0) - first element
* .at(-1) - last element
*
* @param {number} index
* @return {Collection}
*/
at(index) {
return fromPaths(
this.__paths.slice(
index,
index === -1 ? undefined : index + 1
),
this
);
}
/**
* Proxies to NodePath#get of the first path.
*
* @param {string|number} ...fields
*/
get() {
const path = this.__paths[0];
if (!path) {
throw Error(
'You cannot call "get" on a collection with no paths. ' +
'Instead, check the "length" property first to verify at least 1 path exists.'
);
}
return path.get.apply(path, arguments);
}
/**
* Returns the type(s) of the collection. This is only used for unit tests,
* I don't think other consumers would need it.
*
* @return {Array<string>}
*/
getTypes() {
return this._types;
}
/**
* Returns true if this collection has the type 'type'.
*
* @param {Type} type
* @return {boolean}
*/
isOfType(type) {
return !!type && this._types.indexOf(type.toString()) > -1;
}
}
/**
* Given a set of paths, this infers the common types of all paths.
* @private
* @param {Array} paths An array of paths.
* @return {Type} type An AST type
*/
function _inferTypes(paths) {
let _types = [];
if (paths.length > 0 && Node.check(paths[0].node)) {
const nodeType = types[paths[0].node.type];
const sameType = paths.length === 1 ||
paths.every(path => nodeType.check(path.node));
if (sameType) {
_types = [nodeType.toString()].concat(
astTypes.getSupertypeNames(nodeType.toString())
);
} else {
// try to find a common type
_types = intersection(
paths.map(path => astTypes.getSupertypeNames(path.node.type))
);
}
}
return _types;
}
function _toTypeArray(value) {
value = !Array.isArray(value) ? [value] : value;
value = value.map(v => v.toString());
if (value.length > 1) {
return union(
[value].concat(intersection(value.map(_getSupertypeNames)))
);
} else {
return value.concat(_getSupertypeNames(value[0]));
}
}
function _getSupertypeNames(type) {
try {
return astTypes.getSupertypeNames(type);
} catch(error) {
if (error.message === '') {
// Likely the case that the passed type wasn't found in the definition
// list. Maybe a typo. ast-types doesn't throw a useful error in that
// case :(
throw new Error(
'"' + type + '" is not a known AST node type. Maybe a typo?'
);
}
throw error;
}
}
/**
* Creates a new collection from an array of node paths.
*
* If type is passed, it will create a typed collection if such a collection
* exists. The nodes or path values must be of the same type.
*
* Otherwise it will try to infer the type from the path list. If every
* element has the same type, a typed collection is created (if it exists),
* otherwise, a generic collection will be created.
*
* @ignore
* @param {Array} paths An array of paths
* @param {Collection} parent A parent collection
* @param {Type} type An AST type
* @return {Collection}
*/
function fromPaths(paths, parent, type) {
assert.ok(
paths.every(n => n instanceof NodePath),
'Every element in the array should be a NodePath'
);
return new Collection(paths, parent, type);
}
/**
* Creates a new collection from an array of nodes. This is a convenience
* method which converts the nodes to node paths first and calls
*
* Collections.fromPaths(paths, parent, type)
*
* @ignore
* @param {Array} nodes An array of AST nodes
* @param {Collection} parent A parent collection
* @param {Type} type An AST type
* @return {Collection}
*/
function fromNodes(nodes, parent, type) {
assert.ok(
nodes.every(n => Node.check(n)),
'Every element in the array should be a Node'
);
return fromPaths(
nodes.map(n => new NodePath(n)),
parent,
type
);
}
const CPt = Collection.prototype;
/**
* This function adds the provided methods to the prototype of the corresponding
* typed collection. If no type is passed, the methods are added to
* Collection.prototype and are available for all collections.
*
* @param {Object} methods Methods to add to the prototype
* @param {Type=} type Optional type to add the methods to
*/
function registerMethods(methods, type) {
for (const methodName in methods) {
if (!methods.hasOwnProperty(methodName)) {
return;
}
if (hasConflictingRegistration(methodName, type)) {
let msg = `There is a conflicting registration for method with name "${methodName}".\nYou tried to register an additional method with `;
if (type) {
msg += `type "${type.toString()}".`
} else {
msg += 'universal type.'
}
msg += '\nThere are existing registrations for that method with ';
const conflictingRegistrations = CPt[methodName].typedRegistrations;
if (conflictingRegistrations) {
msg += `type ${Object.keys(conflictingRegistrations).join(', ')}.`;
} else {
msg += 'universal type.';
}
throw Error(msg);
}
if (!type) {
CPt[methodName] = methods[methodName];
} else {
type = type.toString();
if (!CPt.hasOwnProperty(methodName)) {
installTypedMethod(methodName);
}
var registrations = CPt[methodName].typedRegistrations;
registrations[type] = methods[methodName];
astTypes.getSupertypeNames(type).forEach(function (name) {
registrations[name] = false;
});
}
}
}
function installTypedMethod(methodName) {
if (CPt.hasOwnProperty(methodName)) {
throw new Error(`Internal Error: "${methodName}" method is already installed`);
}
const registrations = {};
function typedMethod() {
const types = Object.keys(registrations);
for (let i = 0; i < types.length; i++) {
const currentType = types[i];
if (registrations[currentType] && this.isOfType(currentType)) {
return registrations[currentType].apply(this, arguments);
}
}
throw Error(
`You have a collection of type [${this.getTypes()}]. ` +
`"${methodName}" is only defined for one of [${types.join('|')}].`
);
}
typedMethod.typedRegistrations = registrations;
CPt[methodName] = typedMethod;
}
function hasConflictingRegistration(methodName, type) {
if (!type) {
return CPt.hasOwnProperty(methodName);
}
if (!CPt.hasOwnProperty(methodName)) {
return false;
}
const registrations = CPt[methodName] && CPt[methodName].typedRegistrations;
if (!registrations) {
return true;
}
type = type.toString();
if (registrations.hasOwnProperty(type)) {
return true;
}
return astTypes.getSupertypeNames(type.toString()).some(function (name) {
return !!registrations[name];
});
}
var _defaultType = [];
/**
* Sets the default collection type. In case a collection is created form an
* empty set of paths and no type is specified, we return a collection of this
* type.
*
* @ignore
* @param {Type} type
*/
function setDefaultCollectionType(type) {
_defaultType = _toTypeArray(type);
}
exports.fromPaths = fromPaths;
exports.fromNodes = fromNodes;
exports.registerMethods = registerMethods;
exports.hasConflictingRegistration = hasConflictingRegistration;
exports.setDefaultCollectionType = setDefaultCollectionType;

View File

@@ -0,0 +1,318 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const child_process = require('child_process');
const chalk = require('chalk');
const fs = require('graceful-fs');
const path = require('path');
const http = require('http');
const https = require('https');
const temp = require('temp');
const ignores = require('./ignoreFiles');
const availableCpus = Math.max(require('os').cpus().length - 1, 1);
const CHUNK_SIZE = 50;
function lineBreak(str) {
return /\n$/.test(str) ? str : str + '\n';
}
const bufferedWrite = (function() {
const buffer = [];
let buffering = false;
process.stdout.on('drain', () => {
if (!buffering) return;
while (buffer.length > 0 && process.stdout.write(buffer.shift()) !== false);
if (buffer.length === 0) {
buffering = false;
}
});
return function write(msg) {
if (buffering) {
buffer.push(msg);
}
if (process.stdout.write(msg) === false) {
buffering = true;
}
};
}());
const log = {
ok(msg, verbose) {
verbose >= 2 && bufferedWrite(chalk.white.bgGreen(' OKK ') + msg);
},
nochange(msg, verbose) {
verbose >= 1 && bufferedWrite(chalk.white.bgYellow(' NOC ') + msg);
},
skip(msg, verbose) {
verbose >= 1 && bufferedWrite(chalk.white.bgYellow(' SKIP ') + msg);
},
error(msg, verbose) {
verbose >= 0 && bufferedWrite(chalk.white.bgRed(' ERR ') + msg);
},
};
function report({file, msg}) {
bufferedWrite(lineBreak(`${chalk.white.bgBlue(' REP ')}${file} ${msg}`));
}
function concatAll(arrays) {
const result = [];
for (const array of arrays) {
for (const element of array) {
result.push(element);
}
}
return result;
}
function showFileStats(fileStats) {
process.stdout.write(
'Results: \n'+
chalk.red(fileStats.error + ' errors\n')+
chalk.yellow(fileStats.nochange + ' unmodified\n')+
chalk.yellow(fileStats.skip + ' skipped\n')+
chalk.green(fileStats.ok + ' ok\n')
);
}
function showStats(stats) {
const names = Object.keys(stats).sort();
if (names.length) {
process.stdout.write(chalk.blue('Stats: \n'));
}
names.forEach(name => process.stdout.write(name + ': ' + stats[name] + '\n'));
}
function dirFiles (dir, callback, acc) {
// acc stores files found so far and counts remaining paths to be processed
acc = acc || { files: [], remaining: 1 };
function done() {
// decrement count and return if there are no more paths left to process
if (!--acc.remaining) {
callback(acc.files);
}
}
fs.readdir(dir, (err, files) => {
// if dir does not exist or is not a directory, bail
// (this should not happen as long as calls do the necessary checks)
if (err) throw err;
acc.remaining += files.length;
files.forEach(file => {
let name = path.join(dir, file);
fs.stat(name, (err, stats) => {
if (err) {
// probably a symlink issue
process.stdout.write(
'Skipping path "' + name + '" which does not exist.\n'
);
done();
} else if (ignores.shouldIgnore(name)) {
// ignore the path
done();
} else if (stats.isDirectory()) {
dirFiles(name + '/', callback, acc);
} else {
acc.files.push(name);
done();
}
});
});
done();
});
}
function getAllFiles(paths, filter) {
return Promise.all(
paths.map(file => new Promise(resolve => {
fs.lstat(file, (err, stat) => {
if (err) {
process.stderr.write('Skipping path ' + file + ' which does not exist. \n');
resolve([]);
return;
}
if (stat.isDirectory()) {
dirFiles(
file,
list => resolve(list.filter(filter))
);
} else if (ignores.shouldIgnore(file)) {
// ignoring the file
resolve([]);
} else {
resolve([file]);
}
})
}))
).then(concatAll);
}
function run(transformFile, paths, options) {
let usedRemoteScript = false;
const cpus = options.cpus ? Math.min(availableCpus, options.cpus) : availableCpus;
const extensions =
options.extensions && options.extensions.split(',').map(ext => '.' + ext);
const fileCounters = {error: 0, ok: 0, nochange: 0, skip: 0};
const statsCounter = {};
const startTime = process.hrtime();
ignores.add(options.ignorePattern);
ignores.addFromFile(options.ignoreConfig);
if (/^http/.test(transformFile)) {
usedRemoteScript = true;
return new Promise((resolve, reject) => {
// call the correct `http` or `https` implementation
(transformFile.indexOf('https') !== 0 ? http : https).get(transformFile, (res) => {
let contents = '';
res
.on('data', (d) => {
contents += d.toString();
})
.on('end', () => {
const ext = path.extname(transformFile);
temp.open({ prefix: 'jscodeshift', suffix: ext }, (err, info) => {
if (err) return reject(err);
fs.write(info.fd, contents, function (err) {
if (err) return reject(err);
fs.close(info.fd, function(err) {
if (err) return reject(err);
transform(info.path).then(resolve, reject);
});
});
});
})
})
.on('error', (e) => {
reject(e);
});
});
} else if (!fs.existsSync(transformFile)) {
process.stderr.write(
chalk.white.bgRed('ERROR') + ' Transform file ' + transformFile + ' does not exist \n'
);
return;
} else {
return transform(transformFile);
}
function transform(transformFile) {
return getAllFiles(
paths,
name => !extensions || extensions.indexOf(path.extname(name)) != -1
).then(files => {
const numFiles = files.length;
if (numFiles === 0) {
process.stdout.write('No files selected, nothing to do. \n');
return [];
}
const processes = options.runInBand ? 1 : Math.min(numFiles, cpus);
const chunkSize = processes > 1 ?
Math.min(Math.ceil(numFiles / processes), CHUNK_SIZE) :
numFiles;
let index = 0;
// return the next chunk of work for a free worker
function next() {
if (!options.silent && !options.runInBand && index < numFiles) {
process.stdout.write(
'Sending ' +
Math.min(chunkSize, numFiles-index) +
' files to free worker...\n'
);
}
return files.slice(index, index += chunkSize);
}
if (!options.silent) {
process.stdout.write('Processing ' + files.length + ' files... \n');
if (!options.runInBand) {
process.stdout.write(
'Spawning ' + processes +' workers...\n'
);
}
if (options.dry) {
process.stdout.write(
chalk.green('Running in dry mode, no files will be written! \n')
);
}
}
const args = [transformFile, options.babel ? 'babel' : 'no-babel'];
const workers = [];
for (let i = 0; i < processes; i++) {
workers.push(options.runInBand ?
require('./Worker')(args) :
child_process.fork(require.resolve('./Worker'), args)
);
}
return workers.map(child => {
child.send({files: next(), options});
child.on('message', message => {
switch (message.action) {
case 'status':
fileCounters[message.status] += 1;
log[message.status](lineBreak(message.msg), options.verbose);
break;
case 'update':
if (!statsCounter[message.name]) {
statsCounter[message.name] = 0;
}
statsCounter[message.name] += message.quantity;
break;
case 'free':
child.send({files: next(), options});
break;
case 'report':
report(message);
break;
}
});
return new Promise(resolve => child.on('disconnect', resolve));
});
})
.then(pendingWorkers =>
Promise.all(pendingWorkers).then(() => {
const endTime = process.hrtime(startTime);
const timeElapsed = (endTime[0] + endTime[1]/1e9).toFixed(3);
if (!options.silent) {
process.stdout.write('All done. \n');
showFileStats(fileCounters);
showStats(statsCounter);
process.stdout.write(
'Time elapsed: ' + timeElapsed + 'seconds \n'
);
if (options.failOnError && fileCounters.error > 0) {
process.exit(1);
}
}
if (usedRemoteScript) {
temp.cleanupSync();
}
return Object.assign({
stats: statsCounter,
timeElapsed: timeElapsed
}, fileCounters);
})
);
}
}
exports.run = run;

View File

@@ -0,0 +1,208 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const EventEmitter = require('events').EventEmitter;
const async = require('neo-async');
const fs = require('graceful-fs');
const writeFileAtomic = require('write-file-atomic');
const { DEFAULT_EXTENSIONS } = require('@babel/core');
const getParser = require('./getParser');
const jscodeshift = require('./core');
let presetEnv;
try {
presetEnv = require('@babel/preset-env');
} catch (_) {}
let emitter;
let finish;
let notify;
let transform;
let parserFromTransform;
if (module.parent) {
emitter = new EventEmitter();
emitter.send = (data) => { run(data); };
finish = () => { emitter.emit('disconnect'); };
notify = (data) => { emitter.emit('message', data); };
module.exports = (args) => {
setup(args[0], args[1]);
return emitter;
};
} else {
finish = () => setImmediate(() => process.disconnect());
notify = (data) => { process.send(data); };
process.on('message', (data) => { run(data); });
setup(process.argv[2], process.argv[3]);
}
function prepareJscodeshift(options) {
const parser = parserFromTransform ||
getParser(options.parser, options.parserConfig);
return jscodeshift.withParser(parser);
}
function setup(tr, babel) {
if (babel === 'babel') {
const presets = [];
if (presetEnv) {
presets.push([
presetEnv.default,
{targets: {node: true}},
]);
}
presets.push(
/\.tsx?$/.test(tr) ?
require('@babel/preset-typescript').default :
require('@babel/preset-flow').default
);
require('@babel/register')({
babelrc: false,
presets,
plugins: [
require('@babel/plugin-proposal-class-properties').default,
require('@babel/plugin-proposal-nullish-coalescing-operator').default,
require('@babel/plugin-proposal-optional-chaining').default,
require('@babel/plugin-transform-modules-commonjs').default,
],
extensions: [...DEFAULT_EXTENSIONS, '.ts', '.tsx'],
// By default, babel register only compiles things inside the current working directory.
// https://github.com/babel/babel/blob/2a4f16236656178e84b05b8915aab9261c55782c/packages/babel-register/src/node.js#L140-L157
ignore: [
// Ignore parser related files
/@babel\/parser/,
/\/flow-parser\//,
/\/recast\//,
/\/ast-types\//,
],
});
}
const module = require(tr);
transform = typeof module.default === 'function' ?
module.default :
module;
if (module.parser) {
parserFromTransform = typeof module.parser === 'string' ?
getParser(module.parser) :
module.parser;
}
}
function free() {
notify({action: 'free'});
}
function updateStatus(status, file, msg) {
msg = msg ? file + ' ' + msg : file;
notify({action: 'status', status: status, msg: msg});
}
function report(file, msg) {
notify({action: 'report', file, msg});
}
function empty() {}
function stats(name, quantity) {
quantity = typeof quantity !== 'number' ? 1 : quantity;
notify({action: 'update', name: name, quantity: quantity});
}
function trimStackTrace(trace) {
if (!trace) {
return '';
}
// Remove this file from the stack trace of an error thrown in the transformer
const lines = trace.split('\n');
const result = [];
lines.every(function(line) {
if (line.indexOf(__filename) === -1) {
result.push(line);
return true;
}
});
return result.join('\n');
}
function run(data) {
const files = data.files;
const options = data.options || {};
if (!files.length) {
finish();
return;
}
async.each(
files,
function(file, callback) {
fs.readFile(file, async function(err, source) {
if (err) {
updateStatus('error', file, 'File error: ' + err);
callback();
return;
}
source = source.toString();
try {
const jscodeshift = prepareJscodeshift(options);
const out = await transform(
{
path: file,
source: source,
},
{
j: jscodeshift,
jscodeshift: jscodeshift,
stats: options.dry ? stats : empty,
report: msg => report(file, msg),
},
options
);
if (!out || out === source) {
updateStatus(out ? 'nochange' : 'skip', file);
callback();
return;
}
if (options.print) {
console.log(out); // eslint-disable-line no-console
}
if (!options.dry) {
writeFileAtomic(file, out, function(err) {
if (err) {
updateStatus('error', file, 'File writer error: ' + err);
} else {
updateStatus('ok', file);
}
callback();
});
} else {
updateStatus('ok', file);
callback();
}
} catch(err) {
updateStatus(
'error',
file,
'Transformation error ('+ err.message.replace(/\n/g, ' ') + ')\n' + trimStackTrace(err.stack)
);
callback();
}
});
},
function(err) {
if (err) {
updateStatus('error', '', 'This should never be shown!');
}
free();
}
);
}

View File

@@ -0,0 +1,273 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
function throwError(exitCode, message, helpText) {
const error = new Error(
helpText ? `${message}\n\n---\n\n${helpText}` : message
);
error.exitCode = exitCode;
throw error;
}
function formatOption(option) {
let text = ' ';
text += option.abbr ? `-${option.abbr}, ` : ' ';
text += `--${option.flag ? '(no-)' : ''}${option.full}`;
if (option.choices) {
text += `=${option.choices.join('|')}`;
} else if (option.metavar) {
text += `=${option.metavar}`;
}
if (option.list) {
text += ' ...';
}
if (option.defaultHelp || option.default !== undefined || option.help) {
text += ' ';
if (text.length < 32) {
text += ' '.repeat(32 - text.length);
}
const textLength = text.length;
if (option.help) {
text += option.help;
}
if (option.defaultHelp || option.default !== undefined) {
if (option.help) {
text += '\n';
}
text += `${' '.repeat(textLength)}(default: ${option.defaultHelp || option.default})`;
}
}
return text;
}
function getHelpText(options) {
const opts = Object.keys(options)
.map(k => options[k])
.sort((a,b) => a.display_index - b.display_index);
const text = `
Usage: jscodeshift [OPTION]... PATH...
or: jscodeshift [OPTION]... -t TRANSFORM_PATH PATH...
or: jscodeshift [OPTION]... -t URL PATH...
or: jscodeshift [OPTION]... --stdin < file_list.txt
Apply transform logic in TRANSFORM_PATH (recursively) to every PATH.
If --stdin is set, each line of the standard input is used as a path.
Options:
"..." behind an option means that it can be supplied multiple times.
All options are also passed to the transformer, which means you can supply custom options that are not listed here.
${opts.map(formatOption).join('\n')}
`;
return text.trimLeft();
}
function validateOptions(parsedOptions, options) {
const errors = [];
for (const optionName in options) {
const option = options[optionName];
if (option.choices && !option.choices.includes(parsedOptions[optionName])) {
errors.push(
`Error: --${option.full} must be one of the values ${option.choices.join(',')}`
);
}
}
if (errors.length > 0) {
throwError(
1,
errors.join('\n'),
getHelpText(options)
);
}
}
function prepareOptions(options) {
options.help = {
display_index: 5,
abbr: 'h',
help: 'print this help and exit',
callback() {
return getHelpText(options);
},
};
const preparedOptions = {};
for (const optionName of Object.keys(options)) {
const option = options[optionName];
if (!option.full) {
option.full = optionName;
}
option.key = optionName;
preparedOptions['--'+option.full] = option;
if (option.abbr) {
preparedOptions['-'+option.abbr] = option;
}
if (option.flag) {
preparedOptions['--no-'+option.full] = option;
}
}
return preparedOptions;
}
function isOption(value) {
return /^--?/.test(value);
}
function parse(options, args=process.argv.slice(2)) {
const missingValue = Symbol();
const preparedOptions = prepareOptions(options);
const parsedOptions = {};
const positionalArguments = [];
for (const optionName in options) {
const option = options[optionName];
if (option.default !== undefined) {
parsedOptions[optionName] = option.default;
} else if (option.list) {
parsedOptions[optionName] = [];
}
}
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (isOption(arg)) {
let optionName = arg;
let value = null;
let option = null;
if (optionName.includes('=')) {
const index = arg.indexOf('=');
optionName = arg.slice(0, index);
value = arg.slice(index+1);
}
if (preparedOptions.hasOwnProperty(optionName)) {
option = preparedOptions[optionName];
} else {
// Unknown options are just "passed along".
// The logic is as follows:
// - If an option is encountered without a value, it's treated
// as a flag
// - If the option has a value, it's initialized with that value
// - If the option has been seen before, it's converted to a list
// If the previous value was true (i.e. a flag), that value is
// discarded.
const realOptionName = optionName.replace(/^--?(no-)?/, '');
const isList = parsedOptions.hasOwnProperty(realOptionName) &&
parsedOptions[realOptionName] !== true;
option = {
key: realOptionName,
full: realOptionName,
flag: !parsedOptions.hasOwnProperty(realOptionName) &&
value === null &&
isOption(args[i+1]),
list: isList,
process(value) {
// Try to parse values as JSON to be compatible with nomnom
try {
return JSON.parse(value);
} catch(_e) {}
return value;
},
};
if (isList) {
const currentValue = parsedOptions[realOptionName];
if (!Array.isArray(currentValue)) {
parsedOptions[realOptionName] = currentValue === true ?
[] :
[currentValue];
}
}
}
if (option.callback) {
throwError(0, option.callback());
} else if (option.flag) {
if (optionName.startsWith('--no-')) {
value = false;
} else if (value !== null) {
value = value === '1';
} else {
value = true;
}
parsedOptions[option.key] = value;
} else {
if (value === null && i < args.length - 1 && !isOption(args[i+1])) {
// consume next value
value = args[i+1];
i += 1;
}
if (value !== null) {
if (option.process) {
value = option.process(value);
}
if (option.list) {
parsedOptions[option.key].push(value);
} else {
parsedOptions[option.key] = value;
}
} else {
parsedOptions[option.key] = missingValue;
}
}
} else {
positionalArguments.push(/^\d+$/.test(arg) ? Number(arg) : arg);
}
}
for (const optionName in parsedOptions) {
if (parsedOptions[optionName] === missingValue) {
throwError(
1,
`Missing value: --${options[optionName].full} requires a value`,
getHelpText(options)
);
}
}
const result = {
positionalArguments,
options: parsedOptions,
};
validateOptions(parsedOptions, options);
return result;
}
module.exports = {
/**
* `options` is an object of objects. Each option can have the following
* properties:
*
* - full: The name of the option to be used in the command line (if
* different than the property name.
* - abbr: The short version of the option, a single character
* - flag: Whether the option takes an argument or not.
* - default: The default value to use if option is not supplied
* - choices: Restrict possible values to these values
* - help: The help text to print
* - metavar: Value placeholder to use in the help
* - callback: If option is supplied, call this function and exit
* - process: Pre-process value before returning it
*/
options(options) {
return {
parse(args) {
return parse(options, args);
},
getHelpText() {
return getHelpText(options);
},
};
},
};

View File

@@ -0,0 +1,221 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const Collection = require('../Collection');
const NodeCollection = require('./Node');
const assert = require('assert');
const once = require('../utils/once');
const recast = require('recast');
const requiresModule = require('./VariableDeclarator').filters.requiresModule;
const types = recast.types.namedTypes;
const JSXElement = types.JSXElement;
const JSXAttribute = types.JSXAttribute;
const Literal = types.Literal;
/**
* Contains filter methods and mutation methods for processing JSXElements.
* @mixin
*/
const globalMethods = {
/**
* Finds all JSXElements optionally filtered by name
*
* @param {string} name
* @return {Collection}
*/
findJSXElements: function(name) {
const nameFilter = name && {openingElement: {name: {name: name}}};
return this.find(JSXElement, nameFilter);
},
/**
* Finds all JSXElements by module name. Given
*
* var Bar = require('Foo');
* <Bar />
*
* findJSXElementsByModuleName('Foo') will find <Bar />, without having to
* know the variable name.
*/
findJSXElementsByModuleName: function(moduleName) {
assert.ok(
moduleName && typeof moduleName === 'string',
'findJSXElementsByModuleName(...) needs a name to look for'
);
return this.find(types.VariableDeclarator)
.filter(requiresModule(moduleName))
.map(function(path) {
const id = path.value.id.name;
if (id) {
return Collection.fromPaths([path])
.closestScope()
.findJSXElements(id)
.paths();
}
});
}
};
const filterMethods = {
/**
* Filter method for attributes.
*
* @param {Object} attributeFilter
* @return {function}
*/
hasAttributes: function(attributeFilter) {
const attributeNames = Object.keys(attributeFilter);
return function filter(path) {
if (!JSXElement.check(path.value)) {
return false;
}
const elementAttributes = Object.create(null);
path.value.openingElement.attributes.forEach(function(attr) {
if (!JSXAttribute.check(attr) ||
!(attr.name.name in attributeFilter)) {
return;
}
elementAttributes[attr.name.name] = attr;
});
return attributeNames.every(function(name) {
if (!(name in elementAttributes) ){
return false;
}
const value = elementAttributes[name].value;
const expected = attributeFilter[name];
// Only when value is truthy access it's properties
const actual = !value
? value
: Literal.check(value)
? value.value
: value.expression;
if (typeof expected === 'function') {
return expected(actual);
}
// Literal attribute values are always strings
return String(expected) === actual;
});
};
},
/**
* Filter elements which contain a specific child type
*
* @param {string} name
* @return {function}
*/
hasChildren: function(name) {
return function filter(path) {
return JSXElement.check(path.value) &&
path.value.children.some(
child => JSXElement.check(child) &&
child.openingElement.name.name === name
);
};
}
};
/**
* @mixin
*/
const traversalMethods = {
/**
* Returns all child nodes, including literals and expressions.
*
* @return {Collection}
*/
childNodes: function() {
const paths = [];
this.forEach(function(path) {
const children = path.get('children');
const l = children.value.length;
for (let i = 0; i < l; i++) {
paths.push(children.get(i));
}
});
return Collection.fromPaths(paths, this);
},
/**
* Returns all children that are JSXElements.
*
* @return {JSXElementCollection}
*/
childElements: function() {
const paths = [];
this.forEach(function(path) {
const children = path.get('children');
const l = children.value.length;
for (let i = 0; i < l; i++) {
if (types.JSXElement.check(children.value[i])) {
paths.push(children.get(i));
}
}
});
return Collection.fromPaths(paths, this, JSXElement);
},
/**
* Returns all children that are of jsxElementType.
*
* @return {Collection<jsxElementType>}
*/
childNodesOfType: function(jsxChildElementType) {
const paths = [];
this.forEach(function(path) {
const children = path.get('children');
const l = children.value.length;
for (let i = 0; i < l; i++) {
if (jsxChildElementType.check(children.value[i])) {
paths.push(children.get(i));
}
}
});
return Collection.fromPaths(paths, this, jsxChildElementType);
},
};
const mappingMethods = {
/**
* Given a JSXElement, returns its "root" name. E.g. it would return "Foo" for
* both <Foo /> and <Foo.Bar />.
*
* @param {NodePath} path
* @return {string}
*/
getRootName: function(path) {
let name = path.value.openingElement.name;
while (types.JSXMemberExpression.check(name)) {
name = name.object;
}
return name && name.name || null;
}
};
function register() {
NodeCollection.register();
Collection.registerMethods(globalMethods, types.Node);
Collection.registerMethods(traversalMethods, JSXElement);
}
exports.register = once(register);
exports.filters = filterMethods;
exports.mappings = mappingMethods;

View File

@@ -0,0 +1,187 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const Collection = require('../Collection');
const matchNode = require('../matchNode');
const once = require('../utils/once');
const recast = require('recast');
const Node = recast.types.namedTypes.Node;
var types = recast.types.namedTypes;
/**
* @mixin
*/
const traversalMethods = {
/**
* Find nodes of a specific type within the nodes of this collection.
*
* @param {type}
* @param {filter}
* @return {Collection}
*/
find: function(type, filter) {
const paths = [];
const visitorMethodName = 'visit' + type;
const visitor = {};
function visit(path) {
/*jshint validthis:true */
if (!filter || matchNode(path.value, filter)) {
paths.push(path);
}
this.traverse(path);
}
this.__paths.forEach(function(p, i) {
const self = this;
visitor[visitorMethodName] = function(path) {
if (self.__paths[i] === path) {
this.traverse(path);
} else {
return visit.call(this, path);
}
};
recast.visit(p, visitor);
}, this);
return Collection.fromPaths(paths, this, type);
},
/**
* Returns a collection containing the paths that create the scope of the
* currently selected paths. Dedupes the paths.
*
* @return {Collection}
*/
closestScope: function() {
return this.map(path => path.scope && path.scope.path);
},
/**
* Traverse the AST up and finds the closest node of the provided type.
*
* @param {Collection}
* @param {filter}
* @return {Collection}
*/
closest: function(type, filter) {
return this.map(function(path) {
let parent = path.parent;
while (
parent &&
!(
type.check(parent.value) &&
(!filter || matchNode(parent.value, filter))
)
) {
parent = parent.parent;
}
return parent || null;
});
},
/**
* Finds the declaration for each selected path. Useful for member expressions
* or JSXElements. Expects a callback function that maps each path to the name
* to look for.
*
* If the callback returns a falsey value, the element is skipped.
*
* @param {function} nameGetter
*
* @return {Collection}
*/
getVariableDeclarators: function(nameGetter) {
return this.map(function(path) {
/*jshint curly:false*/
let scope = path.scope;
if (!scope) return;
const name = nameGetter.apply(path, arguments);
if (!name) return;
scope = scope.lookup(name);
if (!scope) return;
const bindings = scope.getBindings()[name];
if (!bindings) return;
const decl = Collection.fromPaths(bindings)
.closest(types.VariableDeclarator);
if (decl.length === 1) {
return decl.paths()[0];
}
}, types.VariableDeclarator);
},
};
function toArray(value) {
return Array.isArray(value) ? value : [value];
}
/**
* @mixin
*/
const mutationMethods = {
/**
* Simply replaces the selected nodes with the provided node. If a function
* is provided it is executed for every node and the node is replaced with the
* functions return value.
*
* @param {Node|Array<Node>|function} nodes
* @return {Collection}
*/
replaceWith: function(nodes) {
return this.forEach(function(path, i) {
const newNodes =
(typeof nodes === 'function') ? nodes.call(path, path, i) : nodes;
path.replace.apply(path, toArray(newNodes));
});
},
/**
* Inserts a new node before the current one.
*
* @param {Node|Array<Node>|function} insert
* @return {Collection}
*/
insertBefore: function(insert) {
return this.forEach(function(path, i) {
const newNodes =
(typeof insert === 'function') ? insert.call(path, path, i) : insert;
path.insertBefore.apply(path, toArray(newNodes));
});
},
/**
* Inserts a new node after the current one.
*
* @param {Node|Array<Node>|function} insert
* @return {Collection}
*/
insertAfter: function(insert) {
return this.forEach(function(path, i) {
const newNodes =
(typeof insert === 'function') ? insert.call(path, path, i) : insert;
path.insertAfter.apply(path, toArray(newNodes));
});
},
remove: function() {
return this.forEach(path => path.prune());
}
};
function register() {
Collection.registerMethods(traversalMethods, Node);
Collection.registerMethods(mutationMethods, Node);
Collection.setDefaultCollectionType(Node);
}
exports.register = once(register);

View File

@@ -0,0 +1,201 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const Collection = require('../Collection');
const NodeCollection = require('./Node');
const once = require('../utils/once');
const recast = require('recast');
const astNodesAreEquivalent = recast.types.astNodesAreEquivalent;
const b = recast.types.builders;
var types = recast.types.namedTypes;
const VariableDeclarator = recast.types.namedTypes.VariableDeclarator;
/**
* @mixin
*/
const globalMethods = {
/**
* Finds all variable declarators, optionally filtered by name.
*
* @param {string} name
* @return {Collection}
*/
findVariableDeclarators: function(name) {
const filter = name ? {id: {name: name}} : null;
return this.find(VariableDeclarator, filter);
}
};
const filterMethods = {
/**
* Returns a function that returns true if the provided path is a variable
* declarator and requires one of the specified module names.
*
* @param {string|Array} names A module name or an array of module names
* @return {Function}
*/
requiresModule: function(names) {
if (names && !Array.isArray(names)) {
names = [names];
}
const requireIdentifier = b.identifier('require');
return function(path) {
const node = path.value;
if (!VariableDeclarator.check(node) ||
!types.CallExpression.check(node.init) ||
!astNodesAreEquivalent(node.init.callee, requireIdentifier)) {
return false;
}
return !names ||
names.some(
n => astNodesAreEquivalent(node.init.arguments[0], b.literal(n))
);
};
}
};
/**
* @mixin
*/
const transformMethods = {
/**
* Renames a variable and all its occurrences.
*
* @param {string} newName
* @return {Collection}
*/
renameTo: function(newName) {
// TODO: Include JSXElements
return this.forEach(function(path) {
const node = path.value;
const oldName = node.id.name;
const rootScope = path.scope;
const rootPath = rootScope.path;
Collection.fromPaths([rootPath])
.find(types.Identifier, {name: oldName})
.filter(function(path) { // ignore non-variables
const parent = path.parent.node;
if (
types.MemberExpression.check(parent) &&
parent.property === path.node &&
!parent.computed
) {
// obj.oldName
return false;
}
if (
types.Property.check(parent) &&
parent.key === path.node &&
!parent.computed
) {
// { oldName: 3 }
return false;
}
if (
types.ObjectProperty.check(parent) &&
parent.key === path.node &&
!parent.computed
) {
// { oldName: 3 }
return false;
}
if (
types.ObjectMethod.check(parent) &&
parent.key === path.node &&
!parent.computed
) {
// { oldName() {} }
return false;
}
if (
types.MethodDefinition.check(parent) &&
parent.key === path.node &&
!parent.computed
) {
// class A { oldName() {} }
return false;
}
if (
types.ClassMethod.check(parent) &&
parent.key === path.node &&
!parent.computed
) {
// class A { oldName() {} }
return false;
}
if (
types.ClassProperty.check(parent) &&
parent.key === path.node &&
!parent.computed
) {
// class A { oldName = 3 }
return false;
}
if (
types.JSXAttribute.check(parent) &&
parent.name === path.node &&
!parent.computed
) {
// <Foo oldName={oldName} />
return false;
}
return true;
})
.forEach(function(path) {
let scope = path.scope;
while (scope && scope !== rootScope) {
if (scope.declares(oldName)) {
return;
}
scope = scope.parent;
}
if (scope) { // identifier must refer to declared variable
// It may look like we filtered out properties,
// but the filter only ignored property "keys", not "value"s
// In shorthand properties, "key" and "value" both have an
// Identifier with the same structure.
const parent = path.parent.node;
if (
types.Property.check(parent) &&
parent.shorthand &&
!parent.method
) {
path.parent.get('shorthand').replace(false);
}
path.get('name').replace(newName);
}
});
});
}
};
function register() {
NodeCollection.register();
Collection.registerMethods(globalMethods);
Collection.registerMethods(transformMethods, VariableDeclarator);
}
exports.register = once(register);
exports.filters = filterMethods;

View File

@@ -0,0 +1,13 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
module.exports = {
Node: require('./Node'),
JSXElement: require('./JSXElement'),
VariableDeclarator: require('./VariableDeclarator'),
};

View File

@@ -0,0 +1,183 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const Collection = require('./Collection');
const collections = require('./collections');
const getParser = require('./getParser');
const matchNode = require('./matchNode');
const recast = require('recast');
const template = require('./template');
const Node = recast.types.namedTypes.Node;
const NodePath = recast.types.NodePath;
// Register all built-in collections
for (var name in collections) {
collections[name].register();
}
/**
* Main entry point to the tool. The function accepts multiple different kinds
* of arguments as a convenience. In particular the function accepts either
*
* - a string containing source code
* The string is parsed with Recast
* - a single AST node
* - a single node path
* - an array of nodes
* - an array of node paths
*
* @exports jscodeshift
* @param {Node|NodePath|Array|string} source
* @param {Object} options Options to pass to Recast when passing source code
* @return {Collection}
*/
function core(source, options) {
return typeof source === 'string' ?
fromSource(source, options) :
fromAST(source);
}
/**
* Returns a collection from a node, node path, array of nodes or array of node
* paths.
*
* @ignore
* @param {Node|NodePath|Array} source
* @return {Collection}
*/
function fromAST(ast) {
if (Array.isArray(ast)) {
if (ast[0] instanceof NodePath || ast.length === 0) {
return Collection.fromPaths(ast);
} else if (Node.check(ast[0])) {
return Collection.fromNodes(ast);
}
} else {
if (ast instanceof NodePath) {
return Collection.fromPaths([ast]);
} else if (Node.check(ast)) {
return Collection.fromNodes([ast]);
}
}
throw new TypeError(
'Received an unexpected value ' + Object.prototype.toString.call(ast)
);
}
function fromSource(source, options) {
if (!options) {
options = {};
}
if (!options.parser) {
options.parser = getParser();
}
return fromAST(recast.parse(source, options));
}
/**
* Utility function to match a node against a pattern.
* @augments core
* @static
* @param {Node|NodePath|Object} path
* @parma {Object} filter
* @return boolean
*/
function match(path, filter) {
if (!(path instanceof NodePath)) {
if (typeof path.get === 'function') {
path = path.get();
} else {
path = {value: path};
}
}
return matchNode(path.value, filter);
}
const plugins = [];
/**
* Utility function for registering plugins.
*
* Plugins are simple functions that are passed the core jscodeshift instance.
* They should extend jscodeshift by calling `registerMethods`, etc.
* This method guards against repeated registrations (the plugin callback will only be called once).
*
* @augments core
* @static
* @param {Function} plugin
*/
function use(plugin) {
if (plugins.indexOf(plugin) === -1) {
plugins.push(plugin);
plugin(core);
}
}
/**
* Returns a version of the core jscodeshift function "bound" to a specific
* parser.
*
* @augments core
* @static
*/
function withParser(parser) {
if (typeof parser === 'string') {
parser = getParser(parser);
}
const newCore = function(source, options) {
if (options && !options.parser) {
options.parser = parser;
} else {
options = {parser};
}
return core(source, options);
};
return enrichCore(newCore, parser);
}
/**
* The ast-types library
* @external astTypes
* @see {@link https://github.com/benjamn/ast-types}
*/
function enrichCore(core, parser) {
// add builders and types to the function for simple access
Object.assign(core, recast.types.namedTypes);
Object.assign(core, recast.types.builders);
core.registerMethods = Collection.registerMethods;
/**
* @augments core
* @type external:astTypes
*/
core.types = recast.types;
core.match = match;
core.template = template(parser);
// add mappings and filters to function
core.filters = {};
core.mappings = {};
for (const name in collections) {
if (collections[name].filters) {
core.filters[name] = collections[name].filters;
}
if (collections[name].mappings) {
core.mappings[name] = collections[name].mappings;
}
}
core.use = use;
core.withParser = withParser;
return core;
}
module.exports = enrichCore(core, getParser());

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
module.exports = function getParser(parserName, options) {
switch (parserName) {
case 'babylon':
return require('../parser/babylon')(options);
case 'flow':
return require('../parser/flow')(options);
case 'ts':
return require('../parser/ts')(options);
case 'tsx':
return require('../parser/tsx')(options);
case 'babel':
default:
return require('../parser/babel5Compat')(options);
}
};

View File

@@ -0,0 +1,68 @@
'use strict';
const fs = require('fs');
const mm = require('micromatch');
const matchers = [];
/**
* Add glob patterns to ignore matched files and folders.
* Creates glob patterns to approximate gitignore patterns.
* @param {String} val - the glob or gitignore-style pattern to ignore
* @see {@linkplain https://git-scm.com/docs/gitignore#_pattern_format}
*/
function addIgnorePattern(val) {
if (val && typeof val === 'string' && val[0] !== '#') {
let pattern = val;
if (pattern.indexOf('/') === -1) {
matchers.push('**/' + pattern);
} else if (pattern[pattern.length-1] === '/') {
matchers.push('**/' + pattern + '**');
matchers.push(pattern + '**');
}
matchers.push(pattern);
}
}
/**
* Adds ignore patterns directly from function input
* @param {String|Array<String>} input - the ignore patterns
*/
function addIgnoreFromInput(input) {
let patterns = [];
if (input) {
patterns = patterns.concat(input);
}
patterns.forEach(addIgnorePattern);
}
/**
* Adds ignore patterns by reading files
* @param {String|Array<String>} input - the paths to the ignore config files
*/
function addIgnoreFromFile(input) {
let lines = [];
let files = [];
if (input) {
files = files.concat(input);
}
files.forEach(function(config) {
const stats = fs.statSync(config);
if (stats.isFile()) {
const content = fs.readFileSync(config, 'utf8');
lines = lines.concat(content.split(/\r?\n/));
}
});
lines.forEach(addIgnorePattern);
}
function shouldIgnore(path) {
const matched = matchers.length ? mm.isMatch(path, matchers, { dot:true }) : false;
return matched;
}
exports.add = addIgnoreFromInput;
exports.addFromFile = addIgnoreFromFile;
exports.shouldIgnore = shouldIgnore;

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const hasOwn =
Object.prototype.hasOwnProperty.call.bind(Object.prototype.hasOwnProperty);
/**
* Checks whether needle is a strict subset of haystack.
*
* @param {*} haystack Value to test.
* @param {*} needle Test function or value to look for in `haystack`.
* @return {bool}
*/
function matchNode(haystack, needle) {
if (typeof needle === 'function') {
return needle(haystack);
}
if (isNode(needle) && isNode(haystack)) {
return Object.keys(needle).every(function(property) {
return (
hasOwn(haystack, property) &&
matchNode(haystack[property], needle[property])
);
});
}
return haystack === needle;
}
function isNode(value) {
return typeof value === 'object' && value;
}
module.exports = matchNode;

View File

@@ -0,0 +1,173 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
'use strict';
const recast = require('recast');
const builders = recast.types.builders;
const types = recast.types.namedTypes;
function splice(arr, element, replacement) {
arr.splice.apply(arr, [arr.indexOf(element), 1].concat(replacement));
}
function cleanLocation(node) {
delete node.start;
delete node.end;
delete node.loc;
return node;
}
function ensureStatement(node) {
return types.Statement.check(node) ?
// Removing the location information seems to ensure that the node is
// correctly reprinted with a trailing semicolon
cleanLocation(node) :
builders.expressionStatement(node);
}
function getVistor(varNames, nodes) {
return {
visitIdentifier: function(path) {
this.traverse(path);
const node = path.node;
const parent = path.parent.node;
// If this identifier is not one of our generated ones, do nothing
const varIndex = varNames.indexOf(node.name);
if (varIndex === -1) {
return;
}
let replacement = nodes[varIndex];
nodes[varIndex] = null;
// If the replacement is an array, we need to explode the nodes in context
if (Array.isArray(replacement)) {
if (types.Function.check(parent) &&
parent.params.indexOf(node) > -1) {
// Function parameters: function foo(${bar}) {}
splice(parent.params, node, replacement);
} else if (types.VariableDeclarator.check(parent)) {
// Variable declarations: var foo = ${bar}, baz = 42;
splice(
path.parent.parent.node.declarations,
parent,
replacement
);
} else if (types.ArrayExpression.check(parent)) {
// Arrays: var foo = [${bar}, baz];
splice(parent.elements, node, replacement);
} else if (types.Property.check(parent) && parent.shorthand) {
// Objects: var foo = {${bar}, baz: 42};
splice(
path.parent.parent.node.properties,
parent,
replacement
);
} else if (types.CallExpression.check(parent) &&
parent.arguments.indexOf(node) > -1) {
// Function call arguments: foo(${bar}, baz)
splice(parent.arguments, node, replacement);
} else if (types.ExpressionStatement.check(parent)) {
// Generic sequence of statements: { ${foo}; bar; }
path.parent.replace.apply(
path.parent,
replacement.map(ensureStatement)
);
} else {
// Every else, let recast take care of it
path.replace.apply(path, replacement);
}
} else if (types.ExpressionStatement.check(parent)) {
path.parent.replace(ensureStatement(replacement));
} else {
path.replace(replacement);
}
}
};
}
function replaceNodes(src, varNames, nodes, parser) {
const ast = recast.parse(src, {parser});
recast.visit(ast, getVistor(varNames, nodes));
return ast;
}
let varNameCounter = 0;
function getUniqueVarName() {
return `$jscodeshift${varNameCounter++}$`;
}
module.exports = function withParser(parser) {
function statements(template/*, ...nodes*/) {
template = Array.from(template);
const nodes = Array.from(arguments).slice(1);
const varNames = nodes.map(() => getUniqueVarName());
const src = template.reduce(
(result, elem, i) => result + varNames[i - 1] + elem
);
return replaceNodes(
src,
varNames,
nodes,
parser
).program.body;
}
function statement(/*template, ...nodes*/) {
return statements.apply(null, arguments)[0];
}
function expression(template/*, ...nodes*/) {
// wrap code in `(...)` to force evaluation as expression
template = Array.from(template);
if (template.length > 0) {
template[0] = '(' + template[0];
template[template.length - 1] += ')';
}
const expression = statement.apply(
null,
[template].concat(Array.from(arguments).slice(1))
).expression;
// Remove added parens
if (expression.extra) {
expression.extra.parenthesized = false;
}
return expression;
}
function asyncExpression(template/*, ...nodes*/) {
template = Array.from(template);
if (template.length > 0) {
template[0] = 'async () => (' + template[0];
template[template.length - 1] += ')';
}
const expression = statement.apply(
null,
[template].concat(Array.from(arguments).slice(1))
).expression.body;
// Remove added parens
if (expression.extra) {
expression.extra.parenthesized = false;
}
return expression;
}
return {statements, statement, expression, asyncExpression};
}

View File

@@ -0,0 +1,149 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* global expect, describe, it */
'use strict';
const fs = require('fs');
const path = require('path');
function applyTransform(module, options, input, testOptions = {}) {
// Handle ES6 modules using default export for the transform
const transform = module.default ? module.default : module;
// Jest resets the module registry after each test, so we need to always get
// a fresh copy of jscodeshift on every test run.
let jscodeshift = require('./core');
if (testOptions.parser || module.parser) {
jscodeshift = jscodeshift.withParser(testOptions.parser || module.parser);
}
const output = transform(
input,
{
jscodeshift,
j: jscodeshift,
stats: () => {},
},
options || {}
);
return (output || '').trim();
}
exports.applyTransform = applyTransform;
function runSnapshotTest(module, options, input) {
const output = applyTransform(module, options, input);
expect(output).toMatchSnapshot();
return output;
}
exports.runSnapshotTest = runSnapshotTest;
function runInlineTest(module, options, input, expectedOutput, testOptions) {
const output = applyTransform(module, options, input, testOptions);
expect(output).toEqual(expectedOutput.trim());
return output;
}
exports.runInlineTest = runInlineTest;
function extensionForParser(parser) {
switch (parser) {
case 'ts':
case 'tsx':
return parser;
default:
return 'js'
}
}
/**
* Utility function to run a jscodeshift script within a unit test. This makes
* several assumptions about the environment:
*
* - `dirName` contains the name of the directory the test is located in. This
* should normally be passed via __dirname.
* - The test should be located in a subdirectory next to the transform itself.
* Commonly tests are located in a directory called __tests__.
* - `transformName` contains the filename of the transform being tested,
* excluding the .js extension.
* - `testFilePrefix` optionally contains the name of the file with the test
* data. If not specified, it defaults to the same value as `transformName`.
* This will be suffixed with ".input.js" for the input file and ".output.js"
* for the expected output. For example, if set to "foo", we will read the
* "foo.input.js" file, pass this to the transform, and expect its output to
* be equal to the contents of "foo.output.js".
* - Test data should be located in a directory called __testfixtures__
* alongside the transform and __tests__ directory.
*/
function runTest(dirName, transformName, options, testFilePrefix, testOptions = {}) {
if (!testFilePrefix) {
testFilePrefix = transformName;
}
const extension = extensionForParser(testOptions.parser)
const fixtureDir = path.join(dirName, '..', '__testfixtures__');
const inputPath = path.join(fixtureDir, testFilePrefix + `.input.${extension}`);
const source = fs.readFileSync(inputPath, 'utf8');
const expectedOutput = fs.readFileSync(
path.join(fixtureDir, testFilePrefix + `.output.${extension}`),
'utf8'
);
// Assumes transform is one level up from __tests__ directory
const module = require(path.join(dirName, '..', transformName));
runInlineTest(module, options, {
path: inputPath,
source
}, expectedOutput, testOptions);
}
exports.runTest = runTest;
/**
* Handles some boilerplate around defining a simple jest/Jasmine test for a
* jscodeshift transform.
*/
function defineTest(dirName, transformName, options, testFilePrefix, testOptions) {
const testName = testFilePrefix
? `transforms correctly using "${testFilePrefix}" data`
: 'transforms correctly';
describe(transformName, () => {
it(testName, () => {
runTest(dirName, transformName, options, testFilePrefix, testOptions);
});
});
}
exports.defineTest = defineTest;
function defineInlineTest(module, options, input, expectedOutput, testName) {
it(testName || 'transforms correctly', () => {
runInlineTest(module, options, {
source: input
}, expectedOutput);
});
}
exports.defineInlineTest = defineInlineTest;
function defineSnapshotTest(module, options, input, testName) {
it(testName || 'transforms correctly', () => {
runSnapshotTest(module, options, {
source: input
});
});
}
exports.defineSnapshotTest = defineSnapshotTest;
/**
* Handles file-loading boilerplates, using same defaults as defineTest
*/
function defineSnapshotTestFromFixture(dirName, module, options, testFilePrefix, testName, testOptions = {}) {
const extension = extensionForParser(testOptions.parser)
const fixtureDir = path.join(dirName, '..', '__testfixtures__');
const inputPath = path.join(fixtureDir, testFilePrefix + `.input.${extension}`);
const source = fs.readFileSync(inputPath, 'utf8');
defineSnapshotTest(module, options, source, testName)
}
exports.defineSnapshotTestFromFixture = defineSnapshotTestFromFixture;

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
module.exports = function(arrays) {
const result = new Set(arrays[0]);
let resultSize = result.length;
let i, value, valuesToCheck;
for (i = 1; i < arrays.length; i++) {
valuesToCheck = new Set(arrays[i]);
for (value of result) {
if (!valuesToCheck.has(value)) {
result.delete(value);
resultSize -= 1;
}
if (resultSize === 0) {
return [];
}
}
}
return Array.from(result);
};

View File

@@ -0,0 +1,21 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* This replicates lodash's once functionality for our purposes.
*/
module.exports = function(func) {
let called = false;
let result;
return function(...args) {
if (called) {
return result;
}
called = true;
return result = func.apply(this, args);
};
};

View File

@@ -0,0 +1,20 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
module.exports = function(arrays) {
const result = new Set(arrays[0]);
let i,j, array;
for (i = 1; i < arrays.length; i++) {
array = arrays[i];
for (j = 0; j < array.length; j++) {
result.add(array[j]);
}
}
return Array.from(result);
};