Skip to content

Commit 2819911

Browse files
committed
Merge branch 'master' into pietro909/master
2 parents 69ab782 + 3834b5e commit 2819911

25 files changed

+911
-952
lines changed

.editorconfig

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# https://editorconfig.org
2-
31
root = true
42

53
[*]

.eslintrc.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
11
{
2-
"extends": ["env"]
2+
"extends": ["env"],
3+
"settings": {
4+
"jsdoc": {
5+
"mode": "typescript"
6+
}
7+
},
8+
"rules": {
9+
"jsdoc/check-tag-names": "off",
10+
"jsdoc/require-description": "off",
11+
"jsdoc/require-returns": "off",
12+
"jsdoc/valid-types": "off"
13+
}
314
}

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
strategy:
88
matrix:
99
os: [ubuntu-latest, macos-latest]
10-
node: ['12', '14', '16']
10+
node: ["12", "14", "16", "17"]
1111
steps:
1212
- uses: actions/checkout@v2
1313
- name: Setup Node.js v${{ matrix.node }}

.prettierrc.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
{
2-
"proseWrap": "never",
3-
"singleQuote": true
2+
"proseWrap": "never"
43
}

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"typescript.disableAutomaticTypeAcquisition": true,
3+
"typescript.enablePromptUseWorkspaceTsdk": true,
4+
"typescript.tsdk": "node_modules/typescript/lib"
5+
}

changelog.md

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,45 @@
22

33
## Next
44

