Skip to content

Update to up-to-date tech #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"ocaml.sandbox": {
"kind": "esy",
"kind": "opam",
"root": "${workspaceFolder:fullstack-reason-react-demo}"
},
"[reason]": {
Expand Down
80 changes: 24 additions & 56 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,72 +1,40 @@
FROM node:16.3-alpine3.12 as build
FROM ocaml/opam:ubuntu-22.04-ocaml-5.1

ENV TERM=dumb
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib
RUN sudo apt-get update && sudo apt-get install -y libev-dev libssl-dev curl

RUN set NODE_OPTIONS=--max-old-space-size=30720
RUN sudo apt-get remove -y nodejs npm && \
sudo apt-get autoremove -y

RUN mkdir /esy
WORKDIR /esy
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && \
sudo apt-get update && \
sudo apt-get install -y nodejs && \
sudo npm install -g npm@latest

ENV NPM_CONFIG_PREFIX=/esy
RUN npm install -g esy@0.7.2

# Alpine image where
FROM node:16.3-alpine3.12 as esy

ENV TERM=dumb
ENV LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib

COPY --from=build /esy /esy

RUN apk add --no-cache ca-certificates wget bash curl perl-utils git patch gcc g++ musl-dev make m4 coreutils tar xz linux-headers

RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk
RUN apk add --no-cache glibc-2.28-r0.apk

ENV PATH=/esy/bin:$PATH
RUN sudo ln -sf /usr/bin/opam-2.2 /usr/bin/opam

WORKDIR /app

# Install npm dependencies
COPY package*.json ./
RUN npm ci --only=production
RUN opam --version

# Install esy dependencies
ADD esy.json esy.json
ADD esy.lock/ esy.lock/
RUN esy --version
RUN esy solve
RUN esy fetch
RUN esy build-dependencies
COPY *.opam ./
COPY Makefile ./

# Copy the project (move folder by folder instead of COPY . . to not override _esy folder)
COPY client/ client/
COPY shared/ shared/
COPY server/ server/
COPY dune dune
COPY dune-project dune-project
COPY webpack.config.js webpack.config.js
RUN opam install . --deps-only --with-test --with-doc --with-dev-setup

# Build client
RUN esy build
# Bundle client
RUN node_modules/.bin/webpack
# Build server
RUN esy dune build --profile=prod @@default
WORKDIR "/app/client"

FROM alpine:3.12 as run
COPY client/package.json ./package.json
COPY client/package-lock.json ./package-lock.json

RUN apk update && apk add --update libev gmp git
RUN sudo npm install

WORKDIR /app

RUN chmod -R 755 /var
COPY . .

# Copy server binary
COPY --from=esy /app/_build/default/server/server.exe /bin/server.exe
# Copy client artifacts
COPY --from=esy /app/static /static
RUN sudo chown -R opam:opam /app
RUN opam exec -- dune build @server --profile=dev

ENV SERVER_INTERFACE "0.0.0.0"
EXPOSE 8080

CMD ["/bin/server.exe"]
CMD ["opam", "exec", "--", "_build/default/server/server.exe"]
50 changes: 28 additions & 22 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,37 @@
name = fullstack-reason-react-demo
current_hash = $(shell git rev-parse HEAD | cut -c1-7)

ESY = esy
DUNE = esy dune
OPAM = opam
DUNE = opam exec -- dune
WEBPACK = npx webpack --progress

.PHONY: init
setup-githooks: ## Setup githooks
git config core.hooksPath .githooks

.PHONY: create-switch
create-switch: ## Create opam switch
opam switch create . 5.1.1 --deps-only --with-test -y

.PHONY: install
install: ## Install dependencies from esy.json and package.json
@$(ESY) install
@npm install
install:
opam install . --deps-only --with-test --with-doc --with-dev-setup

.PHONY: pin
pin: ## Pin dependencies
opam pin add styled-ppx "https://github.com/davesnx/styled-ppx.git#fix/upgrade-srr"
opam pin add server-reason-react "https://github.com/ml-in-barcelona/server-reason-react.git#main"

.PHONY: webpack
webpack: ## Bundle the JS code
@$(WEBPACK) --env development
.PHONY: init
init: setup-githooks create-switch pin install install-npm ## Create a local dev enviroment

.PHONY: webpack-prod
webpack-prod: ## Bundle the JS code for production
@$(WEBPACK) --env production
.PHONY: build
build: ## Build
@$(DUNE) build @server

.PHONY: webpack-watch
webpack-watch: ## Watch and bundle the JS code
@$(WEBPACK) --watch --env development
.PHONY: build-watch
build-watch: ## Build
@$(DUNE) build @server --watch

.PHONY: build-client
build-client: ## Build Reason code
Expand All @@ -36,27 +47,22 @@ build-client-watch: ## Watch reason code
build-server-prod: ## Build for production (--profile=prod)
@$(DUNE) build --profile=prod @server

.PHONY: build-server
build-server: ## Build the project, including non installable libraries and executables
@$(DUNE) build @server

.PHONY: start-server
start-server: ## Start the server
@$(DUNE) exec server/server.exe --watch

.PHONY: run
run: ## Start the server in dev mode
dev: ## Start the server in dev mode
@watchexec --no-ignore -w .processes/last_built_at.txt -r -c \
"clear; _build/default/server/server.exe"

.PHONY: watch
watch: ## Build in watch mode
@$(DUNE) build -w @client @server
@$(DUNE) build -w @server

.PHONY: clean
clean: ## Clean artifacts
@$(DUNE) clean
@rm -rf static/

.PHONY: format
format: ## Format the codebase with ocamlformat/refmt
Expand All @@ -80,4 +86,4 @@ docker-build: ## docker build

.PHONY: docker-run
docker-run: ## docker run
@docker run -d --platform linux/amd64 @$(name):$(current_hash)
@docker run -d --platform linux/amd64 $(name):$(current_hash)
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ The folder contains the library for shared code between `client` and `server`. d

The code of shared consist of an app to demostrate a few usages of server-reason-react implementations, such as server-reason-react.css or server-reason-react.js.

- `server-reason-react.css` is the implementation of bs-css in the server. Maintains the same API and does the same functionality as emotion.js but in the server. All Css.* methods are available and generates the hash of the classnames. It also adds a fn `Css.render_style_tag()` to render the resultant CSS in the page, with the intention to be called in native.
- `server-reason-react.css` is the implementation of bs-css in the server. Maintains the same API and does the same functionality as emotion.js but in the server. All CSS.* methods are available and generates the hash of the classnames. It also adds a fn `CSS.render_style_tag()` to render the resultant CSS in the page, with the intention to be called in native.
- `server-reason-react.belt` is the implementation of [Belt](https://rescript-lang.org/docs/manual/latest/api/belt) in pure OCaml.
- `server-reason-react.js` is an incomplete implementation of [Js](https://rescript-lang.org/docs/manual/latest/api/js)
12 changes: 7 additions & 5 deletions client/app.re → client/Index.re
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
let useState = a => {
let (value, setValue) = React.useState(_ => a);
let useState = initialValue => {
let (value, setValue) = React.useState(_ => initialValue);
let setValueStatic = value => setValue(_ => value);
(value, setValueStatic);
};

module Counter = {
[@react.component]
let make = (~name) => {
let (count, setCount) = useState(0);
let (count, setCount) = useState(2);

<div>
<p>
Expand All @@ -16,13 +16,15 @@ module Counter = {
)}
</p>
<button onClick={_ => setCount(count + 1)}>
{React.string("Click me")}
{React.string("Click me nooooow!")}
</button>
</div>;
};
};

switch (ReactDOM.querySelector("#root")) {
| Some(el) => ignore @@ ReactDOM.Client.hydrateRoot(el, <Shared_js.App />)
| Some(el) =>
let _root = ReactDOM.Client.hydrateRoot(el, <Shared_js.App />);
();
| None => ()
};
149 changes: 149 additions & 0 deletions client/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import esbuild from "esbuild";
import Fs from "node:fs/promises";
import Path from "node:path";
import { execSync } from "node:child_process";

async function generateBootstrapFile (output, content) {
let previousContent = undefined;
try {
previousContent = await Fs.readFile(output, "utf8");
} catch (e) {
if (e.code !== "ENOENT") {
throw e;
}
}
const contentHasChanged = previousContent !== content;
if (contentHasChanged) {
await Fs.writeFile(output, content, "utf8");
}
};

function extractClientComponentsPlugin(config) {
return {
name: "extract-client-components",
setup(build) {
if (config.output && typeof config.output !== "string") {
console.error("output must be a string");
return;
}
const output = config.output || "./bootstrap.js";

build.onStart(async () => {
if (!config.target) {
console.error("target is required");
return;
}
if (typeof config.target !== "string") {
console.error("target must be a string");
return;
}

const target = config.target;
try {
const bootstrapContent = execSync(
`opam exec -- server_reason_react.extract_client_components ${target}`,
{ encoding: "utf8" },
);
await generateBootstrapFile(output, bootstrapContent);
} catch (e) {
console.log("Extraction of client components failed:");
console.error(e);
return;
}
});

build.onResolve({ filter: /.*/ }, (args) => {
const isEntryPoint = args.kind === "entry-point";

if (isEntryPoint) {
return {
path: args.path,
namespace: "entrypoint",
};
}
return null;
});

let webpackRequireMock = `
window.__webpack_require__ = window.__webpack_require__ || ((id) => {
const component = window.__client_manifest_map[id];
if (!component) {
throw new Error(\`Could not find client component with id: \${id}\`);
}
return { __esModule: true, default: component };
});
window.__client_manifest_map = window.__client_manifest_map || {};`;

build.initialOptions.banner = {
js: webpackRequireMock,
};

build.onLoad({ filter: /.*/, namespace: "entrypoint" }, async (args) => {
const filePath = args.path.replace(/^entrypoint:/, "");
const entryPointContents = await Fs.readFile(filePath, "utf8");

const contents = `
require("${output}");
${entryPointContents}`;

return {
loader: "jsx",
contents,
resolveDir: Path.dirname(Path.resolve(process.cwd(), filePath)),
};
});
},
};
}

async function build(input, output, extract) {
let outdir = output;
let splitting = true;

let plugins = [];
if (extract) {
plugins.push(extractClientComponentsPlugin({ target: "app" }));
}

try {
const result = await esbuild.build({
entryPoints: [input],
bundle: true,
logLevel: "debug",
platform: "browser",
format: "esm",
splitting,
outdir,
plugins,
write: true,
});

console.log('Build completed successfully for "' + input + '"');
return result;
} catch (error) {
console.error("\nBuild failed:", error);
process.exit(1);
}
}

const input = process.argv[2];
const output = process.argv[3];

let parseExtract = (arg) => {
if (typeof arg == "string") {
if (arg.startsWith("--extract=")) {
return arg.split("--extract=")[1] === "true";
}
}

return false;
};

const extract = parseExtract(process.argv[4]);

if (!input) {
console.error("Please provide an input file path");
process.exit(1);
}

build(input, output, extract);
Loading
Loading