├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .node-version
├── README.md
├── babel.config.js
├── examples
├── script.tsx
└── tsconfig.json
├── package-lock.json
├── package.json
├── renovate.json
├── src
├── __tests__
│ └── index.test.tsx
├── fs-renderer-host-config.ts
├── fs-renderer-types.ts
├── fs-renderer.ts
└── index.ts
├── tsconfig.json
└── types
└── global
└── index.d.ts
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | name: Node.js ${{ matrix.os }} ${{ matrix.node-version }}
9 | runs-on: ${{ matrix.os }}
10 |
11 | strategy:
12 | matrix:
13 | node-version: [12.10.x]
14 | os: [ubuntu-latest, windows-latest]
15 |
16 | steps:
17 | - uses: actions/checkout@v1
18 | - name: Use Node.js ${{ matrix.node-version }}
19 | uses: actions/setup-node@v1
20 | with:
21 | node-version: ${{ matrix.node-version }}
22 | - name: yarn install, and yarn test
23 | run: |
24 | yarn install
25 | yarn test
26 | env:
27 | CI: true
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib/
2 | node_modules/
3 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 12.13.0
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @koba04/react-fs
2 | [](https://github.com/koba04/react-fs/actions?workflow=test)
3 |
4 | **The GitHub repository will be published soon!!**
5 |
6 | A React custom renderer for file system APIs.
7 |
8 |
9 | ## Install
10 |
11 | This package depends on `recursive` option of `fs.rmdirSync`, so you have to use Node.js higher that `v12.10.0`.
12 |
13 | ```
14 | npm i @koba04/react-fs
15 | ```
16 |
17 | ## How to use
18 |
19 | ```js
20 | const React = require('react');
21 | const { ReactFS } = require('@koba04/react-fs');
22 |
23 | const targetDir = "test-react-fs-project";
24 | ReactFS.render(
25 | <>
26 |
27 | # Title
28 |
29 |
30 |
31 | console.log("Hello");
32 |
33 |
34 | >,
35 | targetDir
36 | );
37 | ```
38 |
39 | ## TypeScript
40 |
41 | If you use `@koba04/react-fs` with TypeScript, you have to edit `typeRoots` like the following.
42 |
43 | ```json
44 | "typeRoots": [
45 | "node_modules/@types",
46 | "node_modules/@koba04/react-fs/types",
47 | ]
48 | ```
49 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ["@babel/preset-env", { targets: { node: "current" } }],
4 | "@babel/preset-react",
5 | "@babel/preset-typescript"
6 | ]
7 | };
8 |
--------------------------------------------------------------------------------
/examples/script.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { ReactFS } from "../src";
3 | import readline from "readline";
4 |
5 | const askQuestion = (): Promise => {
6 | return new Promise(resolve => {
7 | const rl = readline.createInterface({
8 | input: process.stdin,
9 | output: process.stdout
10 | });
11 | rl.question("input a text: ", (text: string) => {
12 | if (text == null) return;
13 | resolve(text);
14 | rl.close();
15 | });
16 | });
17 | };
18 |
19 | const App = () => {
20 | const [name, setName] = useState("");
21 |
22 | useEffect(() => {
23 | (async () => {
24 | const answer = await askQuestion();
25 | console.log("answer is ", answer);
26 | setName(answer);
27 | })();
28 | }, [name]);
29 | return (
30 | <>
31 |
32 | const hello = "hello";
33 |
34 |
35 | {name && {name}}
36 |
37 | # Hello File Renderer
38 | >
39 | );
40 | };
41 |
42 | ReactFS.render(, "./demo");
43 |
--------------------------------------------------------------------------------
/examples/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "exclude": []
4 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@koba04/react-fs",
3 | "version": "0.1.3",
4 | "description": "A React custom renderer for file system APIs",
5 | "main": "lib/index.js",
6 | "license": "MIT",
7 | "author": "koba04",
8 | "scripts": {
9 | "build": "run-s clean tsc",
10 | "clean": "rimraf lib",
11 | "example": "ts-node examples/script.tsx",
12 | "start": "tsc --watch",
13 | "test": "jest src",
14 | "tsc": "tsc"
15 | },
16 | "types": "lib/index.d.ts",
17 | "files": [
18 | "lib",
19 | "types"
20 | ],
21 | "keywords": [
22 | "react",
23 | "react-reconciler",
24 | "react-renderer",
25 | "fs"
26 | ],
27 | "engines": {
28 | "node": ">=12.10"
29 | },
30 | "publishConfig": {
31 | "access": "public"
32 | },
33 | "dependencies": {
34 | "react-reconciler": "^0.24.0"
35 | },
36 | "peerDependencies": {
37 | "react": "^16.0.0"
38 | },
39 | "devDependencies": {
40 | "@babel/preset-env": "^7.7.7",
41 | "@babel/preset-react": "^7.7.4",
42 | "@babel/preset-typescript": "^7.7.7",
43 | "@types/jest": "^24.0.25",
44 | "@types/node": "^12.12.42",
45 | "@types/react": "^16.9.35",
46 | "@types/react-reconciler": "^0.18.0",
47 | "jest": "^24.9.0",
48 | "npm-run-all": "^4.1.5",
49 | "react": "^16.12.0",
50 | "rimraf": "^3.0.2",
51 | "ts-node": "^8.8.2",
52 | "typescript": "^3.7.5"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "@cybozu"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/src/__tests__/index.test.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import { ReactFS } from "../index";
3 | import path from "path";
4 | import { tmpdir } from "os";
5 | import { rmdirSync, readFileSync, statSync, existsSync } from "fs";
6 |
7 | const waitEffect = () => new Promise(r => setTimeout(r, 0));
8 |
9 | describe("ReactFS", () => {
10 | const tempDir = path.join(tmpdir(), "react-fs");
11 | console.log("tempDir is ", tempDir);
12 | afterEach(() => {
13 | rmdirSync(tempDir, { recursive: true });
14 | });
15 | describe("create a file and directory", () => {
16 | it("should be able to create a file", () => {
17 | ReactFS.render(Hello World, tempDir);
18 | expect(readFileSync(path.join(tempDir, "test.txt")).toString()).toBe(
19 | "Hello World"
20 | );
21 | });
22 | it("should be able to create a directory", () => {
23 | ReactFS.render(, tempDir);
24 | expect(statSync(path.join(tempDir, "test")).isDirectory()).toBe(true);
25 | });
26 | });
27 | describe("create a file into a directory", () => {
28 | it("should be able to create a file into a directory", () => {
29 | ReactFS.render(
30 |
31 | Hello World
32 | ,
33 | tempDir
34 | );
35 | expect(
36 | readFileSync(path.join(tempDir, "foo", "test.txt")).toString()
37 | ).toBe("Hello World");
38 | });
39 | it("should be able to add a new file", async () => {
40 | const App = () => {
41 | const [text, setText] = useState("");
42 | useEffect(() => {
43 | setText("new");
44 | }, []);
45 | return (
46 |
47 | {text && 123}
48 |
49 | );
50 | };
51 |
52 | ReactFS.render(, tempDir);
53 | expect(existsSync(path.join(tempDir, "foo", "new.txt"))).toBe(false);
54 | await waitEffect();
55 | expect(
56 | readFileSync(path.join(tempDir, "foo", "new.txt")).toString()
57 | ).toBe("123");
58 | });
59 | it("should be able to create a file into a nested directory", () => {
60 | ReactFS.render(
61 |
62 |
63 | Hello World
64 |
65 | ,
66 | tempDir
67 | );
68 | expect(
69 | readFileSync(path.join(tempDir, "foo", "bar", "test.txt")).toString()
70 | ).toBe("Hello World");
71 | });
72 | it("should be able to create multiple fles into a directory", () => {
73 | ReactFS.render(
74 |
75 | Foo
76 | Bar
77 | ,
78 | tempDir
79 | );
80 | expect(
81 | readFileSync(path.join(tempDir, "multiple", "foo.txt")).toString()
82 | ).toBe("Foo");
83 | expect(
84 | readFileSync(path.join(tempDir, "multiple", "bar.txt")).toString()
85 | ).toBe("Bar");
86 | });
87 | });
88 | describe("update a content and file name", () => {
89 | it("should be able to update a content of a file", async () => {
90 | const App = () => {
91 | const [text, setText] = useState("initial");
92 | useEffect(() => {
93 | setText("updated");
94 | }, []);
95 | return {text};
96 | };
97 |
98 | ReactFS.render(, tempDir);
99 | expect(readFileSync(path.join(tempDir, "foo.txt")).toString()).toBe(
100 | "initial"
101 | );
102 | await waitEffect();
103 | expect(readFileSync(path.join(tempDir, "foo.txt")).toString()).toBe(
104 | "updated"
105 | );
106 | });
107 | it("should be able to update a file name", async () => {
108 | const App = () => {
109 | const [text, setText] = useState("initial");
110 | useEffect(() => {
111 | setText("updated");
112 | }, []);
113 | return 123;
114 | };
115 |
116 | ReactFS.render(, tempDir);
117 | expect(readFileSync(path.join(tempDir, "initial.txt")).toString()).toBe(
118 | "123"
119 | );
120 | await waitEffect();
121 | expect(readFileSync(path.join(tempDir, "updated.txt")).toString()).toBe(
122 | "123"
123 | );
124 | expect(existsSync(path.join(tempDir, "initial.txt"))).toBe(false);
125 | });
126 | });
127 | describe("get a public instance", () => {
128 | it("should be able to get an instance filtered rootContainerInstance through ref", async () => {
129 | let ref: any;
130 | const App = () => {
131 | ref = useRef(null);
132 | return (
133 | <>
134 |
135 | const num = 1;
136 |
137 | >
138 | );
139 | };
140 |
141 | ReactFS.render(, tempDir);
142 | expect(ref.current).toEqual({
143 | type: "file",
144 | props: { name: "index.js", children: "const num = 1;" }
145 | });
146 | });
147 | });
148 | });
149 |
--------------------------------------------------------------------------------
/src/fs-renderer-host-config.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Instance,
3 | HostContext,
4 | Props,
5 | Container,
6 | TextInstance,
7 | Type,
8 | UpdatePayload
9 | } from "./fs-renderer-types";
10 | import path from "path";
11 | import { writeFileSync, existsSync, mkdirSync, renameSync } from "fs";
12 |
13 | const HOST_CONTEXT: HostContext = {};
14 |
15 | export const getPublicInstance = (instance: Instance) => {
16 | const { rootContainerInstance, ...rest } = instance;
17 | return rest;
18 | };
19 | export const getRootHostContext = (): HostContext => HOST_CONTEXT;
20 | export const getChildHostContext = () => HOST_CONTEXT;
21 |
22 | export const prepareForCommit = () => {};
23 | export const resetAfterCommit = () => {};
24 | export const createInstance = (
25 | type: string,
26 | props: Props,
27 | rootContainerInstance: Container
28 | ): Instance => ({ type, props, rootContainerInstance });
29 | export const createTextInstance = (
30 | text: string,
31 | rootContainerInstance: Container
32 | ): TextInstance => ({ text, rootContainerInstance });
33 | export const appendInitialChild = (
34 | parentInstance: Instance,
35 | child: Instance | TextInstance
36 | ) => {
37 | child.parent = parentInstance;
38 | };
39 | export const finalizeInitialChildren = () => true;
40 | export const prepareUpdate = () => ({});
41 | export const shouldSetTextContent = () => false;
42 | export const shouldDeprioritizeSubtree = () => false;
43 |
44 | export const appendChildToContainer = () => {};
45 |
46 | const buildParentPath = (instance: Instance | TextInstance) => {
47 | const names = [];
48 | let current = instance.parent;
49 | while (current) {
50 | names.push(current.props.name);
51 | current = current.parent;
52 | }
53 | return path.join(instance.rootContainerInstance.rootPath, ...names.reverse());
54 | };
55 |
56 | export const commitMount = (
57 | instance: Instance,
58 | type: Type,
59 | newProps: Props
60 | ) => {
61 | const parentPath = buildParentPath(instance);
62 | const targetPath = path.join(parentPath, newProps.name);
63 |
64 | if (!existsSync(parentPath)) {
65 | mkdirSync(parentPath, { recursive: true });
66 | }
67 |
68 | if (type === "file") {
69 | writeFileSync(targetPath, newProps.children);
70 | } else if (type === "directory" && !existsSync(targetPath)) {
71 | mkdirSync(targetPath);
72 | }
73 | };
74 |
75 | export const commitUpdate = (
76 | instance: Instance,
77 | updatePayload: UpdatePayload,
78 | type: Type,
79 | oldProps: Props,
80 | newProps: Props
81 | ) => {
82 | if (oldProps.name !== newProps.name) {
83 | instance.props = newProps;
84 | renameSync(
85 | path.join(buildParentPath(instance), oldProps.name),
86 | path.join(buildParentPath(instance), newProps.name)
87 | );
88 | }
89 | };
90 |
91 | export const commitTextUpdate = (
92 | textInstance: TextInstance,
93 | oldText: string,
94 | newText: string
95 | ) => {
96 | if (oldText !== newText) {
97 | textInstance.text = newText;
98 | writeFileSync(buildParentPath(textInstance), newText);
99 | }
100 | };
101 | export const removeChild = () => {};
102 | export const appendChild = (
103 | parentInstance: Instance,
104 | child: Instance | TextInstance
105 | ) => {
106 | child.parent = parentInstance;
107 | };
108 |
109 | export const scheduleDeferredCallback = () => {};
110 | export const cancelDeferredCallback = () => {};
111 | export const setTimeout = global.setTimeout;
112 | export const clearTimeout = global.clearTimeout;
113 | export const noTimeout = {};
114 | export const now = () => Date.now();
115 |
116 | export const isPrimaryRenderer = true;
117 | export const supportsMutation = true;
118 | export const supportsPersistence = false;
119 | export const supportsHydration = false;
120 |
--------------------------------------------------------------------------------
/src/fs-renderer-types.ts:
--------------------------------------------------------------------------------
1 | export type Type = string;
2 | export type Props = {
3 | [key: string]: any;
4 | };
5 |
6 | export type TextInstance = {
7 | text: string;
8 | parent?: Instance;
9 | rootContainerInstance: Container;
10 | };
11 |
12 | export type Instance = {
13 | type: Type;
14 | props: Props;
15 | parent?: Instance;
16 | rootContainerInstance: Container;
17 | };
18 |
19 | export type PublicInstance =
20 | | Omit
21 | | Omit;
22 |
23 | export type Container = {
24 | rootPath: string;
25 | };
26 |
27 | export type HostContext = {};
28 |
29 | export type HydratableInstance = object;
30 | export type UpdatePayload = object;
31 | export type ChildSet = object;
32 | export type TimeoutHandle = object;
33 | export type NoTimeout = object;
34 | export type OpaqueHandle = any;
35 |
--------------------------------------------------------------------------------
/src/fs-renderer.ts:
--------------------------------------------------------------------------------
1 | import Reconciler from "react-reconciler";
2 | import * as HostConfig from "./fs-renderer-host-config";
3 | import {
4 | Type,
5 | Props,
6 | Instance,
7 | TextInstance,
8 | HydratableInstance,
9 | PublicInstance,
10 | HostContext,
11 | UpdatePayload,
12 | ChildSet,
13 | TimeoutHandle,
14 | NoTimeout,
15 | Container
16 | } from "./fs-renderer-types";
17 |
18 | // eslint-disable-next-line new-cap
19 | export const FSRenderer = Reconciler<
20 | Type,
21 | Props,
22 | Container,
23 | Instance,
24 | TextInstance,
25 | HydratableInstance,
26 | PublicInstance,
27 | HostContext,
28 | UpdatePayload,
29 | ChildSet,
30 | TimeoutHandle,
31 | NoTimeout
32 | >(HostConfig);
33 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FSRenderer } from "./fs-renderer";
3 | import { Container } from "./fs-renderer-types";
4 | import ReactReconciler from "react-reconciler";
5 | import { rmdirSync } from "fs";
6 |
7 | type RootContainer = {
8 | fiberRoot?: ReactReconciler.FiberRoot;
9 | container: Container;
10 | };
11 |
12 | const rootContainerMap = new Map();
13 |
14 | export const ReactFS = {
15 | render(element: React.ReactNode, rootPath: string) {
16 | let rootContainer = rootContainerMap.get(rootPath);
17 | if (!rootContainer) {
18 | rootContainer = {
19 | container: {
20 | rootPath
21 | }
22 | };
23 | rootContainerMap.set(rootPath, rootContainer);
24 | }
25 |
26 | // First, we remove the root to clean up.
27 | // TODO: support hydration
28 | rmdirSync(rootPath, { recursive: true });
29 |
30 | rootContainer.fiberRoot = FSRenderer.createContainer(
31 | rootContainer.container,
32 | false,
33 | false
34 | );
35 | FSRenderer.updateContainer(
36 | element,
37 | rootContainer.fiberRoot,
38 | null,
39 | () => {}
40 | );
41 | }
42 | };
43 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | // "incremental": true, /* Enable incremental compilation */
5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
7 | "lib": ["es2015"], /* Specify library files to be included in the compilation. */
8 | // "allowJs": true, /* Allow javascript files to be compiled. */
9 | // "checkJs": true, /* Report errors in .js files. */
10 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11 | "declaration": true, /* Generates corresponding '.d.ts' file. */
12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
13 | // "sourceMap": true, /* Generates corresponding '.map' file. */
14 | // "outFile": "./", /* Concatenate and emit output to single file. */
15 | "outDir": "./lib", /* Redirect output structure to the directory. */
16 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
17 | // "composite": true, /* Enable project compilation */
18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
19 | // "removeComments": true, /* Do not emit comments to output. */
20 | // "noEmit": true, /* Do not emit outputs. */
21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
24 |
25 | /* Strict Type-Checking Options */
26 | "strict": true, /* Enable all strict type-checking options. */
27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
28 | // "strictNullChecks": true, /* Enable strict null checks. */
29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
34 |
35 | /* Additional Checks */
36 | // "noUnusedLocals": true, /* Report errors on unused locals. */
37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
40 |
41 | /* Module Resolution Options */
42 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
46 | "typeRoots": [
47 | "types",
48 | "node_modules/@types",
49 | "../../node_modules/@types",
50 | ], /* List of folders to include type definitions from. */
51 | // "types": [], /* Type declaration files to be included in compilation. */
52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
53 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
56 |
57 | /* Source Map Options */
58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
62 |
63 | /* Experimental Options */
64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
66 | },
67 | "exclude": [
68 | "examples",
69 | "lib"
70 | ]
71 | }
72 |
--------------------------------------------------------------------------------
/types/global/index.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace JSX {
2 | interface IntrinsicElements {
3 | file: {
4 | name: string;
5 | children: React.ReactNode;
6 | ref?: import("react").MutableRefObject;
7 | };
8 | directory: {
9 | name: string;
10 | children?: React.ReactNode;
11 | };
12 | }
13 | }
14 |
--------------------------------------------------------------------------------