@ttsc/strip 0.27.0 → 0.28.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/driver/config.go CHANGED
@@ -77,7 +77,12 @@ func loadStripConfigMapWithReporters(pluginConfig map[string]any, cwd, tsconfigP
77
77
  }
78
78
  configFilePath = resolveStripConfigFilePath(cf, cwd, tsconfigPath)
79
79
  } else {
80
- discovered, err := findStripConfigFile(cwd, tsconfigPath)
80
+ discovered, probed, err := findStripConfigFile(cwd, tsconfigPath)
81
+ // Report the rejected candidates before the error check and before the
82
+ // defaults path below: a search that ended empty examined them just the
83
+ // same, and falling back to the built-in defaults is exactly the state a
84
+ // config appearing later would change.
85
+ driver.ReportRejectedConfigCandidates(probed, hashReporter, realpathReporter)
81
86
  if err != nil {
82
87
  return nil, err
83
88
  }
@@ -106,36 +111,32 @@ func loadStripConfigMapWithReporters(pluginConfig map[string]any, cwd, tsconfigP
106
111
  // tsconfig is set) and returns the first directory that contains exactly one
107
112
  // strip.config.* file. Multiple candidates in the same directory is an error.
108
113
  // Returns "" (no error) when the filesystem root is reached without a match.
109
- func findStripConfigFile(cwd, tsconfigPath string) (string, error) {
110
- dir := stripDiscoveryBaseDir(cwd, tsconfigPath)
111
- for {
112
- matches := make([]string, 0, 1)
113
- for _, name := range stripConfigFilenames {
114
- candidate := filepath.Join(dir, name)
115
- if stat, err := os.Stat(candidate); err == nil && !stat.IsDir() {
116
- matches = append(matches, candidate)
117
- }
118
- }
119
- if len(matches) > 1 {
120
- names := make([]string, 0, len(matches))
121
- for _, m := range matches {
122
- names = append(names, filepath.Base(m))
123
- }
124
- return "", fmt.Errorf(
125
- "@ttsc/strip: multiple strip config files found in %s (%s); "+
126
- "set \"configFile\" explicitly in the tsconfig plugin entry",
127
- dir, strings.Join(names, ", "),
128
- )
129
- }
130
- if len(matches) == 1 {
131
- return matches[0], nil
132
- }
133
- parent := filepath.Dir(dir)
134
- if parent == dir {
135
- return "", nil
114
+ //
115
+ // The second return value is every candidate the walk examined and rejected,
116
+ // each carrying whether it was absent or a directory wearing the name. Those
117
+ // decide the result as much as the file it returned: one created
118
+ // nearer the entry wins the next search, and one created beside the match makes
119
+ // that directory ambiguous. Without them a persistent consumer keeps applying
120
+ // the rules of a config a cold run would no longer choose — or keeps stripping
121
+ // under the built-in defaults after a real config appeared
122
+ // (samchon/ttsc#1271).
123
+ func findStripConfigFile(cwd, tsconfigPath string) (string, []driver.ConfigCandidate, error) {
124
+ discovery := driver.DiscoverConfigFile(stripDiscoveryBaseDir(cwd, tsconfigPath), stripConfigFilenames)
125
+ if len(discovery.Matches) > 1 {
126
+ names := make([]string, 0, len(discovery.Matches))
127
+ for _, match := range discovery.Matches {
128
+ names = append(names, filepath.Base(match))
136
129
  }
137
- dir = parent
130
+ return "", discovery.Probed, fmt.Errorf(
131
+ "@ttsc/strip: multiple strip config files found in %s (%s); "+
132
+ "set \"configFile\" explicitly in the tsconfig plugin entry",
133
+ discovery.Directory, strings.Join(names, ", "),
134
+ )
135
+ }
136
+ if len(discovery.Matches) == 1 {
137
+ return discovery.Matches[0], discovery.Probed, nil
138
138
  }
139
+ return "", discovery.Probed, nil
139
140
  }
140
141
 
141
142
  // stripDiscoveryBaseDir returns the directory from which auto-discovery walks
@@ -305,7 +306,8 @@ func parseStripJSONConfigFile(location string, body []byte) (any, error) {
305
306
  // loadStripScriptConfigFile to evaluate a .js/.cjs/.mjs strip config and
306
307
  // serialize the result to stdout as JSON.
307
308
  const stripScriptLoaderSource = `
308
- const { createRequire, isBuiltin, registerHooks } = require("node:module");
309
+ const nodeModule = require("node:module");
310
+ const { createRequire, isBuiltin, registerHooks } = nodeModule;
309
311
  const crypto = require("node:crypto");
310
312
  const fs = require("node:fs");
311
313
  const path = require("node:path");
@@ -501,6 +503,33 @@ registerHooks({
501
503
  },
502
504
  });
503
505
 
506
+ // The hook above never sees a require() made from inside a CommonJS module the
507
+ // ESM loader evaluated, which on Node 22 is every require the config makes:
508
+ // module.registerHooks observes the import() of that module and nothing within
509
+ // it. A config's own dependencies would then be reported without the candidates
510
+ // that decide them, so a spelling appearing later could change what the config
511
+ // resolves to with nothing in the envelope to notice it (samchon/ttsc#1280).
512
+ // Wrapping the CommonJS resolver records the same two observations the hook
513
+ // does, on the graph the hook cannot reach.
514
+ const nextResolveFilename = nodeModule._resolveFilename;
515
+ nodeModule._resolveFilename = function resolveFilename(request, parent, isMain, options) {
516
+ // _resolveFilename is an internal entry point anything may call, so a
517
+ // non-string request arrives here as readily as a specifier does. Reading it
518
+ // would replace Node's own argument error with a TypeError from this loader.
519
+ if (typeof request !== "string") {
520
+ return nextResolveFilename.call(this, request, parent, isMain, options);
521
+ }
522
+ const parentFile = parent && typeof parent.filename === "string" ? parent.filename : undefined;
523
+ const parentURL = parentFile === undefined ? undefined : pathToFileURL(parentFile).href;
524
+ recordResolutionCandidates(request, parentURL, undefined);
525
+ const resolved = nextResolveFilename.call(this, request, parent, isMain, options);
526
+ if (path.isAbsolute(resolved)) {
527
+ recordResolutionCandidates(request, parentURL, pathToFileURL(resolved).href);
528
+ recordFile(resolved);
529
+ }
530
+ return resolved;
531
+ };
532
+
504
533
  (async () => {
505
534
  const mod = await import(pathToFileURL(process.argv[1]).href);
506
535
  let current = mod;
@@ -609,11 +638,11 @@ func decodeStripConfigLoaderOutput(output []byte) (stripLoadedConfig, error) {
609
638
  // `"./strip.config.ts"`) produced by json.Marshal.
610
639
  func stripTypeScriptLoaderSource(importLiteral string) string {
611
640
  return fmt.Sprintf(`// @ts-nocheck
612
- import { createRequire, isBuiltin, registerHooks } from "node:module";
641
+ import Module, { createRequire, isBuiltin, registerHooks } from "node:module";
613
642
  import crypto from "node:crypto";
614
643
  import fs from "node:fs";
615
644
  import path from "node:path";
616
- import { fileURLToPath } from "node:url";
645
+ import { fileURLToPath, pathToFileURL } from "node:url";
617
646
 
618
647
  const inputs = new Set<string>();
619
648
  const hashes = new Map<string, string | null>();
@@ -820,6 +849,49 @@ registerHooks({
820
849
  },
821
850
  });
822
851
 
852
+ // The hook above never sees a require() made from inside a CommonJS module the
853
+ // ESM loader evaluated, which on Node 22 is every require the config makes:
854
+ // module.registerHooks observes the import() of that module and nothing within
855
+ // it. A config's own dependencies would then be reported without the candidates
856
+ // that decide them, so a spelling appearing later could change what the config
857
+ // resolves to with nothing in the envelope to notice it (samchon/ttsc#1280).
858
+ // Wrapping the CommonJS resolver records the same two observations the hook
859
+ // does, on the graph the hook cannot reach.
860
+ const moduleInternals = Module as unknown as {
861
+ _resolveFilename(
862
+ request: string,
863
+ parent: { filename?: string | null } | null | undefined,
864
+ isMain: boolean,
865
+ options?: unknown,
866
+ ): string;
867
+ };
868
+ const nextResolveFilename = moduleInternals._resolveFilename;
869
+ moduleInternals._resolveFilename = function resolveFilename(
870
+ this: unknown,
871
+ request: string,
872
+ parent: { filename?: string | null } | null | undefined,
873
+ isMain: boolean,
874
+ options?: unknown,
875
+ ): string {
876
+ // _resolveFilename is an internal entry point anything may call, so a
877
+ // non-string request arrives here as readily as a specifier does. Reading it
878
+ // would replace Node's own argument error with a TypeError from this loader.
879
+ if (typeof request !== "string") {
880
+ return nextResolveFilename.call(this, request, parent, isMain, options);
881
+ }
882
+ const parentURL =
883
+ typeof parent?.filename === "string"
884
+ ? pathToFileURL(parent.filename).href
885
+ : undefined;
886
+ recordResolutionCandidates(request, parentURL, undefined);
887
+ const resolved = nextResolveFilename.call(this, request, parent, isMain, options);
888
+ if (path.isAbsolute(resolved)) {
889
+ recordResolutionCandidates(request, parentURL, pathToFileURL(resolved).href);
890
+ recordFile(resolved);
891
+ }
892
+ return resolved;
893
+ };
894
+
823
895
  declare const process: {
824
896
  exitCode?: number;
825
897
  stdout: { write(value: string, callback?: () => void): void };
package/driver/strip.go CHANGED
@@ -27,6 +27,11 @@ func (plugin) ApplyProgram(prog *driver.Program, ctx driver.PluginContext) error
27
27
  if err != nil {
28
28
  return err
29
29
  }
30
+ // The rewrite reads the statements in front of it and the configured
31
+ // patterns, never the checker and never another file, so what a file's
32
+ // output depends on is that file's own text plus strip.config.*, which was
33
+ // reported above as a host input (samchon/ttsc#1263).
34
+ ctx.ReportDependenciesComplete()
30
35
  for _, file := range prog.SourceFiles() {
31
36
  rewriter.apply(file)
32
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttsc/strip",
3
- "version": "0.27.0",
3
+ "version": "0.28.1",
4
4
  "description": "First-party ttsc plugin that removes configured calls and statements from emitted JavaScript.",
5
5
  "main": "src/index.cjs",
6
6
  "types": "src/index.d.ts",