@algolia/wizard 0.13.0 → 0.15.0

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/main.js CHANGED
@@ -1079,12 +1079,12 @@ var sidebarItems = [
1079
1079
  description: "push 100 records to Algolia in seconds"
1080
1080
  },
1081
1081
  {
1082
- title: "detect your framework",
1083
- description: "React, Vue, Angular, Vanilla JS"
1082
+ title: "detect your stack",
1083
+ description: "whatever language and framework you already use"
1084
1084
  },
1085
1085
  {
1086
1086
  title: "scaffold a search UI",
1087
- description: "a styled InstantSearch component, wired into your app"
1087
+ description: "a search box and results, wired into your app"
1088
1088
  },
1089
1089
  {
1090
1090
  title: "ship it",
@@ -2821,11 +2821,19 @@ import z14 from "zod";
2821
2821
  import { readdir as readdir2, readFile as readFile6 } from "node:fs/promises";
2822
2822
  import { join as join8 } from "node:path";
2823
2823
  var MAX_QUERY_LENGTH = 1e3;
2824
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
2825
+ "node_modules",
2826
+ "dist",
2827
+ "build",
2828
+ "vendor",
2829
+ "venv",
2830
+ "__pycache__",
2831
+ "target"
2832
+ ]);
2824
2833
  async function walkFiles(dir) {
2825
- const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "dist"]);
2826
2834
  const out = [];
2827
2835
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2828
- if (e.name.startsWith(".") || skip.has(e.name)) continue;
2836
+ if (e.name.startsWith(".") || SKIP_DIRS.has(e.name)) continue;
2829
2837
  const full = join8(dir, e.name);
2830
2838
  if (e.isDirectory()) out.push(...await walkFiles(full));
2831
2839
  else if (e.isFile()) out.push(full);
