Skip to content
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

Refactor to use make-synchronized #18

Merged
merged 4 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
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 { makeSynchronizedModule } = 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 makeSynchronizedModule(prettierEntry);
}

module.exports = createSynchronizedPrettier({ prettierEntry: "prettier" });
// @ts-expect-error Property 'createSynchronizedPrettier' for named export compatibility
module.exports.createSynchronizedPrettier = createSynchronizedPrettier;
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,8 @@
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
},
"dependencies": {
"make-synchronized": "^0.0.3"
}
}
25 changes: 25 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,31 @@ 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
Copy link
Member

@JounQin JounQin Jan 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also add the following?

For more even complex use cases like TypeScript or P'n'P support natively, you may also try [`synckit`](https://github.com/un-ts/synckit).


```js
// runs-in-worker.js
import * as fs from "node:fs/promises";
import * as prettier from "prettier";

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

```js
import makeSynchronized from "make-synchronized";

const {
formatFile, // Synchronize version of `formatFile` in `runs-in-worker.js`
} = makeSynchronized(new URL("./runs-in-worker.js", import.meta.url));
```

### `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