captive-stack-detector 0.8.3 → 0.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 272eb63b46430d6bf1d609c667e0e6c33a8a103120b0e7d12942989d196c34a8
4
- data.tar.gz: fdcbd149dd558c16d126f7378a88f7318224a6f19e0561c9949765b8266f3ff6
3
+ metadata.gz: b76909efc2f43ad9460c45c09e1d49e2129a71dd6a83e9034d3e7014d838e3e7
4
+ data.tar.gz: d1e3d67c81abed91d14dafdb56b025113a1dc0fdd66c426570dc13b84fc1610c
5
5
  SHA512:
6
- metadata.gz: 55c86da390fd33c90773cdb0a920cfa867f2182dc8c992fc971f76b7fdcee68c4f33f686eff186130d500db2af48a61c954975067775c436e6711df45c464f32
7
- data.tar.gz: 5ef3670152f0c1807d8eb479ff931b5992657728cb09f73d85f464164fe491b489fc4f504c180d384b2d7dc48b6889c03fdedab1cc1f85672edaf78edd9196c0
6
+ metadata.gz: 9afd81fb1afd6ddc07b72d94159aae066675f05ad4a9c8fe2934746de386cf542796162b7e834b7bddbbb2c5bdb5f8eb023c15f69c2824a5e02767e609e4d654
7
+ data.tar.gz: ef0f80061ab951116e7abda178814cfa864843ca924f1fb4de539169c8b3ff52892bb9844cb9e73ba5fc69b7dbd0588d3b061c74d01a1d952a052df1f0525a8e
data/docs/stacks.md CHANGED
@@ -69,6 +69,30 @@ Pour Astro, utilisez l'adaptateur `@astrojs/node` en mode `standalone`, avec `ou
69
69
  - Une ligne `worker: <commande>` dans un `Procfile` lance un worker.
70
70
 
71
71
 
72
+ ### Variables à renseigner
73
+
74
+ La détection Node recherche les lectures à clé littérale `process.env.MA_CLE`,
75
+ `process.env["MA_CLE"]`, `import.meta.env.MA_CLE` et `import.meta.env["MA_CLE"]`
76
+ dans les sources JavaScript, TypeScript, JSX, TSX, Astro, Vue et Svelte, y compris les
77
+ fichiers de configuration à la racine. Les imports nommés de `astro:env/client` et
78
+ `astro:env/server` sont également reconnus.
79
+
80
+ Les modèles `.env.example`, `.env.sample` et `.env.template` à la racine déclarent aussi
81
+ des noms de variables à renseigner. **Leurs valeurs ne sont jamais reprises** : chaque nom
82
+ est signalé avec la valeur `placeholder`. Les fichiers `.env`, `.env.local` et les autres
83
+ fichiers d'environnement réels ne sont pas lus.
84
+
85
+ Les commentaires, chaînes d'exemple, dépendances, sorties de build, scripts d'outillage
86
+ (`scripts/`, `bin/`), documentation, exemples, tests, stories et
87
+ déclarations TypeScript sont ignorés. Les constantes intégrées de Vite et les variables
88
+ `NODE_ENV`, `NODE_OPTIONS`, `HOST` et `PORT` ne sont pas demandées. Une lecture avec une
89
+ valeur de repli littérale (`?? "local"`, `|| "local"`, nombre ou booléen) est facultative ;
90
+ une lecture sans repli ailleurs, ou sa déclaration dans un modèle, demande toujours la clé.
91
+
92
+ L'analyse est statique : les clés calculées, la déstructuration et les appels indirects
93
+ (comme `getSecret(nom)`) ne sont pas déduits. Déclarez ces clés dans un modèle si nécessaire.
94
+ La même analyse est utilisée sur les dépôts locaux et GitHub.
95
+
72
96
  ### Variables exposées au navigateur
73
97
 
74
98
  Une variable n'arrive dans le code du navigateur que si elle porte le préfixe de votre bundler. Captive le déduit de vos dépendances :
