Skip to content

Commit

Permalink
feat: initial dev on new rule for meteor 3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
renanccastro committed Dec 14, 2023
1 parent 9425d5e commit a769fcc
Show file tree
Hide file tree
Showing 12 changed files with 14,803 additions and 8,306 deletions.
3 changes: 2 additions & 1 deletion npm-packages/eslint-plugin-meteor/.eslintrc.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
root: true # ignore config in main folder

parserOptions:
ecmaVersion: 6
ecmaVersion: 2024
sourceType: module

extends:
Expand All @@ -10,6 +10,7 @@ extends:
env:
node: true
mocha: true
es2024: true

plugins:
- prettier
Expand Down
2 changes: 2 additions & 0 deletions npm-packages/eslint-plugin-meteor/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
coverage/
.nyc_output
.DS_Store
node_modules
/.meteor
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This rule checks the usage of syncronous MongoDB Methods on the Server which will stop working starting from Meteor 3.0 with the fiber removal (no-sync-mongo-methods-on-server)

Please describe the origin of the rule here.


## Rule Details

This rule aims to...

The following patterns are considered warnings:

```js

// fill me in

```

The following patterns are not warnings:

```js

// fill me in

```

### Options

If there are any options, describe them here. Otherwise, delete this section.

## When Not To Use It

Give a short description of when it would be appropriate to turn off this rule.

## Further Reading

If there are other links that describe the issue this rule addresses, please include them here in a bulleted list.

4 changes: 3 additions & 1 deletion npm-packages/eslint-plugin-meteor/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ const allRules = {
'scope-dom-lookups': require('./rules/scope-dom-lookups'),
'no-dom-lookup-on-created': require('./rules/no-dom-lookup-on-created'),
'no-template-parent-data': require('./rules/no-template-parent-data'),
'no-sync-mongo-methods-on-server': require('./rules/no-sync-mongo-methods-on-server/no-sync-mongo-methods-on-server'),
};

module.exports = {
rules: allRules,
configs: {
recommended: {
parserOptions: {
ecmaVersion: 6,
ecmaVersion: 2024,
sourceType: 'module',
ecmaFeatures: { jsx: true },
},
Expand All @@ -34,6 +35,7 @@ module.exports = {
'meteor/scope-dom-lookups': 0,
'meteor/no-dom-lookup-on-created': 0,
'meteor/no-template-parent-data': 0,
'meteor/no-sync-mongo-methods-on-server': 0,
},
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
const fs = require('fs');
const path = require('path');
const readAndParse = require('./parse');

// TODO: the order is important
// extensions of files that are compiled into js
// and can import other js files.
const parseableExt = ['.js', '.jsx', '.svelte', '.ts', '.tsx'];

// These folders are not eagerly loaded by Meteor
// TODO: check if we should only exclude some of these when
// they are at the top level
const notEagerlyLoadedDirs = [
'imports',
'node_modules',
'public',
// TODO: have an option to include tests
'tests',
'test',
'packages',
'private',
];

// The path will start with one of these if
// it imports an app file
const appFileImport = ['.', path.posix.sep, path.win32.sep];

function shouldWalk(folderPath, archList) {
const basename = path.basename(folderPath);
if (basename[0] === '.' || notEagerlyLoadedDirs.includes(basename)) {
return false;
}

const parts = folderPath.split(path.sep);
if (!archList.includes('server') && parts.includes('server')) {
return false;
}
if (!archList.includes('client') && parts.includes('client')) {
return false;
}

return true;
}

function findExt(filePath) {
const ext = parseableExt.find((possibleExt) => {
const exists = fs.existsSync(filePath + possibleExt);
return exists;
});

if (ext) {
return filePath + ext;
}

// Maybe it is the index file in a folder
// TODO: check if this should be before or after checking extensions
try {
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
return findExt(`${filePath}${path.sep}index`);
}
} catch (e) {
// TODO: only ignore certain errors
}
}

function shouldParse(filePath) {
// console.log('shouldParse found filePath', filePath)

const ext = path.extname(filePath);
const basename = path.basename(filePath);

// TODO: have an option to parse test files
if (
basename.endsWith(`.app-tests${ext}`) ||
basename.endsWith(`.spec${ext}`) ||
basename.endsWith(`.test${ext}`)
) {
return false;
}

return basename[0] !== '.' && parseableExt.includes(ext);
}

function isMeteorPackage(importPath) {
return importPath.startsWith('meteor/');
}

function isNpmDependency(importPath) {
return !appFileImport.includes(importPath[0]);
}

const handledFiles = new Set();
let cachedParsedFile;

function getAbsFilePath(filePath) {
// some files have no ext or are only the ext (.gitignore, .editorconfig, etc.)
const existingExt =
path.extname(filePath) || path.basename(filePath).startsWith('.');
if (!existingExt) {
// TODO: should maybe only do this if a file doesn't exists with the given path
// since we might be importing a file with no extension.
const pathWithExt = findExt(filePath);
if (!pathWithExt) {
// console.log('unable to find ext', filePath);
return pathWithExt;
}

return pathWithExt;
}

// TODO: if the file doesn't exist, we must try other extensions

return filePath;
}

function handleFile(_filePath, appPath, onFile, cachedParsedFile) {
const filePath = getAbsFilePath(_filePath);

if (!shouldParse(filePath) || handledFiles.has(filePath)) {
return;
}

handledFiles.add(filePath);

const realPath = fs.realpathSync.native(filePath);
if (cachedParsedFile[realPath]) {
return;
}
const ast = readAndParse(filePath);
cachedParsedFile[realPath] = true;
// console.debug('Set key', realPath);

const imports = readAndParse.findImports(filePath, ast, appPath);
onFile({ path: filePath, ast, imports });

imports
.filter(
({ source }) => !isMeteorPackage(source) && !isNpmDependency(source)
)
.map(({ source }) => {
if (source[0] === '/') {
source = appPath + source;
}
return path.resolve(path.dirname(filePath), source);
})
.forEach((importPath) => {
handleFile(importPath, appPath, onFile, cachedParsedFile);
});
}

function handleFolder(folderPath, appPath, archList, onFile, cachedParsedFile) {
const dirents = fs.readdirSync(folderPath, { withFileTypes: true });
// console.log('dirents', dirents)
for (let i = 0; i < dirents.length; i += 1) {
if (dirents[i].isDirectory()) {
if (shouldWalk(path.resolve(folderPath, dirents[i].name), archList)) {
handleFolder(
path.resolve(folderPath, dirents[i].name),
appPath,
archList,
onFile,
cachedParsedFile
);
}
} else if (dirents[i].isFile()) {
const filePath = path.resolve(folderPath, dirents[i].name);
handleFile(filePath, appPath, onFile, cachedParsedFile);
}
}
}

class Walker {
cachedParsedFile;
appPath;

filePath() {
return path.join(this.appPath, '.eslint-meteor-files');
}

constructor(appPath) {
this.appPath = appPath;
this.cachedParsedFile = fs.existsSync(this.filePath())
? JSON.parse(fs.readFileSync(this.filePath()))
: {};
}
walkApp(archList, onFile) {
handleFolder(
this.appPath,
this.appPath,
archList,
onFile,
this.cachedParsedFile
);
fs.writeFileSync(this.filePath(), JSON.stringify(this.cachedParsedFile));
}
get cachedParsedFile() {
return this.cachedParsedFile;
}
}

module.exports = { Walker };
Loading

0 comments on commit a769fcc

Please sign in to comment.