@projitect/blueprint 0.0.0-stage → 1.0.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/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @projitect/blueprint
3
+ *
4
+ * Authoring SDK for blueprint packages. Blueprint authors compose these primitives — the
5
+ * `BlueprintFileSystem` service for FS access, the four ownership-mode constructors
6
+ * (`regionFile` / `jsonMerge` / `ownFile` / `seedFile`), the post-apply `exec` constructor, the section helpers
7
+ * (`markdownSection`, `ignoreSection`), the `directory` scope transform, and detectors for
8
+ * common project introspection.
9
+ *
10
+ * Authors use Effect's own control-flow combinators (`Effect.gen`, `Effect.all`, `Effect.if`,
11
+ * `Effect.when`, `Effect.unless`, `Effect.forEach`) for sequencing and conditionals — we do
12
+ * not ship duplicates.
13
+ */
14
+ export * from "./constructors.js"
15
+ export * from "./sections.js"
16
+ export * from "./directory.js"
17
+ export * from "./detect.js"
18
+ export * from "./splice.js"
19
+ export * from "./add-sections.js"
20
+ export { BlueprintFileSystem } from "@projitect/core"
21
+ export { Blueprint, ChangeSet, Permission, Errors } from "@projitect/core"
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Section helpers — thin wrappers around `regionFile` that bake in the right comment
3
+ * delimiters for two common file shapes:
4
+ *
5
+ * - **`markdownSection`** for HTML/MDX/XML files where comments need a closing `-->`.
6
+ * The fenced region renders invisible in markdown output — readers never see the markers.
7
+ * Useful for README.md, AGENTS.md, CONTRIBUTING.md, generated MDX docs, etc.
8
+ *
9
+ * - **`ignoreSection`** for `#`-comment "ignore"-style files. Same shape as the gitignore
10
+ * convention; the helper exists because the name documents intent better than a bare
11
+ * `regionFile({ commentPrefix: "#" })` does, and the surface guides authors toward the
12
+ * right primitive when they're scaffolding a `.eslintignore` / `.prettierignore` /
13
+ * `.dockerignore` / `.npmignore` blueprint.
14
+ *
15
+ * Both helpers re-emit the underlying `RegionFileSpec`-shaped result of `regionFile`, so
16
+ * everything that works for `regionFile` works for these (extra permissions, descriptions,
17
+ * etc.). They aren't shipped as separate npm packages because there's no opinionated content
18
+ * to bundle — the blueprint author always provides their own.
19
+ */
20
+
21
+ import { StructX } from "@nunofyobiz/effect-extras"
22
+ import type { Blueprint, Permission } from "@projitect/core"
23
+ import { regionFile } from "./constructors.js"
24
+
25
+ export interface SectionSpec {
26
+ readonly id: string
27
+ readonly version: string
28
+ readonly description?: string
29
+ readonly path: string
30
+ readonly content: string
31
+ readonly extraPermissions?: readonly Permission.Permission[]
32
+ }
33
+
34
+ /**
35
+ * Region-mode blueprint for HTML / MDX / XML files. The fence is rendered as two HTML
36
+ * comments on their own lines:
37
+ *
38
+ * <!-- pjt:<owner-id> start -->
39
+ * <body>
40
+ * <!-- pjt:<owner-id> end -->
41
+ *
42
+ * The markers don't render in HTML output (they're comments), so readers of the published
43
+ * file never see them. `pjt inspect` flags hand-edits between the markers; lines outside
44
+ * the fence belong to the user.
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * markdownSection({
49
+ * id: "my-org:readme:badges",
50
+ * version: "1.0.0",
51
+ * path: "README.md",
52
+ * content: "![ci](https://example.com/badge.svg)\n",
53
+ * })
54
+ * ```
55
+ */
56
+ export const markdownSection = (spec: SectionSpec): Blueprint.Blueprint =>
57
+ regionFile({
58
+ id: spec.id,
59
+ version: spec.version,
60
+ ...StructX.defined("description", spec.description),
61
+ path: spec.path,
62
+ commentPrefix: "<!--",
63
+ commentSuffix: " -->",
64
+ content: spec.content,
65
+ ...StructX.defined("extraPermissions", spec.extraPermissions),
66
+ })
67
+
68
+ /**
69
+ * Region-mode blueprint for `#`-comment ignore-style files: `.gitignore`, `.eslintignore`,
70
+ * `.prettierignore`, `.dockerignore`, `.npmignore`, etc. The fence is two `#`-prefixed lines
71
+ * on either side of the section content:
72
+ *
73
+ * # pjt:<owner-id> start
74
+ * <body>
75
+ * # pjt:<owner-id> end
76
+ *
77
+ * Equivalent to `regionFile({ commentPrefix: "#", ... })`, but the name documents the
78
+ * intent and makes the right primitive easy to find.
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * ignoreSection({
83
+ * id: "my-org:eslintignore:generated",
84
+ * version: "1.0.0",
85
+ * path: ".eslintignore",
86
+ * content: "dist/\nbuild/\ncoverage/\n",
87
+ * })
88
+ * ```
89
+ */
90
+ export const ignoreSection = (spec: SectionSpec): Blueprint.Blueprint =>
91
+ regionFile({
92
+ id: spec.id,
93
+ version: spec.version,
94
+ ...StructX.defined("description", spec.description),
95
+ path: spec.path,
96
+ commentPrefix: "#",
97
+ content: spec.content,
98
+ ...StructX.defined("extraPermissions", spec.extraPermissions),
99
+ })
package/src/splice.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { Effect } from "effect"
2
+ import { Errors } from "@projitect/core"
3
+ import { StringX } from "@nunofyobiz/effect-extras"
4
+
5
+ const IMPORTS_START = "// pjt:imports start"
6
+ const IMPORTS_END = "// pjt:imports end"
7
+ const BLUEPRINTS_START = "// pjt:blueprints start"
8
+ const BLUEPRINTS_END = "// pjt:blueprints end"
9
+
10
+ /**
11
+ * Splice an import line and one or more blueprint call lines into `.pjt.ts` text using the
12
+ * convention markers. The import goes between `// pjt:imports start/end` (deduped against
13
+ * existing content), the calls go between `// pjt:blueprints start/end` (appended, with 4-space
14
+ * indent to match the standard template). An empty `callLines` leaves the blueprints block alone.
15
+ *
16
+ * Pure string-to-string: the caller owns reading and writing the file. `blueprintFile` is only
17
+ * used to name the file in the error message.
18
+ *
19
+ * Fails with `pjt.add.markers-missing` if either marker pair is missing — the user has
20
+ * hand-edited the file and we won't guess.
21
+ */
22
+ export const spliceBlueprintFile = (params: {
23
+ readonly content: string
24
+ readonly blueprintFile: string
25
+ /** Import statement to splice (one line, no trailing newline). Deduped. */
26
+ readonly importLine: string
27
+ /** Blueprint call lines (one entry per line, no leading indent, no trailing newline). */
28
+ readonly callLines: readonly string[]
29
+ }): Effect.Effect<string, Errors.AddMarkersMissing> =>
30
+ Effect.gen(function* () {
31
+ const afterImport = yield* spliceImport({
32
+ content: params.content,
33
+ blueprintFile: params.blueprintFile,
34
+ importLine: params.importLine,
35
+ })
36
+ return yield* spliceCalls({
37
+ content: afterImport,
38
+ blueprintFile: params.blueprintFile,
39
+ callLines: params.callLines,
40
+ })
41
+ })
42
+
43
+ const spliceImport = (params: {
44
+ readonly content: string
45
+ readonly blueprintFile: string
46
+ readonly importLine: string
47
+ }): Effect.Effect<string, Errors.AddMarkersMissing> => {
48
+ const { content, blueprintFile, importLine } = params
49
+ return Effect.gen(function* () {
50
+ const lines = content.split("\n")
51
+ const startIndex = lines.findIndex((l) => l.trim() === IMPORTS_START)
52
+ const endIndex = lines.findIndex((l) => l.trim() === IMPORTS_END)
53
+ if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
54
+ return yield* new Errors.AddMarkersMissing({
55
+ id: "pjt.add.markers-missing",
56
+ blueprintFile,
57
+ missingMarker: `${IMPORTS_START} / ${IMPORTS_END}`,
58
+ message:
59
+ `${blueprintFile} is missing the \`pjt:imports start\` / \`pjt:imports end\` markers. ` +
60
+ "Restore them (see `pjt init`'s starter template) and re-run `pjt add`.",
61
+ })
62
+ }
63
+ // Dedupe: skip if any line between the markers already equals the import
64
+ const between = lines.slice(startIndex + 1, endIndex).map((l) => l.trim())
65
+ if (between.includes(importLine.trim())) {
66
+ return content
67
+ }
68
+ return StringX.insertBeforeLine(content, endIndex, [importLine])
69
+ })
70
+ }
71
+
72
+ const spliceCalls = (params: {
73
+ readonly content: string
74
+ readonly blueprintFile: string
75
+ readonly callLines: readonly string[]
76
+ }): Effect.Effect<string, Errors.AddMarkersMissing> => {
77
+ const { content, blueprintFile, callLines } = params
78
+ return Effect.gen(function* () {
79
+ if (callLines.length === 0) {
80
+ return content
81
+ }
82
+ const lines = content.split("\n")
83
+ const startIndex = lines.findIndex((l) => l.trim() === BLUEPRINTS_START)
84
+ const endIndex = lines.findIndex((l) => l.trim() === BLUEPRINTS_END)
85
+ if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) {
86
+ return yield* new Errors.AddMarkersMissing({
87
+ id: "pjt.add.markers-missing",
88
+ blueprintFile,
89
+ missingMarker: `${BLUEPRINTS_START} / ${BLUEPRINTS_END}`,
90
+ message:
91
+ `${blueprintFile} is missing the \`pjt:blueprints start\` / \`pjt:blueprints end\` markers. ` +
92
+ "Restore them and re-run `pjt add`.",
93
+ })
94
+ }
95
+ const indent = " " // 4-space, matching the starter template
96
+ const newCalls = callLines.map((c) => `${indent}${c.replace(/^\s+/u, "")}`)
97
+ return StringX.insertBeforeLine(content, endIndex, newCalls)
98
+ })
99
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.