@@ -77,6 +101,7 @@ Une variable n'arrive dans le code du navigateur que si elle porte le préfixe d
77
101
  | --- | --- |
78
102
  | `expo` | `EXPO_PUBLIC_` |
79
103
  | `next` | `NEXT_PUBLIC_` |
104
+ | `astro` | `PUBLIC_` |
80
105
  | `vite` | `VITE_` |
81
106
 
82
107
  Si plusieurs sont présentes, c'est le premier de la liste qui l'emporte (`vite` est souvent là pour les tests seulement).
@@ -85,6 +110,15 @@ Avec un backend Supabase, Captive pose l'URL et la clé publique sous ce préfix
85
110
 
86
111
  Ces variables sont lues **au build** : après en avoir changé une, redéployez.
87
112
 
113
+ ### Site statique sans build
114
+
115
+ Condition : ni `Gemfile` ni `package.json` à la racine, mais un `public/index.html` ou un `index.html` à la racine. Rien n'est installé ni buildé : vos fichiers sont servis tels quels.
116
+
117
+ - Si `public/` existe, seul ce dossier est servi.
118
+ - Sinon la racine est servie, limitée aux fichiers web (HTML, JavaScript, CSS, JSON, images, polices, manifeste, texte). Les dossiers cachés (`.git/`, `.github/`), `supabase/`, vos scripts SQL ou votre documentation ne sont jamais publiés.
119
+ - Un dossier `supabase/` avec son `config.toml` provisionne un backend Supabase.
120
+ - Faute de build, aucune variable n'est injectée : votre code lit directement l'adresse de l'API, par exemple `location.origin`.
121
+
88
122
  ## Expo
89
123
 
90
124
  Condition : un `package.json` à la racine avec la dépendance `expo` (dans `dependencies` ou `devDependencies`). Aucun script n'est exigé.
