@fourtwelvelabs/fetch-contentful 0.3.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,83 @@
1
1
  # @fourtwelvelabs/fetch-contentful
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - fce4b46: The CLI now reads `.env.local` and `.env`.
8
+
9
+ `tada-init` and `tada-refresh` previously reported the space and token
10
+ missing in any project that keeps them in `.env.local` — which is most
11
+ Next.js projects. The message was technically true (nothing had populated
12
+ `process.env`, because Next reads that file only when Next itself boots) but
13
+ gave no hint that a file sitting in the same directory had been ignored.
14
+
15
+ Each setting now resolves from the first of: a flag, the shell environment,
16
+ `.env.local`, then `.env`. Anything genuinely exported still wins over a
17
+ file, so CI and shell overrides behave as before. A new `--env-file <path>`
18
+ reads somewhere else instead, and fails loudly if the path does not exist.
19
+
20
+ When configuration is still missing, the error now says which files were
21
+ read, or where it looked and found none.
22
+
23
+ Only the CLI does this. The library continues never to touch the disk:
24
+ reading files would break bundlers and edge runtimes, and platforms own that
25
+ job at runtime.
26
+
27
+ ## 0.4.0
28
+
29
+ ### Minor Changes
30
+
31
+ - 73b5ec2: Separate `deliveryToken` and `previewToken`, and stop reading the preview
32
+ token from a `NEXT_PUBLIC_` variable.
33
+
34
+ ### The preview token can no longer leak into a client bundle
35
+
36
+ `readEnvSettings` used to fall back to
37
+ `NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN`. Next.js inlines
38
+ `NEXT_PUBLIC_` variables into client bundles by string-replacing the literal
39
+ `process.env.NEXT_PUBLIC_…` accesses this package ships, so a project that
40
+ set that variable had its preview token baked into public JavaScript —
41
+ whether or not any client code ever requested preview content. Importing the
42
+ library was enough.
43
+
44
+ The preview token is now resolved **only** from the unprefixed
45
+ `CONTENTFUL_PREVIEW_ACCESS_TOKEN`. In Next.js that name is still readable on
46
+ the server, so server-side preview keeps working; the migration is to drop
47
+ the `NEXT_PUBLIC_` prefix from that one variable. The space, environment and
48
+ delivery token keep their client-safe names — a delivery token is read-only
49
+ and commonly public.
50
+
51
+ If your app genuinely needs preview in the browser, pass `previewToken`
52
+ explicitly.
53
+
54
+ ### `deliveryToken` and `previewToken`
55
+
56
+ A single `token` could not serve both modes: it applied to whichever mode was
57
+ active, so a factory configured with one sent the wrong token as soon as a
58
+ call passed `preview: true` — and there was no way to configure both
59
+ explicitly at all.
60
+
61
+ ```ts
62
+ export const fetchContentful = createFetchContentful({
63
+ deliveryToken: process.env.CONTENTFUL_ACCESS_TOKEN,
64
+ previewToken: process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN,
65
+ });
66
+
67
+ await fetchContentful(QUERY); // → deliveryToken
68
+ await fetchContentful(QUERY, { preview: true }); // → previewToken
69
+ ```
70
+
71
+ `token` still works and still applies to the active mode, so existing code is
72
+ unaffected, but it is deprecated in favour of the two specific options.
73
+ `readEnvSettings()` gains `deliveryToken`; its `token` field remains as a
74
+ deprecated alias of the same value.
75
+
76
+ The GraphQL endpoint is unchanged — unlike Contentful's REST APIs, preview
77
+ uses the same `graphql.contentful.com` host and is selected by the token plus
78
+ the injected `preview: true` argument. (The REST locale lookup already
79
+ switched between `cdn.` and `preview.` hosts correctly.)
80
+
3
81
  ## 0.3.0
4
82
 
5
83
  ### Minor Changes
package/README.md CHANGED
@@ -22,7 +22,7 @@ yarn add @fourtwelvelabs/fetch-contentful graphql
22
22
 
23
23
  Settings resolve in this order — first hit wins, per setting:
24
24
 
25
- 1. **Per-call options** — `fetchContentful(query, { space, token, ... })`
25
+ 1. **Per-call options** — `fetchContentful(query, { space, deliveryToken, ... })`
26
26
  2. **Factory defaults** — set once with `createFetchContentful` (below)
