infinite-tag 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/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/src/apply.d.ts +14 -0
- package/dist/src/apply.js +84 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +416 -0
- package/dist/src/frameworks/index.d.ts +4 -0
- package/dist/src/frameworks/index.js +16 -0
- package/dist/src/frameworks/managed-files.d.ts +10 -0
- package/dist/src/frameworks/managed-files.js +104 -0
- package/dist/src/frameworks/next-app-router.d.ts +2 -0
- package/dist/src/frameworks/next-app-router.js +187 -0
- package/dist/src/frameworks/next-pages-router.d.ts +2 -0
- package/dist/src/frameworks/next-pages-router.js +169 -0
- package/dist/src/frameworks/shared.d.ts +17 -0
- package/dist/src/frameworks/shared.js +136 -0
- package/dist/src/frameworks/static-html.d.ts +2 -0
- package/dist/src/frameworks/static-html.js +137 -0
- package/dist/src/frameworks/vite-react.d.ts +2 -0
- package/dist/src/frameworks/vite-react.js +274 -0
- package/dist/src/index.d.ts +12 -0
- package/dist/src/index.js +11 -0
- package/dist/src/inspect.d.ts +10 -0
- package/dist/src/inspect.js +150 -0
- package/dist/src/manifest.d.ts +10 -0
- package/dist/src/manifest.js +83 -0
- package/dist/src/package-manager.d.ts +6 -0
- package/dist/src/package-manager.js +110 -0
- package/dist/src/plan.d.ts +9 -0
- package/dist/src/plan.js +97 -0
- package/dist/src/providers/ga4.d.ts +2 -0
- package/dist/src/providers/ga4.js +73 -0
- package/dist/src/providers/index.d.ts +3 -0
- package/dist/src/providers/index.js +11 -0
- package/dist/src/providers/posthog.d.ts +2 -0
- package/dist/src/providers/posthog.js +77 -0
- package/dist/src/providers/validate.d.ts +31 -0
- package/dist/src/providers/validate.js +110 -0
- package/dist/src/providers/x.d.ts +2 -0
- package/dist/src/providers/x.js +76 -0
- package/dist/src/render.d.ts +25 -0
- package/dist/src/render.js +260 -0
- package/dist/src/types.d.ts +151 -0
- package/dist/src/types.js +8 -0
- package/dist/src/uninstall.d.ts +7 -0
- package/dist/src/uninstall.js +66 -0
- package/dist/src/verify.d.ts +5 -0
- package/dist/src/verify.js +50 -0
- package/dist/src/workspace-artifacts.d.ts +41 -0
- package/dist/src/workspace-artifacts.js +171 -0
- package/package.json +27 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { fileExists, indentBlock, normalizeAppRelativePath, readRequiredFile, readWorkspacePackageJson, writeFileIfChanged } from "./shared.js";
|
|
3
|
+
const managedStartMarker = "<!-- infinite:start -->";
|
|
4
|
+
const managedEndMarker = "<!-- infinite:end -->";
|
|
5
|
+
export const staticHtmlAdapter = {
|
|
6
|
+
id: "static-html",
|
|
7
|
+
displayName: "Static HTML",
|
|
8
|
+
detect(root) {
|
|
9
|
+
if (!fileExists(root, "index.html")) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const pkg = readWorkspacePackageJson(root);
|
|
13
|
+
if (pkg) {
|
|
14
|
+
if (packageJsonHasFrameworkDeps(pkg)) {
|
|
15
|
+
// A real framework dependency is present — this is ambiguous, stay cautious.
|
|
16
|
+
return {
|
|
17
|
+
framework: "static-html",
|
|
18
|
+
confidence: 0.6,
|
|
19
|
+
files: ["index.html"],
|
|
20
|
+
assumptions: [
|
|
21
|
+
"index.html sits next to a package.json, so this may be a framework app rather than a plain static site. Confirm before applying."
|
|
22
|
+
]
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// package.json exists but only has build/lint/test tooling — treat as confidently static.
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
framework: "static-html",
|
|
29
|
+
confidence: 0.78,
|
|
30
|
+
files: ["index.html"],
|
|
31
|
+
assumptions: ["Static HTML wiring will target index.html directly."]
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
plan(root) {
|
|
35
|
+
const detected = this.detect(root);
|
|
36
|
+
const blockers = [];
|
|
37
|
+
if (!fileExists(root, "index.html")) {
|
|
38
|
+
blockers.push("Static HTML apply requires an index.html file.");
|
|
39
|
+
}
|
|
40
|
+
else if (!readRequiredFile(root, "index.html").includes("</head>")) {
|
|
41
|
+
blockers.push("Static HTML apply requires a closing </head> tag.");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
files: ["index.html"],
|
|
45
|
+
applyMode: blockers.length === 0 ? "supported" : "plan-only",
|
|
46
|
+
instructions: [],
|
|
47
|
+
assumptions: [
|
|
48
|
+
"Static HTML wiring uses direct public snippets rather than framework-specific runtime hooks."
|
|
49
|
+
],
|
|
50
|
+
blockers,
|
|
51
|
+
confidence: detected?.confidence ?? 0.75
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
apply(context) {
|
|
55
|
+
const appRoot = context.appRoot === "." ? context.root : join(context.root, context.appRoot);
|
|
56
|
+
const htmlPath = "index.html";
|
|
57
|
+
const rootRelativeHtmlPath = normalizeAppRelativePath(context.appRoot, htmlPath);
|
|
58
|
+
const html = readRequiredFile(appRoot, htmlPath);
|
|
59
|
+
if (!html.includes("</head>")) {
|
|
60
|
+
throw new Error("Static HTML apply requires a closing </head> tag.");
|
|
61
|
+
}
|
|
62
|
+
const providerSnippets = context.plan.instructions
|
|
63
|
+
.filter((instruction) => instruction.path === rootRelativeHtmlPath && instruction.provider)
|
|
64
|
+
.map((instruction) => instruction.snippet.trim())
|
|
65
|
+
.filter((snippet) => snippet.length > 0);
|
|
66
|
+
const managedBlock = [
|
|
67
|
+
managedStartMarker,
|
|
68
|
+
...providerSnippets.flatMap((snippet) => ["", indentBlock(snippet, 2)]),
|
|
69
|
+
"",
|
|
70
|
+
managedEndMarker
|
|
71
|
+
].join("\n");
|
|
72
|
+
const managedPattern = new RegExp(`${escapeForRegExp(managedStartMarker)}[\\s\\S]*?${escapeForRegExp(managedEndMarker)}`, "m");
|
|
73
|
+
const nextHtml = html.includes(managedStartMarker)
|
|
74
|
+
? html.replace(managedPattern, managedBlock)
|
|
75
|
+
: html.replace("</head>", `${managedBlock}\n</head>`);
|
|
76
|
+
const changedFiles = [];
|
|
77
|
+
if (writeFileIfChanged(appRoot, htmlPath, nextHtml)) {
|
|
78
|
+
changedFiles.push(rootRelativeHtmlPath);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
changedFiles,
|
|
82
|
+
warnings: []
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
uninstall(context) {
|
|
86
|
+
const appRoot = context.appRoot === "." ? context.root : join(context.root, context.appRoot);
|
|
87
|
+
const htmlPath = "index.html";
|
|
88
|
+
const rootRelativeHtmlPath = normalizeAppRelativePath(context.appRoot, htmlPath);
|
|
89
|
+
const restoredFiles = [];
|
|
90
|
+
const warnings = [];
|
|
91
|
+
if (!fileExists(appRoot, htmlPath)) {
|
|
92
|
+
warnings.push(`Managed file already absent: ${htmlPath}`);
|
|
93
|
+
return { removedFiles: [], restoredFiles, warnings };
|
|
94
|
+
}
|
|
95
|
+
const html = readRequiredFile(appRoot, htmlPath);
|
|
96
|
+
const managedPattern = new RegExp(`${escapeForRegExp(managedStartMarker)}[\\s\\S]*?${escapeForRegExp(managedEndMarker)}\\n?`, "m");
|
|
97
|
+
const nextHtml = html.replace(managedPattern, "");
|
|
98
|
+
if (nextHtml === html) {
|
|
99
|
+
warnings.push(`No managed Infinite block found in ${htmlPath}.`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
if (!context.dryRun) {
|
|
103
|
+
writeFileIfChanged(appRoot, htmlPath, nextHtml);
|
|
104
|
+
}
|
|
105
|
+
restoredFiles.push(rootRelativeHtmlPath);
|
|
106
|
+
}
|
|
107
|
+
return { removedFiles: [], restoredFiles, warnings };
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
function escapeForRegExp(source) {
|
|
111
|
+
return source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
112
|
+
}
|
|
113
|
+
// Framework-like dependency names that indicate the site is NOT a plain static
|
|
114
|
+
// HTML project, making the static-html adapter a risky choice.
|
|
115
|
+
const frameworkDepNames = new Set([
|
|
116
|
+
"react",
|
|
117
|
+
"react-dom",
|
|
118
|
+
"vue",
|
|
119
|
+
"svelte",
|
|
120
|
+
"@sveltejs/kit",
|
|
121
|
+
"next",
|
|
122
|
+
"nuxt",
|
|
123
|
+
"vite",
|
|
124
|
+
"@angular/core",
|
|
125
|
+
"gatsby",
|
|
126
|
+
"remix",
|
|
127
|
+
"@remix-run/react",
|
|
128
|
+
"astro",
|
|
129
|
+
"solid-js"
|
|
130
|
+
]);
|
|
131
|
+
function packageJsonHasFrameworkDeps(pkg) {
|
|
132
|
+
const allDeps = [
|
|
133
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
134
|
+
...Object.keys(pkg.devDependencies ?? {})
|
|
135
|
+
];
|
|
136
|
+
return allDeps.some((dep) => frameworkDepNames.has(dep));
|
|
137
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { buildAnalyticsModuleSource, hasExistingUnmanagedFile, isManagedInfiniteFile, removeManagedFile } from "./managed-files.js";
|
|
4
|
+
import { fileExists, firstExistingPath, hasDependency, normalizeAppRelativePath, readRequiredFile, writeFileIfChanged } from "./shared.js";
|
|
5
|
+
const mainCandidates = ["src/main.tsx", "src/main.jsx", "src/main.ts", "src/main.js"];
|
|
6
|
+
const analyticsModulePath = "src/lib/infinite-analytics.ts";
|
|
7
|
+
const importLine = 'import { installInfiniteInstrumentation } from "./lib/infinite-analytics"';
|
|
8
|
+
const bootLine = "installInfiniteInstrumentation()";
|
|
9
|
+
export const viteReactAdapter = {
|
|
10
|
+
id: "vite-react",
|
|
11
|
+
displayName: "Vite React",
|
|
12
|
+
detect(root) {
|
|
13
|
+
if (!hasDependency(root, "vite") || !hasDependency(root, "react")) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (!fileExists(root, "index.html")) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const mainFile = firstExistingPath(root, mainCandidates);
|
|
20
|
+
if (!mainFile) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
framework: "vite-react",
|
|
25
|
+
confidence: 0.92,
|
|
26
|
+
files: ["index.html", mainFile, analyticsModulePath],
|
|
27
|
+
assumptions: ["Vite React wiring will target the main entrypoint and index.html."]
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
plan(root) {
|
|
31
|
+
const detected = this.detect(root);
|
|
32
|
+
const mainFile = detected?.files[1] ?? "src/main.tsx";
|
|
33
|
+
const blockers = [];
|
|
34
|
+
if (!fileExists(root, mainFile)) {
|
|
35
|
+
blockers.push("Vite React apply requires a src/main.* entrypoint.");
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
const mainSource = readRequiredFile(root, mainFile);
|
|
39
|
+
if (!mainSource.includes("ReactDOM.createRoot(")) {
|
|
40
|
+
blockers.push("Vite React apply only supports simple main entrypoints with ReactDOM.createRoot().");
|
|
41
|
+
}
|
|
42
|
+
if (findImportSectionEnd(mainSource) === null) {
|
|
43
|
+
blockers.push("Vite React apply requires a simple import block at the top of src/main.*.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (hasExistingUnmanagedFile(root, analyticsModulePath)) {
|
|
47
|
+
blockers.push("Vite React apply will not overwrite an existing unmanaged src/lib/infinite-analytics.ts file.");
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
files: ["index.html", mainFile, analyticsModulePath],
|
|
51
|
+
applyMode: blockers.length === 0 ? "supported" : "plan-only",
|
|
52
|
+
instructions: blockers.length === 0
|
|
53
|
+
? [
|
|
54
|
+
{
|
|
55
|
+
path: mainFile,
|
|
56
|
+
action: "modify",
|
|
57
|
+
description: "Import and invoke installInfiniteInstrumentation() once before the React app bootstraps.",
|
|
58
|
+
snippet: `${importLine}\n\n${bootLine}`
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
: [],
|
|
62
|
+
assumptions: [
|
|
63
|
+
"Vite React public IDs can be surfaced through VITE_* environment variables or direct public wiring."
|
|
64
|
+
],
|
|
65
|
+
blockers,
|
|
66
|
+
confidence: detected?.confidence ?? 0.88
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
apply(context) {
|
|
70
|
+
const appRoot = context.appRoot === "." ? context.root : join(context.root, context.appRoot);
|
|
71
|
+
const mainFile = selectMainFile(appRoot, context.plan.files, context.appRoot);
|
|
72
|
+
const rootRelativeMainFile = normalizeAppRelativePath(context.appRoot, mainFile);
|
|
73
|
+
const rootRelativeAnalyticsFile = normalizeAppRelativePath(context.appRoot, analyticsModulePath);
|
|
74
|
+
const currentMain = readRequiredFile(appRoot, mainFile);
|
|
75
|
+
if (!currentMain.includes("ReactDOM.createRoot(")) {
|
|
76
|
+
throw new Error("Vite React apply only supports simple main entrypoints with ReactDOM.createRoot().");
|
|
77
|
+
}
|
|
78
|
+
const analyticsModuleAbsolutePath = join(appRoot, analyticsModulePath);
|
|
79
|
+
if (existsSync(analyticsModuleAbsolutePath) &&
|
|
80
|
+
!isManagedInfiniteFile(readFileSync(analyticsModuleAbsolutePath, "utf8"))) {
|
|
81
|
+
throw new Error(`Refusing to overwrite existing unmanaged analytics module at ${rootRelativeAnalyticsFile}.`);
|
|
82
|
+
}
|
|
83
|
+
const nextMain = upsertMainEntrypoint(currentMain);
|
|
84
|
+
const nextAnalyticsModule = buildAnalyticsModuleSource(context.plan);
|
|
85
|
+
const changedFiles = [];
|
|
86
|
+
if (writeFileIfChanged(appRoot, mainFile, nextMain)) {
|
|
87
|
+
changedFiles.push(rootRelativeMainFile);
|
|
88
|
+
}
|
|
89
|
+
if (writeFileIfChanged(appRoot, analyticsModulePath, nextAnalyticsModule)) {
|
|
90
|
+
changedFiles.push(rootRelativeAnalyticsFile);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
changedFiles,
|
|
94
|
+
warnings: []
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
uninstall(context) {
|
|
98
|
+
const appRoot = context.appRoot === "." ? context.root : join(context.root, context.appRoot);
|
|
99
|
+
const removedFiles = [];
|
|
100
|
+
const restoredFiles = [];
|
|
101
|
+
const warnings = [];
|
|
102
|
+
if (hasExistingUnmanagedFile(appRoot, analyticsModulePath)) {
|
|
103
|
+
throw new Error(`Refusing to remove ${analyticsModulePath} because it no longer looks managed by Infinite. Remove it manually if it should go.`);
|
|
104
|
+
}
|
|
105
|
+
let wiringFullyRemoved = true;
|
|
106
|
+
const mainFile = selectMainFile(appRoot, context.manifest.files, context.appRoot);
|
|
107
|
+
const mainAbsolutePath = join(appRoot, mainFile);
|
|
108
|
+
if (!existsSync(mainAbsolutePath)) {
|
|
109
|
+
warnings.push(`Managed main entrypoint already absent: ${mainFile}`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const currentMain = readFileSync(mainAbsolutePath, "utf8");
|
|
113
|
+
const nextMain = removeMainWiring(currentMain);
|
|
114
|
+
if (nextMain !== currentMain) {
|
|
115
|
+
if (!context.dryRun) {
|
|
116
|
+
writeFileIfChanged(appRoot, mainFile, nextMain);
|
|
117
|
+
}
|
|
118
|
+
restoredFiles.push(normalizeAppRelativePath(context.appRoot, mainFile));
|
|
119
|
+
}
|
|
120
|
+
if (nextMain.includes(importLine) || nextMain.includes(bootLine)) {
|
|
121
|
+
wiringFullyRemoved = false;
|
|
122
|
+
warnings.push(`Could not remove all instrumentation wiring from ${mainFile} automatically. Remove the leftover lines manually.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (wiringFullyRemoved) {
|
|
126
|
+
const removal = removeManagedFile(appRoot, analyticsModulePath, context.dryRun);
|
|
127
|
+
if (removal.removed) {
|
|
128
|
+
removedFiles.push(normalizeAppRelativePath(context.appRoot, analyticsModulePath));
|
|
129
|
+
}
|
|
130
|
+
if (removal.warning) {
|
|
131
|
+
warnings.push(removal.warning);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { removedFiles, restoredFiles, warnings };
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
function removeMainWiring(source) {
|
|
138
|
+
let next = source.replace(`${importLine}\n`, "");
|
|
139
|
+
next = next.replace(`\n${bootLine}\n`, "");
|
|
140
|
+
return next;
|
|
141
|
+
}
|
|
142
|
+
function selectMainFile(root, planFiles, appRoot) {
|
|
143
|
+
const appRelativeFiles = planFiles.map((file) => appRoot === "." ? file : file.replace(`${appRoot}/`, ""));
|
|
144
|
+
const matched = appRelativeFiles.find((file) => mainCandidates.includes(file));
|
|
145
|
+
if (matched) {
|
|
146
|
+
return matched;
|
|
147
|
+
}
|
|
148
|
+
const fallback = firstExistingPath(root, mainCandidates);
|
|
149
|
+
if (!fallback) {
|
|
150
|
+
throw new Error("Unable to resolve the Vite React main entrypoint.");
|
|
151
|
+
}
|
|
152
|
+
return fallback;
|
|
153
|
+
}
|
|
154
|
+
function upsertMainEntrypoint(source) {
|
|
155
|
+
const importSectionEnd = findImportSectionEnd(source);
|
|
156
|
+
if (importSectionEnd === null) {
|
|
157
|
+
throw new Error("Vite React apply requires a simple import block at the top of src/main.*.");
|
|
158
|
+
}
|
|
159
|
+
let next = source;
|
|
160
|
+
if (!next.includes(importLine)) {
|
|
161
|
+
next = `${next.slice(0, importSectionEnd)}${importLine}\n${next.slice(importSectionEnd)}`;
|
|
162
|
+
}
|
|
163
|
+
if (!next.includes(bootLine)) {
|
|
164
|
+
const refreshedImportSectionEnd = findImportSectionEnd(next);
|
|
165
|
+
if (refreshedImportSectionEnd === null) {
|
|
166
|
+
throw new Error("Unable to refresh the Vite React import block after inserting analytics wiring.");
|
|
167
|
+
}
|
|
168
|
+
next = `${next.slice(0, refreshedImportSectionEnd)}\n${bootLine}\n${next.slice(refreshedImportSectionEnd)}`;
|
|
169
|
+
}
|
|
170
|
+
return next;
|
|
171
|
+
}
|
|
172
|
+
// Finds the end offset of the first contiguous import section, treating each
|
|
173
|
+
// import statement as complete only once its brackets balance — so multi-line
|
|
174
|
+
// imports (`import {\n a,\n b\n} from "x"`) are never split mid-statement.
|
|
175
|
+
function findImportSectionEnd(source) {
|
|
176
|
+
const firstImport = source.match(/^import\b/m);
|
|
177
|
+
if (!firstImport || firstImport.index === undefined) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
let position = firstImport.index;
|
|
181
|
+
while (isImportKeywordAt(source, position)) {
|
|
182
|
+
const statementEnd = consumeImportStatement(source, position);
|
|
183
|
+
if (statementEnd === null) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
position = statementEnd;
|
|
187
|
+
}
|
|
188
|
+
return position;
|
|
189
|
+
}
|
|
190
|
+
// Returns true only when "import" at `pos` is a keyword — i.e. not followed
|
|
191
|
+
// by an identifier character. This prevents `importantSetup()` from being
|
|
192
|
+
// mistaken for an import statement.
|
|
193
|
+
function isImportKeywordAt(source, pos) {
|
|
194
|
+
if (!source.startsWith("import", pos)) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
// The character immediately after "import" must not be an identifier char.
|
|
198
|
+
const charAfter = source[pos + 6];
|
|
199
|
+
if (charAfter === undefined) {
|
|
200
|
+
// "import" at end-of-string — not a real import, stop scanning
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
return !/[A-Za-z0-9_$]/.test(charAfter);
|
|
204
|
+
}
|
|
205
|
+
function consumeImportStatement(source, start) {
|
|
206
|
+
// Scan character-by-character from the start of the statement, tracking string
|
|
207
|
+
// and comment state so delimiters inside them never affect the bracket depth.
|
|
208
|
+
// The statement ends at the first newline reached with balanced brackets
|
|
209
|
+
// (outside any string/comment), matching multi-line imports like
|
|
210
|
+
// `import {\n a,\n b\n} from "x"`. Returns the offset just past that newline,
|
|
211
|
+
// or null if the brackets never balance / a block comment is left unclosed.
|
|
212
|
+
let depth = 0;
|
|
213
|
+
let index = start;
|
|
214
|
+
let stringQuote = null;
|
|
215
|
+
let inBlockComment = false;
|
|
216
|
+
while (index < source.length) {
|
|
217
|
+
const ch = source[index];
|
|
218
|
+
const next = source[index + 1];
|
|
219
|
+
if (inBlockComment) {
|
|
220
|
+
if (ch === "*" && next === "/") {
|
|
221
|
+
inBlockComment = false;
|
|
222
|
+
index += 2;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
index += 1;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (stringQuote !== null) {
|
|
229
|
+
if (ch === "\\") {
|
|
230
|
+
index += 2; // skip the escaped character
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (ch === stringQuote) {
|
|
234
|
+
stringQuote = null;
|
|
235
|
+
}
|
|
236
|
+
index += 1;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (ch === "/" && next === "/") {
|
|
240
|
+
// Line comment: jump to the newline, which the newline branch handles.
|
|
241
|
+
const newlineIndex = source.indexOf("\n", index);
|
|
242
|
+
if (newlineIndex === -1) {
|
|
243
|
+
return depth <= 0 ? source.length : null;
|
|
244
|
+
}
|
|
245
|
+
index = newlineIndex;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (ch === "/" && next === "*") {
|
|
249
|
+
inBlockComment = true;
|
|
250
|
+
index += 2;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
254
|
+
stringQuote = ch;
|
|
255
|
+
index += 1;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (ch === "{" || ch === "(") {
|
|
259
|
+
depth += 1;
|
|
260
|
+
}
|
|
261
|
+
else if (ch === "}" || ch === ")") {
|
|
262
|
+
depth -= 1;
|
|
263
|
+
}
|
|
264
|
+
else if (ch === "\n" && depth <= 0) {
|
|
265
|
+
return index + 1;
|
|
266
|
+
}
|
|
267
|
+
index += 1;
|
|
268
|
+
}
|
|
269
|
+
// End of source with no trailing newline.
|
|
270
|
+
if (inBlockComment) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return depth <= 0 ? source.length : null;
|
|
274
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type { ApplyMode, ApplyResult, FrameworkAdapter, FrameworkApplyContext, FrameworkApplyResult, FrameworkUninstallContext, FrameworkUninstallResult, Ga4PublicArtifact, InspectResult, InstallInstruction, InstallManifest, InstallPlan, PackageManager, PackageManagerCommands, PackageManagerDetection, PosthogPublicArtifact, ProviderAdapter, ProviderId, ProviderPlanDraft, RepoStatus, SupportedFramework, UninstallResult, VerifyResult, WorkspaceInstallArtifacts, XPublicArtifact } from "./types.js";
|
|
2
|
+
export { applyInstallation } from "./apply.js";
|
|
3
|
+
export { frameworkAdapters, getFrameworkAdapter, isSupportedFramework } from "./frameworks/index.js";
|
|
4
|
+
export { detectRepoStatus, inspectWorkspace } from "./inspect.js";
|
|
5
|
+
export { computeContentHashes, installManifestPath, installManifestRelativePath, readInstallManifest, writeInstallManifest, writeInstallManifestIfChanged } from "./manifest.js";
|
|
6
|
+
export { buildPackageManagerCommands, detectPackageManager } from "./package-manager.js";
|
|
7
|
+
export { planInstallation } from "./plan.js";
|
|
8
|
+
export { getProviderAdapter, providerAdapters } from "./providers/index.js";
|
|
9
|
+
export { runCli } from "./cli.js";
|
|
10
|
+
export { uninstallInstallation } from "./uninstall.js";
|
|
11
|
+
export { resolveWorkspaceArtifacts } from "./workspace-artifacts.js";
|
|
12
|
+
export { verifyInstallation } from "./verify.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { applyInstallation } from "./apply.js";
|
|
2
|
+
export { frameworkAdapters, getFrameworkAdapter, isSupportedFramework } from "./frameworks/index.js";
|
|
3
|
+
export { detectRepoStatus, inspectWorkspace } from "./inspect.js";
|
|
4
|
+
export { computeContentHashes, installManifestPath, installManifestRelativePath, readInstallManifest, writeInstallManifest, writeInstallManifestIfChanged } from "./manifest.js";
|
|
5
|
+
export { buildPackageManagerCommands, detectPackageManager } from "./package-manager.js";
|
|
6
|
+
export { planInstallation } from "./plan.js";
|
|
7
|
+
export { getProviderAdapter, providerAdapters } from "./providers/index.js";
|
|
8
|
+
export { runCli } from "./cli.js";
|
|
9
|
+
export { uninstallInstallation } from "./uninstall.js";
|
|
10
|
+
export { resolveWorkspaceArtifacts } from "./workspace-artifacts.js";
|
|
11
|
+
export { verifyInstallation } from "./verify.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { resolveConfinedAppRoot } from "./frameworks/shared.js";
|
|
2
|
+
import type { InspectResult, PackageManager, RepoStatus } from "./types.js";
|
|
3
|
+
export interface InspectOptions {
|
|
4
|
+
appRoot?: string;
|
|
5
|
+
packageManager?: PackageManager;
|
|
6
|
+
}
|
|
7
|
+
export declare function detectRepoStatus(root: string): RepoStatus;
|
|
8
|
+
export { resolveConfinedAppRoot };
|
|
9
|
+
export declare function detectUnmanagedProviders(appRoot: string): string[];
|
|
10
|
+
export declare function inspectWorkspace(root: string, options?: InspectOptions): InspectResult;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readdirSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { frameworkAdapters } from "./frameworks/index.js";
|
|
6
|
+
import { resolveConfinedAppRoot } from "./frameworks/shared.js";
|
|
7
|
+
import { isManagedInfiniteFile } from "./frameworks/managed-files.js";
|
|
8
|
+
import { readInstallManifest } from "./manifest.js";
|
|
9
|
+
import { detectPackageManager } from "./package-manager.js";
|
|
10
|
+
export function detectRepoStatus(root) {
|
|
11
|
+
const insideWorkTree = spawnSync("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], {
|
|
12
|
+
encoding: "utf8"
|
|
13
|
+
});
|
|
14
|
+
if (insideWorkTree.status !== 0) {
|
|
15
|
+
return "not-a-git-repo";
|
|
16
|
+
}
|
|
17
|
+
const status = spawnSync("git", ["-C", root, "status", "--porcelain"], {
|
|
18
|
+
encoding: "utf8"
|
|
19
|
+
});
|
|
20
|
+
if (status.status !== 0) {
|
|
21
|
+
return "not-a-git-repo";
|
|
22
|
+
}
|
|
23
|
+
return status.stdout.trim().length > 0 ? "dirty" : "clean";
|
|
24
|
+
}
|
|
25
|
+
export { resolveConfinedAppRoot };
|
|
26
|
+
function discoverCandidateRoots(root, appRoot) {
|
|
27
|
+
if (appRoot) {
|
|
28
|
+
return [resolveConfinedAppRoot(root, appRoot)];
|
|
29
|
+
}
|
|
30
|
+
const candidates = [root];
|
|
31
|
+
const appsDirectory = join(root, "apps");
|
|
32
|
+
if (existsSync(appsDirectory)) {
|
|
33
|
+
const entries = readdirSync(appsDirectory, { withFileTypes: true })
|
|
34
|
+
.filter((entry) => entry.isDirectory())
|
|
35
|
+
.map((entry) => entry.name)
|
|
36
|
+
.sort();
|
|
37
|
+
for (const entryName of entries) {
|
|
38
|
+
candidates.push(join(appsDirectory, entryName));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return candidates;
|
|
42
|
+
}
|
|
43
|
+
const providerScanCandidates = [
|
|
44
|
+
"index.html",
|
|
45
|
+
"src/main.tsx",
|
|
46
|
+
"src/main.jsx",
|
|
47
|
+
"src/lib/infinite-analytics.ts",
|
|
48
|
+
"app/layout.tsx",
|
|
49
|
+
"pages/_app.tsx",
|
|
50
|
+
"lib/infinite-analytics.ts"
|
|
51
|
+
];
|
|
52
|
+
function scanContentsForProviders(contents, detected) {
|
|
53
|
+
// GA4: match the actual tag loader URL or the gtag() function call signature.
|
|
54
|
+
// Bare "google" or "gtag" strings in prose will not trigger this.
|
|
55
|
+
if (contents.includes("googletagmanager.com/gtag") || contents.includes("gtag(")) {
|
|
56
|
+
detected.add("ga4");
|
|
57
|
+
}
|
|
58
|
+
// PostHog: match the initialisation call or the CDN host, not the bare product name.
|
|
59
|
+
// Ordinary copy mentioning "posthog" (e.g. in a README or marketing page) will not trigger.
|
|
60
|
+
if (contents.includes("posthog.init(") || contents.includes("i.posthog.com")) {
|
|
61
|
+
detected.add("posthog");
|
|
62
|
+
}
|
|
63
|
+
// X/Twitter pixel: match its actual tag signatures only.
|
|
64
|
+
if (contents.includes("twq(") || contents.includes("static.ads-twitter.com")) {
|
|
65
|
+
detected.add("x");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function stripManagedHtmlBlocks(contents) {
|
|
69
|
+
return contents.replace(/<!-- infinite:start -->[\s\S]*?<!-- infinite:end -->/g, "");
|
|
70
|
+
}
|
|
71
|
+
export function detectUnmanagedProviders(appRoot) {
|
|
72
|
+
const detected = new Set();
|
|
73
|
+
for (const candidate of providerScanCandidates) {
|
|
74
|
+
const absolutePath = join(appRoot, candidate);
|
|
75
|
+
if (!existsSync(absolutePath)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const contents = readFileSync(absolutePath, "utf8");
|
|
79
|
+
if (isManagedInfiniteFile(contents)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
scanContentsForProviders(stripManagedHtmlBlocks(contents), detected);
|
|
83
|
+
}
|
|
84
|
+
return [...detected];
|
|
85
|
+
}
|
|
86
|
+
function detectExistingProviders(root, appRoot) {
|
|
87
|
+
const manifest = readInstallManifest(root);
|
|
88
|
+
if (manifest) {
|
|
89
|
+
return manifest.providers;
|
|
90
|
+
}
|
|
91
|
+
const detected = new Set();
|
|
92
|
+
for (const candidate of providerScanCandidates) {
|
|
93
|
+
const absolutePath = join(appRoot, candidate);
|
|
94
|
+
if (!existsSync(absolutePath)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
scanContentsForProviders(readFileSync(absolutePath, "utf8"), detected);
|
|
98
|
+
}
|
|
99
|
+
return [...detected];
|
|
100
|
+
}
|
|
101
|
+
export function inspectWorkspace(root, options = {}) {
|
|
102
|
+
const packageManagerDetection = detectPackageManager(root, options.packageManager);
|
|
103
|
+
const packageManager = packageManagerDetection.kind;
|
|
104
|
+
const repoStatus = detectRepoStatus(root);
|
|
105
|
+
const candidates = discoverCandidateRoots(root, options.appRoot);
|
|
106
|
+
let bestMatch;
|
|
107
|
+
for (const candidate of candidates) {
|
|
108
|
+
for (const adapter of frameworkAdapters) {
|
|
109
|
+
const result = adapter.detect(candidate);
|
|
110
|
+
if (!result) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!bestMatch || result.confidence > bestMatch.result.confidence) {
|
|
114
|
+
bestMatch = { root: candidate, result };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!bestMatch || !bestMatch.result) {
|
|
119
|
+
return {
|
|
120
|
+
framework: "unsupported",
|
|
121
|
+
appRoot: ".",
|
|
122
|
+
packageManager,
|
|
123
|
+
confidence: 0.2,
|
|
124
|
+
existingProviders: [],
|
|
125
|
+
repoStatus,
|
|
126
|
+
assumptions: packageManagerDetection.kind === "ambiguous"
|
|
127
|
+
? ["Multiple lockfiles were detected. Founder choice is required before printing install commands."]
|
|
128
|
+
: [],
|
|
129
|
+
blockers: ["Unsupported repository shape for instrumentation."],
|
|
130
|
+
detectedFiles: packageManagerDetection.lockfiles
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const relativeAppRoot = relative(root, bestMatch.root) || ".";
|
|
134
|
+
const existingProviders = detectExistingProviders(root, bestMatch.root);
|
|
135
|
+
const assumptions = [...bestMatch.result.assumptions];
|
|
136
|
+
if (packageManagerDetection.kind === "ambiguous") {
|
|
137
|
+
assumptions.push("Multiple lockfiles were detected. Founder choice is required before printing install commands.");
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
framework: bestMatch.result.framework,
|
|
141
|
+
appRoot: relativeAppRoot,
|
|
142
|
+
packageManager,
|
|
143
|
+
confidence: bestMatch.result.confidence,
|
|
144
|
+
existingProviders,
|
|
145
|
+
repoStatus,
|
|
146
|
+
assumptions,
|
|
147
|
+
blockers: [],
|
|
148
|
+
detectedFiles: bestMatch.result.files
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { InstallManifest } from "./types.js";
|
|
2
|
+
export declare const installManifestRelativePath = ".infinite/install.json";
|
|
3
|
+
export declare function installManifestPath(root: string): string;
|
|
4
|
+
export declare function readInstallManifest(root: string): InstallManifest | null;
|
|
5
|
+
export declare function writeInstallManifest(root: string, manifest: InstallManifest): string;
|
|
6
|
+
export declare function writeInstallManifestIfChanged(root: string, manifest: InstallManifest): {
|
|
7
|
+
changed: boolean;
|
|
8
|
+
manifestPath: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function computeContentHashes(root: string, files: string[]): Record<string, string>;
|