Skip to content

Commit

Permalink
Refactor to use make-synchronized (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
fisker committed Jan 22, 2024
1 parent 1c353f3 commit 06c7546
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 167 deletions.
13 changes: 13 additions & 0 deletions examples/format-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as fs from "node:fs/promises";
import * as prettier from "prettier";
import makeSynchronized from "make-synchronized";

export default makeSynchronized(import.meta, async function formatFile(file) {
const config = await prettier.resolveConfig(file);
const content = await fs.readFile(file, "utf8");
const formatted = await prettier.format(content, {
...config,
filepath: file,
});
await fs.writeFile(file, formatted);
});
6 changes: 6 additions & 0 deletions examples/run-format-file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import * as url from "node:url";
import formatFile from "./format-file.js";

console.log(
formatFile(url.fileURLToPath(new URL("../readme.md", import.meta.url))),
);
162 changes: 2 additions & 160 deletions index.cjs
Original file line number Diff line number Diff line change
@@ -1,168 +1,10 @@
"use strict";

const {
Worker,
receiveMessageOnPort,
MessageChannel,
} = require("worker_threads");
const url = require("url");
const path = require("path");
const { makeModuleSynchronized } = require("make-synchronized");

/**
@template {keyof PrettierSynchronizedFunctions} FunctionName
@typedef {(...args: Parameters<Prettier[FunctionName]>) => Awaited<ReturnType<Prettier[FunctionName]>> } PrettierSyncFunction
*/

/**
@typedef {import("prettier")} Prettier
@typedef {{ [FunctionName in typeof PRETTIER_ASYNC_FUNCTIONS[number]]: PrettierSyncFunction<FunctionName> }} PrettierSynchronizedFunctions
@typedef {{ [PropertyName in typeof PRETTIER_STATIC_PROPERTIES[number]]: Prettier[PropertyName] }} PrettierStaticProperties
@typedef {PrettierSynchronizedFunctions & PrettierStaticProperties} SynchronizedPrettier
*/

const PRETTIER_ASYNC_FUNCTIONS = /** @type {const} */ ([
"formatWithCursor",
"format",
"check",
"resolveConfig",
"resolveConfigFile",
"clearConfigCache",
"getFileInfo",
"getSupportInfo",
]);

const PRETTIER_STATIC_PROPERTIES = /** @type {const} */ ([
"version",
"util",
"doc",
]);

/** @type {Worker | undefined} */
let worker;
function createWorker() {
if (!worker) {
worker = new Worker(require.resolve("./worker.js"));
worker.unref();
}

return worker;
}

/**
* @template {keyof PrettierSynchronizedFunctions} FunctionName
* @param {FunctionName} functionName
* @param {string} prettierEntry
* @returns {PrettierSyncFunction<FunctionName>}
*/
function createSynchronizedFunction(functionName, prettierEntry) {
return (...args) => {
const signal = new Int32Array(new SharedArrayBuffer(4));
const { port1: localPort, port2: workerPort } = new MessageChannel();
const worker = createWorker();

worker.postMessage(
{ signal, port: workerPort, functionName, args, prettierEntry },
[workerPort],
);

Atomics.wait(signal, 0, 0);

const {
message: { result, error, errorData },
} = receiveMessageOnPort(localPort);

if (error) {
throw Object.assign(error, errorData);
}

return result;
};
}

/**
* @template {keyof PrettierStaticProperties} Property
* @param {Property} property
* @param {string} prettierEntry
*/
function getProperty(property, prettierEntry) {
return /** @type {Prettier} */ (require(prettierEntry))[property];
}

/**
* @template {keyof SynchronizedPrettier} ExportName
* @param {() => SynchronizedPrettier[ExportName]} getter
*/
function createDescriptor(getter) {
let value;
return {
get: () => {
value ??= getter();
return value;
},
enumerable: true,
};
}

/**
* @param {string | URL} entry
*/
function toImportId(entry) {
if (entry instanceof URL) {
return entry.href;
}

if (typeof entry === "string" && path.isAbsolute(entry)) {
return url.pathToFileURL(entry).href;
}

return entry;
}

/**
* @param {string | URL} entry
*/
function toRequireId(entry) {
if (entry instanceof URL || entry.startsWith("file:")) {
return url.fileURLToPath(entry);
}

return entry;
}

/**
* @param {object} options
* @param {string | URL} options.prettierEntry - Path or URL to prettier entry.
* @returns {SynchronizedPrettier}
*/
function createSynchronizedPrettier({ prettierEntry }) {
const importId = toImportId(prettierEntry);
const requireId = toRequireId(prettierEntry);

const prettier = Object.defineProperties(
Object.create(null),
Object.fromEntries(
[
...PRETTIER_ASYNC_FUNCTIONS.map((functionName) => {
return /** @type {const} */ ([
functionName,
() => createSynchronizedFunction(functionName, importId),
]);
}),
...PRETTIER_STATIC_PROPERTIES.map((property) => {
return /** @type {const} */ ([
property,
() => getProperty(property, requireId),
]);
}),
].map(([property, getter]) => {
return /** @type {const} */ ([property, createDescriptor(getter)]);
}),
),
);

return prettier;
return makeModuleSynchronized(prettierEntry);
}

module.exports = createSynchronizedPrettier({ prettierEntry: "prettier" });
// @ts-expect-error Property 'createSynchronizedPrettier' for named export compatibility
module.exports.createSynchronizedPrettier = createSynchronizedPrettier;
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,20 @@
]
},
"peerDependencies": {
"prettier": "^3.0.0"
"prettier": "*"
},
"devDependencies": {
"@types/node": "20.4.1",
"c8": "8.0.0",
"@types/node": "20.11.5",
"c8": "9.1.0",
"npm-run-all": "4.1.5",
"prettier": "3.0.0"
"prettier": "3.2.4"
},
"packageManager": "yarn@3.2.2",
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"make-synchronized": "^0.2.1"
}
}
20 changes: 20 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,26 @@ synchronizedPrettier.format("foo( )", { parser: "babel" });
// => 'foo();\n'
```

This package is a simple wrapper of [`make-synchronized`](https://github.com/fisker/make-synchronized), currently only the functions and primitive values exported from `prettier` is functional, functions not exported directly (eg: `prettier.__debug.parse`) doesn't work, but it can be supported, if you want more functionality, please [open an issue](https://github.com/prettier/prettier-synchronized/issues/new).

For more complex use cases, it more reasonable to extract into a separate file, and run with [`make-synchronized`](https://github.com/fisker/make-synchronized), example

```js
import * as fs from "node:fs/promises";
import * as prettier from "prettier";
import makeSynchronized from "make-synchronized";

export default makeSynchronized(import.meta, async function formatFile(file) {
const config = await prettier.resolveConfig(file);
const content = await fs.readFile(file, "utf8");
const formatted = await prettier.format(content, {
...config,
filepath: file,
});
await fs.writeFile(file, formatted);
});
```

### `createSynchronizedPrettier(options)`

#### `options`
Expand Down
1 change: 0 additions & 1 deletion test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ test("version", () => {
const fakePrettierRelatedPath = "./a-fake-prettier-to-test.cjs";
const fakePrettierUrl = new URL(fakePrettierRelatedPath, import.meta.url);
for (const prettierEntry of [
fakePrettierRelatedPath,
fakePrettierUrl,
fakePrettierUrl.href,
fileURLToPath(fakePrettierUrl),
Expand Down
4 changes: 2 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"checkJs": true,
"noEmit": true,
"target": "esnext",
"module": "NodeNext"
"module": "NodeNext",
},
"files": ["index.cjs"]
"files": ["index.cjs"],
}

0 comments on commit 06c7546

Please sign in to comment.