5+
### Major
6+
7+
- Updated Node.js support to `^12.22.0 || ^14.17.0 || >= 16.0.0`.
8+
- Added a new [`is-plain-obj`](https://npm.im/is-plain-obj) dependency that is ESM.
9+
- Updated dev dependencies, some of which require newer Node.js versions than previously supported.
10+
- Public modules are now individually listed in the package `files` and `exports` fields.
11+
- Removed `./package` from the package `exports` field; the full `package.json` filename must be used in a `require` path.
12+
- Removed the package main index module; deep imports must be used.
13+
- Shortened public module deep import paths, removing the `/public/`.
14+
- The API is now ESM in `.mjs` files instead of CJS in `.js` files, [accessible via `import` but not `require`](https://nodejs.org/dist/latest/docs/api/esm.html#require).
15+
- Implemented TypeScript types via JSDoc and `@deno-types` comments.
16+
- Changed the function `extractFiles` parameters. The previously third `isExtractableFile` parameter has been renamed `isExtractable`, is now the second parameter, and no longer defaults to the function `isExtractableFile` to avoid a redundant import when a custom function is specified.
17+
- The function `extractFiles` now does basic runtime argument type validation.
18+
- The function `extractFiles` now also deep clones “plain” objects that aren’t `Object` instances (e.g. `Object.create(null)`).
19+
- Removed out of the box React Native support. The class `ReactNativeFile` is no longer exported, or matched by the function `isExtractableFile`.
20+
21+
This class was bloating non React Native environments with an extra module, increasing bundle sizes when building and adding an extra step to ESM loading waterfalls in browsers.
22+
23+
It’s the responsibility of Facebook to adhere to web standards and implement spec-complaint `File`, `Glob`, and `FormData` globals in the React Native environment.
24+
25+
In the meantime, React Native projects can manually implement a class `ReactNativeFile` and match it with a custom function `isReactNativeFile` for use with the function `extractFiles`.
26+
527
### Patch
628

29+
- Also run GitHub Actions CI with Node.js v17.
30+
- Simplified package scripts.
31+
- Check TypeScript types via a new package `types` script.
32+
- Removed the [`jsdoc-md`](https://npm.im/jsdoc-md) dev dependency and the related package scripts, replacing the readme “API” section with a manually written “Exports” section.
33+
- Reorganized the test file structure.
34+
- Test the bundle sizes for public modules individually.
35+
- Use a new `assertBundleSize` function to assert module bundle size in tests:
36+
- Failure message contains details about the bundle size and how much the limit was exceeded.
37+
- Errors when the surplus is greater than 25% of the limit, suggesting the limit should be reduced.
38+
- Resolves the minified bundle and its gzipped size for debugging in tests.
39+
- Fixed an `extractFiles` function test bug.
40+
- Added an `extractFiles` function test clarifying that object properties with `Symbol` keys don’t get cloned.
41+
- Configured Prettier option `singleQuote` to the default, `false`.
42+
- Updated the package description.
43+
- Documentation tweaks.
744
- Amended the changelog entry for v10.0.0.
845

946
## 11.0.0
@@ -85,22 +122,22 @@
85122
- Deep imports to specific files are now allowed, e.g.
86123

87124
```js
88-
import extractFiles from 'extract-files/lib/extractFiles.js';
125+
import extractFiles from "extract-files/lib/extractFiles.js";
89126
```
90127

91128
```js
92-
const extractFiles = require('extract-files/lib/extractFiles');
129+
const extractFiles = require("extract-files/lib/extractFiles");
93130
```
94131

95132
- The `package.json` can now be required, e.g.
96133

97134
```js
98-
const pkg = require('extract-files/package.json');
135+
const pkg = require("extract-files/package.json");
99136
```
100137

101138
```js
102139
// With Node.js --experimental-json-modules flag.
103-
import pkg from 'extract-files/package.json';
140+
import pkg from "extract-files/package.json";
104141
```
105142

106143
### Patch

extractFiles.mjs

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// @ts-check
2+
3+
// @deno-types="is-plain-obj/index.d.ts"
4+
import isPlainObject from "is-plain-obj";
5+
6+
/** @typedef {import("./isExtractableFile.mjs").default} isExtractableFile */
7+
8+
/**
9+
* Recursively extracts files and their {@link ObjectPath object paths} within a
10+
* value, replacing them with `null` in a deep clone without mutating the
11+
* original value.
12+
* [`FileList`](https://developer.mozilla.org/en-US/docs/Web/API/Filelist)
13+
* instances are treated as
14+
* [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) instance
15+
* arrays.
16+
* @template Extractable Extractable file type.
17+
* @param {unknown} value Value to extract files from. Typically an object tree.
18+
* @param {(value: unknown) => value is Extractable} isExtractable Matches extractable files. Typically
19+
* {@linkcode isExtractableFile}.
20+
* @param {ObjectPath} [path] Prefix for object paths for extracted files.
21+
* Defaults to `""`.
22+
* @returns {Extraction<Extractable>} Extraction result.
23+
* @example
24+
* Extracting files from an object.
25+
*
26+
* For the following:
27+
*
28+
* ```js
29+
* import extractFiles from "extract-files/extractFiles.mjs";
30+
* import isExtractableFile from "extract-files/isExtractableFile.mjs";
31+
*
32+
* const file1 = new File(["1"], "1.txt", { type: "text/plain" });
33+
* const file2 = new File(["2"], "2.txt", { type: "text/plain" });
34+
* const value = {
35+
* a: file1,
36+
* b: [file1, file2],
37+
* };
38+
*
39+
* const { clone, files } = extractFiles(value, isExtractableFile, "prefix");
40+
* ```
41+
*
42+
* `value` remains the same.
43+
*
44+
* `clone` is:
45+
*
46+
* ```json
47+
* {
48+
* "a": null,
49+
* "b": [null, null]
50+
* }
51+
* ```
52+
*
53+
* `files` is a
54+
* [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
55+
* instance containing:
56+
*
57+
* | Key | Value |
58+
* | :------ | :--------------------------- |
59+
* | `file1` | `["prefix.a", "prefix.b.0"]` |
60+
* | `file2` | `["prefix.b.1"]` |
61+
*/
62+
export default function extractFiles(value, isExtractable, path = "") {
63+
if (!arguments.length) throw new TypeError("Argument 1 `value` is required.");
64+
65+
if (typeof isExtractable !== "function")
66+
throw new TypeError("Argument 2 `isExtractable` must be a function.");
67+
68+
if (typeof path !== "string")
69+
throw new TypeError("Argument 3 `path` must be a string.");
70+
71+
/**
72+
* Deeply clonable value.
73+
* @typedef {Array<unknown> | FileList | Record<PropertyKey, unknown>} Cloneable
74+
*/
75+
76+
/**
77+
* Clone of a {@link Cloneable deeply cloneable value}.
78+
* @typedef {Exclude<Cloneable, FileList>} Clone
79+
*/
80+
81+
/**
82+
* Map of values recursed within the input value and their clones, for reusing
83+
* clones of values that are referenced multiple times within the input value.
84+
* @type {Map<Cloneable, Clone>}
85+
*/
86+
const clones = new Map();
87+
88+
/**
89+
* Extracted files and their object paths within the input value.
90+
* @type {Extraction<Extractable>["files"]}
91+
*/
92+
const files = new Map();
93+
94+
/**
95+
* Recursively clones the value, extracting files.
96+
* @param {unknown} value Value to extract files from.
97+
* @param {ObjectPath} path Prefix for object paths for extracted files.
98+
* @param {Set<Cloneable>} recursed Recursed values for avoiding infinite
99+
* recursion of circular references within the input value.
100+
* @returns {unknown} Clone of the value with files replaced with `null`.
101+
*/
102+
function recurse(value, path, recursed) {
103+
if (isExtractable(value)) {
104+
const filePaths = files.get(value);
105+
106+
filePaths ? filePaths.push(path) : files.set(value, [path]);
107+
108+
return null;
109+
}
110+
111+
const valueIsList =
112+
Array.isArray(value) ||
113+
(typeof FileList !== "undefined" && value instanceof FileList);
114+
const valueIsPlainObject = isPlainObject(value);
115+
116+
if (valueIsList || valueIsPlainObject) {
117+
let clone = clones.get(value);
118+
119+
const uncloned = !clone;
120+
121+
if (uncloned) {
122+
clone = valueIsList
123+
? []
124+
: // Replicate if the plain object is an `Object` instance.
125+
value instanceof /** @type {any} */ (Object)
126+
? {}
127+
: Object.create(null);
128+
129+
clones.set(value, /** @type {Clone} */ (clone));
130+
}
131+
132+
if (!recursed.has(value)) {
133+
const pathPrefix = path ? `${path}.` : "";
134+
const recursedDeeper = new Set(recursed).add(value);
135+
136+
if (valueIsList) {
137+
let index = 0;
138+
139+
for (const item of value) {
140+
const itemClone = recurse(
141+
item,
142+
pathPrefix + index++,
143+
recursedDeeper
144+
);
145+
146+
if (uncloned) /** @type {Array<unknown>} */ (clone).push(itemClone);
147+
}
148+
} else
149+
for (const key in value) {
150+
const propertyClone = recurse(
151+
value[key],
152+
pathPrefix + key,
153+
recursedDeeper
154+
);
155+
156+
if (uncloned)
157+
/** @type {Record<PropertyKey, unknown>} */ (clone)[key] =
158+
propertyClone;
159+
}
160+
}
161+
162+
return clone;
163+
}
164+
165+
return value;
166+
}
167+
168+
return {
169+
clone: recurse(value, path, new Set()),
170+
files,
171+
};
172+
}
173+
174+
/**
175+
* An extraction result.
176+
* @template [Extractable=unknown] Extractable file type.
177+
* @typedef {object} Extraction
178+
* @prop {unknown} clone Clone of the original value with extracted files
179+
* recursively replaced with `null`.
180+
* @prop {Map<Extractable, Array<ObjectPath>>} files Extracted files and their
181+
* object paths within the original value.
182+
*/
183+
184+
/**
185+
* String notation for the path to a node in an object tree.
186+
* @typedef {string} ObjectPath
187+
* @see [`object-path` on npm](https://npm.im/object-path).
188+
* @example
189+
* An object path for object property `a`, array index `0`, object property `b`:
190+
*
191+
* ```
192+
* a.0.b
193+
* ```
194+
*/

0 commit comments

Comments
 (0)