@panneau/codemods 0.0.0-stage → 5.0.0-alpha.6

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.
Files changed (50) hide show
  1. package/README.md +28 -2
  2. package/bin/panneau-codemods.js +5 -0
  3. package/dist/cli.d.ts +7 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +62 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/context.d.ts +80 -0
  8. package/dist/context.d.ts.map +1 -0
  9. package/dist/context.js +117 -0
  10. package/dist/context.js.map +1 -0
  11. package/dist/index.d.ts +5 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +4 -0
  14. package/dist/mappings.d.ts +49 -0
  15. package/dist/mappings.d.ts.map +1 -0
  16. package/dist/mappings.js +574 -0
  17. package/dist/mappings.js.map +1 -0
  18. package/dist/names.d.ts +5 -0
  19. package/dist/names.d.ts.map +1 -0
  20. package/dist/names.js +13 -0
  21. package/dist/names.js.map +1 -0
  22. package/dist/run.d.ts +23 -0
  23. package/dist/run.d.ts.map +1 -0
  24. package/dist/run.js +65 -0
  25. package/dist/run.js.map +1 -0
  26. package/dist/transform.d.ts +28 -0
  27. package/dist/transform.d.ts.map +1 -0
  28. package/dist/transform.js +192 -0
  29. package/dist/transform.js.map +1 -0
  30. package/dist/transforms/bootstrap.d.ts +6 -0
  31. package/dist/transforms/bootstrap.d.ts.map +1 -0
  32. package/dist/transforms/bootstrap.js +65 -0
  33. package/dist/transforms/bootstrap.js.map +1 -0
  34. package/dist/transforms/entry.d.ts +15 -0
  35. package/dist/transforms/entry.d.ts.map +1 -0
  36. package/dist/transforms/entry.js +239 -0
  37. package/dist/transforms/entry.js.map +1 -0
  38. package/dist/transforms/fieldEntries.d.ts +20 -0
  39. package/dist/transforms/fieldEntries.d.ts.map +1 -0
  40. package/dist/transforms/fieldEntries.js +238 -0
  41. package/dist/transforms/fieldEntries.js.map +1 -0
  42. package/dist/transforms/fields.d.ts +3 -0
  43. package/dist/transforms/fields.d.ts.map +1 -0
  44. package/dist/transforms/fields.js +241 -0
  45. package/dist/transforms/fields.js.map +1 -0
  46. package/dist/transforms/imports.d.ts +3 -0
  47. package/dist/transforms/imports.d.ts.map +1 -0
  48. package/dist/transforms/imports.js +60 -0
  49. package/dist/transforms/imports.js.map +1 -0
  50. package/package.json +44 -5
