@paramour-js/next 1.0.0-rc.0 → 1.0.0-rc.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/emit.d.ts CHANGED
@@ -11,6 +11,13 @@ export interface WriteIfChangedResult {
11
11
  */
12
12
  written: boolean;
13
13
  }
14
+ /**
15
+ * The line ending a user-owned file already uses. Init edits files the user
16
+ * owns (package.json, next.config, AGENTS.md), and writing LF text into a CRLF file
17
+ * leaves mixed endings, so those edits keep the file's own ending. Generated
18
+ * files are always written LF instead.
19
+ */
20
+ export declare function lineEnding(text: string): "\n" | "\r\n";
14
21
  /**
15
22
  * Content equality that ignores CRLF vs LF. A consumer repo with git
16
23
  * `core.autocrlf=true` checks the committed LF artifact out as CRLF; that
package/dist/emit.js CHANGED
@@ -1,5 +1,14 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
+ /**
4
+ * The line ending a user-owned file already uses. Init edits files the user
5
+ * owns (package.json, next.config, AGENTS.md), and writing LF text into a CRLF file
6
+ * leaves mixed endings, so those edits keep the file's own ending. Generated
7
+ * files are always written LF instead.
8
+ */
9
+ export function lineEnding(text) {
10
+ return text.includes("\r\n") ? "\r\n" : "\n";
11
+ }
3
12
  /**
4
13
  * Content equality that ignores CRLF vs LF. A consumer repo with git
5
14
  * `core.autocrlf=true` checks the committed LF artifact out as CRLF; that
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { lineEnding, sameContent } from "../emit.js";
3
4
  export const AGENTS_MARKER_START = "<!-- paramour:start -->";
4
5
  export const AGENTS_MARKER_END = "<!-- paramour:end -->";
5
6
  /**
@@ -53,21 +54,22 @@ export function findAgentsFile(projectRoot) {
53
54
  * would mean guessing where the user's own text begins.
54
55
  */