@@ -3155,7 +3163,7 @@ function generateRecordTool(ctx) {
3155
3163
  return {
3156
3164
  filePath: relPath,
3157
3165
  count: records.length,
3158
- message: `Wrote ${records.length} records to ${relPath}. Read and parse this file in the script at runtime (e.g. JSON.parse(readFileSync(...)) in Node/Bun, json.load(open(...)) in Python) \u2014 do not inline the records as literals.`
3166
+ message: `Wrote ${records.length} records to ${relPath}. Read and parse this file in the script at runtime using your language's standard JSON support \u2014 do not inline the records as literals.`
3159
3167
  };
3160
3168
  } catch (err) {
3161
3169
  return `Error generating records: ${err.message}`;
@@ -3344,8 +3352,8 @@ var detectLanguageSchema = z20.object({
3344
3352
  var detectLanguage = () => runAgent({
3345
3353
  instructions: [
3346
3354
  "Analyze the codebase and determine the programming languages and frameworks used",
3347
- "If a superset language is found, exclude the subset language. TS-over-JS.",
3348
- "If a meta-framework is used, exclude the framework. Next-over-React.",
3355
+ "If a superset language is found, exclude the subset language (e.g. TypeScript over JavaScript).",
3356
+ "If a meta-framework is used, exclude the framework it builds on (e.g. Next.js over React, Rails over Rack).",
3349
3357
  "Return the exact version",
3350
3358
  "Exclude things like CSS frameworks, build tools, or testing frameworks",
3351
3359
  'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
@@ -3443,7 +3451,7 @@ async function runAnalysis(mode, extraInstructions = []) {
3443
3451
  // package.json
3444
3452
  var package_default = {
3445
3453
  name: "@algolia/wizard",
3446
- version: "0.13.0",
3454
+ version: "0.15.0",
3447
3455
  description: "Magically implement Algolia functionality in your codebase",
3448
3456
  type: "module",
3449
3457
  engines: {
@@ -3592,9 +3600,11 @@ var CURATED_FRAMEWORKS = [
3592
3600
  "Next.js",
3593
3601
  "React",
3594
3602
  "Vue",
3595
- "Angular",
3596
- "Svelte",
3597
- "Vanilla JS"
3603
+ "Vanilla JS",
3604
+ "Django",
3605
+ "Laravel",
3606
+ "Rails",
3607
+ "Symfony"
3598
3608
  ];
3599
3609
  var OTHER_OPTION = "Other";
3600
3610
  var normalize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
@@ -3607,12 +3617,15 @@ var FRAMEWORK_ALIASES = {
3607
3617
  vuejs: "vue",
3608
3618
  angular: "angular",
3609
3619
  angularjs: "angular",
3610
- svelte: "svelte",
3611
- sveltekit: "svelte",
3612
3620
  vanillajs: "vanillajs",
3613
3621
  vanilla: "vanillajs",
3614
3622
  javascript: "vanillajs",
3615
- js: "vanillajs"
3623
+ js: "vanillajs",
3624
+ django: "django",
3625
+ laravel: "laravel",
3626
+ rails: "rails",
3627
+ rubyonrails: "rails",
3628
+ symfony: "symfony"
3616
3629
  };
3617
3630
  var isSameFramework = (a, b) => {
3618
3631
  const x = FRAMEWORK_ALIASES[normalize(a)] ?? normalize(a);
@@ -4076,6 +4089,7 @@ function getNamedDoc(name, language) {
4076
4089
  return readFileSync(file, "utf8").trim();
4077
4090
  }
4078
4091
  function getFrameworkSpecificDoc(frameworks) {
4092
+ if (frameworks.length === 0) return "";
4079
4093
  const fw = frameworks.map((f) => f.toLowerCase());
4080
4094
  if (fw.includes("vue") || fw.includes("nuxt")) {
4081
4095
  return loadAlgoliaDoc("vue");
@@ -4083,9 +4097,6 @@ function getFrameworkSpecificDoc(frameworks) {
4083
4097
  if (fw.includes("react") || fw.includes("next.js")) {
4084
4098
  return loadAlgoliaDoc("react");
4085
4099
  }
4086
- if (fw.includes("angular")) {
4087
- return loadAlgoliaDoc("angular");
4088
- }
4089
4100
  return loadAlgoliaDoc("js");
4090
4101
  }
4091
4102
 
@@ -4123,30 +4134,34 @@ var verificationOutputSchema = z27.object({
4123
4134
  var MAX_IMPLEMENT_VERIFICATION_ATTEMPTS = 3;
4124
4135
  var DEFAULT_IMPLEMENT_USE_CASES = ["ingestion", "search"];
4125
4136
  var INGEST_DIR = ".algolia-wizard";
4126
- function detectUiFramework(language) {
4127
- const names = language.frameworks.map((f) => f.name.toLowerCase());
4128
- if (names.some((n) => n.includes("vue") || n.includes("nuxt"))) return "Vue";
4129
- if (names.some((n) => n.includes("react") || n.includes("next")))
4130
- return "React";
4131
- if (names.some((n) => n.includes("angular"))) return "Angular";
4132
- return "JavaScript";
4133
- }
4134
- function frameworksForDoc(framework) {
4135
- switch (framework) {
4136
- case "React":
4137
- return ["react"];
4138
- case "Vue":
4139
- return ["vue"];
4140
- case "Angular":
4141
- return ["angular"];
4142
- case "JavaScript":
4143
- return [];
4144
- }
4137
+ var JS_LANGUAGES = ["javascript", "typescript", "jsx", "tsx", "node"];
4138
+ function lower(entries) {
4139
+ return entries.map((entry) => entry.name.toLowerCase());
4145
4140
  }
4146
- function publicEnvPrefix(language) {
4147
- const frameworkNames = language.frameworks.map(
4148
- (framework) => framework.name.toLowerCase()
4141
+ function isJsProject(language) {
4142
+ return lower(language.languages).some(
4143
+ (name) => JS_LANGUAGES.some((js) => name.includes(js))
4149
4144
  );
4145
+ }
4146
+ var UI_FRAMEWORKS = [
4147
+ { match: ["vue", "nuxt"], target: "Vue", doc: "vue" },
4148
+ { match: ["react", "next"], target: "React", doc: "react" },
4149
+ { match: ["angular"], target: "Angular" }
4150
+ ];
4151
+ function matchUiFramework(language) {
4152
+ const names = lower(language.frameworks);
4153
+ return UI_FRAMEWORKS.find(
4154
+ (ui) => ui.match.some((needle) => names.some((name) => name.includes(needle)))
4155
+ );
4156
+ }
4157
+ function searchUiTarget(language) {
4158
+ return matchUiFramework(language)?.target ?? language.frameworks[0]?.name ?? (isJsProject(language) ? "JavaScript" : "this project");
4159
+ }
4160
+ function frameworksForDoc(language) {
4161
+ return [matchUiFramework(language)?.doc ?? "js"];
4162
+ }
4163
+ function publicEnvPrefix(language) {
4164
+ const frameworkNames = lower(language.frameworks);
4150
4165
  if (frameworkNames.some((name) => name.includes("next"))) {
4151
4166
  return "NEXT_PUBLIC_";
4152
4167
  }
@@ -4159,7 +4174,7 @@ function publicEnvPrefix(language) {
4159
4174
  if (frameworkNames.some((name) => name.includes("vite"))) {
4160
4175
  return "VITE_";
4161
4176
  }
4162
- return "PUBLIC_";
4177
+ return isJsProject(language) ? "PUBLIC_" : "";
4163
4178
  }
4164
4179
  var APP_ID_VAR_SUFFIX = "ALGOLIA_APP_ID";
4165
4180
  var SEARCH_KEY_VAR_SUFFIX = "ALGOLIA_SEARCH_API_KEY";
@@ -4200,6 +4215,7 @@ function baseInstructions(input) {
4200
4215
  `Project languages and frameworks: ${JSON.stringify(input.language)}`,
4201
4216
  "Make minimal, idiomatic changes; do not touch unrelated code.",
4202
4217
  `Commands run through a shell on ${process.platform}. Write commands that work there.`,
4218
+ "Use the project's own tooling for every command \u2014 its package manager, task runner, and test/lint commands. Do not assume a JavaScript toolchain.",
4203
4219
  "runShell needs the developer to approve each command, so give every call a clear `explanation` naming what it does and any side effect. If a command is rejected, do not retry it \u2014 take a different approach or report the limitation."
4204
4220
  ];
4205
4221
  }
@@ -4220,12 +4236,21 @@ function sourceSpecificInstructions(input) {
4220
4236
  generated: [
4221
4237
  "No real data source exists; use sample records for each confirmed entity.",
4222
4238
  "Call the generateRecord tool once per entity (entityName, attributes, count 20-50); it invents the values and unique objectIDs and writes them to a JSON file in the worktree, returning the file path. Do not write records or objectIDs yourself.",
4223
- "In the script, read and parse each returned file path at runtime (e.g. JSON.parse(readFileSync(...)) in Node/Bun, json.load(open(...)) in Python) instead of inlining the records as literals.",
4239
+ "In the script, read and parse each returned file path at runtime using your language's standard JSON support, instead of inlining the records as literals.",
4224
4240
  "Add a prominent TODO where the developer swaps the generated records (and the JSON file under `.algolia-wizard/data/`) for their real record source."
4225
4241
  ]
4226
4242
  };
4227
4243
  return byLine[input.ingestionSource];
4228
4244
  }
4245
+ function algoliaClientDoc(input) {
4246
+ const doc = getNamedDoc("save-records", "js");
4247
+ if (!doc) return [];
4248
+ if (isJsProject(input.language)) return [doc];
4249
+ return [
4250
+ "The reference below is written in JavaScript. Use it for the method names, arguments, and record shape, then translate to this project's language and its official Algolia client:",
4251
+ doc
4252
+ ];
4253
+ }
4229
4254
  function ingestionInstructions(input) {
4230
4255
  return [
4231
4256
  ...input.confirmed && input.confirmed.length ? [
@@ -4233,10 +4258,10 @@ function ingestionInstructions(input) {
4233
4258
  `Ingest only these confirmed entities (name, source paths, attributes): ${JSON.stringify(input.confirmed)}.`,
4234
4259
  `Ingesting writes to Algolia, so the script needs a write API key and App ID \u2014 read them from the ${API_KEY_VAR} and ${APP_ID_VAR} environment variables rather than hardcoding them. The wizard sets these when it runs the script.`,
4235
4260
  `Read the index name from the ${INDEX_NAME_VAR} environment variable, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or entity name \u2014 the write key only works for that exact index. Exit with an error if ${INDEX_NAME_VAR} is unset.`,
4236
- "Use the appropriate Algolia package in the ingestion script. Do not use the raw HTTP API.",
4261
+ "Write the script in the project's primary language, using Algolia's official client for that language. Do not use the raw HTTP API.",
4237
4262
  "After a successful ingest, the script must print exactly one line to stdout in the form `ALGOLIA_WIZARD_RECORD_COUNT=<n>`, where <n> is the total number of records pushed to Algolia. Print it last, on its own line, with no surrounding text.",
4238
- getNamedDoc("save-records", "js"),
4239
- 'Install the Algolia client via runShell and add it to package.json "dependencies" with a valid version range, so the dependency is not just installed ad hoc.',
4263
+ ...algoliaClientDoc(input),
4264
+ "Install the Algolia client with the project's own package manager via runShell, declaring it in whatever manifest the project uses (e.g. package.json, requirements.txt, Gemfile, go.mod, composer.json) so the dependency is not just installed ad hoc.",
4240
4265
  'Then run the script yourself via runShell, and report the command you ran as "ingestCommand" so the developer can re-run it. Its explanation must say that running it writes records to Algolia.',
4241
4266
  "The summary should be extremely concise.",
4242
4267
  ...sourceSpecificInstructions(input)
@@ -4244,13 +4269,18 @@ function ingestionInstructions(input) {
4244
4269
  ];
4245
4270
  }
4246
4271
  function searchInstructions(input) {
4247
- const doc = getFrameworkSpecificDoc(frameworksForDoc(input.uiFramework));
4272
+ const doc = getFrameworkSpecificDoc(frameworksForDoc(input.language));
4248
4273
  return [
4249
4274
  "Implement an in-app Algolia search experience.",
4250
- `Build the search UI for ${input.uiFramework}.`,
4251
- "Follow the Algolia JS SDK reference below for client setup and InstantSearch wiring; prefer it over prior knowledge:",
4252
- doc,
4253
- `Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working SearchBox and Hits against the target index.`,
4275
+ `Build the search UI for ${input.searchUiTarget}.`,
4276
+ ...doc ? [
4277
+ "Follow the Algolia SDK reference below for client setup and search UI wiring; prefer it over prior knowledge:",
4278
+ doc
4279
+ ] : [
4280
+ "No bundled Algolia SDK reference exists for this stack, so rely on the project's own conventions and Algolia's official client for its language. Do not invent APIs \u2014 keep to the documented search endpoint and its parameters."
4281
+ ],
4282
+ `Add the search UI at ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working search input and results list against the target index.`,
4283
+ "If a search box already exists, replace it with yours.",
4254
4284
  `Read the index name from the ${searchIndexVar(input.language)} env var, which the wizard sets to "${input.targetIndex}". Never hardcode an index name or derive one from the project, file, or component name.`,
4255
4285
  "Read the App ID, the search-only API key, and the index name from env vars; never hardcode them. A search-only key is safe to expose client-side.",
4256
4286
  // The key is provisioned only after verification passes, so the agent never
@@ -4261,8 +4291,9 @@ function searchInstructions(input) {
4261
4291
  // Not the agent's to rename: the wizard writes these exact names into
4262
4292
  // ".env" right after this step, so a renamed prefix would leave the code
4263
4293
  // reading a var the wizard never wrote.
4264
- `Use exactly these public env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4265
- 'Install any Algolia/InstantSearch packages you import via runShell, and add them to package.json "dependencies" with a valid version range.',
4294
+ `Use exactly these env var names in the code: ${input.searchEnvVars.map(({ name }) => name).join(", ")}.`,
4295
+ "Install any Algolia packages you import with the project's own package manager via runShell, and declare them in the project's dependency manifest.",
4296
+ "Match the styles of the application as closely as possible.",
4266
4297
  "The summary should be extremely concise; do not mention env var setup or manual testing steps \u2014 the wizard writes the resolved credentials to .env and reports that separately."
4267
4298
  ];
4268
4299
  }
@@ -4491,7 +4522,7 @@ async function implement(ctx, useCases = DEFAULT_IMPLEMENT_USE_CASES, existingWo
4491
4522
  ingestDir: INGEST_DIR,
4492
4523
  ingestionSource,
4493
4524
  uploadFilePath,
4494
- uiFramework: detectUiFramework(language)
4525
+ searchUiTarget: searchUiTarget(language)
4495
4526
  };
4496
4527
  const summaries = [];
4497
4528
  if (uploadWarning) summaries.push(uploadWarning);
@@ -10,8 +10,8 @@ InstantSearch).
10
10
  - `algoliasearch` v5 (install `^5`)
11
11
  - `react-instantsearch` v7 (install `^7`)
12
12
  - `vue-instantsearch` v4 (install `^4`)
13
- - `instantsearch.js` v4 (install `^4`) — also the recommended choice for Angular
14
- - `angular-instantsearch` DEPRECATED/archived (Sep 2024); do not use, prefer `instantsearch.js`
13
+ - `instantsearch.js` v4 (install `^4`) — also the choice for any framework without
14
+ its own flavor (Angular, Svelte); `angular-instantsearch` is archived, never use it
15
15
 
16
16
  Always install the **latest stable** within the major (use a caret range like `^5` /
17
17
  `^7`); never pin an exact patch.
@@ -30,7 +30,7 @@ Always install the **latest stable** within the major (use a caret range like `^
30
30
 
31
31
  ## Files
32
32
 
33
- - `instantsearch-setup.md` — framework-specific InstantSearch wiring (React, Vue,
34
- Angular, vanilla). Use this for the in-app search UI.
33
+ - `instantsearch-setup-*.md` — InstantSearch wiring per framework (React, Vue, and
34
+ vanilla for everything else). Use this for the in-app search UI.
35
35
  - `search-single-index.md` — direct/manual search via the core client
36
36
  (`searchSingleIndex`), for cases where InstantSearch is not used.
@@ -31,3 +31,14 @@ search.addWidgets([
31
31
 
32
32
  search.start()
33
33
  ```
34
+
35
+ ## Frameworks without their own flavor
36
+
37
+ Only React and Vue have a maintained InstantSearch wrapper. For any other
38
+ framework — Angular, Svelte, Solid — drive `instantsearch.js` directly from a
39
+ component, mounting widgets against element refs and calling `search.dispose()`
40
+ on teardown.
41
+
42
+ Do NOT install `angular-instantsearch` or a community `*-instantsearch` package:
43
+ `angular-instantsearch` is deprecated and archived (last release v4.4.3, Sep 2024),
44
+ is incompatible with Angular v13+, and receives no security fixes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@algolia/wizard",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Magically implement Algolia functionality in your codebase",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,80 +0,0 @@
1
- # InstantSearch setup
2
-
3
- Install `instantsearch.js` (v4)
4
-
5
- ## Search client: always use the lite client
6
-
7
- Import the client from `algoliasearch/lite` and alias it to `algoliasearch`
8
-
9
- ```ts
10
- import { liteClient as algoliasearch } from 'algoliasearch/lite'
11
-
12
- // Instantiate ONCE, outside components, with a stable reference.
13
- const searchClient = algoliasearch(APP_ID, SEARCH_ONLY_KEY)
14
- ```
15
-
16
- ## Angular (use `instantsearch.js`)
17
-
18
- Do NOT use `angular-instantsearch`: it is deprecated and archived (last release
19
- v4.4.3, Sep 2024), not compatible with Ivy / modern Angular (v13+), and receives no
20
- security fixes. Algolia recommends driving `instantsearch.js` directly from an Angular
21
- component instead.
22
-
23
- ```ts
24
- import {
25
- Component,
26
- ElementRef,
27
- OnDestroy,
28
- OnInit,
29
- ViewChild,
30
- } from '@angular/core'
31
- import { liteClient as algoliasearch } from 'algoliasearch/lite'
32
- import instantsearch from 'instantsearch.js'
33
- import { searchBox, hits } from 'instantsearch.js/es/widgets'
34
-
35
- @Component({
36
- selector: 'app-search',
37
- template: `<div #searchbox></div>
38
- <div #hits></div>`,
39
- })
40
- export class SearchComponent implements OnInit, OnDestroy {
41
- @ViewChild('searchbox', { static: true }) searchbox!: ElementRef
42
- @ViewChild('hits', { static: true }) hits!: ElementRef
43
-
44
- private search = instantsearch({
45
- indexName: 'INDEX_NAME',
46
- searchClient: algoliasearch(APP_ID, SEARCH_ONLY_KEY),
47
- })
48
-
49
- ngOnInit() {
50
- this.search.addWidgets([
51
- searchBox({ container: this.searchbox.nativeElement }),
52
- hits({ container: this.hits.nativeElement }),
53
- ])
54
- this.search.start()
55
- }
56
-
57
- ngOnDestroy() {
58
- this.search.dispose()
59
- }
60
- }
61
- ```
62
-
63
- ## Vanilla (`instantsearch.js` v4)
64
-
65
- ```js
66
- import { liteClient as algoliasearch } from 'algoliasearch/lite'
67
- import instantsearch from 'instantsearch.js'
68
- import { searchBox, hits } from 'instantsearch.js/es/widgets'
69
-
70
- const searchClient = algoliasearch(APP_ID, SEARCH_ONLY_KEY)
71
-
72
- const search = instantsearch({ indexName: 'INDEX_NAME', searchClient })
73
-
74
- search.addWidgets([
75
- searchBox({ container: '#searchbox' }),
76
- hits({ container: '#hits' }),
77
- ])
78
-
79
- search.start()
80
- ```