pax_global_header 0000666 0000000 0000000 00000000064 15047412410 0014510 g ustar 00root root 0000000 0000000 52 comment=14c678e70e714f32e511cdf24c8d2392ac25ba0f
thecodrr-fdir-14c678e/ 0000775 0000000 0000000 00000000000 15047412410 0014607 5 ustar 00root root 0000000 0000000 thecodrr-fdir-14c678e/.gitattributes 0000664 0000000 0000000 00000000065 15047412410 0017503 0 ustar 00root root 0000000 0000000 * text eol=lf
*.png binary
*.jpg binary
*.gif binary thecodrr-fdir-14c678e/.github/ 0000775 0000000 0000000 00000000000 15047412410 0016147 5 ustar 00root root 0000000 0000000 thecodrr-fdir-14c678e/.github/workflows/ 0000775 0000000 0000000 00000000000 15047412410 0020204 5 ustar 00root root 0000000 0000000 thecodrr-fdir-14c678e/.github/workflows/run-tests.yml 0000664 0000000 0000000 00000001712 15047412410 0022674 0 ustar 00root root 0000000 0000000 name: Run tests
on:
pull_request:
push:
branches: [master]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [16.x, 24.x]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
check-latest: true
- run: npm i
- run: npm run test:coverage
env:
CI: true
- name: Send Report to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel: true
finish:
needs: test
runs-on: ubuntu-latest
steps:
- name: Send Parallel Finished
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true
thecodrr-fdir-14c678e/.gitignore 0000664 0000000 0000000 00000000074 15047412410 0016600 0 ustar 00root root 0000000 0000000 node_modules
benchmark
coverage
cli
scripts
dist
.nyc_output thecodrr-fdir-14c678e/.npmignore 0000664 0000000 0000000 00000000015 15047412410 0016602 0 ustar 00root root 0000000 0000000 *
!dist/**/** thecodrr-fdir-14c678e/.prettierrc.json 0000664 0000000 0000000 00000000263 15047412410 0017744 0 ustar 00root root 0000000 0000000 {
"bracketSpacing": true,
"printWidth": 80,
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"arrowParens": "always"
}
thecodrr-fdir-14c678e/.vscode/ 0000775 0000000 0000000 00000000000 15047412410 0016150 5 ustar 00root root 0000000 0000000 thecodrr-fdir-14c678e/.vscode/settings.json 0000664 0000000 0000000 00000000030 15047412410 0020674 0 ustar 00root root 0000000 0000000 {
"files.eol": "\n"
}
thecodrr-fdir-14c678e/ARCHITECTURE.md 0000664 0000000 0000000 00000004610 15047412410 0017014 0 ustar 00root root 0000000 0000000 # Architecture
This document describes the high-level architecture of `fdir`. If you want to familiarize yourself with the code base, you are in the right place!
---
On the highest level, `fdir` is a library that accepts a path to a directory as input and outputs all the file paths in that directory recrusively.
More specifically, input data consists of a path to a directory (`rootDirectory`) and different flags and filters to control the walking process. To increase performance, `fdir` builds internal functions conditionally based on the passed flags. Since these "conditional" functions are tiny, they are inlined by the Javascript Engine reducing branching & allocations.
## Entry Points
`index.js` exports the main `fdir` class and it is the main entry point. However, there is nothing of importance in this file aside from the export.
`src/builder/index.js` contains the main API of `fdir` exposed via a `Builder` class. This is where all the flags & filters are built and passed (as an `options` Object) onto the core of `fdir`.
## Code Map
This section talks briefly about all the directories and what each file in each directory does.
### `src/api`
This is the core of `fdir`.
**`walker.js`:** This contains the `Walker` class which is responsible for controlling and maintaining the state of the directory walker. It builds the conditional functions, processes the `Dirents` and delegates the actual filesystem directory reading to sync/async APIs.
**`async.js`** This contains the asynchronous (`fs.readdir`) logic. This is the starting point of the async crawling process.
**`queue.js`** This contains a tiny `Queue` class to make sure `fdir` doesn't early exit during walking. It increments a counter for each "walk" queued and decrements it when it finishes. Once the counter hits 0, it calls the callback which returns the output to the user.
**`sync.js`** This contains the synchronous (`fs.readdirSync`) logic. This is the starting point of the sync crawling process.
**`fns.js`** This contains the implementations of all the conditional functions.
### `src/builder`
This is what gets exposed to the developer and contains 2 builders that aid in building an `options` object to control various aspects of the walker.
### `src/compat`
Since `fdir` supports Node <= 10.0, this directory contains the compatibility code to bridge the newer (v10.0) filesystem API with the older (v8.0) filesystem API.
thecodrr-fdir-14c678e/BENCHMARKS.md 0000664 0000000 0000000 00000004022 15047412410 0016544 0 ustar 00root root 0000000 0000000 # Benchmarks
**System information:**
```
Package version: 6.1.1
CPU: Intel Coreโข i7-10610U 1.8GHz
Cores: 8 (4 Physical)
RAM: 24GB
Disk: Samsung SAMSUNG MZVLW256HEHP-000L7 238GB SSD (NVMe)
OS: Microsoft Windows 11 Pro (Windows)
Kernel: 10.0.22621 x64
Node: v20.10.0
V8: 11.3.244.8-node.25
```
## Asynchronous
> 4865 files & 745 directories
| Package | ops/s | Error margin | % slower |
|---------------------|-------|----------|-----------------|
| fdir (v6.1.1) | 76.1 | 1.38 | 2.81 |
| fdir (v3.4.2) | 78.3 | 1.04 | 0 |
| fdir (v4.1.0) | 77.8 | 1.13 | 0.64 |
| fdir (v5.0.0) | 78.1 | 0.98 | 0.26 |
| recursive-fs | 69.3 | 1.04 | 11.49 |
| recur-readdir | 1.7 | 1.5 | 97.83 |
| recursive-files | 15.3 | 4.22 | 80.46 |
| recursive-readdir | 14.9 | 2.72 | 80.97 |
| getAllFiles | 68 | 0.96 | 13.15 |
| node:fs.readdir | 13.1 | 1.43 | 83.27 |
## Synchronous
> 4865 files & 745 directories
| Package | ops/s | Error margin | % slower |
|------------------------|-------|----------|-----------------|
| fdir (v6.1.1) | 32.9 | 1.24 | 0 |
| fdir (v1.2.0) | 32.1 | 0.91 | 2.43 |
| fdir (v2.1.1) | 31.8 | 0.87 | 3.34 |
| fdir (v3.4.2) | 31.5 | 1.28 | 4.26 |
| fdir (v4.1.0) | 29.7 | 0.79 | 9.73 |
| fdir (v5.0.0) | 29.9 | 0.61 | 9.12 |
| get-all-files | 28.9 | 0.57 | 12.16 |
| all-files-in-tree | 5.7 | 0.89 | 82.67 |
| fs-readdir-recursive | 2.7 | 0.88 | 91.79 |
| klaw-sync | 5.8 | 0.93 | 82.37 |
| recur-readdir | 3 | 1.58 | 90.88 |
| walk-sync | 2.9 | 0.91 | 91.19 |
| node:fs.readdirSync | 29.1 | 0.72 | 11.55 |
thecodrr-fdir-14c678e/CODE_OF_CONDUCT.md 0000664 0000000 0000000 00000006433 15047412410 0017414 0 ustar 00root root 0000000 0000000 # Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at thecodrr@protonmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
thecodrr-fdir-14c678e/CONTRIBUTING.md 0000664 0000000 0000000 00000010167 15047412410 0017045 0 ustar 00root root 0000000 0000000 # Contributing
When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.
Please note we have a code of conduct, please follow it in all your interactions with the project.
## Pull Request Process
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
build.
2. Update the README.md with details of changes to the interface, this includes new environment
variables, exposed ports, useful file locations and container parameters.
3. Increase the version numbers in any examples files and the README.md to the new version that this
Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
do not have permission to do that, you may request the second reviewer to merge it for you.
## Code of Conduct
### Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
### Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
### Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
### Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
### Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
### Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
thecodrr-fdir-14c678e/LICENSE 0000664 0000000 0000000 00000002035 15047412410 0015614 0 ustar 00root root 0000000 0000000 Copyright 2023 Abdullah Atta
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
thecodrr-fdir-14c678e/README.md 0000664 0000000 0000000 00000007413 15047412410 0016073 0 ustar 00root root 0000000 0000000
The Fastest Directory Crawler & Globber for NodeJS
โก **The Fastest:** Nothing similar (in the NodeJS world) beats `fdir` in speed. It can easily crawl a directory containing **1 million files in < 1 second.**
๐ก **Stupidly Easy:** `fdir` uses expressive Builder pattern to build the crawler increasing code readability.
๐ค **Zero Dependencies\*:** `fdir` only uses NodeJS `fs` & `path` modules.
๐บ **Astonishingly Small:** < 2KB in size gzipped & minified.
๐ฎ **Hackable:** Extending `fdir` is extremely simple now that the new Builder API is here. Feel free to experiment around.
_\* `picomatch` must be installed manually by the user to support globbing._
## ๐ Quickstart
### Installation
You can install using `npm`:
```sh
$ npm i fdir
```
or Yarn:
```sh
$ yarn add fdir
```
### Usage
```ts
import { fdir } from "fdir";
// create the builder
const api = new fdir().withFullPaths().crawl("path/to/dir");
// get all files in a directory synchronously
const files = api.sync();
// or asynchronously
api.withPromise().then((files) => {
// do something with the result here.
});
```
## Documentation:
Documentation for all methods is available [here](/documentation.md).
## ๐ Benchmarks:
Please check the benchmark against the latest version [here](/BENCHMARKS.md).
## ๐Used by:
`fdir` is downloaded over 200k+ times a week by projects around the world. Here's a list of some notable projects using `fdir` in production:
> Note: if you think your project should be here, feel free to open an issue. Notable is anything with a considerable amount of GitHub stars.
1. [rollup/plugins](https://github.com/rollup/plugins)
2. [SuperchupuDev/tinyglobby](https://github.com/SuperchupuDev/tinyglobby)
3. [pulumi/pulumi](https://github.com/pulumi/pulumi)
4. [dotenvx/dotenvx](https://github.com/dotenvx/dotenvx)
5. [mdn/yari](https://github.com/mdn/yari)
6. [streetwriters/notesnook](https://github.com/streetwriters/notesnook)
7. [imba/imba](https://github.com/imba/imba)
8. [moroshko/react-scanner](https://github.com/moroshko/react-scanner)
9. [netlify/build](https://github.com/netlify/build)
10. [yassinedoghri/astro-i18next](https://github.com/yassinedoghri/astro-i18next)
11. [selfrefactor/rambda](https://github.com/selfrefactor/rambda)
12. [whyboris/Video-Hub-App](https://github.com/whyboris/Video-Hub-App)
## ๐ฆฎ LICENSE
Copyright © 2024 Abdullah Atta under MIT. [Read full text here.](https://github.com/thecodrr/fdir/raw/master/LICENSE)
thecodrr-fdir-14c678e/__tests__/ 0000775 0000000 0000000 00000000000 15047412410 0016545 5 ustar 00root root 0000000 0000000 thecodrr-fdir-14c678e/__tests__/fdir.test.ts 0000664 0000000 0000000 00000037125 15047412410 0021027 0 ustar 00root root 0000000 0000000 import { fdir } from "../src/index";
import fs from "fs";
import mock from "mock-fs";
import { test, beforeEach, TestContext, vi } from "vitest";
import path, { sep } from "path";
import { convertSlashes } from "../src/utils";
import picomatch from "picomatch";
import { apiTypes, APITypes, cwd, restricted, root } from "./utils";
beforeEach(() => {
mock.restore();
});
test(`crawl single depth directory with callback`, (t) => {
const api = new fdir().crawl("__tests__");
return new Promise((resolve, reject) => {
api.withCallback((err, files) => {
if (err) return reject(err);
t.expect(files[0]).toBeDefined();
t.expect(files.every((t) => t)).toBeTruthy();
t.expect(files[0].length).toBeGreaterThan(0);
resolve();
});
});
});
async function crawl(type: APITypes, path: string, t: TestContext) {
const api = new fdir().crawl(path);
const files = await api[type]();
if (!files) throw new Error("files cannot be null.");
t.expect(files[0]).toBeDefined();
t.expect(files.every((t) => t)).toBeTruthy();
t.expect(files[0].length).toBeGreaterThan(0);
return files;
}
for (const type of apiTypes) {
test(`[${type}] crawl directory`, async (t) => {
await crawl(type, "__tests__", t);
});
test(`[${type}] crawl directory with options`, async (t) => {
const api = new fdir({ includeBasePath: true }).crawl("__tests__");
const files = await api[type]();
t.expect(files.every((file) => file.startsWith("__tests__"))).toBeTruthy();
});
test("crawl single depth directory with options", async (t) => {
const api = new fdir({
maxDepth: 0,
includeBasePath: true,
}).crawl("node_modules");
const files = await api[type]();
t.expect(files).not.toHaveLength(0);
t.expect(files.every((file) => file.split(path.sep).length === 2)).toBe(
true
);
});
test(`[${type}] crawl multi depth directory with options`, async (t) => {
const api = new fdir({
maxDepth: 1,
includeBasePath: true,
}).crawl("node_modules");
const files = await api[type]();
t.expect(files.some((file) => file.split(path.sep).length === 3)).toBe(
true
);
t.expect(files.every((file) => file.split(path.sep).length <= 3)).toBe(
true
);
});
test(`[${type}] crawl multi depth directory`, async (t) => {
await crawl(type, "node_modules", t);
});
test(`[${type}] crawl directory & limit files to 10`, async (t) => {
const api = new fdir().withMaxFiles(10).crawl("node_modules");
const files = await api[type]();
t.expect(files).toHaveLength(10);
});
test(`[${type}] crawl and get both files and directories (withDirs)`, async (t) => {
const api = new fdir().withDirs().crawl("node_modules");
const files = await api[type]();
t.expect(files[0]).toBeDefined();
t.expect(files.every((t) => t)).toBeTruthy();
t.expect(files[0].length).toBeGreaterThan(0);
t.expect(files[0].endsWith(path.normalize("node_modules/"))).toBeTruthy();
});
test(`[${type}] crawl and get all files (withMaxDepth = 1)`, async (t) => {
const api = new fdir().withMaxDepth(1).withBasePath().crawl("node_modules");
const files = await api[type]();
t.expect(
files.every((file) => file.split(path.sep).length <= 3)
).toBeTruthy();
});
test(`[${type}] crawl and get all files (withMaxDepth = -1)`, async (t) => {
const api = new fdir()
.withMaxDepth(-1)
.withBasePath()
.crawl("node_modules");
const files = await api[type]();
t.expect(files.length).toBe(0);
});
test(`[${type}] crawl and get files that match a glob pattern`, async (t) => {
const api = new fdir()
.withBasePath()
.glob("**/*.js")
.glob("**/*.js")
.crawl("node_modules");
const files = await api[type]();
t.expect(files.every((file) => file.endsWith(".js"))).toBeTruthy();
});
test(`[${type}] crawl but exclude node_modules dir`, async (t) => {
const api = new fdir()
.withBasePath()
.exclude((dir) => dir.includes("node_modules"))
.crawl(cwd());
const files = await api[type]();
t.expect(
files.every((file) => !file.includes("node_modules"))
).toBeTruthy();
});
test(`[${type}] crawl all files with filter`, async (t) => {
const api = new fdir()
.withBasePath()
.filter((file) => file.includes(".git"))
.crawl(cwd());
const files = await api[type]();
t.expect(files.every((file) => file.includes(".git"))).toBeTruthy();
});
test(`[${type}] crawl all files with multifilter`, async (t) => {
const api = new fdir()
.withBasePath()
.filter((file) => file.includes(".git"))
.filter((file) => file.includes(".js"))
.crawl(cwd());
const files = await api[type]();
t.expect(
files.every((file) => file.includes(".git") || file.includes(".js"))
).toBeTruthy();
});
test(`[${type}] crawl all files in a directory (with base path)`, async (t) => {
const api = new fdir()
.withBasePath()
.crawl(path.join(cwd(), "node_modules"));
const files = await api[type]();
t.expect(
files.every((file) => file.startsWith("node_modules"))
).toBeTruthy();
});
test(`[${type}] get all files in a directory and output full paths (withFullPaths)`, async (t) => {
const api = new fdir().withFullPaths().crawl(cwd());
const files = await api[type]();
t.expect(files.every((file) => file.startsWith(root()))).toBeTruthy();
});
test(`[${type}] getting files from restricted directory should throw`, async (t) => {
const api = new fdir().withErrors().crawl(restricted());
t.expect(async () => await api[type]()).rejects.toThrowError();
});
test(`[${type}] getting files from restricted directory shouldn't throw (suppressErrors)`, async (t) => {
const api = new fdir().crawl(restricted());
const files = await api[type]();
t.expect(files.length).toBeGreaterThanOrEqual(0);
});
test(`[${type}] recurse root (files should not contain multiple /)`, async (t) => {
mock({
"/etc": {
hosts: "dooone",
},
});
const api = new fdir().withBasePath().normalize().crawl("/");
const files = await api[type]();
t.expect(files.every((file) => !file.includes("//"))).toBeTruthy();
mock.restore();
});
test(`[${type}] crawl all files with only counts`, async (t) => {
const api = new fdir().onlyCounts().crawl("node_modules");
const result = await api[type]();
t.expect(result.files).toBeGreaterThan(0);
});
test(`[${type}] crawl and return only directories`, async (t) => {
const api = new fdir().onlyDirs().crawl("node_modules");
const result = await api[type]();
t.expect(result.length).toBeGreaterThan(0);
t.expect(
result.every((dir) => {
return fs.statSync(dir).isDirectory;
})
).toBeTruthy();
});
test(`[${type}] crawl with options and return only directories`, async (t) => {
const api = new fdir({
excludeFiles: true,
includeDirs: true,
}).crawl("node_modules");
const result = await api[type]();
t.expect(result.length).toBeGreaterThan(0);
t.expect(
result.every((dir) => {
return fs.statSync(dir).isDirectory;
})
).toBeTruthy();
});
test(`[${type}] crawl and filter all files and get only counts`, async (t) => {
const api = new fdir()
.withBasePath()
.filter((file) => file.includes("node_modules"))
.onlyCounts()
.crawl(cwd());
const result = await api[type]();
t.expect(result.files).toBeGreaterThan(0);
});
test("crawl all files in a directory (path with trailing slash)", async (t) => {
const api = new fdir().normalize().crawl("node_modules/");
const files = await api[type]();
const res = files.every((file) => !file.includes("/"));
t.expect(res).toBeDefined();
});
test(`[${type}] crawl all files and group them by directory`, async (t) => {
const api = new fdir().withBasePath().group().crawl("node_modules");
const result = await api[type]();
t.expect(result.length).toBeGreaterThan(0);
});
test(`[${type}] crawl and filter only directories`, async (t) => {
const api = new fdir()
.onlyDirs()
.filter((path) => path.includes("api"))
.crawl("./src");
const result = await api[type]();
t.expect(result).toHaveLength(2);
});
test(`[${type}] crawl and return relative paths`, async (t) => {
const api = new fdir()
.withRelativePaths()
.crawl(path.normalize(`node_modules/`));
const paths = await api[type]();
t.expect(paths.every((p) => !p.startsWith("node_modules"))).toBeTruthy();
});
test(`[${type}] crawl and return relative paths with only dirs`, async (t) => {
mock({
"/some/dir/dir1": {
file: "some file",
},
"/some/dir/dir2": {
file: "some file",
},
"/some/dir/dir2/dir3": {
file: "some file",
},
});
const api = new fdir({ excludeFiles: true, excludeSymlinks: true })
.withDirs()
.withRelativePaths()
.crawl("/some");
const paths = await api[type]();
t.expect(paths.length).toBe(5);
t.expect(paths.filter((p) => p === ".").length).toBe(1);
t.expect(paths.filter((p) => p === "").length).toBe(0);
mock.restore();
});
test(`[${type}] crawl and return relative paths with filters and only dirs`, async (t) => {
mock({
"/some/dir/dir1": {
file: "some file",
},
"/some/dir/dir2": {
file: "some file",
},
"/some/dir/dir2/dir3": {
file: "some file",
},
});
const api = new fdir({ excludeFiles: true, excludeSymlinks: true })
.withDirs()
.withRelativePaths()
.filter((p) => p !== path.join("dir", "dir1/"))
.crawl("/some");
const paths = await api[type]();
t.expect(paths.length).toBe(4);
t.expect(paths.includes(path.join("dir", "dir1/"))).toBe(false);
t.expect(paths.filter((p) => p === ".").length).toBe(1);
t.expect(paths.filter((p) => p === "").length).toBe(0);
mock.restore();
});
test(`[${type}] crawl and return relative paths that end with /`, async (t) => {
const api = new fdir().withRelativePaths().crawl("./node_modules/");
const paths = await api[type]();
t.expect(
paths.every((p) => !p.startsWith("node_modules") && !p.includes("//"))
).toBeTruthy();
});
test(`[${type}] crawl all files and invert path separator`, async (t) => {
const api = new fdir()
.withPathSeparator(sep === "/" ? "\\" : "/")
.crawl("node_modules");
const files = await api[type]();
t.expect(files.every((f) => !f.includes(sep))).toBeTruthy();
});
test(`[${type}] crawl files that match using a custom glob`, async (t) => {
const globFunction = vi.fn((glob: string | string[]) => {
return (test: string): boolean => test.endsWith(".js");
});
const api = new fdir({ globFunction })
.withBasePath()
.glob("**/*.js")
.crawl("node_modules");
const files = await api[type]();
t.expect(globFunction).toHaveBeenCalled();
t.expect(files.every((file) => file.endsWith(".js"))).toBeTruthy();
});
test(`[${type}] crawl files that match using a custom glob with options`, async (t) => {
const globFunction = vi.fn(
(glob: string | string[], options?: { foo: number }) => {
return (test: string): boolean => test.endsWith(".js");
}
);
const api = new fdir({ globFunction })
.withBasePath()
.globWithOptions(["**/*.js"], { foo: 5 })
.crawl("node_modules");
const files = await api[type]();
t.expect(globFunction).toHaveBeenCalled();
t.expect(files.every((file) => file.endsWith(".js"))).toBeTruthy();
});
test(`[${type}] crawl files that match using a picomatch`, async (t) => {
const globFunction = picomatch;
const api = new fdir({ globFunction })
.withBasePath()
.glob("**/*.js")
.crawl("node_modules");
const files = await api[type]();
t.expect(files.every((file) => file.endsWith(".js"))).toBeTruthy();
});
test(`[${type}] using withGlobFunction to set glob`, async (t) => {
const globFunction = vi.fn((glob: string | string[], input: string) => {
return (test: string): boolean => test === input;
});
new fdir()
.withBasePath()
.withGlobFunction(globFunction)
.globWithOptions(["**/*.js"], "bleep")
.crawl("node_modules");
t.expect(globFunction).toHaveBeenCalledWith(["**/*.js"], "bleep");
});
test(`[${type}] using custom fs implementation`, async (t) => {
const readdirStub = vi.fn>(
(_path, _opts, cb) => {
cb(null, []);
}
);
const readdirSyncStub = vi.fn();
readdirSyncStub.mockReturnValue([]);
const fakeFs = {
...fs,
readdir: readdirStub,
readdirSync: readdirSyncStub,
} as unknown as typeof fs;
const api = new fdir({
fs: fakeFs,
}).crawl("node_modules");
await api[type]();
if (type === "withPromise") {
t.expect(readdirStub).toHaveBeenCalled();
} else {
t.expect(readdirSyncStub).toHaveBeenCalled();
}
});
}
// AbortController is not present on Node v14
if ("AbortController" in globalThis) {
test(`[async] crawl directory & use abort signal to abort`, async (t) => {
const totalFiles = new fdir().onlyCounts().crawl("node_modules").sync();
const abortController = new AbortController();
const api = new fdir()
.withAbortSignal(abortController.signal)
.filter((p) => {
if (p.endsWith(".js")) abortController.abort();
return true;
})
.crawl("node_modules");
const files = await api.withPromise();
t.expect(files.length).toBeLessThan(totalFiles.files);
});
}
test(`paths should never start with ./`, async (t) => {
const apis = [
new fdir().withBasePath().crawl("./node_modules"),
new fdir().withBasePath().crawl("./"),
new fdir().withRelativePaths().crawl("./"),
new fdir().withRelativePaths().crawl("."),
new fdir().withDirs().crawl("."),
new fdir().onlyDirs().crawl("."),
];
for (const api of apis) {
const files = await api.withPromise();
t.expect(
files.every((file) => !file.startsWith("./") && !file.startsWith(".\\"))
).toBe(true);
}
});
test(`default to . if root is not provided`, async (t) => {
const files = await new fdir().crawl().withPromise();
const files2 = await new fdir()
.crawl(".")
.withPromise()
.then((f) => f.sort());
t.expect(files.sort().every((r, i) => r === files2[i])).toBe(true);
});
test(`ignore withRelativePath if root === ./`, async (t) => {
const relativeFiles = await new fdir()
.withRelativePaths()
.crawl("./")
.withPromise();
const files = await new fdir().crawl("./").withPromise();
t.expect(relativeFiles.every((r) => files.includes(r))).toBe(true);
});
test(`add path separator if root path does not end with one`, async (t) => {
const relativeFiles = await new fdir()
.withRelativePaths()
.crawl("node_modules")
.withPromise();
t.expect(relativeFiles.every((r) => !r.startsWith(sep))).toBe(true);
});
test(`there should be no empty directory when using withDirs`, async (t) => {
const files = await new fdir().withDirs().crawl("./").withPromise();
t.expect(files.every((r) => r.length > 0)).toBe(true);
});
test(`there should be no empty directory when using withDirs and filters`, async (t) => {
const files = await new fdir()
.withDirs()
.filter((p) => p !== "node_modules")
.crawl("./")
.withPromise();
t.expect(files.every((r) => r.length > 0)).toBe(true);
});
test(`do not convert \\\\ to \\`, async (t) => {
t.expect(convertSlashes("\\\\wsl.localhost\\Ubuntu\\home\\", "\\")).toBe(
"\\\\wsl.localhost\\Ubuntu\\home\\"
);
});
thecodrr-fdir-14c678e/__tests__/symlinks.test.ts 0000664 0000000 0000000 00000031367 15047412410 0021756 0 ustar 00root root 0000000 0000000 import { afterAll, beforeAll, beforeEach, describe, test } from "vitest";
import { apiTypes, normalize, root } from "./utils";
import mock from "mock-fs";
import { fdir, Options } from "../src";
import path from "path";
const fsWithRelativeSymlinks = {
"../../sym-relative/linked": {
"file-1": "file contents",
"file-excluded-1": "file contents",
},
"../../other-relative/dir": {
"file-2": "file contents2",
},
"relative/dir": {
fileSymlink: mock.symlink({
path: "../../../../other-relative/dir/file-2",
}),
fileSymlink2: mock.symlink({
path: "../../../../other-relative/dir/file-3",
}),
dirSymlink: mock.symlink({
path: "../../../../sym-relative/linked",
}),
},
};
const fsWithRecursiveSymlinks = {
"/double/recursive": {
"another-file": "hello",
"recursive-4": mock.symlink({
path: resolveSymlinkRoot("/recursive"),
}),
},
"/just/some": {
"another-file": "hello",
"another-file2": "hello",
"symlink-to-earth": mock.symlink({
path: resolveSymlinkRoot("/random/other"),
}),
},
"/random/other": {
"another-file": "hello",
"another-file2": "hello",
},
"/recursive": {
"random-file": "somecontent",
},
"/recursive/dir": {
"some-file": "somecontent2",
"recursive-1": mock.symlink({
path: resolveSymlinkRoot("/recursive/dir"),
}),
"recursive-2": mock.symlink({
path: resolveSymlinkRoot("/recursive/dir/recursive-1"),
}),
"recursive-3": mock.symlink({
path: resolveSymlinkRoot("/recursive"),
}),
"recursive-5": mock.symlink({
path: resolveSymlinkRoot("/double/recursive"),
}),
"not-recursive": mock.symlink({
path: resolveSymlinkRoot("/just/some"),
}),
},
};
const fsWithRecursiveRelativeSymlinks = {
"double/recursive": {
"another-file": "hello",
"recursive-4": mock.symlink({
path: "../../recursive",
}),
},
"just/some": {
"another-file": "hello",
"another-file2": "hello",
"symlink-to-earth": mock.symlink({
path: "../../random/other",
}),
},
"random/other": {
"another-file": "hello",
"another-file2": "hello",
},
recursive: {
"random-file": "somecontent",
},
"recursive/dir": {
"some-file": "somecontent2",
"recursive-1": mock.symlink({
path: "../../recursive/dir",
}),
"recursive-2": mock.symlink({
path: "./recursive-1",
}),
"recursive-3": mock.symlink({
path: "../../recursive",
}),
"recursive-5": mock.symlink({
path: "../../double/recursive",
}),
"not-recursive": mock.symlink({
path: "../../just/some",
}),
},
};
const fsWithSymlinkInRootDir = {
"/usr/lib": {
"file-1": "file contents",
},
"/lib": mock.symlink({
path: "/usr/lib",
}),
};
const mockFs = {
...fsWithRelativeSymlinks,
...fsWithRecursiveSymlinks,
...fsWithRecursiveRelativeSymlinks,
...fsWithSymlinkInRootDir,
"/sym/linked": {
"file-1": "file contents",
"file-excluded-1": "file contents",
},
"/other/dir": {
"file-2": "file contents2",
},
"/some/dir": {
fileSymlink: mock.symlink({
path: "/other/dir/file-2",
}),
fileSymlink2: mock.symlink({
path: "/other/dir/file-3",
}),
dirSymlink: mock.symlink({
path: "/sym/linked",
}),
},
};
for (const type of apiTypes) {
describe.concurrent(type, () => {
beforeAll(() => {
mock(mockFs);
});
afterAll(() => {
mock.restore();
});
test(`resolve symlinks`, async (t) => {
const api = new fdir().withSymlinks().crawl("/some/dir");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(
normalize([
"/other/dir/file-2",
"/sym/linked/file-1",
"/sym/linked/file-excluded-1",
])
);
});
test(`resolve recursive symlinks`, async (t) => {
const api = new fdir().withSymlinks().crawl("/recursive");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(
normalize([
"/double/recursive/another-file",
"/just/some/another-file",
"/just/some/another-file2",
"/random/other/another-file",
"/random/other/another-file2",
"/recursive/dir/some-file",
"/recursive/random-file",
])
);
});
test(`resolve recursive symlinks (real paths: false)`, async (t) => {
const api = new fdir()
.withSymlinks({ resolvePaths: false })
.crawl("/recursive");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(
normalize([
"/recursive/dir/not-recursive/another-file",
"/recursive/dir/not-recursive/another-file2",
"/recursive/dir/not-recursive/symlink-to-earth/another-file",
"/recursive/dir/not-recursive/symlink-to-earth/another-file2",
"/recursive/dir/recursive-1/not-recursive/another-file",
"/recursive/dir/recursive-1/not-recursive/another-file2",
"/recursive/dir/recursive-1/not-recursive/symlink-to-earth/another-file",
"/recursive/dir/recursive-1/not-recursive/symlink-to-earth/another-file2",
"/recursive/dir/recursive-1/recursive-5/another-file",
"/recursive/dir/recursive-1/some-file",
"/recursive/dir/recursive-2/not-recursive/another-file",
"/recursive/dir/recursive-2/not-recursive/another-file2",
"/recursive/dir/recursive-2/not-recursive/symlink-to-earth/another-file",
"/recursive/dir/recursive-2/not-recursive/symlink-to-earth/another-file2",
"/recursive/dir/recursive-2/recursive-5/another-file",
"/recursive/dir/recursive-2/some-file",
"/recursive/dir/recursive-3/dir/not-recursive/another-file",
"/recursive/dir/recursive-3/dir/not-recursive/another-file2",
"/recursive/dir/recursive-3/dir/not-recursive/symlink-to-earth/another-file",
"/recursive/dir/recursive-3/dir/not-recursive/symlink-to-earth/another-file2",
"/recursive/dir/recursive-3/dir/recursive-5/another-file",
"/recursive/dir/recursive-3/dir/some-file",
"/recursive/dir/recursive-3/random-file",
"/recursive/dir/recursive-5/another-file",
"/recursive/dir/recursive-5/recursive-4/dir/not-recursive/another-file",
"/recursive/dir/recursive-5/recursive-4/dir/not-recursive/another-file2",
"/recursive/dir/recursive-5/recursive-4/dir/not-recursive/symlink-to-earth/another-file",
"/recursive/dir/recursive-5/recursive-4/dir/not-recursive/symlink-to-earth/another-file2",
"/recursive/dir/recursive-5/recursive-4/dir/some-file",
"/recursive/dir/recursive-5/recursive-4/random-file",
"/recursive/dir/some-file",
"/recursive/random-file",
])
);
});
test(`resolve recursive symlinks (real paths: false, relative paths: true)`, async (t) => {
const api = new fdir()
.withSymlinks({ resolvePaths: false })
.withRelativePaths()
.withErrors()
.crawl("./recursive");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(
normalize([
"dir/not-recursive/another-file",
"dir/not-recursive/another-file2",
"dir/not-recursive/symlink-to-earth/another-file",
"dir/not-recursive/symlink-to-earth/another-file2",
"dir/recursive-1/not-recursive/another-file",
"dir/recursive-1/not-recursive/another-file2",
"dir/recursive-1/not-recursive/symlink-to-earth/another-file",
"dir/recursive-1/not-recursive/symlink-to-earth/another-file2",
"dir/recursive-1/recursive-5/another-file",
"dir/recursive-1/some-file",
"dir/recursive-2/not-recursive/another-file",
"dir/recursive-2/not-recursive/another-file2",
"dir/recursive-2/not-recursive/symlink-to-earth/another-file",
"dir/recursive-2/not-recursive/symlink-to-earth/another-file2",
"dir/recursive-2/recursive-5/another-file",
"dir/recursive-2/some-file",
"dir/recursive-3/dir/not-recursive/another-file",
"dir/recursive-3/dir/not-recursive/another-file2",
"dir/recursive-3/dir/not-recursive/symlink-to-earth/another-file",
"dir/recursive-3/dir/not-recursive/symlink-to-earth/another-file2",
"dir/recursive-3/dir/recursive-5/another-file",
"dir/recursive-3/dir/some-file",
"dir/recursive-3/random-file",
"dir/recursive-5/another-file",
"dir/recursive-5/recursive-4/dir/not-recursive/another-file",
"dir/recursive-5/recursive-4/dir/not-recursive/another-file2",
"dir/recursive-5/recursive-4/dir/not-recursive/symlink-to-earth/another-file",
"dir/recursive-5/recursive-4/dir/not-recursive/symlink-to-earth/another-file2",
"dir/recursive-5/recursive-4/dir/some-file",
"dir/recursive-5/recursive-4/random-file",
"dir/some-file",
"random-file",
])
);
});
test(`resolve recursive symlinks (real paths: true, relative paths: true)`, async (t) => {
const api = new fdir()
.withSymlinks()
.withRelativePaths()
.withErrors()
.crawl("./recursive");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(
normalize([
"..//double/recursive/another-file",
"../just/some/another-file",
"../just/some/another-file2",
"../random/other/another-file",
"../random/other/another-file2",
"dir/some-file",
"random-file",
])
);
});
test(`resolve symlinks (real paths: false)`, async (t) => {
const api = new fdir()
.withSymlinks({ resolvePaths: false })
.crawl("/some/dir");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(
normalize([
"/some/dir/dirSymlink/file-1",
"/some/dir/dirSymlink/file-excluded-1",
"/some/dir/fileSymlink",
])
);
});
test(`resolve symlinks (real paths: false, relative paths: true)`, async (t) => {
const api = new fdir()
.withSymlinks({ resolvePaths: false })
.withRelativePaths()
.crawl("/some/dir");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(
normalize([
"dirSymlink/file-1",
"dirSymlink/file-excluded-1",
"fileSymlink",
])
);
});
test(`resolve symlinks (real paths: true, relative paths: true)`, async (t) => {
const api = new fdir()
.withSymlinks()
.withRelativePaths()
.crawl("./relative/dir");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(
normalize([
"../../../../other-relative/dir/file-2",
"../../../../sym-relative/linked/file-1",
"../../../../sym-relative/linked/file-excluded-1",
])
);
});
test("resolve symlinks (exclude /sym/linked/)", async (t) => {
const api = new fdir()
.withSymlinks()
.exclude((_name, path) => path === resolveSymlinkRoot("/sym/linked/"))
.crawl("/some/dir");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(normalize(["/other/dir/file-2"]));
});
test("resolve symlinks (exclude /some/dir/dirSymlink/, real paths: false)", async (t) => {
const api = new fdir()
.withSymlinks({ resolvePaths: false })
.exclude(
(_name, path) => path === resolveSymlinkRoot("/some/dir/dirSymlink/")
)
.crawl("/some/dir");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(
normalize(["/some/dir/fileSymlink"])
);
});
test(`do not resolve symlinks`, async (t) => {
const api = new fdir().crawl("/some/dir");
const files = await api[type]();
t.expect(files.sort()).toStrictEqual(
normalize(["dirSymlink", "fileSymlink", "fileSymlink2"])
);
});
test(`exclude symlinks`, async (t) => {
const api = new fdir({ excludeSymlinks: true }).crawl("/some/dir");
const files = await api[type]();
t.expect(files).toHaveLength(0);
});
test(
"doesn't hang when resolving symlinks in the root directory",
async (t) => {
const api = new fdir().withSymlinks({ resolvePaths: false }).crawl("/");
const files = await api[type]();
const expectedFiles = normalize(["/lib/file-1", "/usr/lib/file-1"]);
for (const expectedFile of expectedFiles) {
t.expect(files).toContain(expectedFile);
}
},
{ timeout: 1000 }
);
});
}
function resolveSymlinkRoot(p: string) {
return process.platform === "win32"
? path.join(root(), path.normalize(p))
: p;
}
thecodrr-fdir-14c678e/__tests__/utils.ts 0000664 0000000 0000000 00000001067 15047412410 0020261 0 ustar 00root root 0000000 0000000 import path from "path";
export type APITypes = (typeof apiTypes)[number];
export const apiTypes = ["withPromise", "sync"] as const;
export function root() {
return process.platform === "win32" ? process.cwd().split(path.sep)[0] : "/";
}
export function cwd() {
return `.${path.sep}`;
}
export function restricted() {
return process.platform === "win32"
? path.join(root(), "Windows", "System32")
: "/etc";
}
export function normalize(paths: string[]) {
return paths.map((p) =>
path.isAbsolute(p) ? path.resolve(p) : path.normalize(p)
);
}
thecodrr-fdir-14c678e/assets/ 0000775 0000000 0000000 00000000000 15047412410 0016111 5 ustar 00root root 0000000 0000000 thecodrr-fdir-14c678e/assets/fdir.gif 0000664 0000000 0000000 00001564216 15047412410 0017543 0 ustar 00root root 0000000 0000000 GIF89aญ๗1 jO bKlSั=ธ<""!##$$$$%%$.1&'%&''')D(*6(*;(*W(,i(-/(.N(GV(ma)+4):e*8x*A
,+6,ฤ>y@l=DGZD}:FG_G6@GเtHSH\HพIeฃJCDJย_KFLP^M93MฤWM๑qM๙yN+5OาO\PqฃR{จUO\V}rWA@WPNX)5YI(Z\[\n][a_*6_ื`tฃabadjbkbmc55d+6dXLf:?fชโgธhkญlnqoipsupตฒqrxqฒsถ๎tx|vIIwz~x75zww{{|eฉ}\}ัศjpล๒Uq[aL
h
ชฯ
ส๑I7MLY\lพ๊M/26t
า๏ภโ๒iJกกซฒฅฅกงงฃค[<คbYคชชฅT)ฅb6ฅi%ง=5ง~งญญจๅ๏ฉ}Tฉ{ชฏฐซฑฑญo@ญs<ญดดฎเืฐถถฑ๖๒ดรฮธ๙น|:น๋บปyAผ}Oผsผ๗๑ฝ๑๑พ~พธฏฟ`gภผตยซฤsยฤญลษสฦ฿
วH:ศๆึส๙ฬOอ฿ฑอ๊ํัะฯั๗๏ำa!ีVึWึฅOฺกlฺุฆฟ_฿ฦฅ฿฿ฺเญvโcไ๓์ๅ๙ๆ่้ฤ๊]b๊๎่๐ฺช๐๘~๑๋l๑๓๊๒PV๓f#๓ท{๔xฤ๔ฬข๔๖๔๗ศ๕sช๕๕๋ว๖ล๗ฬ๗อ๗๐ต๘่ง๘๘๒๘๙๕๙SF๚VUพ{!NETSCAPE2.0 !๙ , ญ jO bKlSั=ธ<""!##$$$$%%$.1&'%&''')D(*6(*;(*W(,i(-/(.N(GV(ma)+4):e*8x*A
,+6,ฤ>y@l=DGZD}:FG_G6@GเtHSH\HพIeฃJCDJย_KFLP^M93MฤWM๑qM๙yN+5OาO\PqฃR{จUO\V}rWA@WPNX)5YI(Z\[\n][a_*6_ื`tฃabadjbkbmc55d+6dXLf:?fชโgธhkญlnqoipsupตฒqrxqฒsถ๎tx|vIIwz~x75zww{{|eฉ}\}ัศjpล๒Uq[aL
h
ชฯ
ส๑I7MLY\lพ๊M/26t
า๏ภโ๒iJกกซฒฅฅกงงฃค[<คbYคชชฅT)ฅb6ฅi%ง=5ง~งญญจๅ๏ฉ}Tฉ{ชฏฐซฑฑญo@ญs<ญดดฎเืฐถถฑ๖๒ดรฮธ๙น|:น๋บปyAผ}Oผsผ๗๑ฝ๑๑พ~พธฏฟ`gภผตยซฤsยฤญลษสฦ฿
วH:ศๆึส๙ฬOอ฿ฑอ๊ํัะฯั๗๏ำa!ีVึWึฅOฺกlฺุฆฟ_฿ฦฅ฿฿ฺเญvโcไ๓์ๅ๙ๆ่้ฤ๊]b๊๎่๐ฺช๐๘~๑๋l๑๓๊๒PV๓f#๓ท{๔xฤ๔ฬข๔๖๔๗ศ๕sช๕๕๋ว๖ล๗ฬ๗อ๗๐ต๘่ง๘๘๒๘๙๕๙SF๚VUพ{ Hฐ ม*\ศฐกร#JHฑขล3jศฑฃว CIฒคษ(Sช\ษฒฅห0cส,ธคะฬ8s๊ษณงฯ@
JดจัฃH&๕`SฉำงPฃJJตชีซXณjส๕โ:sศัตฌูณhำช]หถญทj70ัยไBฤ,ธKฃ*]hNรpฌ@หธฑใว#KLนrฮ
ฒ๕ํ&Mภฤ
Sวzฑฯ฿(|a๘ฐํ>phXอปท๏฿ภ>ๆอศ๛ekแัคI8หธีฌณรฒ8a
ตoCผผ๙๓Iว`ร6ฆ๙%ุ์JฬQฎ{DlKไcห=๚ ่ะว%\6 ] ]tา%ดมud'แ8ฐ!4FBVงกv่แ%qใลมgW4๙=C:แงE้ไ`Wคศจใฤ=bเ8๐ข@&0hคiฉ&แ๊ผๆBง!b๒hๅXไA> dโ^x0คB๔ูลธม"~G1Yฦ)'\๘#gt๙NgใDศคvU๔A9Lฉ!Ys6๊จy?O4ฆ<C2ุDใ6นh
้\86qD$ุpร8ฒ^4ขfฒ6U<ัpโล6^@0c2*PฐภRL4x)7>Pฑฦ>ภ_`4ะฒ^ฉv๋ํSL๙#ิ TF.ภ๊@Xส$]tจA#(*฿ๆซ/d\C>m#xP7ๆg
7ํ ;ะ&ตC><เ-V
ไ
ั`๋มุฃ[!ษฤซ์
ระMฉqไSซฐ^Plไฐมหส\*S1
Tม๋พL7โจ
A็ขๅ>ะปK6๒$ข๖ฦจำdอีฟญุ\ฺชฐ(*41ซ4$l9pcวบ์-ถ"ุ3oษศG๘?p+6}Rฬ~lแoZNsกdb๖ๆnิศ[ะVGงรตหuvX|}Sยฑn็ดืn๚๔[ท@Uดํฉ#
6,RL2ษิwมฌpcด*.@ๅคc๕ุณHP:^x๑ฯ,-V.)ภCk2ต?หึ๖WE>ฏrํ๘็? +ืAค+h"์pm"Xฺผกฐ)FเMะfนๆจ'a0ES21] n(ฦg~ ผ~แ p[ฟฆ'31$FD4ฤDbt็pื:๐ไUะArึขh๐ธI๐ฤLแย@ HฅCAฑบ&5Q^Pฝศฤ.zั$|@4aฑb๘Na$ฤLอ]ฬ๘+๒๔ ั@FEลขg9ษ่ถ0ลhP อ์Dำแ{ณ" -fฬa12-|๑ด์d l\๑{BะDMIRศ
BุQiiฅ,;๙DJWv8cำ8*ูH,2TวrLฑํ=นยF,p ยฐP"jz ฐiซ)ๆ{ ๚โXฌhสุภ& @ๆ#ฅ:ึNrๆ e๐ษX็ญQ*R๑ph
่. #D๎1จA`
&ศกhพ2s 1H2ขIีส5ภBVy@@,9(DWสาบ๔ฅC1ฉ\่๒R/xัKึb
xภ/Xจ)C| ำข๕จHM๊L.ฐ6d/6HT
BRฅZ๕ชXอชVRีญz๕ซ`
ซXGาีฑ๕ฌhM+Zชึถบ๕ญp-jYใJืบฺ๕ฎต+^๗สืพ๚ีJzซ`Kุยnล ฌaหุฦ:ึ'ญ[KูสZ๖ฒ*,Eูฮz๖ณ Ef'ยูะ๖ดจ]์h%Rฺิบ๖ตฐ
๋j#าฺฺุ๖ถธอ๊l!2ู๚๖ทภM๊j7ฐ6ดa
ญmpหๆJpตKtใ`#(ืนุอฎvถฺ6L7zฝ๎vวK๒๒hต฿nlฬห๖บK่M๏H฿K฿๚ฺื<๑๎|๏ห฿๚72๙๎~Kเธ,V๏>ฬเ;๘) ๆVBz๛เ
[๘ย7๐1ฬแ{๘$^๐GLโc$ฤฎ3ฑWฬโ ุบ-ฑUโwฦ8ฮ๑{kLU๛๘วๆฑกLไ"ืWศนฑฬไิ"Mฒ๛d(yสXฮฒaซ+k๙ห`ฦ+ฝๆ2Yญ\ฆ๐ืฬfปy+ศ:พ ฟ0งอx.๑ต\ใS%ส
n ฬ # ๔ SภhAF"็J๗wฯYแฯเ5$B*P?Fญ
Fo ฃ๕$TโhaศcZxฤ?q
Pฺาธno!eย"0x
ุ^จH๚g.iธuEุ0็PชHร บppา@}จc7pขาE์ใษ@+บ๎ดZง`B~ฏใPvA&๐r฿ฆNฟๅ1ถ๚6AไMopเCxย8ม C Bทr๑o| xศGnX ใR ภsMsช์๚!ุhฐฆ0ณDำ่จก
B๔ซ$\@@R0uจยแภ>ยอฎMBXFภ ๎\@าจว
f.ีGใx49"
๗JDบ @%๐อW_]!% ว:๘P Z4๐/|ญ)r๖ดฏ"0ฯฯ'n=pLะ'์ๅRญๅ[๙อw๓ฐดภภy'ผ@ค5}W0อ
ก่W$6wดฤ๖0ล,
jำ$ก๗Tฟ๐x`v@น`Tฏ:๋ยฆx้๐๎vๆ
มื๗ดภva 9A4เ๒ศ ๒'ไ๑หฟd"O๐Rเ ๐ q๑.เ"ฐ' (
๘ศลNภO '{$Hดทบ็P]PA0๑ 9al!SPะ] ฐไ T
้ ๔ ฿Pฟเ&gf@ฃ
ีอ๐Lทเ`
ัเ แ๐u!ภส ็ฒ๐ ฮ็็เ"Vpแเ
)pะเ
Kpึph
แเๆ gtกuื๗'็&7[ว( ๙์p ฬ๐
๙Pk
ืoำ~-`wฃธ๋ ๊ทฐท๒ oา/%๐อ wPoqฃผh!๐
พ jp๐X1๙ ฟp๑rXM0yั๘ O`กz;ะเhโจ๔=ะฝW๐'E8TyโD:PK DฦF9ฐS`เ 9pb@?ภL ( ป0ฐGXฦะ๐จPOฬvq
ํ๗ Vh
V `|฿(,ษt7`\pฺภ0h0
แะ ^Z๐6 ี lpณP^0
์0
)yจฐฐGห`ภหม|.VngPจomPป@ ้
Pัๆผ
ะooฆ8u ขึ ผฐxLภไp
Eำฐ-0|เZiยูyภ h๙ ภ ์phdi)0y
ะะ
qีXฎ{qะvM๐t฿ธ-๔1ฐrไNคผ้m!/ฐ , ุส a^jV$@ิYP"ฐ;t-d{]PPW@|๘ h็
p๘๐pิPOf
7`อ e
บเ
wg {t่ Ei" ,'_ .{l
ฒ:"U"ษยง|๎แฅฐu0๛๐(๗แ หฐูB9 ข bษ๎1ศ(Uชศฐsw
s7
9zฐฎoซx๗๋0i ข~ำเS๊(บPRJฅ[`ฅ
9:ฝ 4ส]bR( bธOเใ๗?ฺ8ทIง zซ็QงI ง
!ช หyจFqsP)ฅแ)ฤฆOแ3ะ/ Kะ
์ ฉ9Ghk'ทภKใ h06 ่๐ขูุบ`เ๐Rw# iyงh8W`ka|ิgh่่ ร๚gS\g7u`u viชม|eื5*Pา pต}ชฐ yEJ๋j๋ท I*ะ P๒0Xสำxชภ๒p|๙บฏไะฏช$
a๊ฅi๚ :$๘
๏ กl๊wงมงนษจท๛ z๘0 z>pษจ.;๓%๕(UC
t๖aฉกm QสcJ70ชฅzช&Jภฯ๖iม ฺt^PPWiฏะN9Q:
ฺbLะิQ๕กVWpาญฯ
P๑|YเBQฃ๗d@ะ๐คฐฃูจุ%ํบPzทฅธโึ/พhค*s[x ์ฐน6J|ะ
๐^ [Okสฆขดษตูฑพ)ฆ้(1ฒSปฐTป
ป๏๘ฒภ1ซนลF๕Hฃล::;:0ช๎ฉมเ
คzFซฮฆ
สสIซถj^H
zแlU+ฆZอแlw zpqgKK๊๐ฌNคV'#ฟน8 21บ(ศฯป&ภเ
dqน xธ๑h๖ I*์g๔Rีฆ
งlvยบHย๛ท