55
56
  export function upsertAgentsSection(text) {
57
+ const eol = lineEnding(text);
58
+ const snippet = agentsSnippet().replaceAll("\n", eol);
56
59
  const start = text.indexOf(AGENTS_MARKER_START);
57
60
  if (start === -1) {
58
- const base = text === "" || text.endsWith("\n") ? text : `${text}\n`;
59
- const separator = base === "" ? "" : "\n";
61
+ const base = text === "" || text.endsWith("\n") ? text : `${text}${eol}`;
62
+ const separator = base === "" ? "" : eol;
60
63
  return {
61
64
  status: "added",
62
- text: `${base}${separator}${agentsSnippet()}\n`,
65
+ text: `${base}${separator}${snippet}${eol}`,
63
66
  };
64
67
  }
65
68
  const end = text.indexOf(AGENTS_MARKER_END, start);
66
69
  if (end === -1)
67
70
  return { status: "unterminated", text };
68
71
  const current = text.slice(start, end + AGENTS_MARKER_END.length);
69
- const snippet = agentsSnippet();
70
- if (current === snippet)
72
+ if (sameContent(current, snippet))
71
73
  return { status: "unchanged", text };
72
74
  return {
73
75
  status: "updated",
@@ -6,9 +6,9 @@ export interface SetupCheck {
6
6
  }
7
7
  /**
8
8
  * Insert `"paramour": "paramour generate"` into a package.json's scripts,
9
- * preserving the file's own indentation and trailing-newline choice.
10
- * Throws on malformed JSON — a broken package.json is init's one hard
11
- * prerequisite failure.
9
+ * preserving the file's own indentation, line ending, and trailing-newline
10
+ * choice. Throws on malformed JSON — a broken package.json is init's one
11
+ * hard prerequisite failure.
12
12
  */
13
13
  export declare function addPackageScript(text: string): {
14
14
  changed: boolean;
@@ -1,14 +1,15 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join, relative } from "node:path";
3
+ import { lineEnding } from "../emit.js";
3
4
  import { resolveRouteDirs } from "../scan.js";
4
5
  import { loadPackagedSkill } from "../skills/packaged.js";
5
6
  import { auditTarget, isOutdated } from "../skills/sync.js";
6
7
  import { detectTargets } from "../skills/targets.js";
7
8
  /**
8
9
  * Insert `"paramour": "paramour generate"` into a package.json's scripts,
9
- * preserving the file's own indentation and trailing-newline choice.
10
- * Throws on malformed JSON — a broken package.json is init's one hard
11
- * prerequisite failure.
10
+ * preserving the file's own indentation, line ending, and trailing-newline
11
+ * choice. Throws on malformed JSON — a broken package.json is init's one
12
+ * hard prerequisite failure.
12
13
  */
13
14
  export function addPackageScript(text) {
14
15
  const parsed = JSON.parse(text);
@@ -28,8 +29,12 @@ export function addPackageScript(text) {
28
29
  return { changed: false, text };
29
30
  pkg.scripts = { ...scripts, paramour: "paramour generate" };
30
31
  const indent = /^([ \t]+)"/m.exec(text)?.[1] ?? " ";
31
- const trailing = text.endsWith("\n") ? "\n" : "";
32
- return { changed: true, text: JSON.stringify(pkg, null, indent) + trailing };
32
+ const eol = lineEnding(text);
33
+ const trailing = text.endsWith("\n") ? eol : "";
34
+ return {
35
+ changed: true,
36
+ text: JSON.stringify(pkg, null, indent).replaceAll("\n", eol) + trailing,
37
+ };
33
38
  }
34
39
  /**
35
40
  * The detect-and-verify summary: route dirs discoverable, both packages
@@ -1,5 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { lineEnding } from "../emit.js";
3
4
  const IMPORT_LINE = `import { withTypedRoutes } from "@paramour-js/next";`;
4
5
  /** {@link writtenBindings}' name for `module.exports` (no identifier has a dot). */
5
6
  const MODULE_EXPORTS = "module.exports";
@@ -131,7 +132,13 @@ export async function wrapNextConfigSource(source) {
131
132
  });
132
133
  }
133
134
  mod.exports.default = builders.functionCall(local ?? "withTypedRoutes", current);
134
- return { code: generateCode(mod).code, status: "wrapped" };
135
+ // recast prints new nodes with LF and drops the final newline; keep the
136
+ // user's file as it was on both counts.
137
+ const eol = lineEnding(source);
138
+ let code = generateCode(mod).code.replaceAll("\r\n", "\n");
139
+ if (source.endsWith("\n") && !code.endsWith("\n"))
140
+ code += "\n";
141
+ return { code: code.replaceAll("\n", eol), status: "wrapped" };
135
142
  }
