diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2cc8593 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.8.0 \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 23cc0a8..36fa8f4 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/hohoro/README.md b/packages/hohoro/README.md index aec34dd..c7c3bf2 100644 --- a/packages/hohoro/README.md +++ b/packages/hohoro/README.md @@ -52,6 +52,24 @@ Then add the following `dev` script to your `package.json`: } ``` +## Experimental: + +This library also exposes a `hohoro-experimental` binary that builds code using [oxc](https://oxc.rs/) with [`oxc-transform`](https://www.npmjs.com/package/oxc-transform). + +When using the experimental binary - you'll **need to also install `oxc-transform` as a devDependency**. + +### Usage: + +```json +{ + "scripts": { + "build": "hohoro-experimental" + } +} +``` + +You **do not need additional configuration** when using the experimental build. + ## Contributing: ### Code Quality: @@ -62,7 +80,7 @@ This library uses [BiomeJS](https://biomejs.dev/) for linting, run `bun run lint #### Tests -This library uses Node for running unit tests, run `bun run test` from the root or from this workspace! +This library uses Bun for running unit tests, run `bun run test` from the root or from this workspace! ### Publishing: diff --git a/packages/hohoro/__tests__/experimental-incremental-build.test.mjs b/packages/hohoro/__tests__/experimental-incremental-build.test.mjs new file mode 100644 index 0000000..4ca204c --- /dev/null +++ b/packages/hohoro/__tests__/experimental-incremental-build.test.mjs @@ -0,0 +1,119 @@ +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; +import { copyFileSync, rmSync, writeFileSync } from "node:fs"; +import { dirname, join as pathJoin } from "node:path"; +import { fileURLToPath } from "node:url"; +import fg from "fast-glob"; +import { runBuild } from "../experimental-incremental-build.mjs"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +describe("experimental hohoro", () => { + // copy over files from the template dir to the src dir + beforeEach(() => { + const templateDir = pathJoin( + __dirname, + "..", + "sample-workspace-dir", + "template", + ); + const srcDir = pathJoin(__dirname, "..", "sample-workspace-dir", "src"); + const files = fg.sync(pathJoin(templateDir, "**/*.{ts,tsx,js,json}")); + + for (const file of files) { + const filePath = file.replace(templateDir, srcDir); + copyFileSync(file, filePath); + } + }); + + it("[experimental] It correctly builds the library", async () => { + const logs = []; + const errors = []; + const logger = { + log(message) { + logs.push(message); + }, + error(message) { + errors.push(message); + }, + }; + await runBuild({ + rootDirectory: pathJoin(__dirname, "..", "sample-workspace-dir"), + logger, + }); + + const distFiles = fg.sync( + pathJoin(__dirname, "..", "sample-workspace-dir", "dist", "**/*"), + ); + + expect(distFiles.some((file) => file.includes("tsx-file.js"))).toBe(true); + expect(distFiles.some((file) => file.includes("tsx-file.d.ts"))).toBe(true); + expect(distFiles.some((file) => file.includes("js-file.js"))).toBe(true); + expect(distFiles.some((file) => file.includes("json-file.json"))).toBe( + true, + ); + expect(errors.length).toBe(0); + expect(logs[0]).toContain("compiled: 2 files, copied 1 file"); + }); + + it("[experimental] It only builds changed files", async () => { + const logs = []; + const errors = []; + const logger = { + log(message) { + logs.push(message); + }, + error(message) { + errors.push(message); + }, + }; + + await runBuild({ + rootDirectory: pathJoin(__dirname, "..", "sample-workspace-dir"), + logger, + }); + + // change a file + const tsxFile = pathJoin( + __dirname, + "..", + "sample-workspace-dir", + "src", + "tsx-file.tsx", + ); + writeFileSync(tsxFile, `export const foo = 'bar';`); + + await runBuild({ + rootDirectory: pathJoin(__dirname, "..", "sample-workspace-dir"), + logger, + }); + + // most important assertion, only the tsx file should have been compiled! + expect(logs[1]).toContain("compiled: 1 file"); + + const distFiles = fg.sync( + pathJoin(__dirname, "..", "sample-workspace-dir", "dist", "**/*"), + ); + + expect(distFiles.some((file) => file.includes("tsx-file.js"))).toBe(true); + expect(distFiles.some((file) => file.includes("tsx-file.d.ts"))).toBe(true); + expect(distFiles.some((file) => file.includes("js-file.js"))).toBe(true); + expect(distFiles.some((file) => file.includes("json-file.json"))).toBe( + true, + ); + expect(errors.length).toBe(0); + }); + + // cleanup src and dist dirs after the tests run + afterEach(() => { + const srcDir = pathJoin(__dirname, "..", "sample-workspace-dir", "src"); + + const files = fg.sync(pathJoin(srcDir, "**/*.{ts,tsx,js,json}")); + for (const file of files) { + rmSync(file, { recursive: true }); + } + rmSync(pathJoin(__dirname, "..", "sample-workspace-dir", "dist"), { + recursive: true, + }); + }); +}); diff --git a/packages/hohoro/__tests__/incremental-build.test.mjs b/packages/hohoro/__tests__/incremental-build.test.mjs index 40e8489..b41252a 100644 --- a/packages/hohoro/__tests__/incremental-build.test.mjs +++ b/packages/hohoro/__tests__/incremental-build.test.mjs @@ -1,7 +1,6 @@ -import assert from "node:assert"; +import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { copyFileSync, rmSync, writeFileSync } from "node:fs"; import { dirname, join as pathJoin } from "node:path"; -import { after, before, test } from "node:test"; import { fileURLToPath } from "node:url"; import fg from "fast-glob"; import { runBuild } from "../incremental-build.mjs"; @@ -9,131 +8,115 @@ import { runBuild } from "../incremental-build.mjs"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// copy over files from the template dir to the src dir -before(() => { - const templateDir = pathJoin( - __dirname, - "..", - "sample-workspace-dir", - "template", - ); - const srcDir = pathJoin(__dirname, "..", "sample-workspace-dir", "src"); - const files = fg.sync(pathJoin(templateDir, "**/*.{ts,tsx,js,json}")); +describe("stable hohoro", () => { + // copy over files from the template dir to the src dir + beforeEach(() => { + const templateDir = pathJoin( + __dirname, + "..", + "sample-workspace-dir", + "template", + ); + const srcDir = pathJoin(__dirname, "..", "sample-workspace-dir", "src"); + const files = fg.sync(pathJoin(templateDir, "**/*.{ts,tsx,js,json}")); - for (const file of files) { - const filePath = file.replace(templateDir, srcDir); - copyFileSync(file, filePath); - } -}); - -test("It correctly builds the library", async () => { - const logs = []; - const errors = []; - const logger = { - log(message) { - logs.push(message); - }, - error(message) { - errors.push(message); - }, - }; - await runBuild({ - rootDirectory: pathJoin(__dirname, "..", "sample-workspace-dir"), - logger, + for (const file of files) { + const filePath = file.replace(templateDir, srcDir); + copyFileSync(file, filePath); + } }); - const distFiles = fg.sync( - pathJoin(__dirname, "..", "sample-workspace-dir", "dist", "**/*"), - ); + it("correctly builds the library", async () => { + const logs = []; + const errors = []; + const logger = { + log(message) { + logs.push(message); + }, + error(message) { + errors.push(message); + }, + }; + await runBuild({ + rootDirectory: pathJoin(__dirname, "..", "sample-workspace-dir"), + logger, + }); - assert.ok( - distFiles.some((file) => file.includes("tsx-file.js")), - "Couldn't find tsx-file.js!", - ); - assert.ok( - distFiles.some((file) => file.includes("tsx-file.d.ts")), - "Couldn't find tsx-file.d.ts!", - ); - assert.ok( - distFiles.some((file) => file.includes("js-file.js")), - "Couldn't find js-file.js!", - ); - assert.ok( - distFiles.some((file) => file.includes("json-file.json")), - "Couldn't find json-file.json!", - ); - assert.equal(errors.length, 0, "There were errors logged during build!"); - assert.ok( - logs[0].includes(`compiled: 2 files, copied 1 file`), - `Log message doesn't match!\n Received: ${logs[0]}`, - ); -}); + const distFiles = fg.sync( + pathJoin(__dirname, "..", "sample-workspace-dir", "dist", "**/*"), + ); -test("It only builds changed files", async () => { - const logs = []; - const errors = []; - const logger = { - log(message) { - logs.push(message); - }, - error(message) { - errors.push(message); - }, - }; + expect(distFiles.some((file) => file.includes("tsx-file.js"))).toBe(true); + expect(distFiles.some((file) => file.includes("tsx-file.d.ts"))).toBe(true); + expect(distFiles.some((file) => file.includes("js-file.js"))).toBe(true); + expect(distFiles.some((file) => file.includes("json-file.json"))).toBe( + true, + ); + expect(errors.length).toBe(0); + expect(logs[0]).toContain("compiled: 2 files, copied 1 file"); + }); - // change a file - const tsxFile = pathJoin( - __dirname, - "..", - "sample-workspace-dir", - "src", - "tsx-file.tsx", - ); - writeFileSync(tsxFile, `export const foo = 'bar';`); + it("only builds changed files", async () => { + const logs = []; + const errors = []; + const logger = { + log(message) { + logs.push(message); + }, + error(message) { + errors.push(message); + }, + }; - await runBuild({ - rootDirectory: pathJoin(__dirname, "..", "sample-workspace-dir"), - logger, - }); + await runBuild({ + rootDirectory: pathJoin(__dirname, "..", "sample-workspace-dir"), + logger, + }); - // most important assertion, only the tsx file should have been compiled! - assert.ok( - logs[0].includes(`compiled: 1 file`), - `Log message doesn't match!\n Received: ${logs[0]}`, - ); + // change a file + const tsxFile = pathJoin( + __dirname, + "..", + "sample-workspace-dir", + "src", + "tsx-file.tsx", + ); + writeFileSync(tsxFile, `export const foo = 'baz';`); - const distFiles = fg.sync( - pathJoin(__dirname, "..", "sample-workspace-dir", "dist", "**/*"), - ); + await runBuild({ + rootDirectory: pathJoin(__dirname, "..", "sample-workspace-dir"), + logger, + }); - assert.ok( - distFiles.some((file) => file.includes("tsx-file.js")), - "Couldn't find tsx-file.js!", - ); - assert.ok( - distFiles.some((file) => file.includes("tsx-file.d.ts")), - "Couldn't find tsx-file.d.ts!", - ); - assert.ok( - distFiles.some((file) => file.includes("js-file.js")), - "Couldn't find js-file.js!", - ); - assert.ok( - distFiles.some((file) => file.includes("json-file.json")), - "Couldn't find json-file.json!", - ); - assert.equal(errors.length, 0, "There were errors logged during build!"); -}); + // logs[0] is the initial build + // logs[1] is the initial type def build + // logs[2] is the incremental rebuild + // most important assertion, only the tsx file should have been compiled! + expect(logs[2]).toContain("compiled: 1 file"); + + const distFiles = fg.sync( + pathJoin(__dirname, "..", "sample-workspace-dir", "dist", "**/*"), + ); + + expect(distFiles.some((file) => file.includes("tsx-file.js"))).toBe(true); + expect(distFiles.some((file) => file.includes("tsx-file.d.ts"))).toBe(true); + expect(distFiles.some((file) => file.includes("js-file.js"))).toBe(true); + expect(distFiles.some((file) => file.includes("json-file.json"))).toBe( + true, + ); + expect(errors.length).toBe(0); + }); -// cleanup src and dist dirs after the tests run -after(() => { - const srcDir = pathJoin(__dirname, "..", "sample-workspace-dir", "src"); + // cleanup src and dist dirs after the tests run + afterEach(() => { + const srcDir = pathJoin(__dirname, "..", "sample-workspace-dir", "src"); - const files = fg.sync(pathJoin(srcDir, "**/*.{ts,tsx,js,json}")); - for (const file of files) { - rmSync(file, { recursive: true }); - } - rmSync(pathJoin(__dirname, "..", "sample-workspace-dir", "dist"), { - recursive: true, + const files = fg.sync(pathJoin(srcDir, "**/*.{ts,tsx,js,json}")); + for (const file of files) { + rmSync(file, { recursive: true }); + } + rmSync(pathJoin(__dirname, "..", "sample-workspace-dir", "dist"), { + recursive: true, + }); }); }); diff --git a/packages/hohoro/bin/experimental.mjs b/packages/hohoro/bin/experimental.mjs new file mode 100755 index 0000000..0eecf77 --- /dev/null +++ b/packages/hohoro/bin/experimental.mjs @@ -0,0 +1,8 @@ +#!/usr/bin/env node +import { runBuild } from "../experimental-incremental-build.mjs"; + +runBuild({ rootDirectory: process.cwd(), logger: console }).catch((e) => { + console.error("Error running experimental incremental build"); + console.error(e); + process.exit(1); +}); diff --git a/packages/hohoro/experimental-incremental-build.mjs b/packages/hohoro/experimental-incremental-build.mjs new file mode 100644 index 0000000..b2b65c5 --- /dev/null +++ b/packages/hohoro/experimental-incremental-build.mjs @@ -0,0 +1,173 @@ +import { createHash } from "node:crypto"; +import { + copyFileSync, + createReadStream, + mkdirSync, + readFileSync, + writeFileSync, +} from "node:fs"; +import path, { join as pathJoin, basename, extname } from "node:path"; +import createDebug from "debug"; +import fg from "fast-glob"; +import oxc from "oxc-transform"; + +const debug = createDebug("hohoro"); + +function compile({ files, logger }) { + debug(`[compile] Compiling ${files}...`); + let compileCount = 0; + let copyCount = 0; + let errors = []; + for (const filePath of files) { + let distPath = filePath.replace("src", "dist"); + + switch (extname(filePath)) { + case ".ts": + case ".js": + case ".jsx": + case ".tsx": { + let { + code, + declaration, + errors: compileErrors, + } = oxc.transform( + basename(filePath), + readFileSync(filePath).toString(), + { + typescript: { + declaration: { + emit: true, + }, + }, + }, + ); + + if (compileErrors.length) { + errors.push(...compileErrors); + logger.error(compileErrors); + } + + writeFileSync(distPath.replace(/(\.tsx|\.ts)/, ".js"), code); + if (declaration) { + writeFileSync(distPath.replace(/(\.tsx|\.ts)/, ".d.ts"), declaration); + } + compileCount++; + break; + } + default: { + copyFileSync(filePath, distPath); + copyCount++; + break; + } + } + } + return { compileCount, copyCount, errors }; +} + +function loadCacheFile({ cacheFilePath }) { + try { + return JSON.parse(readFileSync(cacheFilePath).toString()); + } catch (e) { + return []; + } +} + +function hashFile(filePath) { + return new Promise((resolve, reject) => { + const hash = createHash("md5"); + const stream = createReadStream(filePath); + + stream.on("data", (data) => { + hash.update(data); + }); + + stream.on("end", () => { + const fileHash = hash.digest("hex"); + resolve(fileHash); + }); + + stream.on("error", (err) => { + reject(err); + }); + }); +} + +export async function runBuild( + { rootDirectory, logger } = { rootDirectory: process.cwd(), logger: console }, +) { + const start = Date.now(); + debug("[runBuild] Starting..."); + const cacheFilePath = pathJoin(rootDirectory, "dist", "build-cache.json"); + const cacheFile = loadCacheFile({ cacheFilePath }); + debug(`Cache file: `, JSON.stringify(cacheFile, null, 2)); + + // Need to convert to a posix path for use with `fast-glob` on Windows. + // @see https://github.com/mrmlnc/fast-glob/issues/237#issuecomment-546057189 + // @see https://github.com/mrmlnc/fast-glob?tab=readme-ov-file#how-to-write-patterns-on-windows + const files = fg.sync( + path.posix.join( + fg.convertPathToPattern(rootDirectory), + "src/**/*.{ts,tsx,js,json}", + ), + { + ignore: ["**/__tests__/**"], + }, + ); + + const relativeFiles = files.map((file) => file.split(rootDirectory)[1]); + + const filesHashed = (await Promise.all(files.map(hashFile))).map( + (hashed, idx) => [relativeFiles[idx], hashed], + ); + + const changedFiles = []; + + for (const [computedFile, computedHash] of filesHashed) { + if ( + cacheFile.find(([filePath]) => filePath === computedFile)?.[1] !== + computedHash + ) { + changedFiles.push(computedFile); + } + } + + if (!changedFiles.length) { + logger.log(`No files changed!`); + debug(`Early exit, ran in ${Date.now() - start}ms`); + process.exit(0); + } + + debug(`[runBuild] Changed files: `, JSON.stringify(changedFiles, null, 2)); + + const absoluteChangedFiles = changedFiles.map( + (changedFile) => rootDirectory + changedFile, + ); + + try { + mkdirSync(pathJoin(rootDirectory, "dist"), { recursive: true }); + } catch {} + + const { compileCount, copyCount, errors } = compile({ + files: absoluteChangedFiles, + logger, + }); + + if (errors.length) { + logger.error(`Failed to compile: ${errors}`); + debug(`Ran in ${Date.now() - start}ms`); + process.exit(1); + } + logger.log( + `compiled: ${compileCount} file${compileCount === 1 ? "" : "s"}${ + copyCount > 0 + ? `, copied ${copyCount} file${copyCount === 1 ? "" : "s"}` + : "" + }`, + ); + try { + writeFileSync(cacheFilePath, JSON.stringify(filesHashed)); + } catch (error) { + logger.error(`Failed to write cache file`); + } + debug(`Ran in ${Date.now() - start}ms`); +} diff --git a/packages/hohoro/incremental-build.mjs b/packages/hohoro/incremental-build.mjs index d8ec371..b09be2d 100644 --- a/packages/hohoro/incremental-build.mjs +++ b/packages/hohoro/incremental-build.mjs @@ -186,7 +186,8 @@ export async function runBuild( `Failed to compile declarations: ${declarationsResult.reason}`, ); } - debug(`Ran in ${Date.now() - start}ms`); + console.log(compileResult, declarationsResult); + debug(`Failed, ran in ${Date.now() - start}ms`); process.exit(1); } try { diff --git a/packages/hohoro/package.json b/packages/hohoro/package.json index 8e10a24..69f6060 100644 --- a/packages/hohoro/package.json +++ b/packages/hohoro/package.json @@ -1,12 +1,14 @@ { "name": "hohoro", "description": "An incremental build tool for JavaScript and TypeScript projects.", - "version": "0.2.1", + "version": "0.2.2", "bin": { - "hohoro": "./bin/index.mjs" + "hohoro": "./bin/index.mjs", + "hohoro-experimental": "./bin/experimental.mjs" }, "exports": { - ".": "./incremental-build.mjs" + ".": "./incremental-build.mjs", + "./experimental": "./experimental-incremental-build.mjs" }, "repository": { "type": "git", @@ -24,16 +26,20 @@ "@swc/cli": "0.3.10", "@swc/core": "1.4.2", "@types/bun": "latest", - "typescript": "5.4.5" + "typescript": "5.4.5", + "oxc-transform": "0.30.4" }, "dependencies": { "debug": "4.3.4", "fast-glob": "3.3.2" }, + "peerDependencies": { + "oxc-transform": "0.30.4" + }, "scripts": { "lint": "biome lint ./", - "test": "node --test", - "test:coverage": "node --test --experimental-test-coverage --test-reporter=spec", + "test": "bun test", + "skip-test:coverage": "node --test --experimental-test-coverage --test-reporter=spec", "prepub": "bun run lint && bun run test", "pub": "npm publish --access public" }, @@ -41,6 +47,7 @@ "README.md", "CHANGELOG.md", "incremental-build.mjs", + "experimental-incremental-build.mjs", "bin", "package.json" ] diff --git a/packages/test-experimental-hohoro/CHANGELOG.md b/packages/test-experimental-hohoro/CHANGELOG.md new file mode 100644 index 0000000..e8b5ca1 --- /dev/null +++ b/packages/test-experimental-hohoro/CHANGELOG.md @@ -0,0 +1,5 @@ +### Unreleased: + +### [0.0.1] - March 3rd, 2024 + +- Build the library using SWC and TypeScript diff --git a/packages/test-experimental-hohoro/README.md b/packages/test-experimental-hohoro/README.md new file mode 100644 index 0000000..1cad605 --- /dev/null +++ b/packages/test-experimental-hohoro/README.md @@ -0,0 +1,33 @@ +# `test-experimental-hohoro` + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run ./src/index.ts +``` + +## Building: + +This library uses [`swc`](https://swc.rs/) and [`TypeScript`](https://www.typescriptlang.org/docs/) to build the source code and generate types. + +To build the library, run `bun run build` from the root, or from this workspace! + +## Code Quality: + +### Type Checking: + +This library uses TypeScript to perform type checks, run `bun run type-check` from the root or from this workspace! + +### Linting + +This library uses [BiomeJS](https://biomejs.dev/) for linting, run `bun run lint` from the root or from this workspace! + +### Tests + +This library uses Bun for running unit tests, run `bun run test` from the root or from this workspace! diff --git a/packages/test-experimental-hohoro/biome.jsonc b/packages/test-experimental-hohoro/biome.jsonc new file mode 100644 index 0000000..0667632 --- /dev/null +++ b/packages/test-experimental-hohoro/biome.jsonc @@ -0,0 +1,12 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.6.3/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + } +} diff --git a/packages/test-experimental-hohoro/package.json b/packages/test-experimental-hohoro/package.json new file mode 100644 index 0000000..02f7136 --- /dev/null +++ b/packages/test-experimental-hohoro/package.json @@ -0,0 +1,27 @@ +{ + "name": "test-experimental-hohoro", + "version": "0.0.1", + "module": "index.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "type": "module", + "devDependencies": { + "@biomejs/biome": "1.6.3", + "@swc/cli": "0.3.10", + "@swc/core": "1.4.2", + "@types/bun": "latest", + "hohoro": "workspace:*" + }, + "scripts": { + "build": "hohoro-experimental", + "type-check": "tsc -p ./tsconfig.json", + "lint": "biome lint ./src/", + "test": "bun test", + "prepub": "bun run build && bun run type-check && bun run lint && bun run test", + "pub": "npm publish --access public" + } +} diff --git a/packages/test-experimental-hohoro/src/__tests__/index.test.ts b/packages/test-experimental-hohoro/src/__tests__/index.test.ts new file mode 100644 index 0000000..d7dbe85 --- /dev/null +++ b/packages/test-experimental-hohoro/src/__tests__/index.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from "bun:test"; + +test("test-experimental-hohoro", () => { + expect("test-experimental-hohoro").toBe("test-experimental-hohoro"); +}); diff --git a/packages/test-experimental-hohoro/src/index.ts b/packages/test-experimental-hohoro/src/index.ts new file mode 100644 index 0000000..37295b3 --- /dev/null +++ b/packages/test-experimental-hohoro/src/index.ts @@ -0,0 +1,7 @@ +console.log("Hello via Bun!"); + +export type Foo = string; + +export function foo(arg: Foo) { + return "Hello!"; +} diff --git a/packages/test-experimental-hohoro/tsconfig.json b/packages/test-experimental-hohoro/tsconfig.json new file mode 100644 index 0000000..2878a3c --- /dev/null +++ b/packages/test-experimental-hohoro/tsconfig.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + /* Linting */ + "skipLibCheck": true, + "strict": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true + } +}