@platforma-sdk/tengo-builder 4.0.28 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/cli.cjs +2 -0
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/build.cjs +1 -14
- package/dist/commands/build.cjs.map +1 -1
- package/dist/commands/build.js +1 -13
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/imports.cjs +79 -0
- package/dist/commands/imports.cjs.map +1 -0
- package/dist/commands/imports.js +76 -0
- package/dist/commands/imports.js.map +1 -0
- package/dist/compiler/imports.cjs +68 -0
- package/dist/compiler/imports.cjs.map +1 -0
- package/dist/compiler/imports.js +67 -0
- package/dist/compiler/imports.js.map +1 -0
- package/dist/compiler/source.cjs +6 -3
- package/dist/compiler/source.cjs.map +1 -1
- package/dist/compiler/source.js +6 -3
- package/dist/compiler/source.js.map +1 -1
- package/dist/compiler/util.cjs +19 -0
- package/dist/compiler/util.cjs.map +1 -1
- package/dist/compiler/util.js +20 -2
- package/dist/compiler/util.js.map +1 -1
- package/package.json +3 -3
- package/src/cli.ts +2 -0
- package/src/commands/build.ts +1 -25
- package/src/commands/imports.ts +110 -0
- package/src/compiler/imports.test.ts +181 -0
- package/src/compiler/imports.ts +96 -0
- package/src/compiler/source.ts +13 -4
- package/src/compiler/util.ts +30 -0
package/README.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
This repo contains a tool, that facilitates integration of `tengo` libraries and template build and distribution process into npm infrastructure.
|
|
4
4
|
|
|
5
|
+
# Commands
|
|
6
|
+
|
|
7
|
+
The package installs one binary, `pl-tengo`.
|
|
8
|
+
|
|
9
|
+
| Command | What it does |
|
|
10
|
+
| ---------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
11
|
+
| `pl-tengo build` | Builds the sources in `./src` into a distributable pack in `./dist`. |
|
|
12
|
+
| `pl-tengo check [paths...]` | Checks the sources with the tengo language processor. Defaults to `./src`. |
|
|
13
|
+
| `pl-tengo test [paths...]` | Runs the tengo tests. Defaults to `./src`. |
|
|
14
|
+
| `pl-tengo imports [paths...]` | Finds the imports the sources do not use. Defaults to `./src`. |
|
|
15
|
+
| `pl-tengo dump artifacts` / `pl-tengo dump software` | Prints the parsed artifacts or software descriptors. |
|
|
16
|
+
|
|
17
|
+
## `pl-tengo imports`
|
|
18
|
+
|
|
19
|
+
An import is used when its alias appears with a dot somewhere else in the code:
|
|
20
|
+
`text.split(...)`. Comments do not count as usage, so an alias that is left only
|
|
21
|
+
in a comment is reported.
|
|
22
|
+
|
|
23
|
+
| Mode | What it does |
|
|
24
|
+
| --------- | -------------------------------------------------------------------- |
|
|
25
|
+
| `--check` | Reports the unused imports and exits with an error. Changes no file. |
|
|
26
|
+
| `--fix` | Removes the unused imports from the sources. |
|
|
27
|
+
| no option | `--fix`, or `--check` when the `CI` environment variable is set. |
|
|
28
|
+
|
|
29
|
+
The default keeps CI honest: a person who builds gets the sources cleaned, while
|
|
30
|
+
CI reports the problem instead of making a change nobody reviewed.
|
|
31
|
+
|
|
32
|
+
A source the parser cannot read is reported and kept as it is. The other
|
|
33
|
+
sources are still cleaned, and the command exits with an error.
|
|
34
|
+
|
|
5
35
|
<!--
|
|
6
36
|
# Example config & file layout
|
|
7
37
|
|
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const require_build = require("./commands/build.cjs");
|
|
2
2
|
const require_check = require("./commands/check.cjs");
|
|
3
|
+
const require_imports = require("./commands/imports.cjs");
|
|
3
4
|
const require_test = require("./commands/test.cjs");
|
|
4
5
|
const require_artifacts = require("./commands/dump/artifacts.cjs");
|
|
5
6
|
const require_software = require("./commands/dump/software.cjs");
|
|
@@ -10,6 +11,7 @@ function buildProgram() {
|
|
|
10
11
|
program.name("pl-tengo").description("Pl Tengo Template Builder");
|
|
11
12
|
program.addCommand(require_build.default());
|
|
12
13
|
program.addCommand(require_check.default());
|
|
14
|
+
program.addCommand(require_imports.default());
|
|
13
15
|
program.addCommand(require_test.default());
|
|
14
16
|
const dump = new commander.Command("dump").description("dump parsed artifacts or software descriptors");
|
|
15
17
|
dump.addCommand(require_artifacts.default());
|
package/dist/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.cjs","names":["Command","buildCommand","checkCommand","testCommand","dumpArtifactsCommand","dumpSoftwareCommand"],"sources":["../src/cli.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport buildCommand from \"./commands/build\";\nimport checkCommand from \"./commands/check\";\nimport testCommand from \"./commands/test\";\nimport dumpArtifactsCommand from \"./commands/dump/artifacts\";\nimport dumpSoftwareCommand from \"./commands/dump/software\";\n\nexport function buildProgram(): Command {\n const program = new Command();\n program.name(\"pl-tengo\").description(\"Pl Tengo Template Builder\");\n\n program.addCommand(buildCommand());\n program.addCommand(checkCommand());\n program.addCommand(testCommand());\n\n // `dump` is a pure topic (no action): `pl-tengo dump` prints help.\n const dump = new Command(\"dump\").description(\"dump parsed artifacts or software descriptors\");\n dump.addCommand(dumpArtifactsCommand());\n dump.addCommand(dumpSoftwareCommand());\n program.addCommand(dump);\n\n return program;\n}\n\nexport async function run(argv: string[] = process.argv): Promise<void> {\n await buildProgram().parseAsync(argv);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.cjs","names":["Command","buildCommand","checkCommand","importsCommand","testCommand","dumpArtifactsCommand","dumpSoftwareCommand"],"sources":["../src/cli.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport buildCommand from \"./commands/build\";\nimport checkCommand from \"./commands/check\";\nimport importsCommand from \"./commands/imports\";\nimport testCommand from \"./commands/test\";\nimport dumpArtifactsCommand from \"./commands/dump/artifacts\";\nimport dumpSoftwareCommand from \"./commands/dump/software\";\n\nexport function buildProgram(): Command {\n const program = new Command();\n program.name(\"pl-tengo\").description(\"Pl Tengo Template Builder\");\n\n program.addCommand(buildCommand());\n program.addCommand(checkCommand());\n program.addCommand(importsCommand());\n program.addCommand(testCommand());\n\n // `dump` is a pure topic (no action): `pl-tengo dump` prints help.\n const dump = new Command(\"dump\").description(\"dump parsed artifacts or software descriptors\");\n dump.addCommand(dumpArtifactsCommand());\n dump.addCommand(dumpSoftwareCommand());\n program.addCommand(dump);\n\n return program;\n}\n\nexport async function run(argv: string[] = process.argv): Promise<void> {\n await buildProgram().parseAsync(argv);\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,eAAwB;CACtC,MAAM,UAAU,IAAIA,UAAAA,QAAQ;CAC5B,QAAQ,KAAK,UAAU,CAAC,CAAC,YAAY,2BAA2B;CAEhE,QAAQ,WAAWC,cAAAA,QAAa,CAAC;CACjC,QAAQ,WAAWC,cAAAA,QAAa,CAAC;CACjC,QAAQ,WAAWC,gBAAAA,QAAe,CAAC;CACnC,QAAQ,WAAWC,aAAAA,QAAY,CAAC;CAGhC,MAAM,OAAO,IAAIJ,UAAAA,QAAQ,MAAM,CAAC,CAAC,YAAY,+CAA+C;CAC5F,KAAK,WAAWK,kBAAAA,QAAqB,CAAC;CACtC,KAAK,WAAWC,iBAAAA,QAAoB,CAAC;CACrC,QAAQ,WAAW,IAAI;CAEvB,OAAO;AACT;AAEA,eAAsB,IAAI,OAAiB,QAAQ,MAAqB;CACtE,MAAM,aAAa,CAAC,CAAC,WAAW,IAAI;AACtC"}
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","names":[],"sources":["../src/cli.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","names":[],"sources":["../src/cli.ts"],"mappings":";;wBAQgB,gBAAgB;wBAkBV,IAAI,kBAAgC"}
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import buildCommand from "./commands/build.js";
|
|
2
2
|
import checkCommand from "./commands/check.js";
|
|
3
|
+
import importsCommand from "./commands/imports.js";
|
|
3
4
|
import testCommand from "./commands/test.js";
|
|
4
5
|
import dumpArtifactsCommand from "./commands/dump/artifacts.js";
|
|
5
6
|
import dumpSoftwareCommand from "./commands/dump/software.js";
|
|
@@ -10,6 +11,7 @@ function buildProgram() {
|
|
|
10
11
|
program.name("pl-tengo").description("Pl Tengo Template Builder");
|
|
11
12
|
program.addCommand(buildCommand());
|
|
12
13
|
program.addCommand(checkCommand());
|
|
14
|
+
program.addCommand(importsCommand());
|
|
13
15
|
program.addCommand(testCommand());
|
|
14
16
|
const dump = new Command("dump").description("dump parsed artifacts or software descriptors");
|
|
15
17
|
dump.addCommand(dumpArtifactsCommand());
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport buildCommand from \"./commands/build\";\nimport checkCommand from \"./commands/check\";\nimport testCommand from \"./commands/test\";\nimport dumpArtifactsCommand from \"./commands/dump/artifacts\";\nimport dumpSoftwareCommand from \"./commands/dump/software\";\n\nexport function buildProgram(): Command {\n const program = new Command();\n program.name(\"pl-tengo\").description(\"Pl Tengo Template Builder\");\n\n program.addCommand(buildCommand());\n program.addCommand(checkCommand());\n program.addCommand(testCommand());\n\n // `dump` is a pure topic (no action): `pl-tengo dump` prints help.\n const dump = new Command(\"dump\").description(\"dump parsed artifacts or software descriptors\");\n dump.addCommand(dumpArtifactsCommand());\n dump.addCommand(dumpSoftwareCommand());\n program.addCommand(dump);\n\n return program;\n}\n\nexport async function run(argv: string[] = process.argv): Promise<void> {\n await buildProgram().parseAsync(argv);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport buildCommand from \"./commands/build\";\nimport checkCommand from \"./commands/check\";\nimport importsCommand from \"./commands/imports\";\nimport testCommand from \"./commands/test\";\nimport dumpArtifactsCommand from \"./commands/dump/artifacts\";\nimport dumpSoftwareCommand from \"./commands/dump/software\";\n\nexport function buildProgram(): Command {\n const program = new Command();\n program.name(\"pl-tengo\").description(\"Pl Tengo Template Builder\");\n\n program.addCommand(buildCommand());\n program.addCommand(checkCommand());\n program.addCommand(importsCommand());\n program.addCommand(testCommand());\n\n // `dump` is a pure topic (no action): `pl-tengo dump` prints help.\n const dump = new Command(\"dump\").description(\"dump parsed artifacts or software descriptors\");\n dump.addCommand(dumpArtifactsCommand());\n dump.addCommand(dumpSoftwareCommand());\n program.addCommand(dump);\n\n return program;\n}\n\nexport async function run(argv: string[] = process.argv): Promise<void> {\n await buildProgram().parseAsync(argv);\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,eAAwB;CACtC,MAAM,UAAU,IAAI,QAAQ;CAC5B,QAAQ,KAAK,UAAU,CAAC,CAAC,YAAY,2BAA2B;CAEhE,QAAQ,WAAW,aAAa,CAAC;CACjC,QAAQ,WAAW,aAAa,CAAC;CACjC,QAAQ,WAAW,eAAe,CAAC;CACnC,QAAQ,WAAW,YAAY,CAAC;CAGhC,MAAM,OAAO,IAAI,QAAQ,MAAM,CAAC,CAAC,YAAY,+CAA+C;CAC5F,KAAK,WAAW,qBAAqB,CAAC;CACtC,KAAK,WAAW,oBAAoB,CAAC;CACrC,QAAQ,WAAW,IAAI;CAEvB,OAAO;AACT;AAEA,eAAsB,IAAI,OAAiB,QAAQ,MAAqB;CACtE,MAAM,aAAa,CAAC,CAAC,WAAW,IAAI;AACtC"}
|
package/dist/commands/build.cjs
CHANGED
|
@@ -6,8 +6,6 @@ let commander = require("commander");
|
|
|
6
6
|
let node_child_process = require("node:child_process");
|
|
7
7
|
let node_path = require("node:path");
|
|
8
8
|
node_path = require_runtime.__toESM(node_path, 1);
|
|
9
|
-
let node_fs = require("node:fs");
|
|
10
|
-
node_fs = require_runtime.__toESM(node_fs, 1);
|
|
11
9
|
let node_fs_promises = require("node:fs/promises");
|
|
12
10
|
node_fs_promises = require_runtime.__toESM(node_fs_promises, 1);
|
|
13
11
|
//#region src/commands/build.ts
|
|
@@ -69,7 +67,7 @@ function checkAndGenerateCtags(logger, flags) {
|
|
|
69
67
|
const fileName = node_path.resolve(flags["tags-file"]);
|
|
70
68
|
const rootDir = node_path.dirname(fileName);
|
|
71
69
|
const additionalArgs = flags["tags-additional-args"];
|
|
72
|
-
const tengoFiles = toRelativePath(rootDir, getTengoFiles(rootDir, 9));
|
|
70
|
+
const tengoFiles = toRelativePath(rootDir, require_util.getTengoFiles(rootDir, 9));
|
|
73
71
|
logger.info(`Generating tags for tengo autocompletion from "${rootDir}" \
|
|
74
72
|
in "${fileName}", additional arguments: "${additionalArgs.join("\" \"")}".
|
|
75
73
|
Found ${tengoFiles.length} tengo files...`);
|
|
@@ -104,17 +102,6 @@ https://marketplace.visualstudio.com/items?itemName=jaydenlin.ctags-support`);
|
|
|
104
102
|
checkRunError(result, "failed to generate ctags");
|
|
105
103
|
logger.info("Generation of tags is done.");
|
|
106
104
|
}
|
|
107
|
-
function getTengoFiles(dir, depth) {
|
|
108
|
-
if (depth === 0) return [];
|
|
109
|
-
const files = node_fs.readdirSync(dir, { withFileTypes: true });
|
|
110
|
-
let tengoFiles = [];
|
|
111
|
-
files.forEach((file) => {
|
|
112
|
-
const absPath = node_path.join(dir, file.name);
|
|
113
|
-
if (file.isDirectory()) tengoFiles = tengoFiles.concat(getTengoFiles(absPath, depth - 1));
|
|
114
|
-
if (!file.isDirectory() && file.name.endsWith(".tengo")) tengoFiles.push(absPath);
|
|
115
|
-
});
|
|
116
|
-
return tengoFiles;
|
|
117
|
-
}
|
|
118
105
|
function toRelativePath(dir, files) {
|
|
119
106
|
return files.map((file) => file.replace(dir, "."));
|
|
120
107
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.cjs","names":["Command","createLogger","getPackageInfo","compile","fsp","path","
|
|
1
|
+
{"version":3,"file":"build.cjs","names":["Command","createLogger","getPackageInfo","compile","fsp","path","getTengoFiles"],"sources":["../../src/commands/build.ts"],"sourcesContent":["import type { SpawnSyncReturns } from \"node:child_process\";\nimport { spawnSync } from \"node:child_process\";\nimport { Command } from \"commander\";\nimport { compile, savePacks, getPackageInfo } from \"../compiler/main\";\nimport { createLogger, getTengoFiles } from \"../compiler/util\";\nimport * as opts from \"../shared/basecmd\";\nimport * as fsp from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport type * as winston from \"winston\";\nimport type { TemplatesAndLibs } from \"../compiler/compiler\";\n\nexport default function buildCommand(): Command {\n const cmd = new Command(\"build\").description(\n \"build tengo sources into single distributable pack file\",\n );\n\n opts.addOptions(cmd, opts.GlobalOptions(), opts.CtagsOptions());\n\n cmd.action(async (o) => {\n const flags = {\n \"log-level\": o.logLevel as string,\n \"generate-tags\": o.generateTags as boolean,\n \"tags-file\": o.tagsFile as string,\n \"tags-additional-args\": o.tagsAdditionalArgs as string[],\n };\n const logger = createLogger(flags[\"log-level\"]);\n\n const packageInfo = getPackageInfo(process.cwd(), logger);\n const compiledDist = compile(logger, packageInfo, \"dist\");\n savePacks(logger, compiledDist, \"dist\");\n logger.info(\"Template Pack build done.\");\n\n // Building TS bindings for templates\n const { dts, cjs, mjs } = generateTsBinding(compiledDist);\n await fsp.writeFile(\"dist/index.d.ts\", dts);\n if (packageInfo.type === \"module\") {\n await fsp.writeFile(\"dist/index.cjs\", cjs);\n await fsp.writeFile(\"dist/index.js\", mjs);\n } else {\n await fsp.writeFile(\"dist/index.js\", cjs);\n await fsp.writeFile(\"dist/index.mjs\", mjs);\n }\n\n mergeTagsEnvs(flags);\n if (flags[\"generate-tags\"]) checkAndGenerateCtags(logger, flags);\n\n logger.info(\"Template Pack build done.\");\n });\n\n return cmd;\n}\n\nfunction generateTsBinding(compiledDist: TemplatesAndLibs) {\n let dts = `declare type TemplateFromFile = { readonly type: \"from-file\"; readonly path: string; };\\n`;\n dts += `declare type TplName = ${compiledDist.templates\n .map((tpl) => '\"' + tpl.fullName.id + '\"')\n .join(\" | \")};\\n`;\n dts += `declare const Templates: Record<TplName, TemplateFromFile>;\\n`;\n dts += `export { Templates };\\n`;\n let cjs = `module.exports = { Templates: {\\n`;\n let mjs = `import { resolve } from 'node:path';\\nexport const Templates = {\\n`;\n const recordsCjs = compiledDist.templates\n .map(\n (tpl) =>\n ` '${tpl.fullName.id}': { type: 'from-file', path: require.resolve('./tengo/tpl/${tpl.fullName.id}.plj.gz') }`,\n )\n .join(\",\\n\");\n const recordsMjs = compiledDist.templates\n .map(\n (tpl) =>\n ` '${tpl.fullName.id}': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/${tpl.fullName.id}.plj.gz') }`,\n )\n .join(\",\\n\");\n cjs += recordsCjs;\n mjs += recordsMjs;\n cjs += `\\n}};\\n`;\n mjs += `\\n};\\n`;\n\n return { dts, cjs, mjs };\n}\n\nfunction mergeTagsEnvs(flags: {\n \"generate-tags\": boolean;\n \"tags-file\": string;\n \"tags-additional-args\": string[] | string;\n}) {\n if (process.env.GENERATE_TAGS != undefined) {\n flags[\"generate-tags\"] = process.env.GENERATE_TAGS == \"true\";\n }\n\n if (process.env.TAGS_FILE != undefined) {\n flags[\"tags-file\"] = process.env.TAGS_FILE;\n }\n\n if (process.env.TAGS_ADDITIONAL_ARGS != undefined) {\n flags[\"tags-additional-args\"] = process.env.TAGS_ADDITIONAL_ARGS.split(\",\");\n }\n}\n\nfunction checkAndGenerateCtags(\n logger: winston.Logger,\n flags: {\n \"tags-file\": string;\n \"tags-additional-args\": string[];\n },\n) {\n const fileName = path.resolve(flags[\"tags-file\"]);\n const rootDir = path.dirname(fileName);\n const additionalArgs = flags[\"tags-additional-args\"];\n\n // all tengo files in dirs and subdirs\n // If we don't limit the depth, it could become unbearably slow\n // and even OOM killed node js.\n // Increase the depth if you need to.\n const tengoFiles = toRelativePath(rootDir, getTengoFiles(rootDir, 9));\n\n logger.info(\n `Generating tags for tengo autocompletion from \"${rootDir}\" \\\nin \"${fileName}\", additional arguments: \"${additionalArgs.join('\" \"')}\".\nFound ${tengoFiles.length} tengo files...`,\n );\n\n // see https://docs.ctags.io/en/latest/man/ctags-optlib.7.html#perl-pod\n const result = spawnSync(\n \"ctags\",\n [\n \"-f\",\n fileName,\n ...additionalArgs,\n \"--langdef=tengo\",\n \"--map-tengo=+.tengo\",\n \"--kinddef-tengo=f,function,function\",\n \"--regex-tengo=/^\\\\s*(.*)(:| :=| =) ?func.*/\\\\1/f/\",\n \"--kinddef-tengo=c,constant,constant\",\n '--regex-tengo=/^\\\\s*(.*) := (\"|\\\\{).*/\\\\1/c/',\n \"-R\",\n ...tengoFiles,\n ],\n {\n env: process.env,\n stdio: \"inherit\",\n cwd: rootDir,\n },\n );\n\n if (result.error?.message.includes(\"ENOENT\")) {\n console.log(`\npl-tengo can create tags for tengo autocompletion,\nbut the program should be installed\nwith \"brew install universal-ctags\" on OSX\nor \"sudo apt install universal-ctags\" on Ubuntu.\n\nFor vscode, you should also install ctags extension:\nhttps://marketplace.visualstudio.com/items?itemName=jaydenlin.ctags-support`);\n\n return;\n }\n\n checkRunError(result, \"failed to generate ctags\");\n\n logger.info(\"Generation of tags is done.\");\n}\n\nfunction toRelativePath(dir: string, files: string[]): string[] {\n // Note that VS Code extension likes only relatives paths to the root of the opened dir.\n return files.map((file) => file.replace(dir, \".\"));\n}\n\nfunction checkRunError(result: SpawnSyncReturns<Buffer>, message?: string) {\n if (result.error) {\n console.log(result.error);\n }\n\n const msg = message ?? \"failed to run command\";\n\n if (result.status !== 0) {\n console.log(`WARN: ${msg} the build will continue as-is`);\n }\n}\n"],"mappings":"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"}
|
package/dist/commands/build.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { createLogger } from "../compiler/util.js";
|
|
1
|
+
import { createLogger, getTengoFiles } from "../compiler/util.js";
|
|
2
2
|
import { compile, getPackageInfo, savePacks } from "../compiler/main.js";
|
|
3
3
|
import { CtagsOptions, GlobalOptions, addOptions } from "../shared/basecmd.js";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import * as fs from "node:fs";
|
|
8
7
|
import * as fsp from "node:fs/promises";
|
|
9
8
|
//#region src/commands/build.ts
|
|
10
9
|
function buildCommand() {
|
|
@@ -100,17 +99,6 @@ https://marketplace.visualstudio.com/items?itemName=jaydenlin.ctags-support`);
|
|
|
100
99
|
checkRunError(result, "failed to generate ctags");
|
|
101
100
|
logger.info("Generation of tags is done.");
|
|
102
101
|
}
|
|
103
|
-
function getTengoFiles(dir, depth) {
|
|
104
|
-
if (depth === 0) return [];
|
|
105
|
-
const files = fs.readdirSync(dir, { withFileTypes: true });
|
|
106
|
-
let tengoFiles = [];
|
|
107
|
-
files.forEach((file) => {
|
|
108
|
-
const absPath = path.join(dir, file.name);
|
|
109
|
-
if (file.isDirectory()) tengoFiles = tengoFiles.concat(getTengoFiles(absPath, depth - 1));
|
|
110
|
-
if (!file.isDirectory() && file.name.endsWith(".tengo")) tengoFiles.push(absPath);
|
|
111
|
-
});
|
|
112
|
-
return tengoFiles;
|
|
113
|
-
}
|
|
114
102
|
function toRelativePath(dir, files) {
|
|
115
103
|
return files.map((file) => file.replace(dir, "."));
|
|
116
104
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","names":["opts.GlobalOptions","opts.CtagsOptions"],"sources":["../../src/commands/build.ts"],"sourcesContent":["import type { SpawnSyncReturns } from \"node:child_process\";\nimport { spawnSync } from \"node:child_process\";\nimport { Command } from \"commander\";\nimport { compile, savePacks, getPackageInfo } from \"../compiler/main\";\nimport { createLogger } from \"../compiler/util\";\nimport * as opts from \"../shared/basecmd\";\nimport * as
|
|
1
|
+
{"version":3,"file":"build.js","names":["opts.GlobalOptions","opts.CtagsOptions"],"sources":["../../src/commands/build.ts"],"sourcesContent":["import type { SpawnSyncReturns } from \"node:child_process\";\nimport { spawnSync } from \"node:child_process\";\nimport { Command } from \"commander\";\nimport { compile, savePacks, getPackageInfo } from \"../compiler/main\";\nimport { createLogger, getTengoFiles } from \"../compiler/util\";\nimport * as opts from \"../shared/basecmd\";\nimport * as fsp from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport type * as winston from \"winston\";\nimport type { TemplatesAndLibs } from \"../compiler/compiler\";\n\nexport default function buildCommand(): Command {\n const cmd = new Command(\"build\").description(\n \"build tengo sources into single distributable pack file\",\n );\n\n opts.addOptions(cmd, opts.GlobalOptions(), opts.CtagsOptions());\n\n cmd.action(async (o) => {\n const flags = {\n \"log-level\": o.logLevel as string,\n \"generate-tags\": o.generateTags as boolean,\n \"tags-file\": o.tagsFile as string,\n \"tags-additional-args\": o.tagsAdditionalArgs as string[],\n };\n const logger = createLogger(flags[\"log-level\"]);\n\n const packageInfo = getPackageInfo(process.cwd(), logger);\n const compiledDist = compile(logger, packageInfo, \"dist\");\n savePacks(logger, compiledDist, \"dist\");\n logger.info(\"Template Pack build done.\");\n\n // Building TS bindings for templates\n const { dts, cjs, mjs } = generateTsBinding(compiledDist);\n await fsp.writeFile(\"dist/index.d.ts\", dts);\n if (packageInfo.type === \"module\") {\n await fsp.writeFile(\"dist/index.cjs\", cjs);\n await fsp.writeFile(\"dist/index.js\", mjs);\n } else {\n await fsp.writeFile(\"dist/index.js\", cjs);\n await fsp.writeFile(\"dist/index.mjs\", mjs);\n }\n\n mergeTagsEnvs(flags);\n if (flags[\"generate-tags\"]) checkAndGenerateCtags(logger, flags);\n\n logger.info(\"Template Pack build done.\");\n });\n\n return cmd;\n}\n\nfunction generateTsBinding(compiledDist: TemplatesAndLibs) {\n let dts = `declare type TemplateFromFile = { readonly type: \"from-file\"; readonly path: string; };\\n`;\n dts += `declare type TplName = ${compiledDist.templates\n .map((tpl) => '\"' + tpl.fullName.id + '\"')\n .join(\" | \")};\\n`;\n dts += `declare const Templates: Record<TplName, TemplateFromFile>;\\n`;\n dts += `export { Templates };\\n`;\n let cjs = `module.exports = { Templates: {\\n`;\n let mjs = `import { resolve } from 'node:path';\\nexport const Templates = {\\n`;\n const recordsCjs = compiledDist.templates\n .map(\n (tpl) =>\n ` '${tpl.fullName.id}': { type: 'from-file', path: require.resolve('./tengo/tpl/${tpl.fullName.id}.plj.gz') }`,\n )\n .join(\",\\n\");\n const recordsMjs = compiledDist.templates\n .map(\n (tpl) =>\n ` '${tpl.fullName.id}': { type: 'from-file', path: resolve(import.meta.dirname, './tengo/tpl/${tpl.fullName.id}.plj.gz') }`,\n )\n .join(\",\\n\");\n cjs += recordsCjs;\n mjs += recordsMjs;\n cjs += `\\n}};\\n`;\n mjs += `\\n};\\n`;\n\n return { dts, cjs, mjs };\n}\n\nfunction mergeTagsEnvs(flags: {\n \"generate-tags\": boolean;\n \"tags-file\": string;\n \"tags-additional-args\": string[] | string;\n}) {\n if (process.env.GENERATE_TAGS != undefined) {\n flags[\"generate-tags\"] = process.env.GENERATE_TAGS == \"true\";\n }\n\n if (process.env.TAGS_FILE != undefined) {\n flags[\"tags-file\"] = process.env.TAGS_FILE;\n }\n\n if (process.env.TAGS_ADDITIONAL_ARGS != undefined) {\n flags[\"tags-additional-args\"] = process.env.TAGS_ADDITIONAL_ARGS.split(\",\");\n }\n}\n\nfunction checkAndGenerateCtags(\n logger: winston.Logger,\n flags: {\n \"tags-file\": string;\n \"tags-additional-args\": string[];\n },\n) {\n const fileName = path.resolve(flags[\"tags-file\"]);\n const rootDir = path.dirname(fileName);\n const additionalArgs = flags[\"tags-additional-args\"];\n\n // all tengo files in dirs and subdirs\n // If we don't limit the depth, it could become unbearably slow\n // and even OOM killed node js.\n // Increase the depth if you need to.\n const tengoFiles = toRelativePath(rootDir, getTengoFiles(rootDir, 9));\n\n logger.info(\n `Generating tags for tengo autocompletion from \"${rootDir}\" \\\nin \"${fileName}\", additional arguments: \"${additionalArgs.join('\" \"')}\".\nFound ${tengoFiles.length} tengo files...`,\n );\n\n // see https://docs.ctags.io/en/latest/man/ctags-optlib.7.html#perl-pod\n const result = spawnSync(\n \"ctags\",\n [\n \"-f\",\n fileName,\n ...additionalArgs,\n \"--langdef=tengo\",\n \"--map-tengo=+.tengo\",\n \"--kinddef-tengo=f,function,function\",\n \"--regex-tengo=/^\\\\s*(.*)(:| :=| =) ?func.*/\\\\1/f/\",\n \"--kinddef-tengo=c,constant,constant\",\n '--regex-tengo=/^\\\\s*(.*) := (\"|\\\\{).*/\\\\1/c/',\n \"-R\",\n ...tengoFiles,\n ],\n {\n env: process.env,\n stdio: \"inherit\",\n cwd: rootDir,\n },\n );\n\n if (result.error?.message.includes(\"ENOENT\")) {\n console.log(`\npl-tengo can create tags for tengo autocompletion,\nbut the program should be installed\nwith \"brew install universal-ctags\" on OSX\nor \"sudo apt install universal-ctags\" on Ubuntu.\n\nFor vscode, you should also install ctags extension:\nhttps://marketplace.visualstudio.com/items?itemName=jaydenlin.ctags-support`);\n\n return;\n }\n\n checkRunError(result, \"failed to generate ctags\");\n\n logger.info(\"Generation of tags is done.\");\n}\n\nfunction toRelativePath(dir: string, files: string[]): string[] {\n // Note that VS Code extension likes only relatives paths to the root of the opened dir.\n return files.map((file) => file.replace(dir, \".\"));\n}\n\nfunction checkRunError(result: SpawnSyncReturns<Buffer>, message?: string) {\n if (result.error) {\n console.log(result.error);\n }\n\n const msg = message ?? \"failed to run command\";\n\n if (result.status !== 0) {\n console.log(`WARN: ${msg} the build will continue as-is`);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_util = require("../compiler/util.cjs");
|
|
3
|
+
const require_main = require("../compiler/main.cjs");
|
|
4
|
+
const require_basecmd = require("../shared/basecmd.cjs");
|
|
5
|
+
const require_imports = require("../compiler/imports.cjs");
|
|
6
|
+
let commander = require("commander");
|
|
7
|
+
let node_path = require("node:path");
|
|
8
|
+
node_path = require_runtime.__toESM(node_path, 1);
|
|
9
|
+
let node_fs = require("node:fs");
|
|
10
|
+
node_fs = require_runtime.__toESM(node_fs, 1);
|
|
11
|
+
//#region src/commands/imports.ts
|
|
12
|
+
const maxScanDepth = 9;
|
|
13
|
+
function importsCommand() {
|
|
14
|
+
const cmd = new commander.Command("imports").description("find imports tengo sources do not use");
|
|
15
|
+
cmd.argument("[paths...]", "source paths to scan (defaults to ./src)");
|
|
16
|
+
require_basecmd.addOptions(cmd, require_basecmd.GlobalOptions());
|
|
17
|
+
cmd.addOption(new commander.Option("--check", "report unused imports and fail, change no file"));
|
|
18
|
+
cmd.addOption(new commander.Option("--fix", "remove unused imports from the sources"));
|
|
19
|
+
cmd.action(async (paths, o) => {
|
|
20
|
+
const logger = require_util.createLogger(o.logLevel);
|
|
21
|
+
if (o.check && o.fix) cmd.error("error: options '--check' and '--fix' cannot be used together");
|
|
22
|
+
const fix = o.fix ? true : o.check ? false : !process.env.CI;
|
|
23
|
+
const roots = paths.length == 0 ? ["./src"] : paths;
|
|
24
|
+
const localPackageName = getLocalPackageName();
|
|
25
|
+
let hasUnusedImports = false;
|
|
26
|
+
let hasBrokenSources = false;
|
|
27
|
+
for (const file of collectSources(roots)) {
|
|
28
|
+
const src = node_fs.readFileSync(file).toString();
|
|
29
|
+
let unused;
|
|
30
|
+
try {
|
|
31
|
+
unused = require_imports.findUnusedImports(logger, src, localPackageName, file);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
logger.error(err);
|
|
34
|
+
hasBrokenSources = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (unused.length == 0) continue;
|
|
38
|
+
hasUnusedImports = true;
|
|
39
|
+
logger.warn(`${file}: unused imports: ${unused.map((im) => im.alias).join(", ")}`);
|
|
40
|
+
if (fix) {
|
|
41
|
+
logger.info(`fixing ${file}...`);
|
|
42
|
+
node_fs.writeFileSync(file, require_imports.dropImportLines(src, unused.map((im) => im.lineNo)));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (hasBrokenSources || hasUnusedImports && !fix) process.exit(1);
|
|
46
|
+
});
|
|
47
|
+
return cmd;
|
|
48
|
+
}
|
|
49
|
+
/** Package name of the sources we scan. Used to resolve local ':<item>'
|
|
50
|
+
* imports. An empty name keeps the parser working for a directory that is
|
|
51
|
+
* not a package. */
|
|
52
|
+
function getLocalPackageName() {
|
|
53
|
+
try {
|
|
54
|
+
const packageJson = node_fs.readFileSync(require_main.resolvePackageJsonRoot(process.cwd())).toString();
|
|
55
|
+
return JSON.parse(packageJson).name ?? "";
|
|
56
|
+
} catch {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function collectSources(roots) {
|
|
61
|
+
const sources = [];
|
|
62
|
+
for (const root of roots) {
|
|
63
|
+
const type = require_util.pathType(root);
|
|
64
|
+
if (type === "dir") {
|
|
65
|
+
sources.push(...require_util.getTengoFiles(node_path.resolve(root), maxScanDepth));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (type === "file") {
|
|
69
|
+
sources.push(node_path.resolve(root));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`source path not found: ${root}`);
|
|
73
|
+
}
|
|
74
|
+
return sources;
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
exports.default = importsCommand;
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=imports.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imports.cjs","names":["Command","Option","createLogger","fs","findUnusedImports","dropImportLines","resolvePackageJsonRoot","pathType","getTengoFiles","path"],"sources":["../../src/commands/imports.ts"],"sourcesContent":["import { Command, Option } from \"commander\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { createLogger, getTengoFiles, pathType } from \"../compiler/util\";\nimport { dropImportLines, findUnusedImports } from \"../compiler/imports\";\nimport { resolvePackageJsonRoot } from \"../compiler/main\";\nimport * as opts from \"../shared/basecmd\";\n\n// The same limit the tags generation uses: deep enough for any real source\n// tree, shallow enough to keep a scan started in a wrong place cheap.\nconst maxScanDepth = 9;\n\nexport default function importsCommand(): Command {\n const cmd = new Command(\"imports\").description(\"find imports tengo sources do not use\");\n\n cmd.argument(\"[paths...]\", \"source paths to scan (defaults to ./src)\");\n opts.addOptions(cmd, opts.GlobalOptions());\n cmd.addOption(new Option(\"--check\", \"report unused imports and fail, change no file\"));\n cmd.addOption(new Option(\"--fix\", \"remove unused imports from the sources\"));\n\n cmd.action(async (paths: string[], o) => {\n const logger = createLogger(o.logLevel as string);\n\n if (o.check && o.fix) {\n cmd.error(\"error: options '--check' and '--fix' cannot be used together\");\n }\n\n // Without an explicit mode: fix the sources when a human runs the command,\n // report and fail when CI does. CI must never commit a change of its own.\n const fix = o.fix ? true : o.check ? false : !process.env.CI;\n\n const roots = paths.length == 0 ? [\"./src\"] : paths;\n const localPackageName = getLocalPackageName();\n\n let hasUnusedImports = false;\n let hasBrokenSources = false;\n\n for (const file of collectSources(roots)) {\n const src = fs.readFileSync(file).toString();\n\n let unused;\n try {\n unused = findUnusedImports(logger, src, localPackageName, file);\n } catch (err: unknown) {\n // A source we cannot parse is a problem for 'pl-tengo check' to\n // report in full. Here we keep the file as it is and move on, so a\n // single broken source does not block the cleanup of all the others.\n logger.error(err);\n hasBrokenSources = true;\n continue;\n }\n\n if (unused.length == 0) {\n continue;\n }\n\n hasUnusedImports = true;\n logger.warn(`${file}: unused imports: ${unused.map((im) => im.alias).join(\", \")}`);\n\n if (fix) {\n logger.info(`fixing ${file}...`);\n fs.writeFileSync(\n file,\n dropImportLines(\n src,\n unused.map((im) => im.lineNo),\n ),\n );\n }\n }\n\n if (hasBrokenSources || (hasUnusedImports && !fix)) {\n process.exit(1);\n }\n });\n\n return cmd;\n}\n\n/** Package name of the sources we scan. Used to resolve local ':<item>'\n * imports. An empty name keeps the parser working for a directory that is\n * not a package. */\nfunction getLocalPackageName(): string {\n try {\n const packageJson = fs.readFileSync(resolvePackageJsonRoot(process.cwd())).toString();\n return (JSON.parse(packageJson) as { name?: string }).name ?? \"\";\n } catch {\n return \"\";\n }\n}\n\nfunction collectSources(roots: string[]): string[] {\n const sources: string[] = [];\n\n for (const root of roots) {\n const type = pathType(root);\n if (type === \"dir\") {\n sources.push(...getTengoFiles(path.resolve(root), maxScanDepth));\n continue;\n }\n if (type === \"file\") {\n sources.push(path.resolve(root));\n continue;\n }\n\n throw new Error(`source path not found: ${root}`);\n }\n\n return sources;\n}\n"],"mappings":";;;;;;;;;;;AAUA,MAAM,eAAe;AAErB,SAAwB,iBAA0B;CAChD,MAAM,MAAM,IAAIA,UAAAA,QAAQ,SAAS,CAAC,CAAC,YAAY,uCAAuC;CAEtF,IAAI,SAAS,cAAc,0CAA0C;CACrE,gBAAA,WAAgB,KAAA,gBAAA,cAAwB,CAAC;CACzC,IAAI,UAAU,IAAIC,UAAAA,OAAO,WAAW,gDAAgD,CAAC;CACrF,IAAI,UAAU,IAAIA,UAAAA,OAAO,SAAS,wCAAwC,CAAC;CAE3E,IAAI,OAAO,OAAO,OAAiB,MAAM;EACvC,MAAM,SAASC,aAAAA,aAAa,EAAE,QAAkB;EAEhD,IAAI,EAAE,SAAS,EAAE,KACf,IAAI,MAAM,8DAA8D;EAK1E,MAAM,MAAM,EAAE,MAAM,OAAO,EAAE,QAAQ,QAAQ,CAAC,QAAQ,IAAI;EAE1D,MAAM,QAAQ,MAAM,UAAU,IAAI,CAAC,OAAO,IAAI;EAC9C,MAAM,mBAAmB,oBAAoB;EAE7C,IAAI,mBAAmB;EACvB,IAAI,mBAAmB;EAEvB,KAAK,MAAM,QAAQ,eAAe,KAAK,GAAG;GACxC,MAAM,MAAMC,QAAG,aAAa,IAAI,CAAC,CAAC,SAAS;GAE3C,IAAI;GACJ,IAAI;IACF,SAASC,gBAAAA,kBAAkB,QAAQ,KAAK,kBAAkB,IAAI;GAChE,SAAS,KAAc;IAIrB,OAAO,MAAM,GAAG;IAChB,mBAAmB;IACnB;GACF;GAEA,IAAI,OAAO,UAAU,GACnB;GAGF,mBAAmB;GACnB,OAAO,KAAK,GAAG,KAAK,oBAAoB,OAAO,KAAK,OAAO,GAAG,KAAK,CAAC,CAAC,KAAK,IAAI,GAAG;GAEjF,IAAI,KAAK;IACP,OAAO,KAAK,UAAU,KAAK,IAAI;IAC/B,QAAG,cACD,MACAC,gBAAAA,gBACE,KACA,OAAO,KAAK,OAAO,GAAG,MAAM,CAC9B,CACF;GACF;EACF;EAEA,IAAI,oBAAqB,oBAAoB,CAAC,KAC5C,QAAQ,KAAK,CAAC;CAElB,CAAC;CAED,OAAO;AACT;;;;AAKA,SAAS,sBAA8B;CACrC,IAAI;EACF,MAAM,cAAcF,QAAG,aAAaG,aAAAA,uBAAuB,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;EACpF,OAAQ,KAAK,MAAM,WAAW,CAAC,CAAuB,QAAQ;CAChE,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,eAAe,OAA2B;CACjD,MAAM,UAAoB,CAAC;CAE3B,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAOC,aAAAA,SAAS,IAAI;EAC1B,IAAI,SAAS,OAAO;GAClB,QAAQ,KAAK,GAAGC,aAAAA,cAAcC,UAAK,QAAQ,IAAI,GAAG,YAAY,CAAC;GAC/D;EACF;EACA,IAAI,SAAS,QAAQ;GACnB,QAAQ,KAAKA,UAAK,QAAQ,IAAI,CAAC;GAC/B;EACF;EAEA,MAAM,IAAI,MAAM,0BAA0B,MAAM;CAClD;CAEA,OAAO;AACT"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createLogger, getTengoFiles, pathType } from "../compiler/util.js";
|
|
2
|
+
import { resolvePackageJsonRoot } from "../compiler/main.js";
|
|
3
|
+
import { GlobalOptions, addOptions } from "../shared/basecmd.js";
|
|
4
|
+
import { dropImportLines, findUnusedImports } from "../compiler/imports.js";
|
|
5
|
+
import { Command, Option } from "commander";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
//#region src/commands/imports.ts
|
|
9
|
+
const maxScanDepth = 9;
|
|
10
|
+
function importsCommand() {
|
|
11
|
+
const cmd = new Command("imports").description("find imports tengo sources do not use");
|
|
12
|
+
cmd.argument("[paths...]", "source paths to scan (defaults to ./src)");
|
|
13
|
+
addOptions(cmd, GlobalOptions());
|
|
14
|
+
cmd.addOption(new Option("--check", "report unused imports and fail, change no file"));
|
|
15
|
+
cmd.addOption(new Option("--fix", "remove unused imports from the sources"));
|
|
16
|
+
cmd.action(async (paths, o) => {
|
|
17
|
+
const logger = createLogger(o.logLevel);
|
|
18
|
+
if (o.check && o.fix) cmd.error("error: options '--check' and '--fix' cannot be used together");
|
|
19
|
+
const fix = o.fix ? true : o.check ? false : !process.env.CI;
|
|
20
|
+
const roots = paths.length == 0 ? ["./src"] : paths;
|
|
21
|
+
const localPackageName = getLocalPackageName();
|
|
22
|
+
let hasUnusedImports = false;
|
|
23
|
+
let hasBrokenSources = false;
|
|
24
|
+
for (const file of collectSources(roots)) {
|
|
25
|
+
const src = fs.readFileSync(file).toString();
|
|
26
|
+
let unused;
|
|
27
|
+
try {
|
|
28
|
+
unused = findUnusedImports(logger, src, localPackageName, file);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
logger.error(err);
|
|
31
|
+
hasBrokenSources = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (unused.length == 0) continue;
|
|
35
|
+
hasUnusedImports = true;
|
|
36
|
+
logger.warn(`${file}: unused imports: ${unused.map((im) => im.alias).join(", ")}`);
|
|
37
|
+
if (fix) {
|
|
38
|
+
logger.info(`fixing ${file}...`);
|
|
39
|
+
fs.writeFileSync(file, dropImportLines(src, unused.map((im) => im.lineNo)));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (hasBrokenSources || hasUnusedImports && !fix) process.exit(1);
|
|
43
|
+
});
|
|
44
|
+
return cmd;
|
|
45
|
+
}
|
|
46
|
+
/** Package name of the sources we scan. Used to resolve local ':<item>'
|
|
47
|
+
* imports. An empty name keeps the parser working for a directory that is
|
|
48
|
+
* not a package. */
|
|
49
|
+
function getLocalPackageName() {
|
|
50
|
+
try {
|
|
51
|
+
const packageJson = fs.readFileSync(resolvePackageJsonRoot(process.cwd())).toString();
|
|
52
|
+
return JSON.parse(packageJson).name ?? "";
|
|
53
|
+
} catch {
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function collectSources(roots) {
|
|
58
|
+
const sources = [];
|
|
59
|
+
for (const root of roots) {
|
|
60
|
+
const type = pathType(root);
|
|
61
|
+
if (type === "dir") {
|
|
62
|
+
sources.push(...getTengoFiles(path.resolve(root), maxScanDepth));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (type === "file") {
|
|
66
|
+
sources.push(path.resolve(root));
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`source path not found: ${root}`);
|
|
70
|
+
}
|
|
71
|
+
return sources;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { importsCommand as default };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=imports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imports.js","names":["opts.GlobalOptions"],"sources":["../../src/commands/imports.ts"],"sourcesContent":["import { Command, Option } from \"commander\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { createLogger, getTengoFiles, pathType } from \"../compiler/util\";\nimport { dropImportLines, findUnusedImports } from \"../compiler/imports\";\nimport { resolvePackageJsonRoot } from \"../compiler/main\";\nimport * as opts from \"../shared/basecmd\";\n\n// The same limit the tags generation uses: deep enough for any real source\n// tree, shallow enough to keep a scan started in a wrong place cheap.\nconst maxScanDepth = 9;\n\nexport default function importsCommand(): Command {\n const cmd = new Command(\"imports\").description(\"find imports tengo sources do not use\");\n\n cmd.argument(\"[paths...]\", \"source paths to scan (defaults to ./src)\");\n opts.addOptions(cmd, opts.GlobalOptions());\n cmd.addOption(new Option(\"--check\", \"report unused imports and fail, change no file\"));\n cmd.addOption(new Option(\"--fix\", \"remove unused imports from the sources\"));\n\n cmd.action(async (paths: string[], o) => {\n const logger = createLogger(o.logLevel as string);\n\n if (o.check && o.fix) {\n cmd.error(\"error: options '--check' and '--fix' cannot be used together\");\n }\n\n // Without an explicit mode: fix the sources when a human runs the command,\n // report and fail when CI does. CI must never commit a change of its own.\n const fix = o.fix ? true : o.check ? false : !process.env.CI;\n\n const roots = paths.length == 0 ? [\"./src\"] : paths;\n const localPackageName = getLocalPackageName();\n\n let hasUnusedImports = false;\n let hasBrokenSources = false;\n\n for (const file of collectSources(roots)) {\n const src = fs.readFileSync(file).toString();\n\n let unused;\n try {\n unused = findUnusedImports(logger, src, localPackageName, file);\n } catch (err: unknown) {\n // A source we cannot parse is a problem for 'pl-tengo check' to\n // report in full. Here we keep the file as it is and move on, so a\n // single broken source does not block the cleanup of all the others.\n logger.error(err);\n hasBrokenSources = true;\n continue;\n }\n\n if (unused.length == 0) {\n continue;\n }\n\n hasUnusedImports = true;\n logger.warn(`${file}: unused imports: ${unused.map((im) => im.alias).join(\", \")}`);\n\n if (fix) {\n logger.info(`fixing ${file}...`);\n fs.writeFileSync(\n file,\n dropImportLines(\n src,\n unused.map((im) => im.lineNo),\n ),\n );\n }\n }\n\n if (hasBrokenSources || (hasUnusedImports && !fix)) {\n process.exit(1);\n }\n });\n\n return cmd;\n}\n\n/** Package name of the sources we scan. Used to resolve local ':<item>'\n * imports. An empty name keeps the parser working for a directory that is\n * not a package. */\nfunction getLocalPackageName(): string {\n try {\n const packageJson = fs.readFileSync(resolvePackageJsonRoot(process.cwd())).toString();\n return (JSON.parse(packageJson) as { name?: string }).name ?? \"\";\n } catch {\n return \"\";\n }\n}\n\nfunction collectSources(roots: string[]): string[] {\n const sources: string[] = [];\n\n for (const root of roots) {\n const type = pathType(root);\n if (type === \"dir\") {\n sources.push(...getTengoFiles(path.resolve(root), maxScanDepth));\n continue;\n }\n if (type === \"file\") {\n sources.push(path.resolve(root));\n continue;\n }\n\n throw new Error(`source path not found: ${root}`);\n }\n\n return sources;\n}\n"],"mappings":";;;;;;;;AAUA,MAAM,eAAe;AAErB,SAAwB,iBAA0B;CAChD,MAAM,MAAM,IAAI,QAAQ,SAAS,CAAC,CAAC,YAAY,uCAAuC;CAEtF,IAAI,SAAS,cAAc,0CAA0C;CACrE,WAAgB,KAAKA,cAAmB,CAAC;CACzC,IAAI,UAAU,IAAI,OAAO,WAAW,gDAAgD,CAAC;CACrF,IAAI,UAAU,IAAI,OAAO,SAAS,wCAAwC,CAAC;CAE3E,IAAI,OAAO,OAAO,OAAiB,MAAM;EACvC,MAAM,SAAS,aAAa,EAAE,QAAkB;EAEhD,IAAI,EAAE,SAAS,EAAE,KACf,IAAI,MAAM,8DAA8D;EAK1E,MAAM,MAAM,EAAE,MAAM,OAAO,EAAE,QAAQ,QAAQ,CAAC,QAAQ,IAAI;EAE1D,MAAM,QAAQ,MAAM,UAAU,IAAI,CAAC,OAAO,IAAI;EAC9C,MAAM,mBAAmB,oBAAoB;EAE7C,IAAI,mBAAmB;EACvB,IAAI,mBAAmB;EAEvB,KAAK,MAAM,QAAQ,eAAe,KAAK,GAAG;GACxC,MAAM,MAAM,GAAG,aAAa,IAAI,CAAC,CAAC,SAAS;GAE3C,IAAI;GACJ,IAAI;IACF,SAAS,kBAAkB,QAAQ,KAAK,kBAAkB,IAAI;GAChE,SAAS,KAAc;IAIrB,OAAO,MAAM,GAAG;IAChB,mBAAmB;IACnB;GACF;GAEA,IAAI,OAAO,UAAU,GACnB;GAGF,mBAAmB;GACnB,OAAO,KAAK,GAAG,KAAK,oBAAoB,OAAO,KAAK,OAAO,GAAG,KAAK,CAAC,CAAC,KAAK,IAAI,GAAG;GAEjF,IAAI,KAAK;IACP,OAAO,KAAK,UAAU,KAAK,IAAI;IAC/B,GAAG,cACD,MACA,gBACE,KACA,OAAO,KAAK,OAAO,GAAG,MAAM,CAC9B,CACF;GACF;EACF;EAEA,IAAI,oBAAqB,oBAAoB,CAAC,KAC5C,QAAQ,KAAK,CAAC;CAElB,CAAC;CAED,OAAO;AACT;;;;AAKA,SAAS,sBAA8B;CACrC,IAAI;EACF,MAAM,cAAc,GAAG,aAAa,uBAAuB,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;EACpF,OAAQ,KAAK,MAAM,WAAW,CAAC,CAAuB,QAAQ;CAChE,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,eAAe,OAA2B;CACjD,MAAM,UAAoB,CAAC;CAE3B,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,SAAS,IAAI;EAC1B,IAAI,SAAS,OAAO;GAClB,QAAQ,KAAK,GAAG,cAAc,KAAK,QAAQ,IAAI,GAAG,YAAY,CAAC;GAC/D;EACF;EACA,IAAI,SAAS,QAAQ;GACnB,QAAQ,KAAK,KAAK,QAAQ,IAAI,CAAC;GAC/B;EACF;EAEA,MAAM,IAAI,MAAM,0BAA0B,MAAM;CAClD;CAEA,OAAO;AACT"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const require_source = require("./source.cjs");
|
|
2
|
+
//#region src/compiler/imports.ts
|
|
3
|
+
function newParserContext() {
|
|
4
|
+
return {
|
|
5
|
+
isInCommentBlock: false,
|
|
6
|
+
canDetectOptions: true,
|
|
7
|
+
artifactImportREs: /* @__PURE__ */ new Map(),
|
|
8
|
+
importLikeREs: /* @__PURE__ */ new Map(),
|
|
9
|
+
multilineStatement: "",
|
|
10
|
+
lineNo: 0
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function escapeForRegExp(s) {
|
|
14
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Find imports the source does not use.
|
|
18
|
+
*
|
|
19
|
+
* An import is used when its alias appears with a dot somewhere else in the
|
|
20
|
+
* code: 'text.split(...)'. A module passed around as a value, say
|
|
21
|
+
* 'myFunc(text)', counts as unused. This is deliberate: the cost of keeping an
|
|
22
|
+
* import the source no longer needs is nothing, while the cost of dropping one
|
|
23
|
+
* it does need is a broken build.
|
|
24
|
+
*
|
|
25
|
+
* Throws when the source does not parse, with the same line context the
|
|
26
|
+
* compiler reports.
|
|
27
|
+
*/
|
|
28
|
+
function findUnusedImports(logger, src, localPackageName, srcName) {
|
|
29
|
+
const imports = [];
|
|
30
|
+
const codeLines = [];
|
|
31
|
+
let context = newParserContext();
|
|
32
|
+
const lines = src.split("\n");
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
context.lineNo++;
|
|
35
|
+
try {
|
|
36
|
+
const result = require_source.parseSingleSourceLine(logger, line, context, localPackageName, false);
|
|
37
|
+
context = result.context;
|
|
38
|
+
codeLines.push(result.line);
|
|
39
|
+
if (result.moduleImport) imports.push({
|
|
40
|
+
...result.moduleImport,
|
|
41
|
+
lineNo: context.lineNo - 1
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const err = error;
|
|
45
|
+
throw new Error(`[line ${context.lineNo} in ${srcName}]: ${err.message}\n\t${line}`, { cause: err });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return imports.filter((im) => {
|
|
49
|
+
const usageRE = new RegExp(`\\b${escapeForRegExp(im.alias)}\\s*\\.`);
|
|
50
|
+
return !codeLines.some((code, lineNo) => lineNo !== im.lineNo && usageRE.test(code));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Drop the given lines from the source.
|
|
55
|
+
*
|
|
56
|
+
* The source is cut as it was written: comments, blank lines and formatting
|
|
57
|
+
* outside the dropped lines stay byte for byte the same. An import is always a
|
|
58
|
+
* single line, as the parser accepts it only as an assignment.
|
|
59
|
+
*/
|
|
60
|
+
function dropImportLines(src, lineNos) {
|
|
61
|
+
const drop = new Set(lineNos);
|
|
62
|
+
return src.split("\n").filter((_, lineNo) => !drop.has(lineNo)).join("\n");
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
exports.dropImportLines = dropImportLines;
|
|
66
|
+
exports.findUnusedImports = findUnusedImports;
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=imports.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imports.cjs","names":["parseSingleSourceLine"],"sources":["../../src/compiler/imports.ts"],"sourcesContent":["// Unused import detection for Tengo sources.\n//\n// The check runs on the parser's own line results, not on the raw text: a\n// comment line comes back empty, so an alias that appears only in a comment\n// correctly counts as unused.\n\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport type { ImportInfo, sourceParserContext } from \"./source\";\nimport { parseSingleSourceLine } from \"./source\";\n\n/** A module import statement and the line it is on. */\nexport type TengoImport = ImportInfo & {\n /** Line index in the source, counted from 0. */\n lineNo: number;\n};\n\nfunction newParserContext(): sourceParserContext {\n return {\n isInCommentBlock: false,\n canDetectOptions: true,\n artifactImportREs: new Map(),\n importLikeREs: new Map(),\n multilineStatement: \"\",\n lineNo: 0,\n };\n}\n\nfunction escapeForRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Find imports the source does not use.\n *\n * An import is used when its alias appears with a dot somewhere else in the\n * code: 'text.split(...)'. A module passed around as a value, say\n * 'myFunc(text)', counts as unused. This is deliberate: the cost of keeping an\n * import the source no longer needs is nothing, while the cost of dropping one\n * it does need is a broken build.\n *\n * Throws when the source does not parse, with the same line context the\n * compiler reports.\n */\nexport function findUnusedImports(\n logger: MiLogger,\n src: string,\n localPackageName: string,\n srcName: string,\n): TengoImport[] {\n const imports: TengoImport[] = [];\n\n // Lines as the parser sees them: comments are empty, code is untouched.\n const codeLines: string[] = [];\n\n let context = newParserContext();\n\n const lines = src.split(\"\\n\");\n for (const line of lines) {\n context.lineNo++;\n\n try {\n const result = parseSingleSourceLine(logger, line, context, localPackageName, false);\n context = result.context;\n codeLines.push(result.line);\n\n if (result.moduleImport) {\n imports.push({ ...result.moduleImport, lineNo: context.lineNo - 1 });\n }\n } catch (error: unknown) {\n const err = error as Error;\n throw new Error(`[line ${context.lineNo} in ${srcName}]: ${err.message}\\n\\t${line}`, {\n cause: err,\n });\n }\n }\n\n return imports.filter((im) => {\n const usageRE = new RegExp(`\\\\b${escapeForRegExp(im.alias)}\\\\s*\\\\.`);\n return !codeLines.some((code, lineNo) => lineNo !== im.lineNo && usageRE.test(code));\n });\n}\n\n/**\n * Drop the given lines from the source.\n *\n * The source is cut as it was written: comments, blank lines and formatting\n * outside the dropped lines stay byte for byte the same. An import is always a\n * single line, as the parser accepts it only as an assignment.\n */\nexport function dropImportLines(src: string, lineNos: number[]): string {\n const drop = new Set(lineNos);\n return src\n .split(\"\\n\")\n .filter((_, lineNo) => !drop.has(lineNo))\n .join(\"\\n\");\n}\n"],"mappings":";;AAgBA,SAAS,mBAAwC;CAC/C,OAAO;EACL,kBAAkB;EAClB,kBAAkB;EAClB,mCAAmB,IAAI,IAAI;EAC3B,+BAAe,IAAI,IAAI;EACvB,oBAAoB;EACpB,QAAQ;CACV;AACF;AAEA,SAAS,gBAAgB,GAAmB;CAC1C,OAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;;;;;;;;;;;;;AAcA,SAAgB,kBACd,QACA,KACA,kBACA,SACe;CACf,MAAM,UAAyB,CAAC;CAGhC,MAAM,YAAsB,CAAC;CAE7B,IAAI,UAAU,iBAAiB;CAE/B,MAAM,QAAQ,IAAI,MAAM,IAAI;CAC5B,KAAK,MAAM,QAAQ,OAAO;EACxB,QAAQ;EAER,IAAI;GACF,MAAM,SAASA,eAAAA,sBAAsB,QAAQ,MAAM,SAAS,kBAAkB,KAAK;GACnF,UAAU,OAAO;GACjB,UAAU,KAAK,OAAO,IAAI;GAE1B,IAAI,OAAO,cACT,QAAQ,KAAK;IAAE,GAAG,OAAO;IAAc,QAAQ,QAAQ,SAAS;GAAE,CAAC;EAEvE,SAAS,OAAgB;GACvB,MAAM,MAAM;GACZ,MAAM,IAAI,MAAM,SAAS,QAAQ,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAAQ,EACnF,OAAO,IACT,CAAC;EACH;CACF;CAEA,OAAO,QAAQ,QAAQ,OAAO;EAC5B,MAAM,UAAU,IAAI,OAAO,MAAM,gBAAgB,GAAG,KAAK,EAAE,QAAQ;EACnE,OAAO,CAAC,UAAU,MAAM,MAAM,WAAW,WAAW,GAAG,UAAU,QAAQ,KAAK,IAAI,CAAC;CACrF,CAAC;AACH;;;;;;;;AASA,SAAgB,gBAAgB,KAAa,SAA2B;CACtE,MAAM,OAAO,IAAI,IAAI,OAAO;CAC5B,OAAO,IACJ,MAAM,IAAI,CAAC,CACX,QAAQ,GAAG,WAAW,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC,CACxC,KAAK,IAAI;AACd"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { parseSingleSourceLine } from "./source.js";
|
|
2
|
+
//#region src/compiler/imports.ts
|
|
3
|
+
function newParserContext() {
|
|
4
|
+
return {
|
|
5
|
+
isInCommentBlock: false,
|
|
6
|
+
canDetectOptions: true,
|
|
7
|
+
artifactImportREs: /* @__PURE__ */ new Map(),
|
|
8
|
+
importLikeREs: /* @__PURE__ */ new Map(),
|
|
9
|
+
multilineStatement: "",
|
|
10
|
+
lineNo: 0
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function escapeForRegExp(s) {
|
|
14
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Find imports the source does not use.
|
|
18
|
+
*
|
|
19
|
+
* An import is used when its alias appears with a dot somewhere else in the
|
|
20
|
+
* code: 'text.split(...)'. A module passed around as a value, say
|
|
21
|
+
* 'myFunc(text)', counts as unused. This is deliberate: the cost of keeping an
|
|
22
|
+
* import the source no longer needs is nothing, while the cost of dropping one
|
|
23
|
+
* it does need is a broken build.
|
|
24
|
+
*
|
|
25
|
+
* Throws when the source does not parse, with the same line context the
|
|
26
|
+
* compiler reports.
|
|
27
|
+
*/
|
|
28
|
+
function findUnusedImports(logger, src, localPackageName, srcName) {
|
|
29
|
+
const imports = [];
|
|
30
|
+
const codeLines = [];
|
|
31
|
+
let context = newParserContext();
|
|
32
|
+
const lines = src.split("\n");
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
context.lineNo++;
|
|
35
|
+
try {
|
|
36
|
+
const result = parseSingleSourceLine(logger, line, context, localPackageName, false);
|
|
37
|
+
context = result.context;
|
|
38
|
+
codeLines.push(result.line);
|
|
39
|
+
if (result.moduleImport) imports.push({
|
|
40
|
+
...result.moduleImport,
|
|
41
|
+
lineNo: context.lineNo - 1
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
const err = error;
|
|
45
|
+
throw new Error(`[line ${context.lineNo} in ${srcName}]: ${err.message}\n\t${line}`, { cause: err });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return imports.filter((im) => {
|
|
49
|
+
const usageRE = new RegExp(`\\b${escapeForRegExp(im.alias)}\\s*\\.`);
|
|
50
|
+
return !codeLines.some((code, lineNo) => lineNo !== im.lineNo && usageRE.test(code));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Drop the given lines from the source.
|
|
55
|
+
*
|
|
56
|
+
* The source is cut as it was written: comments, blank lines and formatting
|
|
57
|
+
* outside the dropped lines stay byte for byte the same. An import is always a
|
|
58
|
+
* single line, as the parser accepts it only as an assignment.
|
|
59
|
+
*/
|
|
60
|
+
function dropImportLines(src, lineNos) {
|
|
61
|
+
const drop = new Set(lineNos);
|
|
62
|
+
return src.split("\n").filter((_, lineNo) => !drop.has(lineNo)).join("\n");
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { dropImportLines, findUnusedImports };
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=imports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"imports.js","names":[],"sources":["../../src/compiler/imports.ts"],"sourcesContent":["// Unused import detection for Tengo sources.\n//\n// The check runs on the parser's own line results, not on the raw text: a\n// comment line comes back empty, so an alias that appears only in a comment\n// correctly counts as unused.\n\nimport type { MiLogger } from \"@milaboratories/ts-helpers\";\nimport type { ImportInfo, sourceParserContext } from \"./source\";\nimport { parseSingleSourceLine } from \"./source\";\n\n/** A module import statement and the line it is on. */\nexport type TengoImport = ImportInfo & {\n /** Line index in the source, counted from 0. */\n lineNo: number;\n};\n\nfunction newParserContext(): sourceParserContext {\n return {\n isInCommentBlock: false,\n canDetectOptions: true,\n artifactImportREs: new Map(),\n importLikeREs: new Map(),\n multilineStatement: \"\",\n lineNo: 0,\n };\n}\n\nfunction escapeForRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Find imports the source does not use.\n *\n * An import is used when its alias appears with a dot somewhere else in the\n * code: 'text.split(...)'. A module passed around as a value, say\n * 'myFunc(text)', counts as unused. This is deliberate: the cost of keeping an\n * import the source no longer needs is nothing, while the cost of dropping one\n * it does need is a broken build.\n *\n * Throws when the source does not parse, with the same line context the\n * compiler reports.\n */\nexport function findUnusedImports(\n logger: MiLogger,\n src: string,\n localPackageName: string,\n srcName: string,\n): TengoImport[] {\n const imports: TengoImport[] = [];\n\n // Lines as the parser sees them: comments are empty, code is untouched.\n const codeLines: string[] = [];\n\n let context = newParserContext();\n\n const lines = src.split(\"\\n\");\n for (const line of lines) {\n context.lineNo++;\n\n try {\n const result = parseSingleSourceLine(logger, line, context, localPackageName, false);\n context = result.context;\n codeLines.push(result.line);\n\n if (result.moduleImport) {\n imports.push({ ...result.moduleImport, lineNo: context.lineNo - 1 });\n }\n } catch (error: unknown) {\n const err = error as Error;\n throw new Error(`[line ${context.lineNo} in ${srcName}]: ${err.message}\\n\\t${line}`, {\n cause: err,\n });\n }\n }\n\n return imports.filter((im) => {\n const usageRE = new RegExp(`\\\\b${escapeForRegExp(im.alias)}\\\\s*\\\\.`);\n return !codeLines.some((code, lineNo) => lineNo !== im.lineNo && usageRE.test(code));\n });\n}\n\n/**\n * Drop the given lines from the source.\n *\n * The source is cut as it was written: comments, blank lines and formatting\n * outside the dropped lines stay byte for byte the same. An import is always a\n * single line, as the parser accepts it only as an assignment.\n */\nexport function dropImportLines(src: string, lineNos: number[]): string {\n const drop = new Set(lineNos);\n return src\n .split(\"\\n\")\n .filter((_, lineNo) => !drop.has(lineNo))\n .join(\"\\n\");\n}\n"],"mappings":";;AAgBA,SAAS,mBAAwC;CAC/C,OAAO;EACL,kBAAkB;EAClB,kBAAkB;EAClB,mCAAmB,IAAI,IAAI;EAC3B,+BAAe,IAAI,IAAI;EACvB,oBAAoB;EACpB,QAAQ;CACV;AACF;AAEA,SAAS,gBAAgB,GAAmB;CAC1C,OAAO,EAAE,QAAQ,uBAAuB,MAAM;AAChD;;;;;;;;;;;;;AAcA,SAAgB,kBACd,QACA,KACA,kBACA,SACe;CACf,MAAM,UAAyB,CAAC;CAGhC,MAAM,YAAsB,CAAC;CAE7B,IAAI,UAAU,iBAAiB;CAE/B,MAAM,QAAQ,IAAI,MAAM,IAAI;CAC5B,KAAK,MAAM,QAAQ,OAAO;EACxB,QAAQ;EAER,IAAI;GACF,MAAM,SAAS,sBAAsB,QAAQ,MAAM,SAAS,kBAAkB,KAAK;GACnF,UAAU,OAAO;GACjB,UAAU,KAAK,OAAO,IAAI;GAE1B,IAAI,OAAO,cACT,QAAQ,KAAK;IAAE,GAAG,OAAO;IAAc,QAAQ,QAAQ,SAAS;GAAE,CAAC;EAEvE,SAAS,OAAgB;GACvB,MAAM,MAAM;GACZ,MAAM,IAAI,MAAM,SAAS,QAAQ,OAAO,MAAM,QAAQ,KAAK,IAAI,QAAQ,MAAM,QAAQ,EACnF,OAAO,IACT,CAAC;EACH;CACF;CAEA,OAAO,QAAQ,QAAQ,OAAO;EAC5B,MAAM,UAAU,IAAI,OAAO,MAAM,gBAAgB,GAAG,KAAK,EAAE,QAAQ;EACnE,OAAO,CAAC,UAAU,MAAM,MAAM,WAAW,WAAW,GAAG,UAAU,QAAQ,KAAK,IAAI,CAAC;CACrF,CAAC;AACH;;;;;;;;AASA,SAAgB,gBAAgB,KAAa,SAA2B;CACtE,MAAM,OAAO,IAAI,IAAI,OAAO;CAC5B,OAAO,IACJ,MAAM,IAAI,CAAC,CACX,QAAQ,GAAG,WAAW,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC,CACxC,KAAK,IAAI;AACd"}
|