primitive-app 3.1.0 → 3.2.0-alpha.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.
@@ -35,8 +35,46 @@ export interface CoreEnvironmentEntry {
35
35
  apiUrl: string;
36
36
  appId?: string;
37
37
  appName?: string;
38
+ /**
39
+ * The app's web counterpart for this environment, as a normalized origin
40
+ * (#2982). Read through `normalizeWebOrigin`, so a value this reader carries
41
+ * is always an origin — see that function for why the typed read other
42
+ * fields get is not enough here.
43
+ */
44
+ webUrl?: string;
38
45
  description?: string;
39
46
  }
47
+ /**
48
+ * The app's web counterpart as a normalized ORIGIN, or undefined when the
49
+ * value cannot be one (#2982).
50
+ *
51
+ * Unlike `appId`/`appName`, a typed read is not enough. This value is not
52
+ * merely carried: the Swift app points its emailed sign-in link at
53
+ * `<webUrl>/oauth/callback` and trusts incoming universal links from the same
54
+ * origin, and BOTH jobs need an origin —
55
+ *
56
+ * - https except loopback, the rule the server applies to every redirect URI
57
+ * (`src/auth/redirect-uri.ts`): an origin that cannot be allow-listed turns
58
+ * every sign-in request into a 400, and a plain-http link would carry the
59
+ * magic token in clear;
60
+ * - no path, query or fragment: the web client serves its callback on its own
61
+ * origin plus `/oauth/callback`, which is also the exact path the
62
+ * `apple-app-site-association` component claims;
63
+ * - no credentials, which have no business in a link that goes out by email.
64
+ *
65
+ * `primitive env add --web-url` and the project-config validation reject those
66
+ * shapes with a field-named error, but neither is on the path from a
67
+ * hand-edited `.primitive/config.json` to an Xcode build: the Swift template's
68
+ * pre-build script reads the file directly. So every reader that feeds
69
+ * `primitive.json` applies the contract, and a value that misses it is read as
70
+ * "this environment has no web counterpart" — code-only email, no trusted
71
+ * origin, nothing silently pointed at the wrong URL.
72
+ *
73
+ * The message-producing twin of this rule lives in `cli/src/lib/web-url.ts`;
74
+ * this file cannot import it (see COPYABILITY above), and a test pins that the
75
+ * two agree.
76
+ */
77
+ export declare function normalizeWebOrigin(value: unknown): string | undefined;
40
78
  export interface CoreProjectConfig {
41
79
  version: number;
42
80
  defaultEnvironment?: string;
@@ -51,6 +89,8 @@ export interface ResolvedPrimitiveEnv {
51
89
  wsUrl: string;
52
90
  appId?: string;
53
91
  appName?: string;
92
+ /** The app's web counterpart for the SELECTED environment, if it has one. */
93
+ webUrl?: string;
54
94
  description?: string;
55
95
  source: PrimitiveEnvSource;
56
96
  configPath: string;
@@ -65,6 +65,6 @@ const observer = new MutationObserver(async (mutations, obs) => {
65
65
  });
66
66
 
67
67
  observer.observe(document.body, { childList: true, subtree: true });
68
- `},transformIndexHtml(e){if(!h||!d)return e;let t=`<style data-primitive-devtools-sonner>${g}</style>`;return e.replace(`</body>`,`${t}\n<script type="module" src="/@id/__x00__virtual:primitive-devtools-init"><\/script>\n</body>`)},hotUpdate({file:e,server:t}){if(e.endsWith(`.primitive-test.ts`)){let e=t.moduleGraph.getModuleById(c);e&&(t.moduleGraph.invalidateModule(e),t.ws.send({type:`full-reload`}))}}}}var f=`.primitive`,p=`config.json`,m=`local.json`,h=`.primitive/config.json`,g=`.primitive/local.json`,_=class extends Error{kind;path;constructor(e,t,n){super(t),this.name=`PrimitiveEnvError`,this.kind=e,this.path=n}};function v(e){return e.startsWith(`https://`)?`wss://`+e.slice(8):e.startsWith(`http://`)?`ws://`+e.slice(7):e}function y(e={}){let n=(e.env??process.env).PRIMITIVE_PROJECT_CONFIG;if(n){let e=(0,a.resolve)(n);return(0,t.existsSync)(e)?e:null}let r=(0,a.resolve)(e.cwd??process.cwd());for(;;){let e=(0,a.join)(r,f,p);if((0,t.existsSync)(e))return e;let n=(0,a.dirname)(r);if(n===r)return null;r=n}}function b(e){let t=(0,a.dirname)(e);return(0,a.basename)(t)===`.primitive`?(0,a.dirname)(t):t}function x(e){return(0,a.join)((0,a.dirname)(e),m)}function S(e){if(!(0,t.existsSync)(e))throw new _(`missing-config`,`No ${h} found at ${e}. Run 'primitive init' to create one.`,e);let n;try{n=(0,t.readFileSync)(e,`utf-8`)}catch(t){throw new _(`malformed-config`,`Failed to read ${h} (${e}): ${t.message}`,e)}let r;try{r=JSON.parse(n)}catch(t){throw new _(`malformed-config`,`Failed to parse ${h} (${e}): ${t.message}`,e)}if(!r||typeof r!=`object`||Array.isArray(r))throw new _(`malformed-config`,`${h} (${e}) must contain a JSON object at the top level.`,e);let i=r;if(typeof i.version!=`number`||!Number.isInteger(i.version))throw new _(`malformed-config`,`${h} (${e}) is missing required integer field "version".`,e);if(i.version!==1)throw new _(`unsupported-version`,`${h} (${e}) has version ${i.version}, but this tool understands version 1.`,e);if(!i.environments||typeof i.environments!=`object`||Array.isArray(i.environments))throw new _(`malformed-config`,`${h} (${e}) is missing required "environments" object.`,e);let a={};for(let[t,n]of Object.entries(i.environments)){if(!n||typeof n!=`object`||Array.isArray(n))throw new _(`malformed-config`,`Environment "${t}" must be an object in ${h} (${e}).`,e);let r=n;if(typeof r.apiUrl!=`string`||!r.apiUrl)throw new _(`malformed-config`,`Environment "${t}" must have a non-empty "apiUrl" string in ${h} (${e}).`,e);a[t]={apiUrl:r.apiUrl.replace(/\/$/,``),appId:typeof r.appId==`string`?r.appId:void 0,appName:typeof r.appName==`string`?r.appName:void 0,description:typeof r.description==`string`?r.description:void 0}}return{version:i.version,defaultEnvironment:typeof i.defaultEnvironment==`string`&&i.defaultEnvironment?i.defaultEnvironment:void 0,environments:a}}function C(e){let n=x(e);if(!(0,t.existsSync)(n))return null;let r=e=>{throw new _(`corrupt-local-state`,`${g} (${n}) is unreadable: ${e}. Delete the file or re-run 'primitive env use <name>'.`,n)},i;try{i=JSON.parse((0,t.readFileSync)(n,`utf-8`))}catch(e){return r(e.message)}if(!i||typeof i!=`object`||Array.isArray(i))return r(`expected a JSON object`);let a=i.selectedEnvironment;return a==null||a===``?null:typeof a==`string`?a:r(`"selectedEnvironment" must be a string`)}function w(e,t){let n=Object.keys(e.environments),r=n.join(`, `)||`(none)`,i=t.configPath?`${h} (${t.configPath})`:h,a=(n,a,o)=>{if(!e.environments[n])throw new _(`unknown-environment`,`Environment "${n}" is not defined in ${i} (selected ${o}). Available: ${r}`,t.configPath);return{name:n,source:a}};if(t.explicitEnvName)return a(t.explicitEnvName,`explicit`,`explicitly`);if(t.envVarName)return a(t.envVarName,`env-var`,`via PRIMITIVE_ENV`);if(t.localSelection)return a(t.localSelection,`local`,`in ${g}`);if(e.defaultEnvironment)return a(e.defaultEnvironment,`default`,`as "defaultEnvironment"`);if(n.length===1)return{name:n[0],source:`sole`};throw n.length===0?new _(`no-selection`,`${i} has no environments defined. Run 'primitive env add <name>' or 'primitive init'.`,t.configPath):new _(`no-selection`,`No environment selected. Run 'primitive env use <name>', set "defaultEnvironment" in ${i}, or export PRIMITIVE_ENV. Available: ${r}`,t.configPath)}function T(e={}){let t=e.env??process.env,n=e.configPath??y({cwd:e.cwd,env:t});if(!n)throw new _(`missing-config`,`No ${h} found in ${e.cwd??process.cwd()} or any parent directory. Run 'primitive init' to create one, or 'primitive env add <name>' to add an environment.`);let r=S(n),{name:i,source:a}=w(r,{explicitEnvName:e.explicitEnvName||null,envVarName:t.PRIMITIVE_ENV||null,localSelection:C(n),configPath:n}),o=r.environments[i];if(e.requireAppId&&!o.appId)throw new _(`missing-app-id`,`Environment "${i}" in ${h} (${n}) has no "appId", which this build needs. Add one with 'primitive env add ${i} --api-url ${o.apiUrl} --app-id <id>' or edit the config.`,n);return{name:i,apiUrl:o.apiUrl,wsUrl:v(o.apiUrl),appId:o.appId,appName:o.appName,description:o.description,source:a,configPath:n,localStatePath:x(n),projectRoot:b(n)}}var E=[`VITE_APP_ID`,`VITE_API_URL`,`VITE_WS_URL`,`VITE_APP_NAME`],D=`VITE_EXPECTED_PRIMITIVE_ENV`,O=new Map;function k(e,t){let n=process.env[e];n!==void 0&&n!==``&&O.get(e)!==n||(process.env[e]=t,O.set(e,t))}function A(e={}){return T({cwd:e.root,env:e.env,explicitEnvName:e.primitiveEnv??null,requireAppId:e.requireAppId})}function j(e,t,n){let r=e=>n.includes(e)?` (overridden)`:``,i=[`[primitive-env] Primitive environment: ${e.name} (selected: ${e.source})`,`[primitive-env] apiUrl: ${t.apiUrl}${r(`VITE_API_URL`)}`,`[primitive-env] wsUrl: ${t.wsUrl}${r(`VITE_WS_URL`)}`,`[primitive-env] appId: ${t.appId??`(unset)`}${r(`VITE_APP_ID`)}`,`[primitive-env] appName: ${t.appName??`(unset)`}${r(`VITE_APP_NAME`)}`,`[primitive-env] config: ${e.configPath}`];return n.length>0&&i.push(`[primitive-env] overridden by the environment: ${n.join(`, `)}`),i.join(`
69
- `)}function M(e,t){let n=e?.envDir;return n===!1?null:typeof n==`string`&&n!==``?(0,a.resolve)(t,n):t}function N(e,t){let n=process.env[D];if(n!==void 0&&n!==``)return{value:n,where:`the process environment`};if(t===null)return null;for(let n of[`.env.${e}.local`,`.env.${e}`,`.env.local`,`.env`]){let e=R((0,a.join)(t,n))[D];if(e!==void 0)return{value:e,where:n}}return null}function P(e,t,n){return[`[primitive-env] Wrong pair: vite mode "${e}" is declared to run against Primitive environment "${t.value}", but "${n.name}" resolved.`,`[primitive-env] expected: ${t.value} (${D} in ${t.where})`,`[primitive-env] resolved: ${n.name} (selected: ${n.source})`,`[primitive-env] config: ${n.configPath}`,``,`Run the pair you meant — PRIMITIVE_ENV=${t.value} <command> --mode ${e}, or 'primitive env use ${t.value}' — or change/remove ${D} in ${t.where}.`,``,`A mode declares this when its .env.${e} carries values coupled to one backend, so continuing would run this environment's identity with another's configuration.`].join(`
70
- `)}function F(e,t,n){let r=N(e,t);if(r&&r.value!==``&&r.value!==n.name)throw Error(P(e,r,n))}function I(e={}){let t=null,n=null,r,i=[],a=!1,o=null;return{name:`primitive-env`,enforce:`pre`,config(s,c){a=c?.command===`serve`,o=null;let l=s?.root??process.cwd(),u=c?.mode??`development`,d=L(u,l),f={};for(let e of E){let t=process.env[e],n=t!==void 0&&O.get(e)!==t?t:d[e];n!==void 0&&n!==``&&(f[e]=n)}let p;try{p=T({cwd:l,explicitEnvName:e.primitiveEnv??null,requireAppId:!f.VITE_APP_ID})}catch(e){if(e instanceof _&&e.kind===`missing-config`&&f.VITE_APP_ID&&f.VITE_API_URL)return console.warn(`[primitive-env] No .primitive/config.json found; using VITE_APP_ID / VITE_API_URL from the environment. Run 'primitive init' to make the project the source of truth.`),r=f.VITE_APP_NAME,{};throw e}if(!(f.VITE_APP_ID&&f.VITE_API_URL)){let e=M(s,l);F(u,e,p),o={root:l,mode:u,dir:e}}t=p,r=f.VITE_APP_NAME??p.appName;let m={VITE_APP_ID:p.appId,VITE_API_URL:p.apiUrl,VITE_WS_URL:p.wsUrl,VITE_APP_NAME:p.appName},h={"import.meta.env.VITE_PRIMITIVE_ENV":JSON.stringify(p.name)},g=[];for(let e of E){if(f[e]!==void 0){g.push(e);continue}let t=m[e];t!==void 0&&(h[`import.meta.env.${e}`]=JSON.stringify(t),k(e,t))}return k(`VITE_PRIMITIVE_ENV`,p.name),i=g,n={apiUrl:f.VITE_API_URL??p.apiUrl,wsUrl:f.VITE_WS_URL??p.wsUrl,appId:f.VITE_APP_ID??p.appId,appName:r},e.logResolved!==!1&&console.log(j(p,n,g)),g.length>0&&console.warn(`[primitive-env] Overridden by an explicit value in .env or the shell: ${g.join(`, `)}. Remove it to use .primitive/config.json (deploys reject these outright).`),{define:h}},configResolved(e){if(!o||!t)return;let n=M(e,o.root),r=e?.mode??o.mode;n===o.dir&&r===o.mode||(F(r,n,t),o={root:o.root,mode:r,dir:n})},transformIndexHtml(){if(!a||!t||!n)return[];let e=`%c[primitive] env=${t.name} api=${n.apiUrl} app=${n.appId??`(unset)`} (${t.configPath})`+(i.length>0?` overridden: ${i.join(`, `)}`:``);return[{tag:`script`,injectTo:`head`,children:`console.info(${JSON.stringify(e)}, "color:#888");`}]},appName(){return r},resolved(){return t}}}function L(e,t){let n=[`.env`,`.env.local`,`.env.${e}`,`.env.${e}.local`],r={};for(let e of n)Object.assign(r,R((0,a.join)(t,e)));return r}function R(e){if(!(0,t.existsSync)(e))return{};let n={};for(let r of(0,t.readFileSync)(e,`utf-8`).split(/\r?\n/)){let e=r.trim();if(!e||e.startsWith(`#`))continue;let t=e.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);if(!t)continue;let i=t[2].trim(),a=i[0],o=a===`"`||a===`'`||a==="`"?i.indexOf(a,1):-1;n[t[1]]=o===-1?i.replace(/\s+#.*$/,``).trim():i.slice(1,o)}return n}exports.PrimitiveEnvError=_,exports.primitiveDevTools=d,exports.primitiveEnv=I,exports.resolvePrimitiveEnv=A;
68
+ `},transformIndexHtml(e){if(!h||!d)return e;let t=`<style data-primitive-devtools-sonner>${g}</style>`;return e.replace(`</body>`,`${t}\n<script type="module" src="/@id/__x00__virtual:primitive-devtools-init"><\/script>\n</body>`)},hotUpdate({file:e,server:t}){if(e.endsWith(`.primitive-test.ts`)){let e=t.moduleGraph.getModuleById(c);e&&(t.moduleGraph.invalidateModule(e),t.ws.send({type:`full-reload`}))}}}}var f=`.primitive`,p=`config.json`,m=`local.json`,h=`.primitive/config.json`,g=`.primitive/local.json`,_=class extends Error{kind;path;constructor(e,t,n){super(t),this.name=`PrimitiveEnvError`,this.kind=e,this.path=n}};function v(e){if(typeof e!=`string`)return;let t=e.trim();if(!t)return;let n;try{n=new URL(t)}catch{return}if(!(n.protocol!==`https:`&&n.protocol!==`http:`)&&!(n.protocol===`http:`&&n.hostname!==`localhost`&&n.hostname!==`127.0.0.1`)&&!(n.username||n.password)&&!(n.pathname!==`/`&&n.pathname!==``)&&!(n.search||n.hash))return n.origin}function y(e){return e.startsWith(`https://`)?`wss://`+e.slice(8):e.startsWith(`http://`)?`ws://`+e.slice(7):e}function b(e={}){let n=(e.env??process.env).PRIMITIVE_PROJECT_CONFIG;if(n){let e=(0,a.resolve)(n);return(0,t.existsSync)(e)?e:null}let r=(0,a.resolve)(e.cwd??process.cwd());for(;;){let e=(0,a.join)(r,f,p);if((0,t.existsSync)(e))return e;let n=(0,a.dirname)(r);if(n===r)return null;r=n}}function x(e){let t=(0,a.dirname)(e);return(0,a.basename)(t)===`.primitive`?(0,a.dirname)(t):t}function S(e){return(0,a.join)((0,a.dirname)(e),m)}function C(e){if(!(0,t.existsSync)(e))throw new _(`missing-config`,`No ${h} found at ${e}. Run 'primitive init' to create one.`,e);let n;try{n=(0,t.readFileSync)(e,`utf-8`)}catch(t){throw new _(`malformed-config`,`Failed to read ${h} (${e}): ${t.message}`,e)}let r;try{r=JSON.parse(n)}catch(t){throw new _(`malformed-config`,`Failed to parse ${h} (${e}): ${t.message}`,e)}if(!r||typeof r!=`object`||Array.isArray(r))throw new _(`malformed-config`,`${h} (${e}) must contain a JSON object at the top level.`,e);let i=r;if(typeof i.version!=`number`||!Number.isInteger(i.version))throw new _(`malformed-config`,`${h} (${e}) is missing required integer field "version".`,e);if(i.version!==1)throw new _(`unsupported-version`,`${h} (${e}) has version ${i.version}, but this tool understands version 1.`,e);if(!i.environments||typeof i.environments!=`object`||Array.isArray(i.environments))throw new _(`malformed-config`,`${h} (${e}) is missing required "environments" object.`,e);let a={};for(let[t,n]of Object.entries(i.environments)){if(!n||typeof n!=`object`||Array.isArray(n))throw new _(`malformed-config`,`Environment "${t}" must be an object in ${h} (${e}).`,e);let r=n;if(typeof r.apiUrl!=`string`||!r.apiUrl)throw new _(`malformed-config`,`Environment "${t}" must have a non-empty "apiUrl" string in ${h} (${e}).`,e);a[t]={apiUrl:r.apiUrl.replace(/\/$/,``),appId:typeof r.appId==`string`?r.appId:void 0,appName:typeof r.appName==`string`?r.appName:void 0,webUrl:v(r.webUrl),description:typeof r.description==`string`?r.description:void 0}}return{version:i.version,defaultEnvironment:typeof i.defaultEnvironment==`string`&&i.defaultEnvironment?i.defaultEnvironment:void 0,environments:a}}function w(e){let n=S(e);if(!(0,t.existsSync)(n))return null;let r=e=>{throw new _(`corrupt-local-state`,`${g} (${n}) is unreadable: ${e}. Delete the file or re-run 'primitive env use <name>'.`,n)},i;try{i=JSON.parse((0,t.readFileSync)(n,`utf-8`))}catch(e){return r(e.message)}if(!i||typeof i!=`object`||Array.isArray(i))return r(`expected a JSON object`);let a=i.selectedEnvironment;return a==null||a===``?null:typeof a==`string`?a:r(`"selectedEnvironment" must be a string`)}function T(e,t){let n=Object.keys(e.environments),r=n.join(`, `)||`(none)`,i=t.configPath?`${h} (${t.configPath})`:h,a=(n,a,o)=>{if(!e.environments[n])throw new _(`unknown-environment`,`Environment "${n}" is not defined in ${i} (selected ${o}). Available: ${r}`,t.configPath);return{name:n,source:a}};if(t.explicitEnvName)return a(t.explicitEnvName,`explicit`,`explicitly`);if(t.envVarName)return a(t.envVarName,`env-var`,`via PRIMITIVE_ENV`);if(t.localSelection)return a(t.localSelection,`local`,`in ${g}`);if(e.defaultEnvironment)return a(e.defaultEnvironment,`default`,`as "defaultEnvironment"`);if(n.length===1)return{name:n[0],source:`sole`};throw n.length===0?new _(`no-selection`,`${i} has no environments defined. Run 'primitive env add <name>' or 'primitive init'.`,t.configPath):new _(`no-selection`,`No environment selected. Run 'primitive env use <name>', set "defaultEnvironment" in ${i}, or export PRIMITIVE_ENV. Available: ${r}`,t.configPath)}function E(e={}){let t=e.env??process.env,n=e.configPath??b({cwd:e.cwd,env:t});if(!n)throw new _(`missing-config`,`No ${h} found in ${e.cwd??process.cwd()} or any parent directory. Run 'primitive init' to create one, or 'primitive env add <name>' to add an environment.`);let r=C(n),{name:i,source:a}=T(r,{explicitEnvName:e.explicitEnvName||null,envVarName:t.PRIMITIVE_ENV||null,localSelection:w(n),configPath:n}),o=r.environments[i];if(e.requireAppId&&!o.appId)throw new _(`missing-app-id`,`Environment "${i}" in ${h} (${n}) has no "appId", which this build needs. Add one with 'primitive env add ${i} --api-url ${o.apiUrl} --app-id <id>' or edit the config.`,n);return{name:i,apiUrl:o.apiUrl,wsUrl:y(o.apiUrl),appId:o.appId,appName:o.appName,webUrl:o.webUrl,description:o.description,source:a,configPath:n,localStatePath:S(n),projectRoot:x(n)}}var D=[`VITE_APP_ID`,`VITE_API_URL`,`VITE_WS_URL`,`VITE_APP_NAME`],O=`VITE_EXPECTED_PRIMITIVE_ENV`,k=new Map;function A(e,t){let n=process.env[e];n!==void 0&&n!==``&&k.get(e)!==n||(process.env[e]=t,k.set(e,t))}function j(e={}){return E({cwd:e.root,env:e.env,explicitEnvName:e.primitiveEnv??null,requireAppId:e.requireAppId})}function M(e,t,n){let r=e=>n.includes(e)?` (overridden)`:``,i=[`[primitive-env] Primitive environment: ${e.name} (selected: ${e.source})`,`[primitive-env] apiUrl: ${t.apiUrl}${r(`VITE_API_URL`)}`,`[primitive-env] wsUrl: ${t.wsUrl}${r(`VITE_WS_URL`)}`,`[primitive-env] appId: ${t.appId??`(unset)`}${r(`VITE_APP_ID`)}`,`[primitive-env] appName: ${t.appName??`(unset)`}${r(`VITE_APP_NAME`)}`,`[primitive-env] config: ${e.configPath}`];return n.length>0&&i.push(`[primitive-env] overridden by the environment: ${n.join(`, `)}`),i.join(`
69
+ `)}function N(e,t){let n=e?.envDir;return n===!1?null:typeof n==`string`&&n!==``?(0,a.resolve)(t,n):t}function P(e,t){let n=process.env[O];if(n!==void 0&&n!==``)return{value:n,where:`the process environment`};if(t===null)return null;for(let n of[`.env.${e}.local`,`.env.${e}`,`.env.local`,`.env`]){let e=z((0,a.join)(t,n))[O];if(e!==void 0)return{value:e,where:n}}return null}function F(e,t,n){return[`[primitive-env] Wrong pair: vite mode "${e}" is declared to run against Primitive environment "${t.value}", but "${n.name}" resolved.`,`[primitive-env] expected: ${t.value} (${O} in ${t.where})`,`[primitive-env] resolved: ${n.name} (selected: ${n.source})`,`[primitive-env] config: ${n.configPath}`,``,`Run the pair you meant — PRIMITIVE_ENV=${t.value} <command> --mode ${e}, or 'primitive env use ${t.value}' — or change/remove ${O} in ${t.where}.`,``,`A mode declares this when its .env.${e} carries values coupled to one backend, so continuing would run this environment's identity with another's configuration.`].join(`
70
+ `)}function I(e,t,n){let r=P(e,t);if(r&&r.value!==``&&r.value!==n.name)throw Error(F(e,r,n))}function L(e={}){let t=null,n=null,r,i=[],a=!1,o=null;return{name:`primitive-env`,enforce:`pre`,config(s,c){a=c?.command===`serve`,o=null;let l=s?.root??process.cwd(),u=c?.mode??`development`,d=R(u,l),f={};for(let e of D){let t=process.env[e],n=t!==void 0&&k.get(e)!==t?t:d[e];n!==void 0&&n!==``&&(f[e]=n)}let p;try{p=E({cwd:l,explicitEnvName:e.primitiveEnv??null,requireAppId:!f.VITE_APP_ID})}catch(e){if(e instanceof _&&e.kind===`missing-config`&&f.VITE_APP_ID&&f.VITE_API_URL)return console.warn(`[primitive-env] No .primitive/config.json found; using VITE_APP_ID / VITE_API_URL from the environment. Run 'primitive init' to make the project the source of truth.`),r=f.VITE_APP_NAME,{};throw e}if(!(f.VITE_APP_ID&&f.VITE_API_URL)){let e=N(s,l);I(u,e,p),o={root:l,mode:u,dir:e}}t=p,r=f.VITE_APP_NAME??p.appName;let m={VITE_APP_ID:p.appId,VITE_API_URL:p.apiUrl,VITE_WS_URL:p.wsUrl,VITE_APP_NAME:p.appName},h={"import.meta.env.VITE_PRIMITIVE_ENV":JSON.stringify(p.name)},g=[];for(let e of D){if(f[e]!==void 0){g.push(e);continue}let t=m[e];t!==void 0&&(h[`import.meta.env.${e}`]=JSON.stringify(t),A(e,t))}return A(`VITE_PRIMITIVE_ENV`,p.name),i=g,n={apiUrl:f.VITE_API_URL??p.apiUrl,wsUrl:f.VITE_WS_URL??p.wsUrl,appId:f.VITE_APP_ID??p.appId,appName:r},e.logResolved!==!1&&console.log(M(p,n,g)),g.length>0&&console.warn(`[primitive-env] Overridden by an explicit value in .env or the shell: ${g.join(`, `)}. Remove it to use .primitive/config.json (deploys reject these outright).`),{define:h}},configResolved(e){if(!o||!t)return;let n=N(e,o.root),r=e?.mode??o.mode;n===o.dir&&r===o.mode||(I(r,n,t),o={root:o.root,mode:r,dir:n})},transformIndexHtml(){if(!a||!t||!n)return[];let e=`%c[primitive] env=${t.name} api=${n.apiUrl} app=${n.appId??`(unset)`} (${t.configPath})`+(i.length>0?` overridden: ${i.join(`, `)}`:``);return[{tag:`script`,injectTo:`head`,children:`console.info(${JSON.stringify(e)}, "color:#888");`}]},appName(){return r},resolved(){return t}}}function R(e,t){let n=[`.env`,`.env.local`,`.env.${e}`,`.env.${e}.local`],r={};for(let e of n)Object.assign(r,z((0,a.join)(t,e)));return r}function z(e){if(!(0,t.existsSync)(e))return{};let n={};for(let r of(0,t.readFileSync)(e,`utf-8`).split(/\r?\n/)){let e=r.trim();if(!e||e.startsWith(`#`))continue;let t=e.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);if(!t)continue;let i=t[2].trim(),a=i[0],o=a===`"`||a===`'`||a==="`"?i.indexOf(a,1):-1;n[t[1]]=o===-1?i.replace(/\s+#.*$/,``).trim():i.slice(1,o)}return n}exports.PrimitiveEnvError=_,exports.primitiveDevTools=d,exports.primitiveEnv=L,exports.resolvePrimitiveEnv=j;
@@ -71,9 +71,21 @@ var h = ".primitive", g = "config.json", _ = "local.json", v = ".primitive/confi
71
71
  }
72
72
  };