136
143
  catch {
137
144
  return { snippet: manualSnippet(), status: "manual" };
@@ -38,9 +38,10 @@ export interface RouteDefinition {
38
38
  * config globs replace the default patterns when the heuristic misfires.
39
39
  *
40
40
  * jiti evaluates matched files (the loader carry-over from config loading).
41
- * Known limits, both handled by the per-module degrade: tsconfig `paths`
42
- * aliases are not resolved (a future improvement could feed them into jiti's
43
- * `alias` option), and `server-only`-style imports throw outside Next.
41
+ * Imports resolve through the project's tsconfig `paths`, like Next's own
42
+ * resolution, so definitions that import shared codecs via `@/…` load. One
43
+ * known limit is left to the per-module degrade: `server-only`-style
44
+ * imports throw outside Next.
44
45
  */
45
46
  export declare function discoverRouteDefinitions(projectRoot: string, options?: {
46
47
  routeFiles?: readonly string[] | undefined;
@@ -15,6 +15,11 @@ const IGNORE_PATTERNS = [
15
15
  "**/node_modules/**",
16
16
  "**/out/**",
17
17
  ];
18
+ // Test files often mention defineAppRoute (they test route codecs), and
19
+ // evaluating them outside the runner executes their top-level
20
+ // describe/it calls and any setup side effects. These are ignored only in
21
+ // the automatic scan: explicit `routeFiles` globs are taken as written.
22
+ const TEST_IGNORE_PATTERNS = ["**/*.{test,spec}.*", "**/__tests__/**"];
18
23
  /** Files past this size skip the content pre-filter (bundles, lockfiles). */
19
24
  const MAX_PREFILTER_BYTES = 512 * 1024;
20
25
  /**
@@ -24,17 +29,24 @@ const MAX_PREFILTER_BYTES = 512 * 1024;
24
29
  * config globs replace the default patterns when the heuristic misfires.
25
30
  *
26
31
  * jiti evaluates matched files (the loader carry-over from config loading).
27
- * Known limits, both handled by the per-module degrade: tsconfig `paths`
28
- * aliases are not resolved (a future improvement could feed them into jiti's
29
- * `alias` option), and `server-only`-style imports throw outside Next.
32
+ * Imports resolve through the project's tsconfig `paths`, like Next's own
33
+ * resolution, so definitions that import shared codecs via `@/…` load. One
34
+ * known limit is left to the per-module degrade: `server-only`-style
35
+ * imports throw outside Next.
30
36
  */
31
37
  export async function discoverRouteDefinitions(projectRoot, options = {}) {
32
38
  // Dynamic imports, same stance as config.ts: only commands that actually
33
39
  // discover definitions pay for tinyglobby/jiti.
34
40
  const { glob } = await import("tinyglobby");
35
- const files = await glob(options.routeFiles === undefined
36
- ? DEFAULT_PATTERNS
37
- : [...options.routeFiles], { absolute: true, cwd: projectRoot, ignore: IGNORE_PATTERNS });
41
+ const { routeFiles } = options;
42
+ const [patterns, ignore] = routeFiles === undefined
43
+ ? [DEFAULT_PATTERNS, [...IGNORE_PATTERNS, ...TEST_IGNORE_PATTERNS]]
44
+ : [[...routeFiles], IGNORE_PATTERNS];
45
+ const files = await glob(patterns, {
46
+ absolute: true,
47
+ cwd: projectRoot,
48
+ ignore,
49
+ });
38
50
  // Deterministic load order — dedupe's "first wins" must not depend on
39
51
  // filesystem enumeration order.
40
52
  files.sort();
@@ -43,6 +55,11 @@ export async function discoverRouteDefinitions(projectRoot, options = {}) {
43
55
  const jiti = createJiti(import.meta.url, {
44
56
  fsCache: false,
45
57
  interopDefault: true,
58
+ // A directory, not `true`: `true` searches upward from this package's
59
+ // own install location inside node_modules, not from the user's project.
60
+ // jiti's bundled get-tsconfig finds the governing tsconfig.json from
61
+ // here and handles `extends`, `baseUrl`, and wildcard patterns.
62
+ tsconfigPaths: projectRoot,
46
63
  });
47
64
  const definitions = [];
48
65
  const duplicates = [];
@@ -55,7 +72,10 @@ export async function discoverRouteDefinitions(projectRoot, options = {}) {
55
72
  mod = await jiti.import(file);
56
73
  }
57
74
  catch (error) {
58
- loadFailures.push({ file: relFile, message: message(error) });
75
+ // First line only: Node appends a multi-line "Require stack" to
76
+ // MODULE_NOT_FOUND, which would bury the one-line-per-module report.
77
+ const [firstLine = ""] = message(error).split(/\r?\n/, 1);
78
+ loadFailures.push({ file: relFile, message: firstLine });
59
79
  continue;
60
80
  }
61
81
  if (typeof mod !== "object" || mod === null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paramour-js/next",
3
- "version": "1.0.0-rc.0",
3
+ "version": "1.0.0-rc.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -51,7 +51,7 @@
51
51
  "react-dom": "^19.2.0",
52
52
  "typescript": "^6.0.3",
53
53
  "zod": "^4.4.3",
54
- "paramour": "1.0.0-rc.0"
54
+ "paramour": "1.0.0-rc.1"
55
55
  },
56
56
  "description": "Next.js integration for paramour: withTypedRoutes, App and Pages Router hooks, PageProps glue, and the codegen CLI.",
57
57
  "author": "Jason Paff <jasonpaff@gmail.com>",