27
27
  3. **Environment variables** — framework-neutral names first, then `NEXT_PUBLIC_`-prefixed equivalents
28
28
 
@@ -34,14 +34,22 @@ If `space` or the appropriate token can't be resolved, the promise rejects with
34
34
  | --- | --- | --- |
35
35
  | `space` | `CONTENTFUL_SPACE_ID` | `NEXT_PUBLIC_CONTENTFUL_SPACE_ID` |
36
36
  | `environment` | `CONTENTFUL_ENVIRONMENT` | `NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT` |
37
- | Delivery token | `CONTENTFUL_ACCESS_TOKEN` | `NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN` |
38
- | Preview token | `CONTENTFUL_PREVIEW_ACCESS_TOKEN` | `NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN` |
37
+ | `deliveryToken` | `CONTENTFUL_ACCESS_TOKEN` | `NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN` |
38
+ | `previewToken` | `CONTENTFUL_PREVIEW_ACCESS_TOKEN` | **none, by design** |
39
39
 
40
40
  There is no environment variable for the locale: it defaults to **`en-US`**, and a project that wants a different one sets it once with `createFetchContentful` (below).
41
41
 
42
42
  The library reads the already-populated `process.env`; loading `.env` / `.env.local` files from disk is your platform's job (Next.js, Vite, dotenv all do this), so precedence between those files always matches your framework's rules.
43
43
 
44
- **In a Next.js project you don't need both names.** Set only the `NEXT_PUBLIC_` variant and it works everywhere: the server reads it like any other env var (the prefix only controls *client* exposure), and client bundles get it because the library reads these variables with literal `process.env.NEXT_PUBLIC_...` accesses — the exact pattern Next's build-time inliner string-replaces. Server-only projects (or any other framework) should use the neutral names. Think twice before exposing tokens to the browser at all: Contentful delivery tokens are read-only and commonly made public, but preview tokens should stay server-side.
44
+ **In a Next.js project you don't need both names.** Set only the `NEXT_PUBLIC_` variant and it works everywhere: the server reads it like any other env var (the prefix only controls *client* exposure), and client bundles get it because the library reads these variables with literal `process.env.NEXT_PUBLIC_...` accesses — the exact pattern Next's build-time inliner string-replaces. Server-only projects (or any other framework) should use the neutral names. This applies to the space, the environment and the delivery token; the preview token has no client-safe name, for the reason below.
45
+
46
+ ### The preview token is never read from a `NEXT_PUBLIC_` variable
47
+
48
+ Next.js exposes `NEXT_PUBLIC_` variables to the browser by string-replacing literal `process.env.NEXT_PUBLIC_…` accesses at build time. This library contains such accesses, so if it read the preview token from a prefixed name, setting that variable would bake the token into your public JavaScript — whether or not any client code ever asked for preview content. Importing the library would be enough.
49
+
50
+ A delivery token is read-only and commonly public. A **preview token reads unpublished content**, so it is resolved only from the unprefixed `CONTENTFUL_PREVIEW_ACCESS_TOKEN`. In Next.js that name is still readable on the server — the prefix only controls *client* exposure — so server-side preview (Draft Mode, route handlers, server components) works unchanged.
51
+
52
+ If you have decided that your app genuinely needs preview in the browser, pass `previewToken` explicitly. Shipping it is then your deliberate choice rather than something the library did on your behalf.
45
53
 
46
54
  ### Project-level defaults with `createFetchContentful`
47
55
 
@@ -56,6 +64,10 @@ export const fetchContentful = createFetchContentful({
56
64
  locale: 'de-DE', // override the 'en-US' default for the whole project
57
65
  retries: 3,
58
66
  });
67
+
68
+ // Configure both tokens once, and every call picks the right one:
69
+ // fetchContentful(QUERY) → deliveryToken
70
+ // fetchContentful(QUERY, { preview: true }) → previewToken
59
71
  ```
60
72
 
61
73
  ```ts
