@kurotako/core 0.1.0 → 0.1.1

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/dist/index.js CHANGED
@@ -77,24 +77,24 @@ var DependencyCycleError = class extends TakoError {
77
77
  var OutputCollisionError = class extends TakoError {
78
78
  path;
79
79
  generators;
80
- constructor(path5, generators, hint) {
80
+ constructor(path6, generators, hint) {
81
81
  super(
82
82
  "output_collision",
83
- `generators '${generators[0]}' and '${generators[1]}' both emit '${path5}'${hint ? `. ${hint}` : ""}`
83
+ `generators '${generators[0]}' and '${generators[1]}' both emit '${path6}'${hint ? `. ${hint}` : ""}`
84
84
  );
85
- this.path = path5;
85
+ this.path = path6;
86
86
  this.generators = generators;
87
87
  }
88
88
  };
89
89
  var InvalidOutputPathError = class extends TakoError {
90
90
  path;
91
91
  generator;
92
- constructor(path5, generator) {
92
+ constructor(path6, generator) {
93
93
  super(
94
94
  "invalid_output_path",
95
- `generator '${generator}' emits '${path5}', which escapes the output root`
95
+ `generator '${generator}' emits '${path6}', which escapes the output root`
96
96
  );
97
- this.path = path5;
97
+ this.path = path6;
98
98
  this.generator = generator;
99
99
  }
100
100
  };
@@ -186,12 +186,12 @@ ${guidance}`
186
186
  };
187
187
  var OutputNotGeneratedError = class extends TakoError {
188
188
  path;
189
- constructor(path5) {
189
+ constructor(path6) {
190
190
  super(
191
191
  "output_not_generated",
192
- `refusing to wipe '${path5}': it is non-empty and its package.json lacks the '"//": "Generated by tako\u2026"' marker`
192
+ `refusing to wipe '${path6}': it is non-empty and its package.json lacks the '"//": "Generated by tako\u2026"' marker`
193
193
  );
194
- this.path = path5;
194
+ this.path = path6;
195
195
  }
196
196
  };
197
197
  var PackageInstallError = class extends TakoError {
@@ -258,6 +258,9 @@ function childLogger(base, prefixMeta) {
258
258
  };
259
259
  }
260
260
 
261
+ // src/run.ts
262
+ import { refCycleMembers } from "@kurotako/ir";
263
+
261
264
  // src/collect.ts
262
265
  import path from "path";
263
266
  function normalizePath(rawPath, generator) {
@@ -404,13 +407,8 @@ function findCycle(nodes, dependents) {
404
407
  }
405
408
 
406
409
  // src/merge.ts
407
- import {
408
- assertIR,
409
- IR_VERSION,
410
- IrValidationError as IrModelValidationError,
411
- validateSourceIR
412
- } from "@kurotako/ir";
413
- function mergeSources(entries) {
410
+ import { IR_VERSION, validateIR, validateSourceIR } from "@kurotako/ir";
411
+ function mergeSources(entries, logger) {
414
412
  const sources = {};
415
413
  for (const { namespace, sourceIR } of entries) {
416
414
  if (sourceIR.namespace !== namespace) {
@@ -426,26 +424,27 @@ function mergeSources(entries) {
426
424
  sources[namespace] = validation.value;
427
425
  }
428
426
  const ir = { irVersion: IR_VERSION, sources };
429
- try {
430
- assertIR(ir);
431
- } catch (error) {
432
- if (error instanceof IrModelValidationError) {
433
- throw new IrValidationError(error.issues);
434
- }
435
- throw error;
427
+ const result = validateIR(ir);
428
+ if (!result.ok) {
429
+ throw new IrValidationError(result.issues);
430
+ }
431
+ for (const note of result.info ?? []) {
432
+ logger?.warn(
433
+ `IR ${note.code} at ${note.path === "" ? "<root>" : note.path}: ${note.message}`
434
+ );
436
435
  }
437
- return ir;
436
+ return result.value;
438
437
  }
439
438
 
440
439
  // src/writer/banner.ts
441
440
  var BANNER = "// Generated by tako. Do not edit.\n";
442
441
  var GITATTRIBUTES = "* linguist-generated=true\n";
443
442
  var COMMENTABLE_BASENAMES = /* @__PURE__ */ new Set(["tsconfig.json"]);
444
- function takesBanner(path5) {
445
- if (path5.endsWith(".ts") || path5.endsWith(".tsx")) {
443
+ function takesBanner(path6) {
444
+ if (path6.endsWith(".ts") || path6.endsWith(".tsx")) {
446
445
  return true;
447
446
  }
448
- const basename = path5.slice(path5.lastIndexOf("/") + 1);
447
+ const basename = path6.slice(path6.lastIndexOf("/") + 1);
449
448
  return COMMENTABLE_BASENAMES.has(basename);
450
449
  }
451
450
  function applyBanner(files) {
@@ -457,6 +456,10 @@ function applyBanner(files) {
457
456
  });
458
457
  }
459
458
 
459
+ // src/writer/barrel.ts
460
+ import path2 from "path";
461
+ import * as ts from "typescript";
462
+
460
463
  // src/writer/tree.ts
461
464
  function contributingGenerators(files) {
462
465
  const acc = /* @__PURE__ */ new Map();
@@ -484,65 +487,81 @@ function contributingGenerators(files) {
484
487
  }
485
488
 
486
489
  // src/writer/barrel.ts
487
- function synthesizeRootBarrels(files, artifactsByGenerator, logger) {
490
+ function synthesizeRootBarrels(files, _artifactsByGenerator, logger) {
488
491
  const contributors = contributingGenerators(files);
489
492
  const barrels = [];
490
493
  for (const namespace of [...contributors.keys()].sort()) {
491
494
  const generators = contributors.get(namespace) ?? [];
492
- if (artifactsByGenerator && logger) {
493
- warnAmbiguousReExports(
494
- namespace,
495
- generators,
496
- artifactsByGenerator,
497
- logger
495
+ const collisions = exportedNameCollisions(namespace, generators, files);
496
+ const content = [
497
+ ...generators.map((name) => `export * from './${name}';`),
498
+ ...[...collisions.entries()].map(
499
+ ([identifier, owners]) => `export { ${identifier} } from './${owners[0]}';`
500
+ ),
501
+ ""
502
+ ].join("\n");
503
+ for (const [identifier, owners] of collisions) {
504
+ logger?.warn(
505
+ `namespace '${namespace}': identifier '${identifier}' is re-exported by generators [${owners.join(
506
+ ", "
507
+ )}]; '${namespace}/index.ts' explicitly re-exports it from '${owners[0]}'. Import a generator subpath to select another declaration.`,
508
+ { namespace, identifier, generators: owners }
498
509
  );
499
510
  }
500
- const content = generators.map((name) => `export * from './${name}';
501
- `).join("");
502
511
  barrels.push({ path: `${namespace}/index.ts`, content });
503
512
  }
504
513
  return barrels;
505
514
  }
506
- function warnAmbiguousReExports(namespace, generators, artifactsByGenerator, logger) {
515
+ function exportedNameCollisions(namespace, generators, files) {
516
+ const sourceFiles = new Map(
517
+ files.filter((file) => file.path.startsWith(`${namespace}/`)).map((file) => [toVirtualPath(file.path), file.content])
518
+ );
519
+ const rootNames = generators.map((generator) => toVirtualPath(`${namespace}/${generator}/index.ts`)).filter((fileName) => sourceFiles.has(fileName));
520
+ if (rootNames.length < 2) {
521
+ return /* @__PURE__ */ new Map();
522
+ }
523
+ const options = {
524
+ module: ts.ModuleKind.NodeNext,
525
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
526
+ noLib: true
527
+ };
528
+ const host = ts.createCompilerHost(options, true);
529
+ host.fileExists = (fileName) => sourceFiles.has(fileName);
530
+ host.readFile = (fileName) => sourceFiles.get(fileName);
531
+ host.getSourceFile = (fileName, languageVersion) => {
532
+ const text = sourceFiles.get(fileName);
533
+ return text === void 0 ? void 0 : ts.createSourceFile(fileName, text, languageVersion, true);
534
+ };
535
+ host.directoryExists = (directoryName) => [...sourceFiles.keys()].some(
536
+ (fileName) => fileName.startsWith(`${directoryName}/`)
537
+ );
538
+ const program = ts.createProgram({ rootNames, options, host });
539
+ const checker = program.getTypeChecker();
507
540
  const owners = /* @__PURE__ */ new Map();
508
- for (const name of generators) {
509
- const artifact = artifactsByGenerator[name];
510
- if (!artifact) {
511
- continue;
512
- }
513
- const identifiers = /* @__PURE__ */ new Set();
514
- for (const [key, entity] of Object.entries(artifact.entities)) {
515
- const dot = key.indexOf(".");
516
- const entityNamespace = dot === -1 ? key : key.slice(0, dot);
517
- if (entityNamespace !== namespace) {
518
- continue;
519
- }
520
- for (const identifier of Object.values(entity.symbols)) {
521
- identifiers.add(identifier);
522
- }
523
- }
524
- for (const identifier of identifiers) {
525
- const list = owners.get(identifier) ?? [];
526
- list.push(name);
527
- owners.set(identifier, list);
528
- }
529
- }
530
- for (const [identifier, list] of owners) {
531
- if (list.length > 1) {
532
- const sorted = [...list].sort();
533
- logger.warn(
534
- `namespace '${namespace}': identifier '${identifier}' is re-exported by generators [${sorted.join(
535
- ", "
536
- )}]; the ambiguous star re-export from '${namespace}/index.ts' will be dropped. Import it from a generator subpath instead.`,
537
- { namespace, identifier, generators: sorted }
538
- );
541
+ for (const generator of generators) {
542
+ const source = program.getSourceFile(
543
+ toVirtualPath(`${namespace}/${generator}/index.ts`)
544
+ );
545
+ if (!source) continue;
546
+ const module = checker.getSymbolAtLocation(source);
547
+ if (!module) continue;
548
+ for (const exported of checker.getExportsOfModule(module)) {
549
+ const list = owners.get(exported.name) ?? [];
550
+ list.push(generator);
551
+ owners.set(exported.name, list);
539
552
  }
540
553
  }
554
+ return new Map(
555
+ [...owners].filter(([, owners2]) => owners2.length > 1).sort(([a], [b]) => a.localeCompare(b))
556
+ );
557
+ }
558
+ function toVirtualPath(fileName) {
559
+ return path2.posix.join("/", fileName);
541
560
  }
542
561
 
543
562
  // src/writer/directory.ts
544
563
  import fs from "fs/promises";
545
- import path2 from "path";
564
+ import path3 from "path";
546
565
  var GITATTRIBUTES2 = "* linguist-generated=true\n";
547
566
  function sortByPath(entries) {
548
567
  return [...entries].sort(
@@ -557,24 +576,24 @@ var directoryWriter = {
557
576
  "mode 'dir' requires 'output.dir'"
558
577
  );
559
578
  }
560
- const dir = path2.resolve(output.dir);
579
+ const dir = path3.resolve(output.dir);
561
580
  const planned = files.map((file) => ({
562
- path: path2.join(dir, file.path),
581
+ path: path3.join(dir, file.path),
563
582
  content: file.content
564
583
  }));
565
584
  planned.push({
566
- path: path2.join(dir, ".gitattributes"),
585
+ path: path3.join(dir, ".gitattributes"),
567
586
  content: GITATTRIBUTES2
568
587
  });
569
588
  return sortByPath(planned);
570
589
  },
571
590
  async write(input) {
572
591
  const planned = await this.plan(input);
573
- const dir = path2.resolve(input.output.dir);
592
+ const dir = path3.resolve(input.output.dir);
574
593
  await fs.rm(dir, { recursive: true, force: true });
575
594
  await fs.mkdir(dir, { recursive: true });
576
595
  for (const file of planned) {
577
- await fs.mkdir(path2.dirname(file.path), { recursive: true });
596
+ await fs.mkdir(path3.dirname(file.path), { recursive: true });
578
597
  await fs.writeFile(file.path, file.content, "utf8");
579
598
  }
580
599
  return planned.map((file) => file.path);
@@ -585,7 +604,7 @@ var directoryWriter = {
585
604
  import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
586
605
  import fs2 from "fs/promises";
587
606
  import { createRequire } from "module";
588
- import path4 from "path";
607
+ import path5 from "path";
589
608
 
590
609
  // src/writer/peers.ts
591
610
  function collectPeerDependencies(artifactsByGenerator, files) {
@@ -629,7 +648,7 @@ function collectPeerDependencies(artifactsByGenerator, files) {
629
648
  // src/writer/pm.ts
630
649
  import { execFile } from "child_process";
631
650
  import { existsSync, readFileSync } from "fs";
632
- import path3 from "path";
651
+ import path4 from "path";
633
652
  import { promisify } from "util";
634
653
  var execFileAsync = promisify(execFile);
635
654
  var PACKAGE_MANAGERS = [
@@ -649,10 +668,10 @@ function isPackageManager(value) {
649
668
  return PACKAGE_MANAGERS.includes(value);
650
669
  }
651
670
  function* ancestors(startDir) {
652
- let dir = path3.resolve(startDir);
671
+ let dir = path4.resolve(startDir);
653
672
  while (true) {
654
673
  yield dir;
655
- const parent = path3.dirname(dir);
674
+ const parent = path4.dirname(dir);
656
675
  if (parent === dir) {
657
676
  return;
658
677
  }
@@ -665,16 +684,16 @@ function resolvePackageManager(opts) {
665
684
  }
666
685
  for (const dir of ancestors(opts.startDir)) {
667
686
  for (const [file, pm] of LOCKFILES) {
668
- if (existsSync(path3.join(dir, file))) {
687
+ if (existsSync(path4.join(dir, file))) {
669
688
  return pm;
670
689
  }
671
690
  }
672
- if (existsSync(path3.join(dir, ".git"))) {
691
+ if (existsSync(path4.join(dir, ".git"))) {
673
692
  break;
674
693
  }
675
694
  }
676
695
  for (const dir of ancestors(opts.startDir)) {
677
- const pkgPath = path3.join(dir, "package.json");
696
+ const pkgPath = path4.join(dir, "package.json");
678
697
  if (existsSync(pkgPath)) {
679
698
  try {
680
699
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
@@ -687,7 +706,7 @@ function resolvePackageManager(opts) {
687
706
  } catch {
688
707
  }
689
708
  }
690
- if (existsSync(path3.join(dir, ".git"))) {
709
+ if (existsSync(path4.join(dir, ".git"))) {
691
710
  break;
692
711
  }
693
712
  }
@@ -720,7 +739,7 @@ function computePackageLayout({
720
739
  "mode 'package' requires 'output.scope'"
721
740
  );
722
741
  }
723
- const packagesDir = path4.resolve(output.packagesDir);
742
+ const packagesDir = path5.resolve(output.packagesDir);
724
743
  const scope = output.scope;
725
744
  const scopeSlug = scope.replace(/^@/, "");
726
745
  const peersByNamespace = collectPeerDependencies(artifacts ?? {}, files);
@@ -743,17 +762,17 @@ function computePackageLayout({
743
762
  const sources = [...byNamespace.get(namespace) ?? []].sort(
744
763
  (a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0
745
764
  );
746
- const pkgDir = path4.join(packagesDir, `${scopeSlug}-${namespace}`);
765
+ const pkgDir = path5.join(packagesDir, `${scopeSlug}-${namespace}`);
747
766
  const entries = [];
748
767
  for (const src of sources) {
749
768
  planned.push({
750
- path: path4.join(pkgDir, "src", src.path),
769
+ path: path5.join(pkgDir, "src", src.path),
751
770
  content: src.content
752
771
  });
753
772
  entries.push(`src/${src.path}`);
754
773
  }
755
774
  planned.push({
756
- path: path4.join(pkgDir, "package.json"),
775
+ path: path5.join(pkgDir, "package.json"),
757
776
  content: buildPackageJson(
758
777
  scope,
759
778
  namespace,
@@ -761,22 +780,22 @@ function computePackageLayout({
761
780
  )
762
781
  });
763
782
  planned.push({
764
- path: path4.join(pkgDir, "tsconfig.json"),
783
+ path: path5.join(pkgDir, "tsconfig.json"),
765
784
  content: buildTsconfig(namespace)
766
785
  });
767
786
  const sortedEntries = entries.sort();
768
787
  entriesByNamespace.set(namespace, sortedEntries);
769
788
  planned.push({
770
- path: path4.join(pkgDir, "tsup.config.ts"),
789
+ path: path5.join(pkgDir, "tsup.config.ts"),
771
790
  content: buildTsupConfig(sortedEntries)
772
791
  });
773
792
  planned.push({
774
- path: path4.join(pkgDir, ".gitattributes"),
793
+ path: path5.join(pkgDir, ".gitattributes"),
775
794
  content: GITATTRIBUTES
776
795
  });
777
796
  }
778
797
  planned.push({
779
- path: path4.join(packagesDir, ".gitattributes"),
798
+ path: path5.join(packagesDir, ".gitattributes"),
780
799
  content: `${scopeSlug}-*/** linguist-generated=true
781
800
  `
782
801
  });
@@ -792,11 +811,11 @@ var packageWriter = {
792
811
  assertWorkspaceBaseFiles(packagesDir);
793
812
  await fs2.mkdir(packagesDir, { recursive: true });
794
813
  for (const namespace of namespaces) {
795
- await guardAndReset(path4.join(packagesDir, `${scopeSlug}-${namespace}`));
814
+ await guardAndReset(path5.join(packagesDir, `${scopeSlug}-${namespace}`));
796
815
  }
797
816
  const written = [];
798
817
  for (const file of planned) {
799
- await fs2.mkdir(path4.dirname(file.path), { recursive: true });
818
+ await fs2.mkdir(path5.dirname(file.path), { recursive: true });
800
819
  await fs2.writeFile(file.path, file.content, "utf8");
801
820
  written.push(file.path);
802
821
  }
@@ -818,13 +837,13 @@ var packageWriter = {
818
837
  };
819
838
  var TSUP_CONFIG_BASE_EXTENSIONS = [".ts", ".js", ".mjs", ".cjs"];
820
839
  function assertWorkspaceBaseFiles(packagesDir) {
821
- const workspaceRoot = path4.dirname(packagesDir);
840
+ const workspaceRoot = path5.dirname(packagesDir);
822
841
  const missing = [];
823
- if (!existsSync2(path4.join(workspaceRoot, "tsconfig.base.json"))) {
842
+ if (!existsSync2(path5.join(workspaceRoot, "tsconfig.base.json"))) {
824
843
  missing.push("tsconfig.base.json");
825
844
  }
826
845
  const hasTsupBase = TSUP_CONFIG_BASE_EXTENSIONS.some(
827
- (ext) => existsSync2(path4.join(workspaceRoot, `tsup.config.base${ext}`))
846
+ (ext) => existsSync2(path5.join(workspaceRoot, `tsup.config.base${ext}`))
828
847
  );
829
848
  if (!hasTsupBase) {
830
849
  missing.push("tsup.config.base.{ts,js,mjs,cjs}");
@@ -838,7 +857,7 @@ function assertWorkspaceBaseFiles(packagesDir) {
838
857
  }
839
858
  function isResolvableFrom(specifier, from) {
840
859
  try {
841
- createRequire(path4.join(from, "noop.js")).resolve(specifier);
860
+ createRequire(path5.join(from, "noop.js")).resolve(specifier);
842
861
  return true;
843
862
  } catch {
844
863
  return false;
@@ -860,7 +879,7 @@ async function guardAndReset(pkgDir) {
860
879
  function hasGeneratedMarker(pkgDir) {
861
880
  try {
862
881
  const pkg = JSON.parse(
863
- readFileSync2(path4.join(pkgDir, "package.json"), "utf8")
882
+ readFileSync2(path5.join(pkgDir, "package.json"), "utf8")
864
883
  );
865
884
  return typeof pkg["//"] === "string" && pkg["//"].startsWith("Generated by tako");
866
885
  } catch {
@@ -872,7 +891,7 @@ async function buildPackages(packagesDir, scopeSlug, namespaces, entriesByNamesp
872
891
  const originalCwd = process.cwd();
873
892
  try {
874
893
  for (const namespace of namespaces) {
875
- const pkgDir = path4.join(packagesDir, `${scopeSlug}-${namespace}`);
894
+ const pkgDir = path5.join(packagesDir, `${scopeSlug}-${namespace}`);
876
895
  process.chdir(pkgDir);
877
896
  try {
878
897
  await build({
@@ -892,13 +911,13 @@ async function buildPackages(packagesDir, scopeSlug, namespaces, entriesByNamesp
892
911
  entry: Object.fromEntries(
893
912
  (entriesByNamespace.get(namespace) ?? []).map((entry) => [
894
913
  entry.replace(/^src\//, "").replace(/\.ts$/, ""),
895
- path4.join(pkgDir, entry)
914
+ path5.join(pkgDir, entry)
896
915
  ])
897
916
  ),
898
- tsconfig: path4.join(pkgDir, "tsconfig.json"),
917
+ tsconfig: path5.join(pkgDir, "tsconfig.json"),
899
918
  format: ["esm", "cjs"],
900
919
  dts: { compilerOptions: { composite: false, incremental: false } },
901
- outDir: path4.join(pkgDir, "dist"),
920
+ outDir: path5.join(pkgDir, "dist"),
902
921
  silent: true
903
922
  });
904
923
  } catch (cause) {
@@ -911,10 +930,10 @@ async function buildPackages(packagesDir, scopeSlug, namespaces, entriesByNamesp
911
930
  }
912
931
  function findWorkspaceRoot(startDir) {
913
932
  for (const dir of ancestors(startDir)) {
914
- if (existsSync2(path4.join(dir, "pnpm-workspace.yaml"))) {
933
+ if (existsSync2(path5.join(dir, "pnpm-workspace.yaml"))) {
915
934
  return dir;
916
935
  }
917
- const pkgPath = path4.join(dir, "package.json");
936
+ const pkgPath = path5.join(dir, "package.json");
918
937
  if (existsSync2(pkgPath)) {
919
938
  try {
920
939
  const pkg = JSON.parse(readFileSync2(pkgPath, "utf8"));
@@ -1028,7 +1047,13 @@ async function run(config, opts) {
1028
1047
  }
1029
1048
  }
1030
1049
  checkSignal();
1031
- const ir = mergeSources(entries);
1050
+ const ir = mergeSources(entries, logger);
1051
+ const cycles = /* @__PURE__ */ new Set();
1052
+ for (const [namespace, source] of Object.entries(ir.sources)) {
1053
+ for (const member of refCycleMembers(source)) {
1054
+ cycles.add(`${namespace}.${member}`);
1055
+ }
1056
+ }
1032
1057
  checkSignal();
1033
1058
  const order = generatorOrder(config.generators);
1034
1059
  const artifacts = {};
@@ -1056,6 +1081,7 @@ async function run(config, opts) {
1056
1081
  const out = await generator.generate({
1057
1082
  ir: view,
1058
1083
  dependencies,
1084
+ cycles,
1059
1085
  logger: childLogger(logger, { generator: name })
1060
1086
  });
1061
1087
  artifacts[name] = out.artifact;