@@ -101,7 +135,7 @@ Condition : un `package.json` à la racine avec la dépendance `expo` (dans `dep
101
135
 
102
136
  | Message | Cause | Que faire |
103
137
  | --- | --- | --- |
104
- | Aucun Gemfile ni package.json à la racine du dépôt. | Pas de fichier de dépendances à la racine. | Vérifiez que l'application est bien à la racine du dépôt, pas dans un sous-dossier (`app/`, `frontend/`…). Les monorepos ne sont pas pris en charge. |
138
+ | Aucun Gemfile ni package.json à la racine du dépôt. | Pas de fichier de dépendances ni d'`index.html` à la racine. | Vérifiez que l'application est bien à la racine du dépôt, pas dans un sous-dossier (`app/`, `frontend/`…). Les monorepos ne sont pas pris en charge. |
105
139
  | Gemfile trouvé, mais sans la gem rails. | Application Ruby non Rails (Sinatra, Hanami…), ou `Gemfile` d'outillage dans un projet JavaScript. | Seul Rails est pris en charge côté Ruby. Si le `Gemfile` ne sert qu'à de l'outillage, retirez-le du dépôt. |
106
140
  | package.json trouvé, mais sans dépendance expo ni script start ou build. | Captive ne sait pas comment construire l'application. | Ajoutez un script `build` (application web) ou `start` (serveur), comme dans les exemples ci-dessus. |
107
141
 
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveStackDetector
4
+ # Les modèles déclarent des noms ; leurs valeurs d'exemple ne sont jamais importées.
5
+ module EnvTemplateScanner
6
+ def self.scan(content)
7
+ content.scan(/^\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=/).flatten.to_h { |key| [ key, "placeholder" ] }
8
+ end
9
+ end
10
+ end
@@ -30,7 +30,7 @@ module CaptiveStackDetector
30
30
  end
31
31
 
32
32
  def list(dir)
33
- Dir.glob("#{dir}/**/*", base: @path).select { |entry| File.file?(File.join(@path, entry)) }.sort
33
+ Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH, base: @path).select { |entry| File.file?(File.join(@path, entry)) }.sort
34
34
  end
35
35
  end
36
36
 
@@ -9,7 +9,7 @@ module CaptiveStackDetector
9
9
  end
10
10
 
11
11
  def files_in(dir)
12
- prefix = "#{dir}/"
12
+ prefix = dir == "." ? "" : "#{dir}/"
13
13
  @entries.filter_map { |entry| entry["path"] if entry["type"] == "blob" && entry["path"].start_with?(prefix) }
14
14
  end
15
15
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "js_env_files"
4
+
5
+ module CaptiveStackDetector
6
+ # Ne lit que les sources et les modèles d'environnement, jamais les secrets .env.
7
+ class JsEnvDetector
8
+ def initialize(reader)
9
+ @reader = reader
10
+ end
11
+
12
+ def detect
13
+ vars = @reader.list(".").sort.each_with_object({}) do |entry, found|
14
+ path = entry.delete_prefix("./")
15
+ parser = JsEnvFiles.parser(path)
16
+ found.merge!(parser.scan(@reader.read(path).to_s)) if parser
17
+ end
18
+ vars.reject { |key, _| JsEnvScanner::RUNTIME_VARS.include?(key) }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "js_env_scanner"
4
+ require_relative "env_template_scanner"
5
+
6
+ module CaptiveStackDetector
7
+ # Choisit les fichiers lisibles avant tout téléchargement de contenu.
8
+ module JsEnvFiles
9
+ TEMPLATES = %w[.env.example .env.sample .env.template].freeze
10
+ EXCLUDED = %r{
11
+ (?:\A|/)(?:node_modules|vendor|\.git|dist|build|\.output|\.next|\.nuxt|\.astro|coverage|
12
+ test|tests|spec|__tests__|__mocks__|\.storybook|\.github|scripts|bin|docs|examples|fixtures|e2e)(?:/|\z)
13
+ }x
14
+ SOURCE = /(?<!\.test)(?<!\.spec)(?<!\.stories)(?<!\.d)\.(?:[cm]?[jt]sx?|astro|vue|svelte)\z/
15
+
16
+ def self.parser(path)
17
+ return nil if EXCLUDED.match?(path)
18
+ return EnvTemplateScanner if TEMPLATES.include?(path)
19
+
20
+ JsEnvScanner if SOURCE.match?(path)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "strscan"
4
+ require_relative "js_env_syntax"
5
+
6
+ module CaptiveStackDetector
7
+ # Énumère les accès littéraux en ignorant commentaires et texte non exécuté.
8
+ class JsEnvLexer
9
+ include JsEnvSyntax
10
+
11
+ def initialize(content)
12
+ @scanner = StringScanner.new(content, fixed_anchor: true)
13
+ @references = []
14
+ end
15
+
16
+ def references
17
+ code
18
+ @references
19
+ end
20
+
21
+ private
22
+
23
+ def code(terminator = nil)
24
+ until @scanner.eos?
25
+ next if @scanner.skip(IGNORED)
26
+
27
+ break if consume == terminator
28
+ end
29
+ end
30
+
31
+ def consume
32
+ if @scanner.scan(ASTRO_IMPORT)
33
+ astro_import(@scanner[1])
34
+ elsif @scanner.scan(REFERENCE)
35
+ reference
36
+ else
37
+ punctuation(@scanner.getch)
38
+ end
39
+ end
40
+
41
+ def punctuation(char)
42
+ code("}") if char == "{"
43
+ template if char == "`"
44
+ char
45
+ end
46
+
47
+ def astro_import(names)
48
+ JsEnvSyntax.astro_keys(names).each { |key| @references << [ key, "astro", "" ] }
49
+ end
50
+
51
+ def reference
52
+ scope = @scanner[1].start_with?("import") ? "vite" : "node"
53
+ @references << [ @scanner[2] || @scanner[3], scope, @scanner.rest ]
54
+ end
55
+
56
+ def template
57
+ until @scanner.eos? || @scanner.skip(/`/)
58
+ code("}") if @scanner.skip(/\$\{/)
59
+ @scanner.skip(/(?:\\.|[^`$]|\$(?!\{))+/m)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "js_env_lexer"
4
+
5
+ module CaptiveStackDetector
6
+ # Ne demande que les lectures sans valeur de repli et hors variables intégrées.
7
+ module JsEnvScanner
8
+ DEFAULT = /\A\s*(?:\?\?|\|\|)\s*(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|true\b|false\b|\d+(?:\.\d+)?\b)/
9
+ ASSIGNMENT = /\A\s*(?:=(?!=|>)|\?\?=|\|\|=|&&=)/
10
+ RUNTIME_VARS = %w[NODE_ENV NODE_OPTIONS PORT HOST].freeze
11
+ VITE_VARS = %w[BASE_URL MODE DEV PROD SSR].freeze
12
+
13
+ def self.scan(content)
14
+ JsEnvLexer.new(content).references.each_with_object({}) do |(key, scope, rest), found|
15
+ next if RUNTIME_VARS.include?(key) || (scope == "vite" && VITE_VARS.include?(key))
16
+ next if DEFAULT.match?(rest) || ASSIGNMENT.match?(rest)
17
+
18
+ found[key] = "placeholder"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CaptiveStackDetector
4
+ # Tokens reconnus sans interpréter le programme JavaScript.
5
+ module JsEnvSyntax
6
+ KEY = /[A-Z_][A-Z0-9_]*/
7
+ REFERENCE = /
8
+ (?<![\w$.])(process\s*\.\s*env|import\s*\.\s*meta\s*\.\s*env)
9
+ (?:\s*\.\s*(#{KEY})(?![\w$])|\s*\[\s*['"](#{KEY})['"]\s*\])
10
+ /x
11
+ ASTRO_IMPORT = /import\s*\{([^}]+)\}\s*from\s*['"]astro:env\/(?:client|server)['"]/m
12
+ COMMENT = %r{//[^\n]*|/\*.*?\*/|<!--.*?-->}m
13
+ IGNORED = Regexp.union(COMMENT, /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/m)
14
+ def self.astro_keys(names)
15
+ names.gsub(COMMENT, "").split(",").filter_map { |name| name.strip[/\A#{KEY}\b/] }
16
+ end
17
+ end
18
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "../captive_stack_detector"
4
4
  require_relative "package_json_analyzer"
5
+ require_relative "js_env_detector"
5
6
 
6
7
  module CaptiveStackDetector
7
8
  class JsStackDetector
@@ -20,7 +21,7 @@ module CaptiveStackDetector
20
21
  services: Services.new(database: @analyzer.database, queue: @analyzer.queue, backend: @analyzer.backend),
21
22
  worker: build_worker,
22
23
  runtime: Runtime.new(ruby: nil, node: @reader.node_version),
23
- env_vars: {},
24
+ env_vars: JsEnvDetector.new(@reader).detect,
24
25
  system_packages: [],
25
26
  public_env_prefix: @analyzer.public_env_prefix,
26
27
  )
@@ -5,7 +5,7 @@ require_relative "ssr_framework_detector"
5
5
 
6
6
  module CaptiveStackDetector
7
7
  class PackageJsonAnalyzer
8
- PUBLIC_ENV_PREFIXES = { "expo" => "EXPO_PUBLIC_", "next" => "NEXT_PUBLIC_", "vite" => "VITE_" }.freeze
8
+ PUBLIC_ENV_PREFIXES = { "expo" => "EXPO_PUBLIC_", "next" => "NEXT_PUBLIC_", "astro" => "PUBLIC_", "vite" => "VITE_" }.freeze
9
9
  # Des clients, pas des ORM : Prisma ou Drizzle parlent aussi MySQL et SQLite, leur
10
10
  # présence ne dit pas que l'app attend une base Postgres.
11
11
  POSTGRES_CLIENTS = %w[pg postgres].freeze
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "types"
4
+
5
+ module CaptiveStackDetector
6
+ # Un site statique sans manifeste ni build (index.html qui charge ses scripts) : stack node
7
+ # sans subtype, servie telle quelle. Supabase se reconnaît à son dossier, faute de package.json.
8
+ class StaticSiteDetector
9
+ ENTRIES = %w[public/index.html index.html].freeze
10
+
11
+ def initialize(reader)
12
+ @reader = reader
13
+ end
14
+
15
+ def site?
16
+ ENTRIES.any? { |entry| @reader.read(entry) }
17
+ end
18
+
19
+ def detect
20
+ Result.new(
21
+ type: "node", subtype: nil, worker: nil, env_vars: [], system_packages: [],
22
+ services: Services.new(database: nil, queue: nil, backend: backend),
23
+ runtime: Runtime.new(ruby: nil, node: nil),
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def backend = ("supabase" if @reader.read("supabase/config.toml"))
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CaptiveStackDetector
4
- VERSION = "0.8.3"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -6,6 +6,7 @@ require_relative "captive_stack_detector/file_reader"
6
6
  require_relative "captive_stack_detector/gemfile_analyzer"
7
7
  require_relative "captive_stack_detector/rails_stack_detector"
8
8
  require_relative "captive_stack_detector/js_stack_detector"
9
+ require_relative "captive_stack_detector/static_site_detector"
9
10
 
10
11
  module CaptiveStackDetector
11
12
  # Doc publique des stacks supportées, livrée dans la gem et affichée par le dashboard (ADR 0003).
@@ -23,7 +24,10 @@ module CaptiveStackDetector
23
24
  return RailsStackDetector.new(reader, GemfileAnalyzer.new(gemfile)).detect if gemfile
24
25
  return JsStackDetector.new(reader, package_json).detect if package_json
25
26
 
26
- raise UnsupportedStack, :no_manifest
27
+ static_site = StaticSiteDetector.new(reader)
28
+ raise UnsupportedStack, :no_manifest unless static_site.site?
29
+
30
+ static_site.detect
27
31
  end
28
32
  private_class_method :detect_from
29
33
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: captive-stack-detector
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Captive Studio
@@ -21,6 +21,7 @@ files:
21
21
  - lib/captive/stack/detector.rb
22
22
  - lib/captive_stack_detector.rb
23
23
  - lib/captive_stack_detector/aptfile_parser.rb
24
+ - lib/captive_stack_detector/env_template_scanner.rb
24
25
  - lib/captive_stack_detector/env_vars_scanner.rb
25
26
  - lib/captive_stack_detector/file_content_parser.rb
26
27
  - lib/captive_stack_detector/file_reader.rb
@@ -28,6 +29,11 @@ files:
28
29
  - lib/captive_stack_detector/gemfile_analyzer.rb
29
30
  - lib/captive_stack_detector/github_api_client.rb
30
31
  - lib/captive_stack_detector/github_tree.rb
32
+ - lib/captive_stack_detector/js_env_detector.rb
33
+ - lib/captive_stack_detector/js_env_files.rb
34
+ - lib/captive_stack_detector/js_env_lexer.rb
35
+ - lib/captive_stack_detector/js_env_scanner.rb
36
+ - lib/captive_stack_detector/js_env_syntax.rb
31
37
  - lib/captive_stack_detector/js_stack_detector.rb
32
38
  - lib/captive_stack_detector/node_version_detector.rb
33
39
  - lib/captive_stack_detector/package_json_analyzer.rb
@@ -37,6 +43,7 @@ files:
37
43
  - lib/captive_stack_detector/solid_queue_usage.rb
38
44
  - lib/captive_stack_detector/solid_queue_worker.rb
39
45
  - lib/captive_stack_detector/ssr_framework_detector.rb
46
+ - lib/captive_stack_detector/static_site_detector.rb
40
47
  - lib/captive_stack_detector/system_package_detector.rb
41
48
  - lib/captive_stack_detector/types.rb
42
49
  - lib/captive_stack_detector/version.rb