package/dist/run.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { Finding } from './context';
2
+ export interface RunOptions {
3
+ /** Files and folders to rewrite. Folders are read recursively, without `node_modules`. */
4
+ paths: readonly string[];
5
+ /** Reports what would change, and writes nothing. */
6
+ dry?: boolean;
7
+ /** The folder that paths and findings are relative to. Defaults to the current one. */
8
+ cwd?: string;
9
+ }
10
+ export interface RunResult {
11
+ /** Files read, relative to `cwd`. */
12
+ files: string[];
13
+ /** Files rewritten (or that would be, with `dry`), relative to `cwd`. */
14
+ changed: string[];
15
+ findings: Finding[];
16
+ }
17
+ /** Runs the codemods from Panneau 4 to Panneau 5 on files and folders. */
18
+ export declare function runCodemods({ paths, dry, cwd, }: RunOptions): Promise<RunResult>;
19
+ /** A run as text: the findings by file, then a summary. */
20
+ export declare function formatReport(result: RunResult, { dry }?: {
21
+ dry?: boolean;
22
+ }): string;
23
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2BzC,MAAM,WAAW,UAAU;IACvB,0FAA0F;IAC1F,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,qDAAqD;IACrD,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,uFAAuF;IACvF,GAAG,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACtB,qCAAqC;IACrC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,yEAAyE;IACzE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACvB;AAED,0EAA0E;AAC1E,wBAAsB,WAAW,CAAC,EAC9B,KAAK,EACL,GAAW,EACX,GAAmB,GACtB,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAqBjC;AAED,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,GAAW,EAAE,GAAE;IAAE,GAAG,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CA0B/F"}
package/dist/run.js ADDED
@@ -0,0 +1,65 @@
1
+ import { EXTENSIONS, transformSources } from "./transform.js";
2
+ import { readFile, readdir, stat, writeFile } from "node:fs/promises";
3
+ import { extname, join, relative, resolve } from "node:path";
4
+ //#region src/run.ts
5
+ /** Folders the codemods never read. */
6
+ var IGNORED = /* @__PURE__ */ new Set([
7
+ "node_modules",
8
+ "vendor",
9
+ "dist",
10
+ "build",
11
+ "public",
12
+ ".git"
13
+ ]);
14
+ async function collectFiles(path, files) {
15
+ if ((await stat(path)).isFile()) {
16
+ files.push(path);
17
+ return;
18
+ }
19
+ const entries = await readdir(path, { withFileTypes: true });
20
+ await Promise.all(entries.map(async (entry) => {
21
+ const child = join(path, entry.name);
22
+ if (entry.isDirectory()) {
23
+ if (!IGNORED.has(entry.name) && !entry.name.startsWith(".")) await collectFiles(child, files);
24
+ } else if (EXTENSIONS.includes(extname(entry.name)) && !entry.name.endsWith(".d.ts")) files.push(child);
25
+ }));
26
+ }
27
+ /** Runs the codemods from Panneau 4 to Panneau 5 on files and folders. */
28
+ async function runCodemods({ paths, dry = false, cwd = process.cwd() }) {
29
+ const files = [];
30
+ for (const path of paths) await collectFiles(resolve(cwd, path), files);
31
+ const unique = [...new Set(files)].sort();
32
+ const sources = /* @__PURE__ */ new Map();
33
+ for (const file of unique) sources.set(file, await readFile(file, "utf8"));
34
+ const { outputs, findings } = transformSources(sources, cwd);
35
+ if (!dry) for (const [file, output] of outputs) await writeFile(file, output);
36
+ return {
37
+ files: unique.map((file) => relative(cwd, file)),
38
+ changed: [...outputs.keys()].map((file) => relative(cwd, file)),
39
+ findings
40
+ };
41
+ }
42
+ /** A run as text: the findings by file, then a summary. */
43
+ function formatReport(result, { dry = false } = {}) {
44
+ const lines = [];
45
+ const byFile = /* @__PURE__ */ new Map();
46
+ result.findings.forEach((finding) => {
47
+ byFile.set(finding.file, [...byFile.get(finding.file) ?? [], finding]);
48
+ });
49
+ [...byFile.keys()].sort().forEach((file) => {
50
+ lines.push(file);
51
+ (byFile.get(file) ?? []).sort((a, b) => (a.line ?? 0) - (b.line ?? 0)).forEach((finding) => {
52
+ const line = finding.line !== null ? String(finding.line) : "-";
53
+ lines.push(` ${line.padStart(4)} ${finding.rule.padEnd(15)} ${finding.message}`);
54
+ });
55
+ lines.push("");
56
+ });
57
+ const changed = result.changed.length;
58
+ lines.push(`${result.files.length} files read, ${changed} ${dry ? "would change" : "changed"}, ${result.findings.length} findings.`);
59
+ if (!dry && changed > 0) lines.push("Format the changed files with Prettier: the codemods print what they add their own way. Each finding to fix by hand also has a `// TODO(panneau-5): …` comment in its file. Run the codemods again with --dry to list what is left.");
60
+ return lines.join("\n");
61
+ }
62
+ //#endregion
63
+ export { formatReport, runCodemods };
64
+
65
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","names":[],"sources":["../src/run.ts"],"sourcesContent":["import { readFile, readdir, stat, writeFile } from 'node:fs/promises';\nimport { extname, join, relative, resolve } from 'node:path';\n\nimport type { Finding } from './context';\nimport { EXTENSIONS, transformSources } from './transform';\n\n/** Folders the codemods never read. */\nconst IGNORED = new Set(['node_modules', 'vendor', 'dist', 'build', 'public', '.git']);\n\nasync function collectFiles(path: string, files: string[]): Promise<void> {\n const stats = await stat(path);\n if (stats.isFile()) {\n files.push(path);\n return;\n }\n const entries = await readdir(path, { withFileTypes: true });\n await Promise.all(\n entries.map(async (entry) => {\n const child = join(path, entry.name);\n if (entry.isDirectory()) {\n if (!IGNORED.has(entry.name) && !entry.name.startsWith('.')) {\n await collectFiles(child, files);\n }\n } else if (EXTENSIONS.includes(extname(entry.name)) && !entry.name.endsWith('.d.ts')) {\n files.push(child);\n }\n }),\n );\n}\n\nexport interface RunOptions {\n /** Files and folders to rewrite. Folders are read recursively, without `node_modules`. */\n paths: readonly string[];\n /** Reports what would change, and writes nothing. */\n dry?: boolean;\n /** The folder that paths and findings are relative to. Defaults to the current one. */\n cwd?: string;\n}\n\nexport interface RunResult {\n /** Files read, relative to `cwd`. */\n files: string[];\n /** Files rewritten (or that would be, with `dry`), relative to `cwd`. */\n changed: string[];\n findings: Finding[];\n}\n\n/** Runs the codemods from Panneau 4 to Panneau 5 on files and folders. */\nexport async function runCodemods({\n paths,\n dry = false,\n cwd = process.cwd(),\n}: RunOptions): Promise<RunResult> {\n const files: string[] = [];\n for (const path of paths) {\n await collectFiles(resolve(cwd, path), files);\n }\n const unique = [...new Set(files)].sort();\n const sources = new Map<string, string>();\n for (const file of unique) {\n sources.set(file, await readFile(file, 'utf8'));\n }\n const { outputs, findings } = transformSources(sources, cwd);\n if (!dry) {\n for (const [file, output] of outputs) {\n await writeFile(file, output);\n }\n }\n return {\n files: unique.map((file) => relative(cwd, file)),\n changed: [...outputs.keys()].map((file) => relative(cwd, file)),\n findings,\n };\n}\n\n/** A run as text: the findings by file, then a summary. */\nexport function formatReport(result: RunResult, { dry = false }: { dry?: boolean } = {}): string {\n const lines: string[] = [];\n const byFile = new Map<string, Finding[]>();\n result.findings.forEach((finding) => {\n byFile.set(finding.file, [...(byFile.get(finding.file) ?? []), finding]);\n });\n [...byFile.keys()].sort().forEach((file) => {\n lines.push(file);\n (byFile.get(file) ?? [])\n .sort((a, b) => (a.line ?? 0) - (b.line ?? 0))\n .forEach((finding) => {\n const line = finding.line !== null ? String(finding.line) : '-';\n lines.push(` ${line.padStart(4)} ${finding.rule.padEnd(15)} ${finding.message}`);\n });\n lines.push('');\n });\n const changed = result.changed.length;\n lines.push(\n `${result.files.length} files read, ${changed} ${dry ? 'would change' : 'changed'}, ${result.findings.length} findings.`,\n );\n if (!dry && changed > 0) {\n lines.push(\n 'Format the changed files with Prettier: the codemods print what they add their own way. Each finding to fix by hand also has a `// TODO(panneau-5): …` comment in its file. Run the codemods again with --dry to list what is left.',\n );\n }\n return lines.join('\\n');\n}\n"],"mappings":";;;;;AAOA,IAAM,0BAAU,IAAI,IAAI;CAAC;CAAgB;CAAU;CAAQ;CAAS;CAAU;AAAM,CAAC;AAErF,eAAe,aAAa,MAAc,OAAgC;CAEtE,KAAI,MADgB,KAAK,IAAI,EAAA,CACnB,OAAO,GAAG;EAChB,MAAM,KAAK,IAAI;EACf;CACJ;CACA,MAAM,UAAU,MAAM,QAAQ,MAAM,EAAE,eAAe,KAAK,CAAC;CAC3D,MAAM,QAAQ,IACV,QAAQ,IAAI,OAAO,UAAU;EACzB,MAAM,QAAQ,KAAK,MAAM,MAAM,IAAI;EACnC,IAAI,MAAM,YAAY,GACd;OAAA,CAAC,QAAQ,IAAI,MAAM,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,GAAG,GACtD,MAAM,aAAa,OAAO,KAAK;EAAA,OAEhC,IAAI,WAAW,SAAS,QAAQ,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,SAAS,OAAO,GAC/E,MAAM,KAAK,KAAK;CAExB,CAAC,CACL;AACJ;;AAoBA,eAAsB,YAAY,EAC9B,OACA,MAAM,OACN,MAAM,QAAQ,IAAI,KACa;CAC/B,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,QAAQ,OACf,MAAM,aAAa,QAAQ,KAAK,IAAI,GAAG,KAAK;CAEhD,MAAM,SAAS,CAAC,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK;CACxC,MAAM,0BAAU,IAAI,IAAoB;CACxC,KAAK,MAAM,QAAQ,QACf,QAAQ,IAAI,MAAM,MAAM,SAAS,MAAM,MAAM,CAAC;CAElD,MAAM,EAAE,SAAS,aAAa,iBAAiB,SAAS,GAAG;CAC3D,IAAI,CAAC,KACD,KAAK,MAAM,CAAC,MAAM,WAAW,SACzB,MAAM,UAAU,MAAM,MAAM;CAGpC,OAAO;EACH,OAAO,OAAO,KAAK,SAAS,SAAS,KAAK,IAAI,CAAC;EAC/C,SAAS,CAAC,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,SAAS,KAAK,IAAI,CAAC;EAC9D;CACJ;AACJ;;AAGA,SAAgB,aAAa,QAAmB,EAAE,MAAM,UAA6B,CAAC,GAAW;CAC7F,MAAM,QAAkB,CAAC;CACzB,MAAM,yBAAS,IAAI,IAAuB;CAC1C,OAAO,SAAS,SAAS,YAAY;EACjC,OAAO,IAAI,QAAQ,MAAM,CAAC,GAAI,OAAO,IAAI,QAAQ,IAAI,KAAK,CAAC,GAAI,OAAO,CAAC;CAC3E,CAAC;CACD,CAAC,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,SAAS;EACxC,MAAM,KAAK,IAAI;EACf,CAAC,OAAO,IAAI,IAAI,KAAK,CAAC,EAAA,CACjB,MAAM,GAAG,OAAO,EAAE,QAAQ,MAAM,EAAE,QAAQ,EAAE,CAAC,CAC7C,SAAS,YAAY;GAClB,MAAM,OAAO,QAAQ,SAAS,OAAO,OAAO,QAAQ,IAAI,IAAI;GAC5D,MAAM,KAAK,KAAK,KAAK,SAAS,CAAC,EAAE,IAAI,QAAQ,KAAK,OAAO,EAAE,EAAE,IAAI,QAAQ,SAAS;EACtF,CAAC;EACL,MAAM,KAAK,EAAE;CACjB,CAAC;CACD,MAAM,UAAU,OAAO,QAAQ;CAC/B,MAAM,KACF,GAAG,OAAO,MAAM,OAAO,eAAe,QAAQ,GAAG,MAAM,iBAAiB,UAAU,IAAI,OAAO,SAAS,OAAO,WACjH;CACA,IAAI,CAAC,OAAO,UAAU,GAClB,MAAM,KACF,qOACJ;CAEJ,OAAO,MAAM,KAAK,IAAI;AAC1B"}
@@ -0,0 +1,28 @@
1
+ import { type FileInfo, type Finding, type Project } from './context';
2
+ /** Extensions of the files the codemods read. */
3
+ export declare const EXTENSIONS: readonly string[];
4
+ /** Reads what the codemods need to know about a file before rewriting the others. */
5
+ export declare function analyze(path: string, source: string): FileInfo;
6
+ /** What the transforms know about the files of a run. */
7
+ export declare function createProject(files: ReadonlyMap<string, FileInfo>): Project;
8
+ export interface TransformResult {
9
+ /** The new source, or `null` when the file doesn't change. */
10
+ output: string | null;
11
+ findings: Finding[];
12
+ }
13
+ /**
14
+ * Rewrites one file of a run. `file` is the path shown in findings. Parse errors are findings,
15
+ * and leave the file as it is.
16
+ */
17
+ export declare function transformFile(path: string, source: string, project: Project, file?: string): TransformResult;
18
+ export interface SourcesResult {
19
+ /** New sources of the files that change, by path. */
20
+ outputs: Map<string, string>;
21
+ findings: Finding[];
22
+ }
23
+ /**
24
+ * Rewrites a set of files, given by path. Paths of findings are relative to `cwd`. The files are
25
+ * read together, so that the entry knows the field components and the plugin module.
26
+ */
27
+ export declare function transformSources(sources: ReadonlyMap<string, string>, cwd?: string): SourcesResult;
28
+ //# sourceMappingURL=transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AAGA,OAAO,EACH,KAAK,QAAQ,EACb,KAAK,OAAO,EACZ,KAAK,OAAO,EAKf,MAAM,WAAW,CAAC;AAWnB,iDAAiD;AACjD,eAAO,MAAM,UAAU,EAAE,SAAS,MAAM,EAA2C,CAAC;AAUpF,qFAAqF;AACrF,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,CAiC9D;AAoBD,yDAAyD;AACzD,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,OAAO,CAgC3E;AAED,MAAM,WAAW,eAAe;IAC5B,8DAA8D;IAC9D,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACvB;AAgCD;;;GAGG;AACH,wBAAgB,aAAa,CACzB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,IAAI,SAAO,GACZ,eAAe,CAuDjB;AAED,MAAM,WAAW,aAAa;IAC1B,qDAAqD;IACrD,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,QAAQ,EAAE,OAAO,EAAE,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,GAAG,SAAM,GAAG,aAAa,CAoB/F"}
@@ -0,0 +1,192 @@
1
+ import { TODO_PREFIX, addTodo, getLine } from "./context.js";
2
+ import { reportBootstrap } from "./transforms/bootstrap.js";
3
+ import { readContainer, transformAppEntry, transformContainer } from "./transforms/entry.js";
4
+ import { findDefaultComponent, getEntryName, transformFieldEntries } from "./transforms/fieldEntries.js";
5
+ import { transformFields } from "./transforms/fields.js";
6
+ import { transformImports } from "./transforms/imports.js";
7
+ import { basename, dirname, extname, join, normalize, relative } from "node:path";
8
+ import jscodeshift from "jscodeshift";
9
+ //#region src/transform.ts
10
+ /** Extensions of the files the codemods read. */
11
+ var EXTENSIONS = [
12
+ ".js",
13
+ ".jsx",
14
+ ".mjs",
15
+ ".ts",
16
+ ".tsx"
17
+ ];
18
+ function getParser(path) {
19
+ return jscodeshift.withParser(extname(path) === ".ts" ? "ts" : "tsx");
20
+ }
21
+ function isTypeScript(path) {
22
+ return extname(path) === ".ts" || extname(path) === ".tsx";
23
+ }
24
+ /** Reads what the codemods need to know about a file before rewriting the others. */
25
+ function analyze(path, source) {
26
+ const j = getParser(path);
27
+ const root = j(source);
28
+ const defaultReExports = /* @__PURE__ */ new Map();
29
+ root.find(j.ExportNamedDeclaration).forEach((declaration) => {
30
+ const module = declaration.node.source?.value;
31
+ if (typeof module !== "string") return;
32
+ (declaration.node.specifiers ?? []).forEach((specifier) => {
33
+ const local = specifier.local?.name;
34
+ if (specifier.type === "ExportSpecifier" && local !== void 0) {
35
+ if (local === "default" || local.endsWith("Entry")) defaultReExports.set(String(specifier.exported.name), module);
36
+ }
37
+ });
38
+ });
39
+ const component = findDefaultComponent(j, root);
40
+ return {
41
+ ...readContainer(j, root),
42
+ defaultReExports,
43
+ importsPanneau: root.find(j.ImportDeclaration).filter((declaration) => String(declaration.node.source.value).startsWith("@panneau/")).size() > 0,
44
+ hasDefaultExport: root.find(j.ExportDefaultDeclaration).size() > 0,
45
+ fieldEntry: component !== null ? getEntryName(component.name) : null
46
+ };
47
+ }
48
+ /** Resolves a relative module against the files of the run, as bundlers do. */
49
+ function resolveModule(files, from, specifier) {
50
+ if (!specifier.startsWith(".")) return null;
51
+ const base = normalize(join(dirname(from), specifier));
52
+ return [
53
+ base,
54
+ ...EXTENSIONS.map((extension) => `${base}${extension}`),
55
+ ...EXTENSIONS.map((extension) => join(base, `index${extension}`))
56
+ ].find((candidate) => files.has(candidate)) ?? null;
57
+ }
58
+ /** What the transforms know about the files of a run. */
59
+ function createProject(files) {
60
+ const resolve = (from, specifier) => resolveModule(files, from, specifier);
61
+ const fieldFiles = /* @__PURE__ */ new Set();
62
+ let hasFieldsModule = false;
63
+ files.forEach((info, path) => {
64
+ const module = info.namespaces.get("fields");
65
+ const index = module !== void 0 ? resolve(path, module) : null;
66
+ if (index === null) return;
67
+ hasFieldsModule = true;
68
+ files.get(index)?.defaultReExports.forEach((specifier) => {
69
+ const file = resolve(index, specifier);
70
+ if (file !== null) fieldFiles.add(file);
71
+ });
72
+ });
73
+ return {
74
+ resolve,
75
+ info: (path) => files.get(path) ?? null,
76
+ isField: (path) => hasFieldsModule ? fieldFiles.has(path) : basename(dirname(path)) === "fields" && files.get(path)?.hasDefaultExport === true && files.get(path)?.importsPanneau === true,
77
+ pluginModules: () => [...files.entries()].filter(([, info]) => info.isContainer).map(([path]) => path)
78
+ };
79
+ }
80
+ /**
81
+ * Entries are inserted right after their component: they get a blank line before them, and
82
+ * before the comments over them.
83
+ */
84
+ function spaceEntries(source) {
85
+ const lines = source.split("\n");
86
+ for (let index = lines.length - 1; index > 0; index -= 1) if ((lines[index] ?? "").startsWith("/** The registry entry of ")) {
87
+ let top = index;
88
+ while (top > 0 && (lines[top - 1] ?? "").startsWith(`// TODO(panneau-5):`)) top -= 1;
89
+ if (top > 0 && (lines[top - 1] ?? "").trim() !== "") lines.splice(top, 0, "");
90
+ }
91
+ return lines.join("\n");
92
+ }
93
+ /** `// TODO(panneau-5): …` comments of a source, with their lines. */
94
+ function readTodos(source) {
95
+ return source.split("\n").flatMap((text, index) => {
96
+ const position = text.indexOf(TODO_PREFIX);
97
+ return position === -1 ? [] : [{
98
+ line: index + 1,
99
+ message: text.slice(position + TODO_PREFIX.length).trim()
100
+ }];
101
+ });
102
+ }
103
+ /**
104
+ * Rewrites one file of a run. `file` is the path shown in findings. Parse errors are findings,
105
+ * and leave the file as it is.
106
+ */
107
+ function transformFile(path, source, project, file = path) {
108
+ const findings = [];
109
+ const j = getParser(path);
110
+ let root;
111
+ try {
112
+ root = j(source);
113
+ } catch (error) {
114
+ return {
115
+ output: null,
116
+ findings: [{
117
+ file,
118
+ line: null,
119
+ rule: "parse",
120
+ message: `can't be parsed: ${error instanceof Error ? error.message : String(error)}`
121
+ }]
122
+ };
123
+ }
124
+ const api = {
125
+ j,
126
+ root,
127
+ path,
128
+ typescript: isTypeScript(path),
129
+ project,
130
+ report(node, rule, message, options = {}) {
131
+ findings.push({
132
+ file,
133
+ line: getLine(node),
134
+ rule,
135
+ message
136
+ });
137
+ if (options.todo === true && node !== null) addTodo(j, node, message);
138
+ }
139
+ };
140
+ const plugin = transformContainer(api);
141
+ let output;
142
+ if (plugin !== null) output = plugin;
143
+ else {
144
+ transformAppEntry(api);
145
+ transformFields(api);
146
+ transformFieldEntries(api);
147
+ transformImports(api);
148
+ reportBootstrap(api);
149
+ output = spaceEntries(root.toSource({ quote: "single" }));
150
+ }
151
+ const reported = new Set(findings.map((finding) => finding.message));
152
+ readTodos(output).forEach(({ line, message }) => {
153
+ if (!reported.has(message)) findings.push({
154
+ file,
155
+ line,
156
+ rule: "todo",
157
+ message
158
+ });
159
+ });
160
+ return {
161
+ output: output.replace(/\s+/g, "") !== source.replace(/\s+/g, "") ? output : null,
162
+ findings
163
+ };
164
+ }
165
+ /**
166
+ * Rewrites a set of files, given by path. Paths of findings are relative to `cwd`. The files are
167
+ * read together, so that the entry knows the field components and the plugin module.
168
+ */
169
+ function transformSources(sources, cwd = "/") {
170
+ const infos = /* @__PURE__ */ new Map();
171
+ const findings = [];
172
+ sources.forEach((source, path) => {
173
+ try {
174
+ infos.set(path, analyze(path, source));
175
+ } catch {}
176
+ });
177
+ const project = createProject(infos);
178
+ const outputs = /* @__PURE__ */ new Map();
179
+ sources.forEach((source, path) => {
180
+ const result = transformFile(path, source, project, relative(cwd, path) || path);
181
+ if (result.output !== null) outputs.set(path, result.output);
182
+ findings.push(...result.findings);
183
+ });
184
+ return {
185
+ outputs,
186
+ findings
187
+ };
188
+ }
189
+ //#endregion
190
+ export { EXTENSIONS, analyze, createProject, transformFile, transformSources };
191
+
192
+ //# sourceMappingURL=transform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.js","names":[],"sources":["../src/transform.ts"],"sourcesContent":["import jscodeshift, { type ASTPath, type Collection, type JSCodeshift } from 'jscodeshift';\nimport { basename, dirname, extname, join, normalize, relative } from 'node:path';\n\nimport {\n type FileInfo,\n type Finding,\n type Project,\n TODO_PREFIX,\n type TransformApi,\n addTodo,\n getLine,\n} from './context';\nimport { reportBootstrap } from './transforms/bootstrap';\nimport { readContainer, transformAppEntry, transformContainer } from './transforms/entry';\nimport {\n findDefaultComponent,\n getEntryName,\n transformFieldEntries,\n} from './transforms/fieldEntries';\nimport { transformFields } from './transforms/fields';\nimport { transformImports } from './transforms/imports';\n\n/** Extensions of the files the codemods read. */\nexport const EXTENSIONS: readonly string[] = ['.js', '.jsx', '.mjs', '.ts', '.tsx'];\n\nfunction getParser(path: string): JSCodeshift {\n return jscodeshift.withParser(extname(path) === '.ts' ? 'ts' : 'tsx');\n}\n\nfunction isTypeScript(path: string): boolean {\n return extname(path) === '.ts' || extname(path) === '.tsx';\n}\n\n/** Reads what the codemods need to know about a file before rewriting the others. */\nexport function analyze(path: string, source: string): FileInfo {\n const j = getParser(path);\n const root = j(source);\n const defaultReExports = new Map<string, string>();\n root.find(j.ExportNamedDeclaration).forEach((declaration) => {\n const module = declaration.node.source?.value;\n if (typeof module !== 'string') {\n return;\n }\n (declaration.node.specifiers ?? []).forEach((specifier) => {\n const local = (specifier as { local?: { name: string } | null }).local?.name;\n if (specifier.type === 'ExportSpecifier' && local !== undefined) {\n // A file already rewritten re-exports the entry (`AgencyEntry as Agency`).\n if (local === 'default' || local.endsWith('Entry')) {\n defaultReExports.set(String(specifier.exported.name), module);\n }\n }\n });\n });\n const component = findDefaultComponent(j, root);\n return {\n ...readContainer(j, root),\n defaultReExports,\n importsPanneau:\n root\n .find(j.ImportDeclaration)\n .filter((declaration) =>\n String(declaration.node.source.value).startsWith('@panneau/'),\n )\n .size() > 0,\n hasDefaultExport: root.find(j.ExportDefaultDeclaration).size() > 0,\n fieldEntry: component !== null ? getEntryName(component.name) : null,\n };\n}\n\n/** Resolves a relative module against the files of the run, as bundlers do. */\nfunction resolveModule(\n files: ReadonlyMap<string, FileInfo>,\n from: string,\n specifier: string,\n): string | null {\n if (!specifier.startsWith('.')) {\n return null;\n }\n const base = normalize(join(dirname(from), specifier));\n const candidates = [\n base,\n ...EXTENSIONS.map((extension) => `${base}${extension}`),\n ...EXTENSIONS.map((extension) => join(base, `index${extension}`)),\n ];\n return candidates.find((candidate) => files.has(candidate)) ?? null;\n}\n\n/** What the transforms know about the files of a run. */\nexport function createProject(files: ReadonlyMap<string, FileInfo>): Project {\n const resolve = (from: string, specifier: string) => resolveModule(files, from, specifier);\n const fieldFiles = new Set<string>();\n let hasFieldsModule = false;\n files.forEach((info, path) => {\n const module = info.namespaces.get('fields');\n const index = module !== undefined ? resolve(path, module) : null;\n if (index === null) {\n return;\n }\n hasFieldsModule = true;\n files.get(index)?.defaultReExports.forEach((specifier) => {\n const file = resolve(index, specifier);\n if (file !== null) {\n fieldFiles.add(file);\n }\n });\n });\n return {\n resolve,\n info: (path) => files.get(path) ?? null,\n // Without the container of the app (or the plugin module made from it) in the run, the\n // components of a `fields` folder that import Panneau are fields.\n isField: (path) =>\n hasFieldsModule\n ? fieldFiles.has(path)\n : basename(dirname(path)) === 'fields' &&\n files.get(path)?.hasDefaultExport === true &&\n files.get(path)?.importsPanneau === true,\n pluginModules: () =>\n [...files.entries()].filter(([, info]) => info.isContainer).map(([path]) => path),\n };\n}\n\nexport interface TransformResult {\n /** The new source, or `null` when the file doesn't change. */\n output: string | null;\n findings: Finding[];\n}\n\n/**\n * Entries are inserted right after their component: they get a blank line before them, and\n * before the comments over them.\n */\nfunction spaceEntries(source: string): string {\n const lines = source.split('\\n');\n for (let index = lines.length - 1; index > 0; index -= 1) {\n if ((lines[index] ?? '').startsWith('/** The registry entry of ')) {\n let top = index;\n while (top > 0 && (lines[top - 1] ?? '').startsWith(`// ${TODO_PREFIX}`)) {\n top -= 1;\n }\n if (top > 0 && (lines[top - 1] ?? '').trim() !== '') {\n lines.splice(top, 0, '');\n }\n }\n }\n return lines.join('\\n');\n}\n\n/** `// TODO(panneau-5): …` comments of a source, with their lines. */\nfunction readTodos(source: string): { line: number; message: string }[] {\n return source.split('\\n').flatMap((text, index) => {\n const position = text.indexOf(TODO_PREFIX);\n return position === -1\n ? []\n : [{ line: index + 1, message: text.slice(position + TODO_PREFIX.length).trim() }];\n });\n}\n\n/**\n * Rewrites one file of a run. `file` is the path shown in findings. Parse errors are findings,\n * and leave the file as it is.\n */\nexport function transformFile(\n path: string,\n source: string,\n project: Project,\n file = path,\n): TransformResult {\n const findings: Finding[] = [];\n const j = getParser(path);\n let root: Collection;\n try {\n root = j(source);\n } catch (error) {\n return {\n output: null,\n findings: [\n {\n file,\n line: null,\n rule: 'parse',\n message: `can't be parsed: ${error instanceof Error ? error.message : String(error)}`,\n },\n ],\n };\n }\n const api: TransformApi = {\n j,\n root,\n path,\n typescript: isTypeScript(path),\n project,\n report(node: ASTPath | null, rule: string, message: string, options = {}) {\n findings.push({ file, line: getLine(node), rule, message });\n if (options.todo === true && node !== null) {\n addTodo(j, node, message);\n }\n },\n };\n const plugin = transformContainer(api);\n let output: string;\n if (plugin !== null) {\n output = plugin;\n } else {\n transformAppEntry(api);\n transformFields(api);\n transformFieldEntries(api);\n transformImports(api);\n reportBootstrap(api);\n output = spaceEntries(root.toSource({ quote: 'single' }));\n }\n // Comments left by an earlier run, or written into a plugin module, are findings too.\n const reported = new Set(findings.map((finding) => finding.message));\n readTodos(output).forEach(({ line, message }) => {\n if (!reported.has(message)) {\n findings.push({ file, line, rule: 'todo', message });\n }\n });\n // Recast reprints some untouched expressions once the tree is traversed (a multi-line\n // condition in parentheses). Such a file has no change of its own: it stays as it is.\n const changed = output.replace(/\\s+/g, '') !== source.replace(/\\s+/g, '');\n return { output: changed ? output : null, findings };\n}\n\nexport interface SourcesResult {\n /** New sources of the files that change, by path. */\n outputs: Map<string, string>;\n findings: Finding[];\n}\n\n/**\n * Rewrites a set of files, given by path. Paths of findings are relative to `cwd`. The files are\n * read together, so that the entry knows the field components and the plugin module.\n */\nexport function transformSources(sources: ReadonlyMap<string, string>, cwd = '/'): SourcesResult {\n const infos = new Map<string, FileInfo>();\n const findings: Finding[] = [];\n sources.forEach((source, path) => {\n try {\n infos.set(path, analyze(path, source));\n } catch {\n // `transformFile` reports the parse error.\n }\n });\n const project = createProject(infos);\n const outputs = new Map<string, string>();\n sources.forEach((source, path) => {\n const result = transformFile(path, source, project, relative(cwd, path) || path);\n if (result.output !== null) {\n outputs.set(path, result.output);\n }\n findings.push(...result.findings);\n });\n return { outputs, findings };\n}\n"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { TransformApi } from '../context';
2
+ /** Whether a class is one of Bootstrap 5 (and 4's `ml-*`, `sr-only`), or of Bootstrap Icons. */
3
+ export declare function isBootstrapClass(name: string): boolean;
4
+ /** Reports the Bootstrap classes of each `className` (or `*ClassName`) prop, with a TODO. */
5
+ export declare function reportBootstrap(api: TransformApi): void;
6
+ //# sourceMappingURL=bootstrap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../src/transforms/bootstrap.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAiC/C,gGAAgG;AAChG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtD;AA6BD,6FAA6F;AAC7F,wBAAgB,eAAe,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAyBvD"}
@@ -0,0 +1,65 @@
1
+ //#region src/transforms/bootstrap.ts
2
+ /** Prefixes of Bootstrap classes, which always have a suffix: `mb-3`, `btn-primary`. */
3
+ var PREFIXED = /^(d|m[tbsexylr]?|p[tbsexylr]?|g[xy]?|gap|row-cols|col|offset|order|flex|justify-content|align-(items|self|content)|text|fw|fs|fst|lh|bg|border|rounded|shadow|w|h|mw|mh|vw|vh|position|top|bottom|start|end|float|overflow|opacity|z|ratio|display|btn|card|badge|alert|list-group|nav|navbar|dropdown|modal|table|form|input-group|spinner|progress|pagination|breadcrumb|container|bi)-[a-z0-9-]+$/;
4
+ /** Bootstrap classes without a suffix. */
5
+ var STANDALONE = /* @__PURE__ */ new Set([
6
+ "row",
7
+ "col",
8
+ "container",
9
+ "btn",
10
+ "close",
11
+ "card",
12
+ "badge",
13
+ "alert",
14
+ "nav",
15
+ "navbar",
16
+ "dropdown",
17
+ "modal",
18
+ "table",
19
+ "small",
20
+ "lead",
21
+ "visible",
22
+ "invisible",
23
+ "visually-hidden",
24
+ "sr-only",
25
+ "border",
26
+ "rounded",
27
+ "shadow",
28
+ "bi"
29
+ ]);
30
+ /** Whether a class is one of Bootstrap 5 (and 4's `ml-*`, `sr-only`), or of Bootstrap Icons. */
31
+ function isBootstrapClass(name) {
32
+ return STANDALONE.has(name) || PREFIXED.test(name);
33
+ }
34
+ /** The literal strings of an expression: class names and `classNames({ 'mb-3': … })` keys. */
35
+ function collectStrings(node, strings) {
36
+ if (node === null || typeof node !== "object") return;
37
+ const value = node;
38
+ if ((value.type === "StringLiteral" || value.type === "Literal") && typeof value.value === "string") {
39
+ strings.push(value.value);
40
+ return;
41
+ }
42
+ if (value.type === "TemplateLiteral") (value.quasis ?? []).forEach((quasi) => strings.push(quasi.value.raw));
43
+ Object.entries(node).forEach(([key, child]) => {
44
+ if (key !== "loc" && key !== "comments" && key !== "tokens") {
45
+ if (Array.isArray(child)) child.forEach((item) => collectStrings(item, strings));
46
+ else if (typeof child === "object" && child !== null && key !== "type") collectStrings(child, strings);
47
+ }
48
+ });
49
+ }
50
+ /** Reports the Bootstrap classes of each `className` (or `*ClassName`) prop, with a TODO. */
51
+ function reportBootstrap(api) {
52
+ const { j, root } = api;
53
+ root.find(j.JSXAttribute).forEach((path) => {
54
+ const { name } = path.node;
55
+ if (name.type !== "JSXIdentifier" || !/^className$|ClassName$/.test(name.name)) return;
56
+ const strings = [];
57
+ collectStrings(path.node.value, strings);
58
+ const classes = [...new Set(strings.flatMap((string) => string.split(/\s+/)).filter((value) => value !== "" && isBootstrapClass(value)))];
59
+ if (classes.length > 0) api.report(path, "bootstrap", `Bootstrap ${classes.length > 1 ? "classes" : "class"} ${classes.map((value) => `\`${value}\``).join(", ")} in \`${name.name}\`: Panneau 5 has no Bootstrap (\`@panneau/ui\` primitives, CSS Modules, or the Bootstrap-compatible theme)`, { todo: true });
60
+ });
61
+ }
62
+ //#endregion
63
+ export { isBootstrapClass, reportBootstrap };
64
+
65
+ //# sourceMappingURL=bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.js","names":[],"sources":["../../src/transforms/bootstrap.ts"],"sourcesContent":["// Panneau 5 has no Bootstrap: components style themselves with CSS Modules and design tokens.\n// The Bootstrap classes of custom components are reported, not rewritten: they are replaced with\n// `@panneau/ui` primitives, or styled by the Bootstrap-compatible theme.\nimport type { ASTPath, JSXAttribute } from 'jscodeshift';\n\nimport type { TransformApi } from '../context';\n\n/** Prefixes of Bootstrap classes, which always have a suffix: `mb-3`, `btn-primary`. */\nconst PREFIXED =\n /^(d|m[tbsexylr]?|p[tbsexylr]?|g[xy]?|gap|row-cols|col|offset|order|flex|justify-content|align-(items|self|content)|text|fw|fs|fst|lh|bg|border|rounded|shadow|w|h|mw|mh|vw|vh|position|top|bottom|start|end|float|overflow|opacity|z|ratio|display|btn|card|badge|alert|list-group|nav|navbar|dropdown|modal|table|form|input-group|spinner|progress|pagination|breadcrumb|container|bi)-[a-z0-9-]+$/;\n\n/** Bootstrap classes without a suffix. */\nconst STANDALONE = new Set([\n 'row',\n 'col',\n 'container',\n 'btn',\n 'close',\n 'card',\n 'badge',\n 'alert',\n 'nav',\n 'navbar',\n 'dropdown',\n 'modal',\n 'table',\n 'small',\n 'lead',\n 'visible',\n 'invisible',\n 'visually-hidden',\n 'sr-only',\n 'border',\n 'rounded',\n 'shadow',\n 'bi',\n]);\n\n/** Whether a class is one of Bootstrap 5 (and 4's `ml-*`, `sr-only`), or of Bootstrap Icons. */\nexport function isBootstrapClass(name: string): boolean {\n return STANDALONE.has(name) || PREFIXED.test(name);\n}\n\n/** The literal strings of an expression: class names and `classNames({ 'mb-3': … })` keys. */\nfunction collectStrings(node: unknown, strings: string[]): void {\n if (node === null || typeof node !== 'object') {\n return;\n }\n const value = node as { type?: string; value?: unknown; quasis?: { value: { raw: string } }[] };\n if (\n (value.type === 'StringLiteral' || value.type === 'Literal') &&\n typeof value.value === 'string'\n ) {\n strings.push(value.value);\n return;\n }\n if (value.type === 'TemplateLiteral') {\n (value.quasis ?? []).forEach((quasi) => strings.push(quasi.value.raw));\n }\n Object.entries(node).forEach(([key, child]) => {\n if (key !== 'loc' && key !== 'comments' && key !== 'tokens') {\n if (Array.isArray(child)) {\n child.forEach((item) => collectStrings(item, strings));\n } else if (typeof child === 'object' && child !== null && key !== 'type') {\n collectStrings(child, strings);\n }\n }\n });\n}\n\n/** Reports the Bootstrap classes of each `className` (or `*ClassName`) prop, with a TODO. */\nexport function reportBootstrap(api: TransformApi): void {\n const { j, root } = api;\n root.find(j.JSXAttribute).forEach((path: ASTPath<JSXAttribute>) => {\n const { name } = path.node;\n if (name.type !== 'JSXIdentifier' || !/^className$|ClassName$/.test(name.name)) {\n return;\n }\n const strings: string[] = [];\n collectStrings(path.node.value, strings);\n const classes = [\n ...new Set(\n strings\n .flatMap((string) => string.split(/\\s+/))\n .filter((value) => value !== '' && isBootstrapClass(value)),\n ),\n ];\n if (classes.length > 0) {\n api.report(\n path,\n 'bootstrap',\n `Bootstrap ${classes.length > 1 ? 'classes' : 'class'} ${classes.map((value) => `\\`${value}\\``).join(', ')} in \\`${name.name}\\`: Panneau 5 has no Bootstrap (\\`@panneau/ui\\` primitives, CSS Modules, or the Bootstrap-compatible theme)`,\n { todo: true },\n );\n }\n });\n}\n"],"mappings":";;AAQA,IAAM,WACF;;AAGJ,IAAM,6BAAa,IAAI,IAAI;CACvB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACJ,CAAC;;AAGD,SAAgB,iBAAiB,MAAuB;CACpD,OAAO,WAAW,IAAI,IAAI,KAAK,SAAS,KAAK,IAAI;AACrD;;AAGA,SAAS,eAAe,MAAe,SAAyB;CAC5D,IAAI,SAAS,QAAQ,OAAO,SAAS,UACjC;CAEJ,MAAM,QAAQ;CACd,KACK,MAAM,SAAS,mBAAmB,MAAM,SAAS,cAClD,OAAO,MAAM,UAAU,UACzB;EACE,QAAQ,KAAK,MAAM,KAAK;EACxB;CACJ;CACA,IAAI,MAAM,SAAS,mBACf,CAAC,MAAM,UAAU,CAAC,EAAA,CAAG,SAAS,UAAU,QAAQ,KAAK,MAAM,MAAM,GAAG,CAAC;CAEzE,OAAO,QAAQ,IAAI,CAAC,CAAC,SAAS,CAAC,KAAK,WAAW;EAC3C,IAAI,QAAQ,SAAS,QAAQ,cAAc,QAAQ,UAAU;GACzD,IAAI,MAAM,QAAQ,KAAK,GACnB,MAAM,SAAS,SAAS,eAAe,MAAM,OAAO,CAAC;QAClD,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,QAAQ,QAC9D,eAAe,OAAO,OAAO;EAErC;CACJ,CAAC;AACL;;AAGA,SAAgB,gBAAgB,KAAyB;CACrD,MAAM,EAAE,GAAG,SAAS;CACpB,KAAK,KAAK,EAAE,YAAY,CAAC,CAAC,SAAS,SAAgC;EAC/D,MAAM,EAAE,SAAS,KAAK;EACtB,IAAI,KAAK,SAAS,mBAAmB,CAAC,yBAAyB,KAAK,KAAK,IAAI,GACzE;EAEJ,MAAM,UAAoB,CAAC;EAC3B,eAAe,KAAK,KAAK,OAAO,OAAO;EACvC,MAAM,UAAU,CACZ,GAAG,IAAI,IACH,QACK,SAAS,WAAW,OAAO,MAAM,KAAK,CAAC,CAAC,CACxC,QAAQ,UAAU,UAAU,MAAM,iBAAiB,KAAK,CAAC,CAClE,CACJ;EACA,IAAI,QAAQ,SAAS,GACjB,IAAI,OACA,MACA,aACA,aAAa,QAAQ,SAAS,IAAI,YAAY,QAAQ,GAAG,QAAQ,KAAK,UAAU,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,IAAI,EAAE,QAAQ,KAAK,KAAK,8GAC7H,EAAE,MAAM,KAAK,CACjB;CAER,CAAC;AACL"}
@@ -0,0 +1,15 @@
1
+ import type { Collection, JSCodeshift } from 'jscodeshift';
2
+ import { type FileInfo, type TransformApi } from '../context';
3
+ /** What `analyze` reads about the entry of a file. */
4
+ export declare function readContainer(j: JSCodeshift, root: Collection): Pick<FileInfo, 'isContainer' | 'namespaces'>;
5
+ /**
6
+ * Rewrites a v4 container as a plugin module. Returns the new source, or `null` when the file
7
+ * isn't a container.
8
+ */
9
+ export declare function transformContainer(api: TransformApi): string | null;
10
+ /**
11
+ * Adds the plugins made from containers to the `plugins` of `createPanneauApp()` in the entry of
12
+ * Panneau 5, when it is one of the files.
13
+ */
14
+ export declare function transformAppEntry(api: TransformApi): void;
15
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../../src/transforms/entry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAER,UAAU,EAEV,WAAW,EAId,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,KAAK,QAAQ,EAAe,KAAK,YAAY,EAAgB,MAAM,YAAY,CAAC;AAkJzF,sDAAsD;AACtD,wBAAgB,aAAa,CACzB,CAAC,EAAE,WAAW,EACd,IAAI,EAAE,UAAU,GACjB,IAAI,CAAC,QAAQ,EAAE,aAAa,GAAG,YAAY,CAAC,CAiB9C;AAyKD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI,CAiBnE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAuEzD"}