73
73
  function x(e) {
74
+ if (typeof e != "string") return;
75
+ let t = e.trim();
76
+ if (!t) return;
77
+ let n;
78
+ try {
79
+ n = new URL(t);
80
+ } catch {
81
+ return;
82
+ }
83
+ if (!(n.protocol !== "https:" && n.protocol !== "http:") && !(n.protocol === "http:" && n.hostname !== "localhost" && n.hostname !== "127.0.0.1") && !(n.username || n.password) && !(n.pathname !== "/" && n.pathname !== "") && !(n.search || n.hash)) return n.origin;
84
+ }
85
+ function S(e) {
74
86
  return e.startsWith("https://") ? "wss://" + e.slice(8) : e.startsWith("http://") ? "ws://" + e.slice(7) : e;
75
87
  }
76
- function S(t = {}) {
88
+ function C(t = {}) {
77
89
  let n = (t.env ?? process.env).PRIMITIVE_PROJECT_CONFIG;
78
90
  if (n) {
79
91
  let t = c(n);
@@ -88,14 +100,14 @@ function S(t = {}) {
88
100
  r = n;
89
101
  }
90
102
  }
91
- function C(e) {
103
+ function w(e) {
92
104
  let t = o(e);
93
105
  return a(t) === ".primitive" ? o(t) : t;
94
106
  }
95
- function w(e) {
107
+ function T(e) {
96
108
  return s(o(e), _);
97
109
  }
98
- function T(n) {
110
+ function E(n) {
99
111
  if (!e(n)) throw new b("missing-config", `No ${v} found at ${n}. Run 'primitive init' to create one.`, n);
100
112
  let r;
101
113
  try {
@@ -123,6 +135,7 @@ function T(n) {
123
135
  apiUrl: r.apiUrl.replace(/\/$/, ""),
124
136
  appId: typeof r.appId == "string" ? r.appId : void 0,
125
137
  appName: typeof r.appName == "string" ? r.appName : void 0,
138
+ webUrl: x(r.webUrl),
126
139
  description: typeof r.description == "string" ? r.description : void 0
127
140
  };
128
141
  }
@@ -132,8 +145,8 @@ function T(n) {
132
145
  environments: o
133
146
  };
134
147
  }
135
- function E(n) {
136
- let r = w(n);
148
+ function D(n) {
149
+ let r = T(n);
137
150
  if (!e(r)) return null;
138
151
  let i = (e) => {
139
152
  throw new b("corrupt-local-state", `${y} (${r}) is unreadable: ${e}. Delete the file or re-run 'primitive env use <name>'.`, r);
@@ -147,7 +160,7 @@ function E(n) {
147
160
  let o = a.selectedEnvironment;
148
161
  return o == null || o === "" ? null : typeof o == "string" ? o : i("\"selectedEnvironment\" must be a string");
149
162
  }
150
- function D(e, t) {
163
+ function O(e, t) {
151
164
  let n = Object.keys(e.environments), r = n.join(", ") || "(none)", i = t.configPath ? `${v} (${t.configPath})` : v, a = (n, a, o) => {
152
165
  if (!e.environments[n]) throw new b("unknown-environment", `Environment "${n}" is not defined in ${i} (selected ${o}). Available: ${r}`, t.configPath);
153
166
  return {
@@ -165,53 +178,54 @@ function D(e, t) {
165
178
  };
166
179
  throw n.length === 0 ? new b("no-selection", `${i} has no environments defined. Run 'primitive env add <name>' or 'primitive init'.`, t.configPath) : new b("no-selection", `No environment selected. Run 'primitive env use <name>', set "defaultEnvironment" in ${i}, or export PRIMITIVE_ENV. Available: ${r}`, t.configPath);
167
180
  }
168
- function O(e = {}) {
169
- let t = e.env ?? process.env, n = e.configPath ?? S({
181
+ function k(e = {}) {
182
+ let t = e.env ?? process.env, n = e.configPath ?? C({
170
183
  cwd: e.cwd,
171
184
  env: t
172
185
  });
173
186
  if (!n) throw new b("missing-config", `No ${v} found in ${e.cwd ?? process.cwd()} or any parent directory. Run 'primitive init' to create one, or 'primitive env add <name>' to add an environment.`);
174
- let r = T(n), { name: i, source: a } = D(r, {
187
+ let r = E(n), { name: i, source: a } = O(r, {
175
188
  explicitEnvName: e.explicitEnvName || null,
176
189
  envVarName: t.PRIMITIVE_ENV || null,
177
- localSelection: E(n),
190
+ localSelection: D(n),
178
191
  configPath: n
179
192
  }), o = r.environments[i];
180
193
  if (e.requireAppId && !o.appId) throw new b("missing-app-id", `Environment "${i}" in ${v} (${n}) has no "appId", which this build needs. Add one with 'primitive env add ${i} --api-url ${o.apiUrl} --app-id <id>' or edit the config.`, n);
181
194
  return {
182
195
  name: i,
183
196
  apiUrl: o.apiUrl,
184
- wsUrl: x(o.apiUrl),
197
+ wsUrl: S(o.apiUrl),
185
198
  appId: o.appId,
186
199
  appName: o.appName,
200
+ webUrl: o.webUrl,
187
201
  description: o.description,
188
202
  source: a,
189
203
  configPath: n,
190
- localStatePath: w(n),
191
- projectRoot: C(n)
204
+ localStatePath: T(n),
205
+ projectRoot: w(n)
192
206
  };
193
207
  }
194
208
  //#endregion
195
209
  //#region src/dev-tools/vite-plugin/primitive-env.ts
196
- var k = [
210
+ var A = [
197
211
  "VITE_APP_ID",
198
212
  "VITE_API_URL",
199
213
  "VITE_WS_URL",
200
214
  "VITE_APP_NAME"
201
- ], A = "VITE_EXPECTED_PRIMITIVE_ENV", j = /* @__PURE__ */ new Map();
202
- function M(e, t) {
215
+ ], j = "VITE_EXPECTED_PRIMITIVE_ENV", M = /* @__PURE__ */ new Map();
216
+ function N(e, t) {
203
217
  let n = process.env[e];
204
- n !== void 0 && n !== "" && j.get(e) !== n || (process.env[e] = t, j.set(e, t));
218
+ n !== void 0 && n !== "" && M.get(e) !== n || (process.env[e] = t, M.set(e, t));
205
219
  }
206
- function N(e = {}) {
207
- return O({
220
+ function P(e = {}) {
221
+ return k({
208
222
  cwd: e.root,
209
223
  env: e.env,
210
224
  explicitEnvName: e.primitiveEnv ?? null,
211
225
  requireAppId: e.requireAppId
212
226
  });
213
227
  }
214
- function P(e, t, n) {
228
+ function F(e, t, n) {
215
229
  let r = (e) => n.includes(e) ? " (overridden)" : "", i = [
216
230
  `[primitive-env] Primitive environment: ${e.name} (selected: ${e.source})`,
217
231
  `[primitive-env] apiUrl: ${t.apiUrl}${r("VITE_API_URL")}`,
@@ -222,12 +236,12 @@ function P(e, t, n) {
222
236
  ];
223
237
  return n.length > 0 && i.push(`[primitive-env] overridden by the environment: ${n.join(", ")}`), i.join("\n");
224
238
  }
225
- function F(e, t) {
239
+ function I(e, t) {
226
240
  let n = e?.envDir;
227
241
  return n === !1 ? null : typeof n == "string" && n !== "" ? c(t, n) : t;
228
242
  }
229
- function I(e, t) {
230
- let n = process.env[A];
243
+ function L(e, t) {
244
+ let n = process.env[j];
231
245
  if (n !== void 0 && n !== "") return {
232
246
  value: n,
233
247
  where: "the process environment"
@@ -239,7 +253,7 @@ function I(e, t) {
239
253
  ".env.local",
240
254
  ".env"
241
255
  ]) {
242
- let e = V(s(t, n))[A];
256
+ let e = H(s(t, n))[j];
243
257
  if (e !== void 0) return {
244
258
  value: e,
245
259
  where: n
@@ -247,37 +261,37 @@ function I(e, t) {
247
261
  }
248
262
  return null;
249
263
  }
250
- function L(e, t, n) {
264
+ function R(e, t, n) {
251
265
  return [
252
266
  `[primitive-env] Wrong pair: vite mode "${e}" is declared to run against Primitive environment "${t.value}", but "${n.name}" resolved.`,
253
- `[primitive-env] expected: ${t.value} (${A} in ${t.where})`,
267
+ `[primitive-env] expected: ${t.value} (${j} in ${t.where})`,
254
268
  `[primitive-env] resolved: ${n.name} (selected: ${n.source})`,
255
269
  `[primitive-env] config: ${n.configPath}`,
256
270
  "",
257
- `Run the pair you meant — PRIMITIVE_ENV=${t.value} <command> --mode ${e}, or 'primitive env use ${t.value}' — or change/remove ${A} in ${t.where}.`,
271
+ `Run the pair you meant — PRIMITIVE_ENV=${t.value} <command> --mode ${e}, or 'primitive env use ${t.value}' — or change/remove ${j} in ${t.where}.`,
258
272
  "",
259
273
  `A mode declares this when its .env.${e} carries values coupled to one backend, so continuing would run this environment's identity with another's configuration.`
260
274
  ].join("\n");
261
275
  }
262
- function R(e, t, n) {
263
- let r = I(e, t);
264
- if (r && r.value !== "" && r.value !== n.name) throw Error(L(e, r, n));
276
+ function z(e, t, n) {
277
+ let r = L(e, t);
278
+ if (r && r.value !== "" && r.value !== n.name) throw Error(R(e, r, n));
265
279
  }
266
- function z(e = {}) {
280
+ function B(e = {}) {
267
281
  let t = null, n = null, r, i = [], a = !1, o = null;
268
282
  return {
269
283
  name: "primitive-env",
270
284
  enforce: "pre",
271
285
  config(s, c) {
272
286
  a = c?.command === "serve", o = null;
273
- let l = s?.root ?? process.cwd(), u = c?.mode ?? "development", d = B(u, l), f = {};
274
- for (let e of k) {
275
- let t = process.env[e], n = t !== void 0 && j.get(e) !== t ? t : d[e];
287
+ let l = s?.root ?? process.cwd(), u = c?.mode ?? "development", d = V(u, l), f = {};
288
+ for (let e of A) {
289
+ let t = process.env[e], n = t !== void 0 && M.get(e) !== t ? t : d[e];
276
290
  n !== void 0 && n !== "" && (f[e] = n);
277
291
  }
278
292
  let p;
279
293
  try {
280
- p = O({
294
+ p = k({
281
295
  cwd: l,
282
296
  explicitEnvName: e.primitiveEnv ?? null,
283
297
  requireAppId: !f.VITE_APP_ID
@@ -287,8 +301,8 @@ function z(e = {}) {
287
301
  throw e;
288
302
  }
289
303
  if (!(f.VITE_APP_ID && f.VITE_API_URL)) {
290
- let e = F(s, l);
291
- R(u, e, p), o = {
304
+ let e = I(s, l);
305
+ z(u, e, p), o = {
292
306
  root: l,
293
307
  mode: u,
294
308
  dir: e
@@ -301,25 +315,25 @@ function z(e = {}) {
301
315
  VITE_WS_URL: p.wsUrl,
302
316
  VITE_APP_NAME: p.appName
303
317
  }, h = { "import.meta.env.VITE_PRIMITIVE_ENV": JSON.stringify(p.name) }, g = [];
304
- for (let e of k) {
318
+ for (let e of A) {
305
319
  if (f[e] !== void 0) {
306
320
  g.push(e);
307
321
  continue;
308
322
  }
309
323
  let t = m[e];
310
- t !== void 0 && (h[`import.meta.env.${e}`] = JSON.stringify(t), M(e, t));
324
+ t !== void 0 && (h[`import.meta.env.${e}`] = JSON.stringify(t), N(e, t));
311
325
  }
312
- return M("VITE_PRIMITIVE_ENV", p.name), i = g, n = {
326
+ return N("VITE_PRIMITIVE_ENV", p.name), i = g, n = {
313
327
  apiUrl: f.VITE_API_URL ?? p.apiUrl,
314
328
  wsUrl: f.VITE_WS_URL ?? p.wsUrl,
315
329
  appId: f.VITE_APP_ID ?? p.appId,
316
330
  appName: r
317
- }, e.logResolved !== !1 && console.log(P(p, n, g)), g.length > 0 && console.warn(`[primitive-env] Overridden by an explicit value in .env or the shell: ${g.join(", ")}. Remove it to use .primitive/config.json (deploys reject these outright).`), { define: h };
331
+ }, e.logResolved !== !1 && console.log(F(p, n, g)), g.length > 0 && console.warn(`[primitive-env] Overridden by an explicit value in .env or the shell: ${g.join(", ")}. Remove it to use .primitive/config.json (deploys reject these outright).`), { define: h };
318
332
  },
319
333
  configResolved(e) {
320
334
  if (!o || !t) return;
321
- let n = F(e, o.root), r = e?.mode ?? o.mode;
322
- n === o.dir && r === o.mode || (R(r, n, t), o = {
335
+ let n = I(e, o.root), r = e?.mode ?? o.mode;
336
+ n === o.dir && r === o.mode || (z(r, n, t), o = {
323
337
  root: o.root,
324
338
  mode: r,
325
339
  dir: n
@@ -342,17 +356,17 @@ function z(e = {}) {
342
356
  }
343
357
  };
344
358
  }
345
- function B(e, t) {
359
+ function V(e, t) {
346
360
  let n = [
347
361
  ".env",
348
362
  ".env.local",
349
363
  `.env.${e}`,
350
364
  `.env.${e}.local`
351
365
  ], r = {};
352
- for (let e of n) Object.assign(r, V(s(t, e)));
366
+ for (let e of n) Object.assign(r, H(s(t, e)));
353
367
  return r;
354
368
  }
355
- function V(n) {
369
+ function H(n) {
356
370
  if (!e(n)) return {};
357
371
  let r = {};
358
372
  for (let e of t(n, "utf-8").split(/\r?\n/)) {
@@ -366,4 +380,4 @@ function V(n) {
366
380
  return r;
367
381
  }
368
382
  //#endregion
369
- export { b as PrimitiveEnvError, m as primitiveDevTools, z as primitiveEnv, N as resolvePrimitiveEnv };
383
+ export { b as PrimitiveEnvError, m as primitiveDevTools, B as primitiveEnv, P as resolvePrimitiveEnv };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primitive-app",
3
- "version": "3.1.0",
3
+ "version": "3.2.0-alpha.0",
4
4
  "license": "UNLICENSED",
5
5
  "author": "Primitive LLC",
6
6
  "type": "module",