Skip to content
Merged
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
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,23 @@ Ensures that the starting of all source files follows the following convention:
const Import = require("./Import");
const SortedAlphabetically = require("./SortedAlphabetically");

/** @typedef {import("../TypeImport")} TypeImport */
/** @import TypeImport from "../TypeImport" */
/** @typedef {import("../SortedAlphabetically")} SortedAlphabetically */
```

Type imports may use either the `@import` tag or the legacy `@typedef {import("...")}` form.
Both are sorted together by the module they refer to.
`@import` tags may also be wrapped over multiple lines:

```js
/**
* @import {
* LongTypeName,
* AnotherLongTypeName
* } from "../TypeImport"
*/
```

```text
--source ./lib/**/*.js
```
Expand Down
43 changes: 38 additions & 5 deletions format-file-header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,52 @@ const sortImport = (a, b) => {
return 0;
};

const execToArray = (content, regexp) => {
const defaultKey = (match) => match[1] + match[2];

const execToArray = (content, regexp, getKey = defaultKey) => {
const items = [];
let match = regexp.exec(content);
while (match) {
items.push({
content: match[0],
key: match[1] + match[2],
key: getKey(match),
});
match = regexp.exec(content);
}
return items;
};

// Anything that can appear inside a jsdoc comment, i. e. everything up to the
// closing `*/`. Used to match both single line and multi line jsdoc comments.
const JSDOC_CONTENT = String.raw`(?:[^*]|\*(?!\/))*?`;

// Optional type arguments like `<T>` or `<K, V>`
const TYPE_ARGUMENTS = String.raw`(?:<(?:(?:\w\.)*\w+, )*(?:\w\.)*\w+>)?`;

// Legacy type import:
// /** @typedef {import("./Module")} Module */
// /** @template T @typedef {import("./Module").Item<T>} Item<T> */
const TYPEDEF_TYPE_IMPORT = String.raw`\/\*\* (?:@template \w+ )*@typedef \{(?:typeof )?import\("(?<typedefFrom>[^"]+)"\)(?<typedefMember>(?:\.\w+)*${TYPE_ARGUMENTS})\} \w+${TYPE_ARGUMENTS} \*\/\n`;

// `@import` type import, single line or wrapped over multiple lines:
// /** @import Module from "./Module" */
// /** @import { Item, Other as Alias } from "./Module" */
// /** @import Module, { Item } from "./Module" */
// /**
// * @import {
// * Item,
// * Other
// * } from "./Module"
// */
const IMPORT_TYPE_IMPORT = String.raw`\/\*\*${JSDOC_CONTENT}@import\b${JSDOC_CONTENT}from "(?<importFrom>[^"]+)"\s*\*\/\n`;

const TYPE_IMPORT = `(?:${TYPEDEF_TYPE_IMPORT}|${IMPORT_TYPE_IMPORT})`;

const typeImportKey = ({ groups }) =>
groups.importFrom === undefined
? groups.typedefFrom + groups.typedefMember
: groups.importFrom;

/**
* @typedef {Object} Schema
* @property {string} title
Expand Down Expand Up @@ -103,13 +136,13 @@ const schema = [
},
{
title: "type imports",
regexp:
/(\/\*\* (?:@template \w+ )*@typedef \{(?:typeof )?import\("[^"]+"\)(\.\w+)*(?:<(?:(?:\w\.)*\w+, )*(?:\w\.)*\w+>)?\} \w+(?:<(?:(?:\w\.)*\w+, )*(?:\w\.)*\w+>)? \*\/\n)+\n/g,
regexp: new RegExp(`(?:${TYPE_IMPORT})+\\n`, "g"),
updateMessage: "sort type imports alphabetically",
update(content) {
const items = execToArray(
content,
/\/\*\* (?:@template \w+ )*@typedef \{(?:typeof )?import\("([^"]+)"\)((?:\.\w+)*(?:<(?:(?:\w\.)*\w+, )*(?:\w\.)*\w+>)?)\} \w+(?:<(?:(?:\w\.)*\w+, )*(?:\w\.)*\w+>)? \*\/\n/g,
new RegExp(TYPE_IMPORT, "g"),
typeImportKey,
);
items.sort(sortImport);
return items.map((item) => item.content).join("") + "\n";
Expand Down
44 changes: 44 additions & 0 deletions generate-types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const prettier = require("prettier");

process.exitCode = 1;
let exitCode = 0;
let hasUnresolvableTypes = false;

const AnonymousType = "__Type";

Expand Down Expand Up @@ -364,15 +365,34 @@ const printError = (diagnostic) => {
return name;
};

/**
* @param {ts.Symbol} symbol symbol
* @returns {string} location of the symbol for error messages
*/
const getSymbolLocation = (symbol) => {
const decls = symbol.getDeclarations();
const decl = decls && decls[0];
if (!decl) return "<unknown location>";
const source = decl.getSourceFile();
const { line, character } = ts.getLineAndCharacterOfPosition(
source,
decl.getStart(),
);
return `${source.fileName} (${line + 1},${character + 1})`;
};

const getTypeOfSymbol = (symbol, isValue) => {
let decl;
/** @type {ts.Type | undefined} */
let errorType;
const type = (() => {
let type;
if (!isValue) {
type = checker.getDeclaredTypeOfSymbol(symbol);
if (type && type.intrinsicName !== "error") {
return type;
}
errorType = errorType || type;
}
if (symbol.type) return symbol.type;
const decls = symbol.getDeclarations();
Expand All @@ -382,12 +402,26 @@ const printError = (diagnostic) => {
if (type && type.intrinsicName !== "error") {
return type;
}
errorType = errorType || type;
type = checker.getTypeAtLocation(decl);
if (type && type.intrinsicName !== "error") {
return type;
}
errorType = errorType || type;
}
})();
if (!type) {
// Without a type there is nothing to generate, but reporting all
// unresolvable symbols at once is a lot more useful than failing on the
// first one, so continue with the error type and bail out before writing.
hasUnresolvableTypes = true;
console.error(
`${getSymbolLocation(symbol)}: Unable to resolve the type of "${
symbol.name
}".`,
);
return errorType || checker.getDeclaredTypeOfSymbol(symbol);
}
if (type && decl) {
// Learn about type nodes
if (
Expand Down Expand Up @@ -2668,6 +2702,16 @@ const printError = (diagnostic) => {
fn();
}

if (hasUnresolvableTypes) {
console.error(
"Some types can't be resolved, so no declarations are generated.",
);
console.error(
"Note that a type imported with `@import` is only a local alias, it's not re-exported from the module like a `@typedef` is.",
);
return;
}

const outputFilePath = path.resolve(root, outputFile);

const sortedDeclarations = [...declarations].sort((a, b) => {
Expand Down
Loading