@@ -209,7 +221,8 @@ fetchContentful(query, {
209
221
  preview: false, // default: false
210
222
  locale: 'en-US', // default: 'en-US'. null sends no locale at all
211
223
  validateLocale: true, // reject with LOCALE if locale isn't configured
212
- token: '...', // override the env-derived token
224
+ deliveryToken: '...', // CDA token, used when preview is false
225
+ previewToken: '...', // CPA token, used when preview is true
213
226
 
214
227
  // Request behavior
215
228
  variables: { slug: 'home' },
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
2
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
3
3
  import { resolve, dirname, relative, isAbsolute, sep, join } from 'path';
4
4
  import { getIntrospectionQuery, printSchema, buildClientSchema } from 'graphql';
5
5
 
@@ -12,10 +12,14 @@ function readEnvSettings() {
12
12
  return {
13
13
  space: void 0,
14
14
  environment: void 0,
15
+ deliveryToken: void 0,
15
16
  token: void 0,
16
17
  previewToken: void 0
17
18
  };
18
19
  }
20
+ const deliveryToken = orUndefined(
21
+ process.env.CONTENTFUL_ACCESS_TOKEN || process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN
22
+ );
19
23
  return {
20
24
  space: orUndefined(
21
25
  process.env.CONTENTFUL_SPACE_ID || process.env.NEXT_PUBLIC_CONTENTFUL_SPACE_ID
@@ -23,12 +27,10 @@ function readEnvSettings() {
23
27
  environment: orUndefined(
24
28
  process.env.CONTENTFUL_ENVIRONMENT || process.env.NEXT_PUBLIC_CONTENTFUL_ENVIRONMENT
25
29
  ),
26
- token: orUndefined(
27
- process.env.CONTENTFUL_ACCESS_TOKEN || process.env.NEXT_PUBLIC_CONTENTFUL_ACCESS_TOKEN
28
- ),
29
- previewToken: orUndefined(
30
- process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN || process.env.NEXT_PUBLIC_CONTENTFUL_PREVIEW_ACCESS_TOKEN
31
- )
30
+ deliveryToken,
31
+ token: deliveryToken,
32
+ // No `NEXT_PUBLIC_` fallback: see the note at the top of this file.
33
+ previewToken: orUndefined(process.env.CONTENTFUL_PREVIEW_ACCESS_TOKEN)
32
34
  };
33
35
  }
34
36
 
@@ -51,7 +53,8 @@ var VALUE_FLAGS = [
51
53
  "tada-output",
52
54
  "graphql-file",
53
55
  "tsconfig",
54
- "cwd"
56
+ "cwd",
57
+ "env-file"
55
58
  ];
56
59
  function isBooleanFlag(name) {
57
60
  return BOOLEAN_FLAGS.includes(name);
@@ -156,6 +159,55 @@ function formatBytes(bytes) {
156
159
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} kB`;
157
160
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
158
161
  }
162
+ var ENV_FILES = [".env.local", ".env"];
163
+ var ASSIGNMENT = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/;
164
+ function parseEnvFile(source) {
165
+ const values = {};
166
+ for (const rawLine of source.split(/\r?\n/)) {
167
+ const line = rawLine.trim();
168
+ if (line === "" || line.startsWith("#")) continue;
169
+ const match = ASSIGNMENT.exec(line);
170
+ if (!match) continue;
171
+ const key = match[1];
172
+ let value = match[2].trim();
173
+ const quote = value[0];
174
+ if ((quote === '"' || quote === "'") && value.length > 1 && value.endsWith(quote)) {
175
+ value = value.slice(1, -1);
176
+ if (quote === '"') value = value.replace(/\\n/g, "\n");
177
+ } else {
178
+ const comment = value.search(/\s#/);
179
+ if (comment !== -1) value = value.slice(0, comment).trimEnd();
180
+ }
181
+ values[key] = value;
182
+ }
183
+ return values;
184
+ }
185
+ function loadEnvFiles(directory, explicit) {
186
+ const load = { directory, files: [], applied: [] };
187
+ if (explicit !== void 0 && !existsSync(resolve(directory, explicit))) {
188
+ throw new CliError(`No env file at ${explicit}.`);
189
+ }
190
+ for (const candidate of explicit === void 0 ? ENV_FILES : [explicit]) {
191
+ const path = resolve(directory, candidate);
192
+ if (!existsSync(path)) continue;
193
+ load.files.push(candidate);
194
+ for (const [key, value] of Object.entries(
195
+ parseEnvFile(readFileSync(path, "utf8"))
196
+ )) {
197
+ if (!process.env[key]) {
198
+ process.env[key] = value;
199
+ load.applied.push(key);
200
+ }
201
+ }
202
+ }
203
+ return load;
204
+ }
205
+ function describeEnvFiles(load) {
206
+ if (load.files.length === 0) {
207
+ return `No ${ENV_FILES.join(" or ")} file was found in ${load.directory}, so only the shell environment was read. Point at one with --env-file if it lives elsewhere.`;
208
+ }
209
+ return `Read ${load.files.join(" and ")}; anything already set in the environment takes precedence over them.`;
210
+ }
159
211
  var PACKAGE_NAME = "@fourtwelvelabs/fetch-contentful";
160
212
  function relativeSpecifier(fromFile, toFile) {
161
213
  const path = relative(dirname(fromFile), toFile).split(sep).join("/");
@@ -666,14 +718,17 @@ Other:
666
718
  --dry-run Show what would change; write nothing
667
719
  --force Overwrite an existing graphql file (tada-init)
668
720
  --cwd <path> Run against another directory
721
+ --env-file <path> Read this file instead of .env.local / .env
669
722
  -h, --help Show this help
670
723
 
671
- The NEXT_PUBLIC_-prefixed variable names are read as fallbacks, exactly as
672
- the library reads them at runtime.`;
673
- function resolveConfig(args) {
724
+ Configuration is read from .env.local, then .env, then the shell \u2014 anything
725
+ already exported wins over a file, and flags win over everything. The
726
+ NEXT_PUBLIC_-prefixed names are accepted as fallbacks, exactly as the
727
+ library reads them at runtime.`;
728
+ function resolveConfig(args, envFiles) {
674
729
  const env = readEnvSettings();
675
730
  const space = args.values.space ?? env.space;
676
- const token = args.values.token ?? env.token;
731
+ const token = args.values.token ?? env.deliveryToken;
677
732
  const environment = args.values.environment ?? env.environment ?? "master";
678
733
  const missing = [];
679
734
  if (!space) {
@@ -691,7 +746,8 @@ function resolveConfig(args) {
691
746
  `Missing required Contentful configuration:
692
747
  - ${missing.join(
693
748
  "\n - "
694
- )}`
749
+ )}
750
+ ${describeEnvFiles(envFiles)}`
695
751
  );
696
752
  }
697
753
  return { space, environment, token };
@@ -842,8 +898,9 @@ function reportPeers(paths, io) {
842
898
  io.stdout(` ${installCommand(detectPackageManager(paths.root), missing)}`);
843
899
  }
844
900
  async function tadaInit(args, io) {
845
- const config = resolveConfig(args);
846
901
  const paths = resolvePaths(args, io);
902
+ const envFiles = loadEnvFiles(paths.root, args.values["env-file"]);
903
+ const config = resolveConfig(args, envFiles);
847
904
  const dryRun = args.flags.has("dry-run");
848
905
  const tsconfigAction = planTsconfig(paths);
849
906
  const sdl = await fetchSchemaSdl({ ...config, fetch: io.fetch });
@@ -881,8 +938,9 @@ async function tadaInit(args, io) {
881
938
  return 0;
882
939
  }
883
940
  async function tadaRefresh(args, io) {
884
- const config = resolveConfig(args);
885
941
  const paths = resolvePaths(args, io);
942
+ const envFiles = loadEnvFiles(paths.root, args.values["env-file"]);
943
+ const config = resolveConfig(args, envFiles);
886
944
  const dryRun = args.flags.has("dry-run");
887
945
  const sdl = await fetchSchemaSdl({ ...config, fetch: io.fetch });
888
946
  const action = planSchema(sdl, paths);
@@ -916,7 +974,10 @@ async function dispatch(argv, io) {
916
974
  }
917
975
  function knownTokens(argv) {
918
976
  const env = readEnvSettings();
919
- const tokens = [env.token, env.previewToken];
977
+ const tokens = [
978
+ env.deliveryToken,
979
+ env.previewToken
980
+ ];
920
981
  for (const [index, argument] of argv.entries()) {
921
982
  if (argument.startsWith("--token=")) tokens.push(argument.slice(8));
922
983
  else if (argument === "--token") tokens.push(argv[index + 1]);